Знаете, что объединяет всех iOS-разработчиков, работающих над крупными проектами? Все мы когда-то сталкивались с этим старым знакомым — файлом .xcodeproj, который хранит в себе десятки, а то и сотни конфликтов после каждого merge. Мы тоже жили с этой проблемой много лет, пока не нашли решение.

Знакомьтесь, Tuist — инструмент, который кардинально изменил наш подход к управлению проектами. Он не только помогает избежать конфликтов, но и автоматизирует генерацию ключевых компонентов, значительно облегчая команде жизнь.

Если у вас всё еще сохранился этот «реликт прошлого», то, возможно, наш опыт поможет вам наконец-то избавиться от него. Давайте разберемся, как это сделать.


Оглавление

  1. Планируем

  2. Анализируем текущие настройки проекта и отдельных таргетов

  3. Наводим порядок в настройках

  4. Наводим порядок в схемах

  5. Генерируем новый проект

  6. Всё только начинается

Планируем

Прежде чем бросаться в омут с головой и переходить на Tuist, стоит задуматься: а нужно ли вам это вообще? Если ваша основная проблема — конфликты в файле проекта при слиянии изменений от нескольких разработчиков, возможно, Tuist будет излишним. В таких случаях проще и быстрее воспользоваться XcodeGen.

Мы же решили двигаться в сторону Tuist по нескольким причинам:

  1. Автоматическая генерация файла проекта.
    Мы, как настоящие «староверы», годами поддерживали и редактировали его вручную. Но внутренняя инициатива по распилу монолита породила столько конфликтов, что это стало настоящим испытанием для наших iOS-разработчиков.

  1. Анализ и проверка графа зависимостей.
    Используемый нами Cocoapods не предоставляет такой функциональности "из коробки". Мы пытались написать свое решение на коленке, но оно не закрыло все наши потребности. Утилита graph в составе Tuist значительно упрощает написание собственного инструмента для Impact Analysis.

  1. Кэширование зависимостей и модулей.
    Опять же, Cocoapods не умеет этого по умолчанию. Мы использовали Rugby для локального кэширования, но это лишь частичное решение проблемы.

Если ваши задачи схожи с нашими, то Tuist может стать именно тем инструментом, который вам нужен.

Мы составили Roadmap с конкретными шагами и сроками, которые помогут нам плавно перейти на Tuist и решить описанные выше задачи.

Первым шагом было изучение документации по составлению манифеста проекта Project.swift для Tuist. Это позволило нам заранее понять, что нужно изменить в текущем проекте, чтобы переход был как можно менее болезненным. Этот этап стал нашим «нулевым» шагом в Roadmap.

Анализируем текущие настройки проекта и отдельных таргетов

Tuist предоставляет набор утилит, которые могут значительно облегчить миграцию проекта. В их числе — утилита для выгрузки текущих настроек проекта или отдельных таргетов в файл xcconfig.

Для выгрузки настроек проекта используйте следующую команду:

tuist migration settings-to-xcconfig -p MyProject.xcodeproj -x MyProject.xcconfig

В результате настройки из проекта MyProject.xcodeproj будут сохранены в файл MyProject.xcconfig.

Для выгрузки настроек отдельного таргета используйте аналогичную команду:

tuist migration settings-to-xcconfig -p MyProject.xcodeproj -t MyTarget -x MyTarget.xcconfig

Настройки таргета MyTarget будут сохранены в файл MyTarget.xcconfig.

Чтобы проверить, насколько выгруженные таким образом настройки соответствуют актуальным настройкам Xcode, создайте новый проект и выполните в нем ту же выгрузку. Далее проанализируйте дифф между двумя наборами параметров и их значениями.

С большой вероятностью на этом этапе вы обнаружите что-то, требующее правки в текущем проекте. Например, в нашем случае это были настройки CLANG_WARN_STRICT_PROTOTYPES и CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF, которые «глушили» warning-и в Objective-C, на который до сих пор приходится около 17% всей кодовой базы.

Разобравшись с настройками проекта и таргетов, можно переходить к схемам.

Наводим порядок в настройках

Мы решили вынести все настройки в xcconfig файлы и выстроить такую иерархию.

Настройки уровня всего проекта хранятся в файле Project.xcconfig. Далее идет базовый конфиг Base.xcconfig с общими настройками для всех таргетов. Вот пример файлов из нашего проекта:

Project.xcconfig

CN_VERSION = 1.275

CN_VERSION_RS = 1.275

CN_BUILD_NUMBER = 1420

MARKETING_VERSION = $(CN_VERSION)

CURRENT_PROJECT_VERSION = $(CN_BUILD_NUMBER)

VERSIONING_SYSTEM = apple-generic

Base.xcconfig

DEVELOPMENT_TEAM = $(CN_DEV_TEAM)

EXCLUDED_ARCHS = armv7

IPHONEOS_DEPLOYMENT_TARGET = 14.0

SWIFT_VERSION = 5.0

На первый взгляд, эти два файла очень похожи и их можно было бы объединить. Однако мы предпочли разделить настройки всего проекта и глобальные настройки для всех таргетов. В нашем случае это разделение продиктовано тем, что в процессе сборок наш CI работает именно с Project.xcconfig, подставляя нужные значения параметров.

Далее следует набор файлов, таких как Target1.common.xcconfig, Target2.common.xcconfig и т.д., которые включают в себя базовый файл #include Base.xcconfig и содержат общие настройки для конкретных таргетов. Пример общих настроек для главного таргета нашего проекта:

Cian.common.xcconfig

#include "Base.xcconfig"

BUILD_LIBRARY_FOR_DISTRIBUTION = NO

CODE_SIGN_ENTITLEMENTS = Cian/CIAN.entitlements

GCC_PRECOMPILE_PREFIX_HEADER = YES

GCC_PREFIX_HEADER = Cian/Cian-Prefix.pch

SWIFT_OBJC_BRIDGING_HEADER = Cian/Cian-Bridging-Header.h

ONLY_ACTIVE_ARCH = YES

Затем идут конкретные настройки для отдельных конфигураций таргетов, такие как Target1.debug.xcconfig, Target2.debug.xcconfig и т.д., которые включают соответствующий общий файл #include Target.common.xcconfig и содержат специфичные настройки. Пример настройки debug-конфигурации главного таргета из нашего проекта:

Cian.debug.xcconfig

#include "../../Pods/Target Support Files/Pods-Cian/Pods-Cian.debug.xcconfig"

#include "Cian.common.xcconfig"

#include "Signing.debug.xcconfig"

PROVISIONING_PROFILE_SPECIFIER=

GCC_PREPROCESSOR_DEFINITIONS = $(inherited) $(PODS_GCC_PREPROCESSOR_DEFINITIONS) DEBUG=1

GCC_SYMBOLS_PRIVATE_EXTERN = NO

OTHER_SWIFT_FLAGS = $(inherited) $(PODS_OTHER_SWIFT_FLAGS) -DDEBUG

SWIFT_OPTIMIZATION_LEVEL = -Onone

OTHER_LDFLAGS[arch=arm*] = $(inherited) -Xlinker -interposable

Помимо этих конфигурационных файлов, в проекте могут быть полезны и дополнительные, специфичные наборы настроек. Например, у нас есть файлы Signing.debug.xcconfig и Signing.release.xcconfig, в которых хранятся все параметры, связанные с подписью приложения для release и debug конфигураций.

Наводим порядок в схемах

В Project.swift вам нужно будет описать все схемы основного проекта.

Когда мы открыли список наших схем и проанализировали каждую из них, стало ясно, что часть из них уже неактуальна. Это отличный момент, чтобы удалить лишние схемы. Например, у нас было несколько схем, которые использовались только для запуска небольшого набора тестов, проверяющих конкретные сценарии в отдельных фичах. Эти тесты также входили в тестпланы других схем, поэтому необходимость в отдельных схемах отпала.

Среди оставшихся схем мы обнаружили, что некоторые из них включали запуск подмножества UI-тестов, список которых был сформирован вручную. 

При переходе на Tuist нам пришлось бы описывать каждый из этих тестов вручную в манифесте. Это явно не то, что мы хотели делать.

Решение этой проблемы — создание файла xctestplan, в котором можно собрать все необходимые тесты. Таким образом, мы сможем легко указать testAction в Project.swift для конкретной схемы, например, следующим образом:

.scheme(

    name: "AuthUITests",

    shared: true,

    buildAction: .buildAction(targets: ["Cian", "CianUITests"]),

    testAction: .testPlans(["CIANUITests/Plans/AuthUITests.xctestplan"], configuration: "Debug")

)

Такой подход позволяет упростить управление схемами и тестами в проекте.

Генерируем новый проект

После подготовки текущего проекта на нулевом шаге нашего roadmap, мы переходим к следующему этапу — непосредственной генерации проекта.

Для того чтобы сгенерировать новый проект, описанный в манифесте Project.swift, достаточно всего лишь одной команды:

tuist generate

Однако, чтобы эта простая команда выдала нужный результат, потребуется немало усилий на написание самого манифеста Project.swift.

Мы подошли к этому процессу итеративно. Сначала наша цель была получить сгенерированный из манифеста проект с необходимым набором конфигураций (Debug, Release и т.д.) и глобальными настройками (поддерживаемая версия iOS, версия Swift и т.д.). Затем мы планировали добавлять в манифест по одному таргету с нужными настройками и схемами.

Чтобы определиться, с какого таргета начать, можно воспользоваться еще одной утилитой из состава Tuist, которая показывает отсортированный по количеству зависимостей список таргетов проекта.

Запуск утилиты выглядит так:

tuist migration list-targets -p MyProject.xcodeproj

В результате вы получите примерно такой вывод:

[

  {

    "targetName" : "MyExtension",

    "linkedFrameworksCount" : 1,

    "targetDependenciesNames" : []

  },

  {

    "targetName" : "MainTarget",

    "linkedFrameworksCount" : 2,

    "targetDependenciesNames" : ["MyExtension"]

  },

  {

    "targetName" : "MainTargetUITests",

    "linkedFrameworksCount" : 3,

    "targetDependenciesNames" : ["MainTarget"]

  },

  {

    "targetName" : "MainTargetUnitTests",

    "linkedFrameworksCount" : 4,

    "targetDependenciesNames" : ["MainTarget"]

  }

  ...

]

Берем таргет с наименьшим количеством зависимостей, переносим его, затем следующий и так до полной победы. Однако, подход может сильно зависеть от специфики вашего проекта, поэтому в некоторых случаях более эффективным окажется иной путь.

Как же убедиться, что в процессе переноса из старого проекта в новый мы ничего не потеряли?

Во-первых, при генерации лучше не затирать имеющийся проект, а создавать новый рядом с ним. Например, чтобы у нас были MyProject.xcodeproj и NewMyProject.xcodeproj. Это позволит легко сравнивать текущую и новую версии проектов.

Для упрощения сравнения можно воспользоваться сторонним инструментом xcdiff, который позволяет сравнивать два проекта и выдавать отчет о различиях в настройках, таргетах, шагах сборки и т.д.

xcdiff поддерживает несколько форматов отчетов: вывод в консоль, HTML, JSON и Markdown.

Пример отчета в формате html:

Чтобы получить подобный отчет, выполните следующую команду:

xcdiff -p1 ../MyProject.xcodeproj -p2 MyNewProject.xcodeproj/ -t Cian -f htmlSideBySide -d > xcdiff.html

Параметры -p1 и -p2 указывают пути до сравниваемых проектов. Параметр -t задает таргет, для которого будет построен отчет. Параметр -f отвечает за формат отчета, а флаг -d указывает, что нас интересуют только отличия между двумя проектами. В результате будет создан файл xcdiff.html с отчетом, который поможет вам найти различия между старой и новой версией проекта.

Далее вам предстоит многократно повторять цикл: внесение изменений в Project.swift, генерация проекта и проверка диффа. Этот процесс требует терпения, но в конечном итоге ваши усилия будут вознаграждены — вы получите новый, полностью работающий проект. По крайней мере, в нашем случае мы достигли желаемого результата.

А еще тут стоит рассказать про маленькую хитрость, которая поможет подружить Tuist и Cocoapods. Внимательный читатель, возможно, уже заметил следующую строчку в файле Cian.debug.xcconfig, о котором мы говорили ранее при настройке проекта:

#include "../../Pods/Target Support Files/Pods-Cian/Pods-Cian.debug.xcconfig"

Если ничего специально не делать и указать такой конфигурационный файл в манифесте Project.swift для таргета, то при чистой установке и вызове команды tuist generate вас ожидает вот такая ошибка:

Configuration file not found at path /Users/john.appleseed/cian/Pods/Target Support Files/Pods-Cian/Pods-Cian.debug.xcconfig

Fatal linting issues found

Consider creating an issue using the following link: https://github.com/tuist/tuist/issues/new/choose


Проблема в том, что Tuist при генерации проекта валидирует указанные в манифесте файлы xcconfig, включая те, которые подключаются внутри них.

Соответственно, Tuist проверяет наличие xcconfig файлов от Cocoapods, которые на этом этапе еще не созданы, так как для их генерации нужно выполнить команду pod install. Однако, чтобы выполнить pod install, нам нужен файл проекта, который мы как раз и пытаемся сгенерировать. Получается замкнутый круг.

Решение — обмануть Tuist, сгенерировав заглушки по нужным путям до выполнения команды tuist generate. Эти заглушки затем будут перезаписаны настоящими файлами от Cocoapods.

Чтобы провернуть такой трюк, можно использовать скрипт, который проверяет наличие файлов по соответствующим путям для каждого таргета и конфигурации:

/Users/john.appleseed/cian/Pods/Target Support Files/Pods-<TARGET>/Pods-<TARGET>.<CONFIGURATION>.xcconfig

Если файл отсутствует, скрипт создает пустой файл, который затем будет перезаписан результатом выполнения команды pod install.

Возможно, в вашем проекте уже есть скрипт, который разработчики запускают при настройке проекта или переключении на новую ветку. Этот скрипт можно дополнить проверкой на наличие нужных xcconfig файлов и их генерацией при необходимости. После этого можно смело запускать генерацию проекта через Tuist и установку зависимостей через Cocoapods.

Идем дальше. Как только получите устраивающий вас результат на локальной машине, обязательно проверьте генерацию и сборку на своем CI/CD. Не поленитесь создать отдельную ветку для CI и настроить хотя бы одну сборочную машину для запуска и сборки вашего проекта с автогенерированным файлом. Ведь в конечном итоге наша цель — не просто сгенерировать проект, а убедиться, что приложение с этим файлом благополучно доходит до пользователей.

Всё только начинается

Итак, мы подошли к той точке, где выполнили первые два шага нашего roadmap: подготовили текущий проект к переезду и научились генерировать новый проект с помощью Tuist.

Теперь можно переходить к следующему этапу — переводу проектных модулей на Tuist. Это будет гораздо более масштабное и сложное приключение, чем те шаги, которые мы уже прошли. В нашем случае мы только в самом его начале. О том, как в Циан будем переводить модули на Tuist и что из этого получится, мы обязательно расскажем в следующих сериях. А на этом пока всё.

Надеюсь, вы почерпнули для себя что-то полезное. Ну, или хотя бы улыбнулись, узнав, что в 2024 году кто-то еще решает конфликты в xcodeproj вручную, и порадовались, что количество таких проектов стало на один меньше.

Если уже используете Tuist в своих проектах — делитесь своим опытом в комментариях!

Комментарии (0)