在 macOS 上打包 Windows EXE 的好办法:用 GitHub Actions 自动编译
如果你平时在 macOS 上写 Python 脚本、桌面小工具或命令行工具,最后却要发给 Windows 用户使用,最容易遇到的问题就是:我能不能直接在 Mac 上打包出 .exe?
结论先说清楚:不建议在 macOS 本机强行交叉打包 Windows EXE。更稳的办法是把代码推到 GitHub,让 GitHub Actions 在 Windows 环境里自动打包。
这篇文章按使用教程来写,从准备项目、创建 workflow、触发构建、下载产物,到常见问题排查,完整走一遍。
总览:为什么要用 GitHub Actions 打包 EXE#
PyInstaller 这类打包工具通常不是“万能交叉编译器”。也就是说:
- 要打 Windows
.exe,最好在 Windows 上运行打包命令。 - 要打 macOS 应用,就在 macOS 上打。
- 要打 Linux 可执行文件,就在 Linux 上打。
这不是 GitHub Actions 的特殊限制,而是很多 Python 原生依赖、动态库、运行时文件和系统 API 都跟目标操作系统绑定。你在 macOS 上直接打包,得到的通常是 macOS 可执行文件,而不是 Windows 真正可用的 .exe。
GitHub Actions 的思路是:
macOS 本地开发
-> git push 到 GitHub
-> GitHub Actions 启动 Windows Runner
-> 安装 Python 和依赖
-> 执行 PyInstaller
-> 上传 exe 产物
-> 在 Actions 页面下载
这样做的好处是:
- 不需要自己安装 Windows 虚拟机。
- 每次构建环境更干净,问题更容易复现。
- 可以把打包流程固化成配置文件,后面一键复用。
- 可以同时扩展出 Windows、macOS、Linux 多平台产物。
准备一个最小 Python 项目#
假设你的项目结构是这样:
my-tool/
├── main.py
├── requirements.txt
└── README.md
main.py 示例:
import sys
def main():
print("Hello from packaged exe")
print(f"Python: {sys.version}")
if __name__ == "__main__":
main()
requirements.txt 至少要写上 PyInstaller:
pyinstaller
如果你的项目还用到了其他库,比如 requests、pillow、pyqt6,也写进去:
pyinstaller
requests
pillow
本地可以先确认脚本能正常运行:
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python main.py
本地能跑通之后,再交给 GitHub Actions 打包。
第一步:创建 workflow 文件#
在项目根目录创建目录:
mkdir -p .github/workflows
然后创建文件:
.github/workflows/build-windows-exe.yml
写入下面这份配置:
name: Build Windows EXE
on:
workflow_dispatch:
push:
branches:
- main
jobs:
build-windows:
runs-on: windows-latest
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: "pip"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Build exe with PyInstaller
run: |
pyinstaller --onefile --name my-tool main.py
- name: Upload exe artifact
uses: actions/upload-artifact@v4
with:
name: my-tool-windows-exe
path: dist/my-tool.exe
这份 workflow 做了几件事:
runs-on: windows-latest:让任务运行在 GitHub 提供的 Windows 环境里。actions/checkout@v4:拉取你的仓库代码。actions/setup-python@v5:安装指定版本的 Python。pip install -r requirements.txt:安装项目依赖。pyinstaller --onefile --name my-tool main.py:打包成单文件 EXE。actions/upload-artifact@v4:把dist/my-tool.exe上传成可下载产物。
第二步:提交到 GitHub#
把 workflow 文件提交上去:
git add .github/workflows/build-windows-exe.yml requirements.txt main.py
git commit -m "Add Windows exe build workflow"
git push origin main
推送后,GitHub 会自动执行一次构建。你也可以手动触发,因为上面的配置里写了:
workflow_dispatch:
手动触发路径是:
GitHub 仓库页面
-> Actions
-> Build Windows EXE
-> Run workflow
第三步:下载打包好的 EXE#
构建完成后:
GitHub 仓库页面
-> Actions
-> 选择最新一次运行记录
-> 页面底部 Artifacts
-> 下载 my-tool-windows-exe
下载下来通常是一个压缩包,解压后可以看到:
my-tool.exe
把这个文件发给 Windows 用户测试即可。
常用 PyInstaller 参数#
最常见的是单文件命令:
pyinstaller --onefile --name my-tool main.py
如果你是 GUI 程序,不想弹出黑色命令行窗口,可以加:
pyinstaller --onefile --windowed --name my-tool main.py
如果程序需要图标:
pyinstaller --onefile --name my-tool --icon assets/app.ico main.py
如果需要带上资源文件,比如配置模板、图片、模型文件,可以使用 --add-data。在 Windows 上分隔符是分号:
pyinstaller --onefile --name my-tool --add-data "assets;assets" main.py
对应到 workflow:
- name: Build exe with PyInstaller
run: |
pyinstaller --onefile --name my-tool --add-data "assets;assets" main.py
如果资源较多,建议先用 .spec 文件管理配置,而不是把所有参数堆在命令行里。
使用 spec 文件管理复杂打包#
先在本地或 CI 里生成 spec 文件:
pyinstaller --onefile --name my-tool main.py
生成后会出现:
my-tool.spec
把这个文件提交到仓库,然后 workflow 改成:
- name: Build exe with PyInstaller spec
run: |
pyinstaller my-tool.spec
适合使用 .spec 的情况:
- 需要打包多个入口文件。
- 需要加入大量资源目录。
- 需要处理 hidden imports。
- GUI 框架、OCR、机器学习模型等依赖比较复杂。
只在发布 tag 时打包#
如果你不想每次 push 都构建 EXE,可以改成只在打 tag 时构建:
name: Build Windows EXE
on:
workflow_dispatch:
push:
tags:
- "v*"
jobs:
build-windows:
runs-on: windows-latest
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: "pip"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Build exe with PyInstaller
run: |
pyinstaller --onefile --name my-tool main.py
- name: Upload exe artifact
uses: actions/upload-artifact@v4
with:
name: my-tool-windows-exe
path: dist/my-tool.exe
发版时执行:
git tag v1.0.0
git push origin v1.0.0
这样只有 v1.0.0、v1.1.0 这种 tag 会触发构建。
自动发布到 GitHub Release#
如果你希望打包完成后自动挂到 GitHub Release,可以加上 release 步骤。
完整示例:
name: Release Windows EXE
on:
workflow_dispatch:
push:
tags:
- "v*"
permissions:
contents: write
jobs:
release-windows:
runs-on: windows-latest
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: "pip"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Build exe with PyInstaller
run: |
pyinstaller --onefile --name my-tool main.py
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: dist/my-tool.exe
这里要注意:
permissions: contents: write是为了让 workflow 有权限创建 Release。softprops/action-gh-release@v2会把dist/my-tool.exe上传到当前 tag 对应的 Release。- 这个流程适合正式版本,不一定适合每次普通提交。
多平台一起打包#
如果你还想顺便生成 macOS 和 Linux 产物,可以用矩阵构建:
name: Build Multi Platform
on:
workflow_dispatch:
jobs:
build:
strategy:
matrix:
include:
- os: windows-latest
artifact_name: my-tool-windows
output_path: dist/my-tool.exe
- os: macos-latest
artifact_name: my-tool-macos
output_path: dist/my-tool
- os: ubuntu-latest
artifact_name: my-tool-linux
output_path: dist/my-tool
runs-on: ${{ matrix.os }}
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: "pip"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Build executable
run: |
pyinstaller --onefile --name my-tool main.py
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_name }}
path: ${{ matrix.output_path }}
这个例子里,每个平台都在自己的系统环境中构建自己的可执行文件。不要指望 macOS 那一份产物能变成 Windows .exe,Windows .exe 是 windows-latest 那个任务产出的。
常见问题 / 排查思路#
1. Actions 里提示找不到 requirements.txt#
先确认文件确实在仓库根目录:
my-tool/
├── requirements.txt
└── .github/workflows/build-windows-exe.yml
如果你的 Python 项目在子目录,比如:
my-tool/
└── app/
├── main.py
└── requirements.txt
那 workflow 要加工作目录:
- name: Install dependencies
working-directory: app
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Build exe with PyInstaller
working-directory: app
run: |
pyinstaller --onefile --name my-tool main.py
2. EXE 打开后闪退#
命令行程序双击后窗口可能一闪而过。先在 Windows 的 PowerShell 里运行:
.\my-tool.exe
这样可以看到具体报错。
如果是缺少模块,常见处理方式是加 hidden import:
pyinstaller --onefile --name my-tool --hidden-import some_module main.py
复杂项目建议改 .spec 文件。
3. 资源文件找不到#
打包后当前目录不一定等于源码目录。读取资源时不要只依赖相对路径,可以封装一个函数:
from pathlib import Path
import sys
def resource_path(relative_path: str) -> Path:
if hasattr(sys, "_MEIPASS"):
return Path(sys._MEIPASS) / relative_path
return Path(__file__).resolve().parent / relative_path
使用时:
config_path = resource_path("assets/config.json")
打包时带上资源:
pyinstaller --onefile --name my-tool --add-data "assets;assets" main.py
4. 杀毒软件误报#
单文件 EXE 被误报并不少见,尤其是小工具、无签名、压缩过的可执行文件。可以尝试:
- 不使用 UPX 压缩。
- 改用
--onedir方式发布。 - 给正式分发版本做代码签名。
- 在 Release 里附上源码、版本说明和校验值。
--onedir 示例:
pyinstaller --onedir --name my-tool main.py
这会生成一个目录,而不是单个 EXE。体积和文件数量会增加,但有时兼容性更好。
5. 中文路径或空格路径导致问题#
CI 环境里一般不会遇到本地用户目录那种中文路径问题,但资源文件名仍建议使用英文、小写、中横线,例如:
assets/default-config.json
assets/app-icon.ico
这样可以减少 Windows 编码、路径转义、命令行参数解析带来的麻烦。
推荐的最终 workflow#
实际项目里,我更推荐从这个版本开始:
name: Build Windows EXE
on:
workflow_dispatch:
push:
tags:
- "v*"
jobs:
build-windows:
runs-on: windows-latest
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: "pip"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Build exe
run: |
pyinstaller --onefile --name my-tool main.py
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: my-tool-windows-exe
path: dist/my-tool.exe
它的特点是:
- 平时普通提交不会频繁打包。
- 需要发版本时打一个
v*tag 即可。 - 也可以在 Actions 页面手动运行。
- 产物通过 artifact 下载,简单直接。
参考资料#
- PyInstaller 官方文档说明:PyInstaller 不是通用交叉编译器,通常应在目标系统上构建目标系统的可执行文件。
- GitHub Actions 官方文档说明:GitHub-hosted runners 提供 Windows、macOS、Ubuntu 等运行环境,
windows-latest这类标签会指向 GitHub 当前维护的稳定镜像。 actions/setup-python官方说明:在 workflow 中推荐使用它安装和缓存 Python 环境。
总结#
在 macOS 上想给 Windows 用户打包 .exe,最稳定的办法不是在 Mac 上硬凑交叉编译环境,而是把“打包动作”放到 Windows 环境中执行。
GitHub Actions 正好提供了这个能力:你继续在 macOS 上开发,提交代码后让 windows-latest runner 自动安装依赖、运行 PyInstaller、上传 EXE。这样流程清晰、环境可复现,也方便以后扩展成自动发布 Release。