Flutter + SPM: как не сломать сборку при переходе с CocoaPods
Сборка Flutter-проекта после перехода на Swift Package Manager чаще всего падает из-за того, что часть плагинов ещё подключается через CocoaPods, а часть — уже через SPM, и в проект попадают два экземпляра одной библиотеки, например GoogleUtilities или BoringSSL. Чтобы починить сборку: свести зависимости к одному источнику, зафиксировать Package.resolved и Podfile.lock, отключать CocoaPods только после проверки, что все плагины реально поддерживают SPM.
откуда берётся конфликт CocoaPods и Swift Package Manager во Flutter-проекте
Пятница, до отправки сборки в App Store Connect — три дня. Команда обновила Flutter до версии 3.24 и включила экспериментальную поддержку Swift Package Manager для iOS-таргета: часть плагинов уже собирается через SPM, часть по привычке тянется через Podfile. У тимлида на макбуке всё собирается за 40 секунд. На CI сборка падает с ошибкой «Multiple commands produce ...GoogleUtilities.framework» и «duplicate symbol» для arm64.
Разница между машинами в одном: у CI — чистый checkout и свежий pod install, у тимлида — старый DerivedData и Pods, которые случайно замаскировали конфликт кэшем. Но релиз не уедет с зелёной галочкой на локальной машине — Xcode Cloud или Bitrise собирают архив с нуля, а видят они сразу две копии одной библиотеки: одну версию тянет CocoaPods через Podfile.lock, вторую — Package.resolved того же транзитивного пакета Firebase или gRPC.
Дело не в баге Flutter, а в переходном периоде между двумя менеджерами пакетов. С версии 3.24 SDK умеет резолвить нативные зависимости плагинов через SPM, но большинство пакетов в pub.dev по-прежнему публикуются только с podspec, без Package.swift. Пока в графе зависимостей есть хотя бы один такой плагин, убрать CocoaPods целиком нельзя — можно только временно свести оба менеджера в одном проекте. И ровно в этой точке начинаются дубли фреймворков, конфликты minimum deployment target и падения по duplicate symbol.
Если не разобраться до релиза, теряется не абстрактная стабильность, а конкретные дни разработчика на дебаг сборки вместо доработки функциональности — и риск не попасть в еженедельное окно ревью App Store, из-за чего дата релиза сдвигается уже для отдела продаж, который анонсировал обновление клиентам.
CocoaPods и SPM в одном проекте: что меняется в сборке iOS-таргета
Xcode с одиннадцатой версии умеет резолвить SPM-пакеты нативно, без Podfile, но Flutter встроил такую поддержку только в 3.24 — и то не для всех плагинов, а только для тех, где авторы сами добавили Package.swift. Разница принципиальная: CocoaPods строит единый граф зависимостей и кладёт результат в отдельный Pods.xcodeproj, а SPM резолвит каждый пакет независимо и подключает его как отдельную цель сборки. Когда оба механизма работают в одном проекте одновременно, Xcode не «сливает» одинаковые транзитивные зависимости — он видит два разных таргета с одинаковым именем символа и не знает, какой из них правильный.
| критерий | CocoaPods | Swift Package Manager |
|---|---|---|
| подключение зависимостей | через Podfile и pod install, отдельный Pods.xcodeproj | напрямую в Xcode или Package.swift, без отдельного workspace |
| резолвинг версий | единый Podfile.lock на весь проект | Package.resolved у каждого пакета отдельно |
| поддержка Flutter-плагинов | почти все пакеты pub.dev | только плагины с Package.swift — пока меньшинство |
| линковка по умолчанию | статическая, если включён use_frameworks! | динамическая |
| типичная ошибка при смешении | не возникает в изоляции | Multiple commands produce, duplicate symbol |
откуда берутся дубли зависимостей
Дубли чаще всего дают не сами Flutter-плагины, а их транзитивные зависимости — библиотеки, которые тянутся автоматически вместе с пакетом. Firebase-обвязка тащит за собой GoogleUtilities и nanopb, сетевые пакеты — gRPC-Core и BoringSSL-GRPC. Если один плагин в проекте перешёл на SPM, а второй — ещё нет, но оба используют, например, GoogleUtilities, в сборке оказываются две версии одной библиотеки одновременно: одна через Podfile.lock, вторая через Package.resolved. Xcode об этом не предупреждает заранее — ошибка вылезает только на этапе линковки.
как исправить ошибки сборки после отключения CocoaPods
Когда конфликт уже пойман по логам сборки, дальше — три типичных сценария, с которыми сталкивается почти любая команда, мигрирующая с CocoaPods на SPM.
«Multiple commands produce» и дубли фреймворков
Ошибка значит, что один и тот же выходной файл фреймворка собирается дважды. Решение — найти, какие плагины уже отдают Package.swift (посмотреть репозиторий пакета или запустить flutter pub deps и свериться со списком), и для конфликтующей транзитивной библиотеки закрепить одну версию сразу в двух местах: в Podfile строкой вида pod 'GoogleUtilities', '~> 7.13', и в Package.resolved того же названия и той же версии. Если версии расходятся хотя бы на минорный релиз, Xcode продолжит видеть два разных таргета.
несовпадение minimum deployment target
Podfile прописывает platform :ios, '13.0', а Package.swift одного из плагинов требует .iOS(.v14) — Xcode линкует итоговый бинарник против более высокой версии и падает уже на тестировании на старых устройствах, а не на этапе сборки. Таргет нужно синхронизировать в трёх местах сразу: ios/Podfile, настройках Runner-таргета в project.pbxproj и в Package.swift каждого подключённого плагина. Разница даже в одну минорную версию iOS достаточна, чтобы сборка либо не прошла, либо прошла, но упала на реальном устройстве.
кэш DerivedData и Pods, которые никто не чистил
Локальная машина разработчика часто «прощает» конфликт, потому что старый кэш DerivedData и ранее собранные Pods маскируют проблему — Xcode просто переиспользует уже слинкованные артефакты. Полный сброс перед проверкой: flutter clean, затем удалить ios/Pods, ios/Podfile.lock и ios/.symlinks, почистить DerivedData командой xcodebuild clean или вручную из ~/Library/Developer/Xcode/DerivedData, и только после этого заново flutter pub get и pod install. Если ошибка исчезла — дело было в кэше. Если появилась снова — дело в версиях зависимостей, и нужно возвращаться к предыдущему пункту.
почему ошибка сборки возвращается после каждого pod install
Если чистая переустановка не помогает и ошибка возвращается каждый раз — дело не в кэше, а в том, что версии зависимостей нигде не закреплены. Частая причина: Package.resolved или Podfile.lock не закоммичены в git — тогда каждый разработчик и каждый прогон CI резолвят свежие версии транзитивных пакетов независимо друг от друга и получают разные графы зависимостей. Сборка у одного человека проходит, у другого падает, а через неделю падает уже у всех — потому что где-то вышла новая минорная версия GoogleUtilities.
Второй частый источник повторяющейся ошибки — разные версии Xcode на CI и на машинах команды: SPM резолвит зависимости по-разному в зависимости от версии тулчейна, и то, что собралось на Xcode 16.1, не обязательно соберётся на 16.3.
Третий сценарий — миграция упёрлась в один конкретный плагин без поддержки SPM, который блокирует переход остальных зависимостей. В этом случае разумнее не пытаться выключить CocoaPods силой, а оставить проект в гибридном режиме до тех пор, пока автор плагина не выпустит Package.swift, либо подобрать поддерживаемую альтернативу с тем же функционалом.
что держит сборку стабильной при обновлении Flutter, Xcode и сторонних SDK
Закреплять версии зависимостей — Podfile.lock и Package.resolved должны лежать в git, а не в .gitignore. Без этого каждое обновление любого из полутора десятков транзитивных пакетов способно снова развалить сборку без единой строчки изменений в коде приложения.
Тестировать миграцию в отдельной ветке на матрице версий Xcode, а не сразу в main — если в команде используются две-три версии Xcode одновременно, стоит собрать проект на каждой перед мерджем.
Зафиксировать базовый набор версий для всей команды: конкретную версию Flutter (через fvm или .fvmrc), конкретную версию Xcode (файл .xcode-version), конкретную версию CocoaPods. Без этого правила «у меня собирается» будет звучать после каждого обновления macOS.
Перед крупным апгрейдом — Flutter, Xcode, iOS SDK — фиксировать рабочее состояние отдельным тегом или веткой, чтобы откат занимал минуты, а не полдня разбора git-истории.
Планировать миграцию заранее, а не в неделю перед релизом — особенно если приложение завязано на операционные процессы бизнеса: курьерские и складские приложения, которые обмениваются данными с 1С в реальном времени, не прощают простой сборки так же легко, как внутренний корпоративный инструмент.
когда миграцию на SPM стоит отдать подрядчику
Если в команде нет отдельного iOS-разработчика, миграция с CocoaPods на SPM обычно съедает несколько дней на дебаг линковщика вместо работы над функциями продукта — а конфликт зависимостей у каждого проекта свой, и универсального скрипта-решения не существует.
Мы в UKVED разрабатываем мобильные приложения на Flutter с закреплённым графом зависимостей и CI, который проверяет сборку на нескольких версиях Xcode до релиза, а не после — разработка приложения обходится от 500 000 руб. Часто это не самостоятельный продукт, а мобильный клиент для бизнеса, уже работающего на 1С: приложение кладовщика или курьера, которое читает и пишет остатки и статусы заказов в 1С:Управление торговлей, или полевой инструмент менеджера, синхронизированный с 1С:ERP.
В таких проектах риск миграции двойной: ломается не только сборка мобильного приложения, но и обмен данными с учётной системой, если на стороне 1С не предусмотрен стабильный API. Поэтому обмен обычно закрывают отдельной доработкой 1С — тем же принципом, что и версии зависимостей в SPM: один зафиксированный контракт данных вместо расходящихся версий на клиенте и на сервере.
❓ Частые вопросы
Можно ли использовать CocoaPods и Swift Package Manager в одном Flutter-проекте одновременно?
Да, Flutter с версии 3.24 поддерживает гибридный режим: часть плагинов резолвится через SPM, часть — через CocoaPods. Это рабочий, но временный режим на период миграции: как только все зависимости получат поддержку SPM, CocoaPods можно убрать полностью.
Почему сборка падает с ошибкой Multiple commands produce после обновления Flutter?
Эта ошибка значит, что один и тот же файл фреймворка собирается дважды — обычно транзитивная библиотека вроде GoogleUtilities подключена и через Podfile, и через Package.swift другого плагина одновременно. Нужно свести версию к одному источнику и убрать дублирующую цель сборки.
Сколько времени занимает миграция Flutter-проекта с CocoaPods на SPM?
Зависит от числа плагинов без нативной поддержки SPM: небольшой проект с 10-15 зависимостями обычно мигрирует за 2-4 дня, включая тестирование на нескольких версиях Xcode. Проект с кастомными нативными модулями и Firebase может занять полторы-две недели.
Что делать, если один плагин из pub.dev не поддерживает Swift Package Manager?
Оставить его на CocoaPods в гибридном режиме — Flutter это позволяет — и мигрировать остальные зависимости. Полный переход имеет смысл только тогда, когда обновится сам плагин или найдётся поддерживаемая альтернатива с Package.swift.
Стоит ли переходить на SPM прямо сейчас, если сборка и так работает на CocoaPods?
Если релиз не горит и все зависимости стабильны — не обязательно, поддержка SPM во Flutter пока экспериментальная. Стоит перейти, если CocoaPods перестал устанавливаться на новых версиях Xcode или один из плагинов уже требует SPM как единственный вариант подключения.
Или позвоните: +7 495 133-92-44 — в рабочее время с 9:00 до 19:00
Остались вопросы? Нужна помощь?
Менеджеры компании с радостью ответят на ваши вопросы, произведут расчет стоимости услуг и подготовят индивидуальное коммерческое предложение.
Бесплатная консультация

