跳转至

包管理系统

主要作者

@taoky@2403772980ygy

本文已完成,等待校对

包管理系统是现代 Linux 发行版的重要组成部分。以下介绍与 Debian 的包管理系统相关工具,例如 APT(Advanced Package Tool)。其他发行版的包管理系统会有所不同,可参考 Arch Linux Wiki 的 pacman/Rosetta 页面

本文假设读者了解最基础的 apt 使用方法,如 apt install, apt remove, apt update, apt upgrade

APT 常用操作

Debian 有多个与软件包管理相关的工具。

其中的底层工具为 dpkg。dpkg 不负责管理软件依赖关系,只管理具体某一个包的安装、卸载等操作。因此除非需要排查疑难问题,否则不应该直接使用 dpkg 修改系统状态

避免在安装 deb 文件时使用 dpkg

网络上许多教程,甚至是一些官方文档,都会建议使用 dpkg -i 安装 deb 文件。当 deb 存在依赖,并且系统未安装满足要求的依赖时,直接使用 dpkg 会导致系统依赖管理出现问题,需要额外花费精力修复。

建议始终使用 apt install ./path/to/package.deb 的方式安装 deb 文件。

从用户视角来看,最常使用的工具是 apt(以及其他以 apt- 开头的命令)。

标记软件包为自动/手动安装

绝大多数软件包都不是孤立的:它们也有自己的依赖。那么,如果安装了一个带有其他依赖的软件,然后再删除这个软件,其引入的依赖不会被自动删除,不过:

# apt install x11-apps
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following additional packages will be installed:
  bsdextrautils bsdutils fontconfig-config fonts-dejavu-core groff-base libblkid1 libbrotli1 libbsd0 libexpat1 libfontconfig1 libfreetype6 libgdbm6 libice6 libmount1
  libpipeline1 libpng16-16 libsm6 libsmartcols1 libuchardet0 libuuid1 libx11-6 libx11-data libx11-xcb1 libxau6 libxaw7 libxcb-damage0 libxcb-present0 libxcb-xfixes0 libxcb1
  libxcursor1 libxdmcp6 libxext6 libxfixes3 libxft2 libxi6 libxkbfile1 libxmu6 libxmuu1 libxpm4 libxrender1 libxt6 man-db mount util-linux util-linux-extra x11-common
  xbitmaps
Suggested packages:
  groff gdbm-l10n cryptsetup-bin apparmor less www-browser nfs-common dosfstools kbd util-linux-locales mesa-utils
Recommended packages:
  uuid-runtime sensible-utils
The following NEW packages will be installed:
  bsdextrautils fontconfig-config fonts-dejavu-core groff-base libbrotli1 libbsd0 libexpat1 libfontconfig1 libfreetype6 libgdbm6 libice6 libpipeline1 libpng16-16 libsm6
  libuchardet0 libx11-6 libx11-data libx11-xcb1 libxau6 libxaw7 libxcb-damage0 libxcb-present0 libxcb-xfixes0 libxcb1 libxcursor1 libxdmcp6 libxext6 libxfixes3 libxft2
  libxi6 libxkbfile1 libxmu6 libxmuu1 libxpm4 libxrender1 libxt6 man-db x11-apps x11-common xbitmaps
The following packages will be upgraded:
  bsdutils libblkid1 libmount1 libsmartcols1 libuuid1 mount util-linux util-linux-extra
8 upgraded, 40 newly installed, 0 to remove and 4 not upgraded.
(以下省略)
# apt remove x11-apps
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following packages were automatically installed and are no longer required:
  fontconfig-config fonts-dejavu-core groff-base libbrotli1 libbsd0 libexpat1 libfontconfig1 libfreetype6 libgdbm6 libice6 libpipeline1 libpng16-16 libsm6 libuchardet0
  libx11-6 libx11-data libx11-xcb1 libxau6 libxaw7 libxcb-damage0 libxcb-present0 libxcb-xfixes0 libxcb1 libxcursor1 libxdmcp6 libxext6 libxfixes3 libxft2 libxi6
  libxkbfile1 libxmu6 libxmuu1 libxpm4 libxrender1 libxt6 man-db x11-common xbitmaps
Use 'apt autoremove' to remove them.
The following packages will be REMOVED:
  x11-apps
0 upgraded, 0 newly installed, 1 to remove and 4 not upgraded.

可以发现,虽然 x11-apps 在该环境中引入的依赖没有被自动删除,但是 APT 知道哪些依赖是不再被需要的了。这有赖于 APT 的软件包标记功能:用户直接安装的包会被标记为手动安装(manual),而被这样引入的依赖会被标记为自动安装(automatic)。于是,没有被任何手动安装的包直接以及间接依赖的自动安装的包就可以被 apt autoremove 移除。

apt-mark 命令可以显示、修改标记:

  • apt-mark showautoapt-mark showmanual 可以显示系统中被标记为自动安装与手动安装的包。
  • apt-mark auto <package>apt-mark manual <package> 可以修改包的标记。

推荐与建议

安装软件包时,APT 在默认配置下会安装推荐(Recommended)的包。建议(Suggested)的包会显示在安装界面,但是不会自动被安装。例如在 Debian 12 中,docker.io 包的推荐有 apparmor、ca-certificates 等,建议包有 btrfs-progs、debootstrap 等。那么在安装 docker.io 时,包括 apparmor、ca-certificates 等包就会默认被安装,并且用户也可以看到这些包建议,并且可以在当前包安装完成后自行安装。

大部分情况下,被设置为「推荐」的包是有意义的,如果不安装,可能程序仍然可以运行,但是会缺失一些重要的功能。不过在某些环境下,例如容器场景,我们需要安装的包尽可能得少。为了精简安装的软件包,可以使用 --no-install-recommends 的选项,以跳过推荐的软件包。还可以在 apt.conf 配置中添加 Apt::Install-Recommends "false" 以使默认配置不会安装推荐的包。

使用 .conf.d 目录形式,避免直接修改 .conf 配置文件

对于绝大多数 Debian 包来说,软件包对应的配置文件(以下称为 .conf 文件)是直接由软件包安装(管理)的。尽管直接修改配置也可以达到目的,但是在软件包升级,特别是系统大版本更新时,apt 会要求用户手工介入配置冲突问题(保留原配置,或者安装新配置),会带来一些困扰。

目前大部分软件包的配置文件都支持 .conf.d 目录形式,允许用户以不同文件的形式添加自己的配置片段,对应的程序会「导入」这些配置。这种方式不仅可以避免直接修改软件包的配置文件,还可以更好地管理配置文件(例如,用户可以将不同目的的配置以不同的文件名存储,提升配置的可维护性)。

以上文的 apt.conf 为例,这里推荐的做法是在 /etc/apt/apt.conf.d/ 目录下创建一个新的 .conf 文件,在其中写入需要的配置,例如不安装推荐包:

echo 'APT::Install-Recommends "false";' | sudo tee /etc/apt/apt.conf.d/99no-install-recommends

某些软件会根据文件名的字典序来决定配置的优先级,因此这里使用 99 作为前缀,确保这个配置文件在其他配置文件之后被读取。

Debian 与 Ubuntu 均提供了网页端搜索软件包的服务:Debian 软件包Ubuntu Packages Search。不过,使用 apt 工具搜索来快得多。

apt search 与 apt 搜索模式

apt search <name> 会根据包名与描述进行包的查找,支持正则表达式:

$ apt search wayland
Sorting... Done
Full Text Search... Done
bemenu/noble 0.6.15+dfsg-1build2 amd64
  Dynamic menu inspired by dmenu

cage/noble 0.1.5+20240127-2build1 amd64
  Kiosk compositor for Wayland
(以下省略)
$ apt search ^docker
Sorting... Done
Full Text Search... Done
debocker/noble 0.2.5 all
  docker-powered package builder for Debian

docker-buildx/noble-updates 0.14.1-0ubuntu1~24.04.1 amd64
  Docker CLI plugin for extended build capabilities with BuildKit
(以下省略)

不过,有些时候这种搜索也不太符合需求,例如有些时候我们只想搜索包名等,此时可以使用 apt 搜索模式(search pattern)来进行更具体的查找。完整文档可以参考 apt-patterns(7)。搜索模式不适用于 apt search,但适用于其他各类 apt 命令,例如 apt listapt showapt remove 等。

以下是一些常见的 apt 搜索模式,句尾括号为更加繁琐的完整表示:

  • ~nREGEX 包名称满足正则表达式的包(?name(REGEX))。
  • ~c 已经删除,但是仍然有配置残留的包,可以使用 apt purge 彻底删除(?config-files)。
  • ~i 为已经安装的包(?installed)。
  • ~U 可以升级的包(?upgradable)。
  • ~o 远程已经不再存在的包,一般是在系统大版本更新后残留的旧包,或者是本地手动安装的包(?obsolete)。

搜索模式练习

请尝试写出以下查询的搜索模式,并且在自己的环境中试一试:

  • 输出(提示:apt list)所有未完全删除以及远程仓库不再提供的包(虽然 apt purge 也支持搜索模式,小心执行,因为所有配置都会被删除!)
  • 输出本地安装的名字里有 top 的所有包
    • 提示:可以像这样要求同时满足多个 pattern: apt list 'P1 P2 P3'

文件搜索:apt-file

如果使用过默认安装的 Ubuntu 的话,可能会发现,在输入命令时,如果命令不存在,会有类似下面的提示:

$ htop
Command 'htop' not found, but can be installed with:
sudo apt install htop

这是由 command-not-found 包支持的,不过可以注意到,这一项功能会拖慢与 shell 交互时的速度,因此这里更加推荐删除这个包,只在需要的时候用 apt-file 命令搜索。

在操作前,需要执行 apt-file update 命令更新本地文件与包关系的数据库的初始化及更新。之后使用 apt-file search <file> 就可以搜索包含某个文件的包:

$ apt-file search execsnoop
bpfcc-tools: /usr/sbin/execsnoop-bpfcc
bpfcc-tools: /usr/share/doc/bpfcc-tools/examples/doc/execsnoop_example.txt
bpfcc-tools: /usr/share/man/man8/execsnoop-bpfcc.8.gz
bpftrace: /usr/sbin/execsnoop.bt
bpftrace: /usr/share/doc/bpftrace/examples/execsnoop_example.txt
bpftrace: /usr/share/man/man8/execsnoop.bt.8.gz
golang-github-iovisor-gobpf-dev: /usr/share/gocode/src/github.com/iovisor/gobpf/examples/bcc/execsnoop/execsnoop.go
golang-github-iovisor-gobpf-dev: /usr/share/gocode/src/github.com/iovisor/gobpf/examples/bcc/execsnoop/output.go
libbpf-tools: /usr/sbin/execsnoop
pcp: /usr/lib/pcp/pmdas/bcc/modules/execsnoop.bpf
pcp: /usr/lib/pcp/pmdas/bcc/modules/execsnoop.python
pcp: /usr/lib/pcp/pmdas/bpf/modules/execsnoop.so
pcp: /usr/share/pcp/htop/screens/execsnoop
pcp: /var/lib/pcp/pmdas/bcc/modules/execsnoop.bpf
pcp: /var/lib/pcp/pmdas/bcc/modules/execsnoop.python
pcp: /var/lib/pcp/pmdas/bpf/modules/execsnoop.so
perf-tools-unstable: /usr/sbin/execsnoop-perf
perf-tools-unstable: /usr/share/doc/perf-tools-unstable/examples/execsnoop_example.txt
perf-tools-unstable: /usr/share/man/man8/execsnoop-perf.8.gz
systemtap-doc: /usr/share/systemtap/examples/lwtools/execsnoop-nd.8
systemtap-doc: /usr/share/systemtap/examples/lwtools/execsnoop-nd.meta
systemtap-doc: /usr/share/systemtap/examples/lwtools/execsnoop-nd.stp
systemtap-doc: /usr/share/systemtap/examples/lwtools/execsnoop-nd_example.txt

此外,apt-file list <package> 可以查看某个包中包含的文件:

$ apt-file list htop
htop: /usr/bin/htop
htop: /usr/share/applications/htop.desktop
htop: /usr/share/doc/htop/AUTHORS
htop: /usr/share/doc/htop/README.gz
htop: /usr/share/doc/htop/changelog.Debian.gz
htop: /usr/share/doc/htop/copyright
htop: /usr/share/icons/hicolor/scalable/apps/htop.svg
htop: /usr/share/man/man1/htop.1.gz
htop: /usr/share/pixmaps/htop.png

使用 dpkg 类命令在已安装的包内查找文件

apt-file 依赖于对完整仓库的索引,并且搜索也是一个略微耗时的过程。如果只需要确认本地已经安装的包,以及已有的 deb 包文件中的文件情况,有更快的方法:

  • dpkg -S <file> 可以查找所有已安装包中的文件。
  • dpkg-deb -c <name_version.deb> 可以查看 .deb 文件中的内容。
  • dpkg-query -L <name> 查看给定的安装了的包提供了哪些文件。

固定包

有时我们希望固定一个包,使得这个包不会被安装或升级,例如:

  • 我们自行打包了某个有 bug 的包的修复版本,同时不希望系统自动升级到官方的版本。这时可以使用 apt-mark hold <name> 来标记这个包为固定的。
  • 我们不希望 Ubuntu 服务器安装 Snap,此时可以使用 apt-mark hold snapd 来固定(阻止)Snap 的安装:

    $ sudo apt-mark hold snapd
    $ sudo apt install chromium
    Reading package lists... Done
    Building dependency tree... Done
    Reading state information... Done
    Note, selecting 'chromium-browser' instead of 'chromium'
    Some packages could not be installed. This may mean that you have
    requested an impossible situation or if you are using the unstable
    distribution that some required packages have not yet been created
    or been moved out of Incoming.
    The following information may help to resolve the situation:
    
    The following packages have unmet dependencies:
    chromium-browser : PreDepends: snapd but it is not going to be installed
    E: Unable to correct problems, you have held broken packages.
    

apt-mark unhold 可以取消固定,而 apt-mark showhold 可以查看所有被固定的包。

Alternatives

Debian 的 Alternatives 机制允许系统安装同一软件的多个版本(例如 Java),或者同一类型软件的多个实现(例如文本编辑器),并且用户可以切换默认使用的版本。/etc/alternatives/ 目录包含了所有在 Alternatives 机制下的软链接。以 editor 为例,在 Debian 下 /usr/bin/editor 是一个指向 /etc/alternatives/editor 的软链接,而 /etc/alternatives/editor 指向的对象由用户配置决定。

使用 update-alternatives 命令可以管理 Alternatives 机制的映射:

$ ls -lh /etc/alternatives/editor
lrwxrwxrwx 1 root root 9 Aug  29 2022  /etc/alternatives/editor -> /bin/nano
$ sudo update-alternatives --config editor
There are 4 choices for the alternative editor (providing /usr/bin/editor).

  Selection    Path                Priority   Status
------------------------------------------------------------
* 0            /bin/nano            40        auto mode
  1            /bin/ed             -100       manual mode
  2            /bin/nano            40        manual mode
  3            /usr/bin/vim.basic   30        manual mode
  4            /usr/bin/vim.tiny    15        manual mode

Press <enter> to keep the current choice[*], or type selection number: 3
update-alternatives: using /usr/bin/vim.basic to provide /usr/bin/editor (editor) in manual mode
$ ls -lh /etc/alternatives/editor
lrwxrwxrwx 1 root root 18 Feb  9 15:54 /etc/alternatives/editor -> /usr/bin/vim.basic

自动更新

一般而言,使用 apt 的系统默认安装了 unattended-upgrades 包,如果系统上没有,安装该包即可。一些 Debian 系统镜像在预配置阶段会关闭自动更新,这可以通过以下命令确认:

debconf-get-selections | grep unattended-upgrades/enable_auto_updates

如果输出的内容类似下面:

unattended-upgrades unattended-upgrades/enable_auto_updates boolean false

则代表自动更新被关闭,反之则启用。可以执行 dpkg-reconfigure unattended-upgrades 修改配置。当自动更新启用时,/etc/apt/apt.conf.d/20auto-upgrades 文件应当存在。

可以使用以下命令:

sudo unattended-upgrades --dry-run --debug

查看并确认系统自动更新时的行为。默认情况下,自动更新只会操作 Debian 官方上游的包,用户自己设置的源不会被自动更新,这一点可以在 /etc/apt/apt.conf.d/50unattended-upgrades 文件中验证:

// Automatically upgrade packages from these (origin:archive) pairs
//
// Note that in Ubuntu security updates may pull in new dependencies
// from non-security sources (e.g. chromium). By allowing the release
// pocket these get automatically pulled in.
Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}";
    "${distro_id}:${distro_codename}-security";
    // Extended Security Maintenance; doesn't necessarily exist for
    // every release and this system may not have it installed, but if
    // available, the policy for updates is such that unattended-upgrades
    // should also install from here by default.
    "${distro_id}ESMApps:${distro_codename}-apps-security";
    "${distro_id}ESM:${distro_codename}-infra-security";
//  "${distro_id}:${distro_codename}-updates";
//  "${distro_id}:${distro_codename}-proposed";
//  "${distro_id}:${distro_codename}-backports";
};

此外,systemd 服务 unattended-upgrades.service 会确保系统在关机或重启前正确进行软件包升级的收尾工作。因此也需要确认该服务已启动并会开机自启。

APT 前端

APT 面向用户使用的前端除了 apt 以外,还有 apt-getaptitudesynaptic 等。其中 apt-get 是早期的 Debian 的包管理工具,基础功能与 apt 类似(如 apt-get updateapt-get install 等),但是用户体验不如 apt 友好,由于其交互界面不再变化,因此仅适用于需要使用脚本交互的场景;synaptic 是图形界面的包管理工具(中文名为「新立得软件包管理器」)。

Synaptic

Ubuntu 24.04 下的新立得软件包管理器截图

aptitude 提供了 TUI 界面的包管理功能,不过对于运维的场景下,其更加重要的是相比于 apt 更灵活的依赖解析功能。在系统出现损坏包的情况下,apt 可能无法提供有效的解决方案,而 aptitude 会计算出多种解法,并且提供给用户选择。

完整性校验

dpkg 可以对已经安装的包进行完整性校验。dpkg --verify <name> 可以校验已经安装的包的完整性,可以省略 <name> 选项,以对于所有包进行检查。如果怀疑软件包文件因意外被破坏(例如在升级时断电,或误删除等),可以使用该命令确认哪些软件包需要重新安装。

检查某系统强制重启后无法开机的问题

一个现实发生的例子是,某系统强制重启后无法正常开机,提示:

[    4.634427] systemd[1]: Assertion 'close_nointr(fd) != -EBADF' failed at src/basic/fd-util.c:77, function safe_close(). Aborting.
[    4.635043] systemd[1]: Caught <ABRT> from our own process.
[    4.635624] systemd[1]: Caught <ABRT>, core dump failed (child 225, code=killed, status=6/ABRT).
[    4.635750] systemd[1]: Freezing execution.

使用 ISO 引导后 chroot 到系统,执行 dpkg --verify,发现:

# dpkg --verify
(省略)
??5??????   /usr/lib/x86_64-linux-gnu/systemd/libsystemd-core-252.so

重新安装 libsystemd-shared 包后,问题解决。

dpkg --verify 默认输出为 rpm -V 风格,类似如下:

??5??????   /some/file
??5?????? c /some/config_file
missing     /some/missing_file

dpkg 目前只会检查文件的 MD5(即上面的 5),因此其他列均标记为 ?(未检查)。c 代表是配置文件,missing 代表文件不存在。

dpkg --verify 不是为安全性用途设计的

如果怀疑攻击者已经有对应机器的 root 权限,那么 dpkg --verify 的结果是不可信的,因为攻击者可以修改 dpkg 本身,或者修改本地的包数据库。

软件优先级

有时候,我们会设置多个不同的源,而这些源会提供相同名称的软件包,例如:

  • 正常安装的系统中,security 源提供了一些主源已有的软件包的,包含安全修复的更新版本。
  • 在 stable 版本的 Debian 中添加 backports 源,以获取一些来自 testing 的,在 stable 下重新编译的新版本的软件包。
  • Ubuntu 源中的 firefox 为 Snap 包,而来自 Mozilla 的 APT 仓库的 firefox 为原生的 deb 包(Mozilla 的帮助信息)。

APT 选择包的逻辑并非单纯的「版本越新越好」(比如,用户添加 backports 源不代表用户希望所有 backports 有的软件都安装最新版本),而是根据优先级来选择。默认的优先级为 500,如果优先级一致,才会根据版本号来选择。

Backports 源的优先级为 100,因为其 Release 文件中 NotAutomaticButAutomaticUpgrades 字段都为 yes因此 APT 会授予 backports 100 的优先级。如果只有 NotAutomaticyes,则优先级为 1。

我们通过 apt-cache policy <name> 查看包的安装状态与优先级信息,以某配置了 backports、deb-multimedia,并且有一段时间未升级的系统为例:

$ apt-cache policy yt-dlp
yt-dlp:
  Installed: 1:2024.10.07-dmo1
  Candidate: 1:2025.01.26-dmo1
  Version table:
     1:2025.01.26-dmo1 500
        500 http://mirrors.ustc.edu.cn/deb-multimedia bookworm/main amd64 Packages
 *** 1:2024.10.07-dmo1 100
        100 /var/lib/dpkg/status
     2025.01.26-1~bpo12+1 100
        100 http://mirrors.ustc.edu.cn/debian bookworm-backports/main amd64 Packages
     2023.03.04-1 500
        500 http://mirrors.ustc.edu.cn/debian bookworm/main amd64 Packages

可以看到,APT 已知四个不同的 yt-dlp 版本,分别是 deb-multimedia 的 1:2025.01.26-dmo1、本地安装的 1:2024.10.07-dmo1、backports 的 2025.01.26-1~bpo12+1 以及官方源的 2023.03.04-1。特别地,本地的版本的优先级为 100。因此当执行更新命令时,APT 会首先选择优先级最高的(deb-multimedia 或者官方源),然后选择版本最高的(1:2025.01.26-dmo1)。

如果本地的版本比远程的版本更高……

此时 APT 不会降级软件包,除非远程对应的优先级超过 1000。如果需要手动降级,可以使用 apt install <name>=<version>

如果需要指定从某个特定的源安装软件包,可以使用 -t 选项,例如:

apt install -t bookworm-backports yt-dlp

这样就会安装 backports 源中的 yt-dlp。其实际上是把 bookworm-backports 源的优先级临时拉高到了 990:

$ apt-cache policy -t bookworm-backports yt-dlp
yt-dlp:
  Installed: 1:2024.10.07-dmo1
  Candidate: 1:2025.01.26-dmo1
  Version table:
     1:2025.01.26-dmo1 500
        500 http://mirrors.ustc.edu.cn/deb-multimedia bookworm/main amd64 Packages
 *** 1:2024.10.07-dmo1 100
        100 /var/lib/dpkg/status
     2025.01.26-1~bpo12+1 990
        990 http://mirrors.ustc.edu.cn/debian bookworm-backports/main amd64 Packages
     2023.03.04-1 500
        500 http://mirrors.ustc.edu.cn/debian bookworm/main amd64 Packages

用户也可以手动配置软件的优先级,相关配置位于 /etc/apt/preferences/etc/apt/preferences.d/ 中。优先级配置条目的一般格式如下:

Package: <name>
Pin: <clause>
Pin-Priority: <priority>

例如:

Package: *
Pin: origin packages.mozilla.org
Pin-Priority: 1000

那么 packages.mozilla.org 拥有的包会优先于官方仓库被选择。优先级系统也可以用来固定包,例如将 mtr-tiny 包固定在 0.87 版本:

Package: mtr-tiny
Pin: version 0.87*
Pin-Priority: 1000

详细文档请参考 apt_preferences(5)

使用优先级机制阻止软件包安装

Pin-Priority 为负数时,APT 会拒绝安装这个包。请阅读文档,尝试创建一个配置文件,阻止安装 snapd(可在 Ubuntu 容器中实验)。

DEB 软件包

本节介绍 DEB 软件包包含的内容,以及如何在已有的基础上进行简单的修改与打包操作。

如果对为 Debian 打包感兴趣,强烈建议阅读 Debian 维护者指南。其包含了全面、完整的打包流程的介绍,以及最佳实践等。以下提供的步骤仅适用于简单、非官方的需求。

软件包结构

Deb 包是一个 ar 格式的包,包含三个文件(可以使用 ar t 查看,ar x 解压):

  • debian-binary:包含版本号的文本文件,目前版本为 2.0
  • control.tar.xz(或 control.tar.zst 等):包含软件包的元数据,例如软件包的依赖、描述、安装脚本等。
  • data.tar.xz(或 data.tar.zst 等):包含软件包的实际文件。

ar 与 tar

ar 格式(1971)与 tar(1979)类似,都是归档格式。由于 ar 不支持目录,因此目前 ar 仅用于生成静态链接库(.a 文件)与 deb 包。

control

control.tar.xz 中的 control 文件是包的元数据,包含版本、依赖、描述、维护者等等信息,类似如下:

Package: sudo
Version: 1.9.9-1ubuntu2.3
Architecture: amd64
Maintainer: Ubuntu Developers <[email protected]>
Installed-Size: 2504
Depends: libaudit1 (>= 1:2.2.1), libc6 (>= 2.34), libpam0g (>= 0.99.7.1), libselinux1 (>= 3.1~), zlib1g (>= 1:1.2.0.2), libpam-modules, lsb-base
Conflicts: sudo-ldap
Replaces: sudo-ldap
Section: admin
Priority: optional
Homepage: https://www.sudo.ws/
Description: Provide limited super user privileges to specific users
 Sudo is a program designed to allow a sysadmin to give limited root
 privileges to users and log root activity.  The basic philosophy is to give
 as few privileges as possible but still allow people to get their work done.
 .
 This version is built with minimal shared library dependencies, use the
 sudo-ldap package instead if you need LDAP support for sudoers.
Original-Maintainer: Sudo Maintainers <[email protected]>

有关具体各个字段的含义,可参考 Debian Policy Manual第五章 Control files and their fields第七章 Declaring relationships between packages

其他文件

此外,control.tar.xz 可以包含一些 hook 脚本,在安装与删除前后进行操作,包括 preinst, prerm, postinst, postrm。还可以包含以下文件:

  • md5sums,用于校验包文件的完整性。
  • conffiles,标志包安装的哪些文件是配置文件。
  • shlibs,如果软件包包含了动态库(.so),那么这个文件就需要包含库的版本信息,以帮助其他软件包解决相关的依赖问题。
  • triggers,定义了软件包感兴趣(interest)的触发器,以及软件包状态变化时会触发(activate)的触发器。

获取软件包源码

Debian 目前大多数的包的源代码都可以在 Debian Salsa GitLab 上找到,可以在 Debian Package Tracker 上找到相关信息。

除了直接使用 git clone 之外,还可以使用 apt source <package> 来下载源码。需要注意的是,该功能需要安装 dpkg-dev,且需要在 /etc/apt/sources.list/etc/apt/sources.list.d/debian.sources 中添加 deb-src,类似于这样:

deb-src http://deb.debian.org/debian/ bookworm main

或者这样(DEB822):

Types: deb deb-src
URIs: http://deb.debian.org/debian/
Suites: bookworm
Components: main
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg

DEB822

DEB822 是 Debian 新的仓库配置格式,用于取代旧的 sources.list 格式(又被称为 One-Line-Style 格式)。详情可阅读 https://repolib.readthedocs.io/en/latest/deb822-format.html

apt source 会下载必要的文件、解压并应用 Debian 的补丁:

$ apt source sudo
Reading package lists... Done
NOTICE: 'sudo' packaging is maintained in the 'Git' version control system at:
https://salsa.debian.org/sudo-team/sudo.git
Please use:
git clone https://salsa.debian.org/sudo-team/sudo.git
to retrieve the latest (possibly unreleased) updates to the package.
Skipping already downloaded file 'sudo_1.9.13p3-1+deb12u1.dsc'
Skipping already downloaded file 'sudo_1.9.13p3.orig.tar.gz'
Skipping already downloaded file 'sudo_1.9.13p3.orig.tar.gz.asc'
Skipping already downloaded file 'sudo_1.9.13p3-1+deb12u1.debian.tar.xz'
Need to get 0 B of source archives.
dpkg-source: info: extracting sudo in sudo-1.9.13p3
dpkg-source: info: unpacking sudo_1.9.13p3.orig.tar.gz
dpkg-source: info: unpacking sudo_1.9.13p3-1+deb12u1.debian.tar.xz
dpkg-source: info: using patch list from debian/patches/series
dpkg-source: info: applying debian-bug-1039557
dpkg-source: info: applying paths-in-samples.diff
dpkg-source: info: applying Whitelist-DPKG_COLORS-environment-variable.diff
dpkg-source: info: applying sudo-ldap-docs

修改软件包源码

得到的源码中除了软件包本身的代码以外,还有 debian 目录。这个目录包含了软件包的构建相关信息,包括 controlchangelogpatches/ 等等。

使用 quilt 管理补丁

补丁简介

补丁是后缀为 patch 的文件,由 diff 生成。补丁有多种不同的格式:unified diff (diff -u)、context diff (diff -c) 和 normal diff(默认)。其中最常见的是 unified diff 格式,类似如下(命令为 diff -u test.txt test_new.txt):

--- test.txt    2025-02-07 16:00:16.591987241 +0800
+++ test_new.txt    2025-02-07 16:00:28.462299382 +0800
@@ -1 +1 @@
-Hello, world!
+Goodbye, world!

Git 也可以使用 git diff 来生成补丁,一个例子:

diff --git a/pytorch/sync.py b/pytorch/sync.py
index 577a88f..0e6b4d8 100644
--- a/pytorch/sync.py
+++ b/pytorch/sync.py
@@ -4,7 +4,7 @@ import httpx
from pathlib import Path
import os
import re
-from urllib.parse import urlparse, urljoin
+from urllib.parse import urlparse, urljoin, unquote
import asyncio
import time

@@ -83,7 +83,7 @@ async def get_with_progress(client: httpx.AsyncClient, url: str):


async def recursive_download(client: httpx.AsyncClient, url: str):
-    path = urlparse(url).path
+    path = unquote(urlparse(url).path)
    while path.startswith("/"):
        path = path[1:]
    if url.endswith("/"):

生成的补丁可以使用 patch 命令应用在未打补丁的文件上:

$ patch < test.patch
$ # git diff 生成的 patch 开头是 a/ b/,因此需要使用 -p1 剥离这一层前缀
$ # 当然也可以直接 git apply
$ patch -p1 < git_diff.patch

发行版打包的软件包很多时候需要在原始代码上做一些修改(例如修复 bug,添加功能等),而补丁管理工具可以省掉一些麻烦。尽管对简单修改代码的需求来说,也可以绕过补丁系统,直接编辑,但是这会在日后升级软件版本时带来麻烦,因此这里会介绍与补丁管理有关的主题。quilt 是一个栈式(stack)的补丁管理工具。首先配置 ~/.quiltrc 如下:

~/.quiltrc
QUILT_PATCHES=debian/patches

之后就可以使用 quilt 查看当前目录的补丁情况:

$ quilt series
debian-bug-1039557
paths-in-samples.diff
Whitelist-DPKG_COLORS-environment-variable.diff
sudo-ldap-docs
$ quilt applied
debian-bug-1039557
paths-in-samples.diff
Whitelist-DPKG_COLORS-environment-variable.diff
sudo-ldap-docs
$ quilt unapplied
File series fully applied, ends at patch sudo-ldap-docs
$

可以看到,所有的补丁都已经被应用。可以使用 quilt popquilt push 取消/应用最底层的补丁。我们需要将我们的修改添加为新的补丁,因此:

$ quilt new custom-patch.diff
$ quilt add src/sudo.c  # 将修改前的文件 add 到 quilt 中
File src/sudo.c added to patch custom-patch.diff
$ # 如果误操作将修改后的文件 add 了,quilt remove 即可。
$ vim src/sudo.c  # 随便做一些修改,比如在 main() 最开始用 printf() 输出一行文本
$ quilt refresh  # 刷新补丁
Refreshed patch custom-patch.diff

可以验证:

debian/patches/custom-patch.diff
Index: sudo-1.9.13p3/src/sudo.c
===================================================================
--- sudo-1.9.13p3.orig/src/sudo.c   2025-02-07 17:37:38.372195671 +0800
+++ sudo-1.9.13p3/src/sudo.c    2025-02-07 17:37:50.968524513 +0800
@@ -149,6 +149,7 @@
     const char * const allowed_prognames[] = { "sudo", "sudoedit", NULL };
     sigset_t mask;
     debug_decl_vars(main, SUDO_DEBUG_MAIN);
+    printf("Hello, world!\n");

     /* Only allow "sudo" or "sudoedit" as the program name. */
     initprogname2(argc > 0 ? argv[0] : "sudo", allowed_prognames);

更新修改记录

debian/changelog 文件包含了包完整的修改记录,格式类似如下:

sudo (1.9.13p3-1+deb12u1) bookworm; urgency=medium

  * add upstream patch to fix event log format.
    Thanks to Kimmo Suominen (Closes: #1039557)

 -- Marc Haber <[email protected]>  Tue, 27 Jun 2023 13:45:00 +0200

建议使用 dch (debchange) 命令来更新这个文件,以下命令会创建一个新的版本号,并且打开编辑器以编辑修改记录:

EMAIL=[email protected] dch -i

编辑结果类似如下:

sudo (1.9.13p3-1+deb12u1.1) UNRELEASED; urgency=medium

  * Non-maintainer upload.
  * Add "Hello, world!\n" in main().
  * Test with very simple packaging.

 -- yourname <[email protected]>  Fri, 07 Feb 2025 17:48:19 +0800

构建软件包

在构建之前,我们需要确保构建需要的依赖已经安装:

sudo apt build-dep sudo

从源代码目录构建软件包的命令为 dpkg-buildpackage,执行以下命令即可:

dpkg-buildpackage -b -uc -us

-b 代表构建二进制包,而由于我们很明显没法签名,因此需要使用 -uc-us 选项来跳过对变更(changes)与源代码包(source package)的签名。构建完成后,我们可以在上一级目录找到 1.9.13p3-1+deb12u1.1 这个版本的 sudo 的 deb 包,以及其调试符号包。

可以试试安装一下:

$ sudo apt install ./sudo_1.9.13p3-1+deb12u1.1_amd64.deb
$ sudo uname
Hello, world!
Linux

试完记得装回正常的版本哦!

$ apt-cache policy sudo
sudo:
  Installed: 1.9.13p3-1+deb12u1.1
  Candidate: 1.9.13p3-1+deb12u1.1
  Version table:
 *** 1.9.13p3-1+deb12u1.1 100
        100 /var/lib/dpkg/status
     1.9.13p3-1+deb12u1 990
        990 http://mirrors.ustc.edu.cn/debian bookworm/main amd64 Packages
$ sudo apt install sudo=1.9.13p3-1+deb12u1
Hello, world!
(以下省略)
$ sudo uname
Linux

简易软件包打包

以上介绍了从官方源代码包中打包的过程。不过有时候,我们的需求是从零开始打包一个软件包。以下提供一些简单的例子。

配置类软件包

USTC-vlab/deb 是 Vlab 项目为学生虚拟机(容器)提供的一部分补充包仓库,所有的包均为简单的配置文件与脚本。

这一类不需要编译操作的包目录树类似如下,DEBIAN 目录包含了打包后 control.tar.xz 的内容,剩下的内容则组成了 data.tar.xz

.
├── DEBIAN
│   ├── control
│   ├── postinst
│   └── preinst
└── etc
    ├── lightdm
    │   └── lightdm.conf
    └── vlab
        ├── greeter-setup.sh
        └── vncserver-lightdm

dpkg-deb --build -Z <dir> <output_dir> 可以将 <dir> 目录下的内容打包为 deb 包,存储到 <output_dir> 目录中(-Z 压缩默认为 xz)。

特别地,如果这一类软件包需要覆盖其他软件包的文件,可以使用 dpkg-divert 命令。例如,vlab-vnc 包需要覆盖 lightdm 的配置文件 /etc/lightdm/lightdm.conf,因此其在 preinst 中使用了以下命令:

dpkg-divert --package vlab-vnc --divert /etc/lightdm/lightdm.conf.dpkg-divert --add /etc/lightdm/lightdm.conf

再例如,该仓库中修改后的 firefox 需要移除/重命名 /usr/share/mate/applications/firefox.desktop,因此在 preinst 中使用了以下命令:

MATE_DESKTOP=/usr/share/mate/applications/firefox.desktop
dpkg-divert --rename --divert "$MATE_DESKTOP".dpkg-divert "$MATE_DESKTOP"

同时,hook 脚本在执行时的 argv[1] 为用户执行的操作(例如 install, upgrade 等),因此脚本中可以根据操作执行不同的命令,类似如下:

case "$1" in
    install)
        # do something
        ;;
    upgrade|abort-upgrade)
        # do something
        ;;
    *)
        echo "postinst called with unknown argument \`$1'" >&2
        exit 0
        ;;
esac

使用 checkinstall 快速打包

checkinstall 工具仅适用于临时打包

checkinstall 的工作原理是,在 make install 使用 installwatch 环节拦截文件相关系统调用(类似 strace),检查文件的变化,并将其打包为 deb 包。因此其只适用于使用 Makefile 构建系统(包括 cmakeautotools 等)的软件包。并且 checkinstall 的工作原理决定了其无法处理复杂的软件包,例如如果某软件包在安装时修改了 /etc/passwd,那么 checkinstall 无法正确打包这个软件包——在卸载时可能会破坏系统1

Debian 打包是一项复杂的工作。针对不同的编程语言与构建系统,Debian 提供了一些工具(例如 dh-make-golang 可以打包 Go 语言项目)辅助打包,建议在操作前先行搜索。debmake 工具也可以帮助生成一些 Debian 打包的模板文件。

以下以编译 rsync 为例子介绍。首先正常配置并编译:

git clone https://github.com/RsyncProject/rsync.git
./configure
make -j$(nproc)

然后执行 checkinstall:

sudo checkinstall

checkinstall 会询问一些问题,例如软件包的名称、版本、描述等等。版本默认为空,但是必须填写,否则后续步骤会报错。配置并安装完成后:

**********************************************************************

 Done. The new package has been installed and saved to

 /path/to/rsync/rsync_3.4.1-7_amd64.deb

 You can remove it from your system anytime using: 

      dpkg -r rsync

**********************************************************************

可以看到 rsync 安装在了 /usr/local/bin/ 下:

$ which rsync
/usr/local/bin/rsync

在卸载前,建议手工检查生成的 deb 包,避免其他文件被删除。

软件源目录结构

Debian 的软件源存储了多个软件包,以 APT 可以理解的格式提供给用户。

简单结构与生成示例

最简单的软件源目录结构如下:

.
├── firefox-vlab_1vlab3_all.deb
├── Packages
├── Release
├── vlab-cli_1.0-1_all.deb
├── vlab-earlyoom_1.1-5_all.deb
├── vlab-earlyoom-static_1.1-5_amd64.deb
└── vlab-vnc_1.0-3_all.deb

除了 deb 文件以外,两个关键的文件是 PackagesReleasePackages 文件类似于将所有包的 control 文件连接起来,像这样:

Package: firefox-vlab
Version: 2:1vlab3
Architecture: all
Origin: Ubuntu
Maintainer: USTC Vlab <[email protected]>
Pre-Depends: debconf
Depends: debconf (>= 0.5) | debconf-2.0
Breaks: firefox, firefox-dbg, firefox-dev, firefox-geckodriver, firefox-mozsymbols
Replaces: firefox, firefox-dbg, firefox-dev, firefox-geckodriver, firefox-mozsymbols
Provides: gnome-www-browser, iceweasel, www-browser, x-www-browser
Filename: ./firefox-vlab_1vlab3_all.deb
Size: 7376
MD5sum: 943c78a714ec598de5ccb1a1d704fa4d
SHA1: 7818966860329bc18ff224b8ff549006a3ef8146
SHA256: a9e1d012abe7dc4e341674770d6297b7acea0e3dfb541f189e02f1c8ffe9715a
Section: web
Priority: optional
Description: System files for Firefox on Vlab Software

Package: vlab-cli
Version: 1.0-1
Architecture: all
Maintainer: USTC Vlab <[email protected]>
Depends: bash (>= 4.2)
Recommends: vlab-vnc
Filename: ./vlab-cli_1.0-1_all.deb
Size: 1440
MD5sum: 8411c78ab73dc18c914c7409e3c22357
SHA1: c3a5da87a8e5ebc3729a3afda5779413c6bc8b3a
SHA256: 58a804b96e7b8bdd450436ca07c75d7a2e05cd121a680dd17474514736d7c99e
Section: base
Priority: optional
Description: CLI for legacy Vlab VM settings

不过其包含一些新的字段,例如 Filename(仓库中的路径)、SizeMD5sumSHA1SHA256。该文件使用 dpkg-scanpackages 生成:

dpkg-scanpackages . > Packages

Release 文件则包含了软件源的元数据,例如:

Date: Fri, 07 Feb 2025 16:21:05 +0000
Label: Vlab
Origin: vlab.ustc.edu.cn
MD5Sum:
 ddf53358082c24a5c89516c46eb00b0d             2770 Packages
 1e0a4d927bce3b4c809157264c731de9               75 Release
SHA1:
 0be434c15790a9875f4217709996d1140abc330b             2770 Packages
 43414ec15d46888bf6752f8d4e65afb0a8c67b26               75 Release
SHA256:
 e65405d61f505b6c62c61adc4330820ce9a8d850b839ce9de3c8114da9313b1c             2770 Packages
 f78327f58a7cf57703d206399bd693766c66c98a3296e3a84aef33c54f6c8b1f               75 Release
SHA512:
 88ba61c27d624c01a61eff8722669a8e51d7055db4f4bdd49aa8ff965f44ab6388b6cd4aa502d22ef1a793e8fbf8510ed92946c6a9b53b313bddc0e2069421b3             2770 Packages
 30ef3ac9a9cabc3d40b63b2eaec615a77ef7eb33bb5a3c7c76cc95eab86cf74355a87811d985519b2d144952433b8c20e039aaca7eee26b6d459a86d78eaf9ee               75 Release

其中包含了生成时间、相关文件的哈希(Release 的哈希对应的是 MD5Sum 之前的部分),以及软件源相关的配置。该文件使用 apt-ftparchive 生成:

$ cd ..
$ cat main.conf
APT::FTPArchive::Release::Origin "vlab.ustc.edu.cn";
APT::FTPArchive::Release::Label "Vlab";
$ apt-ftparchive -c main.conf release builddir > builddir/Release

实际使用时的配置为:

deb [trusted=yes] https://deb.example.com/repo/ ./

或 DEB822 格式:

Types: deb
URIs: https://deb.example.com/repo/
Suites: ./
Trusted: yes

Signed-By 与 GPG 签名

从 apt 2.9.24 开始,没有 Signed-By 字段的源会视作废弃状态。以上不包含 GPG 签名的仓库在未来的 Debian 版本上可能会被拒绝使用。

官方源结构

如果观察过 Debian 或 Ubuntu 官方源的结构,可以发现它们会更加复杂,一般来讲,仓库的元数据会存储在 dists 目录下,而软件包则存储在 pool 目录下。dists 目录下第一层为发行版(Suites)目录。以 Debian 12 为例,最常使用的包含 bookworm, bookworm-updatesbookworm-backports 目录。

再向里一层则包含上面提到的 Release 文件,除了 Release 以外:

  • Release.gpgRelease 文件的 GPG 签名。
    • 正规的发行版都会自带维护者的 GPG 公钥,在安装软件包时会自动验证软件包的签名,避免恶意篡改。
  • InRelease:附加了 GPG 签名的 Release 文件。
  • ChangeLog:变更记录。
  • contrib/, main/, non-free/, non-free-firmware/:不同的组件(Components)。

@taoky: 安全更新源与镜像站

曾经有人问过这个问题:镜像站提供安全更新源的话,怎么保证镜像站本身不会通过拖延更新等方式阻止用户获取安全更新?

首先,Debian 不鼓励修改默认的安全更新源,甚至其安装器在默认模式下,即使设置了镜像站,安全更新仍然会从官方获取(这也是 Debian 安装器在某些网络环境下很慢的一大原因),需要使用专家模式才能修改。

然后,安全更新源的 Release 文件有一个重要的字段:Valid-Until,例如:

Date: Sat, 08 Feb 2025 21:33:46 UTC
Valid-Until: Sat, 15 Feb 2025 21:33:46 UTC

Valid-Until 距离 Release 生成为期一周,因此即使拖延,也不允许超过一周,否则 APT 会拒绝使用这个软件源。不过仍然请注意,在生产环境下,谨慎使用任何镜像站提供安全更新源

组件目录中则包含了:

  • Contents-<arch>.gz:用于帮助 apt-file 等工具快速查找包含某文件的软件包。
  • binary-<arch>/:二进制包。
  • source/:源代码包。
  • by-hash/:文件名为哈希值的元数据文件。APT 在读取 Release 之后,在 Acquire-By-Hash: yes 时,会首先根据哈希读取该目录下的需要的文件。这可以帮助使用 rsync 同步内容的镜像站点提升可用性,避免在同步过程中部分文件失效/未同步等导致的无法使用的问题。

架构目录(二进制/源代码包):

  • Packages.{gz,xz}:压缩后的 Packages 文件。
    • 如果软件源不在本地文件系统上,那么 Packages 必须压缩,否则 APT 可能不会读取。
  • Release:该架构的元数据。
  • by-hash/:同上。

  1. https://askubuntu.com/a/1138405,引用自 The Debian System: Concepts and Techniques 一书。