![](https://habrastorage.org/webt/eo/2z/gn/eo2zgnh7wmofkn9u5lkycjb1kus.png)
В этой статье я расскажу, в чем менеджеры зависимостей (package manager) схожи по внутреннему устройству, алгоритму работы, и в чем их принципиальные отличия. Я рассматривал package manager’ы, предназначенные для разработки под iOS/OS X, но содержание статьи с некоторыми допущениями применимо и к другим.
Разновидности менеджеров зависимостей
- Системные менеджеры зависимостей – устанавливают недостающие утилиты в операционную систему. Например, Homebrew.
- Менеджеры зависимостей языка – собирают исходники, написанные на одном из языков программирования, в конечные исполняемые программы. Например, go build.
- Менеджеры зависимостей проекта – управляют зависимостями в разрезе конкретного проекта. То есть, в их задачи входит описание зависимостей, скачивание, обновление их исходного кода. Это, например, Cocoapods.
Основное отличие между ними в том, кому они «служат». Системные МЗ – пользователям, МЗ проекта – разработчикам, а МЗ языка – и тем, и тем сразу.
Далее я буду рассматривать менеджеры зависимостей проекта – мы их используем чаще всего, и они проще для понимания.
Схема проекта при использовании менедежера зависимостей
Рассмотрим на примере популярного package manager’а Cocoapods.
Обычно мы выполняем условную команду pod install, а затем менеджер зависимостей все делает за нас. Рассмотрим, из чего должен состоять проект, чтобы эта команда завершилась успешно.
![](https://habrastorage.org/webt/b_/zz/3g/b_zz3gkcyt_pskamlnd_4qutocw.png)
- Есть наш код, в котором мы используем ту или иную зависимость, скажем, библиотеку Alamofire.
- Из manifest-файла менеджер зависимостей знает, какие зависимости мы используем в исходном коде. Если мы забудем указать там какую-либо библиотеку, зависимость не установится, и проект в итоге не соберется.
- Lock-файл – генерируемый менеджером зависимостей файл определенного формата, в котором перечисляются все зависимости, успешно установленные в проект.
- Код зависимостей – внешний исходный код, который «подтягивает» менеджер зависимостей и который будет вызываться из нашего кода.
Это было бы невозможно без конкретного алгоритма, который запускается каждый раз после команды установки зависимостей.
Все 4 компонента перечислены друг за другом, т.к. последующий компонент формируется исходя из предыдущего.
![](https://habrastorage.org/webt/7h/ez/e1/7heze1iy5kalhr3v2vqddu6wrzs.png)
Не у всех менеджеров зависимостей есть все 4 компонента, но с учетом функций менеджера зависимостей наличие всех — оптимальный вариант.
После установки зависимостей все 4 компонента идут на вход компилятору либо интерпретатору в зависимости от языка.
![](https://habrastorage.org/webt/h5/se/fs/h5sefs2_akqx7zraxli8mx7rpae.png)
Также обращу внимание, что за первые две составляющие ответственны разработчики – мы пишем этот код, а за оставшиеся две – сам менеджер зависимостей – он генерирует файл(ы) и скачивает исходный код зависимостей.
![](https://habrastorage.org/webt/dp/el/nn/dpelnn1xpwqlfvhw9qduvhcimfm.png)
Алгоритм работы менеджера зависимостей
С составными частями более-менее разобрались, теперь перейдем к алгоритмической части работы МЗ.
Типовой алгоритм работы выглядит так:
- Валидация проекта и среды окружения. За это отвечает объект, который именуется Analyzer.
- Построение графа. Из зависимостей МЗ должен выстроить граф. Этим занимается объект Resolver.
- Скачивание зависимостей. Очевидно, что исходный код зависимостей должен быть скачан для того, чтобы мы его использовали в своих исходниках.
- Интеграция зависимостей. Того, что исходный код зависимостей лежит в соседней директории на диске, может быть недостаточно, поэтому их еще нужно прикрепить к нашему проекту.
- Обновление зависимостей. Этот шаг выполняется не сразу за шагом 4, а при необходимости обновиться на новую версии библиотек. Здесь есть свои особенности, поэтому я выделил их в отдельный шаг — о них далее.
Валидация проекта и среды окружения
Валидация включает проверку версий ОС, вспомогательных утилит, которые необходимы менеджеру зависимостей, а также линтовку настроек проекта и manifest-файла: начиная от проверки на синтаксис, заканчивая несовместимыми настройками.
Типовой podfile
source 'https://github.com/CocoaPods/Specs.git'
source 'https://github.com/RedMadRobot/cocoapods-specs'
platform :ios, '10.0'
use_frameworks!
project 'Project.xcodeproj'
workspace 'Project.xcworkspace'
target 'Project' do
project 'Project.xcodeproj'
pod 'Alamofire'
pod 'Fabric'
pod 'GoogleMaps'
end
Возможные предупреждения и ошибки при проверке podfile:
- Не найдена зависимость ни в одном из spec-репозитории;
- Явно не указана операционная система и версия;
- Некорректное имя workspace или проекта.
Построение графа зависимостей
Так как у нужных нашему проекту зависимостей могут быть свои зависимости, а у тех в свою очередь — собственные вложенные зависимости или подзависимости, менеджеру использовались корректные версии. Схематично все зависимости в результате должны выстроиться в направленный ацикличный граф.
![](https://habrastorage.org/webt/km/un/14/kmun144wbc7e-vk53es-hxtj1b8.png)
Построение направленного ацикличного графа сводится к задаче топологической сортировки. У нее есть несколько алгоритмов решения.
- Алгоритм Кана – перебор вершин, сложность O(n).
- Алгоритм Тарьяна – на основе поиска в глубину, сложность O(n).
- Алгоритм Демукрона – послойное разбиение графа.
- Параллельные алгоритмы, использующие полиномиальное количество процессоров. В таком случае сложность «упадет» до O(log(n)^2)
Сама по себе задача является NP-полной, тот же алгоритм используется в компиляторах и машинном обучении.
Результатом решения является созданный lock-файл, который полностью описывает отношения между зависимостями.
![](https://habrastorage.org/webt/ev/go/vd/evgovd36odyii-s2iy1xnmgmyka.png)
Какие проблемы могут возникать при работе данного алгоритма? Рассмотрим пример: есть проект с зависимостями A, B, E с вложенными зависимостями C, F, D.
![](https://habrastorage.org/webt/ao/0c/im/ao0cima3ra6vkhqtwxazzqtbhas.png)
Зависимости A и B имеют общую зависимость C. И здесь С должна удовлетворить требованиям зависимости A и B. Какой-то менеджер зависимостей допускает установку отдельных версий, если это необходимо, а cocoapods, например, нет. Поэтому в случае несовместимости требований: A требует версию, равную 2.0 зависимости С, а B требует версию 1.0, установка завершится с ошибкой. А если зависимости A нужна версия 1.0 и выше до версии 2.0, а зависимости B версия 1.2 или менее до 1.0, будет установлена максимально совместимая для A и В версия 1.2.?Не стоит забывать, что может возникнуть ситуация циклической зависимости, пусть даже не напрямую – в этом случае установка также завершится с ошибкой.
![](https://habrastorage.org/webt/c9/50/ef/c950efn7lgxntydbqgbn4le7aoq.png)
Рассмотрим, как это выглядит в коде наиболее популярных менеджеров зависимостей для iOS.
Carthage
typealias DependencyGraph = [Dependency: Set<Dependency>]
public enum Dependency {
/// A repository hosted on GitHub.com or GitHub Enterprise.
case gitHub(Server, Repository)
/// An arbitrary Git repository.
case git(GitURL)
/// A binary-only framework
case binary(URL)
}
/// Protocol for resolving acyclic dependency graphs.
public protocol ResolverProtocol {
init(
versionsForDependency: @escaping (Dependency) -> SignalProducer<PinnedVersion, CarthageError>,
dependenciesForDependency: @escaping (Dependency, PinnedVersion) -> SignalProducer<(Dependency, VersionSpecifier), CarthageError>,
resolvedGitReference: @escaping (Dependency, String) -> SignalProducer<PinnedVersion, CarthageError>
)
func resolve(
dependencies: [Dependency: VersionSpecifier],
lastResolved: [Dependency: PinnedVersion]?,
dependenciesToUpdate: [String]?
) -> SignalProducer<[Dependency: PinnedVersion], CarthageError>
}
Реализация Resolver находится тут, а NewResolver тут, Analyzer как такового нет.
Cocoapods
Реализация алгоритма построения графа выделена в отдельный репозиторий. Здесь же реализация графа и Resolver. В Analyzer можно найти, что проверяется соответствие версий cocoapods системы и lock-файла.
def validate_lockfile_version!
if lockfile && lockfile.cocoapods_version > Version.new(VERSION)
STDERR.puts '[!] The version of CocoaPods used to generate ' "the lockfile (#{lockfile.cocoapods_version}) is " "higher than the version of the current executable (#{VERSION}). " 'Incompatibility issues may arise.'.yellow
end
end
Из исходников также видно, что Analyzer генерирует таргеты для зависимостей.
Типовой lock-файл cocoapods выглядит примерно так:
PODS:
- Alamofire (4.7.0)
- Fabric (1.7.5)
- GoogleMaps (2.6.0):
- GoogleMaps/Maps (= 2.6.0)
- GoogleMaps/Base (2.6.0)
- GoogleMaps/Maps (2.6.0):
- GoogleMaps/Base
SPEC CHECKSUMS:
Alamofire: 907e0a98eb68cdb7f9d1f541a563d6ac5dc77b25
Fabric: ae7146a5f505ea370a1e44820b4b1dc8890e2890
GoogleMaps: 42f91c68b7fa2f84d5c86597b18ceb99f5414c7f
PODFILE CHECKSUM: 5294972c5dd60a892bfcc35329cae74e46aac47b
COCOAPODS: 1.4.0
В секции PODS перечисляются прямые и вложенные зависимости с указанием версий, далее подсчитываются их контрольные суммы в отдельности и вместе и указывается версия cocoapods, которая использовалась для установки.
Скачивание зависимостей
После успешного построения графа и создания lock-файла, менеджер зависимостей переходит к их скачиванию. Необязательно это будут исходные коды, это могут быть так же исполняемые файлы или собранные фреймворки. Также все менеджеры зависимостей как правило поддерживают возможность установки по локальному пути.
![](https://habrastorage.org/webt/jn/pd/im/jnpdimi2oy5cywb3v2lmchqtmaw.png)
Нет ничего сложного, чтобы их скачать по ссылке (которую, конечно же, нужно откуда-то взять), поэтому я не буду рассказывать как происходит само скачивание, а остановлюсь на вопросах централизации и безопасности.
Централизация
Говоря простым языком, менеджер зависимостей имеет два пути при скачивании зависимостей:
- Сходить в какой-то перечень доступных зависимостей и по названию получить ссылку для скачивания.
- Мы должны явно указать источник для каждой зависимости в manifest-файле.
По первому пути идут централизованные менеджеры зависимостей, по второму – децентрализованные.
![](https://habrastorage.org/webt/q2/9r/uo/q29ruopiznp2qp4hzmyqvgqtngm.png)
Безопасность
Если вы скачиваете зависимости по https или ssh, то можете спать спокойно. Тем не менее, часто разработчики предоставляют http-ссылки на свои официальные библиотеки. И здесь мы можем столкнуться с атакой «человек посередине», когда злоумышленник подменит исходный код, исполняемый файл или фреймворк. Какие-то менеджеры зависимостей не защищаются от этого, а некоторые делают это следующим образом.
Homebrew
Проверка curl в устаревших версиях OS X.
def check_for_bad_curl
return unless MacOS.version <= "10.8"
return if Formula["curl"].installed?
<<~EOS
The system curl on 10.8 and below is often incapable of supporting
modern secure connections & will fail on fetching formulae.
We recommend you:
brew install curl
EOS
end
Также есть проверка хэша SHA256 при скачивании по http.
def curl_http_content_headers_and_checksum(url, hash_needed: false, user_agent: :default)
max_time = hash_needed ? "600" : "25"
output, = curl_output(
"--connect-timeout", "15", "--include", "--max-time", max_time, "--location", url,
user_agent: user_agent
)
status_code = :unknown
while status_code == :unknown || status_code.to_s.start_with?("3")
headers, _, output = output.partition("\r\n\r\n")
status_code = headers[%r{HTTP\/.* (\d+)}, 1]
end
output_hash = Digest::SHA256.digest(output) if hash_needed
{
status: status_code,
etag: headers[%r{ETag: ([wW]\/)?"(([^"]|\\")*)"}, 2],
content_length: headers[/Content-Length: (\d+)/, 1],
file_hash: output_hash,
file: output,
}
end
А еще можно запретить небезопасные редиректы на http (переменная HOMEBREW_NO_INSECURE_REDIRECT).
Carthage и Cocoapods
Здесь все попроще – нельзя использовать http на исполняемые файлы.
guard binaryURL.scheme == "file" || binaryURL.scheme == "https" else { return .failure(BinaryJSONError.nonHTTPSURL(binaryURL)) }
def validate_source_url(spec)
return if spec.source.nil? || spec.source[:http].nil?
url = URI(spec.source[:http])
return if url.scheme == 'https' || url.scheme == 'file'
warning('http', "The URL (`#{url}`) doesn't use the encrypted HTTPs protocol. " 'It is crucial for Pods to be transferred over a secure protocol to protect your users from man-in-the-middle attacks. ' 'This will be an error in future releases. Please update the URL to use https.')
end
Полный код тут.
Swift Package Manager
На данный момент ничего, связанного с безопасностью, найти не удалось, но в предложениях по развитию есть короткое упоминание про некий механизм подписи пакетов с помощью сертификатов.
Интеграция зависимостей
Под интеграцией я понимаю подключение зависимостей к проекту таким образом, чтобы мы беспрепятственно могли их использовать, и они компилировались с основным кодом приложения.
Интеграция может быть либо ручной (Carthage), либо автоматической (Cocoapods). Плюсы автоматической – минимум телодвижений со стороны разработчика, но может добавиться много магии в проект.
Diff после установки зависимостей в проект с помощью Cocoapods
--- a/PODInspect/PODInspect.xcodeproj/project.pbxproj
+++ b/PODInspect/PODInspect.xcodeproj/project.pbxproj
@@ -12,6 +12,7 @@
5132347E1FE94F0900031F77 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5132347C1FE94F0900031F77 /* Main.storyboard */; };
513234801FE94F0900031F77 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5132347F1FE94F0900031F77 /* Assets.xcassets */; };
513234831FE94F0900031F77 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 513234811FE94F0900031F77 /* LaunchScreen.storyboard */; };
+ 80BFE252F8CC89026D002347 /* Pods_PODInspect.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F92C797D84680452FD95785F /* Pods_PODInspect.framework */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@@ -22,6 +23,9 @@
5132347F1FE94F0900031F77 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
513234821FE94F0900031F77 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
513234841FE94F0900031F77 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+ 700D806167013759DC590135 /* Pods-PODInspect.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PODInspect.debug.xcconfig"; path = "Pods/Target Support Files/Pods-PODInspect/Pods-PODInspect.debug.xcconfig"; sourceTree = "<group>"; };
+ E03230E2AEDEF09BD80A4BCB /* Pods-PODInspect.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PODInspect.release.xcconfig"; path = "Pods/Target Support Files/Pods-PODInspect/Pods-PODInspect.release.xcconfig"; sourceTree = "<group>"; };
+ F92C797D84680452FD95785F /* Pods_PODInspect.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PODInspect.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -29,6 +33,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 80BFE252F8CC89026D002347 /* Pods_PODInspect.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -40,6 +45,8 @@
children = (
513234771FE94F0900031F77 /* PODInspect */,
513234761FE94F0900031F77 /* Products */,
+ 78E8125D6DC3597E7EBE4521 /* Pods */,
+ 7DB1871A5E08D43F92A5D931 /* Frameworks */,
);
sourceTree = "<group>";
};
@@ -64,6 +71,23 @@
path = PODInspect;
sourceTree = "<group>";
};
+ 78E8125D6DC3597E7EBE4521 /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ 700D806167013759DC590135 /* Pods-PODInspect.debug.xcconfig */,
+ E03230E2AEDEF09BD80A4BCB /* Pods-PODInspect.release.xcconfig */,
+ );
+ name = Pods;
+ sourceTree = "<group>";
+ };
+ 7DB1871A5E08D43F92A5D931 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ F92C797D84680452FD95785F /* Pods_PODInspect.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "<group>";
+ };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -71,9 +95,12 @@
isa = PBXNativeTarget;
buildConfigurationList = 513234871FE94F0900031F77 /* Build configuration list for PBXNativeTarget "PODInspect" */;
buildPhases = (
+ 5A5E7D86F964C22F5DF60143 /* [CP] Check Pods Manifest.lock */,
513234711FE94F0900031F77 /* Sources */,
513234721FE94F0900031F77 /* Frameworks */,
513234731FE94F0900031F77 /* Resources */,
+ 5FD616368597C8B1F8138B2B /* [CP] Embed Pods Frameworks */,
+ F5ECBE5F431B568B7F8C9B0B /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -131,6 +158,62 @@
};
/* End PBXResourcesBuildPhase section */
+/* Begin PBXShellScriptBuildPhase section */
+ 5A5E7D86F964C22F5DF60143 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-PODInspect-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 5FD616368597C8B1F8138B2B /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${SRCROOT}/Pods/Target Support Files/Pods-PODInspect/Pods-PODInspect-frameworks.sh",
+ "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework",
+ "${BUILT_PRODUCTS_DIR}/HTTPTransport/HTTPTransport.framework",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/HTTPTransport.framework",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-PODInspect/Pods-PODInspect-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ F5ECBE5F431B568B7F8C9B0B /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-PODInspect/Pods-PODInspect-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+/* End PBXShellScriptBuildPhase section */
+
/* Begin PBXSourcesBuildPhase section */
513234711FE94F0900031F77 /* Sources */ = {
isa = PBXSourcesBuildPhase;
@@ -272,6 +355,7 @@
};
513234881FE94F0900031F77 /* Debug */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 700D806167013759DC590135 /* Pods-PODInspect.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
@@ -287,6 +371,7 @@
};
513234891FE94F0900031F77 /* Release */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = E03230E2AEDEF09BD80A4BCB /* Pods-PODInspect.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
В случае ручной, вы, следуя, например, этой инструкции Carthage, полностью контролируете процесс добавления зависимостей в проект. Надежно, но дольше.
Обновление зависимостей
Контролировать исходный код зависимостей в проекте можно с помощью их версий.
В менеджерах зависимостей используются 3 способа:
- Версии библиотеки. Наиболее удобный и распространенный способ. Можно указать как конкретную версию, так и интервал. Вполне предсказуемый способ для поддержки совместимости зависимостей при условии корректного изменения версий авторами библиотек.
- Ветка. При обновлении ветки и дальнейшем обновлении зависимости мы не можем предсказать, какие изменения произойдут.
- Коммит или тэг. При выполнении команды на обновление, зависимости со ссылками на конкретный коммит или тэг (если его не изменят) никогда не будут обновляться.
Заключение
В статье я дал поверхностное понимание внутреннего устройства менеджеров зависимостей. Если хотите узнать больше, стоит покопаться в исходном коде package manager'a. Проще всего найти тот, которой написан на знакомом языке. Описанная схема является типовой, но в отдельно взятом менеджере зависимостей может что-то отсутствовать или наоборот появиться новое.
Замечания и обсуждение в комментариях приветствуется.