引言
第九章前八篇解决的是”把命令行工具做对”:argparse 解析参数、JSON 输出与退出码、logging 记录运行、unittest 保证正确。但第八篇的 todo 工具目前只是你机器上的一个 todo.py 脚本——换个目录就找不到它,想让同事 pip install 装上更是无从谈起。本篇解决”把工具交出去”:用 pyproject.toml 描述项目,把脚本组织成 src 布局的包,用 pip install -e . 做可编辑安装,用 python -m build 构建出 wheel 与 sdist 两种分发产物,最后梳理发布到 PyPI(含测试服 TestPyPI)的完整步骤,并简述企业私有源的做法。本篇不实际发布——发布需要账号与网络凭证,但所有本地步骤都在本机 CPython 3.12.10(Windows,Git Bash)上真实执行,展示的输出逐字来自实际运行。前置知识:第一章的 pip 与 venv、第八章的 todo 工具。
概念与原理:为什么脚本要变成”包”
一个脚本变成可分发的包,要回答三个”为什么”。
为什么需要元数据文件。 脚本时代,程序的全部信息就是代码本身;一旦要被别人安装,就必须回答一连串问题:包名叫什么、版本是多少、依赖哪些库、装完在命令行敲什么命令、支持哪些 Python 版本。这些问题以前散落在 setup.py、setup.cfg 和若干约定里,混杂着可执行代码,解析顺序晦涩。从 PEP 518(2016)起,构建系统的声明统一放进 pyproject.toml;PEP 621(2020)又把项目元数据(名称、版本、作者、依赖)也搬进来。今天新建一个纯 Python 包的标准答案是:一个 pyproject.toml 管一切,setup.py 只在极少数需要动态计算的场合才出现。
为什么会有”构建后端”与”构建前端”的分工。 pyproject.toml 的 [build-system] 段声明了 build-backend(构建后端,负责把源码变成产物)和 requires(后端需要的依赖)。常见后端:setuptools(最老牌)、hatchling(社区流行)、flit。pip 和 python -m build 是”前端”:它们只负责调用后端暴露的统一接口(PEP 517 定义的 build_wheel、build_sdist),不关心后端内部实现。这种解耦意味着:换后端只改 pyproject.toml 三行,安装与构建工具完全不用动。本机实测时,python -m build 会在隔离环境里自动安装 setuptools==84.0.0 再调用它——前端与后端各司其职,从日志里看得清清楚楚。
分发产物:wheel 与 sdist。 pip install 包名 时,pip 去仓库找的优先是 wheel:一个按约定目录结构打包的 zip 文件(PEP 427),里面直接是可安装的 .py 文件与元数据,pip 解压即用,无需构建。sdist(source distribution)是源码压缩包,收到它的 pip 需要现场构建成 wheel 再装——所以只要仓库里有 wheel,正常用户永远不会走到构建那步。另外注意 PEP 660 的”可编辑安装”(pip install -e .):它生成的也是一种特殊 wheel,安装后包仍指向你的源码目录,改代码立即生效,开发期调试专用。
入口点(entry point)是什么。 [project.scripts] 声明把”某个函数”暴露成命令行命令。安装时后端生成一个可执行文件(Windows 上是 .exe),运行它等于:用该环境的 Python 导入你指定的模块、调用你指定的函数、把函数返回值交给 sys.exit。所以入口函数必须返回退出码(或抛 SystemExit)——这正是第六篇退出码知识的用武之地。本机装好的 todo_tool.exe 内容可以从 wheel 里的 entry_points.txt 查到,后面会展示。
操作与实现:把 todo 工具打包
第一步:整理出 src 布局的包
上一篇的 todo 工具只有 todo.py 一个文件。打包前把它拆成三个职责清晰的模块,并采用 src 布局——源码放在 src/ 目录下而不是仓库根目录。src 布局的原理是”让未安装的项目不可导入”:如果不小心在仓库根目录直接 import todo_tool,会拿到源码目录里的旧状态,掩盖”装的是 wheel 里的版本”这类问题;放一层 src/ 后,只有安装过的环境才能导入,肉眼可见包的真实来源。项目结构如下(本机演示在 C:\Users\Administrator\ch9-demo 目录完成,你的路径不同):
ch9-demo/
├── pyproject.toml # 项目元数据 + 构建配置
├── README.md # 长描述:上线后展示在 PyPI 主页
└── src/
└── todo_tool/
├── __init__.py # 包标记 + __version__
├── cli.py # 命令行入口:参数解析与分发
└── core.py # 业务逻辑:增、查、改、持久化
三个 Python 文件的内容如下(与第八篇功能完全一致:add / list / done,list 支持 —json;只是分了模块)。__init__.py 里放版本号,让 --version、元数据、发布三者引用同一处:
"""todo_tool:待办事项命令行工具包。"""
__version__ = "0.1.0"
core.py 只做数据操作,不碰命令行——这个”逻辑与 I/O 分离”让下一篇的单元测试可以不打命令行直接测:
"""核心逻辑:任务的增、查、改与 JSON 持久化。
存储文件默认是用户目录下的 .todo_tool.json,内容为任务字典的列表:
[{"text": "买菜", "done": false}, ...]
"""
import json
from pathlib import Path
DEFAULT_FILE = Path.home() / ".todo_tool.json"
def load_tasks(path=DEFAULT_FILE):
"""读取任务列表;文件不存在时返回空列表。"""
if not path.exists():
return []
with open(path, encoding="utf-8") as f:
return json.load(f)
def save_tasks(tasks, path=DEFAULT_FILE):
"""把任务列表写回文件,中文不转义、保留缩进。"""
with open(path, "w", encoding="utf-8") as f:
json.dump(tasks, f, ensure_ascii=False, indent=2)
def add_task(tasks, text):
"""追加一条未完成任务,返回当前总数。"""
tasks.append({"text": text, "done": False})
return len(tasks)
def mark_done(tasks, index):
"""把第 index 条(从 1 开始)标记为完成;越界抛 IndexError。"""
if not 1 <= index <= len(tasks):
raise IndexError(f"任务编号 {index} 不存在")
tasks[index - 1]["done"] = True
cli.py 注意一个小改动:上一版的 --json 直接挂在根解析器上,运行 todo_tool list --json 会报”unrecognized arguments”。因为 argparse 一旦见到子命令词,剩余参数就交给子解析器处理,根解析器上的选项必须放在子命令之前才能生效。修正办法是用一个 parents=[common] 的公共父解析器,把 --json 注入每个子命令:
"""命令行入口:todo_tool add / list / done,list 支持 --json。"""
import argparse
import json
import sys
from . import __version__
from .core import add_task, load_tasks, mark_done, save_tasks
def build_parser():
# 公共父解析器:--json 通过 parents 注入每个子命令,
# 这样 todo_tool list --json 的写法可以正常工作。
common = argparse.ArgumentParser(add_help=False)
common.add_argument("--json", action="store_true", help="以 JSON 格式输出")
parser = argparse.ArgumentParser(
prog="todo_tool", description="待办事项命令行工具")
parser.add_argument("--version", action="version",
version=f"todo_tool {__version__}")
sub = parser.add_subparsers(dest="command", required=True)
p_add = sub.add_parser("add", parents=[common], help="新增一条待办")
p_add.add_argument("text", help="待办内容")
sub.add_parser("list", parents=[common], help="列出全部待办")
p_done = sub.add_parser("done", parents=[common], help="把某条待办标记为完成")
p_done.add_argument("index", type=int, help="任务编号(从 1 开始)")
return parser
def main(argv=None):
args = build_parser().parse_args(argv)
if args.command == "add":
tasks = load_tasks()
n = add_task(tasks, args.text)
save_tasks(tasks)
print(f"已添加第 {n} 条:{args.text}")
elif args.command == "list":
tasks = load_tasks()
if args.json:
print(json.dumps(tasks, ensure_ascii=False, indent=2))
else:
for i, task in enumerate(tasks, 1):
mark = "[x]" if task["done"] else "[ ]"
print(f"{mark} {i}. {task['text']}")
elif args.command == "done":
tasks = load_tasks()
try:
mark_done(tasks, args.index)
except IndexError as exc:
print(f"错误:{exc}", file=sys.stderr)
return 1
save_tasks(tasks)
print(f"已完成:{args.index} 号任务")
return 0
if __name__ == "__main__":
sys.exit(main())
pyproject.toml 是打包的核心,三个区段各有职责:[build-system] 声明构建后端;[project] 是 PEP 621 元数据;[project.scripts] 声明命令入口。注意几个细节:license = "MIT" 用的是一次到位写 SPDX 标识符的写法(较新的 setuptools 支持,本机 pip show 显示 License-Expression: MIT);readme = "README.md" 让 README 成为 PyPI 主页的长描述;requires-python 声明支持的最低版本;[tool.setuptools.packages.find] 的 where = ["src"] 告诉 setuptools 到 src/ 下找包:
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[project]
name = "todo-tool"
version = "0.1.0"
description = "命令行待办事项工具:add / list / done,支持 JSON 输出"
readme = "README.md"
requires-python = ">=3.9"
license = "MIT"
keywords = ["todo", "cli", "productivity"]
authors = [{ name = "你的名字", email = "you@example.com" }]
[project.scripts]
todo_tool = "todo_tool.cli:main"
[tool.setuptools.packages.find]
where = ["src"]
README.md 用几句话说明项目是什么、怎么装、怎么用(安装用 pip install todo-tool,用法列出 add/list/done 示例,数据保存在 ~/.todo_tool.json)。它是 PyPI 主页的门面,别只写两行。
第二步:pip install -e . 本地安装
在项目根目录建虚拟环境并做可编辑安装(本机 pip 25.0.1 实测):
$ python -m venv venv
$ venv/Scripts/python.exe -m pip install -e .
输出(已省略末尾两行”pip 有新版可升级”的 notice):
Obtaining file:///C:/Users/Administrator/ch9-demo
Installing build dependencies: started
Installing build dependencies: finished with status 'done'
Checking if build backend supports build_editable: started
Checking if build backend supports build_editable: finished with status 'done'
Getting requirements to build editable: started
Getting requirements to build editable: finished with status 'done'
Preparing editable metadata (pyproject.toml): started
Preparing editable metadata (pyproject.toml): finished with status 'done'
Building wheels for collected packages: todo-tool
Building editable for todo-tool (pyproject.toml): started
Building editable for todo-tool (pyproject.toml): finished with status 'done'
Created wheel for todo-tool: filename=todo_tool-0.1.0-0.editable-py3-none-any.whl size=1871 sha256=41b826261c7a16d5cb37fa20aad5d47d2c11daacff44cb2836e0cba6b369b6e0
Stored in directory: C:\Users\Administrator\AppData\Local\Temp\2\pip-ephem-wheel-cache-p1z2m3m3\wheels\a4\b1\fb\df639b5353bd44348a4ffe9394a0a63a560165dd34df2a83b1
Successfully built todo-tool
Installing collected packages: todo-tool
Successfully installed todo-tool-0.1.0
pip install -e . 里的 . 指当前目录,-e 是可编辑模式。Stored in directory 那行是 pip 的临时 wheel 缓存路径,与本次运行的随机目录有关,每次都可能不同,不用在意。验证安装结果,pip show 会看到 Editable project location 指向我们的源码目录,这正是”可编辑”的证据:
Name: todo-tool
Version: 0.1.0
Summary: 命令行待办事项工具:add / list / done,支持 JSON 输出
Home-page:
Author:
Author-email: 你的名字 <you@example.com>
License-Expression: MIT
Location: C:\Users\Administrator\ch9-demo\venv\Lib\site-packages
Editable project location: C:\Users\Administrator\ch9-demo
Requires:
Required-by:
现在命令行就能用了。本机实测整条功能链路(注意 Windows 下未激活 venv 时要用 venv/Scripts/ 下的完整路径;激活 venv 后直接敲 todo_tool 即可):
$ venv/Scripts/todo_tool.exe --version
todo_tool 0.1.0
$ venv/Scripts/todo_tool.exe add 买菜
已添加第 1 条:买菜
$ venv/Scripts/todo_tool.exe add 写周报
已添加第 2 条:写周报
$ venv/Scripts/todo_tool.exe add 完成博客打包文章
已添加第 3 条:完成博客打包文章
$ venv/Scripts/todo_tool.exe list
[ ] 1. 买菜
[ ] 2. 写周报
[ ] 3. 完成博客打包文章
$ venv/Scripts/todo_tool.exe list --json
[
{
"text": "买菜",
"done": false
},
{
"text": "写周报",
"done": false
},
{
"text": "完成博客打包文章",
"done": false
}
]
$ venv/Scripts/todo_tool.exe done 2
已完成:2 号任务
错误路径与退出码也逐一核对:业务错误返回 1,参数错误由 argparse 返回 2——与第六篇的约定完全一致:
$ venv/Scripts/todo_tool.exe done 99
错误:任务编号 99 不存在
$ echo $?
1
$ venv/Scripts/todo_tool.exe
usage: todo_tool [-h] [--version] {add,list,done} ...
todo_tool: error: the following arguments are required: command
$ echo $?
2
$ venv/Scripts/todo_tool.exe done abc
usage: todo_tool done [-h] [--json] index
todo_tool done: error: argument index: invalid int value: 'abc'
$ echo $?
2
可编辑安装的最大价值是”改代码立即生效”。本机实测:把 __init__.py 里的版本号改成 0.1.1,删除字节码缓存后(原因见易错点 2),--version 立刻显示新版本;改回 0.1.0 亦然——全程没有重新安装:
# 把 src/todo_tool/__init__.py 的版本号改为 0.1.1,清空 __pycache__ 后运行:
$ venv/Scripts/todo_tool.exe --version
todo_tool 0.1.1
# 把版本号改回 0.1.0,清空 __pycache__ 再运行:
$ venv/Scripts/todo_tool.exe --version
todo_tool 0.1.0
第三步:构建 wheel 与 sdist
可编辑安装在本地开发。要交付给别人,需要构建正规分发产物。python -m build 不在标准库,先把它装进 venv(本机实际执行):
$ venv/Scripts/python.exe -m pip install build
输出结尾(已省略各依赖的下载行):
Installing collected packages: pyproject_hooks, packaging, colorama, build
Successfully installed build-1.6.0 colorama-0.4.6 packaging-26.3 pyproject_hooks-1.2.0
然后构建。build 前端先构建 sdist,再基于 sdist 构建 wheel——整个过程分两段,每段都会创建隔离环境安装 setuptools(日志显示安装的是 setuptools==84.0.0):
$ venv/Scripts/python.exe -m build
* Creating isolated environment: venv+pip...
* Installing packages in isolated environment:
- setuptools>=68
* Getting build dependencies for sdist...
running egg_info
creating src\todo_tool.egg-info
writing src\todo_tool.egg-info\PKG-INFO
writing dependency_links to src\todo_tool.egg-info\dependency_links.txt
writing entry points to src\todo_tool.egg-info\entry_points.txt
writing top-level names to src\todo_tool.egg-info\top_level.txt
reading manifest file 'src\todo_tool.egg-info\SOURCES.txt'
writing manifest file 'src\todo_tool.egg-info\SOURCES.txt'
* Installed build dependency versions:
- setuptools==84.0.0
* Building sdist...
(……构建与打包清单的中间行省略……)
* Building wheel from sdist
* Creating isolated environment: venv+pip...
* Installing packages in isolated environment:
- setuptools>=68
* Getting build dependencies for wheel...
(……wheel 构建与 wheel 元数据写入的中间行省略……)
adding 'todo_tool/__init__.py'
adding 'todo_tool/cli.py'
adding 'todo_tool/core.py'
adding 'todo_tool-0.1.0.dist-info/METADATA'
adding 'todo_tool-0.1.0.dist-info/WHEEL'
adding 'todo_tool-0.1.0.dist-info/entry_points.txt'
adding 'todo_tool-0.1.0.dist-info/top_level.txt'
adding 'todo_tool-0.1.0.dist-info/RECORD'
removing build\bdist.win-amd64\wheel
Successfully built todo_tool-0.1.0.tar.gz and todo_tool-0.1.0-py3-none-any.whl
产物出现在 dist/ 目录:todo_tool-0.1.0-py3-none-any.whl 和 todo_tool-0.1.0.tar.gz。文件名本身是一份自述:py3-none-any 表示”纯 Python、不依赖平台”,全平台通用。用 zipfile 查看 wheel 内部,能直接看到安装后会长成什么样:
File Name Modified Size
todo_tool/__init__.py 2026-09-03 04:07:12 78
todo_tool/cli.py 2026-09-03 04:05:08 2121
todo_tool/core.py 2026-09-03 04:04:36 1144
todo_tool-0.1.0.dist-info/METADATA 2026-09-03 04:07:28 648
todo_tool-0.1.0.dist-info/WHEEL 2026-09-03 04:07:28 91
todo_tool-0.1.0.dist-info/entry_points.txt 2026-09-03 04:07:28 49
todo_tool-0.1.0.dist-info/top_level.txt 2026-09-03 04:07:28 10
todo_tool-0.1.0.dist-info/RECORD 2026-09-03 04:07:28 625
注意 dist-info 目录里没有出现 pyproject.toml——wheel 只给运行时需要的东西。而 sdist 是全家桶,连 pyproject 和 README 都带(别人拿到它能原样重建):tar -tzf dist/todo_tool-0.1.0.tar.gz 列出 todo_tool-0.1.0/pyproject.toml、todo_tool-0.1.0/README.md、src/ 下全部源码与 egg-info。命令入口藏在 entry_points.txt 里,把它解出来看(用 python -c 从 zip 读取):
[console_scripts]
todo_tool = todo_tool.cli:main
这一行就是”安装后命令可用”的全部秘密:运行 todo_tool = 导入 todo_tool.cli 模块、执行 main() 函数、把返回值交给 sys.exit。
新环境安装 wheel 的完整验证。 空 venv、直接装 wheel、然后跨目录运行(模拟”任何用户的机器”)。本机实测:
$ python -m venv venv-wheel
$ venv-wheel/Scripts/python.exe -m pip install dist/todo_tool-0.1.0-py3-none-any.whl
输出结尾(notice 两行已省略):
Successfully installed todo-tool-0.1.0
$ cd C:/Users/Administrator # 离开项目目录,模拟普通用户位置
$ C:/Users/Administrator/ch9-demo/venv-wheel/Scripts/todo_tool.exe --version
todo_tool 0.1.0
$ C:/Users/Administrator/ch9-demo/venv-wheel/Scripts/todo_tool.exe list
[ ] 1. 买菜
[x] 2. 写周报
[ ] 3. 完成博客打包文章
注意数据仍在用户目录的 .todo_tool.json 里——工具是”装在哪台机器上、数据就跟着那个用户走”的,这正是当时把存储文件放在 Path.home() 的设计。安装变得与目录无关了:这,就是分发包的意义。
第四步:发布到 PyPI 的完整步骤(不实际执行)
发布需要账号与凭证,本篇只给出可直接照做的步骤。以测试服 TestPyPI 为例(正式 PyPI 流程完全一致):
- 到
test.pypi.org注册账号,进入 Settings → API tokens,生成一个以pypi-开头的 token(这就是上传密码,妥善保存)。 - 安装上传工具并做发布前体检:
pip install twine,然后python -m twine check dist/*。本机对刚才的产物实测通过:
Checking dist/todo_tool-0.1.0-py3-none-any.whl: PASSED
Checking dist/todo_tool-0.1.0.tar.gz: PASSED
- 上传。用户名固定为
__token__,密码填 token。使用显式的仓库地址最稳妥:
$ python -m twine upload --repository-url https://test.pypi.org/legacy/ dist/*
- 验证安装(用测试服当下载源):
$ python -m pip install --index-url https://test.pypi.org/simple/ todo-tool
- 测试服验证通过后,再对正式 PyPI 重复 1–4,只需把仓库地址换成
https://upload.pypi.org/legacy/(下载源换成https://pypi.org/simple/)。发布后pip install todo-tool全球可用。
发布环节必须注意三件事。命名唯一:PyPI 上包名全局唯一,todo-tool 大概率已被人占用,上传会直接被拒;发布前先到 PyPI 搜索确认,或起 todo-tool-demo-2026 这类带特征的唯一名。版本号规范:必须符合 PEP 440(见易错点 3),同一个版本号不能重复上传,改代码必须升版本。README 与 license:readme = "README.md" 让主页有内容;若包缺少长描述,twine 会警告(本机对无 README 的包实测警告原文:long_description_content_type missing. defaulting to text/x-rst)。license 按 license = "MIT" 的 SPDX 写法声明即可。
第五步:企业私有源简述
很多公司不允许直连公网 PyPI,改用自己的私有索引。做法是把下载源指到内部服务器,一行命令全局生效(本机实测命令与输出):
$ venv/Scripts/python.exe -m pip config set global.index-url https://pypi.example.com/simple
Writing to C:\Users\Administrator\AppData\Roaming\pip\pip.ini
之后所有 pip install 默认走私有源;也可以不加配置、单次指定:pip install --index-url https://pypi.example.com/simple todo-tool。私有源的搭建工具常见有 devpi、Nexus、Artifactory,它们对内网 pip 完全透明——与公网 PyPI 使用同一种协议,pip install、twine upload 的用法都不变。用完恢复默认源:pip config unset global.index-url。
易错点与陷阱
1. 入口函数忘了 sys.exit(main())。 [project.scripts] 的入口函数返回值会交给 sys.exit,但若 cli.py 里只有 return 1 而没在 __main__ 下 sys.exit(main()),命令永远以 0 退出。本篇演示的命令能正确返回 1/2,正是因为末尾写了 sys.exit(main())。命令行工具的最后一公里,就是”错误要能通过退出码被别人看见”。
2. 可编辑安装下”改了源码不生效”。 这是本机实测踩到的真坑:同一秒内把 __init__.py 改来改去,版本号大小不变(都是 5 个字符),再运行 --version 拿到的仍是旧值。原因是 Python 导入时会校验 .pyc 字节码缓存,而校验用的时间戳粒度是 1 秒——同秒内改回原内容、文件大小又不便时,缓存被判定为”新鲜”,直接复用。删除 __pycache__ 目录立即恢复。这也是为什么上面的演示里每次改版本号都先清缓存;真实开发中改完代码等一秒再跑通常也没事,但碰到”改了没效果”要首先怀疑这一点,而不是怀疑安装坏了。
3. 版本号不符合 PEP 440,构建直接失败。 写入 version = "1.0-final" 这种非法格式,python -m build 在解析元数据阶段就报错(本机实测):
configuration error: `project.version` must be pep440
反过来,"v0.1" 能顺利构建——PEP 440 允许可选的 v 前缀,它会被规范化为 0.1。别把”构建通过”当成”版本合规”的依据,发布前用 twine check 或直接在构建时观察错误信息。
4. src 布局忘了声明包的查找位置。 采用 src 布局却漏写 [tool.setuptools.packages.find] 的 where = ["src"],setuptools 会找不到包,构建出的 wheel 里没有 todo_tool 目录——装上去 import todo_tool 直接报 ModuleNotFoundError,仿佛一切正常实则空包。构建完成后养成习惯:用 python -m zipfile -l dist/*.whl 瞄一眼里面有没有包目录。
5. Windows 下命令不在 PATH。 pip install -e . 成功后直接敲 todo_tool 报”不是内部或外部命令”,多是因为 venv 未激活。激活(source venv/Scripts/activate)后命令就在 PATH 里;不激活就老老实实用 venv/Scripts/todo_tool.exe 完整路径。这不是打包的错,是 Shell 环境的问题。
小结
本篇把第八篇的脚本变成了可交付的分发包:pyproject.toml 用三个区段描述”谁来构建、包是什么、命令从哪来”;src 布局让包只有在被安装后才可见;pip install -e . 提供开发期的可编辑安装;python -m build 产出 wheel 与 sdist 两种分发产物;twine 与 TestPyPI 打通了发布链路;私有源用 pip config 一行切换。打包解决的不是”能不能跑”,而是”别处能不能装、别人能不能查、版本能不能追溯”。至此,第九章的工程闭环只剩最后一块拼图:第十篇综合测验会把这些知识全部串联起来,并让你亲手给 todo 工具加上新功能与测试。
练习与思考题
- 把
pyproject.toml的版本改成0.1.1重新python -m build,观察dist/里多了什么、少了什么;pip install dist/todo_tool-0.1.1-py3-none-any.whl与旧版共存时,两个版本的命令入口如何区分? - 用
python -m zipfile -l对比 wheel 与 sdist 的文件清单,说出 wheel 刻意不包含哪三类文件,并解释各自的原因。 - 把构建后端从 setuptools 换成 hatchling(
[build-system]的requires = ["hatchling"]、build-backend = "hatchling.build"),重新构建,观察同一套项目代码能否无缝交付;思考”前端/后端解耦”的设计带来的工程价值。 - 如果不做任何修改,把当前
todo-tool原样上传到 TestPyPI,你预计会遇到哪两类失败?先列清单,再对照本篇”命名唯一、版本号规范”两条找到答案。