如何实现一个最简单的包管理系统
综述
本文是在学习python的过程中的一个简单的练手小项目,用于熟悉python内部的一些机制,全部的代码位于 tiny package manager。
整体的逻辑是,当我们在编译一个 yarn 项目的过程中,我们会存在一个 package.json
文件,在文件中我们会声明项目的所有依赖。我们通过该依赖文件来进行一个包依赖分析,判断是否项目是否存在版本冲突,如果可以编译,则给出选择对应的版本。
例如:
--- title: dependencies --- flowchart LR jest("jest"):::pink jest2("0.0.61"):::green jest1("0.0.71"):::green express-resource("express-resource-*"):::purple underscore("underscore-*"):::purple sji("sji-*"):::purple express("express-*"):::purple api-easy("api-easy-*"):::purple mongoose("mongoose-*"):::purple indirect01("indirect dependency ..."):::animate jest --> jest1 jest --> jest2 jest1 --> express-resource --> indirect01 jest1 --> underscore --> indirect01 jest1 --> sji --> indirect01 jest2 --> express --> indirect01 jest2 --> api-easy --> indirect01 jest2 --> mongoose --> indirect01 jest2 --> express-resource classDef pink 1,fill:#FFCCCC,stroke:#333, color: #fff, font-weight:bold; classDef green fill: #696,color: #fff,font-weight: bold; classDef purple fill:#969,stroke:#333, font-weight: bold; classDef error fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5 classDef coral fill:#f9f,stroke:#333,stroke-width:4px; classDef animate stroke-dasharray: 9,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
涉及模块
- uv
- Classes
- Inheritance
- http
- File and Directory Access
- python-semanticversion
- json — JSON encoder and decoder
参考文档
逻辑描述
在依赖分析的过程中,我们复杂度是指数级的,所以完全没有办法进行暴力破解。假设:
- M 个直接依赖(每个依赖的版本约束为
C_i
) - 每个依赖有 N 个候选版本(实际可能不同,但简化为 N)
最低复杂度 = 验证所有可能的版本组合
每个直接依赖需选择 1 个版本,总组合数为 N^M(每个依赖有 N 种选择,M 个依赖的组合数为 N 的 M 次方)。
因此,**理论最低复杂度为 O (N^M)**(指数级),而非 O (M×N)。
现代依赖解析算法通过以下优化降低复杂度:
- 版本约束预筛选
- 依赖图剪枝
- 缓存与增量解析
版本约束预筛选
- 核心:在遍历候选版本前,先排除不满足直接约束的版本。
- 示例:若依赖 A 的约束为
^1.0.0
,而可用版本为[0.9.0, 1.0.0, 1.1.0, 2.0.0]
,则直接排除0.9.0
和2.0.0
,候选数从 N 降为 K(K ≤ N)。 - 复杂度:优化为 O (K^M),但仍为指数级。
依赖图剪枝
- 核心:若某个依赖的版本选择导致后续冲突,立即回溯,避免遍历其所有子依赖。
- 示例:若选择 A@1.0.0 后发现其依赖的 B@2.0.0 与其他约束冲突,则不再尝试 A@1.0.0 的子依赖组合。
- 复杂度:平均情况下接近 O (M×K)(假设每个依赖的有效候选版本为 K),但最坏情况仍为 O (K^M)。
缓存与增量解析
- 核心:缓存已验证的依赖组合,避免重复计算。
- 示例:若已验证 A@1.0.0 + B@2.0.0 有效,则下次遇到相同组合时直接复用结果。
- 复杂度:理想情况下可降至 O (M)(假设所有依赖已缓存),但实际受缓存命中率影响。
复杂度对比
场景 | 复杂度 | 说明 |
---|---|---|
无优化的暴力枚举 | O(N^M) | 遍历所有可能的版本组合,指数级复杂度,不可行。 |
版本约束预筛选 + 剪枝 | O(K^M) | K 为每个依赖的有效候选版本数(K ≤ N),仍为指数级,但实际 K 可能很小。 |
缓存 + 增量解析 | 接近 O (M) | 理想情况下,但依赖高度重复时才能达到。 |
npm/yarn 实际复杂度 | 平均 O (M×K) | 通过扁平化、锁文件等优化,接近线性复杂度,但最坏情况仍可能退化。 |
如何实现?
逻辑描述
需要实现包管理算法,流程如下:
- 有一个初始的配置文件,声明了依赖的包和版本;
- 在第一份配置文件中,扫描所有的依赖;
- 随后拉取扫描到的所有依赖的依赖并继续检查;
- 直到所有的依赖都判定成功
实现的逻辑如下:
假设我们存在三个直接依赖,分别是
direct_1, direct_2, direct_3
这三个包都有自己的依赖。
首选我们选择 direct_1 的某个版本,此时,我们可以知道 direct_1 所有的包约束条件;
我们维护以下数据结构:
- 依赖的版本到包约束条件的映射关系,例如:direct_1的0.0.1的版本,只能使用 indirect_1 的 >1.0.0 的版本;
- 已经选择的包;
- 等待选择的包;
我们的逻辑就是,不断的更新已经选择的包和等待选择的包,当等待选择的包为空时我们的依赖解析结束;或者当我们在解析中发现不兼容的包时,解析失败。
整体的执行逻辑如下:
- 将三个直接依赖添加到等待选择的包中;
- 选择第一个包的最大版本,并执行以下操作:
- 将这个包添加到已经选择的包
- 将这个包移除出等待选择的包;
- 将这个包的依赖,也就是间接依赖添加到等到选择的包的末尾;
- 递归的去选择后面的包:
- 判断这个包是否符合前面的约束条件,如果不满足约束直接选择该包的下一个版本;
- 如果当前包的所有版本全部尝试过了,没有找到满足条件的包,再次回退到前一个包,选择前一个包的下一个版本;
- 如果回退到所有的包都尝试过并且没有找到符合条件的包,则解析失败;
- 如果等待选择的包为空,则此时已经选择的包就是我们找到的解析结果。
伪代码
1 | def find(manifest: dict, remaining: list, accumulator: list, current: list, count: int) -> int: |
数据结构
在我们的实际执行过程中,当我们选中某一个特定的包时,我们必须知道选择的包的依赖,**而这个依赖就是我们引入的间接依赖
**。当间接依赖和我们已经选择的依赖冲突时,我们是无法正常编译的!所以,我们必须存在一个如下的依赖查询文件,格式如下:
1 | { |
而整体的数据结构描述如下:从这个图我们可以查询到:jest
的 0.0.1
版本依赖于:
babel
的[(0.0.2), (0.0.3)]
express
的[(0.0.2)]
--- title: dependencies --- flowchart LR package_name("jest"):::pink package_version_1("0.0.1"):::green package_version_2("0.0.2"):::green package_version_3("0.0.3"):::green express("express"):::pink babel("babel"):::pink version_1("0.0.2"):::green version_2("0.0.3"):::green package_name --> package_version_1 package_name --> package_version_2 package_name --> package_version_3 package_version_1 --> express package_version_1 --> babel express --> version_1 express --> version_2 babel --> version_1 classDef pink 1,fill:#FFCCCC,stroke:#333, color: #fff, font-weight:bold; classDef green fill: #696,color: #fff,font-weight: bold; classDef purple fill:#969,stroke:#333, font-weight: bold; classDef error fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5 classDef coral fill:#f9f,stroke:#333,stroke-width:4px; classDef animate stroke-dasharray: 9,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
实际代码
--- title: code --- flowchart LR Package("Pacakge"):::green Reference("Reference"):::green LocalPackage("LocalPackage"):::purple RemotePackage("RemotePackage"):::purple JsonReference("JsonReference"):::purple YarnReference("YarnReference"):::purple Package --> LocalPackage Package --> RemotePackage Reference --> JsonReference Reference --> YarnReference Dependencies("Dependencies"):::green VersionDependencies("VersionDependencies"):::green VersionDependency("VersionDependency"):::green Cache("Cache"):::pink Dependencies -->|package_name| VersionDependencies -->|version_name| VersionDependency Reference --> Package Reference --> Dependencies Reference --> Cache classDef pink 1,fill:#FFCCCC,stroke:#333, color: #fff, font-weight:bold; classDef green fill: #696,color: #fff,font-weight: bold; classDef purple fill:#969,stroke:#333, font-weight: bold; classDef error fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5 classDef coral fill:#f9f,stroke:#333,stroke-width:4px; classDef animate stroke-dasharray: 9,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;