用PyInstaller将PySide6应用构建macOS应用程序的教程

1,544 阅读25分钟

如果你不能与其他人分享你自己的桌面应用程序,那就没有什么乐趣了--无论这意味着以商业方式发布它,在网上分享它,还是只是把它送给你认识的人。分享你的应用程序可以让其他人从你的辛勤工作中受益

好消息是,有一些工具可以帮助你在你的Python应用程序中做到这一点,它们与使用PySide6构建的应用程序配合得很好。在本教程中,我们将看看最流行的打包Python应用程序的工具。PyInstaller

本教程分为一系列步骤,使用PyInstaller首先构建简单的,然后是更复杂的PySide6应用程序,使之成为可分发的macOS应用程序包。你可以选择完全跟随它,或者跳到与你自己的项目最相关的部分。

最后,我们将建立一个macOS磁盘镜像,这是在macOS上分发应用程序的通常方法。

你总是需要在你的目标系统上编译你的应用程序。因此,如果你想创建一个Mac .app,你需要在Mac上完成,对于EXE,你需要使用Windows。

Example Disk Image for macOS macOS的磁盘镜像安装程序示例

如果你没有耐心,你可以先下载macOS的实例磁盘镜像

要求

PyInstaller与PySide6开箱即用,截至本文撰写时,PyInstaller的当前版本与Python 3.6以上版本兼容。无论你在做什么项目,你都应该能够打包你的应用程序。

你可以使用pip 来安装PyInstaller

bash

pip3 install PyInstaller

如果你在打包你的应用程序遇到问题,你的第一步应该是更新你的PyInstallerhooks打包的最新版本,使用

bash

pip3 install --upgrade PyInstaller pyinstaller-hooks-contrib

钩子模块包含了PyInstaller的特定打包指令,该模块会定期更新。

在虚拟环境中安装(可选)

你也可以选择将PySide6和PyInstaller安装在一个虚拟环境中(或你的应用程序虚拟环境),以保持你的环境清洁。

bash

python3 -m venv packenv

一旦创建,通过从命令行运行激活虚拟环境----

bash

call packenv\scripts\activate.bat

最后,安装所需的库。 对于PySide6,你将使用 -

bash

pip3 install PySide6 PyInstaller

起步

从一开始就开始打包你的应用程序是个好主意,这样你就可以在开发过程中确认打包仍然有效。如果你添加了额外的依赖项,这一点尤其重要。如果你只在最后才考虑打包,那么就很难准确地调试出问题所在

在这个例子中,我们将从一个简单的骨架应用开始,它并不做任何有趣的事情。一旦我们得到了基本的打包过程,我们将扩展应用程序以包括图标和数据文件。我们将在进行过程中确认构建。

首先,为你的应用程序创建一个新的文件夹,然后在一个名为app.py 的文件中添加以下骨架应用程序。你也可以下载源代码和相关文件

bash

from PySide6 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

这应该产生以下窗口(在macOS上):

Simple skeleton app in PySide6 PySide6中的简单骨架应用程序

构建一个基本的应用程序

现在我们有了简单的应用程序骨架,我们可以运行我们的第一个构建测试,以确保一切都在工作。

打开你的终端(命令提示符)并导航到包含你的项目的文件夹。现在你可以运行以下命令来运行PyInstaller构建。

pyinstaller --windowed app.py

--windowed 标志对于告诉PyInstaller构建一个macOS.app 捆绑包是必要的。

你会看到一些信息输出,给出关于PyInstaller正在做什么的调试信息。这些信息对于调试你的构建中的问题很有用,但也可以忽略不计。我在我的系统上运行该命令得到的输出如下所示。

bash

martin@MacBook-Pro pyside6 % pyinstaller --windowed app.py
74 INFO: PyInstaller: 4.8
74 INFO: Python: 3.9.9
83 INFO: Platform: macOS-10.15.7-x86_64-i386-64bit
84 INFO: wrote /Users/martin/app/pyside6/app.spec
87 INFO: UPX is not available.
88 INFO: Extending PYTHONPATH with paths
['/Users/martin/app/pyside6']
447 INFO: checking Analysis
451 INFO: Building because inputs changed
452 INFO: Initializing module dependency graph...
455 INFO: Caching module graph hooks...
463 INFO: Analyzing base_library.zip ...
3914 INFO: Processing pre-find module path hook distutils from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks/pre_find_module_path/hook-distutils.py'.
3917 INFO: distutils: retargeting to non-venv dir '/usr/local/Cellar/python@3.9/3.9.9/Frameworks/Python.framework/Versions/3.9/lib/python3.9'
6928 INFO: Caching module dependency graph...
7083 INFO: running Analysis Analysis-00.toc
7091 INFO: Analyzing /Users/martin/app/pyside6/app.py
7138 INFO: Processing module hooks...
7139 INFO: Loading module hook 'hook-PyQt6.QtWidgets.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7336 INFO: Loading module hook 'hook-xml.etree.cElementTree.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7337 INFO: Loading module hook 'hook-lib2to3.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7360 INFO: Loading module hook 'hook-PyQt6.QtGui.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7397 INFO: Loading module hook 'hook-PyQt6.QtCore.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7422 INFO: Loading module hook 'hook-encodings.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7510 INFO: Loading module hook 'hook-distutils.util.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7513 INFO: Loading module hook 'hook-pickle.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7515 INFO: Loading module hook 'hook-heapq.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7517 INFO: Loading module hook 'hook-difflib.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7519 INFO: Loading module hook 'hook-PyQt6.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7564 INFO: Loading module hook 'hook-multiprocessing.util.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7565 INFO: Loading module hook 'hook-sysconfig.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7574 INFO: Loading module hook 'hook-xml.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7677 INFO: Loading module hook 'hook-distutils.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7694 INFO: Looking for ctypes DLLs
7712 INFO: Analyzing run-time hooks ...
7715 INFO: Including run-time hook '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks/rthooks/pyi_rth_subprocess.py'
7719 INFO: Including run-time hook '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks/rthooks/pyi_rth_pkgutil.py'
7722 INFO: Including run-time hook '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks/rthooks/pyi_rth_multiprocessing.py'
7726 INFO: Including run-time hook '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks/rthooks/pyi_rth_inspect.py'
7727 INFO: Including run-time hook '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks/rthooks/pyi_rth_pyqt6.py'
7736 INFO: Looking for dynamic libraries
7977 INFO: Looking for eggs
7977 INFO: Using Python library /usr/local/Cellar/python@3.9/3.9.9/Frameworks/Python.framework/Versions/3.9/Python
7987 INFO: Warnings written to /Users/martin/app/pyside6/build/app/warn-app.txt
8019 INFO: Graph cross-reference written to /Users/martin/app/pyside6/build/app/xref-app.html
8032 INFO: checking PYZ
8035 INFO: Building because toc changed
8035 INFO: Building PYZ (ZlibArchive) /Users/martin/app/pyside6/build/app/PYZ-00.pyz
8390 INFO: Building PYZ (ZlibArchive) /Users/martin/app/pyside6/build/app/PYZ-00.pyz completed successfully.
8397 INFO: EXE target arch: x86_64
8397 INFO: Code signing identity: None
8398 INFO: checking PKG
8398 INFO: Building because /Users/martin/app/pyside6/build/app/PYZ-00.pyz changed
8398 INFO: Building PKG (CArchive) app.pkg
8415 INFO: Building PKG (CArchive) app.pkg completed successfully.
8417 INFO: Bootloader /usr/local/lib/python3.9/site-packages/PyInstaller/bootloader/Darwin-64bit/runw
8417 INFO: checking EXE
8418 INFO: Building because console changed
8418 INFO: Building EXE from EXE-00.toc
8418 INFO: Copying bootloader EXE to /Users/martin/app/pyside6/build/app/app
8421 INFO: Converting EXE to target arch (x86_64)
8449 INFO: Removing signature(s) from EXE
8484 INFO: Appending PKG archive to EXE
8486 INFO: Fixing EXE headers for code signing
8496 INFO: Rewriting the executable's macOS SDK version (11.1.0) to match the SDK version of the Python library (10.15.6) in order to avoid inconsistent behavior and potential UI issues in the frozen application.
8499 INFO: Re-signing the EXE
8547 INFO: Building EXE from EXE-00.toc completed successfully.
8549 INFO: checking COLLECT
WARNING: The output directory "/Users/martin/app/pyside6/dist/app" and ALL ITS CONTENTS will be REMOVED! Continue? (y/N)y
On your own risk, you can use the option `--noconfirm` to get rid of this question.
10820 INFO: Removing dir /Users/martin/app/pyside6/dist/app
10847 INFO: Building COLLECT COLLECT-00.toc
12460 INFO: Building COLLECT COLLECT-00.toc completed successfully.
12469 INFO: checking BUNDLE
12469 INFO: Building BUNDLE because BUNDLE-00.toc is non existent
12469 INFO: Building BUNDLE BUNDLE-00.toc
13848 INFO: Moving BUNDLE data files to Resource directory
13901 INFO: Signing the BUNDLE...
16049 INFO: Building BUNDLE BUNDLE-00.toc completed successfully.

如果你看看你的文件夹,你会发现你现在有两个新的文件夹distbuild

build & dist folders created by PyInstaller 由PyInstaller创建的build和dist文件夹

下面是文件夹内容的一个截断列表,显示了builddist 文件夹。

bash

.
├── app.py
├── app.spec
├── build
│   └── app
│       ├── 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
    │   ├── libcrypto.1.1.dylib
    │   ├── PySide6
    │   ...
    │   ├── app
    │   └── Qt5Core
    └── app.app

build 文件夹被PyInstaller用来收集和准备捆绑的文件,它包含分析的结果和一些额外的日志。在大多数情况下,你可以忽略这个文件夹的内容,除非你想调试问题。

dist (代表 "分发")文件夹包含要分发的文件。这包括你的应用程序,捆绑成一个可执行文件,以及任何相关的库(例如PySide6)和二进制.so 文件。

由于我们在上面提供了--windowed 标志,PyInstaller实际上已经为我们创建了两个构建文件。文件夹app 是一个简单的文件夹,包含了你需要的一切,以便能够运行你的应用程序。PyInstaller还创建了一个应用程序捆绑包app.app ,这是你通常会分发给用户的东西。

app 文件夹是一个有用的调试工具,因为你可以很容易看到库和其他打包的数据文件。

你现在可以尝试自己运行你的应用程序,可以通过双击应用程序包,或者通过运行可执行文件,命名为dist 文件夹中的app.exe 。无论哪种情况,在短暂的延迟之后,你会看到你的应用程序的熟悉窗口弹出,如下图所示:

Simple app, running after being packaged

简单的应用程序,在被打包后运行

在与你的Python文件相同的文件夹中,除了builddist 文件夹外,PyInstaller还将创建一个.spec 文件。 在下一节中,我们将看一下这个文件,它是什么,它做什么。

Spec文件

.spec 文件包含了PyInstaller用来打包你的应用程序的构建配置和说明。每个PyInstaller项目都有一个.spec 文件,它是根据你在运行pyinstaller 时传递的命令行选项生成的。

当我们用脚本运行pyinstaller 时,除了我们的 Python 应用文件的名字和--windowed 标志外,我们没有传入任何东西。这意味着我们的规范文件目前只包含默认配置。如果你打开它,你会看到类似于我们下面的内容。

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=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='app')
app = BUNDLE(coll,
             name='app.app',
             icon=None,
             bundle_identifier=None)

首先要注意的是,这是一个Python文件,意味着你可以编辑它,使用Python代码来计算设置值。这对于复杂的构建来说非常有用,例如,当你针对不同的平台,想要有条件地定义额外的库或依赖关系来捆绑。

因为我们使用了--windowed 命令行标志,所以EXE(console=) 属性被设置为False 。如果是True ,当你的应用程序启动时将会显示一个控制台窗口--这不是你通常希望的GUI应用程序。

一旦生成了.spec 文件,你可以把它传递给pyinstaller ,而不是你的脚本,以重复之前的构建过程。现在运行它来重建你的可执行文件。

bash

pyinstaller app.spec

由此产生的构建将与用于生成.spec 文件的构建相同(假设你没有做任何修改)。对于许多PyInstaller配置的改变,你可以选择通过命令行参数,或者修改你现有的.spec 文件。你选择哪种方式取决于你。

调整构建

到目前为止,我们已经为一个非常基本的应用程序创建了一个简单的首次构建。现在我们将看看PyInstaller提供的几个最有用的选项,以调整我们的构建。然后我们将继续研究如何构建更复杂的应用程序。

命名你的应用程序

你可以做的最简单的改变之一是为你的应用程序提供一个合适的 "名字"。默认情况下,应用程序采用你的源文件的名称(去掉扩展名),例如mainapp 。这通常不是你想要的。

你可以通过编辑.spec 文件,在EXE、COLLECT和BUNDLE块下添加一个name= ,为PyInstaller提供一个更好的名字,以便为应用程序(和dist 文件夹)使用。

python

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')
app = BUNDLE(coll,
             name='Hello World.app',
             icon=None,
             bundle_identifier=None)

EXE下的名字是可执行文件的名字,BUNDLE下的名字是应用程序包的名字。

或者,你可以重新运行pyinstaller 命令,并将-n--name 配置标志与你的app.py 脚本一起传递。

bash

pyinstaller -n "Hello World" --windowed app.py
# or
pyinstaller --name "Hello World" --windowed app.py

由此产生的应用程序文件将被命名为Hello World.app ,解压后的构建文件将被放置在文件夹dist\Hello World\

Application with custom name "Hello World"

带有自定义名称 "Hello World "的应用程序

.spec 文件的名称取自命令行上传递的名称,因此这将为你创建一个新的规格文件,在你的根文件夹中称为Hello World.spec

确保你删除了旧的app.spec 文件,以避免混乱地编辑错误的文件。

应用程序图标

默认情况下,PyInstaller应用程序捆绑包会有以下图标:

Default PyInstaller application icon, on app bundle 默认的PyInstaller应用程序图标,在应用程序捆绑包上

你可能想自定义它,使你的应用程序更容易被识别。这可以通过传递--icon 命令行参数,或编辑你的.spec 文件中BUNDLE部分的icon= 参数轻松完成。对于macOS应用程序捆绑,你需要提供一个.icns 文件。

python

app = BUNDLE(coll,
             name='Hello World.app',
             icon='Hello World.icns',
             bundle_identifier=None)

要从图像中创建macOS图标,你可以使用image2icon工具

如果你现在重新运行构建(通过使用命令行参数,或使用你修改过的.spec 文件运行),你会看到指定的图标文件现在已经设置在你的应用程序捆绑包上。

Custom application icon (a hand) on the app bundle 应用包上的自定义应用程序图标

在macOS上,应用程序的图标来自应用程序捆绑包。如果你重新打包你的应用程序并运行捆绑程序,你将会在Dock上看到你的应用程序图标!

Custom application icon in the dock 在Dock上的自定义应用程序图标

数据文件和资源

到目前为止,我们的应用程序只由一个Python文件组成,没有任何依赖关系。大多数现实世界的应用程序要复杂得多,而且通常会有相关的数据文件,如图标或用户界面设计文件。在这一节中,我们将看看我们如何用PyInstaller完成这个任务,从一个文件开始,然后捆绑完整的资源文件夹。

首先,让我们用更多的按钮来更新我们的应用程序,并为每个按钮添加图标。

from PySide6.QtWidgets import QMainWindow, QApplication, QLabel, QVBoxLayout, QPushButton, QWidget
from PySide6.QtGui import QIcon

import sys

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)

        button1 = QPushButton("Hide")
        button1.setIcon(QIcon("icons/hand.png"))
        button1.pressed.connect(self.lower)
        layout.addWidget(button1)

        button2 = QPushButton("Close")
        button2.setIcon(QIcon("icons/lightning.png"))
        button2.pressed.connect(self.close)
        layout.addWidget(button2)

        container = QWidget()
        container.setLayout(layout)

        self.setCentralWidget(container)

        self.show()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = MainWindow()
    app.exec_()

在有这个脚本的文件夹中,添加一个文件夹icons ,其中包含两个PNG格式的图标,hand.pnglightning.png 。你可以自己创建这些图标,或者从本教程的源代码下载中获取它们。

现在运行该脚本,你会看到一个显示两个带图标的按钮的窗口:

Window with two icons

有两个带图标的按钮的窗口。

即使你没有看到图标,也要继续读下去!

处理相对路径

这里有一个小问题,可能不是很明显。为了证明这一点,请打开一个shell,切换到我们的脚本所在的文件夹。用以下命令运行它

bash

python3 app.py

如果图标在正确的位置,你应该看到它们。现在换到父文件夹,并尝试再次运行你的脚本(将<folder> 改为你的脚本所在的文件夹的名称)。

bash

cd ..
python3 <folder>/app.py

Window with two icons missing

窗口中的两个按钮的图标不见了。

图标没有出现。发生了什么事?

我们正在使用相对路径来引用我们的数据文件。这些路径是相对于当前工作目录而言的,而不是你的脚本所在的文件夹。因此,如果你从其他地方运行脚本,它将无法找到这些文件。

图标不显示的一个常见原因是在IDE中运行的例子,它使用项目根目录作为当前工作目录。

这在应用打包前是个小问题,但一旦安装后,它将以其当前工作目录作为根/ ,你的应用将无法找到任何东西。我们需要在进一步行动之前解决这个问题,我们可以通过使我们的路径与我们的应用程序文件夹相对应来做到这一点。

在下面更新的代码中,我们定义了一个新的变量basedir ,用os.path.dirname 来获取__file__ 的包含文件夹,该文件夹包含了当前 Python 文件的完整路径。然后我们用这个来建立图标的相对路径,使用os.path.join()

由于我们的app.py 文件在我们文件夹的根部,所有其他的路径都是相对于它的。

python

from PySide6.QtWidgets import QMainWindow, QApplication, QLabel, QVBoxLayout, QPushButton, QWidget
from PySide6.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)

        button1 = QPushButton("Hide")
        button1.setIcon(QIcon(os.path.join(basedir, "icons", "hand.png")))
        button1.pressed.connect(self.lower)
        layout.addWidget(button1)

        button2 = QPushButton("Close")
        button2.setIcon(QIcon(os.path.join(basedir, "icons", "lightning.png")))
        button2.pressed.connect(self.close)
        layout.addWidget(button2)

        container = QWidget()
        container.setLayout(layout)

        self.setCentralWidget(container)

        self.show()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = MainWindow()
    app.exec_()

试着从父文件夹再次运行你的应用程序 -- 你会发现,无论你从哪里启动应用程序,图标现在都会如期出现在按钮上。

包装图标

所以,现在我们的应用程序显示了图标,而且无论从哪里启动应用程序,它们都能发挥作用。用pyinstaller "Hello World.spec" 再次打包该应用程序,然后像以前一样从dist 文件夹中再次运行它。你会发现图标又不见了。

Window with two icons missing

有两个按钮的窗口,图标不见了。

现在的问题是,图标没有被复制到dist/Hello World 文件夹中--看看它。我们的脚本希望图标是相对于它的一个特定位置,如果它们不是,那么就不会有任何显示。

这个原则同样适用于你随应用程序打包的任何其他数据文件,包括Qt Designer UI文件、设置文件或源数据。你需要确保相对路径结构在打包后被复制。

用PyInstaller捆绑数据文件

为了使应用程序在打包后继续工作,它所依赖的文件需要在相同的相对位置。

为了让数据文件进入dist 文件夹,我们可以指示PyInstaller将它们复制过来。PyInstaller接受一个要复制的单个路径列表,以及一个相对于dist/<app name> 文件夹的文件夹路径,它应该将它们复制到那里。与其他选项一样,这可以通过命令行参数或在.spec 文件中指定。

在命令行中指定的文件是用--add-data ,传递源文件和目标文件夹,用冒号隔开:

路径分隔符是特定于平台的。Linux或Mac使用: ,在Windows上使用;

bash

pyinstaller --windowed --name="Hello World" --icon="Hello World.icns" --add-data="icons/hand.png:icons" --add-data="icons/lightning.png:icons" app.py

这里我们指定目标位置为icons 。该路径是相对于我们的应用程序在dist 中的文件夹的根部而言的--所以dist/Hello World 与我们当前的应用程序。路径icons 意味着在这个位置下有一个名为icons 的文件夹,所以dist/Hello World/icons 。把我们的图标放在我们的应用程序期望找到它们的地方!

你也可以通过规格文件的分析部分的datas 列表来指定数据文件,如下所示:

python

a = Analysis(['app.py'],
             pathex=[],
             binaries=[],
             datas=[('icons/hand.png', 'icons'), ('icons/lightning.png', 'icons')],
             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将指定的文件复制到输出文件夹中的./icons/ ,即dist/Hello World/icons 。 如果你运行构建,你应该看到你的.png 文件现在在dist 输出文件夹中,在一个名为icons的文件夹下。

The icon file copied to the dist folder

复制到dist文件夹中的图标文件

如果你从dist 中运行你的应用程序,你现在应该如愿以偿地在你的窗口中看到了图标的图标

Window with two icons

有两个带图标的按钮的窗口,终于出现了

捆绑数据文件夹

通常情况下,你会有不止一个数据文件想要包含在你打包的文件中。最新的PyInstaller版本让你像捆绑文件一样捆绑文件夹,保持子文件夹的结构。

让我们更新我们的配置,一次性地捆绑我们的图标文件夹,这样即使我们将来添加更多的图标,它也会继续工作。

为了将icons 文件夹复制到我们的构建应用程序中,我们只需要将该文件夹添加到我们的.spec 文件Analysis 块中。至于单个文件,我们把它作为一个元组添加到dist ,在生成的文件夹下有源路径(来自我们的项目文件夹)和目标文件夹。

# ...
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)
# ...

如果你使用这个规格文件运行构建,你会看到icons 文件夹被复制到dist\Hello World 文件夹。如果你从该文件夹中运行应用程序,图标将按预期显示 -- 相对路径在新的位置保持正确。

另外,你也可以使用Qt的QResource架构来捆绑你的数据文件。参见我们的教程以了解更多信息。

将应用程序捆绑到磁盘镜像中

到目前为止,我们已经使用PyInstaller将应用程序与相关的数据文件一起捆绑到macOS应用程序中。这个捆绑过程的输出是一个文件夹和一个macOS应用捆绑包,名为Hello World.app

如果你尝试分发这个应用程序捆绑包,你会发现一个问题:应用程序捆绑包实际上只是一个特殊的文件夹。虽然macOS将其显示为一个应用程序,但如果你试图分享它,你实际上将分享数百个单独的文件。为了正确地分发应用程序,我们需要某种方式将其打包成一个单一的文件。

最简单的方法是使用一个.zip 文件。你可以将文件夹压缩,然后交给其他人,让他们在自己的电脑上解压,给他们一个完整的应用程序包,他们可以复制到他们的应用程序文件夹。

然而,如果你以前安装过macOS应用程序,你会知道这不是通常的方法。通常情况下,你会得到一个磁盘镜像 .dmg ,打开后会显示应用程序包,以及一个指向应用程序文件夹的链接。要安装该应用程序,你只需将其拖到目标上。

为了使我们的应用程序看起来尽可能的专业,我们应该复制这种预期的行为。接下来,我们将看看如何将我们的应用包打包成一个macOS磁盘镜像

确保构建工作已经准备就绪

如果你到目前为止一直在遵循本教程,你已经在/dist 文件夹中准备好了你的应用程序。如果没有,或者你的应用程序不工作,你也可以下载本教程的源代码文件,其中包括一个样本.spec 文件。如上所述,你可以使用提供的Hello World.spec 文件运行相同的构建。

bash

pyinstaller "Hello World.spec"

这将所有东西打包成一个应用包,放在dist/ 文件夹中,并有一个自定义图标。运行应用程序捆绑包以确保所有东西都被正确捆绑,你应该看到和以前一样的窗口,图标是可见的。

Two icons

有两个图标的窗口,和一个按钮。

创建一个磁盘镜像

现在我们已经成功地捆绑了我们的应用程序,接下来我们将看看如何利用我们的应用程序捆绑,并使用它来创建一个macOS磁盘镜像来分发。

为了创建我们的磁盘镜像,我们将使用create-dmg工具。这是一个命令行工具,它提供了一个简单的方法来自动建立磁盘镜像。如果你使用Homebrew,你可以用以下命令安装come-dmg。

bash

brew install create-dmg

...否则,请参阅Github仓库的说明。

create-dmg 工具需要很多选项,但下面是最有用的。

bash

create-dmg --help
create-dmg 1.0.9

Creates a fancy DMG file.

Usage:  create-dmg [options] <output_name.dmg> <source_folder>

All contents of <source_folder> will be copied into the disk image.

Options:
  --volname <name>
      set volume name (displayed in the Finder sidebar and window title)
  --volicon <icon.icns>
      set volume icon
  --background <pic.png>
      set folder background image (provide png, gif, or jpg)
  --window-pos <x> <y>
      set position the folder window
  --window-size <width> <height>
      set size of the folder window
  --text-size <text_size>
      set window text size (10-16)
  --icon-size <icon_size>
      set window icons size (up to 128)
  --icon file_name <x> <y>
      set position of the file's icon
  --hide-extension <file_name>
      hide the extension of file
  --app-drop-link <x> <y>
      make a drop link to Applications, at location x,y
  --no-internet-enable
      disable automatic mount & copy
  --add-file <target_name> <file>|<folder> <x> <y>
      add additional file or folder (can be used multiple times)
  -h, --help
        display this help screen

最需要注意的是,该命令需要一个<source folder> ,该文件夹的所有内容将被复制到磁盘镜像中。因此,为了建立镜像,我们首先需要把我们的应用包放在一个自己的文件夹里。

我建议创建一个shell脚本,而不是每次你想构建磁盘镜像时都手动做这个。这可以确保构建是可重复的,并使其更容易配置。

下面是一个从我们的应用程序创建磁盘镜像的工作脚本。它创建了一个临时文件夹dist/dmg ,我们将把我们想放在磁盘镜像中的东西放在那里--在我们的例子中,这只是应用包,但如果你愿意,你可以添加其他文件。然后我们确保该文件夹是空的(以防它仍然包含以前运行的文件)。.dmg 我们把我们的应用程序捆绑包复制到该文件夹中,最后检查一下dist ,如果有的话,把它也删除。然后,我们准备运行create-dmg 工具。

bash

#!/bin/sh
# Create a folder (named dmg) to prepare our DMG in (if it doesn't already exist).
mkdir -p dist/dmg
# Empty the dmg folder.
rm -r dist/dmg/*
# Copy the app bundle to the dmg folder.
cp -r "dist/Hello World.app" dist/dmg
# If the DMG already exists, delete it.
test -f "dist/Hello World.dmg" && rm "dist/Hello World.dmg"
create-dmg \
  --volname "Hello World" \
  --volicon "Hello World.icns" \
  --window-pos 200 120 \
  --window-size 600 300 \
  --icon-size 100 \
  --icon "Hello World.app" 175 120 \
  --hide-extension "Hello World.app" \
  --app-drop-link 425 120 \
  "dist/Hello World.dmg" \
  "dist/dmg/"

我们传递给create-dmg 的选项设置了磁盘图像窗口打开时的尺寸,以及其中图标的位置。

把这个shell脚本保存在你项目的根目录下,例如命名为builddmg.sh 。为了使它能够运行,你需要设置执行位

bash

chmod +x builddmg.sh

有了这个,你现在可以用命令为你的Hello World应用程序建立一个磁盘镜像

bash

./builddmg.sh

这将需要几秒钟来运行,产生相当多的输出:

bash

 No such file or directory
Creating disk image...
...............................................................
created: /Users/martin/app/dist/rw.Hello World.dmg
Mounting disk image...
Mount directory: /Volumes/Hello World
Device name:     /dev/disk2
Making link to Applications dir...
/Volumes/Hello World
Copying volume icon file 'Hello World.icns'...
Running AppleScript to make Finder stuff pretty: /usr/bin/osascript "/var/folders/yf/1qvxtg4d0vz6h2y4czd69tf40000gn/T/createdmg.tmp.XXXXXXXXXX.RvPoqdr0" "Hello World"
waited 1 seconds for .DS_STORE to be created.
Done running the AppleScript...
Fixing permissions...
Done fixing permissions
Blessing started
Blessing finished
Deleting .fseventsd
Unmounting disk image...
hdiutil: couldn't unmount "disk2" - Resource busy
Wait a moment...
Unmounting disk image...
"disk2" ejected.
Compressing disk image...
Preparing imaging engineReading Protective Master Boot Record (MBR : 0)…
   (CRC32 $38FC6E30: Protective Master Boot Record (MBR : 0))
Reading GPT Header (Primary GPT Header : 1)…
   (CRC32 $59C36109: GPT Header (Primary GPT Header : 1))
Reading GPT Partition Data (Primary GPT Table : 2)…
   (CRC32 $528491DC: GPT Partition Data (Primary GPT Table : 2))
Reading  (Apple_Free : 3)…
   (CRC32 $00000000:  (Apple_Free : 3))
Reading disk image (Apple_HFS : 4)…
...............................................................................
   (CRC32 $FCDC1017: disk image (Apple_HFS : 4))
Reading  (Apple_Free : 5)…
...............................................................................
   (CRC32 $00000000:  (Apple_Free : 5))
Reading GPT Partition Data (Backup GPT Table : 6)…
...............................................................................
   (CRC32 $528491DC: GPT Partition Data (Backup GPT Table : 6))
Reading GPT Header (Backup GPT Header : 7)…
...............................................................................
   (CRC32 $56306308: GPT Header (Backup GPT Header : 7))
Adding resources…
...............................................................................
Elapsed Time:  3.443s
File size: 23178950 bytes, Checksum: CRC32 $141F3DDC
Sectors processed: 184400, 131460 compressed
Speed: 18.6Mbytes/sec
Savings: 75.4%
created: /Users/martin/app/dist/Hello World.dmg
hdiutil does not support internet-enable. Note it was removed in macOS 10.15.
Disk image done

在构建过程中,磁盘镜像将弹出。先不要太兴奋,它还在构建中。等待脚本完成,你会在dist/ 文件夹中找到完成的.dmg 文件。

The Disk Image in the dist folder

在dist文件夹中创建的磁盘镜像

运行安装程序

双击磁盘镜像以打开它,你会看到通常的macOS安装视图。点击并拖动你的应用程序到Applications 文件夹来安装它。

The Disk Image containing your file

磁盘镜像包含应用程序包和应用程序文件夹的快捷方式

如果你打开Showcase视图(按F4),你会看到你的应用程序已经安装。如果你有很多应用程序,你可以通过输入 "Hello "来搜索它

The app is installed!

安装在macOS上的应用程序

重复构建

现在你已经设置好了一切,你可以随时创建一个新的应用程序包和你的应用程序的磁盘镜像,通过在命令行中运行这两个命令。

bash

pyinstaller "Hello World.spec"
./builddmg.sh

就这么简单!

总结

在本教程中,我们介绍了如何使用PyInstaller将你的PySide6应用程序构建成一个macOS应用程序包,包括将数据文件与你的代码一起添加。然后,我们走过了创建磁盘镜像的过程,将你的应用程序分发给其他人。按照这些步骤,你应该能够将你自己的应用程序打包并提供给其他人使用。