Cocoapods: управление зависимостями в iOS-проектах

Собрался завтра рассказать коллегам про управление зависимостями в iOS-проектах и решил, что написать пост — самый лучший способ подготовиться и привести мысли в порядок. Надеюсь, этот “побочный продукт” поможет и кому-то из читателей.

Upd: Слайды с прошедшей презентации есть тут.

Проблемы?

Наверное, каждый, кто добавлял в iOS- (или, если брать шире, Cocoa-) проект библиотеку-другую, знает, насколько бесчеловечно организована работа со сторонним кодом в XCode и всём эппловском тулчейне. Особенно очевидна проблема для тех, кто ранее разрабатывал под платформы, где есть развитые средства для управления зависимостями. В частности это Java (и всё разношёрстное JVM-based-племя) c монстрообразным Maven и инструментами на его основе (ivy, gradle, buildr, sbt) и Ruby (с общепринятыми gems и bundler). Собственно, так было и в моём случае. Попробую описать вкратце встречающиеся проблемы.

Установка

Начинается всё с установки. Инструкция по оной к более-менее развесистой библиотеке занимает обычно не один экран текста со скриншотами, да ещё и имеет неприятное свойство устаревать или быть несовместимой с какими-то версиями XCode. Хорошими примерами могут быть RestKit или, например, SQLCipher. Да и если библиотека устанавливается просто добавлением исходников (или статической либы) в проект, процедура скачать-распаковать-перенести-добавить-в-проект начинает несколько доставать. И уж совершенно точно это всё отбивает охоту попробовать несколько библиотек, с тем, чтобы найти подходящую под нужды конкретного проекта.

Хранение

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

  • Положить статически собранную библиотеку в проект и VCS.
  • Положить сорцы библиотеки в проект и VCS. Обычно в какую-нибудь отдельную папочку типа Vendors.
  • Положить сорцы в виде Git-сабмодуля.
  • Держать библиотеки вообще вне VCS.
  • Вытаскивать библиотеки откуда-либо шелл-скриптом при сборке.

У каждого из этих способов есть куча разных достоинств и надостатков. Даже ставшие стандартом де-факто git submodules неидеальны. Библиотека-то может лежать и в SVN. А уж сколько плясок с бубном придётся выполнить, чтобы сменить репозиторий (например, на свой форк библиотеки). Впрочем, на первых порах проблемы вызывает даже процесс обновления исходников.

Подзависимости

Иногда наши библиотеки используют “ещё более другие”© библиотеки. Чаще всего, они лежат вместе с исходниками родительской библиотеки. Весёлости начинаются тогда, когда в вашем проекте, или в другой библиотеке эта дочерняя библиотека уже используется. А если она ещё и другой версии? В этом случае приходится выбирать, от какой от них избавиться.

Обновление

Вот где начинается главное веселье. Проблемы могут возникнуть уже при попытке определить, а какая, собственно, версия используется сейчас. Хорошо если это сабмодуль, или есть какой-ниубдь README. А если нет? Проблема вовсе не надумана: невозможность узнать текущую версию библиотеки мешает понять, например, исправлен ли в ней определённый баг.

Дальше — интереснее. Хорошим примером тут будет всё тот же RestKit. Обновление с версии 0.9.4 до 0.10.0 с непривычки может легко съесть рабочий день. Тут и пляски с сабмодулем, и обновление Header Search Paths (которые, кстати, теперь, оказываются в разных местах при сборке из XCode и на билд-сервере). По сути, обновление библиотеки, это просто ещё одна установка. Как-то спасают здесь (не всегда) только git submodules.

Решение

Итак, как мы выяснили, проблем при использовании библиотек в Cocoa-разработке возникает целая куча. При этом на других платформах она вполне себе решается использованием dependency management tools, таких как maven или bundler. Как оказалось, альтернатив для платформы от Apple, оказалось даже несколько, однако стандартом де-факто сегодня становится одна: CocoaPods. Этот инструмент создан бывшим Ruby-девелопером Eloy Durán, написан на Ruby, и очень напоминает bundler.

Установка

Cocoapods является рубёвым гемом, поэтому установка проста до безобразия:

1
2
$ [sudo] gem install cocoapods
$ pod setup

Собственно всё, теперь cocoapods установлен на вашей машине.

Podfile

Все зависимости прописываются в специальном файле под названием Podfile. Синтаксис довольно простой. Предположим, мы хотим добавить в наш проект (Example.xcodeproj) библиотеку AFNetworking. Создаём в папке с проектом (рядом с .xcodeproj) оный подфайл и пишем туда:

1
2
3
platform :ios

dependency 'AFNetworking', '>= 0.9.1'

затем делаем:

1
$ pod install Example.xcodeproj

и вуаля:

1
2
3
4
5
Updating spec repo `master'
Installing AFNetworking (0.9.1)
Generating support files

[!] From now on use 'Example.xcworkspace' instead of 'Example.xcodeproj'.

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

1
2
3
4
5
6
7
8
9
10
$ tree -L 1
.
├── Default.png
├── Default@2x.png
├── Example
├── Example.xcodeproj
├── Example.xcworkspace
├── Pods
├── Podfile
└── Podfile.lock

Этот воркспейс состоит из нашего проекта и проекта Pods, содержащего все указанные в подфайле библиотеки с зависимостями и собирающегося в одну единственную библиотеку libPods.a. Собственно, это всё. Никаких копирований-распаковок, никаких ковыряний в build settings. Собcтвенно и хранить-то папочку с проектом Pods в VCS не нужно, достаточно добавить только Podfile и Example.xcworkspace (и то я не уверен, что второе обязательно).

Specs

Естественно, никакой магии в этом нет, вся информация о зависимостях (откуда тянуть файлы, какие есть подзависимости, нужен ли для сборки ARC, какие платформы поддерживаются) содержится в большом репозитории на гитхабе в виде файликов, называющихся Specs. Искать по нему библиотеки можно с помощью команды pod search, или на сайте проекта, а присоединиться к команде мейнтейнеров может любой: достаточно написать свою спеку (это довольно просто) и сделать pull request, что собственно я и сделал.

Кстати, слежение за пулл-реквестами в specs-репозиторий, равно как и слежение за твиттером проекта — хороший способ находить полезные библиотеки, чем я и пользуюсь для своих линкопостов.

Версии и обновление

Cocoapods использует semantic versioning для зависимостей, поэтому их версии можно указывать несколькими способами. Здесь, в принципе, почти всё понятно из синтаксиса.

  • > 0.1
  • >= 0.1
  • < 0.1
  • <= 0.1
  • ~> 0.1.2 - использовать версии от 0.1.2 до 0.2 (не включая последнюю).

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

Чтобы обновить библиотеку (или добавить ещё одну), достаточно просто обновить Podfile и опять сделать pod install

Посторонние библиотеки

Всё это прекрасно — скажете вы — но что делать, если нужной мне библиотеки нет в спеках, а заморачиваться с пулл-реквестом лень? Здесь есть несколько способов. Во-первых, можно использовать спеку, не лежащую в главном репозитории:

1
dependency 'JSONKit', :podspec => 'https://raw.github.com/gist/1346394/1d26570f68ca27377a27430c65841a0880395d72/JSONKit.podspec'

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

1
2
3
4
5
6
dependency do |spec|
  spec.name         = 'JSONKit'
  spec.version      = '1.4'
  spec.source       = { :git => 'https://github.com/johnezang/JSONKit.git', :tag => 'v1.4' }
  spec.source_files = 'JSONKit.*'
end

В-третьих, если спека на библиотеку уже есть, но вы хотите использовать другую (более новую, например, версию), можно указать репозиторий, откуда тащить код:

1
2
dependency 'AFNetworking', :git => 'https://github.com/gowalla/AFNetworking.git'
dependency 'AFNetworking', :git => 'https://github.com/gowalla/AFNetworking.git', :commit => '082f8319af'

Точное управление зависимостями

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

1
2
3
4
5
6
7
target :debug do
    dependency 'CocoaLumberjack'
end

target :test, :exclusive => true do
    dependency 'Kiwi'
end

Опция exclusive указывает на то, что в библиотеку включаются только указанные зависимости. Нужные библиотеки нужно будет прицепить куда надо ручками, но это не так уж и сложно.

Грабли и бонусы

Поскольку cocoapods — молодой проект, то, естественно, не обходится без каких-то проблем. Впрочем, пока всё что я встречал - довольно легко решалось. Одной такой граблей была проблема с тем же злополучным RestKit: как оказалось, необходимо было вместо добавления существующей большой спеки, добавить штук пять под-спек (вида RestKit/CoreData). Впрочем, возможно это уже починили.

Другая проблема — xcodebuild на билд-сервере не видит схем внутри воркспейса. Для починки нужно один раз открыть воркспейс в XCode на машине, где происходит сборка. Впрочем, эта потеря времени должна компенсироваться наличием кеша для библиотек: лично у меня добавление RestKit в проект запросто добавляет лишние пять минут к сборке на сервере именно за счёт долгого git clone.

Заключение

В общем, по опыту тех нескольких проектов, где я использовал CocoaPods — впечатления очень хорошие. Довольно много времени экономится на добавлении и обновлении библиотек, да и единый их список для проекта очень удобен. Я очень надеюсь, что нам удастся встроить этот инструмент в стандартный процесс iOS-разработки в e-Legion, для чего, собственно я и создал это введение. Спасибо за внимание и пишите фидбеки.

Пока.

P.S.

Решил сменить дефолтную тему на slash. Как вам?

Comments