站内链接:

包构建工具

元数据

一个 python 软件包在安装之后元数据文件夹包含两部分: {package}-{version} .dist-info, 业务逻辑文件, 其中:

  • 发行信息(dist info): 描述了安装软件包的安装程序, 软件包的许可证, 安装过程创建的文件, 软件包暴露的入口等
  • 业务逻辑: 开发者本身编写的业务代码目录

比如, toml 包在安装之前的 source tree(源码树)结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
toml git:(master) tree -NC -L 1
.
├── CONTRIBUTING
├── LICENSE
├── MANIFEST.in
├── README.rst
├── RELEASE.rst
├── examples
├── setup.cfg
├── setup.py
├── test.toml
├── tests
├── toml
└── tox.ini

3 directories, 9 files

在运行命令python setup.py install安装命令之后会在相应虚拟环境的 site-packages 目录下生成如下两个文件夹:

1
2
3
4
5
6
➜  site-packages ls tom*
toml:
__init__.py __pycache__ decoder.py encoder.py ordered.py tz.py

toml-0.10.2.dist-info:
INSTALLER LICENSE METADATA RECORD WHEEL top_level.txt

发行版

上节讲解了 Python 包源码树的概念, 目前包的发行主要分为两种类型:

  • 源发行包(source distribution, 简称 sdist), 即发行的是源码, 包含了元数据源码文件, 必须在安装者本地编译才能使用, 构建包的时候可用命令python setup.py sdist来产生源发行包
  • 已编译发行包(built distribution), 即发行的是已编译的发行版, pip 会自动搜索与当前系统当前 CPU 架构相适配的包并进行安装, 目前默认的二进制发行版是 wheel 包

那么, 一个项目是否构建二进制发行包还是源发行包呢? 我们从使用者和开发者的角度来考虑这件事情:

  1. 使用者: 最好两种选择都可以有, 简单的使用那我就直接安装 wheel 包, 这样快速方便, 复杂配合项目使用那我可以使用源发行包自行进行编译安装(高级用法)
  2. 开发者: 从项目的复杂性和依赖关系等因素进行考量, 如果是源码包当然是最方便了, 但若是 whl 包则需要对不同的平台兼容性做各种准备工作

另外, 除了使用者和开发者视角以外, whl 包本身确实给安装包带来了极大的便捷性

  • 体积远远小于源发行包, 下载速度快, 安装速度快
  • 免除了 setup.py 的执行安装, 而 setup.py 是依托于老版本的 python 包管理 disutils, setuptools 的编译机制, 为了搭建编译环境还需要安装各种基础包
  • 不需要编译器

那么, 安装 whl 包的速度和 sdist 的速度到底有和区别呢? 下面以 requests 包为例(如果时依赖项多的大型包可能时间更明显)讲解两者的安装便捷性:

1
2
3
4
5
6
7
8
9
# 1. whl安装requests
# --no-binary=:all: 表示不允许使用二进制包, :all:表示所有的依赖包也是同样的选择
# --only-binary=requests 表示requests仅仅允许二进制包
time python -m pip install --no-cache-dir --force-reinstall --only-binary=requests requests
# -> 输出: python -m pip install --no-cache-dir --force-reinstall --only-binary=requests 1.39s user 0.21s system 25% cpu 6.373 total

# 2. 源码安装
time python -m pip install --no-cache-dir --force-reinstall --no-binary=:all: requests
# -> 输出: python -m pip install --no-cache-dir --force-reinstall --no-binary=:all: 4.39s user 0.92s system 37% cpu 14.044 total

在安装 whl 包时下载到客户端本地的是一个 whl 文件而并非上面的 source tree 文件:

1
2
3
4
5
(bamboo3.6) ➜  Downloads ls requests*
requests-2.27.1-py2.py3-none-any.whl

(bamboo3.6) ➜ Downloads du -sh requests-2.27.1-py2.py3-none-any.whl
64K requests-2.27.1-py2.py3-none-any.whl

其中:

  • py2.py3: 表示支持 python2, python3
  • none: ABI 标签
  • any: 平台范围

但是注意了, 虽然下载的是一个 whl 文件, 但是最终安装到site-packages目录下的元数据还是同源发行包安装方式产生的数据保持一致.

打包工具

上文已经简单的提及 disutils, setuptools, wheel, 这节对 python 包管理中各类打包工具做个简单的阐述, 在介绍包管理工具之前先看下它们的发展历史

  • distutils: 发布于 2000 年, 支持在 Python 中构建和安装其他模块
  • setuptools: 在 2006 年(可能更早?)在 python2.3.5 版本引入, 作为 distutils 的增强集合, 其功能齐全, 旨在帮助打包 python 项目
  • distutils2(已废弃): 在 2009 年提出, 在 2012 年更新停滞, 其旨在替代 Distutils 库, 尽可能包含大部分功能以统一包管理
  • distlib(已废弃): distutils2 的部分实现, 它为 distutils2/packaging 提供底层的功能来增加高级的 API, 并试图使其便于使用
  • distribute(已废弃): distribute 是 setuptools 的替代方案, 其是 setuptools 的一个分支, 其大部分功能在 setuptools 0.7 中被合并
  • pip: 在 2008 年发布后, 其就已经成为 python 包管理的标准, 其是easy_instal(setuptools包含该模块)的替代品, 但仍是基于 setuptools 基础组件.

setuptools

  1. 发展历程和功能

从 1.3 节我们可以看出, 包的整个发展了历程大概如下:

  • 主流程: distutils --> setuptools --> pip
  • 主流程-分支 1: distutils --> setuptools --> distribute --> setuptools --> pip
  • 其他分支

其中 setuptools 最终囊括了 distutils 和 distribute 的大部分功能, 包装了:

  • Python 包和模块的定义, 关于模块术语见站内链接-模块导入文章介绍
  • 构建分发包元数据和安装源发行包, 其中元数据已经上文讲述过,
  • 测试钩子
  • python2 和 Python3 的支持

一个空的setup.py文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from setuptools import setup, find_packages

setup(
name = "demo",
version="0.1.0",
packages = find_packages(),
zip_safe = False,

description = "egg test demo.",
long_description = "egg test demo, haha.",
author = "unusebamboo",
author_email = "unusebamboo@163.com",

license = "GPL",
keywords = ("test", "egg"),
platforms = "Independant",
url = "",
)

该文件会在下文用于 eggs 文件的生成, 同时在该目录下创建一个和 name 名称相同的文件夹用于存放源代码

1
2
3
4
5
6
7
# cat demo/__init__.py
def test():
print("Hello, I'm amoblin.")


if __name__ == '__main__':
test()
  1. easy_install

easy_intallPEAK开发, 上文讲过 pip 是easy_install的替代品, 但实际上easy_install是 setuptools 中的一部分, setuptools 和easy_install的关系可以简单的理解为 setuptools 是项目名称, 而easy_install是项目产生的工具.

  1. eggs

Eggs 在 2004 年成为 setuptools 的一种文件格式, 其使用.egg扩展名, egg 包是比较流行的 python 应用打包部署方式. setuptools通过 eggs 进行包的分发, 开发者在配置好上文的setup.py之后只需要输入python setup.py bdist_egg就会在 dist 目录下生成一个 egg 包, 其实际上是一个 zip 压缩包.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(bamboo3.6) ➜  egg-demo tree -NC
.
├── build
│   ├── bdist.macosx-10.9-x86_64
│   └── lib
│   └── demo
│   └── __init__.py
├── demo
│   └── __init__.py
├── demo.egg-info
│   ├── PKG-INFO
│   ├── SOURCES.txt
│   ├── dependency_links.txt
│   ├── not-zip-safe
│   └── top_level.txt
├── dist
│   └── demo-0.1.0-py3.6.egg
└── setup.py

7 directories, 9 files

此时解压并查看demo-0.1.0-py3.6.egg文件发现其中包含了demo源码目信息, 其他EGG-INFO信息.

1
2
3
4
5
6
7
8
9
(bamboo3.6) ➜  dist unzip demo-0.1.0-py3.6.egg
Archive: demo-0.1.0-py3.6.egg
inflating: EGG-INFO/PKG-INFO
inflating: EGG-INFO/SOURCES.txt
inflating: EGG-INFO/dependency_links.txt
inflating: EGG-INFO/not-zip-safe
inflating: EGG-INFO/top_level.txt
inflating: demo/__init__.py
inflating: demo/__pycache__/__init__.cpython-36.pyc
  1. wheel

wheel 在 2012 年在 PEP 427 被正式引入用于替代 eggs, 其使用.whl扩展名, 该文件实质上也是 zip 包格式, 关于 wheel 包的说明见 1.2 节的发行版说明, 构建 wheel 的命令为: python setup.py bdist_wheel, 若想在生成 wheel 包的同时还生成源发行包, 则可以加入sdist命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(bamboo3.6) ➜  egg-demo tree -NC
.
├── build
│   ├── bdist.macosx-10.9-x86_64
│   └── lib
│   └── demo
│   └── __init__.py
├── demo
│   └── __init__.py
├── demo.egg-info
│   ├── PKG-INFO
│   ├── SOURCES.txt
│   ├── dependency_links.txt
│   ├── not-zip-safe
│   └── top_level.txt
├── dist
│   └── demo-0.1.0-py3-none-any.whl
└── setup.py

7 directories, 9 files

此时解压并查看demo-0.1.0-py3-none-any.whl, 其输出如下:

1
2
3
4
5
6
7
(bamboo3.6) ➜  dist unzip demo-0.1.0-py3-none-any.whl
Archive: demo-0.1.0-py3-none-any.whl
inflating: demo/__init__.py
inflating: demo-0.1.0.dist-info/METADATA
inflating: demo-0.1.0.dist-info/WHEEL
inflating: demo-0.1.0.dist-info/top_level.txt
inflating: demo-0.1.0.dist-info/RECORD
  1. egg 转为 wheel
1
python -m wheel convert demo-0.1.0-py3.6.egg

pip

关于 pip 命令的使用见pip 包管理说明. pip 是目前的包管理的事实标准, 其不再使用 eggs 格式而是采用源发行版来进行包的创建, 这样可以配合requirement file format来提供更加便捷的功能, 生成源发行版的命令为python setup.py sdist, 注意该命令和上面 eggs 生成命令的不同之处.

最后再简单介绍下包分发相关的一些术语, 这些术语主要是 distutils 的术语:

  • module distribution: 模块分发, 作为单一资源下载的所有 python 模块的集合体
  • pure module distribution: 纯模块分发, 仅包含 python 模块和包的集合体
  • non-pure module distribution: 至少包含一个扩展模块的模块集合体
  • distribution root: 源树的顶级目录

二进制包构建

分类

python 的二进制打包经过漫长的发展时期产生了很多二进制打包工具, 有些慢慢被淘汰, 一些则慢慢成为主流, 下面简单的讲解下 python 二进制的打包工具的分类

py2exe py2app pyinstaller appimage fbs Nuitka
win7 ✔️ ✔️ ✔️ ✔️
win8 ✔️ ✔️ ✔️ ✔️
win10 ✔️ ✔️ ✔️ ✔️
mac ✔️ ✔️ ✔️ ✔️
linux ✔️ ✔️ ✔️ ✔️

其中 py2exe, py2app, appimage 因为平台兼容性和软件更新不活跃等原因逐渐小众化, 这里就不再对这三者进行介绍了, 在很长一段时间 python 二进制打包工具都是使用pyinstaller来完成, 即使是 fbs 也是对 pyinstaller 的封装以支持 pysite2 和 pyqt5 项目的. Nuitka则是最新 python 二进制打包工具, 其包含更加丰富的功能, 而且其解决了 python 二进制包安全性的问题, 当然, 目前来说Nuitka的解决方案可能不够多, 文档可能不够完善, 若是碰到问题则可能需要花费一些时间来查找问题, 这是需要注意的.

pyinstaller

fbs

fman build system, 其适用于 pyside 和 pyqt 项目, 不过对于 pyside6 和 pyqt6, 免费版不能使用需要使用fbs pro, 在使用 python 开发桌面应用程序的时候一般都是基于 pyqt, 若是使用 pyinstaller 倒也是可以进行打包操作, 但是相比而言 fbs 封装了 pyinstaller 之外还包含很多附加功能, 后者相比前者更加方便, 而且配合 NIS 能够是打包后的安装包在 windows 环境有一个非常好的安装体验.

  1. fbs 目录结构

在运行fbs startproject之后按照提示就会创建如下的项目目录结构, 其中关于 nsi 和 dos 文档相关为后来添加的 nsi 支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(venv) ➜  japan_bambooo git:(develop) ✗ tree -NC -L 3 .
.
├── README.md
├── __pycache__
├── deploy.nsi
├── doc
│   ├── visa_bambooo_deploy.md
│   └── visa_bambooo_restart.md
├── license.txt
├── scripts
│   └── EnvVarUpdate.nsh
└── src
├── build
│   └── settings
└── main
├── icons
├── python
└── resources

10 directories, 6 files

其中各个目录的介绍如下

  • 真正的 python 源码放在src/main/python/目录下, 该源码是基于 pyqt5 编写的一个桌面应用程序和其他内容逻辑
  • 资源文件放在src/main/resources/base目录下, 包含应用图片以及需要一起打包到应用中的其他配置文件或者二进制文件

其中资源文件默认会放在包主目录下, 在代码中可以通过项目目录所在绝对路径 + 资源文件就可使用某一个资源文件了.

  1. 打包

运行命令fbs freeze就会检查环境并开始进行打包工作, 注意在开始进行打包之前请确保程序本身已经能够运行顺利, 打包完成之后会生成 target 目录, 下面是一个在 mac 环境下打包生成的 target 目录

1
2
3
4
5
6
7
8
9
10
(venv) ➜  japan_bamboo git:(develop) ✗ fbs freeze
(venv) ➜ japan_bamboo git:(develop) ✗ tree -NC -L 1 target
target
├── Icon.icns
├── Icon.iconset
├── PyInstaller
├── mybamboo
└── mybamboo.app

4 directories, 1 file

此时可以通过fbs run在本地运行检查刚刚打包好的应用是否能正常运行.

  1. nis

对于 windows 应用, 通过 pyinstaller 或者 fbs 生成为唯一文件之后就可以直接将二进制程序拷贝到客户服务器上指定的文件夹下运行了(代码兼容不够需要指定位置时), 但是这样显得不够专业, 此时可以配合 NIS 实现正常的 windows 程序安装引导流程, 一步步引导客户进行应用的安装, 这就是 NIS 可以做到的事情.

首先, 根据HM NIS Edit生成脚本向导文件(.nis), 在弹出的多个模板窗口中填入应用程序名, 版本, 出版人, 应用程序网站信息, 应用图标, 安装程序语言, 最后在.nis文件中以如下变量呈现:

1
2
3
4
5
6
7
!define PRODUCT_NAME "bamboo"
!define PRODUCT_VERSION "1.0"
!define PRODUCT_PUBLISHER "大宇宙科技团"
!define PRODUCT_WEB_SITE "https://unusebamboo.top"
!define PRODUCT_DIR_REGKEY "Software\Microsoft\Windows\CurrentVersion\App Paths\bamboo.exe"
!define PRODUCT_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}"
!define PRODUCT_UNINST_ROOT_KEY "HKLM"

其次, 在第四步骤设置应用的默认安装目录, 对于兼容性不够的程序这一步非常重要, 在后续的步骤还有程序的选择, 参数的设置等等

最后, 设置软件卸载的语言提示等操作, 若最后生成的.nis文件不符合需求, 还可以手动编辑更改该文件

在上面的所有设置都完成之后就尅通过HM NIS Edit软件进行编译生成安装包, 到此一个完成的有牌面的 windows 安装包就生成完成了, 其中 NIS 仅仅是在安装包的上层再次进行了封装已符合 windows 安装风格, 换言之就是包了一个非常人性化的安装教程.

Nuitka

  1. 功能说明

python 二进制打包文件一直存在两个问题: 运行速度, 源码反编译, 其中 pyinstaller 的编译时通过将设置 key 来对源码进行加密, 此方法不太可靠, 而 nuitka 包则是将 python 源码转为C++, 即 pyd 文件, 然后再编译为可执行程序, 这样加大了源码反编译的难度, 从而解决了 python 打包的一大问题.

当然, nuitka 也有他自身的问题, 其发布的时间比较晚, 支持的范围可能没有 pyinstaller 那么大, 而且碰到问题时无法快速的从网络上到问题的解决方案, 整体解决方案可能没有 pyinstaller 那么问题, 这是开发者需要考虑的因素.

建议使用的 nuitka 版本为 1.0.6, 高级版本的 PYQT5 打包有问题.

  1. 命令或选项

nuitka 的打包就几个选项, 其中 mac 上可能会有一些额外的选项需要设置

  • --standalone: 独立环境, 方便移植, 不需要再安装其他依赖包, 这点跟 pyinstaller 类似
  • --onefile: 类似 pyinstaller 一样导报成一个单独 exe
  • --plugin-enable=pyqt5: 需要加载的插件, 可以通过nuitka3 --plugin-list命令获取当前支持的插件
  • --output-dir=out: 在指定的目录下进行 build 并生成可执行程序
  • --show-progress: 显示编译进度, 这里并非指代进度条(默认就有)而是输出Nuitka-Progress的很多信息
  • --show-memory: 显示使用的内存, 类似上面的 progress, 都是可有可无的选项
  • --windows-disable-console: 运行可执行程序的时候取消弹框, 一般只在最后才会设置, 开发调试时不会设置该选项
  • --windows-icon-from-icon=./logo.ico: 指定 logo 图标
  • --nofollow-imports: 不编译所有的 import, 一些依赖包例如动态库可能已经不需要编译了, 故这里直接设置所有不需要编译
  • --follow-import-to=util,src: 需要编译成 C++代码的文件夹目录
  1. 实战
1
2
# M1上打包无法运行, 在导入PYQT包的时候直接退出, 为何呢? 直接运行都是没问题的
nuitka3 --macos-create-app-bundle --standalone --plugin-enable=pyqt5 --show-memory --output-dir=qt main.py

引用