构建与发布
到现在为止,你写的每一个工程都只在自己电脑上活得好好的。可是代码的价值,恰恰在于它能跑到别人的环境里去——同事要用、服务器要用、三个月后换了机器的你也要用。这一章就解决一件事:怎么把一坨源码,变成一条命令就能装进任何环境…
到现在为止,你写的每一个工程都只在自己电脑上活得好好的。可是代码的价值,恰恰在于它能跑到别人的环境里去——同事要用、服务器要用、三个月后换了机器的你也要用。这一章就解决一件事:怎么把一坨源码,变成一条命令就能装进任何环境的"成品"。
本章配了一个可以动手折腾的示例工程 daylab-toolkit/(目录就放在本章课件旁边),后面所有演示都围绕它展开。
1. 一个库的完整旅程
先在脑子里铺一张地图。一个公共库从出生到被人使用,要走过这样一条流水线:
编写源码 → 填写工程清单 → 本地构建 → 上传仓库 → 他人安装
(src 布局) (pyproject.toml) (生成产物) (PyPI/私有源) (pip install)
打个比方:源码好比刚出锅的菜,没法直接快递;构建就是把它做成罐头(统一规格、密封包装);发布是把罐头摆上超市货架(仓库);安装则是顾客下单提货。这一章我们只干"做罐头、上货架"这两件事,全程手动用最原始的工具操作一遍——下一章会有一个把整条流水线一脚油门踩到底的现代工具。
2. pyproject.toml:工程的说明书
构建工具怎么知道你的工程叫什么、依赖什么、代码放在哪?答案全在一个文件里:pyproject.toml。它是工程的"身份证 + 装箱单"。
先看 daylab-toolkit 的完整配置,逐行拆解:
# pyproject.toml
[build-system] # 构建环节的声明:告诉前端用哪个后端干活
requires = ["hatchling"] # 干活前需要先装上的后端工具
build-backend = "hatchling.build" # 具体调用哪个包的哪个模块来完成构建
[project] # 工程的对外名片,发布后所有人都能看到
name = "daylab-toolkit" # 发行版名称(在 PyPI 上的门牌号)
version = "0.2.1" # 版本号,按「主.次.修订」的语义化规则递增
description = "演示公共库从源码到分发包完整链路的教学工程"
requires-python = ">=3.14" # 声明支持的最低 Python 版本
readme = "README.md" # 首页说明文件的路径,会展示在仓库页面上
dependencies = [ # 运行期必需的第三方库及其版本约束
"pypinyin>=0.53", # 汉字转拼音
"python-slugify>=8.0", # 文本转 URL 友好别名
]
[tool.hatch.build.targets.sdist] # 源码包(sdist)的打包规则
# 这里留空:默认把工程里的文件全部纳入
[tool.hatch.build.targets.wheel] # 安装包(wheel)的打包规则
packages = ["src/daylab_toolkit"] # 指明真正的包在 src 目录下,只打包它
几个容易踩的点提前说清:
- 两个名字:发行版名
daylab-toolkit(给人类和仓库看,中划线连接)与导入名daylab_toolkit(给import语句看,下划线连接),是一对约定俗成的"双胞胎",别写混了。 - 版本号不是随便写的:修 bug 升修订位、加功能升次位、破坏兼容才动主位——这是生态默认的默契,遵循语义化版本规范。
packages为什么指向src/里面:这是 src 布局的必然结果。源码藏在src/下可以避免"从工程根目录直接 import 到未安装版本"的糊涂账,但代价就是打包时必须显式告诉后端去哪儿找包。
实用小贴士:在 VSCode 里装一个 Even Better TOML 插件,
pyproject.toml会有语法高亮和字段补全,体验完全不一样。
3. 构建的前台与后台
你可能注意到:构建这件事被拆成了两个角色——
- 构建前端:负责调度流程的"传菜员",常见选择有
build、uv、Poetry、PDM; - 构建后端:真正动手打包的"后厨",常见选择有
hatchling、setuptools、pdm-backend、uv-build、poetry-core。
为什么要拆开?因为打包这件事存在一份社区公认的标准协议:
构建协议:https://peps.python.org/pep-0517/ 产物标准:https://peps.python.org/pep-0427/
只要前后端都遵守协议,就可以任意搭配:用 uv 当前端、hatchling 当后端;明天换成 Poetry 前端、setuptools 后端也行。pyproject.toml 里的 [build-system] 就是在声明这次用哪个组合。
本章为了让你看清底层细节,用最朴素的方式操作:前端用官方的 build,后端用 hatchling。
4. 亲手构建一次
进入 daylab-toolkit 目录,四步走:
# 1. 造一个干净的虚拟环境
python -m venv .venv
# 2. 进入这个环境
source .venv/bin/activate
# 3. 把官方前端工具装进来
pip install build
# 4. 一声令下,开始打包
python -m build
跑完之后,工程里会多出一个 dist/ 目录,里面躺着两件产物:
dist/
├── daylab_toolkit-0.2.1.tar.gz # sdist:源码包
└── daylab_toolkit-0.2.1-py3-none-any.whl # wheel:安装包
它们俩的区别值得记一辈子:
| 产物 | 本质 | 安装时 |
|---|---|---|
sdist(.tar.gz) |
源码快照,连测试、文档一起装进去 | 环境里要先现场"编译组装"一遍 |
wheel(.whl) |
预先组装好的成品,解开就能用 | 直接落位,装完即走 |
所以真实世界里 pip 拿到两个包时会优先选 wheel,又快又稳;sdist 则是"源码备胎",保证极端情况下也能从源码装起来。
5. 出货前的质检
产物生成不等于万事大吉。上传之前,用官方工具做个体检:
# 安装发布工具(里面带检查功能)
pip install twine
# 检查产物元数据是否齐全合规
twine check dist/*
它会核对包的名称、版本、描述等字段是否填写规范。仓库那边也会做类似校验,但在上传之前本地先查一遍,能省掉"传上去被打回来"的来回折腾。
6. 发布到官方仓库 PyPI
PyPI(https://pypi.org)是 Python 世界的中央货架,全世界的 pip install 默认都从这里取货。把罐头摆上去的流程:
- 注册账号:https://pypi.org/account/register/
- 创建访问令牌:https://pypi.org/manage/account/token/ ——不要用账号密码直接上传,令牌更安全,且可以随时吊销
- 复制令牌(一串
pypi-开头的字符) - 上传:
twine upload dist/*
# 按提示粘贴令牌即可
名字是先到先得的:
daylab-toolkit这种名字八成已经被人占了,动手前先去 PyPI 搜一下,占用了就换一个。
上传成功后,任何人(包括另一台机器上的你)都能直接安装:
pip install daylab-toolkit
7. 发布到私有仓库
公司内部往往不想把代码公开上 PyPI,这时候就需要"自家超市"——私有制品仓库。自己从零搭一个相当费事,主流做法是直接用云服务,例如阿里云的云效制品仓库(https://packages.aliyun.com):开通服务、新建一个 Python 类型的仓库,按页面指引把包传上去即可。
私有仓库真正的价值在于团队统一供给:内部公共库 + 外部第三方库,一个入口全搞定。
从私有仓库安装
原理很简单:把 pip 默认取货的地址改成你的仓库。在 pip 的配置文件里(Linux/macOS 是 ~/.pip/pip.conf,Windows 是 %APPDATA%\pip\pip.ini)写入:
[global]
index-url = 私有仓库地址 # 主货架:优先来这里找包
extra-index-url = https://pypi.tuna.tsinghua.edu.cn/simple # 备用货架:私有仓库没有的去镜像源找
trusted-host = packages.aliyun.com # 跳过该域名的 HTTPS 校验(仅内网场景建议)
配置生效后,安装体验与从前完全一致:
pip install daylab-toolkit # 先查私有仓库,查不到再去备用源
8. 可编辑安装:边改边用
发布流程走完,来聊一个日常开发离不开的技巧。
场景很常见:你在写一个公共库 daylab-toolkit,同时另一个工程正在使用它。如果每次改一行库代码都要重新构建、重新安装,一天下来时间全耗在"装了卸、卸了装"上。
可编辑安装(editable install,也叫开发模式安装)专治这个问题:
# 在使用方工程的环境里执行(指向库工程的路径)
pip install -e ../daylab-toolkit
装完之后:
# 任意位置都能导入
from daylab_toolkit import full_pinyin
# 改了库的源码?不用重装,下一次调用自动是新代码
它的原理一点也不神秘:普通安装是把代码复制一份进 site-packages;可编辑安装则是在 site-packages 里放一个 .pth 文件——相当于一张指路牌,指向你工程里的源码目录。解释器每次导入时顺着指路牌找到的是源码本体,所以你怎么改,运行时就用什么。
9. 动手玩示例工程
本章的 daylab-toolkit/ 就在课件同级目录,结构如下:
daylab-toolkit/
├── src/
│ └── daylab_toolkit/ # 包本体(src 布局)
│ ├── pinyin/ # 工具一:汉字转拼音(依赖 pypinyin)
│ ├── slug/ # 工具二:标题转 URL 别名(依赖 python-slugify)
│ ├── hello.py # 零依赖的小工具:欢迎语
│ └── __init__.py
├── pyproject.toml # 工程说明与打包配置
└── README.md
它麻雀虽小、五脏俱全:src 布局、双子模块、两个运行时依赖、完整的打包配置——正好把本章所有知识点串起来。建议按第 4 节的步骤对它真实构建一次,再试试第 8 节的可编辑安装,改一行 hello.py 的返回值,看看不重装的情况下效果是否立刻变化。
作业
- 挑一个你自己写的小工具(或者直接用
daylab-toolkit),完整走一遍链路:构建出产物 → 本地校验 → 发布到一个私有制品仓库 → 换一个全新的虚拟环境,从私有仓库把它装回来并运行成功。 - 合上课件,找一张白纸,凭记忆画出"源码 → 构建 → 发布 → 安装"的全流程图,并向一位同伴(或者对着空气,费曼学习法)讲清楚每个环节产出了什么、为什么要分 sdist 和 wheel 两种产物。讲不圆的地方,就是你还没真正吃透的地方。
下一章:23 · 工程化工具链