Gestionnaires de dépendance



Dans cet article, je vais vous expliquer en quoi les gestionnaires de packages sont similaires en termes de structure interne, d'algorithme de fonctionnement et quelles sont leurs différences fondamentales. J'ai examiné les gestionnaires de packages conçus pour le développement sous iOS / OS X, mais le contenu de l'article avec certaines hypothèses s'applique à d'autres.

Variétés de gestionnaires de dépendances


  • Gestionnaires de dépendances système - installez les utilitaires manquants dans le système d'exploitation. Par exemple, Homebrew .
  • Gestionnaires de dépendance linguistique - collectez les sources écrites dans l'un des langages de programmation dans les programmes exécutables finaux. Par exemple, allez construire .
  • Gestionnaires de dépendances de projet - gérez les dépendances dans le contexte d'un projet spécifique. Autrement dit, leurs tâches comprennent la description des dépendances, le téléchargement, la mise à jour de leur code source. Ce sont, par exemple, les Cocoapods .

La principale différence entre eux est qui ils «servent». Système MH pour les utilisateurs, MH du projet pour les développeurs et MH du langage pour les deux.

Ensuite, je considérerai les gestionnaires de dépendance de projet - nous les utilisons le plus souvent et ils sont plus faciles à comprendre.

Le schéma du projet lors de l'utilisation du gestionnaire de dépendances


Considérez Cocoapods, un gestionnaire de paquets populaire.
Habituellement, nous exécutons la commande conditionnelle d' installation de pod , puis le gestionnaire de dépendances fait tout pour nous. Réfléchissez à la composition du projet pour que cette équipe réussisse.



  1. Il y a notre code dans lequel nous utilisons telle ou telle dépendance, disons la bibliothèque Alamofire .
  2. À partir du fichier manifeste, le gestionnaire de dépendances sait quelles dépendances nous utilisons dans le code source. Si nous oublions d'indiquer une bibliothèque là-bas, la dépendance ne sera pas établie et le projet ne sera finalement pas assemblé.
  3. Fichier de verrouillage - un fichier d'un certain format généré par le gestionnaire de dépendances, qui répertorie toutes les dépendances installées avec succès dans le projet.
  4. Le code de dépendance est le code source externe que le gestionnaire de dépendances "extrait" et qui sera appelé à partir de notre code.

Cela n'aurait pas été possible sans un algorithme spécifique qui s'exécute à chaque fois après la commande d'installation de dépendance.

Les 4 composants sont répertoriés l'un après l'autre, comme le composant suivant est formé sur la base du précédent.



Tous les gestionnaires de dépendances n'ont pas les 4 composants, mais compte tenu des fonctions du gestionnaire de dépendances, la présence de tous est la meilleure option.

Après l'installation des dépendances, les 4 composants vont tous à l'entrée du compilateur ou de l'interpréteur, selon la langue.



J'attire également l'attention sur le fait que les développeurs sont responsables des deux premiers composants - nous écrivons ce code et le gestionnaire de dépendances pour les deux autres composants - il génère le (s) fichier (s) et télécharge le code source des dépendances.



Flux de travail de Dependency Manager


Avec les composants plus ou moins triés, passons maintenant à la partie algorithmique du MOH.

Un algorithme de travail typique ressemble à ceci:

  1. Validation du projet et de l'environnement. L'objet appelé Analyzer en est responsable.
  2. Construire un graphique. Parmi les dépendances, le ministère de la Santé devrait construire un graphique. L'objet Resolver le fait.
  3. Téléchargement des dépendances. Évidemment, le code source des dépendances doit être téléchargé pour que nous puissions l'utiliser dans nos sources.
  4. Intégration des dépendances. Le fait que le code source des dépendances se trouve dans un répertoire voisin sur le disque peut ne pas être suffisant, donc elles doivent encore être attachées à notre projet.
  5. Mise à jour des dépendances. Cette étape n'est pas effectuée immédiatement après l'étape 4, mais si nécessaire, effectuez une mise à niveau vers la nouvelle version des bibliothèques. Il y a quelques particularités ici, alors je les ai distinguées dans une étape distincte - plus à leur sujet plus tard.

Validation du projet et de l'environnement


La validation comprend la vérification des versions du système d'exploitation, les utilitaires auxiliaires requis par le gestionnaire de dépendances, ainsi que la liaison des paramètres de projet et des fichiers manifestes: de la vérification de la syntaxe aux paramètres incompatibles.

Exemple de fichier pod

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 

Avertissements et erreurs possibles lors de la vérification du podfile:

  • Aucune dépendance trouvée dans aucun des référentiels de spécifications ;
  • Le système d'exploitation et la version ne sont pas explicitement spécifiés;
  • Espace de travail ou nom de projet non valide.

Création d'un graphe de dépendances


Étant donné que les dépendances requises par notre projet peuvent avoir leurs propres dépendances, et que celles-ci peuvent à leur tour avoir leurs propres dépendances ou sous-dépendances imbriquées, le gestionnaire a utilisé les versions correctes. Schématiquement, toutes les dépendances en conséquence doivent s'aligner dans un graphique acyclique dirigé .



La construction d'un graphe acyclique dirigé se réduit au problème du tri topologique. Elle possède plusieurs algorithmes de décision.

  1. Algorithme de Kahn - énumération des sommets, complexité O (n).
  2. Algorithme de Tarjan - basé sur une recherche approfondie, complexité O (n).
  3. L'algorithme de Demucron est une partition graphique en couches.
  4. Algorithmes parallèles utilisant un nombre polynomial de processeurs. Dans ce cas, la complexité «tombera» en O (log (n) ^ 2)

La tâche elle-même est NP-complete; le même algorithme est utilisé dans les compilateurs et l'apprentissage automatique.

Le résultat de la solution est le fichier de verrouillage créé, qui décrit entièrement la relation entre les dépendances.



Quels problèmes peuvent survenir lorsque cet algorithme fonctionne? Prenons un exemple: il existe un projet avec les dépendances A, B, E avec les dépendances imbriquées C, F, D.



Les dépendances A et B ont une dépendance commune C. Et ici, C doit satisfaire aux exigences des dépendances A et B. Certains gestionnaires de dépendances permettent l'installation de versions distinctes si nécessaire, mais les cocoapods, par exemple, ne le font pas. Par conséquent, en cas d'incompatibilité des exigences: A nécessite une version égale à 2.0 de la dépendance C, et B nécessite la version 1.0, l'installation échouera. Et si les dépendances A nécessitent la version 1.0 et supérieure à la version 2.0, et les dépendances B version 1.2 ou inférieure à 1.0, la version la plus compatible pour A et B version 1.2 sera installée. N'oubliez pas qu'une situation de dépendance cyclique peut se produire, même si ce n'est pas directement - dans ce cas, l'installation échouera également.



Voyons à quoi cela ressemble dans le code des gestionnaires de dépendances les plus populaires pour 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> } 

L'implémentation de Resolver est ici , et NewResolver est ici , Analyzer en tant que tel ne l'est pas.

Cocoapods


L'implémentation de l'algorithme de construction de graphe est allouée à un référentiel séparé. Voici l'implémentation du graphe et du résolveur . Dans Analyzer, vous pouvez constater qu'il vérifie la cohérence des versions cocoapods du système et du fichier de verrouillage.

 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 

Depuis la source, vous pouvez également voir que Analyzer génère des cibles pour les dépendances.

Un fichier de verrouillage cocoapods typique ressemble à ceci:

 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 

La section PODS répertorie les dépendances directes et imbriquées indiquant les versions, puis leurs sommes de contrôle sont calculées séparément et ensemble et la version des cocoapods qui a été utilisée pour l'installation est indiquée.

Télécharger les dépendances


Après avoir correctement créé le graphique et créé le fichier de verrouillage, le gestionnaire de dépendances procède à leur téléchargement. Ce ne sera pas nécessairement le code source, il peut également s'agir de fichiers exécutables ou de frameworks compilés. En outre, tous les gestionnaires de dépendances prennent généralement en charge la possibilité d'installer sur un chemin local.



Il n'y a rien de compliqué à télécharger à partir du lien (que, bien sûr, vous devez obtenir de quelque part), donc je ne dirai pas comment le téléchargement lui-même se produit, mais concentrez-vous sur les questions de centralisation et de sécurité.

Centralisation


En termes simples, le gestionnaire de dépendances a deux manières lors du téléchargement de dépendances:

  1. Accédez à une liste de dépendances disponibles et obtenez un lien de téléchargement par nom.
  2. Nous devons explicitement spécifier la source de chaque dépendance dans le fichier manifeste.

Les gestionnaires de dépendance centralisés suivent la première voie, décentralisés la seconde.



La sécurité


Si vous téléchargez des dépendances via https ou ssh, vous pouvez dormir paisiblement. Cependant, les développeurs fournissent souvent des liens http vers leurs bibliothèques officielles. Et ici, nous pouvons rencontrer une attaque man-in-the-middle lorsqu'un attaquant usurpe le code source, le fichier exécutable ou le framework. Certains gestionnaires de dépendances ne sont pas protégés contre cela, et certains le font comme suit.

Homebrew

Vérification de curl sur les anciennes versions d'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 

Il existe également une vérification du hachage SHA256 lors du téléchargement via 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 

Et vous pouvez également désactiver les redirections non sécurisées vers http (variable HOMEBREW_NO_INSECURE_REDIRECT ).

Carthage et Cocoapods

Tout est plus simple ici - vous ne pouvez pas utiliser http sur des fichiers exécutables.

 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 

Code complet ici .

Gestionnaire de paquets Swift

Pour le moment, rien de ce qui concerne la sécurité n'a pu être trouvé, mais dans les propositions de développement, il est fait brièvement mention d' un mécanisme de signature des packages à l'aide de certificats.

Intégration des dépendances


Par intégration, j'entends la connexion des dépendances au projet de manière à ce que nous puissions les utiliser librement, et elles sont compilées avec le code d'application principal.
L'intégration peut être manuelle (Carthage) ou automatique (Cocoapods). Les avantages d'un automatique sont un minimum de gestes de la part du développeur, mais beaucoup de magie peut être ajoutée au projet.

Diff après l'installation des dépendances dans un projet utilisant des 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; 


Dans le cas du manuel, vous, par exemple, en suivant cette instruction Carthage, contrôlez entièrement le processus d'ajout de dépendances au projet. Fiable, mais plus long.

Mise à jour des dépendances


Vous pouvez contrôler le code source des dépendances dans le projet à l'aide de leurs versions.
Il existe 3 méthodes utilisées dans les gestionnaires de dépendances:
  1. Versions de la bibliothèque. La manière la plus pratique et la plus courante. Vous pouvez spécifier à la fois une version spécifique et un intervalle. C'est un moyen complètement prévisible de prendre en charge la compatibilité des dépendances, à condition que les auteurs aient correctement versionné les bibliothèques.
  2. Branche. Lors de la mise à jour d'une branche et de la mise à jour d'une dépendance, nous ne pouvons pas prédire quels changements se produiront.
  3. Validez ou taggez. Lorsque la commande update est exécutée, les dépendances avec des liens vers une validation ou une balise spécifique (si elle n'est pas modifiée) ne seront jamais mises à jour.

Conclusion


Dans l'article, j'ai donné une compréhension superficielle de la structure interne des gestionnaires de dépendance. Si vous voulez en savoir plus, vous devriez vous plonger dans le code source du gestionnaire de paquets. Le moyen le plus simple d'en trouver un qui est écrit dans une langue familière. Le schéma décrit est typique, mais dans un gestionnaire de dépendance particulier, quelque chose peut être manquant ou, au contraire, un nouveau peut apparaître.
Les commentaires et discussions dans les commentaires sont les bienvenus.

Source: https://habr.com/ru/post/fr412945/


All Articles