
Flutter активно движется в сторону Swift Package Manager (SPM) и, как и весь Swift-мир, постепенно отказывается от CocoaPods. Сам Flutter описывает миграцию в два шага: включить флаг flutter config --enable-swift-package-manager и запустить сборку. Если после этого приложение собирается и в консоли появляется предложение deintegrate CocoaPods — вы счастливчик. Если нет, добро пожаловать под кат.
Привет! Меня зовут Даниил Гиренко, я Flutter-разработчик в Яндекс Про. Проект по переводу нашего супераппа с CocoaPods на Swift Package Manager оказался одним из самых сложных и долгих в моей карьере программиста. Весь мой пережитый ужас опыт я решил выразить в этой статье, чтобы решившиеся на миграцию разработчики (или агенты) понимали, какие подводные камни их ждут. Буду рад, если ваш проект займёт недели, а не месяцы, как у меня.
В этой статье — полноценный гайд по миграции любого Flutter-приложения. Разберём граф зависимостей, научимся мигрировать плагины, зарезолвим зависимости и доберёмся до успешного билда. Внутри есть готовые скрипты для анализа и автоматического перевода плагинов, множественные нюансы миграции и рассказы о моих костылях.
Везде я говорю про iOS, но все шаги идентичны и для macOS.
Анализ CocoaPods-зависимостей
Flutter поддерживает сборку iOS-приложения сразу с обоими менеджерами зависимостей, разделяя все плагины между ними. Это значит, что не требуется переводить всё приложение за раз, это можно делать итерациями. SPM и CocoaPods не знают о существовании друг друга и потому могут затянуть в приложение две версии одной и той же транзитивной зависимости. В таких случаях линкер Xcode ругается на duplicated symbols и падает с ошибкой.
Чтобы такого не допустить, необходимо переводить все подграфы зависимостей за раз. Например, если зависимость A зависит от C и B зависит от C, то для успешной сборки обязательно нужно переводить A и B; если перевести только А — будет ошибка. Это правило не распространяется лишь на сам Flutter, от которого зависит любой iOS-плагин.
Перед сборкой iOS Flutter запускает команду pod install, чтобы скачать все iOS-зависимости и зафиксировать их версии в ios/Podfile.lock. С анализа его графа необходимо начать миграцию, чтобы понять, какими частями она будет проходить. Из-за особенностей записи зависимостей в Podfile.lock сложно на глаз понять, какие подграфы можно выделить. Для этой задачи мной был написан простой Dart-скрипт, который выделит все подграфы. В скрипте с анализом подграфов есть подсветка тех зависимостей, которые уже мигрированы на SPM. Пример вывода подграфа скриптом:
┌─── Subgraph #3 [22 nodes] ─── │ Entry points [APP]: file_picker, flutter_image_compress │ │ [APP] file_picker [SPM ✗] │ └── DKImagePickerController/PhotoGallery │ ├── DKImagePickerController/Core │ │ ├── DKImagePickerController/ImageDataManager │ │ └── DKImagePickerController/Resource │ └── DKPhotoGallery │ ├── DKPhotoGallery/Core │ │ ├── DKPhotoGallery/Model │ │ │ ├── SDWebImage │ │ │ │ └── SDWebImage/Core │ │ │ └── SwiftyGif │ │ ├── DKPhotoGallery/Preview │ │ │ ├── DKPhotoGallery/Model [↑] │ │ │ ├── DKPhotoGallery/Resource │ │ │ │ ├── SDWebImage [↑] │ │ │ │ └── SwiftyGif [↑] │ │ │ ├── SDWebImage [↑] │ │ │ └── SwiftyGif [↑] │ │ ├── SDWebImage [↑] │ │ └── SwiftyGif [↑] │ ├── DKPhotoGallery/Model [↑] │ ├── DKPhotoGallery/Preview [↑] │ ├── DKPhotoGallery/Resource [↑] │ ├── SDWebImage [↑] │ └── SwiftyGif [↑] │ │ [APP] flutter_image_compress [SPM ✓] │ ├── Mantle │ │ └── Mantle/extobjc │ ├── SDWebImage [↑] │ └── SDWebImageWebPCoder │ ├── SDWebImage/Core [↑] │ └── libwebp │ ├── libwebp/demux │ │ └── libwebp/webp │ │ └── libwebp/sharpyuv │ ├── libwebp/mux │ │ └── libwebp/demux [↑] │ ├── libwebp/sharpyuv [↑] │ └── libwebp/webp [↑] └────────────────────────────────────────────────────────────
В нашем случае первый большой подграф и граф Google-пакетов пришлось переводить вместе из-за особенностей интеграции SPM и Flutter. Она реализована через кодогенерируемый Package.swift, который после каждого flutter pub get обновляет свой список зависимостей всеми плагинами, которые поддерживают SPM на данный момент. В этом и заключается проблема: после включения SPM в проекте все плагины, которые могут мигрировать, автоматически будут мигрировать на SPM. Это может задеть сразу несколько подграфов, из-за чего всех их придётся мигрировать разом. Как вариант костыля, это можно обойти с помощью удаления файла Package.swift из кеша зависимостей.
Больше никакого x86_64
Последняя возможная проблема при переводе зависимостей, которую тоже подсветит скрипт, — это отсутствие у XCFramework-зависимостей (бинарных) поддержки слайса iOS-arm64-simulator, так как Flutter-приложения с SPM более не могут собираться на x86_64-симуляторе.
Что же делать в таком случае, если даже на самой новой версии библиотеки нет поддержки arm64 iOS-симулятора, но хочется сохранить возможность его собирать? Например, серия MLKit-библиотек от Google не имеет поддержки arm64 iOS-симулятора, и, судя по году создания ишью с этой проблемой, уже не будет иметь. Равно как и поддерживать SPM для этих библиотек. В таком случае остаётся либо самому добавлять недостающий срез, либо искать репозитории от сообщества.
Добавление поддержки arm64 к уже существующему бинарнику достаточно легко делается. Но только если это уже XCFramework, а не fat-бинарник, с которым поможет только xcframework-maker. Добавляя в свой проект пересобранные XCFramework-файлы, не забудьте, что они могут потерять подпись и не пройти проверки в TestFlight. К счастью, Apple проверяет лишь валидность подписи, а не от кого она, так что можно переподписать фреймворк. Это можно сделать с помощью команды codesign --force --sign "$CODESIGN_IDENTITY" --timestamp --generate-entitlement-der "$xcframework_path", где в качестве CODESIGN_IDENTITY можно указать свою или Apple Development. Проверить валидность подписи можно через codesign --verify --verbose "$xcframework_path".
Маленькая справка для тех, кому интересно, почему именно на x86_64-симуляторе запуститься не получится: Apple достаточно давно перешла на собственные M-чипы с архитектурой arm64, из-за чего архитектура x86_64 начала считаться устаревшей и сейчас не поддерживается. Симуляторы до сих пор можно собирать под x86_64, если сделать определённые настройки в проекте. Это решение нужно было для приложений на время, пока все XCFramework-зависимости не получат поддержку arm64 iOS-симулятора.
В SPM архитектура arm64 выбрана по умолчанию. А также, по неизвестным мне причинам, SPM игнорирует запись EXCLUDED_ARCHS в собираемом проекте. Из-за этого для перевода на x86_64-сборку симулятора нужно использовать костыль, хорошо описанный тут. Если кратко, то для сборки сразу под все архитектуры необходимо поменять название Build Configuration в Xcode с Debug на любое другое, например Dev.
Однако этот способ не работает для Flutter-приложений, потому что Flutter ожидает конфигурацию именно с названием Debug. Как следствие, для Flutter-приложений с SPM невозможно собрать iOS-приложение под x86_64, даже если сама библиотека добавляется через CocoaPods.
Миграция: как перевести плагин
На момент написания статьи практически все популярные и основные Flutter-плагины переведены на SPM, однако большинство плагинов от сообщества всё ещё нуждаются в переводе. Миграция всего супераппа на SPM складывается из перевода каждого конкретного плагина.
Для начала стоит проверить, есть ли поддержка SPM в последней версии плагина, чтобы получить её самым лёгким путём. Если, конечно, для последней версии не придётся обновлять Flutter, как в случае с sqlite3. Если же поддержки нет, а ишью на GitHub выглядит заброшенным, либо вы сами автор непереведённого плагина — этот раздел для вас.
Миграция плагина состоит из двух шагов:
Рефакторинг файловой структуры, который подробно описан в документации Flutter.
Подключение зависимостей-аналогов.
В директории ios создаётся директория с именем плагина. Там создаётся Package.swift - аналог .podspec-файла, в котором будет зарегистрирован код данного плагина и подключены зависимости. Все файлы с нативным кодом перемещаются в директории-таргеты. Каждый таргет может содержать лишь один язык: Swift или Objective-C. Если плагин должен содержать код на двух языках, то необходимо их разделить на два таргета. Как это будет выглядеть в дереве:
# До (CocoaPods) ios/ Classes/ PluginName.swift PluginNameObjc.m plugin_name.podspec # После (SPM) ios/ plugin_name/ Sources/ plugin_name/ PluginName.swift plugin_name_objc/ PluginNameObjc.m PluginNameObjc.h Package.swift plugin_name.podspec
Для Objective-C-файлов меняются пути импортов, а для Swift добавляются недостающие импорты библиотек. Если CocoaPods разрешал опускать некоторые базовые импорты, например Flutter или UIKit, то SPM уже требует их указывать. Некоторый код в Swift-файлах может меняться, в зависимости от менеджера зависимостей (как, например, работа с ресурсами), в таком случае поможет предпроцессорная директива #if SWIFT_PACKAGE. Чтобы при миграции плагина сохранить возможность его использовать и на подах, что крайне рекомендуется, достаточно в .podspec-файле изменить пути, по которым CocoaPods берёт сурцы и хедеры.
Работа достаточно механическая, так что для всего этого был создан скрипт миграции, который делает всю эту работу для плагинов Swift, Objective-C и Mixed и пишет Warnings, если что-то не может выполнить самостоятельно, например добавление зависимостей. Для подавляющего большинства плагинов скрипт сработает без необходимости правок.
После успешного рефакторинга остаётся лишь найти SPM-аналоги зависимостей, указанных в podspec. Их гиты достаточно легко гуглятся, именно они и указываются в качестве источника зависимости, например: .package(url: "https://github.com/Mantle/Mantle", from: "2.2.0"). Если же была зависимость на уже собранный фреймворк, то можно использовать его же и в SPM. Подробнее о том, как заполнять Package.swift, можете узнать в различных гайдах или у нейронок. Главное, не забыть, что для Flutter-плагинов название пакета должно писаться через _, а название библиотеки в продуктах должно быть аналогичным, но через -. Если же аналогичной SPM-зависимости от авторов просто нет, то стоит поискать репозитории от сообщества или форкнуть и добавить Package.swift самому.
Пример Package.swift для Objective-C-плагина:
// swift-tools-version: 5.9 import PackageDescription let package = Package( name: "flutter_image_compress", platforms: [ .iOS("12.0"), ], products: [ .library(name: "flutter-image-compress", targets: ["flutter_image_compress"]), ], dependencies: [ .package(url: "https://github.com/Mantle/Mantle", from: "2.2.0"), .package(url: "https://github.com/SDWebImage/SDWebImage", from: "5.10.0"), .package(url: "https://github.com/SDWebImage/SDWebImageWebPCoder", from: "0.14.6"), ], targets: [ .target( name: "flutter_image_compress", dependencies: [ .product(name: "Mantle", package: "Mantle"), .product(name: "SDWebImage", package: "SDWebImage"), .product(name: "SDWebImageWebPCoder", package: "SDWebImageWebPCoder"), ], cSettings: [ .headerSearchPath("include/flutter_image_compress"), ], ), ] )
Некоторые Flutter-плагины зависят от других Flutter-плагинов. Чтобы в их коде работал import, необходимо добавить в Package.swift зависимость по пути "../plugin_name/ios/plugin_name". На новых версиях это будет верный путь благодаря директории .packages, а на старых — за счёт автоматической подмены путей SPM.
После перевода плагина необходимо проверить его работу на примере. Так станет известно обо всех допущенных ошибках: пропущенных import, неправильных путях, возможной разнице между зависимостями-аналогами с CocoaPods и SPM и прочем. Отмечу, что чтобы быстро проверить собираемость примера без Flutter, можно в ios-поддиректории воспользоваться командой:xcodebuild build -workspace Runner.xcworkspace -scheme Runner -destination 'generic/platform=iOS Simulator' 2>&1 | grep -E "Swift Compiler Error|error: " | grep -v "^$".
Таким образом, плагин за плагином, и будет происходить миграция на SPM. А когда все конфликтующие зависимости с CocoaPods будут переведены, можно начинать процесс первой сборки приложения на новом менеджере зависимостей.
Включаем SPM в приложении
Интеграция SPM во Flutter состоит из двух шагов: запустить flutter config --enable-swift-package-manager и flutter build/run. Во время подготовки Flutter добавит в project.pbxproj в качестве зависимости свой кодогенерируемый пакет-обвёртку по пути ios/Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage, а также запуск его генерации в Runner.xcscheme. В этот кодогенерируемый Package.swift-файл Flutter-тулинг записывает все плагины, поддерживающие SPM, и указывает до них локальный путь. Поэтому этот файл не нужно коммитить в репозиторий, а также вносить в него свои изменения. На новых версиях Flutter также создаст .packages-директорию, содержащую симлинки на директорию пакета каждого из плагинов.
В поле версии SPM-пакета Flutter указывает свою дефолтную версию. Во время сборки приложения, перед запуском xcodebuild, она меняется на указанную в поле IPHONEOS_DEPLOYMENT_TARGET файла project.pbxproj. От себя добавлю, что иногда во время попыток получить первую успешную сборку приложения на SPM дефолтная версия не успевала замениться на необходимую, из-за чего резолвинг падал с ошибкой Xcode Integrity. Приходилось после flutter run быстро менять версию на нужную. Также помогало прогнать flutter build ios --config-only. После успешного билда проблема исчезла.
На момент написания статьи SPM не поддержан в Flutter Add to App, только обычные приложения.
Резолвинг: где SPM зависает и берёт не те версии
И вот мы дошли до этапа, когда все нужные нам зависимости сгенерировались в новом Package.swift. При запуске приложения Flutter сам вызывает резолвинг зависимостей, если он не был сделан. Но его можно вызвать вручную с помощью команды: xcodebuild -resolvePackageDependencies -project Runner.xcodeproj -scheme Runner для проекта и отдельно xcodebuild -resolvePackageDependencies -workspace Runner.xcworkspace -scheme Runner для воркспейса.
Чтобы убедиться, что всё работает отлично, достаточно проверить работу хотя бы одной команды, так как они обе генерируют идентичный граф, просто отдельно для проекта и для воркспейса. Это происходит потому, что сгенерированный FlutterGeneratedPluginSwiftPackage подключается в таргет Runner-проекта, а проект — часть Workspace. С ними по отдельности может работать Xcode, поэтому каждому нужен свой Package.resolved.
Во время резолвинга могут появиться проблемы невозможности решить граф зависимостей. SPM, подобно flutter pub, хорошо расписывает в логе, какие версии каких плагинов конфликтуют. В моём случае резолвинга SPM даже навечно зависал, попадая в какой-то цикл. Это разрешилось с помощью более точного указания версий в собственных плагинах. Кроме того, чем больше плагинов и сложнее граф, тем дольше проходит резолвинг, что в нашем случае неприятно отразилось на времени сборки в период частичной миграции.
После успешного резолвинга зависимостей по путям в ios-папке появятся два файла Package.resolved, дерево ios-поддиректории будет выглядеть теперь так:
ios/ Flutter/ ephemeral/ Packages/ .packages FlutterGeneratedPluginSwiftPackage/ Package.swift Pods/ Runner/ Runner.xcodeproj/ project.xcworkspace/ xcshareddata/ swiftpm/ configuration/ Package.resolved Runner.xcworkspace/ xcshareddata/ swiftpm/ configuration/ Package.resolved Podfile Podfile.lock
Эти файлы — аналоги файлов Podfile.lock с зафиксированными версиями зависимостей проекта и их источниками. При прогоне pod install список плагинов в Podfile.lock уменьшится, так как эти плагины мигрировали. Также из project.pbxproj пропадут строчки регистрации bundle-файлов и CocoaPods-фреймворков.
Если версия какой-либо транзитивной зависимости в Package.resolved вам не подходит, можно в Package.swift приложения добавить зависимость на строго нужную версию, после чего прогнать резолвинг. При следующем flutter pub get изменения в Package.swift сотрутся, но зафиксированная зависимость останется в resolved-файле. Зачастую это необходимо из-за того, что SPM по умолчанию берёт самые высокие из доступных версий зависимостей, но не обращает внимания на соответствие iOS-версий или используемую версию Swift Tools. Это создавало немало проблем.
Необходимо ещё упомянуть, что в директориях swiftpm можно создать поддиректорию configuration. В ней опционально можно разместить два файла:
registries.json— для задания отличного от дефолтного хоста источника зависимостей. Это необходимо, если вы хотите, например, развернуть свой SPM registry в своей сети. Подробнее можно почитать тут.mirrors.json— для указания замены источников зависимостей. Поможет как для форков, так и для своего registry.
И последнее: все скачанные SPM-зависимости находятся по пути ~/Library/Caches/org.swift.swiftpm, если не задана другая папка. Во время билда они копируются и используются в ~/Library/Developer/Xcode/DerivedData. Также появляется папка ~/.swiftpm, которая служит просто сборником ссылок на папки кеша. Так что для того, чтобы полностью очистить кеш, связанный с SPM, можно выполнить: rm -rf ~/Library/Caches/org.swift.swiftpm && rm -rf ~/.swiftpm && rm -rf ~/Library/Developer/Xcode/DerivedData.
Поддержка целостности файлов Package.resolved, параллельный прогон
Этот раздел может пригодиться тем, кто занимается CI/CD своего проекта. Если в вашей зоне ответственности лишь одно приложение, то вряд ли здесь будет что-то интересное. В моём же случае в репозитории нашего проекта с помощью CI-проверок всегда поддерживается актуальность всех lock-файлов всех примеров. После появления SPM появилась необходимость поддерживать актуальность и resolved-файлов. Для этого был написан такой скрипт с логикой, которую можно упрощённо представить блок-схемой:

Для каждого из наших плагинов первым шагом запускаем резолвинг зависимостей командой: xcodebuild -resolvePackageDependencies -workspace/-project Runner.xcworkspace/Runner.xcodeproj -scheme Runner -skipPackageUpdates -disableAutomaticPackageResolution. Здесь появляются два новых флага:
disableAutomaticPackageResolution— при резолвинге Xcode воспользуется другими версиями пакетов только в том случае, если невозможно зарезолвить зависимости из нынешнего Package.resolved. Другими словами, он пытается сохранять resolved-файл и завершает команду ошибкой, если это невозможно. Этот флаг советует сама Apple. ФлагonlyUsePackageVersionsFromResolvedFileдействует аналогично.skipPackageUpdates— пропускает обновление зависимостей, если это возможно. Немного ускоряет резолвинг сразу нескольких пакетов.
Смысл этих двух флагов в том, чтобы вносить изменения в resolved-файл только в крайнем случае. Если у SPM это не получается, он пишет в лог ошибку со словами: an out-of-date resolved file was detected. При обнаружении этой ошибки достаточно перезапустить резолвинг, но уже без флага disableAutomaticPackageResolution, который точно принесёт изменение в resolved-файл.
Иногда появлялась ошибка с текстом differs from count of index set. Это ошибка в самом SPM из-за того, что он ожидал одно количество плагинов из resolved-файлов, а на деле их стало меньше. Звучит так, что SPM не должен падать с ошибкой при такой ситуации, но работаем с тем, что есть. В этом случае помогает удалить лишние зависимости из resolved-файла, но часто легче удалить весь resolved-файл.
Плагинов в нашем репозитории много, поэтому скрипт запускает резолвинг параллельно на нескольких плагинах. Из-за этого в SPM может произойти конфликт доступа к файлам, что приведёт к ошибкам already exists или failed extracting. Чтобы надёжно решить проблему, скрипт помещает плагин с такой ошибкой в отдельную синхронную очередь. Когда скрипт отработал все параллельные запуски, он запускается вновь на плагинах из этой очереди и точно проходит без конфликтов.
В итоге получился скрипт, проверяющий целостность всех Package.resolved-файлов и вносящий в них изменения лишь в крайнем случае, который может работать параллельно на любом количестве пакетов.
Подготовка файла project.pbxproj
Теперь resolved-файлы хранят точный набор версий новых SPM-зависимостей проекта. Но перед тем как начинать сборку, необходимо разобраться с оставшимися упоминаниями переведённых зависимостей из project.pbxproj.
Если у вас были Build Phases с использованием ныне переведённых зависимостей, то необходимо поменять пути до них. Например, для фазы передачи dSYM в Firebase это была замена с обычного ${PODS_ROOT} на путь в DerivedData:
DERIVED_DATA_ROOT="${PROJECT_TEMP_DIR%/Build/*}" CRASHLYTICS_PATH="$(echo "${DERIVED_DATA_ROOT}"/SourcePackages/repositories/firebase-ios-sdk*/Crashlytics)"
Также из-за особенностей работы SPM в нативный Swift-код самого приложения теперь не получится подключать транзитивные зависимости как в CocoaPods, только напрямую подключённые.
Например, если у вашего приложения был плагин plugin_name_1, который зависел от PluginName2, то, чтобы строка import PluginName2 работала в коде внутри ios/Runner, необходимо подключить PluginName2 плагин через Xcode. Есть хороший гайд от Apple, каким образом это сделать. Также это можно сделать через вкладку проекта Package Dependencies.
В поисковую строку можно вставлять ссылки на Git-репозитории зависимостей. В результате Xcode добавит запись о зависимости в project.pbxproj. Если у вас несколько таргетов, например Runner и Notifications, то нужно добавить зависимости в каждый из тех, которые её импортируют. Чтобы проверить правильно подключённые зависимости, откройте их в Xcode, перейдите на вкладку General и просмотрите пункт Frameworks and Libraries.
Последнее, что советую проверить, это наличие в OTHER_LDFLAGS строки вида "-Wl,-force_load,$(SDKROOT)/usr/lib/libc++abi.tbd" в вашем файле проекта. Это часть патча одного креша на версиях iOS ниже 18.4, который по неведомой мне причине перестаёт работать для проектов с SPM до тех пор, пока в этой и двух других строках не уберутся запятые. Эта маленькая мелочь забрала у меня пару дней, и потому уделяю ей отдельное внимание в статье.
Патчинг зависимостей и макросы
Если ваш проект достаточно большой, то однажды вы могли столкнуться с необходимостью патчить поды перед сборкой. Это крайне удобно делать в Podfile, однако у SPM нет такого аналога. Сам Package.swift кодогенерируется при каждом запуске, так что тоже не подходит. Для частично мигрированных приложений не поможет pod install, потому что кеши SPM ещё не скачены, а также билд фазы в project.pbxproj, потому что в этот момент зависимости есть, но граф уже зафиксирован.
Чтобы на устройстве без кешей проект запустился сразу по нажатию кнопки Play в IDE, придётся добавлять загрузку зависимостей и их патчинг либо в bootstrap проекта, либо в preLaunchTask вашей IDE. То есть производить все операции до запуска команды flutter run/build. Если же вы используете пару команд flutter build ipa --config-only + xcodebuild, например на CI, то патчинг зависимостей можно разместить между ними. Повторю, что все кеши SPM по умолчанию находятся по пути ~/Library/Caches/org.swift.swiftpm.
Если же вам нужно вносить правки в кодогенерируемый Package.swift приложения, то на время частичной миграции это можно сделать в Podfile, а при полной — уже только в скриптах перед запуском flutter run, которые имеют отложенное во времени действие. Но лучше всего направить мысль в сторону изменений Package.swift-файлов плагинов. Там можно написать любую обработку на Swift. Например, подключение мокового или реального пакета в зависимости от env во время сборки.
Один из самых популярных патчей зависимостей — смена динамической и статической линковки. В большинстве случаев Xcode подключает SPM-пакеты статически к проекту, а Pods, наоборот, подключаются динамически для Flutter-приложений. Чтобы сделать Swift-пакет динамическим, нужно в поставляемых им библиотеках указать type: .dynamic, например:
Package( name: "plugin_example", products: [ .library(name: "plugin-example", type: .dynamic, targets: ["plugin_example"]), ],.. )
Последнее, что хотел бы обсудить в этом разделе, это макросы — фича Swift-пакетов, позволяющая преобразовывать исходный код во время компиляции. Использовать макросы небезопасно, так что Xcode при билде требует явного разрешения от пользователя на их использование. Если верите в себя, то можете разрешить использование всех макросов командой defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES.
Либо можно разрешать макросы точечно: при первом билде Xcode покажет предупреждение с предложением доверять конкретному макросу. При запуске через CLI или flutter run таких предупреждений не будет, только ошибка.
Разрешённые макросы записываются в ~/Library/org.swift.swiftpm/security/macros.json, этот файл можно спокойно копировать между устройствами. Записи в нём выглядят например так:
[ { "fingerprint": "5ac90d9265dc412789d277c006f4138457dd2d14", "packageIdentity": "xctestparametrizedmacro", "targetName": "XCTestParametrizedMacroMacros" } ]
Настроив патчи зависимостей и разрешённые макросы, можно начинать последний этап миграции — сборку проекта.
Сборка и поиск ошибок
Добившись успешного резолвинга зависимостей, настроив project.pbxproj и переписав все необходимые скрипты, можно приступать к самому главному этапу — сборке приложения. Впрочем, вам ничего не мешает собирать приложение и исправлять появляющиеся ошибки до тех пор, пока они не закончатся, как это делал я. Но поверьте, я был бы в десятки раз более эффективен, если бы с самого начала знал, на какие части необходимо обратить внимание, чтобы потом за два-три запуска увидеть успешную сборку. Перейдём к делу.
Сборка приложения покажет, насколько правильно вы настраивали ваше приложение на всех предыдущих этапах или какие этапы пропустили. Все возможные ошибки перечислить точно не получится, так что упомяну самые распространённые, на мой взгляд:
Ошибки регистрации плагинов в GeneratedPluginRegistrant — проверьте, какое имя класса указано в
pubspec.yamlплагина и над каким указана @objc-аннотация. Или проверьте подключённые вPackage.swiftфайлы.h.Undefined-/unknown-типы или методы в Swift-коде — добавьте Foundation, Flutter, UIKit или другой импорт в недостающие места. В ошибке всегда указан путь до файла, который вызвал ошибку, и какой именно символ её вызвал.
Ошибки линковки из-за duplicate symbols — обычно Flutter выводит общую зависимость при частичной миграции, из-за которой падает ошибка. Но также эта же ошибка выводится и при дубликатах названий классов или методов между пакетами, например при использовании оригинала и форка одного и того же плагина.
Integrity-ошибки Xcode вида "The package product 'PRODUCT' requires minimum platform version X for the iOS platform, but this target supports Y" — как писал выше, SPM при резолвинге зависимостей не обращает внимания на версии iOS, а берёт наиболее высокие из возможных. Причём в ошибке SPM пишет не сам
Package.swift, в котором произошла проблема, а список зависимостей этого файла.
Получив успешную сборку, можно вздохнуть — бóльшая часть пути для хотя бы частичной миграции пройдена. Но теперь требуется проверить работоспособность всех плагинов. Так как все изменения касаются натива, сразу стоит зааттачить дебаггер Xcode к приложению или сразу запускать сборку в нём. Только так получится поймать креш. Вряд ли вы заметите изменения в логике кода плагина, если сохранили те же версии, что были на CocoaPods. Ошибками миграции будут креши: обращение к ресурсам, которые забыли указать в Package.swift, потерянные фреймворки и многое другое.
Для каждого из плагинов, скорее всего, будет достаточно проверить базовое поведение. Кроме того, сценарий тестирования лучше всего повторить как на последней версии iOS, так и на минимальной версии, поддерживаемой вашим приложением. Например, какая-нибудь библиотека могла указать неправильный minos, из-за чего ошибка этапа сборки перенесётся на runtime.
Также может измениться вес приложения, причём в обе стороны. Увидеть, что именно изменилось, можно с помощью Analyze Size из DevTools. В нашей ситуации он увеличился на десятки мегабайт из-за того, что одна из SPM библиотек поставляла сразу все свои таргеты в одном продукте, в то время как нам требовался лишь один. Решилось это только фиксом в Package.swift-файл библиотеки.
И обязательно проверьте выкладку приложения в TestFlight. На этом этапе может выясниться потерянная подпись какого-либо бинарного файла или проблема с минимальной версией фреймворка. В общем, не стоит во всём доверять SPM. Но проделав все вышеперечисленные шаги, можно быть уверенным в успешности и полноте миграции.
Как жить с SPM и конец CocoaPods
Поздравляю! В вашем проекте есть SPM, и сборки с ним уже успешно отправляются в TestFlight.
Если это была частичная миграция, то pod install с вами ещё останется. Так что со стороны разработки практически ничего не изменится. Только добавится проблема с более долгим flutter clean на больших проектах, которую можно обходить, указывая схему: flutter clean --scheme Runner. Резолвинг зависимостей происходит бесшовно при локальной сборке, однако на CI время сборки увеличилось из-за дополнительного резолвинга, даже несмотря на сохранение кешей между запусками.
Возможно, на момент чтения статьи указанные выше проблемы будут уже неактуальны, ибо по интеграции SPM во Flutter активно ведутся работы. Можете периодически проверять изменения в зонтике.
Дальше остаётся идти лишь до полного перехода на SPM с CocoaPods. Это будет возможно, когда Podfile.lock полностью опустеет, Flutter уведомит о необходимости ручного удаления и напишет гайд, как это сделать, если у него не получится сделать это автоматически. Для этого понадобится запустить команду pod deintegrate из ios-поддиректории (она подчистит зависимости и фазы в project.pbxproj), убрать ссылки на поды из xcconfig-файлов и удалить Podfile и Podfile.lock. Runner.xcworkspace не нужно удалять, он до сих пор используется Flutter для работы с проектом. flutter clean и flutter pub get перетрут оставшиеся ссылки на поды, после чего ваш проект будет полностью на SPM. Миграция завершена.
Заключение
Надеюсь, знания, которыми я хотел поделиться по теме миграции на SPM, будут полезны и помогут сохранить время каждому из прочитавших. Swift-мир, а вслед за ним и Flutter неизбежно движутся в сторону SPM, так что миграция любого из приложений или плагинов приближает сообщество к будущему.
Краткий чек-лист, как мигрировать Flutter-приложение на SPM:
Проанализируйте граф
Podfile.lock-зависимостей с помощью скрипта и распланируйте миграцию выделенных подграфов.Архитектура arm64 теперь стандарт по умолчанию. Скрипт подсветит фреймворки, которым потребуется найти или сделать слайс iOS-arm64-simulator.
Поднимайте версии зависимостей или вручную с помощью скриптов мигрируйте плагины. SPM-аналоги зависимостей придётся искать вручную.
Зарезолвите граф SPM-зависимостей, подготовьте
project.pbxprojи проверьте, нет ли дубликатов зависимостей. Когда это всё будет готово, можно начинать сборку приложения.Проверьте выкладку в TestFlight и отсутствие крашей при использовании плагинов.
Если понадобилось мигрировать какой-то из плагинов сообщества, то будет хорошо сделать пулл-реквест с поддержкой SPM в GitHub автора, чтобы всё больше было счастливчиков, которые смогли перевести свой проект одной командой.
Если вы уже начали свой переход на SPM и столкнулись с какими-то багами, о которых я не упомянул, — напишите в комментарии, давайте разбираться вместе.
kolesnikovleon
После таких статей становится понятно, почему многие не спешат уходить с привычных инструментов. Да, SPM выглядит как будущее, но если для перехода нужно писать свои скрипты, разбираться с костылями и неделями ловить странные ошибки сборки, то вопрос. а стоило ли оно того? пока остается открытым.