У читателя может возникнуть вопрос, зачем Carthage когда уже есть довольно зрелый SwiftPM или целый комбайн функциональности в виде CocoaPods (пусть и официально больше не развивающийся), и это вполне резонный вопрос. Ответ довольно прост, как разработчик, повидавший тонну проектов небольших и средних размеров, в которых оркестрирование зависимостями происходило с помощью CocoaPods и SwiftPM, все без исключения проекты со временем становилось сложнее разрабатывать и я имею в виду именно сложность в написании кода, потому что, чем больше проект имел зависимостей тем более неповоротливый он становился, про неповоротливость тут имеется ввиду долгая сборка, долгая индексация, комплит кода в полу живом состоянии, работа со SwiftUI Preview становится невозможной. Аналогичные причины перехода на Carthage были указаны и в других статьях на эту тему.

Почему проблемы, описанные выше, может решить Carthage, но не могут решить уже ранее упомянутые CocoaPods и SwiftPM ? На самом деле и CocoaPods, и SwiftPM могут решить эти проблемы, но это потребует колоссальных затрат, так как каждую библиотеку-зависимость нужно будет руками переделывать (упаковывать в бинарный вид).

А как конкретно описанные проблемы решит Carthage ? Тут все просто и логично:

  1. CocoaPods затягивает зависимости как подпроекты со всеми исходниками (исключаем случай когда используется vendored_frameworks), всё это "добро" лежит в ./Pods/ директории

  2. SwiftPM также затягивает зависимости со всеми исходниками (исключаем случаи для prebuild зависимостей), все они лежат в /DerivedData/$Project/SourcePackages

В свою очередь зависимости с исходниками порождают 2 проблемы:

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

  2. Редакторы кода, будь это Xcode, AppCode, IntelliJ, Android Studio, все они анализируют и кешируют код проекта и его зависимостей, чтобы работал комплит кода и навигация. Чем больше кода, тем дольше идет индексация. Однажды я открыл небольшое приложение, в нем было порядка 60 SwiftPM зависимостей, Xcode их выкачивал минут 5–10, потом индексировал 7–10 минут, то есть проектом невозможно было пользоваться 15–20 минут (справедливости ради это на холодный старт всё так печально), но мне нужно было поменять в конфиге одну строчку чтобы пушнуть на сервер и там собрать билд, в итоге я закрыл Xcode, чтобы не ждать 15 минут, и просто в vim открыл нужный файл и поправил. Для тех, кто задался вопросом "А если собирать приложение из консоли (через xcodebuild) то тоже будет индексация? Ведь она не нужна, так как редактор кода не открыт...", успокою вас, Xcode выключает в этом случае индексацию.

Пошаговая миграция зависимостей из CocoaPods на Carthage.

Все эксперименты проводятся на Xcode 26.2 и macOS 26.2.

Дано, есть приложение с Firebase, который интегрирован через CocoaPods. Имеем вот такой Podfile:

target 'HabrCocoaPods' do
  
  use_frameworks!

  pod 'FirebaseAnalytics',  '12.9.0'
  pod 'FirebaseCrashlytics',  '12.9.0'
  pod 'FirebaseRemoteConfig',  '12.9.0'
  pod 'FirebaseFirestore',  '12.9.0'
  pod 'FirebaseAuth',  '12.9.0'
  pod 'FirebaseMessaging',  '12.9.0'
  pod 'FirebasePerformance',  '12.9.0'

end

Когда делаем скачивание зависимостей в проект, это всё разворачивается в такую "красоту":

pod install
Analyzing dependencies
Downloading dependencies
Installing BoringSSL-GRPC 0.0.37
Installing FirebaseABTesting 12.9.0
Installing FirebaseAnalytics 12.9.0
Installing FirebaseAppCheckInterop 12.9.0
Installing FirebaseAuth 12.9.0
Installing FirebaseAuthInterop (12.9.0)
Installing FirebaseCore 12.9.0
Installing FirebaseCoreExtension 12.9.0
Installing FirebaseCoreInternal 12.9.0
Installing FirebaseCrashlytics 12.9.0
Installing FirebaseFirestore 12.9.0
Installing FirebaseFirestoreInternal 12.9.0
Installing FirebaseInstallations 12.9.0
Installing FirebaseMessaging 12.9.0
Installing FirebasePerformance 12.9.0
Installing FirebaseRemoteConfig 12.9.0
Installing FirebaseRemoteConfigInterop (12.9.0)
Installing FirebaseSessions 12.9.0
Installing FirebaseSharedSwift 12.9.0
Installing GTMSessionFetcher 5.0.0
Installing GoogleAdsOnDeviceConversion (3.2.0)
Installing GoogleAppMeasurement 12.9.0
Installing GoogleDataTransport 10.1.0
Installing GoogleUtilities 8.1.0
Installing RecaptchaInterop 101.0.0
Installing abseil 1.20240722.0
Installing gRPC-C++ 1.69.0
Installing gRPC-Core 1.69.0
Installing nanopb 3.30910.0
Generating Pods project
Integrating client project
Pod installation complete! There are 7 dependencies from the Podfile and 32 total pods installed.

А теперь не поленимся и заглянем в ./Pods/ директорию и погрепаем исходники на предмет их кол-ва.

find ./Pods -type f -name "*.c" | wc -l
653 # 653 сишных файлов
find ./Pods -type f -name "*.cc" | wc -l
1043 # 1043 с++ файлов
find ./Pods -type f -iname "*.m" | wc -l
301 # 301 Objective-C файлов
find ./Pods -type f -iname "*.mm" | wc -l
44 # 44 Objective-C++ файлов
find ./Pods -type f -iname "*.swift" | wc -l
268 # 268 Swift файлов

В сумме это больше 2к файлов исходников, которые нужно компилировать, + сборка c/c++/objc происходит clang-ом, а swift через swiftc, что также негативно (скорее всего) сказывается на времени компиляции всех зависимостей. Также не забываем, что Xcode будет индексировать все эти файлы чтобы комплитить код и резолвить символы во время навигации по коду, то есть перенаправлять в исходник, а не в интерфейс (.h, .hpp, .hxx, .swiftinterface). То есть условно мы создали пустое приложение "hello world" с Firebase и у нас со старта уже 2000 файлов на компиляцию, которые по сути нам и не нужны, так как нам нужен только интерфейс для работы с Firebase без исходников.

Да что там, давайте просто откроем "hello world" проект с Firebase в Xcode и посмотрим на прогресс индексации и компиляции:

Решение очевидно, перегоняем исходники Firebase в бинарный вид, то есть в .xcframework библиотеки. Это решит сразу обе проблемы:

  1. Библиотеки нужно собрать только один раз, затем их можно положить в шаренный между всеми разработчиками кеш или просто добавить в git (я предпочитаю добавлять в git, так как библиотеки на моей памяти обновляются раз в год, когда выходит новая iOS, и нет смысла замарачиваться с кешем, но, если вы пересобираете зависимости очень часто, тогда ваш git может сильно распухнуть, имейте это в виду)

  2. Так как больше нет исходников, то и Xcode больше нечего индексировать, только интерфейсы и заголовочные файлы, что на порядок меньше и проще чем голые исходники.

Устанавливаем Carthage

brew install carthage

Перенесем описание Firebase зависимостей из Podfile в Carthage аналогичный файл Cartfile

touch Cartfile
cat Cartfile
binary "https://dl.google.com/dl/firebase/ios/carthage/FirebaseAnalyticsBinary.json" == 12.9.0
binary "https://dl.google.com/dl/firebase/ios/carthage/FirebaseCrashlyticsBinary.json" == 12.9.0
binary "https://dl.google.com/dl/firebase/ios/carthage/FirebaseRemoteConfigBinary.json" == 12.9.0
binary "https://dl.google.com/dl/firebase/ios/carthage/FirebaseFirestoreBinary.json" == 12.9.0
binary "https://dl.google.com/dl/firebase/ios/carthage/FirebaseAuthBinary.json" == 12.9.0
binary "https://dl.google.com/dl/firebase/ios/carthage/FirebaseMessagingBinary.json" == 12.9.0
binary "https://dl.google.com/dl/firebase/ios/carthage/FirebasePerformanceBinary.json" == 12.9.0

Создадим bash скрипт в корне проекта откуда будем вызывать carthage утилиту

cd $project_dir
touch build_carthage.sh
chmod +x build_carthage.sh

И запустим команду carthage в скрипте

carthage update --use-xcframeworks --no-use-binaries --platform iOS

Что делает эта команда:

  1. Создает или обновляет файл Cartfile.resolved (аналог Podfile.lock)

  2. Загружает зависимости в Carthage общий кеш (/Users/user/Library/Caches/org.carthage.CarthageKit) по описанию из Cartfile.resolved

  3. По описанию из Cartfile.resolved копирует исходники из кеша в локальную ./Carthage/Checkouts/ директорию

  4. Запускает build команду для получения финальных исходников для iOS платформы

В нашем случае результат команды будет следующим

carthage update --use-xcframeworks --no-use-binaries --platform iOS

*** Downloading binary-only framework FirebasePerformanceBinary at "https://dl.google.com/dl/firebase/ios/carthage/FirebasePerformanceBinary.json"
*** Downloading binary-only framework FirebaseMessagingBinary at "https://dl.google.com/dl/firebase/ios/carthage/FirebaseMessagingBinary.json"
*** Downloading binary-only framework FirebaseAnalyticsBinary at "https://dl.google.com/dl/firebase/ios/carthage/FirebaseAnalyticsBinary.json"
*** Downloading binary-only framework FirebaseRemoteConfigBinary at "https://dl.google.com/dl/firebase/ios/carthage/FirebaseRemoteConfigBinary.json"
*** Downloading binary-only framework FirebaseCrashlyticsBinary at "https://dl.google.com/dl/firebase/ios/carthage/FirebaseCrashlyticsBinary.json"
*** Downloading binary-only framework FirebaseAuthBinary at "https://dl.google.com/dl/firebase/ios/carthage/FirebaseAuthBinary.json"
*** Downloading binary-only framework FirebaseFirestoreBinary at "https://dl.google.com/dl/firebase/ios/carthage/FirebaseFirestoreBinary.json"

Но команда подозрительно выполнилась слишком быстро. Смотрим, что в ./Carthage/Build

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

binary "https://dl.google.com/dl/firebase/ios/carthage/FirebaseAnalyticsBinary.json" == 12.9.0

где binary означает, что зависимость будет скачиваться уже в виде собранной библиотеки, более того, Google это явно прописал в документации к Firebase https://github.com/firebase/firebase-ios-sdk/blob/main/Carthage.md

Note that the Firebase frameworks in the distribution include static libraries.

То есть даже Google осознал, что собирать каждый раз Firebase это too much и поэтому сразу предоставляет собранные версии. Давайте еще раз внимательно глянем на финальный xcframework, что собрал нам Google:

Как можно увидеть Google собрал бинарники под все Apple SDK (iphoneos, tvos, macos, ...) и все базовые архитектуры (x86_64, arm64)

/ios-arm64/
/ios-arm64_x86_64-simulator/
/tvos-arm64_x86_64-simulator/
/macos-arm64_x86_64/
/ios-arm64_x86_64-maccatalyst/
/tvos-arm64/

Так как мы хотим эти бинарники добавлять в git, то в идеале нам нужна библиотека только под iOS, и только arm64 для реального iPhone, и только arm64 для симулятора, x86_64 симуляторная архитектура нужна для Intel машин с macOS, но, так как я давно таких не видел у разработчиков, то и эту архитектуру желательно убрать, чтобы не добавлять лишний вес в git.

Удаляем бинарники для очевидных платформ как macOS и tvOS, так как они нам точно не нужны

for xcframework in $(find Carthage/Build -name "*.xcframework"); do
    for platform in "tvos-arm64" "tvos-arm64_x86_64-simulator" "macos-arm64_x86_64" "ios-arm64_x86_64-maccatalyst"; do
        platform_path="$xcframework/$platform"
        if [ -d "$platform_path" ]; then
            rm -fr "$platform_path"
        fi
    done
done

После того как скрипт удалил лишние платформы у нас осталось 2 платформы

/ios-arm64/
/ios-arm64_x86_64-simulator/

Платформа /ios-arm64/ это под реальные девайсы, оставляем как есть, менять ничего не нужно. Платформа /ios-arm64_x86_64-simulator/ это бинарник под симуляторы, причем это fat бинарник который в себе содержит скомпилированную версию кода для arm64 симулятора (Apple M Chip) и версию под x86_64 Intel машину. Это легко проверить просто командой

file ios-arm64_x86_64-simulator/FirebaseABTesting.framework/FirebaseABTesting 

ios-arm64_x86_64-simulator/FirebaseABTesting.framework/FirebaseABTesting: Mach-O universal binary with 2 architectures:
ios-arm64_x86_64-simulator/FirebaseABTesting.framework/FirebaseABTesting (for architecture x86_64):	current ar archive random library
ios-arm64_x86_64-simulator/FirebaseABTesting.framework/FirebaseABTesting (for architecture arm64):	current ar archive random library

Так как мы хотим избавиться от x86_64, то просто используем lipo утилиту, которая поставляется с Xcode

for xcframework in $(find Carthage/Build -name "*.xcframework"); do
    platform_path="$xcframework/ios-arm64_x86_64-simulator"
    if [ -d "$platform_path" ]; then
        xcframework_name="$(basename "$xcframework")"
        base_name="${xcframework_name%%.*}"
        binary_path="$platform_path/$base_name.framework/$base_name"
        lipo "$binary_path" -remove x86_64 -output "$binary_path.tmp"
        mv "$binary_path.tmp" "$binary_path"
    fi
done

и проверяем через file, что осталась только arm64 архитектура:

file ios-arm64_x86_64-simulator/FirebaseABTesting.framework/FirebaseABTesting 

ios-arm64_x86_64-simulator/FirebaseABTesting.framework/FirebaseABTesting: Mach-O universal binary with 1 architecture:
ios-arm64_x86_64-simulator/FirebaseABTesting.framework/FirebaseABTesting (for architecture arm64):	current ar archive random library

Всё сработало.

Переименовываем из ios-arm64_x86_64-simulator в ios-arm64-simulator и дополненный скрипт становится

for xcframework in $(find Carthage/Build -name "*.xcframework"); do
    platform_path="$xcframework/ios-arm64_x86_64-simulator"
    if [ -d "$platform_path" ]; then
        xcframework_name="$(basename "$xcframework")"
        base_name="${xcframework_name%%.*}"
        binary_path="$platform_path/$base_name.framework/$base_name"
        lipo "$binary_path" -remove x86_64 -output "$binary_path.tmp"
        mv "$binary_path.tmp" "$binary_path"
        mv "$platform_path" "$xcframework/ios-arm64-simulator"
    fi
done

Теперь у нас .xcframework стал выглядеть так:

Где остался последний штрих, это изменить Info.plist убр��в из него удаленные архитектуры. Сам файлы выглядит следующим образом (показана только часть файла):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>AvailableLibraries</key>
	<array>
		<dict>
			<key>LibraryIdentifier</key>
			<string>macos-arm64_x86_64</string>
			<key>LibraryPath</key>
			<string>FirebaseABTesting.framework</string>
			<key>SupportedArchitectures</key>
			<array>
				<string>arm64</string>
				<string>x86_64</string>
			</array>
			<key>SupportedPlatform</key>
			<string>macos</string>
		</dict>
	</array>
	<key>CFBundlePackageType</key>
	<string>XFWK</string>
	<key>XCFrameworkFormatVersion</key>
	<string>1.0</string>
</dict>
</plist>

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

for xcframework in $(find Carthage/Build -name "*.xcframework"); do
    info_plist="$xcframework/Info.plist"
    if [ -f "$info_plist" ]; then
        xcframework_name="$(basename "$xcframework")"
        base_name="${xcframework_name%%.*}"
        cat > $info_plist <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>AvailableLibraries</key>
  <array>
    <dict>
      <key>LibraryIdentifier</key>
      <string>ios-arm64</string>
      <key>LibraryPath</key>
      <string>${base_name}.framework</string>
      <key>SupportedArchitectures</key>
      <array>
        <string>arm64</string>
      </array>
      <key>SupportedPlatform</key>
      <string>ios</string>
    </dict>
    <dict>
      <key>LibraryIdentifier</key>
      <string>ios-arm64-simulator</string>
      <key>LibraryPath</key>
      <string>${base_name}.framework</string>
      <key>SupportedArchitectures</key>
      <array>
        <string>arm64</string>
      </array>
      <key>SupportedPlatform</key>
      <string>ios</string>
      <key>SupportedPlatformVariant</key>
      <string>simulator</string>
    </dict>
  </array>
  <key>CFBundlePackageType</key>
  <string>XFWK</string>
  <key>XCFrameworkFormatVersion</key>
  <string>1.0</string>
</dict>
</plist>
EOF
    fi
done

Теперь всё готово, из мультиплатформенного xcframework мы получили чисто iOS-ную версию, но стоит отметить, что данные оптимизации с xcframework проделали ради экономии места на диске, если вы не собираетесь бинарные зависимости добавлять в git или загружать во внешний кеш, а только будете хранить локально, этот шаг можно пропустить. Подведем итоги по оптимизации места:

До оптимизации Firebase, зависимости в бинарном виде занимали 509 МБ.

du -sh Carthage/Build 
509M Carthage/Build 

После оптимизации суммарный размер Firebase зависимостей стал 112 МБ, то есть больше чем в 4 раза оптимизировали место на диске.

du -sh Carthage/Build
112M Carthage/Build

Теперь финальный шаг - добавляем Firebase зависимости в Xcode. Так как Google за нас собрал статические библиотеки, то их нужно подключать как статические — в линковке выставлять как "Do not embed".

И финально собираем каждую версию приложения и снимаем метрики по сборке.

Время сборки "hello world" c Firebase через CocoaPods составляет 154 секунды, это 2 с половиной минуты.

Время сборки "hello world" c Firebase через Carthage составляет 4 секунды!.

Мне кажется метрики говорят сами за себя.

Дополню, попробовал добавить Firebase через SwiftPM, думал Google и там сделал зависимости через prebuild binary, но, к сожалению, только часть зависимостей сделаны как prebuild, остальные подтягиваются с исходниками на c/c++/objc/swift. В итоге на сборку пустого проекта ушло 45 секунд.

Хотелось на этом месте подвести итоги и закончить статью, но Firebase оказался слишком простым в управлении через Carthage, так как всю грязную работу за нас сделал Google и заранее подготовил бинарные версии библиотек, что немного не честно для полного погружения в работу Carthage. Поэтому дальше рассмотрим добавление зависимости, которая точно будет собираться локально из исходников, и посмотрим, что из этого выйдет.

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

Идем в документацию, Amplitude поддерживает Carthage. Меняем Cartfile на

cat Cartfile
github "amplitude/Amplitude-Swift" == 1.15.1

и пытаемся собрать

carthage update --use-xcframeworks --no-use-binaries --platform iOS

И получаем ошибку

*** Building scheme "AnalyticsConnector" in AnalyticsConnector.xcodeproj
Package.resolved file is corrupted or malformed; fix or delete the file to continue: unknown 'PinsStorage' version '3' at '/Users/user/Downloads/HabrCarthage/Carthage/Checkouts/Amplitude-Swift/Amplitude-Swift.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved'.

Так как для своего проекта для обратной совместимости я собираю бинарники на старом Xcode 15.2, то причин�� ошибки нагуглилась довольно быстро. Если посмотреть на проблемный файл Package.resolved в Amplitude SDK

cat Carthage/Checkouts/Amplitude-Swift/Amplitude-Swift.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
{
  "originHash" : "05dc9d5826a10a55c9206bcf9a1c8cf2fc2da9497ce6bf904707b8c4690be457",
  "pins" : [],
  "version" : 3
}

"version" : 3 указывает, что этот файл был сгенерировать SwiftPM из Xcode 16+, формат которого не поддерживает SwiftPM из Xcode 15.2. Поэтому без изменений Amplitude библиотеки мы не сможет собрать для нее бинарный файл, а изменение нужно сделать просто выставить "version" : 2 которую понимает SwiftPM из Xcode 15.2. В других статьях о Carthage люди зачем-то клонируют репозиторий, вносят изменения и затем уже оттуда подтягивают новый репозиторий в Carthage. На самом деле это можно сделать гораздо проще и быстрее просто внести изменения в зависимость после скачивания из репозитория, но перед тем, как зависимость будет собрана.

Меняем команду, которая скачивает и собирает исходники

carthage update --use-xcframeworks --no-use-binaries --platform iOS

на команду, которая скачивает исходники и обновляет Cartfile.resolved без сборки проектов

carthage update --no-build

Все исходники зависимостей для сборки находятся в ./Carthage/Checkouts/. Теперь просто меняем версию SwiftPM на нужную версию следующей комнадой

sed -i '' 's/"version" : 3/"version" : 2/g' ./Carthage/Checkouts/Amplitude-Swift/Amplitude-Swift.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

и затем запускаем отдельно команду build для сборки зависимостей

carthage build --use-xcframeworks --no-use-binaries --platform iOS

После чего зависимость успешно собирается. На самом деле это частично выдуманный случай, конечно, мы потом просто собрали Amplitude новым Xcode и убрали этот костыль. Этим примером я просто хотел показать, если какая-то Carthage зависимость не собирается, не обязательно делать fork репозитория и усложнять интеграцию, можно в промежутке между скачиванием и сборкой зависимостей выполнить скрипт, который на месте решит проблему.

Чтобы вышеописанная проблема нам больше не мешала, снова переключаюсь на сборку через Xcode 26.2 и Amplitude зависимость поднимаю до последней версии, чтобы она успешно собиралась, на момент написания статьи это версия 1.17.1

cat Cartfile
github "amplitude/Amplitude-Swift" == 1.17.1

Выполняем сборку

carthage update --no-build
carthage build --use-xcframeworks --no-use-binaries --platform iOS

что дает нам успешный результат

*** Building scheme "AnalyticsConnector" in AnalyticsConnector.xcodeproj
*** Building scheme "Amplitude-Swift-Package" in Amplitude-Swift.xcodeproj
*** Building scheme "Amplitude-Swift-Package-DisableUIKit" in Amplitude-Swift.xcodeproj
*** Success

Идем в директорию с результатами сборки и снова видим, что у нас куча лишних платформ как было с Firebase, причем еще добавились новые это watchOS и visionOS.

Обновляем наш скрипт по удалению ненужных платформ где добавляем обработку watchOS и visionOS

for xcframework in $(find Carthage/Build -name "*.xcframework"); do
    for platform in "tvos-arm64" "tvos-arm64_x86_64-simulator" "macos-arm64_x86_64" "ios-arm64_x86_64-maccatalyst" "watchos-arm64_arm64_32_armv7k" "watchos-arm64_x86_64-simulator" "xros-arm64" "xros-arm64_x86_64-simulator"; do
        platform_path="$xcframework/$platform"
        if [ -d "$platform_path" ]; then
            rm -fr "$platform_path"
        fi
    done
done

Как мы помним по Firebase, Google за нас собрал исходники в бинарный вид, причем в статическом формате. Мы также хотим получить и Amplitude бинарники в статическом, а не динамическом виде. Чтобы это проверить, напишем скрипт, который обойдет все бинарники и выведет их тип.

for xcframework in $(find Carthage/Build -name "*.xcframework"); do
    for platform in "ios-arm64" "ios-arm64-simulator"; do
      platform_path="$xcframework/$platform"
      xcframework_name="$(basename "$xcframework")"
      base_name="${xcframework_name%%.*}"
      binary_path="$platform_path/$base_name.framework/$base_name"
      file "$binary_path"
    done
done

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

AmplitudeSwiftNoUIKit.framework/AmplitudeSwiftNoUIKit:	Mach-O 64-bit dynamically linked shared library arm64
AmplitudeCore.framework/AmplitudeCore: Mach-O 64-bit dynamically linked shared library arm64
AnalyticsConnector.framework/AnalyticsConnector: Mach-O 64-bit dynamically linked shared library arm64
AmplitudeSwift.framework/AmplitudeSwift: Mach-O 64-bit dynamically linked shared library arm64

К нашему сожалению, у нас собрались динамические библиотеки, а не статические. Чтобы переключить на статическую сборку, нам нужно открывать проект в Xcode и в секции Linking - General выставлять Mach-O Type как Static Library (MACH_O_TYPE=staticlib) и так проделать для каждой зависимости.

Но на самом деле Carthage предоставляет возможность передавать .xcconfig file с ключами для Xcode Build Settings, которые будут иметь приоритет над ключами проекта. Используя этот механизм, попробуем переопределить MACH_O_TYPE на статический тип.

# Создаем xcconfig file
XCCONFIG=/tmp/$(uuidgen).xcconfig
# записывает значения для ключа MACH_O_TYPE как статическая библиотека
echo 'MACH_O_TYPE=staticlib' >> $XCCONFIG
# экспортируем путь до нашего xcconfig ��айла в переменную окружения нашей сессии терминала которую Carthge будет использовать во время сборки
export XCODE_XCCONFIG_FILE="$XCCONFIG"
# собираем зависимости снова
carthage build --use-xcframeworks --no-use-binaries --platform iOS

И после успешной сборки снова запускаем проверку типа бинарников и получаем результат:

AmplitudeSwiftNoUIKit.framework/AmplitudeSwiftNoUIKit (for architecture arm64):	current ar archive
AnalyticsConnector.framework/AnalyticsConnector (for architecture arm64):	current ar archive
AmplitudeSwift.framework/AmplitudeSwift (for architecture arm64):	current ar archive
AmplitudeCore.framework/AmplitudeCore: Mach-O 64-bit dynamically linked shared library arm64

И к удивлению, только 3 библиотеки из 4 стали статичными (AmplitudeCore.framework осталась динамической), я подумал сначала, что это какой-то баг, но потом оказалось, что Amplitude одну из библиотек загружает из своего хранилища, которая там лежит в динамическом виде, поэтому она не участвует в сборке и на нее наш .xcconfig файл никаким образом не влияет. Поэтому будьте внимательны и проверяйте, какие либы в итоге получаются, потому что от типа зависит их линковка в приложение. Для статичных нужно использовать "Do not embed", для динамических "Emded and Sign".

После интеграции в проект, запускаем основной "Hello world" проект с Amplitude в Xcode 26.2 и убеждаемся, что собрали библиотеки верно. Но что будет, если запустить проект на Xcode 26.3? Давайте выясним. Проект успешно запускается и на нем, потому что в Amplitude исходниках BUILD_LIBRARY_FOR_DISTRIBUTION выставлено в YES, что позволяет собранный бинарник на одной версии Swift, успешно запускать на более поздних версиях Swift, иначе для разных версий Swift мы бы получили ошибку Module compiled with Swift a.b.c cannot be imported by the Swift x.y.z compiler. Но я всегда, на всякий случай, это явно прописываю в .xcconfig файл, чтобы точно не пересобирать зависимости под каждый новый Xcode и не навязывать всей команде использование одной версии Xcode.

echo 'BUILD_LIBRARY_FOR_DISTRIBUTION=YES' >> $XCCONFIG

Что еще улучшить?

Если вы собираетесь добавлять собранные либы в git, то для дополнительной оптимизации можете поработать с директорией Modules внутри .framework, там есть такие файлы:

arm64-apple-ios.abi.json               
arm64-apple-ios.swiftinterface
arm64-apple-ios.private.swiftinterface 
arm64-apple-ios.swiftmodule
arm64-apple-ios.swiftdoc

Часть из них можно удалить, но нужно проверять. Я точно знаю, что файлы *.private.swiftinterface можно удалять, потому что они содержат описание interface-ов для internal разработки, которые точно не будут доступны приложению потребителю библиотеки.

find ./Carthage/Build -type f -name "*private.swiftinterface" -delete

Подведем итог

Потратив день-два на перевод зависимостей из SwiftPM и CocoaPods на Carthage мы получаем значительное преимущество в скорости сборки и индексации проекта (результаты с Firebase получились внушительные). На своем опыте я переводил 2 средних проекта на Carthage, точнее самые тяжёлые либы из CocoaPods и результат был впечатляющим, сборка проектов упала с 7–10 минут до 2-3 минут, индексация проектов стала существенно быстрее, то есть после открытия проекта нужно было подождать 30 секунд и с проектом можно было начинать работать, даже SwiftUI Preview иногда прогружался, но это уже отдельная боль для отдельной статьи. Я конечно же не предлагаю слепо переходить на Carthage. Каждый менеджер зависим��стей имеет свои плюсы и минусы.

Спасибо, что прочитали до конца.

Весь bash скрипт с комментариями
#!/bin/zsh

xcodebuild -version

# 1. Скачиваем зависимости
# 2. Создаем или обновляем Cartfile.resolved
carthage update --no-build

# Создаем xcconfig файл с ключами для переопределения проектных
XCCONFIG=/tmp/$(uuidgen).xcconfig
echo 'MACH_O_TYPE=staticlib' >> $XCCONFIG
echo 'BUILD_LIBRARY_FOR_DISTRIBUTION=YES' >> $XCCONFIG

# Прокидываем xcconfig файл в Carthage
export XCODE_XCCONFIG_FILE="$XCCONFIG"

# На этом шаге также можно модифицировать исходники библиотек в Carthage/Build/Checkouts

# Запускаем сборку всех зависимостей
carthage build --use-xcframeworks --no-use-binaries --platform iOS

# Удаляем НЕ iOS платформы
for xcframework in $(find Carthage/Build -name "*.xcframework"); do
    for platform in "tvos-arm64" "tvos-arm64_x86_64-simulator" "macos-arm64_x86_64" "ios-arm64_x86_64-maccatalyst" "watchos-arm64_arm64_32_armv7k" "watchos-arm64_x86_64-simulator" "xros-arm64" "xros-arm64_x86_64-simulator"; do
        platform_path="$xcframework/$platform"
        if [ -d "$platform_path" ]; then
            rm -fr "$platform_path"
        fi
    done
done

# Удаляем iOS Simulator x86_64 архитектуру для старых Intel машин
for xcframework in $(find Carthage/Build -name "*.xcframework"); do
    platform_path="$xcframework/ios-arm64_x86_64-simulator"
    if [ -d "$platform_path" ]; then
        xcframework_name="$(basename "$xcframework")"
        base_name="${xcframework_name%%.*}"
        binary_path="$platform_path/$base_name.framework/$base_name"
        lipo "$binary_path" -remove x86_64 -output "$binary_path.tmp"
        mv "$binary_path.tmp" "$binary_path"
        mv "$platform_path" "$xcframework/ios-arm64-simulator"
    fi
done

# Обновляем Info.plist оставляя только нужные нам архитектуры и платформы
for xcframework in $(find Carthage/Build -name "*.xcframework"); do
    info_plist="$xcframework/Info.plist"
    if [ -f "$info_plist" ]; then
        xcframework_name="$(basename "$xcframework")"
        base_name="${xcframework_name%%.*}"
        cat > $info_plist <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>AvailableLibraries</key>
  <array>
    <dict>
      <key>LibraryIdentifier</key>
      <string>ios-arm64</string>
      <key>LibraryPath</key>
      <string>${base_name}.framework</string>
      <key>SupportedArchitectures</key>
      <array>
        <string>arm64</string>
      </array>
      <key>SupportedPlatform</key>
      <string>ios</string>
    </dict>
    <dict>
      <key>LibraryIdentifier</key>
      <string>ios-arm64-simulator</string>
      <key>LibraryPath</key>
      <string>${base_name}.framework</string>
      <key>SupportedArchitectures</key>
      <array>
        <string>arm64</string>
      </array>
      <key>SupportedPlatform</key>
      <string>ios</string>
      <key>SupportedPlatformVariant</key>
      <string>simulator</string>
    </dict>
  </array>
  <key>CFBundlePackageType</key>
  <string>XFWK</string>
  <key>XCFrameworkFormatVersion</key>
  <string>1.0</string>
</dict>
</plist>
EOF
    fi
done

# Выводим в консоль типы собранных библиотек
for xcframework in $(find Carthage/Build -name "*.xcframework"); do
    for platform in "ios-arm64" "ios-arm64-simulator"; do
      platform_path="$xcframework/$platform"
      xcframework_name="$(basename "$xcframework")"
      base_name="${xcframework_name%%.*}"
      binary_path="$platform_path/$base_name.framework/$base_name"
      file "$binary_path"
    done
done

# Удаляем приватные интерфейсы во всех xcframework
find ./Carthage/Build -type f -name "*private.swiftinterface" -delete