手里是一台 Windows 开发机,目标是一块 RK3588(aarch64)板子。最省事的想法是"把源码 scp 过去,在板子上 make",可板子四颗 A76 主频 1.7 GHz,编译一遍要十几分钟,交叉编译依赖还常常装不上;想打 deb 又总觉得"这是 Debian 的东西,得在 Debian 上做"。
其实打 deb 包根本不需要目标机器在场 ——.deb 的本质是一个 ar 归档,里面塞了三个文件:debian-binary(写死 2.0)、control.tar(元数据)、data.tar(要装进根文件系统的文件)。它是个打包格式,不是编译产物。 只要你能产出 aarch64 的二进制、能跑 dpkg-deb,在哪台机器上打都行——Windows 上装个 WSL 就齐活了。
这一篇全程在 WSL Ubuntu 22.04(x86_64)上实操,产出 cfgd_1.0.0_arm64.deb,用 qemu 在打包机上直接把包里的 arm64 二进制跑起来验证,最后给出板子上的安装与回滚命令。所有命令输出都是本机真跑出来的,不是抄的。
1. 三条路线,先选对
把程序送上 aarch64 板子的三条路
① 板子原生编译
scp 源码 → 板子上 gcc → make
✓ 依赖绝对不会错
✗ 慢(分钟级),占板子资源
✗ 无版本、无卸载、无回滚
② WSL 交叉编译 + 打 deb
x86 主机 → arm64 二进制 → .deb
✓ 秒级构建,可进 CI
✓ 有版本/依赖/卸载/回滚
△ 依赖要靠 apt 元数据声明
③ qemu 容器模拟
docker buildx / arm64 rootfs
✓ 环境最接近真板
✗ 慢(指令翻译 5~10 倍)
✗ 配置重,不适合日常迭代
推荐组合:日常迭代走 ②,发布前用 qemu 把包里的二进制跑一遍做冒烟;只有在需要 apt 装一堆 arm64 依赖库时,才临时开 ③ 做完整验证。
关键认知:编译要交叉(需要 arm64 工具链),打包不需要(dpkg-deb 只是归档器,跟架构无关)。
产出物:cfgd_1.0.0_arm64.deb —— 4.4 KB,装完后 /usr/local/bin/cfgd 是一个 ELF aarch64 可执行文件。
实测环境:WSL Ubuntu 22.04.5 LTS(x86_64),gcc 11.4.0,CMake 3.22.1,dpkg-deb 1.21.1。
2. 环境:只需要两行 apt 1 2 3 $ sudo apt update $ sudo apt install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu \ dpkg-dev binutils-aarch64-linux-gnu file qemu-user-static
装完先确认工具链真的能产出 aarch64:
1 2 3 4 5 $ aarch64-linux-gnu-gcc -dumpmachine aarch64-linux-gnu $ uname -m ; dpkg --print-architecture x86_64 amd64
注意最后两行的落差:宿主是 amd64,产物是 arm64 ——这正是我们后面要用 Architecture: arm64 显式声明的原因,链接器不会替你猜。
顺手确认交叉 sysroot 里有 libc(后面 qemu 试跑要用):
1 2 $ ls /usr/aarch64-linux-gnu/lib/libc.so.6 /usr/aarch64-linux-gnu/lib/libc.so.6
3. 交叉编译:先拿到一个 aarch64 二进制 示例程序 cfgd.c——一个读配置文件的迷你服务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #include <stdio.h> #include <stdlib.h> #include <string.h> #define CFGD_VERSION "1.0.0" #define DEFAULT_CONF "/etc/cfgd/cfgd.conf" int main (int argc, char **argv) { if (argc > 1 && strcmp (argv[1 ], "--version" ) == 0 ) { printf ("cfgd %s (%s)\n" , CFGD_VERSION, #ifdef __aarch64__ "aarch64" #else "x86-64" #endif ); return 0 ; } const char *path = (argc > 1 ) ? argv[1 ] : DEFAULT_CONF; FILE *fp = fopen(path, "r" ); if (!fp) { fprintf (stderr , "cfgd: cannot open %s\n" , path); return 1 ; } char line[256 ]; int n = 0 ; while (fgets(line, sizeof (line), fp)) { char *eq = strchr (line, '=' ); if (!eq || line[0 ] == '#' ) continue ; *eq = '\0' ; char *val = eq + 1 ; val[strcspn (val, "\r\n" )] = '\0' ; printf (" %-16s = %s\n" , line, val); n++; } fclose(fp); printf ("cfgd: %d keys loaded from %s\n" , n, path); return 0 ; }
两个方向各编一次,对比输出:
1 2 3 4 5 6 7 8 9 $ aarch64-linux-gnu-gcc -O2 -Wall -o cfgd-arm64 src/cfgd.c $ file cfgd-arm64 cfgd-arm64: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, ... $ gcc -O2 -o cfgd-x86 src/cfgd.c $ file cfgd-x86 cfgd-x86: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, ...
file 这一行是整条流水线的第一道闸门 :ARM aarch64 说明交叉成功。要是这里写着 x86-64,后面打成 deb 也白搭——包会在板子上装成功、然后一运行就 Exec format error。
用 qemu 在打包机上直接跑 arm64 二进制 这一步是我强烈建议加进流程的:不必等到上板子才知道能不能跑。
1 2 3 4 5 6 7 8 $ qemu-aarch64-static -L /usr/aarch64-linux-gnu ./cfgd-arm64 --version cfgd 1.0.0 (aarch64) $ qemu-aarch64-static -L /usr/aarch64-linux-gnu ./cfgd-arm64 src/cfgd.conf listen_port = 8379 snapshot_path = /var/lib/cfgd/snapshot.json log_level = info cfgd: 3 keys loaded from src/cfgd.conf
-L 指定 arm64 的 sysroot,让动态加载器能找到 aarch64 的 libc。有了它,你可以把"冒烟测试"放在 CI 里,而不是放在板子上。
4. deb 解剖:三层结构
.deb = ar 归档,里面三个成员
cfgd_1.0.0_arm64.deb
debian-binary "2.0"
control.tar.zst
data.tar.zst
元数据(装之前读)
control Package/Version/
Architecture: arm64
Depends/Maintainer
md5sums 文件校验和
conffiles 升级时不覆盖的配置
postinst 安装后脚本
prerm 卸载前脚本
安装内容
usr/local/bin/
etc/cfgd/
lib/systemd/
usr/share/doc/
路径是相对根
的,不带前导
斜杠前缀 /
格式版本号
从 1990 年代起
就一直是 2.0
压缩格式取决于
dpkg 版本:
Ubuntu 22.04 → zst
Ubuntu 18.04 → xz
注意:data.tar 里的路径写成 usr/local/bin/...,展开后就是 /usr/local/bin/... —— 千万别写成 /usr/local(会变成 /usr/usr/local)。
control 是唯一必填的元数据文件:
1 2 3 4 5 6 7 8 9 10 11 12 Package: cfgd Version: 1.0.0 Section: utils Priority: optional Architecture: arm64 Depends: libc6 (>= 2.34) Maintainer: Hespethorn <hespethorn@example.com> Description: Tiny config service (arm64 build) cfgd reads /etc/cfgd/cfgd.conf and prints the parsed key/value pairs. . This package is cross-built with aarch64-linux-gnu-gcc on Windows/WSL and is meant to be installed on aarch64 boards such as Rockchip RK3588.
几个字段的讲究:
字段
值
说错会怎样
Architecture
arm64
写 amd64 板子直接拒装;写 all 只适用于纯脚本/数据
Version
1.0.0 或 1.0.0-1
后半段 -1 是 Debian 修订号,同版本改打包方式时要涨
Depends
libc6 (>= 2.34)
交叉编译时 dpkg 不会自动分析,得自己按目标 rootfs 的 glibc 版本写
Description
首行摘要 + 缩进行
缩进行必须以一个空格 起头;空行写 . + 空格
维护者脚本的调用时机(脚本放在 DEBIAN/ 下,权限必须是 0755 ):
脚本
时机
典型用途
preinst
解包前
停止旧服务、创建用户
postinst
解包后
systemctl daemon-reload、enable 服务
prerm
删文件前
停服务
postrm
删文件后
清理运行时目录
5. 手工打包:dpkg-deb --build 先把"要装进根文件系统的东西"摆成一棵树:
1 2 3 4 5 6 7 8 9 pkgroot/ ├── DEBIAN/ │ ├── control │ ├── conffiles │ ├── postinst │ └── prerm ├── etc/cfgd/cfgd.conf ├── lib/systemd/system/cfgd.service └── usr/local/bin/cfgd
postinst 示例(创建系统用户、注册 systemd 服务):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #!/bin/sh set -ecase "$1 " in configure) if ! id -u cfgd >/dev/null 2>&1; then adduser --system --group --no-create-home --quiet cfgd fi install -d -o cfgd -g cfgd /var/lib/cfgd if [ -d /run/systemd/system ]; then systemctl daemon-reload || true systemctl enable --now cfgd.service || true fi ;; abort-upgrade|abort-remove|abort-deconfigure) ;; *) echo "postinst called with unknown argument '$1 '" >&2 exit 1 ;; esac exit 0
然后一行打包:
1 2 3 4 $ dpkg-deb --build --root-owner-group pkgroot cfgd_1.0.0_arm64.deb dpkg-deb: building package 'cfgd' in 'cfgd_1.0.0_arm64.deb' . $ ls -l cfgd_1.0.0_arm64.deb -rw-r--r-- 1 hespethorn hespethorn 4400 Sep 14 13:44 cfgd_1.0.0_arm64.deb
--root-owner-group 是必加的:不加的话包内文件属主会变成你自己的账户。实测对比:
1 2 3 4 $ dpkg-deb --build pkgroot /tmp/noroot.deb $ dpkg-deb -c /tmp/noroot.deb | grep bin/cfgd -rwxr-xr-x hespethorn/hespethorn 13424 ... ./usr/local/bin/cfgd ^^^^^^^^^^^^^^^^^^^ 应该是 root/root
验收三连:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 $ ar t cfgd_1.0.0_arm64.deb debian-binary control.tar.zst data.tar.zst $ dpkg-deb -I cfgd_1.0.0_arm64.deb new Debian package, version 2.0. size 4400 bytes: control archive=799 bytes. 20 bytes, 1 lines conffiles 421 bytes, 12 lines control 566 bytes, 24 lines * postinst 376 bytes, 20 lines * prerm Package: cfgd Version: 1.0.0 Architecture: arm64 Depends: libc6 (>= 2.34) ... $ dpkg-deb -c cfgd_1.0.0_arm64.deb drwxr-xr-x root/root 0 ... ./etc/cfgd/ -rwxr-xr-x root/root 108 ... ./etc/cfgd/cfgd.conf drwxr-xr-x root/root 0 ... ./lib/systemd/system/ -rwxr-xr-x root/root 197 ... ./lib/systemd/system/cfgd.service -rwxr-xr-x root/root 13424 ... ./usr/local/bin/cfgd
6. 五个必踩的坑(全部本机实测) 坑 1:在 /mnt/c 下准备包树 —— dpkg-deb 直接拒绝 这是 Windows 场景独有的坑。/mnt/c 是 drvfs,没有 Unix 权限位 ,所有文件一律 0777:
1 2 3 4 5 6 7 $ find /mnt/c/.../winfs -printf "%M %u:%g %p\n" -rwxrwxrwx hespethorn:hespethorn .../winfs/DEBIAN/control -rwxrwxrwx hespethorn:hespethorn .../winfs/usr/local/bin/cfgd drwxrwxrwx hespethorn:hespethorn .../winfs/DEBIAN $ dpkg-deb --build --root-owner-group /mnt/c/.../winfs /tmp/winfs.deb dpkg-deb: error: control directory has bad permissions 777 (must be >=0755 and <=0775)
结论:包树必须在 WSL 的 ext4 分区里搭(~/、/tmp 都行),绝不能放在 /mnt/c、/mnt/d。 可以源码放 Windows 侧(用 VS Code 编辑方便),构建脚本负责 cp -r 到 ~/ 再 chmod——但这一步必须做 。
坑 2:CRLF 换行 —— 脚本变成 /bin/sh^M Windows 编辑器保存的 postinst,行尾是 \r\n。在 Linux 上执行:
1 2 3 4 5 $ ./postinst configure bash: ./postinst: /bin/sh^M: bad interpreter: No such file or directory $ sh ./postinst configure postinst: 4: Syntax error: word unexpected (expecting "in" )
\r 混进了 case ... in,连语法检查 sh -n 都可能放它过关(实测 sh -n 返回 0),只有真跑才炸 。三道防线:
1 2 3 4 5 6 7 8 git config core.autocrlf input sed -i 's/\r$//' pkgroot/DEBIAN/* file pkgroot/DEBIAN/postinst | grep -q CRLF && { echo "CRLF detected" ; exit 1; }
坑 3:Architecture 写成 amd64 忘了改这一行,包能打出来、能 scp 到板子,但装的时候:
1 2 3 $ dpkg -i cfgd_1.0.0_arm64.deb dpkg: error processing archive cfgd_1.0.0_arm64.deb (--install): package architecture (arm64) does not match system (amd64)
上面这段是我在 amd64 的 WSL 上装 arm64 包时的真实报错——反过来看,它恰好证明我们的 Architecture: arm64 是对的 :同一条命令,在 arm64 板子上就会顺顺利利地装进去。
坑 4:依赖声明靠猜 交叉编译环境下 dpkg-shlibdeps 无从分析 arm64 二进制(它需要目标架构的库文件与符号表)。要么老老实实手写 Depends,要么先把 arm64 库挂进来:
1 2 3 4 $ sudo dpkg --add-architecture arm64 $ sudo apt update $ sudo apt install -y libc6:arm64 libstdc++6:arm64 $ dpkg-shlibdeps -e ./usr/local/bin/cfgd ...
更省事的做法:打完包用 apt install ./xxx.deb(而不是 dpkg -i),让 apt 自己去补依赖。
坑 5:postinst 里无脑调 systemctl 在 chroot、容器、CI 里做包测试时,systemctl 一定失败。标准写法是先探测:
1 2 3 4 if [ -d /run/systemd/system ]; then systemctl daemon-reload || true systemctl enable --now cfgd.service || true fi
注意每个 systemctl 后面的 || true——维护者脚本非 0 退出会导致整个包进入 half-configured 状态 ,比服务没起来更难收拾。
7. 工程化:CMake + CPack 一条命令出包 手工摆树适合一次性打包,长期维护还是交给 CPack。先写工具链文件:
1 2 3 4 5 6 7 8 9 10 11 12 set (CMAKE_SYSTEM_NAME Linux)set (CMAKE_SYSTEM_PROCESSOR aarch64)set (TRIPLE aarch64-linux-gnu)set (CMAKE_C_COMPILER /usr/bin/${TRIPLE} -gcc)set (CMAKE_CXX_COMPILER /usr/bin/${TRIPLE} -g++)set (CMAKE_FIND_ROOT_PATH /usr/${TRIPLE} )set (CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set (CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set (CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
CMakeLists.txt 里的打包段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 set (CPACK_GENERATOR "DEB" )set (CPACK_PACKAGE_NAME "cfgd" )set (CPACK_PACKAGE_CONTACT "Hespethorn <hespethorn@example.com>" )set (CPACK_PACKAGING_INSTALL_PREFIX "/" )set (CPACK_DEBIAN_PACKAGE_ARCHITECTURE "arm64" ) set (CPACK_DEBIAN_FILE_NAME "DEB-DEFAULT" ) set (CPACK_DEBIAN_PACKAGE_DEPENDS "libc6 (>= 2.34)" )set (CPACK_DEBIAN_PACKAGE_SHLIBDEPS OFF ) set (CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA "${CMAKE_CURRENT_SOURCE_DIR}/debian/postinst;\ ${CMAKE_CURRENT_SOURCE_DIR}/debian/prerm;\ ${CMAKE_CURRENT_SOURCE_DIR}/debian/conffiles" )include (CPack)
跑起来:
1 2 3 4 5 6 7 8 $ cmake ../src -DCMAKE_TOOLCHAIN_FILE=../src/toolchain-aarch64.cmake -- Configuring done -- Generating done $ make [100%] Built target cfgd $ cpack -G DEB CPack: Create package using DEB CPack: - package: /home/hespethorn/deb-demo/build/cfgd_1.0.0_arm64.deb generated.
两个 CPack 专属坑,我都踩到了:
坑 A:/usr/usr/local/bin。 CPack 的默认 CPACK_PACKAGING_INSTALL_PREFIX 是 /usr,而 install(DESTINATION usr/local/bin) 是相对路径,两者叠起来就成双前缀。实测第一次打出来的包:
1 2 3 $ dpkg-deb -c cfgd_1.0.0_arm64.deb -rw-r--r-- root/root 108 ... ./usr/etc/cfgd/cfgd.conf -rwxr-xr-x root/root 13392 ... ./usr/usr/local/bin/cfgd <-- 错了!
加上 set(CPACK_PACKAGING_INSTALL_PREFIX "/") 后恢复正确:
1 2 3 -rw-r--r-- root/root 108 ... ./etc/cfgd/cfgd.conf drwxr-xr-x root/root 0 ... ./lib/systemd/system/ -rwxr-xr-x root/root 13392 ... ./usr/local/bin/cfgd <-- 对了
坑 B:CPACK_DEBIAN_PACKAGE_CONFFILES 不生效。 在 CMake 3.22 上设了这个变量,conffiles 依然没进包(dpkg-deb -I 里根本没这一项)。正确做法是把 conffiles 文件塞进 CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA 列表 :
1 2 3 4 5 6 7 8 $ dpkg-deb -I cfgd_1.0.0_arm64.deb | head -8 new Debian package, version 2.0. size 4624 bytes: control archive=840 bytes. 20 bytes, 1 lines conffiles <-- 进来了 255 bytes, 10 lines control 172 bytes, 3 lines md5sums 566 bytes, 24 lines * postinst 376 bytes, 20 lines * prerm
最后,从包里把二进制解出来再跑一遍——验证的是最终的交付物,不是编译目录里的临时文件 :
1 2 3 $ rm -rf /tmp/x && mkdir /tmp/x && dpkg-deb -x cfgd_1.0.0_arm64.deb /tmp/x $ qemu-aarch64-static -L /usr/aarch64-linux-gnu /tmp/x/usr/local/bin/cfgd --version cfgd 1.0.0 (aarch64)
完整流水线:每一步都有可自动化的检查点
源码(Windows)
D:\...\src
cp 到 ext4
~/deb-demo
交叉编译
aarch64-gcc
cpack -G DEB
_arm64.deb
板子
RK3588
检查点 1:file 输出必须是 "ARM aarch64" · 检查点 2:file 不得含 CRLF · 检查点 3:dpkg-deb -I 里 Architecture: arm64
检查点 4:dpkg-deb -c 里没有 ./usr/usr 双前缀 · 检查点 5:qemu-aarch64-static 能从包里解出的二进制跑通
红线:包树不能放 /mnt/c(权限 777 直接失败)
红线:脚本必须 0755,且必须是 LF 换行
交付:scp 到板子后用 apt install ./x.deb(apt 会补依赖,
dpkg -i 不会)· 回滚用 apt remove / purge
8. 到板子上:安装、验证、回滚 1 2 3 4 5 6 7 8 9 10 $ scp cfgd_1.0.0_arm64.deb user@rk3588:/tmp/ $ ssh user@rk3588 'dpkg --print-architecture; uname -m' arm64 aarch64 $ ssh user@rk3588 'sudo apt install -y /tmp/cfgd_1.0.0_arm64.deb'
验证:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $ ssh user@rk3588 'dpkg -l cfgd' ii cfgd 1.0.0 arm64 Tiny config service (arm64 build) $ ssh user@rk3588 'dpkg -L cfgd' /usr/local/bin/cfgd /etc/cfgd/cfgd.conf /lib/systemd/system/cfgd.service $ ssh user@rk3588 'cfgd --version' cfgd 1.0.0 (aarch64) $ ssh user@rk3588 'systemctl status cfgd --no-pager | head -5' ● cfgd.service - Tiny config service (cfgd) Loaded: loaded (/lib/systemd/system/cfgd.service; enabled) Active: active (running)
回滚与清理:
1 2 3 $ sudo apt remove cfgd $ sudo apt purge cfgd $ dpkg -s cfgd
要是出现 Exec format error,别怀疑 deb 打包——回去看第 3 节那个 file 检查点 ,99% 是误把 x86-64 二进制打进了 arm64 包(包照样能装成功,只是跑不起来)。
9. 三种打包方式对照
维度
dpkg-deb --build
CMake + CPack
dpkg-buildpackage
上手成本
最低,一条命令
中,要写 toolchain + CPack 段
最高,要 debian/ 全套
适合场景
一次性交付、救急
日常迭代 / CI(推荐)
进 Debian/Ubuntu 官方源
架构控制
control 里手写
CPACK_DEBIAN_PACKAGE_ARCHITECTURE
debian/control + 构建机架构
依赖分析
手写
手动或 shlibdeps
自动(本机架构)
源码包
无
有(cpack -G TGZ)
有(.dsc + .orig.tar)
交叉编译友好度
好(跟架构无关)
好(显式声明即可)
差(默认假定本机架构)
常见翻车点
/mnt/c 权限、CRLF
/usr/usr 双前缀、conffiles 失效
需要目标架构的构建依赖
小结
deb 是归档格式不是编译产物 ,所以"在 Windows 上打 arm64 包"这件事只需 WSL + 交叉工具链 + dpkg-deb,跟目标机器在不在场无关。
两条独立的正确性 :二进制要 file 确认是 ARM aarch64;元数据要 Architecture: arm64。少任何一条,都会以不同方式翻车(前者是 Exec format error,后者是 does not match system)。
包树必须在 WSL 的 ext4 分区里 ,/mnt/c 下权限全 0777,dpkg-deb 会直接报 control directory has bad permissions 777。
脚本要 0755 + LF 。CRLF 会让 postinst 变成 /bin/sh^M: bad interpreter,而 sh -n 语法检查还抓不到它。
CPack 的两个坑 :必须设 CPACK_PACKAGING_INSTALL_PREFIX "/" 否则出现 /usr/usr/local/bin;conffiles 要走 CONTROL_EXTRA 而不是 CPACK_DEBIAN_PACKAGE_CONFFILES。
交叉编译要关 SHLIBDEPS ,依赖手写或从 arm64 rootfs 分析。
用 qemu-user-static 在打包机上跑一遍包里的二进制 ——把"能不能跑"这个问题从"上板子之后"提前到"出包之后",省掉一轮 scp。
装包用 apt install ./x.deb 而不是 dpkg -i ,前者会自动补依赖;回滚用 apt remove / purge。