В этой статье я расскажу, в чем менеджеры зависимостей (package manager) схожи по внутреннему устройству, алгоритму работы, и в чем их принципиальные отличия. Я рассматривал package manager’ы, предназначенные для разработки под iOS/OS X, но содержание статьи с некоторыми допущениями применимо и к другим.
Разновидности менеджеров зависимостей
- Системные менеджеры зависимостей – устанавливают недостающие утилиты в операционную систему. Например, Homebrew.
- Менеджеры зависимостей языка – собирают исходники, написанные на одном из языков программирования, в конечные исполняемые программы. Например, go build.
- Менеджеры зависимостей проекта – управляют зависимостями в разрезе конкретного проекта. То есть, в их задачи входит описание зависимостей, скачивание, обновление их исходного кода. Это, например, Cocoapods.
Основное отличие между ними в том, кому они «служат». Системные МЗ – пользователям, МЗ проекта – разработчикам, а МЗ языка – и тем, и тем сразу.
Далее я буду рассматривать менеджеры зависимостей проекта – мы их используем чаще всего, и они проще для понимания.
Схема проекта при использовании менедежера зависимостей
Рассмотрим на примере популярного package manager’а Cocoapods.
Обычно мы выполняем условную команду pod install, а затем менеджер зависимостей все делает за нас. Рассмотрим, из чего должен состоять проект, чтобы эта команда завершилась успешно.
- Есть наш код, в котором мы используем ту или иную зависимость, скажем, библиотеку Alamofire.
- Из manifest-файла менеджер зависимостей знает, какие зависимости мы используем в исходном коде. Если мы забудем указать там какую-либо библиотеку, зависимость не установится, и проект в итоге не соберется.
- Lock-файл – генерируемый менеджером зависимостей файл определенного формата, в котором перечисляются все зависимости, успешно установленные в проект.
- Код зависимостей – внешний исходный код, который «подтягивает» менеджер зависимостей и который будет вызываться из нашего кода.
Это было бы невозможно без конкретного алгоритма, который запускается каждый раз после команды установки зависимостей.
Все 4 компонента перечислены друг за другом, т.к. последующий компонент формируется исходя из предыдущего.
Не у всех менеджеров зависимостей есть все 4 компонента, но с учетом функций менеджера зависимостей наличие всех — оптимальный вариант.
После установки зависимостей все 4 компонента идут на вход компилятору либо интерпретатору в зависимости от языка.
Также обращу внимание, что за первые две составляющие ответственны разработчики – мы пишем этот код, а за оставшиеся две – сам менеджер зависимостей – он генерирует файл(ы) и скачивает исходный код зависимостей.
Алгоритм работы менеджера зависимостей
С составными частями более-менее разобрались, теперь перейдем к алгоритмической части работы МЗ.
Типовой алгоритм работы выглядит так:
- Валидация проекта и среды окружения. За это отвечает объект, который именуется 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 или проекта.
Построение графа зависимостей
Так как у нужных нашему проекту зависимостей могут быть свои зависимости, а у тех в свою очередь — собственные вложенные зависимости или подзависимости, менеджеру использовались корректные версии. Схематично все зависимости в результате должны выстроиться в направленный ацикличный граф.
Построение направленного ацикличного графа сводится к задаче топологической сортировки. У нее есть несколько алгоритмов решения.
- Алгоритм Кана – перебор вершин, сложность O(n).
- Алгоритм Тарьяна – на основе поиска в глубину, сложность O(n).
- Алгоритм Демукрона – послойное разбиение графа.
- Параллельные алгоритмы, использующие полиномиальное количество процессоров. В таком случае сложность «упадет» до O(log(n)^2)
Сама по себе задача является NP-полной, тот же алгоритм используется в компиляторах и машинном обучении.
Результатом решения является созданный lock-файл, который полностью описывает отношения между зависимостями.
Какие проблемы могут возникать при работе данного алгоритма? Рассмотрим пример: есть проект с зависимостями A, B, E с вложенными зависимостями C, F, D.
Зависимости 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.?Не стоит забывать, что может возникнуть ситуация циклической зависимости, пусть даже не напрямую – в этом случае установка также завершится с ошибкой.
Рассмотрим, как это выглядит в коде наиболее популярных менеджеров зависимостей для 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-файла, менеджер зависимостей переходит к их скачиванию. Необязательно это будут исходные коды, это могут быть так же исполняемые файлы или собранные фреймворки. Также все менеджеры зависимостей как правило поддерживают возможность установки по локальному пути.
Нет ничего сложного, чтобы их скачать по ссылке (которую, конечно же, нужно откуда-то взять), поэтому я не буду рассказывать как происходит само скачивание, а остановлюсь на вопросах централизации и безопасности.
Централизация
Говоря простым языком, менеджер зависимостей имеет два пути при скачивании зависимостей:
- Сходить в какой-то перечень доступных зависимостей и по названию получить ссылку для скачивания.
- Мы должны явно указать источник для каждой зависимости в manifest-файле.
По первому пути идут централизованные менеджеры зависимостей, по второму – децентрализованные.
Безопасность
Если вы скачиваете зависимости по 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. Проще всего найти тот, которой написан на знакомом языке. Описанная схема является типовой, но в отдельно взятом менеджере зависимостей может что-то отсутствовать или наоборот появиться новое.
Замечания и обсуждение в комментариях приветствуется.