python MANIFEST.in
## Python MANIFEST.in 其实是打包时的一个隐性门槛
很多人刚开始接触 Python 打包时,setup.py 写得挺顺,但忽略了 MANIFEST.in 这个文件。等到项目安装到别人机器上,突然发现少了个数据文件,或者某个配置压根没带进去,才意识到问题。这种事我见的不少,原因其实很直白——太多人以为有了 setuptools 的include_package_data=True就万事大吉了。
这个文件到底是个什么
MANIFEST.in 本质上是一个清单文件,放在项目根目录下。它不是一个 Python 脚本,就是一个纯文本文件,每行一条指令,指示 setuptools 或 distutils 在打源码分发包(sdist)时应该包含哪些文件。
注意“源码分发包”这个词,很容易被忽略。MANIFEST.in 控制的只是python setup.py sdist生成的 .tar.gz 或者 .zip 包里有什么。这跟python setup.py bdist_wheel生成的 wheel 包是两套规则,虽然 wheel 内部也有自己的包含机制。
它能做哪些事,不能做哪些事
MANIFEST.in 最大的价值在于管理那些“被自动排除”的文件。Python 打包工具默认只包含 .py 文件,外加 README、setup.py、setup.cfg 之类的少数文件。但实际项目中可能有:
- 配置文件:YAML、TOML、JSON 格式的,比如
config/default.yaml - 静态资源:Web 框架里的 HTML、CSS、JavaScript 文件
- 文档里的图片:一张
docs/diagram.png - C 扩展所需的头文件:
src/ext/some_header.h - 许可证文件或者版权声明
这些文件不会被自动打包。如果没有 MANIFEST.in,这些文件就会在pip install或python setup.py install时消失。不是少了功能,就是安装了之后跑不起来。
值得一提的是,include_package_data=True只对 VCS(git、svn 等)跟踪的文件有效,而且只在 wheel 打包场景下生效。MANIFEST.in 则是直接指定,不管你用不用版本控制,也不管你打什么类型的包。
实际怎么写,以及常见坑点
文件格式很简单,每行一个指令,通常用到的就这几个:
include README.rst CHANGELOG.rst recursive-include config *.yaml *.yml recursive-include static * prune docs/_build看起来挺直观,但有几件事特别容易出错。
第一个坑,也是最大的坑——graft和recursive-include的区别不搞清楚,就很容易混乱。recursive-include path pattern是在path目录下找匹配pattern的文件,而graft path是直接包含path目录下的所有文件。很多人觉得用graft省事,但graft会把你整个目录结构都带进去,包括.git、__pycache__这些。所以建议用recursive-include更可控。
第二个坑,路径问题。MANIFEST.in 里的路径是相对于项目根目录的,不要写成绝对路径,也不要自以为聪明地写../试图跨越目录。基本不会工作的。
第三个坑,如果使用的是较新版本的 setuptools(比如 40.0 以上),有可能会遇到 MANIFEST.in 和include_package_data相互覆盖的问题。简单来说,如果在 MANIFEST.in 里明确 exclude 了某个文件,而include_package_data又想包含它,最终结果可能跟你预期不一样。我习惯的做法是:在 MANIFEST.in 中只定义需要包含的额外文件,把“排除”逻辑留给 setup.cfg 里的exclude配置。
举个比较完整的例子,假设有一个叫mytool的项目:
# 项目根目录下 . ├── mytool/ │ ├── __init__.py │ ├── core.py │ └── templates/ │ └── main.html ├── config/ │ └── prod.yaml ├── tests/ │ ├── test_core.py │ └── fixtures/ │ └── sample_data.csv ├── docs/ │ └── reference.md ├── setup.py ├── setup.cfg └── MANIFEST.in对应的 MANIFEST.in 可以这样写:
include LICENSE include README.md recursive-include config *.yaml recursive-include mytool/templates * recursive-include tests/fixtures *.csv prune docs/_build这样,源码包就会包含 LICENSE、README、所有 YAML 配置文件、模板文件,还有测试用的固定数据,同时排除掉自动生成的文档目录。
最佳实践的几个建议
第一点,把 MANIFEST.in 当成项目的一部分来维护。很多人在项目初期随手写一个,后面再也不看。但如果加了一个数据目录、变更了文件结构,MANIFEST.in 也需要同步更新。一个比较好的习惯是,每次修改 setup.cfg 或 setup.py 时,顺手看一眼 MANIFEST.in。
第二点,利用python setup.py sdist和检查生成的 tar 包来验证。运行python setup.py sdist后,去dist/目录解压生成的包,看看里面到底有什么文件。这是最直接的验证方式。也可以运行python -m tarfile -l dist/*.tar.gz快速列出内容。
第三点,尽量不要在 MANIFEST.in 中使用global-exclude。这个指令会排除所有目录下匹配的文件,容易误伤。如果想在特定目录排除某些文件,用prune或者recursive-exclude配合具体路径更靠谱。
第四点,如果项目使用了命名空间包(namespace package),或者包含 C 扩展,MANIFEST.in 里要特别注意包含 C 源文件和头文件。缺少头文件会导致在用户机器上编译失败。
同类工具或机制的对比
这里最容易被拿来比较的是几个东西:setup.cfg里的[options.package_data]、setup.py里的include_package_data、以及 wheel 包专用的data_files。
setup.cfg里的setup.cfg其实和 MANIFEST.in 有部分重叠功能,但它控制的是包安装后的文件,不是源码分发包的内容。一个比较尴尬的场景是:用package_data指定了要包含某个文件,但 MANIFEST.in 没写,那么打 sdist 时这个文件不会被包含,当用户通过源码安装时,package_data也找不到这个文件。所以两者往往要配合使用。
include_package_data=True是个看起来省心,实际需要小心的配置。它会自动包含版本控制(git 或 svn)中追踪的所有文件。但如果版本控制里有一堆测试数据、模板、或者自动生成的文档,它们就都会被包含。这时用 MANIFEST.in 提供更精细的控制反而更可靠。
wheel 包有自己的一套规则,wheel 格式本身是基于 zip 的,包含文件相对简单。比如.dist-info/METADATA里记录了文件列表。所以如果项目只提供 wheel 包,MANIFEST.in 的重要度会降低一些。但很多项目仍然需要同时提供 sdist,特别是那些需要编译扩展的现实场景。
还有一点,一些现代构建工具(如 flit、poetry)对 MANIFEST.in 的支持度不同。flit 基本不依赖 MANIFEST.in,而是通过 pyproject.toml 中的tool.flit.include来控制。Poetry 也类似,在tool.poetry.packages或者include字段中指定。但即便用这些新工具,如果项目里还保留了 setup.py(有些项目需要兼容老工具链),MANIFEST.in 仍然需要维护。
一句话总结:如果项目还在用 setuptools、distutils 那一套,MANIFEST.in 是必不可少的部分。它负责在打包阶段,把那些系统默认不会携带但实际必要的文件包含进去。不写的话,大概率会在某个不经意的瞬间,被用户报告“文件缺失”的 bug。
