设计一个包管理工具

背景

包管理工具影响了语言的使用体验,是语言生态的基础设施之一。
因此设计新语言时,包管理工具也是需要仔细设计的一个重要部分。

调研

在设计之前,必须先调研其他语言是怎么设计的。

语言 官方 / 主流包管理 官方仓库
Python pip PyPI
JavaScript / TypeScript npm、pnpm、Yarn npm Registry
Java Maven、Gradle Maven Central
C# NuGet NuGet Gallery
Go go modules proxy.golang.org
Rust Cargo crates.io
Swift Swift Package Manager Swift Package Index / Git
Kotlin Gradle / Maven Maven Central
PHP Composer Packagist
Dart pub pub.dev

包配置文件

每个语言都有一个包配置文件,配置文件里面一般都有:

  1. name
  2. version
  3. dependencies
  4. license
  5. authors
  6. repository

配置文件的文件名和格式各不相同:

语言 配置文件 格式
npm package.json JSON
Cargo Cargo.toml TOML
C# csproj YAML
Python pyproject.toml / requirements.txt TOML
Go go.mod 自定义
Java pom.xml / build.gradle XML / DSL
Swift Package.swift Swift 代码
Dart pubspec.yaml YAML

版本合并

每个语言都支持语义版本(SemVer),同个包的不同版本会被合并,依赖可以指定版本规则:

  1. >=1.2
  2. ^1.2
  3. ~1.2

只是符号略有区别。

Go 设计地更简单:只支持 >=1.2

版本锁

大多有锁文件,以避免版本被自动更新:

  1. package-lock.json
  2. Cargo.lock
  3. poetry.lock
  4. pnpm-lock.yaml
  5. composer.lock

只有 Go 有区别,因为它不支持自动更新,相当于默认就是带锁的效果。

中央仓库

所有语言都有一个中央仓库,也允许私有仓库。

  1. PyPI
  2. npm
  3. crates.io
  4. NuGet
  5. Maven Central
  6. pub.dev
  7. Packagist

工作空间

工作空间可以将多个包打包为整体。

很多语言开始不支持,但新版本支持。

语言 / 工具 Monorepo / Workspace 配置方式 配置示例
Rust Cargo.toml 中的 workspace.members Cargo.toml
members = ["a", "b", "c"]
Go go.work go.work
JavaScript / npm package.json 中的 workspaces workspaces
pnpm pnpm-workspace.yaml pnpm-workspace.yaml
Yarn package.json 中的 workspaces workspaces
Gradle settings.gradle(.kts) 中使用 include(...) include("a", "b", "c")

几乎每个语言都设计为:当有工作空间时,包的配置依然独立,不会自动从工作空间继承。

没有语言设计为可以通过唯一的配置文件管理所有子包。

是否支持源码和二进制混编

语言 支持
Rust 可以
Go 基本源码
Java 可以
C# 可以
Python 可以(wheel)
npm 可以(native addon)
Dart 基本源码

设计决策

很多古老语言一开始都没有统一的包管理工具,直到后来才增加。
说明包管理是前人在项目实践中总结的重要经验。

其他现代新语言在设计时,也都将包管理工具作为一等公民进行考虑的。
总体上,包管理工具并不是只需一两个配置就能实现的,虽然很多人在实际项目中只用到少数配置。

Go 是所有设计方案中,我认为相对最简单的一种,而且实践证明够用。
因此,新设计时,我应该以 Go 的方案为基础进行设计。