Manajer ketergantungan



Dalam artikel ini saya akan memberi tahu Anda apa yang manajer paket serupa dalam struktur internal, algoritma operasi, dan apa perbedaan mendasar mereka. Saya melihat manajer paket yang dirancang untuk pengembangan di bawah iOS / OS X, tetapi konten artikel dengan beberapa asumsi berlaku untuk yang lain.

Varietas manajer ketergantungan


  • Manajer ketergantungan sistem - instal utilitas yang hilang di sistem operasi. Misalnya, Homebrew .
  • Manajer ketergantungan bahasa - mengumpulkan sumber yang ditulis dalam salah satu bahasa pemrograman ke dalam program yang dapat dieksekusi akhir. Misalnya, pergi membangun .
  • Manajer ketergantungan proyek - mengelola ketergantungan dalam konteks proyek tertentu. Yaitu, tugas mereka termasuk mendeskripsikan dependensi, mengunduh, memperbarui kode sumber mereka. Ini, misalnya, Cocoapods .

Perbedaan utama di antara mereka adalah siapa yang mereka “layani”. Sistem MH untuk pengguna, MH proyek untuk pengembang, dan MH bahasa untuk keduanya.

Selanjutnya, saya akan mempertimbangkan manajer ketergantungan proyek - kami paling sering menggunakannya, dan mereka lebih mudah dimengerti.

Skema proyek saat menggunakan manajer dependensi


Pertimbangkan Cocoapods, manajer paket populer.
Biasanya kita menjalankan perintah instal pod install , dan kemudian dependency manager melakukan segalanya untuk kita. Pertimbangkan apa yang harus terdiri dari proyek agar tim ini dapat menyelesaikan dengan sukses.



  1. Ada kode kami di mana kami menggunakan ketergantungan ini atau itu, katakanlah, perpustakaan Alamofire .
  2. Dari file manifes, manajer dependensi tahu dependensi mana yang kami gunakan dalam kode sumber. Jika kita lupa menunjukkan perpustakaan di sana, ketergantungan tidak akan ditetapkan, dan proyek pada akhirnya tidak akan dirakit.
  3. File kunci - file format tertentu yang dihasilkan oleh manajer dependensi, yang mencantumkan semua dependensi yang berhasil diinstal dalam proyek.
  4. Kode ketergantungan adalah kode sumber eksternal yang manajer "tarik" dan yang akan dipanggil dari kode kami.

Ini tidak akan mungkin terjadi tanpa algoritma spesifik yang berjalan setiap kali setelah perintah install dependensi.

Semua 4 komponen terdaftar satu demi satu, sebagai komponen selanjutnya dibentuk berdasarkan yang sebelumnya.



Tidak semua manajer dependensi memiliki semua 4 komponen, tetapi dengan mempertimbangkan fungsi manajer dependensi, kehadiran semua adalah pilihan terbaik.

Setelah menginstal dependensi, semua 4 komponen pergi ke input dari kompiler atau juru bahasa, tergantung pada bahasa.



Saya juga menarik perhatian pada kenyataan bahwa pengembang bertanggung jawab atas dua komponen pertama - kami menulis kode ini, dan manajer dependensi untuk dua komponen lainnya - ini menghasilkan file dan mengunduh kode sumber dependensi.



Alur Kerja Manajer Ketergantungan


Dengan komponen yang sedikit banyak diurutkan, sekarang mari kita beralih ke bagian algoritmik dari Departemen Kesehatan.

Algoritme kerja tipikal terlihat seperti ini:

  1. Validasi proyek dan lingkungan. Objek yang disebut Analyzer bertanggung jawab untuk ini.
  2. Membangun grafik. Dari dependensi, Departemen Kesehatan harus membuat grafik. Objek Resolver melakukan ini.
  3. Mengunduh dependensi. Jelas, kode sumber dependensi harus diunduh agar kami dapat menggunakannya di sumber kami.
  4. Integrasi Ketergantungan. Fakta bahwa kode sumber dependensi terletak di direktori tetangga pada disk mungkin tidak cukup, jadi mereka masih harus dilampirkan ke proyek kami.
  5. Pembaruan ketergantungan. Langkah ini tidak dilakukan segera setelah langkah 4, tetapi jika perlu, tingkatkan ke versi baru perpustakaan. Ada beberapa kekhasan di sini, jadi saya memilihnya secara terpisah - lebih banyak tentang mereka nanti.

Validasi proyek dan lingkungan


Validasi termasuk memeriksa versi OS, utilitas tambahan yang diperlukan oleh manajer dependensi, serta menautkan pengaturan proyek dan file manifes: dari pemeriksaan sintaks ke pengaturan yang tidak kompatibel.

Sampel 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 

Kemungkinan peringatan dan kesalahan saat memeriksa podfile:

  • Tidak ada ketergantungan yang ditemukan di salah satu repositori spec ;
  • Sistem operasi dan versi tidak ditentukan secara eksplisit;
  • Ruang kerja atau nama proyek tidak valid.

Membangun grafik ketergantungan


Karena dependensi yang disyaratkan oleh proyek kami mungkin memiliki dependensinya sendiri, dan dependensi tersebut mungkin memiliki dependensi atau sub-dependensi bersarang sendiri, manajer menggunakan versi yang benar. Secara skematis, semua dependensi sebagai hasilnya harus berbaris dalam grafik asiklik yang diarahkan .



Konstruksi grafik asiklik terarah mengurangi masalah penyortiran topologis. Dia memiliki beberapa algoritma keputusan.

  1. Algoritma Kahn - enumerasi simpul, kompleksitas O (n).
  2. Algoritma Tarjan - berdasarkan pada pencarian yang mendalam, kompleksitas O (n).
  3. Algoritma Demucron adalah partisi grafik berlapis.
  4. Algoritma paralel menggunakan jumlah prosesor polinomial. Dalam hal ini, kompleksitasnya akan “jatuh” ke O (log (n) ^ 2)

Tugas itu sendiri adalah NP-lengkap, algoritma yang sama digunakan dalam kompiler dan pembelajaran mesin.

Hasil dari solusi adalah file kunci yang dibuat, yang sepenuhnya menggambarkan hubungan antara dependensi.



Masalah apa yang mungkin muncul ketika algoritma ini bekerja? Pertimbangkan sebuah contoh: ada proyek dengan dependensi A, B, E dengan dependensi bersarang C, F, D.



Dependensi A dan B memiliki dependensi yang sama C. Dan di sini C harus memenuhi persyaratan dependensi A dan B. Beberapa manajer dependensi memungkinkan instalasi versi terpisah jika perlu, tetapi cocoapod, misalnya, tidak. Oleh karena itu, dalam hal ketidakcocokan persyaratan: A memerlukan versi yang sama dengan 2.0 dari ketergantungan C, dan B membutuhkan versi 1.0, instalasi akan gagal. Dan jika dependensi A membutuhkan versi 1.0 dan lebih tinggi untuk versi 2.0, dan dependensi B versi 1.2 atau kurang ke 1.0, versi yang paling kompatibel untuk A dan B versi 1.2 akan diinstal. Jangan lupa bahwa situasi ketergantungan siklik dapat terjadi, bahkan jika tidak secara langsung - dalam hal ini, instalasi juga akan gagal.



Mari kita lihat tampilannya dalam kode manajer dependensi paling populer untuk iOS.

Kartago


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

Penerapan Resolver ada di sini , dan NewResolver di sini , Analyzer tidak demikian.

Cocoapods


Implementasi algoritma konstruksi grafik dialokasikan ke repositori terpisah. Berikut ini adalah implementasi dari grafik dan Resolver . Di Analyzer, Anda dapat menemukan bahwa ia memeriksa konsistensi versi cocoapods dari sistem dan file kunci.

 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 

Dari sumbernya Anda juga dapat melihat bahwa Analyzer menghasilkan target untuk dependensi.

File kunci cocoapods khas terlihat seperti ini:

 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 

Bagian PODS mencantumkan dependensi langsung dan bersarang yang menunjukkan versi, kemudian checksumnya dihitung secara terpisah dan bersama-sama dan versi cocoapods yang digunakan untuk instalasi ditunjukkan.

Unduh Ketergantungan


Setelah berhasil membuat grafik dan membuat file kunci, manajer dependensi mulai mengunduhnya. Tidak harus berupa kode sumber, itu juga bisa merupakan file yang dapat dieksekusi atau kerangka kerja yang dikompilasi. Juga, semua manajer dependensi umumnya mendukung kemampuan untuk menginstal pada jalur lokal.



Tidak ada yang rumit untuk mengunduhnya dari tautan (yang, tentu saja, Anda harus dapatkan dari suatu tempat), jadi saya tidak akan memberi tahu bagaimana pengunduhan itu sendiri terjadi, tetapi fokus pada masalah sentralisasi dan keamanan.

Sentralisasi


Secara sederhana, manajer dependensi memiliki dua cara saat mengunduh dependensi:

  1. Buka beberapa daftar dependensi yang tersedia dan dapatkan tautan unduhan berdasarkan nama.
  2. Kami harus secara eksplisit menentukan sumber untuk setiap ketergantungan dalam file manifes.

Manajer ketergantungan terpusat mengikuti jalur pertama, terdesentralisasi di jalur kedua.



Keamanan


Jika Anda mengunduh dependensi melalui https atau ssh, maka Anda dapat tidur dengan tenang. Namun, pengembang sering memberikan tautan http ke perpustakaan resmi mereka. Dan di sini kita mungkin menghadapi serangan man-in-the-middle ketika penyerang memalsukan kode sumber, file yang dapat dieksekusi, atau kerangka kerja. Beberapa manajer dependensi tidak dilindungi dari ini, dan beberapa melakukannya sebagai berikut.

Homebrew

Memeriksa ikal pada versi OS X yang lebih lama.

 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 

Ada juga pemeriksaan hash SHA256 saat mengunduh melalui 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 

Dan Anda juga dapat menonaktifkan pengalihan yang tidak aman ke http (variabel HOMEBREW_NO_INSECURE_REDIRECT ).

Kartago dan Cocoapoda

Semuanya lebih sederhana di sini - Anda tidak dapat menggunakan http pada file yang dapat dieksekusi.

 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 

Kode lengkap di sini .

Manajer paket cepat

Saat ini, tidak ada yang terkait dengan keamanan dapat ditemukan, tetapi dalam proposal pengembangan ada menyebutkan singkat tentang mekanisme untuk menandatangani paket menggunakan sertifikat.

Integrasi ketergantungan


Dengan integrasi, maksud saya menghubungkan dependensi ke proyek sedemikian rupa sehingga kita dapat menggunakannya secara bebas, dan mereka dikompilasi dengan kode aplikasi utama.
Integrasi dapat berupa manual (Carthage) atau otomatis (Cocoapods). Kelebihan dari yang otomatis adalah gerakan minimal dari pihak pengembang, tetapi banyak keajaiban dapat ditambahkan ke proyek.

Diff setelah menginstal dependensi dalam suatu proyek menggunakan 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; 


Dalam hal manual, Anda, misalnya, mengikuti instruksi Carthage ini , sepenuhnya mengendalikan proses penambahan dependensi ke proyek. Dapat diandalkan, tetapi lebih lama.

Pembaruan Ketergantungan


Anda dapat mengontrol kode sumber dependensi dalam proyek menggunakan versi mereka.
Ada 3 metode yang digunakan dalam manajer dependensi:
  1. Versi perpustakaan. Cara paling nyaman dan umum. Anda dapat menentukan versi dan interval tertentu. Ini adalah cara yang sepenuhnya dapat diprediksi untuk mendukung kompatibilitas dependensi, asalkan penulis membuat versi perpustakaan dengan benar.
  2. Cabang Saat memperbarui cabang dan memperbarui ketergantungan, kami tidak dapat memprediksi perubahan apa yang akan terjadi.
  3. Komit atau tag. Ketika perintah pembaruan dijalankan, dependensi dengan tautan ke komit atau tag tertentu (jika tidak diubah) tidak akan pernah diperbarui.

Kesimpulan


Dalam artikel tersebut saya memberikan pemahaman yang dangkal tentang struktur internal manajer ketergantungan. Jika Anda ingin tahu lebih banyak, Anda harus mempelajari kode sumber manajer paket. Cara termudah untuk menemukan yang ditulis dalam bahasa yang akrab. Skema yang dideskripsikan adalah tipikal, tetapi dalam manajer ketergantungan tertentu sesuatu mungkin hilang atau, sebaliknya, yang baru mungkin muncul.
Komentar dan diskusi dalam komentar dipersilahkan.

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


All Articles