Gerentes de dependência



Neste artigo, mostrarei como os gerenciadores de pacotes são semelhantes em estrutura interna, algoritmo de operação e quais são suas diferenças fundamentais. Analisei os gerenciadores de pacotes projetados para desenvolvimento no iOS / OS X, mas o conteúdo do artigo com algumas suposições se aplica a outras.

Variedades de gerenciadores de dependência


  • Gerenciadores de dependência do sistema - instale os utilitários ausentes no sistema operacional. Por exemplo, Homebrew .
  • Gerenciadores de dependência de idiomas - colete fontes gravadas em uma das linguagens de programação em programas executáveis ​​finais. Por exemplo, vá construir .
  • Gerentes de dependência de projeto - gerencie dependências no contexto de um projeto específico. Ou seja, suas tarefas incluem descrever dependências, fazer download, atualizar seu código fonte. Estes são, por exemplo, Cocoapods .

A principal diferença entre eles é quem eles "servem". Sistema MH para usuários, MH do projeto para desenvolvedores e MH do idioma para ambos.

A seguir, considerarei os gerentes de dependência do projeto - nós os usamos com mais frequência e são mais fáceis de entender.

O esquema do projeto ao usar o gerenciador de dependência


Considere o Cocoapods, um popular gerenciador de pacotes.
Geralmente, executamos o comando cond pod pod install e, em seguida, o gerenciador de dependências faz tudo por nós. Considere o que o projeto deve consistir para que essa equipe seja concluída com êxito.



  1. Existe nosso código no qual usamos essa ou aquela dependência, digamos, na biblioteca Alamofire .
  2. No arquivo de manifesto, o gerenciador de dependências sabe quais dependências usamos no código-fonte. Se esquecermos de indicar qualquer biblioteca, a dependência não será estabelecida e o projeto não será montado.
  3. Arquivo de bloqueio - um arquivo de um determinado formato gerado pelo gerenciador de dependências, que lista todas as dependências instaladas com sucesso no projeto.
  4. Código de dependência é o código-fonte externo que o gerente de dependência "extrai" e que será chamado a partir do nosso código.

Isso não seria possível sem um algoritmo específico que é executado todas as vezes após o comando install de dependência.

Todos os 4 componentes são listados um após o outro, como o próximo componente é formado com base no anterior.



Nem todos os gerenciadores de dependência têm todos os 4 componentes, mas, levando em consideração as funções do gerenciador de dependência, a presença de todos é a melhor opção.

Após instalar as dependências, todos os 4 componentes vão para a entrada do compilador ou intérprete, dependendo do idioma.



Também chamo a atenção para o fato de os desenvolvedores serem responsáveis ​​pelos dois primeiros componentes - nós escrevemos esse código e o gerenciador de dependências dos dois componentes restantes - ele gera o (s) arquivo (s) e baixa o código fonte das dependências.



Fluxo de Trabalho do Dependency Manager


Com os componentes mais ou menos resolvidos, agora vamos para a parte algorítmica do MOH.

Um algoritmo de trabalho típico se parece com isso:

  1. Validação do projeto e do meio ambiente. O objeto chamado Analyzer é responsável por isso.
  2. Construindo um gráfico. Das dependências, o Ministério da Saúde deve construir um gráfico. O objeto Resolver faz isso.
  3. Baixando dependências. Obviamente, o código fonte das dependências deve ser baixado para que possamos usá-lo em nossas fontes.
  4. Integração de Dependências. O fato de o código fonte das dependências estar em um diretório vizinho no disco pode não ser suficiente, portanto elas ainda precisam ser anexadas ao nosso projeto.
  5. Atualização de dependência. Esta etapa não é executada imediatamente após a etapa 4, mas, se necessário, atualize para a nova versão das bibliotecas. Existem algumas peculiaridades aqui, então eu as destaquei em uma etapa separada - mais sobre elas mais tarde.

Validação do projeto e do meio ambiente


A validação inclui a verificação de versões do SO, utilitários auxiliares exigidos pelo gerenciador de dependências, além de vincular as configurações do projeto e os arquivos de manifesto: da verificação de sintaxe a configurações incompatíveis.

Podfile de amostra

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 

Possíveis avisos e erros ao verificar o podfile:

  • Nenhuma dependência encontrada em nenhum dos repositórios de especificações ;
  • O sistema operacional e a versão não são especificados explicitamente;
  • Espaço de trabalho ou nome do projeto inválido.

Construindo um gráfico de dependência


Como as dependências exigidas pelo nosso projeto podem ter suas próprias dependências e as que, por sua vez, podem ter suas próprias dependências ou subdependências aninhadas, o gerente usou as versões corretas. Esquematicamente, todas as dependências como resultado devem estar alinhadas em um gráfico acíclico direcionado .



A construção de um gráfico acíclico direcionado reduz-se ao problema da classificação topológica. Ela tem vários algoritmos de decisão.

  1. Algoritmo de Kahn - enumeração de vértices, complexidade O (n).
  2. Algoritmo de Tarjan - baseado em uma pesquisa profunda, complexidade O (n).
  3. O algoritmo de Demucron é uma partição gráfica em camadas.
  4. Algoritmos paralelos usando um número polinomial de processadores. Nesse caso, a complexidade "cairá" para O (log (n) ^ 2)

A tarefa em si é NP-completa; o mesmo algoritmo é usado em compiladores e aprendizado de máquina.

O resultado da solução é o arquivo de bloqueio criado, que descreve completamente o relacionamento entre dependências.



Quais problemas podem surgir quando esse algoritmo funciona? Considere um exemplo: existe um projeto com dependências A, B, E com dependências aninhadas C, F, D.



As dependências A e B têm uma dependência comum C. E aqui C deve atender aos requisitos das dependências A e B. Alguns gerenciadores de dependências permitem a instalação de versões separadas, se necessário, mas os cocoapods, por exemplo, não. Portanto, no caso de incompatibilidade de requisitos: A requer uma versão igual a 2.0 da dependência C e B requer a versão 1.0, a instalação falhará. E se as dependências A precisarem da versão 1.0 e posterior à versão 2.0 e as dependências B versão 1.2 ou inferior à 1.0, a versão mais compatível para as versões A e B 1.2 será instalada. Não esqueça que uma situação de dependência cíclica pode ocorrer, mesmo que não diretamente - nesse caso, a instalação também falhará.



Vamos ver como fica o código dos gerenciadores de dependência mais populares para iOS.

Cartago


 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> } 

A implementação do Resolver está aqui e NewResolver está aqui , o Analyzer, como tal, não está.

Cocoapods


A implementação do algoritmo de construção de gráfico é alocada para um repositório separado. Aqui está a implementação do gráfico e do Resolver . No Analyzer, você pode verificar que ele verifica a consistência das versões dos cocoapods do sistema e do arquivo de bloqueio.

 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 

Na fonte, você também pode ver que o Analyzer gera destinos para dependências.

Um arquivo de bloqueio típico de cocoapods se parece com isso:

 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 

A seção PODS lista dependências diretas e aninhadas, indicando as versões, suas somas de verificação são calculadas separadamente e juntas e a versão dos cocoapods que foi usada para instalação é indicada.

Dependências de download


Após compilar com sucesso o gráfico e criar o arquivo de bloqueio, o gerenciador de dependências continua o download. Não necessariamente será o código fonte, também pode ser arquivos executáveis ​​ou estruturas compiladas. Além disso, todos os gerenciadores de dependência geralmente oferecem suporte à capacidade de instalar em um caminho local.



Não há nada complicado para baixá-los a partir do link (que, é claro, você precisa obter de algum lugar), então não vou contar como o download em si acontece, mas focar nos problemas de centralização e segurança.

Centralização


Em termos simples, o gerenciador de dependências possui duas maneiras de baixar dependências:

  1. Vá para uma lista de dependências disponíveis e obtenha um link de download por nome.
  2. Nós devemos especificar explicitamente a fonte de cada dependência no arquivo de manifesto.

Os gerentes de dependência centralizados seguem o primeiro caminho, descentralizados no segundo.



Segurança


Se você baixar dependências via https ou ssh, poderá dormir em paz. No entanto, os desenvolvedores geralmente fornecem links http para suas bibliotecas oficiais. E aqui podemos encontrar um ataque intermediário quando um invasor falsifica o código-fonte, o arquivo executável ou a estrutura. Alguns gerenciadores de dependência não estão protegidos disso e outros o fazem da seguinte maneira.

Homebrew

Verificando a ondulação nas versões mais antigas do 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 

Há também uma verificação de hash SHA256 ao fazer o download 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 

E você também pode desativar os redirecionamentos não seguros para http (variável HOMEBREW_NO_INSECURE_REDIRECT ).

Cartago e Cocoapods

Tudo é mais simples aqui - você não pode usar http em arquivos executáveis.

 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 

Código completo aqui .

Gerenciador de pacotes Swift

No momento, nada relacionado à segurança foi encontrado, mas nas propostas de desenvolvimento há uma breve menção de um mecanismo para assinar pacotes usando certificados.

Integração de dependência


Por integração, quero dizer conectar dependências ao projeto de forma que possamos usá-las livremente, e elas são compiladas com o código principal do aplicativo.
A integração pode ser manual (Cartago) ou automática (Cocoapods). As vantagens de um automático são um mínimo de gestos por parte do desenvolvedor, mas muita mágica pode ser adicionada ao projeto.

Difere após instalar dependências em um projeto usando 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; 


No caso do manual, você, por exemplo, seguindo esta instrução de Cartago, controla totalmente o processo de adição de dependências ao projeto. Confiável, mas por mais tempo.

Atualização de Dependência


Você pode controlar o código fonte das dependências no projeto usando suas versões.
Existem 3 métodos usados ​​nos gerenciadores de dependência:
  1. Versões da biblioteca. A maneira mais conveniente e comum. Você pode especificar uma versão específica e um intervalo. É uma maneira completamente previsível de oferecer suporte à compatibilidade de dependências, desde que os autores tenham feito a versão correta das bibliotecas.
  2. Branch. Ao atualizar uma ramificação e atualizar mais uma dependência, não podemos prever quais mudanças ocorrerão.
  3. Confirmar ou marcar. Quando o comando update é executado, as dependências com links para uma confirmação ou marca específica (se não for alterada) nunca serão atualizadas.

Conclusão


No artigo, dei uma compreensão superficial da estrutura interna dos gerentes de dependência. Se você quiser saber mais, deve investigar o código-fonte do gerenciador de pacotes. A maneira mais fácil de encontrar uma escrita em um idioma familiar. O esquema descrito é típico, mas em um gerenciador de dependências específico, algo pode estar faltando ou, pelo contrário, um novo pode aparecer.
Comentários e discussões nos comentários são bem-vindos.

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


All Articles