news 2026/9/28 18:59:08

Windows 下构建 arm64 deb 安装包:三大误区与完整流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Windows 下构建 arm64 deb 安装包:三大误区与完整流程

干过这类事的朋友应该能理解,接到“在 Windows 上打一个 arm64 的 deb 安装包”这种需求的时候,第一反应多半是有点懵的。我这次的任务,是给一台跑 Debian 系统的 ARM 架构设备发布一个命令行小工具,但我的开发机是一台 Windows 笔记本。折腾完整个流程之后回头复盘,真正绕弯的地方其实不在“Windows”也不在“deb”本身,而在三个我一开始想当然的假设:觉得 deb 必须上 Linux 才能打、觉得 arm64 只是把包描述里的架构字段改一改就行、觉得包打出来就等于能装、能跑。这篇就把这三个误区摊开讲清楚,同时把我最后跑通的完整流程放出来,供同样在 Windows 上做 Linux 包交付的兄弟参考。

1. 任务背景与方案选型

1.1 这到底是个什么任务

先交代一下场景。目标设备是 ARM64(AArch64)架构的 Debian 板子,跑的是精简版系统,没有图形界面,我用一个小型命令行工具去采集设备状态并通过 HTTP 上报。交付方式是 .deb 安装包,这样运维同学拿到包以后执行dpkg -i或者apt install ./xxx.deb就能装好,卸载也很干净。

开发机上装的是 Windows 11,项目仓库在 Git 里,代码本地编译能过。问题来了:我需要产出的,是一个给“Linux + ARM 架构”用的安装包,而我的开发环境是“Windows + x86_64”。任务拆开看其实是三件事:先为 arm64 Linux 交叉编译出可执行文件,再把这个文件按照 deb 包的规范封装成归档,最后在目标设备上实测安装与运行。

这一步接一步的问题,分别踩中了那三个误区。

1.2 三条打包路线的对比

我在动手之前简单列了一下可行方案,各有利弊,我直接把关键参数和取舍列出来:

方案核心工具风险与成本适用场景
A:WSL 2 里完成一切WSL 发行版 + dpkg-deb / lintian依赖 WSL 环境,路径转换时有坑,但最接近原生 Linux经常持续交付 Linux 包的团队
B:Docker 容器挂载目录Linux 容器 + fpm / dpkg-deb需要 Docker Desktop,容器与宿主机路径分离,但环境可复现配合 CI 或本地一键构建
C:Windows 原生打包Ruby + fpm,或手动构造 ar 归档fpm 对环境依赖多,权限位处理麻烦偶尔打一包、不想装 Docker 时使用

我最后选的是方案 B 为主、方案 C 备用的混合办法。理由很简单:Docker 容器里是完整的 Linux 环境,dpkg-deb、lintian、fpm这些工具都是官方的,连交叉编译的gcc-aarch64-linux-gnu也能直接 apt 装上;对 Windows 而言,只要求装个 Docker Desktop,把项目目录挂载进去,构建产物再落回 Windows 磁盘,等于既用了 Linux 的打包工具链,又没离开 Windows 的工作习惯。

1.3 为什么坚持“在 Windows 上”完成

有人会问:既然都要用 Docker 了,为什么不干脆在 Linux 虚拟机里做?我当时的实际约束是:团队协作和文件流转都围绕 Windows 办公环境,运维交付流程里需要从我的开发机直接拿到产物并做签名登记;临时再拉一台 Linux 虚拟机,反而要多维护一套环境。

而且这个需求不是一次性任务,之后每次版本迭代都要在 Windows 上重新打 arm64 deb 包。所以我要的不是“这一次能打出来”,而是一条可重复执行的构建路径。Docker 方案的自然好处是,把整个打包流程写成一个脚本或 Dockerfile 后,Windows 上的任何一台机器都能复现代,不用再折腾环境。

2. 第一个误区:deb 包只能在 Linux 环境里打

2.1 deb 包的物理结构到底是什么

先打破一个心理障碍:deb 不是一个神秘的“Linux 专属格式”,它本质上就是一个 Unix 世界的归档文件。一个标准的 deb 包内部由三部分组成:

  • debian-binary:纯文本文件,内容通常是2.0,表示包格式版本;
  • control.tar.gz:存放软件包元信息,比如control、md5sums、conffiles;
  • data.tar.xz:实际要安装到系统里的文件,按根目录结构排列,比如usr/bin/xxx。

这三部分被ar归档工具封装成一个整体。Linux 下的dpkg-deb做的事情,本质上就是“组装这个归档”和“从归档里还原文件”。理解了这一点,就会明白:只要能在某种环境下构造出这三部分内容,deb 包就能打出来,不一定非得是 Linux 操作系统本身。

我当时想当然地把“deb”和“Linux 桌面环境”绑在一起,以为离开 Linux 就寸步难行。其实 Windows 上完全可以手工构造 ar 归档,只是这样太原始、容易出错,不值得在生产流程里用。

2.2 在 Windows 上干这件事的三条路

第一条,Ruby + fpm。fpm是一个把任意目录变成 deb、rpm 等格式的打包工具,在 Windows 上装上 Ruby 和 DevKit 后,gem install fpm就能用。它能在 Windows 文件系统上直接生成 deb,不需要 Linux。但是有个经典坑:Windows 的 NTFS 没有 Unix 权限位,fpm 打包时不能自动识别“这个文件是否可以执行”,所以最终 deb 里的可执行文件可能没有+x权限,装到 Linux 上直接“Permission denied”。

第二条,Docker 容器。这是我最推荐的方式。Windows 上的 Docker Desktop 可以无缝启动 Linux 容器,容器内部就是完整的 Debian/Ubuntu 环境。把 Windows 的项目目录用-v D:\project:/build方式挂载进去,在容器里安装dpkg-dev、fpm、gcc-aarch64-linux-gnu,然后执行打包命令,生成的 deb 文件会落在 Windows 磁盘上。权限问题也能顺手解决,因为容器内文件系统是真正的 Linux 文件系统,chmod +x生效。

第三条,WSL 2。装了 WSL 2 之后,子系统本身就是 Linux,路径直接就能访问 Windows 盘,用起来最像“在 Linux 里”。但如果你已经在用 Docker Desktop,它底层多半也是 WSL 2 后端,再开一个发行版实例意义不大;而且 WSL 的发行版如果只是临时用,维护成本略高。

2.3 我最终选择容器方案的原因

我选容器方案,还有一个很实际的理由:交叉编译器。我的目标设备是 arm64,如果程序是非解释型的编译产物,必须用交叉编译器生成 ARM 指令集的 ELF。Windows 原生的交叉编译工具链不是没有,但配置起来相当折腾,而 Debian 容器里一条apt install gcc-aarch64-linux-gnu就全齐了。后面小节会详细讲,但这里先点一句:打包很快,真正决定成败的是你有没有把“能在目标架构上跑的二进制”放进去。

3. 第二个误区:arm64 不只是一个字段

3.1 交叉编译的准备工作

这是我这次最深刻的一个教训。我当时以为,只要在 fpm 或者 dpkg 的时候把Architecture字段写成arm64,包打出来就能用。现实是:架构字段只负责“告诉 apt 这个包装到哪种系统上”,它不负责“让包里那个文件真的能在 ARM 上运行”。

deb 包里放的如果是编译好的二进制程序,那这个文件必须已经是 ARM64 指令集的 ELF。换句话说,在 Windows 的GOARCH=amd64下编译出来的程序,即使包名写着_arm64.deb,装到 ARM 设备上也会直接报“cannot execute binary file: Exec format error”。

提前准备的工作很简单:确认目标架构名。Debian 系官方架构名是arm64,对应 AArch64;Android 上常见的arm64-v8a不能直接搬过来用,因为 apt 认识的是arm64。我一开始受过 Android 习惯影响,差点写成arm64-v8a。

3.2 不同语言工具的交叉编译做法

如果你的交付物是二进制程序,具体准备方式取决于语言:

Go 是最省心的。设置两个环境变量即可:

$env:GOOS="linux" $env:GOARCH="arm64" $env:CGO_ENABLED="0" go build -o mytool .

建议关掉 CGO,这样产物是纯静态链接,不依赖目标机的 glibc,装哪都能跑。如果必须开启 CGO 并依赖 C 库,那就需要配合aarch64-linux-gnu-gcc,在 Windows 上直接交叉编译会麻烦很多。

Rust 的步骤也很标准,安装目标后编译即可:

rustup target add aarch64-unknown-linux-gnu cargo build --release --target aarch64-unknown-linux-gnu

注意,aarch64-unknown-linux-gnu这个目标默认是动态链接到 glibc 的。如果你的目标设备是精简镜像、glibc 版本较老,同样可能遇到运行库版本不匹配的问题。想走得稳,可以研究aarch64-unknown-linux-musl的静态目标。

C/C++ 项目通常需要明确的交叉编译工具链,在 Debian 容器里就是:

apt install gcc-aarch64-linux-gnu aarch64-linux-gnu-gcc -static -o mytool main.c

-static能避免目标设备缺共享库的麻烦;如果坚持动态链接,至少要在目标设备上确认相关.so都在,这个排查成本比较高。

3.3 最容易翻车的点:动态链接和架构识别

我用 Go 写的工具,因为默认静态链接,当时信心满满。但后来在容器里跑了一个file命令才踏实:

file mytool # 输出形如:ELF 64-bit LSB executable, ARM aarch64, statically linked

如果你看到dynamically linked,就必须继续查它依赖了哪些库。可以执行:

readelf -d mytool | grep NEEDED

我就在这个问题上栽过:某一次用 C 写的小工具没有加-static,动态链接到了容器里较新版本的libc.so.6,而目标设备的libc6版本比较老,安装时依赖都没问题(因为控制信息只声明了libc6,没有精确到版本),一运行就报/lib/aarch64-linux-gnu/libc.so.6: version 'GLIBC_2.34' not found。这个真的要提前查清楚。

3.4 顺带提醒:脚本类程序也要注意架构

如果你的包里的主程序是 Python 脚本或者 Shell 脚本,没有二进制的架构问题,但依赖的第三方 Python 包可能包含编译好的.so,这时候还是得要求“依赖里写明 Python 版本和平台”。所以“arm64 只是个字段”这个想法,对纯脚本成立,对编译产物和含原生扩展的脚本都不成立。

4. 第三个误区:deb 打出来不等于能安装

4.1 可执行权限是怎么丢的

这是我在 Windows 上打包遇见的最具欺骗性的问题。deb 包里的文件在安装时会按照归档里记录的权限位还原到系统里,如果原始归档里没有标记可执行权限,装完以后/usr/bin/mytool的权限是-rw-r--r--,用户执行时就只能看到Permission denied。

Windows 上为什么容易丢权限?因为 NTFS 文件系统没有完整的 Unix 权限模型,你从 Windows 复制到 Linux 容器或者用 Windows 工具做归档时,+x信息本来就是缺失的。我在第一次试打包时,用 fpm 直接在 Windows 里指向一个打包好的目录,出来的 deb 装到 Linux 上就是“没有执行权限”。

解决办法其实不复杂:在容器内归档之前,对可执行文件执行一次chmod +x。用 fpm 时,也可以显式声明模式:

fpm -s dir -t deb \ --name mytool \ --version 1.0.0 \ --architecture arm64 \ --deb-user root \ --deb-group root \ --deb-file-mode 755 \ ./mytool=/usr/bin/mytool

如果不用 fpm,而是用dpkg-deb,则需要在容器里保证源目录内权限正确后再执行dpkg-deb --build --root-owner-group root ./package_dir。

4.2 架构字段和版本号的坑

架构字段前面讲过,必须写arm64。而版本号也是一个容易出错的地方。Debian 的版本号规则是[epoch:]upstream_version[-debian_revision],upstream_version只允许数字、字母和. + - ~这几个符号,不允许下划线。如果你习惯性地传--version 1.0.0_beta,fpm 会自动把下划线变成别的符号,或者直接报错,安装时会提示 “bad version”。

我建议版本号一律保持a.b.c或a.b.c-build这种格式,简单不容易错。

4.3 包内路径结构

deb 包内的文件路径是相对根目录展开的。也就是说,你希望程序最终落在/usr/bin/mytool,归档里的路径就应该是usr/bin/mytool,不能带前导/,也不能是 Windows 风格路径D:\...。我在构建目录时,采用的是先做好包含usr/bin结构的 staging 目录,再把整个usr目录放进去,这样结构清晰、不容易乱。

4.4 安装前必须跑的验证清单

我最后摸索出一个固定流程,每次打完包都先做一遍,可以提前拦截大部分问题:

  • 用dpkg-deb --info mytool_1.0.0_arm64.deb检查元信息,确认 Architecture、Depends、Size 等;
  • 用dpkg-deb -c mytool_1.0.0_arm64.deb检查归档内的路径和权限位;
  • 在容器内实际执行dpkg -i或apt install ./mytool_1.0.0_arm64.deb,看有没有依赖问题;
  • 安装后直接执行which mytool && mytool --version,确认可执行位和程序本身没问题。

如果是干净容器,直接docker exec进容器里安装验证就行。这比“打完包就发出去”的信任感要强得多。

5. 完整实操流程:从 Windows 到 ARM 设备

5.1 工具准备清单

我在 Windows 上准备这些东西,一次性解决环境问题:

  • Docker Desktop,并确保 Docker 能启动 Linux 容器;
  • 项目代码目录,比如D:\work\mytool;
  • 一个用于交叉编译和打包的 Debian 镜像基础,我用的是debian:bullseye或debian:bookworm都可以。

Docker Desktop 在 Windows 上第一次跑 Linux 容器会有点慢,但第二次开始就快了。用 Docker 而不是直接用 WSL 单独维护环境,是为了让整个构建流程可以放进一个脚本里,后续任何同事在 Windows 上都能一键复现。

5.2 一个完整的构建脚本

我最终落地的方式是:先写一个小的构建脚本,然后在 Windows 上用 Docker 挂载目录执行。下面是脚本的主干,你可以根据项目改。

#!/bin/bash set -e # 1. 编译准备 cd /build export GOOS=linux export GOARCH=arm64 export CGO_ENABLED=0 # 2. 交叉编译 Go 程序 go build -o /out/mytool . # 3. 组装 staging 目录 mkdir -p /work/usr/bin cp /out/mytool /work/usr/bin/mytool chmod +x /work/usr/bin/mytool # 4. 制作 deb 包 cd /work fpm -s dir -t deb \ -n mytool \ -v 1.0.0 \ -a arm64 \ --deb-user root \ --deb-group root \ --deb-file-mode 755 \ --description "A small tool for device status collection" \ --url "https://example.com/mytool" \ --license "MIT" \ -p /out/mytool_1.0.0_arm64.deb \ ./usr=/usr # 5. 验证 dpkg-deb --info /out/mytool_1.0.0_arm64.deb dpkg-deb -c /out/mytool_1.0.0_arm64.deb

注意-p /out/...指定输出路径,/out是容器内挂载点,对应 Windows 上的某个目录。假设项目在D:\work\mytool,保存脚本为build.sh后,在 Windows PowerShell 里执行:

docker run --rm -v D:\work\mytool:/build -v D:\work\output:/out -w /build debian:bookworm bash -c "apt update && apt install -y golang ruby ruby-dev build-essential dpkg-dev && gem install fpm --no-document && bash build.sh"

这行命令会把整个构建过程自动化:容器里自动装好 Go、Ruby、fpm,然后执行脚本,产物直接写到D:\work\output。容器的沙箱化环境还有个额外好处:不会把 Windows 本地的 PATH 变量污染带进去,编译环境每次都是干净的。

5.3 实际安装验证的日志长什么样

为了说明效果,我把验证步骤贴出来。进入目标设备或容器:

apt install ./mytool_1.0.0_arm64.deb

正常输出大体是:

Reading package lists... Done Building dependency tree... Done Reading state information... Done Selecting previously unselected package mytool. (Reading database ... 52348 files and directories currently installed.) Preparing to unpack mytool_1.0.0_arm64.deb ... Unpacking mytool (1.0.0) ... Setting up mytool (1.0.0) ...

然后执行:

which mytool # /usr/bin/mytool mytool --version # mytool version 1.0.0

如果能走到这一步,说明架构、权限、依赖、路径这四关都过了。我自己的经验是:第一次真正的失败就是在最后一步,mytool --version直接Permission denied,当时立刻意识到是权限问题。

5.4 从 Windows 到 ARM 设备

整个链条的最后一段,是把 deb 文件传到目标设备。我在 Windows 上常用scp(OpenSSH 自带):

scp D:\work\output\mytool_1.0.0_arm64.deb user@device:/tmp/

目标设备上执行安装即可。如果你的网络环境内网有 apt 仓库,也可以先把 deb 放到仓库里再apt install mytool,但这不是本文重点。

6. 常见问题排查表格与避坑心得

6.1 高频问题速查表

我把这次实际操作中遇到过的、以及同行常踩的问题整理成一个表,方便直接对照:

现象根本原因处理方式
安装后执行报Permission denied文件没有可执行权限容器内chmod +x,或 fpm 参数--deb-file-mode 755
安装时报package architecture (amd64) does not match system (arm64)deb 内 Architecture 字段错误fpm 加-a arm64,或用 dpkg-deb 时确保 control 里写arm64
运行时提示cannot execute binary file: Exec format error二进制不是 ARM64 指令集重新交叉编译;用file命令检查产物格式
安装时提示缺少依赖,比如libc6 (>= 2.34)动态链接到高版本 glibc尽量静态链接;或确认目标设备 glibc 版本
安装完成后找不到命令路径结构不对检查 deb 包内路径是否usr/bin/, 不要带前导/
dpkg -i执行时提示bad version版本号含下划线等非法字符版本号严格用字母、数字、.、-、~
Windows fpm 打包后 deb 能安装但程序无法运行可能权限/架构/依赖之一有问题优先在 Linux 容器内复核全套四个验证命令

6.2 几个零碎但很容易忽略的提醒

第一个是符号链接。如果你的软件包内需要放符号链接,比如一些服务习惯把配置指向.config,用 Windows 工具做归档时,符号链接经常会被当成普通文本文件。容器内制作 staging 时,用 Linux 的ln -s生成真实符号链接,验证时用ls -l检查是否显示->,这一步别省略。

第二个是关于md5sums控制文件。dpkg-deb --build会自动生成,但如果你用 fpm,它也会自动处理。不需要手工维护,只是提醒你:不要在打包过程中随意修改归档内的控制文件,否则会造成安装校验问题。

第三个小技巧:多次打包后,我发现把“验证命令”也放进同一个脚本,出问题的概率会显著下降。不要省这几十秒,验证命令不需要执行到目标设备上,只需要在干净容器里跑一次apt install ./xxx.deb,就能拦截大部分低级错误。

6.3 我这次印象最深的一条教训

要说印象最深,其实是“权限陷阱”不是架构陷阱。我一开始花了大量时间研究交叉编译,觉得 arm64 是最难的关。实际上,Go 的交叉编译一行命令就解决了;反而是 Windows 打包时代理出来的权限问题,让我在最后一步栽了跟头。这个事很典型地说明了一件事:做跨平台交付的时候,对“源平台”和“目标平台”差异的盲区,往往才是真正的敌人。

最后再分享一个小经验

如果你也经常要在 Windows 上做 Linux 包交付,我强烈建议把整套流程沉淀成脚本加一个简单的说明文档,放进仓库的build/目录里。我这次用的脚本,后续同事直接复制到自己的 Windows 机器上,改一下版本号就能跑,不用重新摸索。打包这事看着不复杂,但每一步的“想当然”都可能让最终产物在目标设备上崩掉;把验证步骤固化下来,比临时记在脑子里可靠得多。以后再遇到类似需求,我也会第一时间先确认目标架构和链接方式,而不是急着动手打包。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 18:59:00

OpenHarmony I2C驱动开发实战:从协议原理到排障优化

1. I2C 总线到底是个什么东西1.1 从两根线说起:I2C 的物理层本质I2C 这玩意儿,全称叫 Inter-Integrated Circuit,中文一般叫“集成电路总线”。名字听着挺唬人,但说白了它就是两根线:一根 SCL(串行时钟线&a…

作者头像 李华
网站建设 2026/9/28 18:55:31

AI驱动Fluent仿真:正弦摆动焊接熔池UDF配置与全流程验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华