知识门户

返回

第12章 跨平台部署:从 Windows 打包到 Linux 工控机

第四篇:交付与工程化运维

views | comments

第12章 跨平台部署:从 Windows 打包到 Linux 工控机#


12.1 Windows 下的交付:PyInstaller 实战#

12.1.1 场景与痛点:客户的电脑上没有 Python#

你的串口调试上位机终于开发完成了。功能完善、测试通过、文档齐全。你信心满满地把整个项目文件夹发给了客户。

客户的回复是:

“双击 .py 文件,提示’无法打开此文件’?Python 是什么?”

或者更委婉一点:

“你发给我的东西我打开是乱码……”

这不是客户的错。客户的电脑上大概率没有安装 Python 环境,更不会有你项目依赖的 pyserialpyqt5loguru 等第三方库。你需要把你的 Python 项目打包成一个独立的可执行文件——双击就能运行,不需要安装任何东西。

PyInstaller 就是干这个活的。

12.1.2 PyInstaller 快速入门:一条命令搞定#

安装:

pip install pyinstaller
bash

最简单的打包——一条命令:

pyinstaller --onefile your_script.py
bash

执行完成后,你会在 dist/ 目录下找到一个 your_script.exe 文件。这个文件是”自包含的”——双击就能运行,不需要 Python 环境。

这条命令做了什么?

your_script.py

    ├── PyInstaller 分析所有 import 语句
    ├── 收集 Python 解释器(嵌入到 exe 中)
    ├── 收集所有依赖库(pyserial、requests、loguru 等)
    ├── 收集所有 DLL 文件
    ├── 压缩打包

    └──→ dist/your_script.exe(一个独立的可执行文件)
plaintext

常用参数速查:

参数说明示例
--onefile打包成单个 exe 文件pyinstaller --onefile app.py
--onedir打包成一个目录(默认模式,启动更快)pyinstaller --onedir app.py
--windowed不显示控制台窗口(GUI 程序用)pyinstaller --windowed app.py
--console显示控制台窗口(命令行工具用,默认)pyinstaller --console app.py
--name指定输出文件名pyinstaller --name "测试工装" app.py
--icon指定图标文件pyinstaller --icon=app.ico app.py
--add-data附加额外文件pyinstaller --add-data "config.ini;." app.py
--hidden-import指定隐式导入pyinstaller --hidden-import=usb.backend libusb1 app.py
--specpath指定 spec 文件生成路径pyinstaller --specpath=. app.py

--onefile vs --onedir 的选型:

┌─────────────┬──────────────────────────┬─────────────────────────────┐
│   维度       │  --onefile(单文件)      │  --onedir(目录模式)        │
├─────────────┼──────────────────────────┼─────────────────────────────┤
│ 交付形式     │ 一个 .exe 文件           │ 一个文件夹(含 exe + DLLs)  │
│ 分发便利性   │ 极好(发一个文件就行)    │ 一般(需要打包成 zip 发送)  │
│ 启动速度     │ 较慢(每次需先解压到临时目录)│ 快(直接运行)            │
│ 文件访问     │ 需要特殊处理(见下文)    │ 正常访问                    │
│ 适用场景     │ 简单命令行工具           │ 复杂项目(GUI + 配置 + DLL)│
└─────────────┴──────────────────────────┴─────────────────────────────┘
plaintext

实用建议:嵌入式上位机项目通常有配置文件、DLL、图标等外部资源,推荐用 --onedir 模式。如果你的脚本非常简单(比如一个纯 Python 的命令行工具),可以用 --onefile

12.1.3 .spec 文件深度定制#

当命令行参数不够用时,你需要定制 .spec 文件。PyInstaller 第一次运行时会自动生成一个 .spec 文件(本质是 Python 语法的配置文件),你可以直接编辑它:

# 第一次运行,生成 spec 文件
pyinstaller --onefile --windowed main.py

# 之后可以直接编辑 spec 文件,然后用 spec 文件来打包
pyinstaller main.spec
bash

一个完整的嵌入式上位机 .spec 文件示例:

关于 UPX 压缩的说明:

UPX(Ultimate Packer for eXecutables)是一个可执行文件压缩工具。启用 UPX 可以显著减小 exe 文件的体积(通常能减小 30%~50%),但有两个注意事项:

  1. 需要先安装 UPX:从 https://github.com/upx/upx/releases 下载,放到系统 PATH 中
  2. 某些 DLL 经 UPX 压缩后可能无法正常加载,需要用 upx_exclude 排除

如果不需要 UPX(推荐新手先不用),把 upx=True 改成 upx=False 即可。

12.1.4 经典问题:打包后找不到资源文件#

这是 PyInstaller 最经典、最让人抓狂的问题。你本地运行好好的,打包成 exe 后就报 FileNotFoundError

根因分析:

当你用 --onefile 模式打包时,exe 运行时会把所有文件解压到一个临时目录(通常是 C:\Users\你的用户名\AppData\Local\Temp\_MEIxxxxxx\)。你的代码里写的相对路径 config/settings.ini 不会指向 exe 所在的目录,而是指向这个临时目录。

解决方案:路径兼容函数

在你的项目中加入这个工具函数,它能同时兼容”开发环境”和”打包后”两种情况:

在项目中使用:

路径逻辑总结:

12.1.5 DLL 找不到的幽灵错误#

嵌入式项目打包后最常见的另一个问题:程序能启动,但调用 DLL 时报错:

OSError: [WinError 126] 找不到指定的模块。
plaintext

或者更隐晦的:

OSError: [WinError 193] %1 不是有效的 Win32 应用程序。
plaintext

常见原因排查清单:

错误现象可能原因解决方案
找不到指定的模块DLL 没有被打包进去.specdatas 中添加 DLL
找不到指定的模块DLL 依赖的其他 DLL 缺失用 Dependency Walker 或 dumpbin /dependents 查看依赖链
不是有效的 Win32 应用程序32/64 位不匹配64 位 Python 不能加载 32 位 DLL,反之亦然
DLL load failedDLL 依赖的运行时缺失安装对应的 Visual C++ Redistributable
打包成功但运行报错DLL 在搜索路径中找不到确认 DLL 被放到了正确的位置

dumpbin 检查 DLL 依赖链:

# 打开 Visual Studio 的 Developer Command Prompt
# 检查 DLL 的依赖关系
dumpbin /dependents CH347DLL.dll
bash

输出示例:

Dump of file CH347DLL.dll

File Type: DLL

  Image has the following dependencies:

    KERNEL32.dll
    USER32.dll
    MSVCR100.dll        ← 这个是 Visual C++ 2010 运行时!
    SETUPAPI.dll
plaintext

如果看到 MSVCRxxx.dllVCRUNTIMExxx.dll,说明你的 DLL 依赖 Visual C++ 运行时。你需要确保目标机器上安装了对应的 VC++ Redistributable,或者把这些 DLL 也一起打包。

Python 版本与 DLL 位数匹配检查:

12.1.6 打包后体积优化#

PyInstaller 打包的 exe 文件通常会比较大(100MB~300MB),因为它把整个 Python 解释器和所有依赖都打包进去了。以下是一些优化策略:

策略1:排除不需要的模块

.spec 文件的 excludes 列表中添加你不需要的模块:

excludes=[
    'tkinter',           # 如果你用的是 PyQt,不需要 tkinter
    'matplotlib',        # 如果 GUI 程序不需要绘图
    'numpy.testing',     # numpy 的测试模块
    'scipy',             # 如果没有用到
    'pandas.tests',      # pandas 的测试模块
    'IPython',           # IPython 交互环境
    'jupyter',           # Jupyter 相关
    'setuptools',        # 安装工具
    'pydoc',             # 文档生成器
    'doctest',           # 文档测试
]
python

策略2:使用虚拟环境打包

不要在全局 Python 环境中打包——全局环境通常装了很多你项目不需要的库。创建一个干净的虚拟环境,只安装项目必需的依赖:

# 创建干净的虚拟环境
python -m venv build_env
build_env\Scripts\activate

# 只安装项目必需的依赖
pip install pyserial pyqt5 loguru requests
pip install pyinstaller

# 在这个干净环境中打包
pyinstaller main.spec
bash

策略3:使用 UPX 压缩

如前所述,安装 UPX 并在 spec 文件中设置 upx=True

优化效果参考:

优化措施体积变化(参考值)
全局环境打包~250MB
虚拟环境打包~150MB
+ 排除不需要的模块~120MB
+ UPX 压缩~80MB
+ strip 去除调试符号~70MB

12.1.7 打包流程 Checklist#

在实际项目中,建议按照以下清单逐项检查:


12.2 后台静默运行:Windows 服务与开机自启#

12.2.1 场景与痛点#

你的测试工装脚本需要在工控机上 7×24 小时运行。但有个问题:

  • 工控机偶尔会重启(停电恢复、系统更新)
  • 重启后需要有人手动登录 Windows 并双击 exe 启动程序
  • 如果是节假日没人值守,产线就停了

你需要让脚本在 Windows 启动时自动运行,而且不需要任何人登录。

有两种方案,根据你的需求选择:

方案说明适用场景
Windows 服务(Service)系统级别的后台服务,开机自动启动,无需用户登录需要无人值守运行的生产环境
任务计划程序Windows 内置的定时任务工具简单的开机自启需求

12.2.2 方案一:注册为 Windows 服务(pywin32)#

Windows 服务是一种特殊的程序,它在系统启动时就自动运行,不需要任何人登录桌面。MySQL、Nginx、Redis 在 Windows 上都是以服务形式运行的。

安装 pywin32:

pip install pywin32
bash

编写服务包装脚本:

使用方法:

服务管理器中的显示效果:

┌──────────────────────────────────────────────────────────────────┐
│  服务名称          │  描述                       │  状态   │ 启动类型 │
├──────────────────────────────────────────────────────────────────┤
│  TestStationService│  自动化测试工装的后台监控...   │  正在运行│  自动    │
│  MySQL             │  MySQL 数据库服务            │  正在运行│  自动    │
│  ...               │                             │         │          │
└──────────────────────────────────────────────────────────────────┘
plaintext

12.2.3 方案二:任务计划程序(简单场景)#

如果你不需要”无人值守”,只是想让 exe 在有人登录时自动启动,任务计划程序是最简单的方案:

手动配置步骤:

  1. Win + R,输入 taskschd.msc,打开任务计划程序
  2. 点击”创建基本任务”
  3. 名称填”测试工装自启动”
  4. 触发器选”当计算机启动时”或”当用户登录时”
  5. 操作选”启动程序”,浏览选择你的 exe 文件
  6. 完成

用 Python 脚本自动创建任务计划(方便部署):

12.2.4 两种方案的选型决策#

你的脚本需要在什么情况下运行?

├── 只需要用户登录后自动启动
│   └── → 任务计划程序(简单、无需额外依赖)

├── 需要在无人登录时也能运行(工控机重启后自动恢复)
│   └── → Windows 服务(pywin32)
│       ├── 需要守护进程(子进程崩溃自动重启)
│       └── → 在服务的 main() 中实现子进程管理

└── 只是偶尔运行的脚本(定时执行)
    └── → 任务计划程序(设置定时触发器)
plaintext

12.3 Linux 工控机交付#

越来越多的嵌入式设备运行的是 Linux 系统——树莓派、RK3568/RK3588、全志 H616 等。在这些设备上部署 Python 脚本与 Windows 有显著不同。

12.3.1 两种部署方案的选型决策树#

你的项目需要什么?

├── 需要跨环境一致性 / 多版本共存 / 快速迁移
│   └── → Docker 容器化部署
│       └── 适合:多台设备统一部署、需要隔离不同项目

├── 单项目部署 / 资源受限 / 快速上手
│   └── → venv + systemd
│       └── 适合:树莓派单项目、工控机资源紧张

└── 不确定?
    └── → 先用 venv + systemd(简单直接)
        └── 后续需要时再迁移到 Docker
plaintext

详细对比:

维度venv + systemdDocker 容器化
学习成本低(Python 开发者基本都会)中(需要学 Docker 基础)
资源开销几乎无额外开销Docker 引擎本身占用约 100MB 内存
环境隔离仅 Python 包隔离完全隔离(系统库、网络、文件系统)
跨设备一致性需要手动保证版本一致镜像即环境,完全一致
部署速度快(pip install)快(docker pull + docker run)
回滚能力手动(需保留旧 venv)简单(切换镜像版本标签)
适用场景树莓派、RK3568 等单项目部署多项目共存、多设备统一管理
最低硬件要求几乎无限制内存 ≥ 512MB,存储 ≥ 1GB

12.3.2 方案一:venv + systemd 部署#

这是最简单直接的方案——在嵌入式 Linux 上创建虚拟环境,安装依赖,用 systemd 管理进程。

第一步:准备项目目录#

# SSH 登录到工控机
ssh pi@192.168.1.100

# 创建项目目录
sudo mkdir -p /opt/test_station
sudo chown pi:pi /opt/test_station

# 上传项目文件(从开发机执行)
scp -r ./src ./config ./requirements.txt pi@192.168.1.100:/opt/test_station/
bash

第二步:创建虚拟环境并安装依赖#

requirements.txt 示例:

pyserial==3.5
loguru==0.7.2
requests==2.31.0
pymodbus==3.6.4
paho-mqtt==1.6.1
pyqtgraph==0.13.3
txt

提示:建议在开发机上用 pip freeze > requirements.txt 生成精确版本列表,确保工控机上安装的版本与开发环境完全一致。

第三步:编写 systemd 服务文件#

systemd 是 Linux 系统的服务管理器,类似于 Windows 的服务管理器。它能让你的 Python 脚本在系统启动时自动运行,崩溃后自动重启。

第四步:启动服务#

服务状态输出示例:

● test-station.service - 测试工装监控服务
     Loaded: loaded (/etc/systemd/system/test-station.service; enabled; vendor preset: enabled)
     Active: active (running) since Mon 2025-07-28 10:30:00 CST; 2h 15min ago
       Docs: file:///opt/test_station/docs/README.md
   Main PID: 12345 (python)
      Tasks: 3 (limit: 4096)
     Memory: 85.3M (max: 512.0M)
        CPU: 12.456s
     CGroup: /system.slice/test-station.service
             └─12345 /opt/test_station/venv/bin/python /opt/test_station/src/main.py

Jul 28 10:30:00 raspberrypi systemd[1]: Started 测试工装监控服务。
Jul 28 10:30:01 raspberrypi test-station[12345]: [INFO] 设备连接成功,端口 /dev/ttyUSB0
Jul 28 10:30:02 raspberrypi test-station[12345]: [INFO] MQTT 连接已建立
plaintext

完整部署脚本#

把上面的步骤封装成一个一键部署脚本:

12.3.3 方案二:Docker 容器化部署#

Docker 是一种”容器化”技术——它把你的程序、依赖、配置打包成一个”容器镜像”,在任何安装了 Docker 的机器上都能”一键运行”。

为什么嵌入式场景需要 Docker?

想象一下:你有 20 台工控机,每台都运行你的测试脚本。不用 Docker 的话,你需要在每台机器上重复执行部署脚本——安装 Python、创建 venv、pip install、配置 systemd。如果其中一台的系统版本不同,可能还会遇到各种依赖冲突。

用 Docker 的话:

# 在任何一台工控机上执行这一条命令,就能运行你的程序
docker run -d --name test-station \
  --device=/dev/ttyUSB0 \
  --restart=always \
  your-registry/test-station:v1.0
bash

一条命令,环境完全一致,不需要关心目标机器上装了什么。

Docker 极简入门#

如果你从来没用过 Docker,先理解三个核心概念:

概念类比说明
镜像(Image)一个安装光盘包含程序、依赖、配置的只读模板
容器(Container)一个运行中的虚拟机从镜像创建的运行实例
Dockerfile安装光盘的制作脚本定义如何构建镜像的配置文件

安装 Docker(以 Debian/Ubuntu 为例):

# 更新包索引
sudo apt update

# 安装 Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

# 将当前用户添加到 docker 组(免 sudo)
sudo usermod -aG docker $USER

# 重新登录后生效,验证安装
docker --version
bash

ARM 设备(树莓派等)注意:

树莓派使用的是 ARM 架构,需要安装 ARM 版本的 Docker。上面的安装脚本会自动检测架构。在构建镜像时,也需要使用 ARM 兼容的基础镜像(下文会说明)。

编写 Dockerfile#

逐段解释:

第一阶段(builder):
  ├── 基于 python:3.11-slim 镜像(包含完整 Python 环境)
  ├── 安装 gcc/g++(编译 C 扩展用)
  ├── pip install --prefix=/install(把包装到独立目录)
  └── 目的:编译和安装所有依赖

第二阶段(runtime):
  ├── 基于 python:3.11-slim(干净的 Python 环境)
  ├── 只安装运行时系统库(不装编译工具)
  ├── 从第一阶段复制已安装的 Python 包
  ├── 复制项目代码
  └── 目的:构建最终的运行镜像(体积最小化)
plaintext

为什么要分两个阶段?

因为编译工具(gcc、g++、头文件)在运行时完全不需要。通过多阶段构建,最终镜像中不包含这些编译工具,体积可以减小 50% 以上。

构建和运行镜像#

镜像体积优化#

工控机的存储通常很有限(16GB~64GB eMMC),镜像体积至关重要。

策略1:选用 slim 或 alpine 基础镜像

# 标准镜像:~900MB
FROM python:3.11

# slim 镜像:~150MB(推荐,兼容性最好)
FROM python:3.11-slim

# alpine 镜像:~50MB(最小,但部分 C 扩展可能编译失败)
FROM python:3.11-alpine
dockerfile

对于嵌入式项目,推荐 python:3.11-slim——它在体积和兼容性之间取得了最佳平衡。alpine 虽然更小,但使用 musl libc 而非 glibc,可能导致某些 Python 包(特别是包含 C 扩展的包如 numpy、pyserial)编译失败或运行异常。

策略2:多阶段构建(前面已经展示了)

策略3:利用 Docker 层缓存

# 错误示范:每次修改代码都要重新安装依赖
COPY . .                              # 代码变了
RUN pip install -r requirements.txt   # 依赖没变,但得重装

# 正确示范:先复制依赖文件,再复制代码
COPY requirements.txt .               # 依赖文件
RUN pip install -r requirements.txt   # 依赖没变时使用缓存
COPY src/ ./src/                      # 代码变了,但这层独立
dockerfile

策略4:清理不必要的文件

# 在同一层中安装和清理(不会增加镜像体积)
RUN apt-get update && \
    apt-get install -y --no-install-recommends gcc && \
    pip install ... && \
    apt-get purge -y gcc && \
    apt-get autoremove -y && \
    rm -rf /var/lib/apt/lists/*
dockerfile

ARM 设备(树莓派等)的镜像构建#

如果你的工控机是 ARM 架构(如树莓派),有两种方式构建镜像:

方式1:直接在 ARM 设备上构建(简单但慢)

# 直接在树莓派上执行
docker build -t test-station:v1.0 .
bash

方式2:在 x86 开发机上交叉构建(快但需要配置)

使用 Docker Compose 简化管理#

当你的项目涉及多个容器(比如测试脚本 + MQTT Broker + 数据库)时,用 Docker Compose 管理更加方便:

使用 Docker Compose:

# 启动所有服务
docker compose up -d

# 查看状态
docker compose ps

# 查看日志
docker compose logs -f test-station

# 停止所有服务
docker compose down

# 重新构建并启动
docker compose up -d --build
bash

12.3.4 实战:串口设备在 Docker 中的权限处理#

嵌入式场景中,串口设备在 Docker 中的使用有一个常见的坑——权限问题

容器内的进程默认以 root 运行,但串口设备(如 /dev/ttyUSB0)的权限可能不允许访问。

解决方案1:以 root 用户运行容器

docker run -d \
    --user root \
    --device=/dev/ttyUSB0 \
    test-station:v1.0
bash

安全提示:这种方式简单但不够安全。如果条件允许,推荐方案2。

解决方案2:将用户加入 dialout 组

# 在宿主机上,将 docker 用户加入 dialout 组
sudo usermod -aG dialout $USER

# 在 Dockerfile 中创建相同组
RUN groupadd -g 20 dialout && usermod -aG dialout root
bash

解决方案3:使用设备映射 + 权限设置

docker run -d \
    --device=/dev/ttyUSB0:/dev/ttyUSB0:rwm \
    --group-add $(stat -c '%g' /dev/ttyUSB0) \
    test-station:v1.0
bash

这条命令做了两件事:

  1. --device 映射设备并设置读写权限(rwm = read + write + mknod)
  2. --group-add 将容器内用户加入设备所属的用户组

12.4 AI 协作指南#

12.4.1 让 AI 帮你编写 Dockerfile#

Prompt 模板:

12.4.2 让 AI 帮你编写 systemd 服务文件#

Prompt 模板:

12.4.3 让 AI 帮你排查 PyInstaller 打包问题#

Prompt 模板:

12.4.4 审查清单#

当 AI 生成部署代码后,你需要重点审查:

审查项关注点
Dockerfile 基础镜像确认架构匹配(x86/ARM)、确认 Python 版本正确
串口设备权限容器内是否能正常访问 /dev/ttyUSB0
环境变量敏感信息(密码、密钥)不要硬编码在 Dockerfile 中
数据持久化日志和数据目录是否正确挂载到宿主机
systemd 用户权限不要用 root 运行业务程序,但要确保有串口访问权限
重启策略确认有合理的重启限制,防止无限重启耗尽资源
资源限制内存和 CPU 限制是否合理,防止资源泄漏拖垮系统
PyInstaller DLL所有外部 DLL 是否被正确打包,32/64位是否匹配
镜像体积基础镜像选择、多阶段构建、是否清理了编译工具
网络配置容器是否需要访问宿主机网络(如连接本地 MQTT)

12.4.5 一键部署脚本的 AI 生成#

Prompt 模板:


本章小结#

知识点解决的工程痛点
PyInstaller 基础打包将 Python 脚本交付给没有 Python 环境的客户
.spec 文件深度定制处理外部 DLL、配置文件、图标等资源的打包
sys._MEIPASS 路径兼容解决打包后”找不到资源文件”的经典问题
DLL 依赖排查解决 32/64 位不匹配、运行时缺失等幽灵错误
打包体积优化虚拟环境打包 + 排除无用模块 + UPX 压缩
Windows 服务(pywin32)工控机无人值守运行,崩溃自动重启
任务计划程序简单的开机自启方案
venv + systemdLinux 工控机最简单直接的部署方式
Docker 容器化跨设备一致性部署,环境完全隔离
多阶段构建Docker 镜像体积优化(减小 50%+)
串口设备权限Docker 容器中访问硬件设备
Docker Compose多服务编排管理

一句话总结:Windows 用 PyInstaller 打包为 exe,让客户双击即用;Linux 工控机用 venv+systemd 或 Docker 部署,让脚本 7×24 稳定运行;所有部署问题都可以让 AI 帮你排查和生成配置文件——你只需要审查和微调。


动手练习

  1. 用 PyInstaller 把你的项目打包为 exe,在一台没有安装 Python 的电脑上运行成功
  2. 编写 .spec 文件,把项目的配置文件和 DLL 正确打包进去
  3. 加入 path_utils.py 中的路径兼容函数,确保打包后资源文件能正常读取
  4. 在 Linux 虚拟机或树莓派上,用 venv + systemd 部署你的脚本
  5. 挑战题:编写 Dockerfile,把项目容器化部署,并验证串口设备能正常访问
  6. 挑战题:让 AI 为你生成一个完整的一键部署脚本,在工控机上运行成功

这是本书正文的最后一章。回顾整个学习旅程——从第1章的”够用”语法,到第2章的现代工具链,从第36章的硬件通讯实战,到第79章的数据处理与 GUI 开发,再到第10~12章的测试、运维与部署——你已经掌握了一套完整的”Python + 嵌入式”工程方法论。

这本书教你的不是语法,而是工程思维。 语法可以随时查、随时让 AI 生成,但”为什么用这个工具”、“怎么选型”、“怎么让 AI 帮我做得更好”——这些方法论,才是你作为嵌入式工程师在 AI 时代的核心竞争力。

Comment seems to stuck. Try to refresh?✨