Борьба с зависимостями: как мы переводили проект на SwiftPM (и сделали еще хуже, чем было) +12


Привет, Хабр! Меня зовут Лена, я iOS-разработчик в inDriver. Расскажу о том, как мы выбирали менеджер зависимостей для проекта. Название «Борьба с зависимостями» может показаться пугающим, но на самом деле все не так страшно.

Сначала поделюсь тем, как у нас обстояли дела с интеграцией сторонних решений. Затем выделю проблемы и цели, которые перед нами стояли. После перейду к сравнению менеджеров зависимостей. И, наконец, расскажу, почему остановились на Swift Package Manager (SwiftPM).

Погнали!

Содержание
Дисклеймер

Эта статья — расшифровка моего доклада на CocoaHeads, его можно посмотреть здесь.

Проблемы и цели

Пару месяцев назад у нас в проекте было 46 зависимостей. Больше половины из них были добавлены через CocoaPods, около трети через Carthage и 3 — зашиты в коде. Так исторически сложилось.

Вот как все было
Вот как все было

С таким подходом что-то было не так. Выделю 3 проблемы:

  1. Не было ясно, какой из менеджеров зависимостей использовать при добавлении новой библиотеки или фреймворка.

  2. При обновлении Xcode регулярно возникали трудности с использованием CocoaPods и Carthage. Много проблем было и при переходе на Xcode 12. Всему виной — подготовка к работе с M1. Например, проект переставал собираться на симуляторе из-за изменений в build settings.

  3. У нас был запутанный граф зависимостей из-за структуры проекта, и иногда приходилось линковать лишние транзитивные зависимости к некоторым модулям. Хотелось бы сделать граф зависимостей наглядным.

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

Зависимости

Мы решили, что нужно с этим что-то делать. Для начала проанализировали ситуацию, как зависимости интегрировались в проект.

Сторонние решения можно добавлять в проект в виде исходного кода, статических и динамических библиотек, фреймворков и XCFrameworks. Кратко расскажу об этих подходах.

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

  2. C помощью библиотек. По способу линковки они бывают статическими (.a) и динамическими (.dylib). Статические — архивы объектных файлов формата Mach-O. Это бинарный формат, внутри — инструкции для конкретной архитектуры. Когда статическая библиотека добавляется в проект, она «склеивается» с нашим бинарником в один. Чем больше статических библиотек в проекте, тем больше итоговый бинарник.

    Динамические библиотеки отличаются от статических тем, что не зашиваются в итоговый бинарник, а загружаются в память при необходимости — на старте приложения или при вызове ее API. За счет этого не увеличивают размер бинарника нашего приложения. Но тем не менее, влияют на вес архива, который отправится в стор; исключения составляют системные библиотеки, которые поставляются вместе с iOS и работа с которыми производится через файлы формата .tbd. Чем больше динамических библиотек в проекте, тем дольше запуск нашего приложения.

    Стоит отметить, что с помощью lipo можно собрать из библиотеки fat binary, который бы содержал инструкции сразу для нескольких выбранных нами архитектур.

  3. С помощью фреймворков. Фреймворк можно считать надстройкой над библиотеками. Это бандл, внутри которого лежит библиотека и, возможно, какие-то ресурсы.

    Существует его разновидность — universal framework, внутри которого находится fat binary. Этот формат был не очень удобен тем, что требовалось убирать лишние архитектуры при загрузке в стор.

    Но с Xcode 11 на замену universal framework пришел новый формат — XCFramework. Он позволяет использовать фреймворк как единую зависимость для разных архитектур и выбор нужной происходит автоматически. В теории это упростит переезд на М1 — не надо пересобирать зависимости под каждую платформу или архитектуру, которую мы поддерживаем. Это оптимальный вариант для собранных заранее зависимостей.

Сравнение менеджеров зависимостей

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

Принцип работы менеджеров зависимостей прост. Есть Manifest-файл, куда заносится информация о требуемых зависимостях. Для CocoaPods это Podfile, для Carthage Cartfile, а для SwiftPM — Package.swift. Менеджер зависимостей использует данные из Manifest-файла, чтобы загрузить исходники или бинарники, а затем формирует слепок зависимостей в lock-файле — это Podfile.lock, Cartfile.resolved, Package.resolved. Дальше требуется интегрировать загруженные зависимости в проект.

Наглядная схема
Наглядная схема

Чем же отличаются менеджеры зависимостей? Можно сравнить их по следующим критериям:

  • насколько активно поддерживается и успевает за обновлениями Xcode.

  • сколько доступных сторонних решений и насколько удобен их поиск.

  • как происходит интеграция зависимостей в проект.

  • насколько гибко можно выбирать способ линковки зависимостей.

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

Принцип работы. Мы заводим Podfile, делаем pod install, он разрешает граф зависимостей — выбирает нужные версии, смотрит, доступны ли решения, загружает их. А дальше берет загруженные исходники, формирует файл .xcodeproj, в котором каждой из зависимостей соответствует таргет. И затем он заворачивается в .xcworkspace, который мы используем для работы с проектом.

Принцип работы CocoaPods
Принцип работы CocoaPods

Проблемы. Из-за того, что CocoaPods скачивает исходники и собирает их, замедляется сборка. Нет контроля над интеграцией и меняется структура проекта — если мы изначально использовали .xcworkspace, потребуются правки, чтобы это все заработало.

2. Carthage. Это первый менеджер зависимостей для Swift. Отличается тем, что интеграция полностью лежит на нас — Carthage только скачивает и собирает зависимости, но не встраивает их в проектный файл. Соответственно, это простой и гибкий инструмент. Является децентрализованным: репозитории сторонних решений мы указываем напрямую, общего хранилища с информацией о них нет.

Сборка зависимостей происходит до проекта, причем Carthage по умолчанию собирает динамические фреймворки (причем universal). Потом они линкуются к нашему проекту — за счет чего мы экономим на скорости сборки. К сожалению, Carthage в последнее время не так активно поддерживается, и у нас были проблемы с ним при обновлении на Xcode 12.

Принцип работы. Сначала вносим ссылки на репозитории нужных зависимостей в Cartfile и вызываем команду, которая скачает зависимости — как исходники, так и бинарники, после чего загруженные исходники собираются. Теперь нужно вручную добавить их в проект. К счастью, мы используем XcodeGen, так что этот шаг автоматизирован. Поэтому Carthage нашим разработчикам нравился гораздо больше, чем CocoaPods.

Принцип работы Carthage
Принцип работы Carthage

Проблемы. В Xcode регулярно происходят изменения, связанные с настройкой проекта, из-за чего у нас не один раз ломалась сборка зависимостей для Carthage. Иногда переставали собираться и наши тесты: например, при линковке зависимостей к тестам в них оставались лишние архитектуры. Решения подобных проблем приходится ждать долго, так как Carthage в последнее время обновляется не так часто.

3. SwiftPM. Мы решили посмотреть в сторону SwiftPM. Он появился еще со Swift 3, но для iOS стал официально доступен только с Xcode 11. SwiftPM подразумевает нативную интеграцию и поставляется вместе со Swift. С ним можно работать в терминале, а можно добавлять и обновлять зависимости через интерфейс Xcode, и видеть их список в структуре проекта.

Также, как и Carthage, это децентрализованный менеджер зависимостей. Но к сожалению, пока еще SwiftPM поддерживает не все сторонние решения, так как для его поддержки в репозитории должен лежать Package.swift. Но в большинстве случаев это легко исправить.

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

Рассмотрим устройство Package.swift (сниппет взят из документации). Сначала объявляется название самого пакета. Пакетом называется исходный код с Manifest-файлом. Дальше указываются продукты — артефакты, которые будут созданы при сборке пакета (библиотеки или бинарники). За ними следует список зависимостей пакета, а также таргеты, входящие в этот пакет, и их внутренние и внешние зависимости.

Вот как это выглядит в Xcode
Вот как это выглядит в Xcode

Принцип работы. Добавить пакет к себе в проект можно из меню в Xcode: выбрать таргет, в который она будет добавлена, указать название пакета и версию. Если все хорошо, эта зависимость попадет в проектный файл. Затем автоматически происходит разрешение зависимости, и она загружается. Сборка пакета будет происходить вместе со сборкой проекта.

Принцип работы SwiftPM
Принцип работы SwiftPM

Проблемы. Дело в том, что наш проект все еще содержит код на Obj-C. Взаимодействие кода на Obj-C и Swift вызывает ряд проблем при работе со SwiftPM: какие-то можно решить самостоятельно, какие-то требуют доработок со стороны SwiftPM.

Итак, если подытожить, сравнение менеджеров зависимостей приведено в таблице:

Для нас среди этих трех менеджеров зависимостей лидирует SwiftPM. Вернусь к таблице и раскрою ее чуть подробнее.

1. Централизованность. У CocoaPods есть единый репозиторий спецификаций, который в случае проблемы не позволит скачать наши зависимости — это минус. Carthage и SwiftPM просто берут исходный ход с сервисов хостинга репозиториев — например, GitHub, и это упрощает задачу загрузки сторонних решений. Работает GitHub — работаем и мы.

2. Интеграция зависимости. У CocoaPods есть автоматическая интеграция в проект. Пользоваться инструментом просто, если нам мало от него нужно. Но так как это черный ящик и CocoaPods меняет структуру проекта, могут быть проблемы, которые на первый взгляд не видны. C Carthage ситуация противоположная: интеграции нет, она происходит вручную — а значит гибко. В случае со SwiftPM у нас есть возможность ближе познакомиться с нашим проектным файлом и как-то на него влиять, а также иметь контроль над подключением зависимости.

3. Способы линковки. CocoaPods изначально подразумевает использование динамических фреймворков. До Xcode 9 нельзя было собирать статические библиотеки, содержащие Swift, но теперь это возможно и при использовании CocoaPods можно указать желаемый способ. С Carthage ситуация аналогичная — по умолчанию собираются динамические фреймворки, но при желании можно собирать и статические. А со SwiftPM все зависит от того, как разработчик стороннего решения объявил Package.swift для своего пакета и какой там указан способ линковки. Если при объявлении продукта-библиотеки ничего не указано, будет собираться статическая, а динамическая — только если указан тип .dynamic. Что же касается XCFramework, Carthage позволяет нам выбрать этот вариант самостоятельно, а в CocoaPods и SwiftPM этот способ интеграции стороннего решения будет выбран, только если его автор явно его задал.

Переезд на SwiftPM

Взвесив «за» и «против», мы решили, что будем постепенно переезжать на SwiftPM. План был такой: сначала составляем общий список зависимостей, добавленных через Carthage и CocoaPods; затем проверяем, доступны ли они через SwiftPM (есть утилиты, которые помогают сэкономить время на этом шаге — например, spmready). Дальше идем по полученному списку и переводим отдельные сторонние решения.

Причем переезд на SwiftPM может быть поэтапным: не требуется делать из своих модулей пакеты, чтобы добавлять зависимости в виде пакетов SwiftPM. Сделать пакеты из модулей своего проекта можно будет тогда, когда переведены все зависимости, но и это не обязательно.

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

В первую очередь, это были проблемы в самих сторонних решениях. Например, часть из них не была доступна через SwiftPM. В таком случае, требовалось либо ждать, пока там появится поддержка, либо добавлять ее самостоятельно. Если использовался свой форк стороннего решения, его перевод ускорялся – нужно было только добавить Package.swift и поднять версию. Помимо этого, была проблема при переводе сторонней зависимости на Obj-C, у которой в Package.swift были неверные пути к заголовочным файлам — требовались исправления в ее репо.

Также были трудности из-за организации нашего проекта. Например, не получилось добавить пакет на Obj-С в модуль на Swift, который потом использовался в модуле на Obj-С. Тут мы получали ошибку поиска заголовочных файлов. А добавить пакет сразу в оба эти модуля нельзя, иначе была бы ошибка дублирования символов, так как он подключался статически. Таких случаев было несколько и мы решили оставить их на потом. Хотя в целом, если требуется подключить статическую библиотеку в несколько независимых модулей, можно добавить ее в свой модуль, который будет подключаться динамически в нужных местах.

Проблемы все еще есть и в самом SwiftPM. Раньше к ним относилось отсутствие поддержки ресурсов, но она появилась в Xcode 12. Правда мы словили проблему с загрузкой ресурсов в одном из сторонних решений, поэтому его перевод на SwiftPM также отложили на потом.

Как теперь
Как теперь

Может показаться, что стало еще хуже, чем было. Но на самом деле больше половины зависимостей удалось перевести на SwiftPM. А скоро мы совсем избавимся от Carthage, что давно хотели сделать. CocoaPods какое-то время еще будем использовать: подождем, пока часть сторонних решений оттуда начнет поддерживать SwiftPM, а часть станет ненужной из-за удаления легаси на Obj-C, в котором она используется.

Вывод

SwiftPM — новый стандарт в iOS-разработке. В отличие от CocoaPods, он не меняет структуру проекта, нативно интегрирован в Xcode и поставляется вместе с ним. Правда, стоит учитывать, что в SwiftPM еще есть недоработки. В частности, возможны трудности в совместном использовании кода на Swift и Obj-C. Но, в любом случае, переезд может быть поэтапным.

Спасибо, что дочитали статью, буду рада ответить на вопросы в комментариях.




К сожалению, не доступен сервер mySQL