Ubuntu 20.04 DEB打包实战:从手工构建到专业工具链
1. 项目概述为什么我们需要自己打包DEB在Ubuntu或者Debian系Linux上混迹过一段时间的开发者或系统管理员大概率都遇到过这样的场景你写了一个好用的脚本工具或者从源码编译了一个软件想分发给团队其他成员使用。直接扔源码过去对方可能缺少编译环境。扔个二进制文件又得手动处理依赖和安装路径繁琐且容易出错。这时候一个标准的DEB包就能完美解决所有问题——它像是一个集装箱把你的应用、配置文件、依赖声明、安装和卸载脚本都打包在一起用户只需一个sudo dpkg -i命令就能完成干净利落的部署。网上关于打包DEB的教程不少但很多要么过于简略只给个dh_make和debuild的命令行就结束了要么就是年代久远步骤在新版本系统上已经走不通。更让人头疼的是那些隐藏的“雷区”比如目录结构不对导致打包失败或者control文件里一个字段写错让安装后的软件无法正常运行。我自己在Ubuntu 20.04 LTS这个依然被广泛使用的稳定版本上就踩过不少坑。今天我就把自己从零开始制作一个功能完整DEB包的详细步骤连同那些容易翻车的地方系统地梳理一遍。目标很明确让你看完之后不仅能成功打出第一个包更能理解每一个步骤背后的逻辑以后遇到任何定制化需求都能从容应对。2. DEB包结构与核心原理拆解在动手之前我们必须先搞清楚一个DEB包肚子里到底装了些什么以及系统在安装它时背后发生了哪些事情。这能帮你从“照葫芦画瓢”上升到“知其所以然”未来排查问题会快得多。2.1 DEB包的本质一个ar归档文件很多人以为DEB是一种特殊的二进制格式其实不然。你可以用ar命令来验证ar t your-package.deb。你会看到类似这样的输出debian-binary control.tar.gz data.tar.gz看DEB包就是一个标准的Unixar归档文件里面固定包含了三个成员debian-binary一个纯文本文件里面就写着DEB格式的版本号例如2.0。这个文件告诉dpkg“嘿我是按2.0版格式打包的。”control.tar.gz这是包的“大脑”和“说明书”。压缩包内包含了描述包元数据如包名、版本、依赖等的control文件以及可能存在的安装前preinst、安装后postinst、卸载前prerm、卸载后postrm等维护者脚本。dpkg在安装时首先会解压这个包来读取“说明书”。data.tar.gz这是包的“身体”。压缩包内包含了软件所有需要安装到目标系统上的文件并且已经按照它们在系统中的最终路径如/usr/bin/,/etc/组织好了目录结构。安装时这些文件会被解压到根目录/下。理解这个结构你就明白了打包的核心工作正确地构建出control.tar.gz和data.tar.gz这两个压缩包。2.2 Control文件包的身份证与合同control文件是control.tar.gz里最重要的文件没有之一。它定义了包的所有元信息。一个最基本的control文件看起来像这样Package: my-awesome-tool Version: 1.0.0-1 Section: utils Priority: optional Architecture: amd64 Depends: bash ( 4.4), python3 ( 3.6) Maintainer: Your Name your.emailexample.com Description: A tool that does awesome things. This is a more detailed description that can span multiple lines. Each new line after the first must start with a space.这里有几个关键字段和极易出错的“雷区”Package包名。只能包含小写字母、数字和连字符-不能以数字开头。这是包的唯一标识。Version版本号。格式为[上游版本]-[Debian修订版]。例如你的软件本身版本是1.0.0这是你第一次为Debian/Ubuntu打包那么修订版就是1。如果后续只修改了打包脚本而软件本身没变版本可以更新为1.0.0-2。雷区1版本号中不能含有冒号:和连字符-以外的其他标点像1.0.0_beta这样的格式是无效的。Architecture架构。amd64、i386、all纯脚本或文档包或any平台相关的二进制包。雷区2如果你打包的是平台无关的脚本如Python、Shell脚本务必使用all否则在别的架构上安装时会报架构不匹配错误。Depends依赖。这是最复杂的字段之一。依赖关系必须足够精确以保证软件运行但又不能过度限制用户环境。bash ( 4.4)表示依赖bash且版本至少为4.4。多个依赖用逗号分隔逗号代表“与”的关系。还可以使用Recommends推荐、Suggests建议、Breaks破坏、Conflicts冲突等字段来定义更丰富的包关系。雷区3依赖包名必须是在官方仓库中存在的确切名称。你可以用apt-cache search来查找。胡乱写一个不存在的包名安装时会直接失败。2.3 维护者脚本安装卸载的钩子这些是可选的Shell脚本但如果你想在安装时创建用户、加载服务或者在卸载时备份配置、清理数据它们就必不可少。preinst在解压data.tar.gz即安装文件之前运行。postinst在解压data.tar.gz之后运行。最常见常用于创建符号链接、更新动态库缓存ldconfig、启用系统服务systemctl enable或交互式询问用户配置。prerm在开始删除文件之前运行。常用于停止正在运行的服务。postrm在删除文件之后运行。常用于删除postinst中创建的符号链接或临时文件。雷区4脚本的健壮性。这些脚本必须以#!/bin/sh开头并且需要充分考虑执行失败的情况。例如在postinst中systemctl enable一个服务前最好先检查服务文件是否存在。脚本的退出状态码非零会被dpkg视为安装/卸载失败。3. 手工打包全流程实操从零到一我们不依赖复杂的dh_make工具链先用手工方式完整地走一遍流程。这能让你对每个环节都有绝对的控制力和清晰的认识。假设我们要打包一个名为hello-ubuntu的简单命令行工具它就是一个打印欢迎信息的Bash脚本。3.1 创建标准的打包工作目录首先建立一个清晰的工作目录结构。这是良好打包习惯的开始。mkdir -p ~/deb-build/hello-ubuntu-1.0.0 cd ~/deb-build/hello-ubuntu-1.0.0在这个目录下我们需要创建DEB包要求的DEBIAN目录注意全大写和模拟系统根目录的data目录。mkdir -p DEBIAN mkdir -p data/usr/binDEBIAN/这个目录下的文件在打包后会被放入control.tar.gz。它不会被安装到用户系统上只包含控制信息。data/这个目录下的结构就是文件在目标系统上的安装路径。data/usr/bin意味着我们的程序最终要安装到/usr/bin/下。3.2 编写软件本体与Control文件创建程序文件在data/usr/bin/下创建我们的“软件”。cat data/usr/bin/hello-ubuntu EOF #!/bin/bash # A simple hello world tool echo Hello from Ubuntu! This is version 1.0.0 EOF chmod x data/usr/bin/hello-ubuntu # 不要忘记加执行权限记住data/目录下的所有文件都应该具备最终安装时你期望的权限。编写核心Control文件在DEBIAN/目录下创建control文件。cat DEBIAN/control EOF Package: hello-ubuntu Version: 1.0.0-1 Section: utils Priority: optional Architecture: all Depends: bash ( 4.4) Maintainer: Alex Wang alexexample.com Description: A friendly greeting tool for Ubuntu users. This tool prints a welcome message. It serves as a simple example for learning DEB package creation. EOF关键点因为我们的软件只是一个Bash脚本所以Architecture设为all。依赖只写了bash。3.3 使用dpkg-deb命令构建DEB包当data和DEBIAN目录都准备好后我们就可以在项目父目录即~/deb-build/下使用dpkg-deb命令进行打包。cd ~/deb-build/ # 回到包含hello-ubuntu-1.0.0目录的层级 dpkg-deb --build hello-ubuntu-1.0.0如果一切顺利你会看到hello-ubuntu-1.0.0.deb文件生成。你可以用以下命令检查它的内容# 查看包信息 dpkg -I hello-ubuntu-1.0.0.deb # 查看包内文件列表 dpkg -c hello-ubuntu-1.0.0.deb3.4 安装测试与卸载在本地测试这个包sudo dpkg -i hello-ubuntu-1.0.0.deb安装后运行hello-ubuntu命令应该能看到输出信息。然后测试卸载sudo dpkg -r hello-ubuntu再运行hello-ubuntu会提示命令未找到。至此一个最基础的DEB包就制作并验证成功了。手工打包的优缺点优点概念清晰控制力强适合简单项目或学习原理。缺点所有步骤手动完成繁琐且易出错。对于需要编译的复杂项目、需要生成man页面、版权文件等标准文档的项目效率太低。4. 使用专业工具链dh_make与debuild对于真实的项目我们通常会使用Debian官方维护的打包工具链它们能自动化处理大量繁琐工作并符合社区最佳实践。这套工具的核心是debhelper。4.1 环境准备与项目初始化首先安装必要的开发工具sudo apt update sudo apt install build-essential dh-make devscripts debhelper假设我们有一个已经开发好的软件项目其源码目录为mysoftware-1.0里面包含了源码、Makefile等。我们进入该目录的父目录然后使用dh_make来初始化打包环境。cd /path/to/parent-directory tar czf mysoftware_1.0.orig.tar.gz mysoftware-1.0/ # 创建上游源码压缩包 cd mysoftware-1.0 dh_make --createorig --single --yes参数解释--createorig如果当前目录没有.orig.tar.gz文件会自动基于当前目录创建一个。--single生成单个二进制包mysoftware。如果你的项目会产生多个包如mysoftware,mysoftware-dev则使用--native纯Debian包或不用此参数需手动编辑debian/control。--yes自动接受默认设置。执行后当前目录下会生成一个debian/目录里面包含了打包所需的所有模板文件。4.2 详解debian目录结构与关键文件生成的debian/目录内容很多我们重点关注以下几个debian/control这是最重要的文件需要你仔细编辑。dh_make生成的模板里已经根据你的包名和源码信息填充了一些字段但Depends、Description等需要你手动完善。对于需要编译的C/C项目Build-Depends字段至关重要它列出了编译本软件所需的依赖如gcc,cmake,libxxx-dev。debian/rules这是make的配置文件是打包过程的“总指挥”。它定义了如何清理clean、配置configure、编译build、安装install等步骤。幸运的是对于大多数标准项目debhelperdh已经帮我们做好了99%的工作所以这个文件通常非常简单可能只有一行#!/usr/bin/make -f %: dh $这表示将所有目标%都委托给dh命令去处理。dh会自动调用一系列以dh_开头的辅助工具如dh_auto_configure,dh_auto_build,dh_auto_install它们会尝试智能地识别你的项目类型Autotools, CMake, Python setuptools等并执行相应操作。debian/changelog版本变更日志。每次打包新版本都必须首先更新此文件因为debhelper会从changelog的第一行提取最新的版本号和发行版信息。你可以使用dch -i命令来交互式地更新它。格式必须严格遵循。debian/install可选如果你的软件构建系统如make install没有将文件安装到debian/tmp目录下或者你需要额外安装一些文件如配置文件、文档就需要这个文件。它列出了源文件相对于编译目录和目标安装路径相对于根目录。例如src/myapp usr/bin configs/myapp.conf etc/myapp/这表示将编译目录下的src/myapp安装到/usr/bin/将configs/myapp.conf安装到/etc/myapp/目录下。debian/postinst,debian/prerm等根据需要从模板修改。默认生成的模板里包含了很多被注释掉的示例代码非常有用。4.3 执行构建与打包编辑好关键文件后就可以在项目根目录即debian/目录的上一级进行构建了。推荐使用debuild命令它会在一个干净的环境下执行构建并自动处理签名等流程。debuild -us -uc参数解释-us不对.dsc源描述文件签名。-uc不对.changes文件签名。对于正式上传到PPA或官方仓库的包需要GPG签名。本地测试可以跳过。debuild会执行一系列复杂的步骤清理环境、安装编译依赖、配置、编译、将文件安装到临时目录debian/tmp、生成control文件、打包成DEB。整个过程会输出大量日志。如果成功你会在父目录下找到生成的.deb文件如../mysoftware_1.0-1_amd64.deb。5. 高级主题与常见“雷区”排查掌握了基础流程后我们来看看那些容易让人栽跟头的高级场景和问题。5.1 处理复杂的依赖关系场景你的软件依赖一个较新版本的库如libssl3但目标系统如Ubuntu 20.04默认只提供旧版本libssl1.1。解决方案与雷区明确声明在debian/control的Depends中准确写明libssl3 ( 3.0.0)。不要模糊地写libssl。提供解决方案在软件文档或README中明确指出用户需要从官方Backports仓库或软件供应商处获取新版库。对于团队内部包可以考虑将依赖库也打包成DEB并建立本地仓库。雷区绝对不要尝试在postinst脚本里用apt或dpkg强行安装或升级系统关键库如libc,openssl。这极有可能破坏系统的稳定性导致其他软件无法运行是一种非常危险且不专业的行为。依赖管理应该由系统包管理器apt在安装时根据声明解决或在安装前由用户手动处理。5.2 打包包含系统服务的软件场景你的软件是一个守护进程daemon需要配置为systemd服务。标准做法在debian/目录下创建服务文件模板例如debian/mysoftware.service。其内容需符合systemd规范。在debian/install文件中添加一行将服务文件安装到正确位置debian/mysoftware.service lib/systemd/system/在debian/postinst脚本中启用并启动服务需考虑升级场景#!/bin/sh set -e # 启用服务 systemctl daemon-reload /dev/null 21 || true if [ $1 configure ]; then systemctl enable mysoftware.service /dev/null 21 || true # 如果是新安装而非升级则尝试启动 if [ -z $2 ]; then systemctl start mysoftware.service /dev/null 21 || true fi fi在debian/prerm脚本中停止服务在卸载前#!/bin/sh set -e if [ $1 remove ]; then systemctl stop mysoftware.service /dev/null 21 || true systemctl disable mysoftware.service /dev/null 21 || true fi雷区在脚本中直接调用systemctl可能在某些特定环境下失败如容器内无systemd。更健壮的做法是检查/run/systemd/system是否存在或者使用deb-systemd-invoke辅助工具dh-systemd包提供它能更好地处理各种边缘情况。5.3 打包Python/Node.js等解释型语言项目对于现代应用用dh_make的默认模板可能不太合适。以Python为例更推荐使用pybuild或dh的Python插件。推荐流程确保项目有标准的setup.py或pyproject.toml。安装Python开发辅助工具sudo apt install dh-python python3-all。使用dh_make初始化时可以尝试dh_make --python --createorig。生成的debian/rules文件会包含对Python的特定支持。debian/control中的Build-Depends需要加入dh-python, python3-all等。debhelperdh会调用dh_python3等工具自动处理Python模块的安装路径、字节码编译.pyc和依赖生成。雷区虚拟环境venv与系统包。不要尝试将虚拟环境整个打包进DEB。DEB包应该将模块安装到系统的Python站点包目录如/usr/lib/python3/dist-packages/。依赖应通过Depends: python3-xxx来声明而不是将依赖库的代码直接打包。5.4 常见错误与排查清单在打包过程中你几乎一定会遇到错误。以下是一个快速排查指南错误现象可能原因排查步骤dpkg-deb: error: archive xxx.deb uses unknown compression for member control.tar.gz打包时使用的压缩工具或参数不被目标dpkg识别。确保使用gzip -9n进行压缩。使用dpkg-deb --build通常不会出此问题。dpkg: error processing package xxx (--install): dependency problems - leaving unconfigured依赖不满足。包A依赖包B但B未安装。使用apt -f install自动安装缺失依赖。检查debian/control中Depends字段的包名和版本是否正确。dpkg: warning: unable to delete old xxx: Directory not empty在postrm或prerm脚本中尝试删除非空目录或目录权限问题。卸载脚本中删除目录前应确保其为空。对于配置文件目录如/etc/myapp/通常不应在卸载时删除应使用dpkg --purge来清除配置。dh: command not founddebhelper未安装或debian/rules文件首行缺少shebang。安装debhelper包。确保debian/rules第一行是#!/usr/bin/make -f。debuild失败提示无法满足Build-Depends编译依赖未安装。运行sudo apt build-dep .在项目目录下可以自动安装debian/control中列出的所有编译依赖。安装后程序找不到共享库.so文件运行时库依赖未声明或库路径不在默认搜索路径。1. 使用ldd /usr/bin/your-program检查缺失的库。2. 在debian/control的Depends中添加对应的libxxx包。3. 如果库在非标准路径需要在postinst中运行ldconfig或设置LD_LIBRARY_PATH不推荐。打包过程成功但安装后文件权限不对debian/install中指定的文件在打包前权限不正确或debian/rules中的dh_fixperms未正确执行。确保源文件有正确权限。检查debian/rules确保包含了dh_fixperms步骤dh $序列默认包含。也可以手动在debian/install中指定权限但更推荐让debhelper管理。一个关键的调试技巧当debuild失败时不要被满屏的输出吓到。仔细查看错误的最后几行通常会有明确的错误信息。你可以使用debuild --no-tgz-check -us -uc来跳过一些检查或者进入debian/rules中手动执行某个目标如fakeroot debian/rules binary来定位具体在哪一步出错。6. 从构建到分发完整工作流建议当你能够稳定地构建出DEB包后下一步就是考虑如何将其分发给用户。版本控制将整个项目包括debian/目录纳入Git管理。debian/changelog的更新应该作为一次提交。持续集成对于开源项目可以利用GitHub Actions或GitLab CI自动完成打包。在CI脚本中安装build-essential、debhelper等然后运行debuild -us -uc并将生成的.deb文件作为构建产物发布。建立本地仓库对于团队内部使用可以搭建一个简单的APT仓库。基本步骤是将生成的.deb文件放入一个目录如/var/www/html/repo/ubuntu。在该目录下运行dpkg-scanpackages . /dev/null | gzip Packages.gz生成包索引。在客户端机器的/etc/apt/sources.list.d/下添加一个源文件指向你的HTTP服务器。运行sudo apt update后即可用apt install your-package来安装。上传到PPA如果你想与更广大的Ubuntu用户分享可以将其上传到Launchpad上的个人PPA。这需要配置dput工具和GPG密钥过程稍复杂但Launchpad提供了详细的官方文档。最后分享一个我个人的深刻体会打包的本质是标准化和自动化部署。一个好的DEB包能让软件的安装体验从“一堆需要手动执行的命令”变成“一个简单可靠的原子操作”。在制作过程中多从最终用户的角度思考——这个依赖是否过于苛刻安装后的服务是否能正常启动卸载后是否留下了用户数据把这些细节处理好你制作的就不再仅仅是一个软件包而是一个专业、可靠的产品交付物。在Ubuntu 20.04这个依然坚挺的系统上熟练掌握DEB打包无疑是提升你开发运维能力的一项硬核技能。