在之前的教程中,我们已经研究了如何为Windows和macOS打包你的PyQt5应用程序--将它们分别变成EXE安装包和macOS捆绑包。但为了使你的应用程序真正跨平台,你也应该为Linux提供安装程序。在本教程中,我们将看看如何做到这一点,首先使用PyInstaller将我们的应用程序捆绑成一个可执行的应用程序,然后使用一个叫做fpm 的工具将其转换为一个Linux包。
本教程分为一系列步骤,使用PyInstaller首先构建简单的,然后是更复杂的PyQt5应用程序到Linux可执行文件。你可以选择完全跟随它,或者跳到与你自己的项目最相关的部分。
最后,我们建立一个Ubuntu.deb 软件包,这是在该系统上发布应用程序的通常方法。多亏了fpm 的魔力,该说明也适用于其他Linux发行版,如Redhat.rpm 或 Arch.pacman 。
你总是需要在你的目标系统上编译你的应用程序。所以,如果你想创建一个Ubuntu包,请在Ubuntu上进行。
如果你没有耐心,你可以先下载Ubuntu实例包。
要求
PyInstaller开箱即可使用PyQt5,截至目前,PyInstaller的当前版本与Python 3.6+兼容。无论你在做什么项目,你都应该能够打包你的应用程序。本教程假设你有一个工作中的Python安装,并且pip 包管理工作。
你可以使用pip 安装PyInstaller。
bash
pip3 install PyInstaller
如果你在打包应用程序时遇到问题,你的第一步应该是更新你的PyInstaller和hooks 包的最新版本,使用
bash
pip3 install --upgrade PyInstaller pyinstaller-hooks-contrib
钩子模块包含针对PyInstaller的打包指令,该模块会定期更新。
在虚拟环境中安装(可选)
你也可以选择将PyQt5和PyInstaller安装在一个虚拟环境中(或你的应用程序虚拟环境),以保持你的环境清洁。
bash
python3 -m venv packenv
一旦创建,通过从命令行运行激活虚拟环境----。
bash
call packenv\scripts\activate.bat
最后,安装所需的库。 对于PyQt5,你可以使用------。
蟒蛇
pip3 install PyQt5 PyInstaller
开始使用
从一开始就开始打包你的应用程序是个好主意,这样你就可以在开发过程中确认打包仍然有效。如果你添加了额外的依赖关系,这一点尤其重要。如果你只在最后才考虑打包,就很难准确地调试出问题所在。
在这个例子中,我们将从一个简单的骨架应用开始,它并不做任何有趣的事情。一旦我们得到了基本的打包过程,我们将扩展应用程序以包括图标和数据文件。我们将在进行过程中确认构建。
首先,为你的应用程序创建一个新的文件夹,然后在一个名为app.py 的文件中添加以下骨架应用程序。你也可以下载源代码和相关文件
蟒蛇
from PyQt5 import QtWidgets
import sys
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Hello World")
l = QtWidgets.QLabel("My simple app.")
l.setMargin(10)
self.setCentralWidget(l)
self.show()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
app.exec()
这是一个基本的裸体应用,它创建了一个自定义的QMainWindow ,并在其中添加了一个简单的小工具QLabel 。你可以按以下方式运行这个应用程序。
bash
python app.py
这应该会产生以下窗口(在Ubuntu上)。
PyQt5中的简单骨架应用程序
构建一个基本的应用程序
现在我们已经有了简单的应用程序骨架,我们可以运行我们的第一个构建测试,以确保一切正常。
打开你的终端(shell)并导航到包含你的项目的文件夹。现在你可以运行以下命令来运行PyInstaller构建。
蟒蛇
pyinstaller app.py
你会看到一些信息输出,给出关于PyInstaller正在做什么的调试信息。这些信息对于调试你的构建中的问题很有用,但也可以忽略不计。我在我的系统上运行该命令得到的输出如下所示。
bash
$ pyinstaller app.py
85 INFO: PyInstaller: 4.10
85 INFO: Python: 3.9.7
88 INFO: Platform: Linux-5.13.0-39-generic-x86_64-with-glibc2.34
89 INFO: wrote /home/martin/pyinstaller/linux2/no-datas/pyqt5/app.spec
91 INFO: UPX is not available.
91 INFO: Extending PYTHONPATH with paths
['/home/martin/pyinstaller/linux2/no-datas/pyqt5']
236 INFO: checking Analysis
240 INFO: Building because inputs changed
240 INFO: Initializing module dependency graph...
243 INFO: Caching module graph hooks...
255 INFO: Analyzing base_library.zip ...
2008 INFO: Processing pre-find module path hook distutils from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks/pre_find_module_path/hook-distutils.py'.
2013 INFO: distutils: retargeting to non-venv dir '/usr/lib/python3.9'
4231 INFO: Caching module dependency graph...
4348 INFO: running Analysis Analysis-00.toc
4379 INFO: Analyzing /home/martin/pyinstaller/linux2/no-datas/pyqt5/app.py
4403 INFO: Processing module hooks...
4403 INFO: Loading module hook 'hook-PyQt5.py' from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks'...
4559 WARNING: Hidden import "sip" not found!
4559 INFO: Loading module hook 'hook-xml.etree.cElementTree.py' from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks'...
4560 INFO: Loading module hook 'hook-heapq.py' from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks'...
4562 INFO: Loading module hook 'hook-distutils.py' from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks'...
4568 INFO: Loading module hook 'hook-xml.py' from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks'...
4627 INFO: Loading module hook 'hook-PyQt5.QtWidgets.py' from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks'...
4709 INFO: Loading module hook 'hook-difflib.py' from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks'...
4711 INFO: Loading module hook 'hook-multiprocessing.util.py' from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks'...
4712 INFO: Loading module hook 'hook-sysconfig.py' from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks'...
4713 INFO: Loading module hook 'hook-encodings.py' from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks'...
4759 INFO: Loading module hook 'hook-PyQt5.QtGui.py' from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks'...
4807 INFO: Loading module hook 'hook-lib2to3.py' from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks'...
4820 INFO: Loading module hook 'hook-pickle.py' from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks'...
4827 INFO: Loading module hook 'hook-PyQt5.QtCore.py' from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks'...
4853 INFO: Loading module hook 'hook-distutils.util.py' from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks'...
4862 INFO: Looking for ctypes DLLs
4897 INFO: Analyzing run-time hooks ...
4900 INFO: Including run-time hook '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks/rthooks/pyi_rth_subprocess.py'
4903 INFO: Including run-time hook '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks/rthooks/pyi_rth_pkgutil.py'
4905 INFO: Including run-time hook '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks/rthooks/pyi_rth_multiprocessing.py'
4910 INFO: Including run-time hook '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks/rthooks/pyi_rth_inspect.py'
4912 INFO: Including run-time hook '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks/rthooks/pyi_rth_pyqt5.py'
4916 INFO: Looking for dynamic libraries
6561 INFO: Looking for eggs
6561 INFO: Python library not in binary dependencies. Doing additional searching...
6596 INFO: Using Python library /lib/x86_64-linux-gnu/libpython3.9.so.1.0
6604 INFO: Warnings written to /home/martin/pyinstaller/linux2/no-datas/pyqt5/build/app/warn-app.txt
6625 INFO: Graph cross-reference written to /home/martin/pyinstaller/linux2/no-datas/pyqt5/build/app/xref-app.html
6643 INFO: checking PYZ
6645 INFO: Building because name changed
6645 INFO: Building PYZ (ZlibArchive) /home/martin/pyinstaller/linux2/no-datas/pyqt5/build/app/PYZ-00.pyz
6923 INFO: Building PYZ (ZlibArchive) /home/martin/pyinstaller/linux2/no-datas/pyqt5/build/app/PYZ-00.pyz completed successfully.
6926 INFO: checking PKG
6926 INFO: Building because name changed
6927 INFO: Building PKG (CArchive) app.pkg
6959 INFO: Building PKG (CArchive) app.pkg completed successfully.
6962 INFO: Bootloader /home/martin/.local/lib/python3.9/site-packages/PyInstaller/bootloader/Linux-64bit-intel/run
6963 INFO: checking EXE
6963 INFO: Building because name changed
6964 INFO: Building EXE from EXE-00.toc
6969 INFO: Copying bootloader EXE to /home/martin/pyinstaller/linux2/no-datas/pyqt5/build/app/app
6970 INFO: Appending PKG archive to custom ELF section in EXE
6979 INFO: Building EXE from EXE-00.toc completed successfully.
6981 INFO: checking COLLECT
6982 INFO: Building COLLECT COLLECT-00.toc
8674 INFO: Building COLLECT COLLECT-00.toc completed successfully.
如果你看看你的文件夹,你会发现你现在有两个新的文件夹dist 和build 。
由PyInstaller创建的build和dist文件夹
下面是文件夹内容的一个截断列表,显示了build 和dist 文件夹。
bash
.
├── app.py
├── app.spec
├── build
│ └── app
│ ├── localpycos
│ ├── Analysis-00.toc
│ ├── COLLECT-00.toc
│ ├── EXE-00.toc
│ ├── PKG-00.pkg
│ ├── PKG-00.toc
│ ├── PYZ-00.pyz
│ ├── PYZ-00.toc
│ ├── app
│ ├── app.pkg
│ ├── base_library.zip
│ ├── warn-app.txt
│ └── xref-app.html
└── dist
├── app
│ ├── lib-dynload
│ ├── PyQt5
│ ...
│ ├── app
│ └── libQt5Core.so.5
└── app.app
build 文件夹被PyInstaller用来收集和准备捆绑的文件,它包含分析的结果和一些额外的日志。在大多数情况下,你可以忽略这个文件夹的内容,除非你试图调试问题。
dist (代表 "分发")文件夹包含要分发的文件。这包括你的应用程序,捆绑成一个可执行文件,以及任何相关的库(例如PyQt5)和二进制.so 文件。
运行你的应用程序所需的一切都在这个文件夹中,这意味着你可以把这个文件夹 "分发 "给其他人来运行你的应用程序。
你现在可以尝试自己运行你的应用程序,从dist 文件夹中运行可执行文件,命名为app 。在短暂的延迟之后,你会看到你的应用程序的熟悉窗口弹出,如下图所示。
简单的应用程序,在被打包后运行
在与你的Python文件相同的文件夹中,除了build 和dist 文件夹外,PyInstaller还将创建一个.spec 文件。 在下一节中,我们将看一下这个文件,它是什么,它做什么。
Spec文件
.spec 文件包含了PyInstaller用来打包你的应用程序的构建配置和说明。每个PyInstaller项目都有一个.spec 文件,它是根据你在运行pyinstaller 时传递的命令行选项生成的。
当我们用我们的脚本运行pyinstaller 时,除了我们的 Python 应用程序文件的名称外,我们没有传入任何其他东西。这意味着我们的规范文件目前只包含默认配置。如果你打开它,你会看到与我们下面类似的东西。
蟒蛇
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(['app.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
[],
exclude_binaries=True,
name='app',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True,
disable_windowed_traceback=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None )
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='app')
首先要注意的是,这是一个Python文件,意味着你可以编辑它,使用Python代码来计算设置值。这对于复杂的构建来说非常有用,例如,当你针对不同的平台,想要有条件地定义额外的库或依赖关系来捆绑。
一旦生成了.spec 文件,你可以把它传递给pyinstaller ,而不是你的脚本,以重复之前的构建过程。现在运行它来重建你的可执行文件。
bash
pyinstaller app.spec
由此产生的构建将与用于生成.spec 文件的构建相同(假设你没有做任何修改)。对于许多PyInstaller配置的改变,你可以选择通过命令行参数,或者修改你现有的.spec 文件。你选择哪种方式取决于你。
调整构建
到目前为止,我们已经为一个非常基本的应用程序创建了一个简单的首次构建。现在我们来看看我们可以做些什么来调整我们的构建。
命名你的应用程序
你可以做的最简单的改变之一是为你的应用程序提供一个合适的 "名字"。默认情况下,应用程序采用你的源文件的名称(去掉扩展名),例如main 或app 。这通常不是你想要的。
你可以通过编辑.spec 文件,在EXE和COLLECT块下添加一个name= ,为PyInstaller提供一个更好的名字,用于你的可执行文件(和dist 文件夹)。 在Linux上,你将希望使用一个没有空格的名字(用连字符代替)。
蟒蛇
exe = EXE(pyz,
a.scripts,
[],
exclude_binaries=True,
name='hello-world',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False
)
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='hello-world')
EXE下的名字是可执行文件的名字,COLLECT下的名字是输出文件夹的名字。通常情况下,你会希望这两个名字是一样的。
另外,你可以重新运行pyinstaller 命令,并将-n 或--name 配置标志与你的app.py 脚本一起传递。
bash
pyinstaller -n "hello-world" app.py
# or
pyinstaller --name "hello-world" app.py
由此产生的可执行文件将被命名为hello-world ,解压后的构建文件将放在dist\hello-world\ 。.spec 文件的名称取自命令行上传递的名称,因此这也将为你创建一个新的规范文件,在你的根文件夹中称为hello-world.spec 。
如果你已经创建了一个新的.spec ,请删除旧的以避免混淆
带有自定义名称 "hello-world "的应用程序
应用程序图标
我们可以做的一个简单改进是改变应用程序的图标,它在应用程序运行时显示。我们可以直接在代码中设置这个图标。为了在我们的窗口上显示一个图标,我们需要稍微修改一下我们的简单应用程序,添加一个对.setWindowIcon() 的调用。
python
from PyQt5 import QtWidgets, QtGui
import sys
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Hello World")
l = QtWidgets.QLabel("My simple app.")
l.setMargin(10)
self.setCentralWidget(l)
self.show()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
app.setWindowIcon(QtGui.QIcon('penguin.svg'))
w = MainWindow()
app.exec()
在这里,我们将.setWindowIcon 的调用添加到app 实例中。这定义了一个默认的图标,用于我们应用程序的所有窗口。如果你愿意,你可以通过在窗口本身调用.setWindowIcon ,在每个窗口的基础上覆盖这个图标。
如果你运行上述程序,你现在应该看到图标出现在Dock上。
显示自定义企鹅图标的窗口
你可以用PNG文件代替SVG,但如果使用PNG,请确保图标足够大,不会因为缩放而显得模糊。
即使你没有看到图标,也要继续阅读
处理相对路径
这里有一个小问题,可能不是很明显。为了证明这一点,请打开一个shell,切换到我们的脚本所在的文件夹。用以下命令运行它
bash
python3 app.py
如果图标在正确的位置,你应该看到它们。现在换到父文件夹,并尝试再次运行你的脚本(将<folder> 改为你的脚本所在的文件夹的名称)。
bash
cd ..
python3 <folder>/app.py
缺少图标的窗口。
图标没有出现。发生了什么事?
我们正在使用相对路径来引用我们的数据文件。这些路径是相对于当前工作目录而言的,而不是你的脚本所在的文件夹。因此,如果你从其他地方运行脚本,它将无法找到这些文件。
图标不显示的一个常见原因是在IDE中运行的例子,它使用项目根目录作为当前工作目录。
这在应用打包前是个小问题,但一旦安装后,你就不知道运行时的当前工作目录是什么了--如果它不对,你的应用就找不到任何东西。 在我们进一步行动之前,我们需要解决这个问题,我们可以通过使我们的路径与我们的应用程序文件夹相对应来做到这一点。
在下面更新的代码中,我们定义了一个新的变量basedir ,使用os.path.dirname 来获取__file__ 的包含文件夹,该文件夹包含当前 Python 文件的完整路径。然后我们用这个来建立图标的相对路径,使用os.path.join() 。
由于我们的app.py 文件在我们的文件夹的根部,所有其他的路径都是相对于它的。
python
from PyQt5 import QtWidgets, QtGui
import sys, os
basedir = os.path.dirname(__file__)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Hello World")
l = QtWidgets.QLabel("My simple app.")
l.setMargin(10)
self.setCentralWidget(l)
self.show()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
app.setWindowIcon(QtGui.QIcon(os.path.join(basedir, 'penguin.svg')))
w = MainWindow()
app.exec_()
试着从父文件夹中再次运行你的应用程序 -- 你会发现,无论你从哪里启动应用程序,图标现在都会如期出现。
把这个添加到你的脚本中,现在运行它应该会在你的窗口和任务栏上显示图标。最后一步是确保这个图标与你的应用程序一起被正确打包,并且在从dist 文件夹中运行时继续显示。
试试吧,它不会的。
问题是,我们的应用程序现在对一个外部数据文件(图标文件)有依赖性,而这个文件并不是我们源文件的一部分。为了使我们的应用程序工作,我们现在需要把这个数据文件一起分发出去。PyInstaller可以为我们做到这一点,但我们需要告诉它我们想要包括什么,以及把它放在输出的哪里。
在下一节中,我们将看一下你在管理与你的应用程序相关的数据文件方面的可用选项。
数据文件和资源
到目前为止,我们成功地建立了一个没有外部依赖性的简单应用。然而,一旦我们需要加载一个外部文件(本例中是一个图标),我们就遇到了一个问题。该文件没有被复制到我们的dist 文件夹中,所以不能被加载。
在这一节中,我们将看看我们有哪些选项能够将外部资源,如图标或Qt Designer.ui 文件,与我们的应用程序捆绑起来。
用PyInstaller捆绑数据文件
让这些数据文件进入dist 文件夹的最简单方法是告诉PyInstaller把它们复制过来。 PyInstaller接受一个要复制的单个文件路径的列表,以及一个相对于dist/<app name> 文件夹的文件夹路径,它应该把它们复制到那里。
与其他选项一样,这可以通过命令行参数指定。--add-data
bash
pyinstaller --add-data "penguin.svg:." --name "hello-world" app.py
你可以多次提供`--add-data`。注意,路径分隔符是特定平台的,在Linux或Mac上使用`:`,而在Windows上使用`;`。
或者通过规格文件的分析部分的datas 列表,如下所示。
python
a = Analysis(['app.py'],
pathex=[],
binaries=[],
datas=[('penguin.svg', '.')],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
然后执行.spec 文件,用
bash
pyinstaller hello-world.spec
在这两种情况下,我们都是告诉PyInstaller将指定的文件penguin.svg 复制到. ,也就是输出文件夹dist 。如果我们愿意,可以在这里指定其他的位置。在命令行中,源文件和目标文件是由路径分隔符: ,而在.spec 文件中,数值是以字符串的2元组形式提供的。
如果你运行构建,你应该看到你的.svg 文件现在在输出文件夹dist ,准备与你的应用程序一起发布。
图标文件被复制到dist文件夹中
如果你从dist 中运行你的应用程序,你现在应该看到窗口上的图标,以及任务栏上的图标,正如预期的那样。
企鹅图标显示在Dock上
该文件必须在Qt中使用相对路径加载,并且在EXE中的相对位置与在.py 文件中的相对位置相同,这样才能发挥作用。
捆绑数据文件夹
通常情况下,你会有不止一个数据文件想要包含在你打包的文件中。最新的PyInstaller版本允许你像捆绑文件一样捆绑文件夹,保持子文件夹的结构。例如,让我们扩展我们的应用程序,增加一些额外的图标,并把它们放在一个文件夹下。
python
from PyQt5.QtWidgets import QMainWindow, QApplication, QLabel, QVBoxLayout, QPushButton, QWidget
from PyQt5.QtGui import QIcon
import sys, os
basedir = os.path.dirname(__file__)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Hello World")
layout = QVBoxLayout()
label = QLabel("My simple app.")
label.setMargin(10)
layout.addWidget(label)
button = QPushButton("Push")
button.setIcon(QIcon(os.path.join(basedir, "icons", "lightning.svg")))
button.pressed.connect(self.close)
layout.addWidget(button)
container = QWidget()
container.setLayout(layout)
self.setCentralWidget(container)
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
app.setWindowIcon(QIcon(os.path.join(basedir, "icons", "penguin.svg")))
w = MainWindow()
app.exec_()
这些图标(都是SVG文件)被存储在一个名为 "图标 "的子文件夹下。
bash
.
├── app.py
└── icons
└── lightning.svg
└── penguin.svg
如果你运行这个,你会看到下面的窗口,按钮上有一个图标,Dock里有一个图标。
有两个图标的窗口,和一个按钮。
这些路径使用了Unix的正斜杠/ 惯例,所以它们对macOS来说是跨平台的。如果你只为Windows开发,你可以使用\\
为了将icons 文件夹复制到我们的构建应用程序中,我们只需要将该文件夹添加到我们的.spec 文件Analysis 块。至于单个文件,我们把它作为一个元组添加,其中包括源路径(来自我们的项目文件夹)和目标文件夹在生成的dist 文件夹下。
python
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(['app.py'],
pathex=[],
binaries=[],
datas=[('icons', 'icons')], # tuple is (source_folder, destination_folder)
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
[],
exclude_binaries=True,
name='hello-world',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False,
disable_windowed_traceback=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None )
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='hello-world')
如果你使用这个规格文件运行构建,你会看到icons 文件夹被复制到dist 文件夹。如果你从该文件夹中运行应用程序,图标将按预期显示 -- 相对路径在新的位置保持正确。
或者,你可以使用Qt的QResource架构来捆绑你的数据文件。请看我们的教程以了解更多信息。
创建一个Linux软件包(Ubuntu deb)
到目前为止,我们已经使用PyInstaller将应用程序和相关的数据文件捆绑成一个Linux可执行文件。这个捆绑过程的输出是一个文件夹。 然而,为了与其他人分享这个应用程序并允许他们安装它,我们需要创建一个Linux软件包。软件包是可分发的文件,允许用户在他们的Linux系统上安装软件,以及设置诸如应用程序在Dock/菜单中的条目。
在Ubuntu(和Debian),软件包被命名为.deb ,在Redhat.rpm ,在Arch Linux.pacman 。这些文件都是不同的格式,但值得庆幸的是,构建它们的过程是相同的:使用一个名为fpm的工具。
在本教程中,我们将通过创建Linux软件包的步骤,以Ubuntu的.deb 文件为例。不过,你也可以在自己的系统中使用同样的步骤。
安装fpm
fpm 工具是用ruby编写的,需要安装ruby才能使用它。例如,使用你的系统软件包管理器安装ruby。
bash
$ sudo apt-get install ruby
一旦ruby安装完毕,你就可以使用gem 工具来安装fpm 。
bash
$ gem install fpm --user-install
如果你看到一个警告,例如你的PATH中没有/home/martin/.local/share/gem/ruby/2.7.0/bin,你需要在你的.bashrc 文件中加入该路径。
...就这样了。一旦安装完成,你就可以使用fpm 。你可以通过运行来检查它是否已经安装并工作。
bash
$ fpm --version
1.14.2
检查你的构建
在终端,切换到包含你的应用程序源文件的文件夹,并运行PyInstaller构建,生成dist 。在文件管理器中打开dist 文件夹,双击应用程序的可执行文件,测试生成的构建文件是否如预期的那样运行(它可以工作,并且出现图标)。
如果一切正常,你就可以打包应用程序了--如果不正常,请回去仔细检查一切。
在打包之前,测试你构建的应用程序总是一个好主意。这样,如果有什么问题,你就知道问题出在哪里了。
现在让我们用fpm 来打包我们的文件夹。
构建你的软件包
Linux文件是用来安装各种应用程序的,包括系统工具。正因为如此,它们被设置成允许你在Linux文件系统中的任何地方放置文件--并且有特定的正确位置来放置不同的文件。对于像我们这样的捆绑包,我们可以--幸好--把我们的可执行文件和相关的数据文件都放在同一个文件夹下(在/opt )。然而,为了让我们的应用程序在菜单/搜索中显示出来,我们还需要在/usr/share/applications 下安装一个.desktop 文件。
最简单的方法是在一个文件夹中重新创建目标文件结构,然后告诉fpm ,以该文件夹为根目录进行打包,以确保事情最终被放在正确的位置。这个过程也很容易用脚本自动完成(见下文)。
在你的项目根目录下,创建一个名为package 的新文件夹,以及映射到目标文件系统的子文件夹 --/opt 将存放我们的应用程序文件夹 hello-world ,/usr/share/applications 将存放我们的.desktop 文件。而/usr/share/icons... 将存放我们的应用程序图标。
bash
$ mkdir -p package/opt
$ mkdir -p package/usr/share/applications
$ mkdir -p package/usr/share/icons/hicolor/scalable/apps
接下来复制(递归,用-r ,包括子文件夹)dist/app 的内容到package/opt/hello-world --/opt/hello-world 路径是安装后我们应用程序文件夹的目的地。
bash
$ cp -r dist/hello-world package/opt/hello-world
我们正在复制dist/hello-world 文件夹。这个文件夹的名称将取决于在PyInstaller中配置的应用程序名称。
图标
我们已经使用penguin.svg 文件为我们的应用程序在运行时设置了一个图标。然而,我们希望我们的应用程序能在Dock/菜单中显示它的图标。为了正确做到这一点,我们需要将我们的应用程序图标复制到一个特定的位置,在/usr/share/icons 。
这个文件夹包含了所有安装在系统上的图标主题,但应用程序的默认图标总是放在后备的 hicolor 主题中,在/usr/share/icons/hicolor 。在这个文件夹中,有各种不同尺寸的图标的文件夹。
bash
$ ls /usr/share/icons/hicolor/
128x128/ 256x256/ 64x64/ scalable/
16x16/ 32x32/ 72x72/ symbolic/
192x192/ 36x36/ 96x96/
22x22/ 48x48/ icon-theme.cache
24x24/ 512x512/ index.theme
我们正在使用scalable 文件夹,因为我们的图标是一个SVG(可扩展矢量图)。如果你使用一个特定尺寸的PNG文件,把它放在正确的位置--并且可以随意添加多个不同的尺寸,以确保你的应用程序图标在缩放时看起来不错。应用程序图标放在子文件夹apps 。
bash
$ cp icons/penguin.svg package/usr/share/icons/hicolor/scalable/apps/hello-world.svg
用你的应用程序的名字来命名图标的目标文件名,以避免它与任何其他文件相冲突在这里,我们把它叫做hello-world.svg 。
.desktop 文件
.desktop 文件是一个文本配置文件,它告诉 Linux 桌面关于桌面应用程序的信息 -- 例如,可执行文件的位置、名称和要显示的图标。你应该为你的应用程序包括一个.desktop 文件,使它们易于使用。 下面显示了一个.desktop 文件的例子 -- 将其添加到你的项目的根文件夹中 -- 名称为hello-world.desktop ,并做任何你喜欢的修改。
ini
[Desktop Entry]
# The type of the thing this desktop file refers to (e.g. can be Link)
Type=Application
# The application name.
Name=Hello World
# Tooltip comment to show in menus.
Comment=A simple Hello World application.
# The path (folder) in which the executable is run
Path=/opt/hello-world
# The executable (can include arguments)
Exec=/opt/hello-world/hello-world
# The icon for the entry, using the name from `hicolor/scalable` without the extension.
# You can also use a full path to a file in /opt.
Icon=hello-world
关于创建.desktop 文件的更多信息,请看这个文档。
现在,hello-world.desktop 文件已经准备好了,我们可以把它复制到我们的安装包中,用。
bash
$ cp hello-world.desktop package/usr/share/applications
权限
软件包保留了打包时的安装文件的权限,但将由root 。为了让普通用户能够运行该应用程序,你需要改变所创建文件的权限。
我们可以递归应用正确的权限755--所有者可以读/写/执行,组/其他人可以读/执行。和644,所有者可以读/写,组/其他人可以红到我们的可执行文件夹和图标/桌面文件的内容。
bash
$ find package/opt/hello-world -type f -exec chmod 755 -- {} +
$ find package/usr/share -type f -exec chmod 644 -- {} +
构建你的软件包
现在一切都在我们的package "文件系统 "中,我们准备开始构建软件包本身。
在你的外壳中输入以下内容。
bash
fpm -C package -s dir -t deb -n "hello-world" -v 0.1.0 -p hello-world.deb
参数依次为:。
-C在搜索文件之前要改变的文件夹:我们的 文件夹package-s要打包的源的类型:在我们的例子中, ,一个文件夹。dir-t要建立的包的类型: Debian/Ubuntu包deb-n应用程序的名称。"hello-world"-v该应用程序的版本。0.1.0-p要输出的软件包名称:hello-world-deb
关于更多的命令行参数,请参见fpm文档。
你可以通过改变-t 参数来创建其他软件包类型(用于其他Linux发行版)。
几秒钟后,你应该看到一条信息,表明软件包已经被创建。
bash
$ fpm -C package -s dir -t deb -n "hello-world" -v 0.1.0 -p hello-world.deb
Created package {:path=>"hello-world.deb"}
安装
包已经准备好了!让我们来安装它。
bash
$ sudo dpkg -i hello-world.deb
在安装完成后,你会看到一些输出。
蟒蛇
Selecting previously unselected package hello-world.
(Reading database ... 172208 files and directories currently installed.)
Preparing to unpack hello-world.deb ...
Unpacking hello-world (0.1.0) ...
Setting up hello-world (0.1.0) ...
一旦安装完成,你可以检查文件是否在你期望的地方,在下面/opt/hello-world
bash
$ ls /opt/hello-world
app libpcre2-8.so.0
base_library.zip libpcre.so.3
icons libpixman-1.so.0
libatk-1.0.so.0 libpng16.so.16
libatk-bridge-2.0.so.0 libpython3.9.so.1.0
etc.
接下来尝试从菜单/底座上运行应用程序 -- 你可以搜索 "Hello World",应用程序会被找到(感谢.desktop 文件)。
应用程序显示在Ubuntu的搜索面板上,并且也会出现在其他环境的菜单中。
如果你运行该应用程序,图标会如期显示出来。
应用程序运行后,所有图标都会如期显示。
编写脚本
我们已经走过了从PyQt5应用程序构建一个可安装的Ubuntu.deb 软件包所需的步骤。这并不复杂,但如果你不得不多做几次,很快就会变得相当乏味,而且容易出错。为了避免问题,我建议用一个简单的bash脚本和fpm 自己的自动化工具来编写这个脚本。
在这一节中,我将给你一些脚本,使我们为Hello World应用程序所做的构建自动化。
package.sh
保存在你的项目根目录和chmod +x ,使其可执行。
sh
#!/bin/sh
# Create folders.
[ -e package ] && rm -r package
mkdir -p package/opt
mkdir -p package/usr/share/applications
mkdir -p package/usr/share/icons/hicolor/scalable/apps
# Copy files (change icon names, add lines for non-scaled icons)
cp -r dist/hello-world package/opt/hello-world
cp icons/penguin.svg package/usr/share/icons/hicolor/scalable/apps/hello-world.svg
cp hello-world.desktop package/usr/share/applications
# Change permissions
find package/opt/hello-world -type f -exec chmod 755 -- {} +
find package/usr/share -type f -exec chmod 644 -- {} +
.fpm 文件
fpm 允许你在一个配置文件中存储打包的配置。文件名必须是 ,而且必须在你运行 工具的文件夹中。我们的配置如下。.fpm fpm
sh
-C package
-s dir
-t deb
-n "hello-world"
-v 0.1.0
-p hello-world.deb
你可以在执行fpm时通过正常的命令行参数来覆盖任何你喜欢的选项。
执行构建
有了这些脚本,我们的应用程序就可以用命令进行可重复的打包了。
bash
pyinstaller hello-world.spec
./package.sh
fpm
欢迎你自己进一步定制这些构建脚本,以适应你自己的项目!
总结
在本教程中,我们已经介绍了如何使用PyInstaller将你的PyQt5应用程序构建成一个Linux可执行文件,包括将数据文件与你的代码一起添加。然后,我们走过了创建Ubuntu.deb 软件包的过程,以便将你的应用程序分发给其他人。按照这些步骤,你应该能够将你自己的应用程序打包并提供给其他人使用。
关于用Python构建图形用户界面的深入指导,请看我的PyQt书。