- PVSM.RU - https://www.pvsm.ru -
В этой статье я расскажу, в чем менеджеры зависимостей (package manager) схожи по внутреннему устройству, алгоритму работы, и в чем их принципиальные отличия. Я рассматривал package manager’ы, предназначенные для разработки под iOS/OS X, но содержание статьи с некоторыми допущениями применимо и к другим.
Основное отличие между ними в том, кому они «служат». Системные МЗ – пользователям, МЗ проекта – разработчикам, а МЗ языка – и тем, и тем сразу.
Далее я буду рассматривать менеджеры зависимостей проекта – мы их используем чаще всего, и они проще для понимания.
Рассмотрим на примере популярного package manager’а Cocoapods [3].
Обычно мы выполняем условную команду pod install, а затем менеджер зависимостей все делает за нас. Рассмотрим, из чего должен состоять проект, чтобы эта команда завершилась успешно.
Это было бы невозможно без конкретного алгоритма, который запускается каждый раз после команды установки зависимостей.
Все 4 компонента перечислены друг за другом, т.к. последующий компонент формируется исходя из предыдущего.
Не у всех менеджеров зависимостей есть все 4 компонента, но с учетом функций менеджера зависимостей наличие всех — оптимальный вариант.
После установки зависимостей все 4 компонента идут на вход компилятору либо интерпретатору в зависимости от языка.
Также обращу внимание, что за первые две составляющие ответственны разработчики – мы пишем этот код, а за оставшиеся две – сам менеджер зависимостей – он генерирует файл(ы) и скачивает исходный код зависимостей.
С составными частями более-менее разобрались, теперь перейдем к алгоритмической части работы МЗ.
Типовой алгоритм работы выглядит так:
Валидация включает проверку версий ОС, вспомогательных утилит, которые необходимы менеджеру зависимостей, а также линтовку настроек проекта и manifest-файла: начиная от проверки на синтаксис, заканчивая несовместимыми настройками.
Типовой podfile [5]
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:
Так как у нужных нашему проекту зависимостей могут быть свои зависимости, а у тех в свою очередь — собственные вложенные зависимости или подзависимости, менеджеру использовались корректные версии. Схематично все зависимости в результате должны выстроиться в направленный ацикличный граф [7].
Построение направленного ацикличного графа сводится к задаче топологической сортировки. У нее есть несколько алгоритмов решения.
Сама по себе задача является 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.
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 находится тут [10], а NewResolver тут [11], Analyzer как такового нет.
Реализация алгоритма построения графа выделена в отдельный репозиторий [12]. Здесь же реализация графа [13] и Resolver [14]. В Analyzer [15] можно найти, что проверяется соответствие версий 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 или ssh, то можете спать спокойно. Тем не менее, часто разработчики предоставляют http-ссылки на свои официальные библиотеки. И здесь мы можем столкнуться с атакой «человек посередине» [16], когда злоумышленник подменит исходный код, исполняемый файл или фреймворк. Какие-то менеджеры зависимостей не защищаются от этого, а некоторые делают это следующим образом.
Проверка curl [17] в устаревших версиях 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 [18] при скачивании по 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("rnrn")
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
А еще можно запретить [19] небезопасные редиректы на http (переменная HOMEBREW_NO_INSECURE_REDIRECT).
Здесь все попроще – нельзя использовать [20] 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
Полный код тут [21].
На данный момент ничего, связанного с безопасностью, найти не удалось, но в предложениях по развитию есть короткое упоминание про некий механизм [22] подписи пакетов с помощью сертификатов.
Под интеграцией я понимаю подключение зависимостей к проекту таким образом, чтобы мы беспрепятственно могли их использовать, и они компилировались с основным кодом приложения.
Интеграция может быть либо ручной (Carthage), либо автоматической (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/nullnif [ $? != 0 ] ; thenn # print error to STDERRn echo "error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation." >&2n exit 1nfin# 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;
В случае ручной, вы, следуя, например, этой [23] инструкции Carthage, полностью контролируете процесс добавления зависимостей в проект. Надежно, но дольше.
Контролировать исходный код зависимостей в проекте можно с помощью их версий.
В менеджерах зависимостей используются 3 способа:
В статье я дал поверхностное понимание внутреннего устройства менеджеров зависимостей. Если хотите узнать больше, стоит покопаться в исходном коде package manager'a. Проще всего найти тот, которой написан на знакомом языке. Описанная схема является типовой, но в отдельно взятом менеджере зависимостей может что-то отсутствовать или наоборот появиться новое.
Замечания и обсуждение в комментариях приветствуется.
Автор: Иван Вавилов
Источник [24]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/281667
Ссылки в тексте:
[1] Homebrew: https://brew.sh
[2] go build: https://golang.org/pkg/go/build/
[3] Cocoapods: https://cocoapods.org
[4] Alamofire: https://github.com/Alamofire/
[5] podfile: https://guides.cocoapods.org/syntax/podfile.html
[6] spec-репозитории: https://guides.cocoapods.org/making/specs-and-specs-repo.html
[7] направленный ацикличный граф: https://ru.wikipedia.org/wiki/%D0%9D%D0%B0%D0%BF%D1%80%D0%B0%D0%B2%D0%BB%D0%B5%D0%BD%D0%BD%D1%8B%D0%B9_%D0%B0%D1%86%D0%B8%D0%BA%D0%BB%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D0%B9_%D0%B3%D1%80%D0%B0%D1%84
[8] Алгоритм Кана: https://ru.wikipedia.org/wiki/%D0%A2%D0%BE%D0%BF%D0%BE%D0%BB%D0%BE%D0%B3%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B0%D1%8F_%D1%81%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0#%D0%90%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC_%D0%9A%D0%B0%D0%BD%D0%B0_(1962)
[9] Алгоритм Тарьяна: https://ru.wikipedia.org/wiki/%D0%A2%D0%BE%D0%BF%D0%BE%D0%BB%D0%BE%D0%B3%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B0%D1%8F_%D1%81%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0#%D0%90%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC_%D0%A2%D0%B0%D1%80%D1%8C%D1%8F%D0%BD%D0%B0_(1976)
[10] тут: https://github.com/Carthage/Carthage/blob/master/Source/CarthageKit/Resolver.swift
[11] тут: https://github.com/Carthage/Carthage/blob/master/Source/CarthageKit/NewResolver.swift
[12] репозиторий: https://github.com/CocoaPods/Molinillo
[13] графа: https://github.com/CocoaPods/Molinillo/blob/04dd4c8789823654bde00569d28b189a79926c9f/lib/molinillo/dependency_graph.rb
[14] Resolver: https://github.com/CocoaPods/Molinillo/blob/1f66e42351ae27167aefdd76bc914598e4b0e5f2/lib/molinillo/resolution.rb
[15] Analyzer: https://github.com/CocoaPods/CocoaPods/blob/31de1ad65c8853ddaf2c83466b09e5d3b4957584/lib/cocoapods/installer/analyzer.rb
[16] «человек посередине»: https://ru.wikipedia.org/wiki/%D0%90%D1%82%D0%B0%D0%BA%D0%B0_%D0%BF%D0%BE%D1%81%D1%80%D0%B5%D0%B4%D0%BD%D0%B8%D0%BA%D0%B0
[17] Проверка curl: https://github.com/Homebrew/brew/blob/837ea74f448092d822a42ff137642c0aaf076650/Library/Homebrew/extend/os/mac/diagnostic.rb
[18] проверка хэша SHA256: https://github.com/Homebrew/brew/blob/02591bdf341b4c33383b5eb537bcff0e49157a82/Library/Homebrew/utils/curl.rb
[19] можно запретить: https://github.com/Homebrew/brew/blob/9b56b133a546d0d40cd6b020290f6a59fc14729f/docs/Manpage.md
[20] нельзя использовать: https://github.com/Carthage/Carthage/blob/192a61d37b6ad27ec5d20d0d267ea3e70917689a/Source/CarthageKit/BinaryProject.swift
[21] тут: https://github.com/CocoaPods/CocoaPods/blob/master/lib/cocoapods/validator.rb
[22] некий механизм: https://github.com/apple/swift-package-manager/blob/57c5be1db1c1e12e089dff02241ffbce5722fb0e/Documentation/PackageManagerCommunityProposal.md#security-and-signing
[23] этой: https://github.com/Carthage/Carthage#getting-started
[24] Источник: https://habr.com/post/412945/?utm_source=habrahabr&utm_medium=rss&utm_campaign=412945
Нажмите здесь для печати.