依赖经理



在本文中,我将告诉您程序包管理器在内部结构,操作算法上有哪些相似之处,以及它们之间的根本区别是什么。 我查看了专为在iOS / OS X下进行开发而设计的程序包管理器,但是本文的内容带有一些假设,而其他情况也适用。

各种依赖管理器


  • 系统依赖性管理器-在操作系统中安装缺少的实用程序。 例如, Homebrew
  • 语言依赖管理器-将以一种编程语言编写的源代码收集到最终的可执行程序中。 例如, 转到build
  • 项目依赖性管理器-在特定项目的上下文中管理依赖性。 也就是说,他们的任务包括描述依赖关系,下载,更新其源代码。 这些是例如Cocoapods

他们之间的主要区别是他们“服务谁”。 为用户提供系统MH,为开发人员提供项目MH,并为两者提供语言MH。

接下来,我将考虑项目依赖管理器-我们最常使用它们,而且它们更易于理解。

使用依赖项管理器时的项目方案


考虑一下流行的包装经理 Cocoapods。
通常,我们执行条件pod install命令,然后依赖项管理器会为我们做所有事情。 考虑项目应该包括什么,以使该团队成功完成。



  1. 在我们的代码中,我们使用了这种依赖性,即Alamofire库。
  2. 从清单文件中,依赖项管理器知道我们在源代码中使用了哪些依赖项。 如果我们忘记指出那里的任何库,则将不会建立依赖关系,并且最终将不会组装项目。
  3. 锁定文件-由依赖项管理器生成的某种格式的文件,列出了成功安装在项目中的所有依赖项。
  4. 依赖关系代码是依赖关系管理器“提取”的外部源代码,将从我们的代码中调用它。

没有在依赖项安装命令之后每次都运行的特定算法,这将是不可能的。

依次列出所有4个组件,例如 下一个组件是在前一个组件的基础上形成的。



并非所有的依赖项管理器都具有全部4个组件,但是考虑到依赖项管理器的功能,最好是全部存在。

安装依赖项后,根据语言,所有4个组件都进入编译器或解释器的输入。



我还注意到,开发人员负责前两个组件-我们编写此代码,其余两个组件负责依赖管理器-它生成文件并下载依赖项的源代码。



依赖管理器工作流程


随着组件的整理或多或少,现在让我们继续进行MOH的算法部分。

典型的工作算法如下所示:

  1. 验证项目和环境。 名为分析器的对象对此负责。
  2. 建立图。 在依赖关系中,卫生部应绘制图表。 解析器对象执行此操作。
  3. 下载依赖项。 显然,必须下载依赖项的源代码,以便我们在源代码中使用它。
  4. 依赖关系集成。 依赖项的源代码位于磁盘上的相邻目录中这一事实可能还不够,因此仍需要将它们附加到我们的项目中。
  5. 依赖关系更新。 步骤4之后不会立即执行此步骤,但如有必要,请升级到新版本的库。 这里有一些特殊之处,所以我在一个单独的步骤中将它们选出来-稍后再详细介绍。

验证项目和环境


验证包括检查OS版本,依赖项管理器所需的辅助实用程序,以及链接项目设置和清单文件:从语法检查到不兼容的设置。

样本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时可能的警告和错误:

  • 在任何规范存储库中都没有找到依赖项;
  • 没有明确指定操作系统和版本;
  • 无效的工作空间或项目名称。

建立依赖图


由于我们项目所需的依赖项可能具有自己的依赖项,而这些依赖项又可能具有自己的嵌套依赖项或子依赖项,因此管理器使用了正确的版本。 从原理上讲,所有依赖关系都应在有向无环图中对齐



有向无环图的构造减少了拓扑排序的问题。 她有几种决策算法。

  1. 卡恩算法 -顶点枚举,复杂度O(n)。
  2. Tarjan的算法 -基于深度搜索,复杂度O(n)。
  3. Demucron的算法是分层图分区。
  4. 使用多项式处理器的并行算法。 在这种情况下,复杂度将“降至” O(log(n)^ 2)

任务本身是NP完成的;在编译器和机器学习中使用相同的算法。

解决方案的结果是创建了锁定文件,该文件完整描述了依赖关系之间的关系。



该算法工作时可能会出现什么问题? 考虑一个示例:有一个项目具有依赖项A,B,E和嵌套的依赖项C,F,D。



依赖项A和B具有共同的依赖项C。这里,C必须满足依赖项A和B的要求。某些依赖项管理器允许在必要时安装单独的版本,但例如cocoapods则不允许。 因此,在需求不兼容的情况下:A要求版本等于C依赖性的2.0,而B要求版本1.0,则安装将失败。 并且,如果依赖项A需要版本1.0或更高版本至版本2.0,并且依赖项B版本1.2或更低版本至1.0,则将安装A和B版本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的实现在这里 ,NewResolver在这里 ,Analyzer不在。

椰子足


图构造算法的实现被分配给一个独立的仓库 。 这是解析器的实现 。 在分析器中,您可以找到它检查系统的cocoapods版本和锁定文件的一致性。

 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为依赖关系生成了目标。

一个典型的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部分列出了指示版本的直接和嵌套依赖项,然后分别并一起计算它们的校验和,并指示用于安装的可可足类动物的版本。

下载依赖项


成功构建图形并创建锁定文件后,依赖关系管理器将继续下载它们。 不一定是源代码,也可以是可执行文件或编译框架。 同样,所有依赖项管理器通常都支持在本地路径上安装的功能。



从链接下载它们没有什么复杂的事情(当然,您需要从某个地方下载),因此我不会告诉您下载本身是如何发生的,而是关注集中化和安全性问题。

集中化


简单来说,依赖性管理器在下载依赖性时有两种方法:

  1. 转到一些可用依赖项列表,并按名称获取下载链接。
  2. 我们必须在清单文件中为每个依赖项明确指定源。

集中式依赖管理器沿着第一个路径,去中心化沿着第二个路径。



安全性


如果通过https或ssh下载依赖项,则可以安然入睡。 但是,开发人员通常会提供指向其官方库的http链接。 当攻击者欺骗源代码,可执行文件或框架时,我们可能会遇到中间人攻击。 有些依赖项管理器不受此保护,有些则按以下方式进行保护。

家酿

检查旧版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 

通过http下载时,还会进行SHA256哈希检查

 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变量)。

迦太基和可可足

这里的一切都比较简单- 您不能在可执行文件上使用 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 

完整代码在这里

迅捷包管理器

目前,未找到与安全性相关的任何信息,但是在开发建议中,仅简短提及使用证书软件包进行签名的机制

依赖整合


通过集成,我的意思是将依赖项连接到项目,以便我们可以自由使用它们,并使用主应用程序代码对其进行编译。
集成可以是手动(迦太基)或自动(Cocoapods)。 自动操作的优点是开发人员的手势很少,但是可以向项目添加很多魔术。

使用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; 


例如,在使用手册的情况下,您可以按照迦太基指令完全控制将依赖项添加到项目的过程。 可靠,但是更长。

依赖关系更新


您可以使用其版本控制项目中依赖项的源代码。
依赖项管理器使用3种方法:
  1. 库版本。 最方便,最常用的方式。 您可以指定特定版本和间隔。 只要作者正确地对库进行了版本控制,这是支持依赖项兼容性的一种完全可预测的方法。
  2. 分公司 在更新分支并进一步更新依赖项时,我们无法预测会发生什么变化。
  3. 提交或标记。 当执行update命令时,带有指向特定提交或标记(如果未更改)链接的依赖关系将永远不会更新。

结论


在本文中,我对依赖项管理器的内部结构有一个肤浅的理解。 如果您想了解更多信息,则应深入研究软件包管理器的源代码。 查找以熟悉的语言编写的内容的最简单方法。 所描述的方案是典型的,但在特定的依赖项管理器中,可能会丢失某些内容,或者相反,可能会出现新的内容。
欢迎发表评论并在评论中进行讨论。

Source: https://habr.com/ru/post/zh-CN412945/


All Articles