软件包实操手册 · 仓库 / 安装 / 查询 全流程

从「仓库到底是什么」到「配一个本地仓库并装包」,再到查询、卸载、回滚、组与模块。每一步都写了「成功长什么样」,勾选框会记住进度。本手册随进度生长:第 1 节是正在做的现状查看,第 2 节之后是按标准路径先写好的流程,走过一遍后会就地修正并补上实测结果。

进度 0/0

0 · 先建立正确的模型 dnf 在做什么,仓库的本质是什么

大多数人对包管理的理解停在「下载 + 安装」。那样记命令,配仓库时必然靠试。先把下面这条链记住,后面每一步都能自己解释。

① 读 .repo→ ② 取 repodata→ ③ 解依赖→ ④ 下载 rpm→ ⑤ 校验签名 + 事务
步骤它实际在做什么失败时你会看到对应的验证手段
①读 /etc/yum.repos.d/*.repo + /etc/dnf/dnf.conf仓库根本没出现dnf repolist --all
②按 baseurl 取 repodata/repomd.xmlCannot download repomd.xmldnf makecache
③在元数据里算出「还要装哪些包」nothing provides ...dnf provides */命令
④下载 .rpm 到 /var/cache/dnf/Curl error / 超时看 /var/log/dnf.log
⑤GPG 校验、执行 rpm 事务GPG key ... NOT OKrpm -qa | grep 包名
本域最重要的一句话:仓库的核心不是 rpm 文件,是 repodata。
把 100 个 rpm 拷进 /opt/myrepo/,再写一个 baseurl=file:///opt/myrepo 的 .repo,dnf 一个包都看不见——因为那个目录里没有 repodata/。这就是 createrepo_c 存在的唯一理由:把 rpm 扫一遍,生成索引。
两个变量,看懂 .repo 的前提
$releasever:系统大版本号(Rocky 9 → 9),来自 system-release(releasever) 这个 provides。
$basearch:CPU 架构(x86_64)。
写成 .../rockylinux/$releasever/BaseOS/$basearch/os/ 就是为了「换机器不用改文件」。

1 · 看清现状 先只读,一个字节都不要改

ls + repolist
ls -l /etc/yum.repos.d/
dnf repolist
dnf repolist --all
成功长什么样:repolist 列出启用的仓库(id / name / status 三列);--all 多出 disabled 的行。
文件数 ≠ 仓库数:一个 .repo 文件里可以有多个 [section],每个方括号就是一个仓库。dnf repoinfo <id> 会告诉你它来自哪个文件第几行。
repoinfo / cat
dnf repoinfo baseos
cat /etc/yum.repos.d/*.repo
成功长什么样:看到 Repo-baseurl(变量已展开成真实 URL)、Repo-filename、Repo-expire。
dnf repolist 是会骗人的命令(你的老账本第 1 条):它读的是 /var/cache/dnf/ 里的本地缓存,不联网。网络断了、baseurl 已经 404,它照样给你列出两行漂亮的仓库。它只证明「配置读通了」,不证明「仓库可用」。
clean + makecache
dnf clean all
dnf makecache
成功长什么样:结尾是 Metadata cache created.,每个启用的仓库一行,没有 error / Curl error。
为什么这条才算数:clean all 把本地缓存清空,makecache 被迫真的去 baseurl 拉一次 repodata。这是唯一能证明「仓库活着」的最小动作。
Repo-expire 里的 21600 是哪来的:.repo 里写的是 metadata_expire=6h,dnf 内部换算成秒:6 × 3600 = 21600。配置文件里给人看,输出里给机器看——所以 grep 21600 在文件里一个都找不到。
单位:d 天 / h 小时 / m 分 / s 秒,不写单位默认按秒。想让元数据 1 小时过期就写 metadata_expire=1h。
注意它不是「还剩多久过期」,而是「有效期长度」;后面括号里的 last: 才是上次刷新时间,两者相加才是下次过期时刻。
不同仓库可以不同:rocky 系列是 6h,epel 是 14d(=1209600s),不写则用 dnf 默认值。
--enablerepo / --disablerepo
dnf --disablerepo='*' --enablerepo=appstream list available | head
dnf --disablerepo=appstream repolist
成功长什么样:第一条只从 appstream 列包;第二条里 appstream 消失。
为什么要会这个:考试里经常要「只从这个仓库装」,改配置文件既慢又有风险。命令行参数的优先级高于 .repo 里的 enabled=,且只在这一次生效。

2 · 配一个本地仓库(file://)本域最常考的一题,也是唯一需要「造」的动作

目标:把一个装满 rpm 的目录变成一个 dnf 能识别的仓库,并只从这个仓库装一个包。顺序不能颠倒:先有 rpm → 再生成 repodata → 最后才写 .repo。

两条来源任选
# 路线 A:手上有 ISO / 安装光盘
mount /dev/sr0 /mnt
ls /mnt/BaseOS/Packages | head

# 路线 B:没有 ISO,从现有远程仓库下载几个包
mkdir -p /opt/localrepo
dnf download --destdir=/opt/localrepo tree htop lsof
成功长什么样:/opt/localrepo/ 里出现若干 .rpm(路线 B);或 /mnt/BaseOS/Packages/ 下有上百个 rpm(路线 A)。
为什么要拷贝出来:createrepo_c 要往目录里写 repodata/,光盘是只读的。所以要么把整个 Packages 拷到可写目录,要么把 ISO 挂载后 cp -r 出来。
createrepo_c
dnf install -y createrepo_c
createrepo_c /opt/localrepo
ls /opt/localrepo/repodata/
成功长什么样:createrepo_c 打印 Working environment ... Saving Primary metadata ... 之类;repodata/ 里有 repomd.xml、primary.xml.gz、filelists.xml.gz。
没有 repomd.xml 就等于没做:dnf 只认这个文件。看到 repodata/ 目录存在但里面是空的,说明 createrepo_c 中途失败了,重跑一次看报错。
什么时候要重跑:往目录里加了新 rpm,必须再执行一次 createrepo_c --update /opt/localrepo,否则新包在索引里不存在。
五个字段
vi /etc/yum.repos.d/local.repo
[local]
name=Local Repo
baseurl=file:///opt/localrepo
enabled=1
gpgcheck=0

cat /etc/yum.repos.d/local.repo
成功长什么样:五个字段齐全;baseurl 是 file:// + 三个斜杠(前两个是协议,第三个是根目录)。
file:// 后面必须三个斜杠:file:///opt/localrepo 对,file://opt/localrepo 会被当成主机名 opt。
gpgcheck=0 的取舍:自建仓库通常没有签名,关掉是最快的路;但生产上应导入 GPG key 并保持 gpgcheck=1。考试按题干要求写,没要求就 0。
字段名拼错不会报错:写成 enable=1(少个 d),dnf 照样跑,因为 dnf 不校验未知键,而 enabled 的默认值就是 1,恰好蒙对了。
危险的是反方向:想关掉却写 enable=0——这个键被忽略、默认仍是 1,仓库照样开着,屏幕上没有任何提示。
所以验证只能看 dnf repoinfo local 里的 Repo-status: enabled / disabled,不能靠「命令没报错」。
唯一算数的验收
dnf clean all
dnf --disablerepo='*' --enablerepo=local repolist
dnf --disablerepo='*' --enablerepo=local list available
成功长什么样:repolist 里只剩 local 一个仓库,status 是包数量而不是 0;list available 能列出你放进去的包名。
只看 repolist 会骗你:仓库能列出来,但 status 是 0,就说明 repodata 是空的或者包没进去。必须看到包名才算过。
--disablerepo='*'
dnf --disablerepo='*' --enablerepo=local install -y tree
rpm -qa | grep tree
成功长什么样:输出里 Installing: tree-...,且 Repository: local;rpm -qa 能查到。

为什么要 --disablerepo='*':不加的话,dnf 会优先从网络仓库拿——那样你根本证明不了本地仓库起作用了。把其它仓库都关掉,剩下的结果才归因明确。

如果显示 already installed:说明这个包本来就在系统里(tree 很常见),dnf 不会重装,直接 Nothing to do。想练「真的从某个仓库装」,要么换一个没装的包,要么先 dnf remove 掉。
另外 list available 只列未安装的包,所以已装的包在那里查不到,要看全部得用 dnf list --all。
只留 local 时装带依赖的包会失败:dnf --disablerepo='*' --enablerepo=local install htop 报 nothing provides libhwloc.so.15()(64bit) needed by htop。
这不是本地仓库坏了,而是依赖求解只在「当前启用的仓库集合」里做——hwloc-libs 在 appstream,被你一起关掉了,dnf 就找不到。
所以这条命令的正确用途是证明「某个包确实来自本地仓库」,不适合用来装带依赖的包。要查依赖归属,得把仓库都开回来再问:dnf provides '*/libhwloc.so.15'。

3 · 加一个远程仓库 EPEL 是最常见的例子

RHEL 9 语法
# ① 启用一个已定义的仓库(这一步会真的改 rocky.repo)
dnf config-manager --set-enabled crb
grep -A6 '\[crb\]' /etc/yum.repos.d/rocky.repo

# ② 关回去
dnf config-manager --set-disabled crb

# ③ 从 URL 安装一个「仓库包」(epel 就是这么来的)
dnf config-manager --add-repo https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm
成功长什么样:① 之后 dnf repolist 多出 crb,且 grep 能看到 crb 段的 enabled=1 已被改写(这就是它与 --enablerepo 的根本区别);② 之后 crb 又消失;③ 之后 /etc/yum.repos.d/epel.repo 存在。
RHEL 9 的语法变了:老的 dnf config-manager --enablerepo=x 已废弃,现在是 --set-enabled / --set-disabled。照着老教程敲会报 unknown option。
三个长得像的参数,命运完全不同:--enablerepo = 本次临时生效;--set-enabled = 改配置文件、永久;--add-repo = 直接写出一个新的 .repo 文件。

4 · 装 / 卸 / 回滚 把「装错了怎么办」也一起练

install / remove / reinstall
dnf install -y htop
rpm -qa | grep htop
dnf remove -y htop
rpm -qa | grep htop          # 应该没有输出
dnf reinstall -y tree
成功长什么样:install 结尾 Complete!;remove 后 rpm -qa 查不到;reinstall 会先 Reinstalling 再 Cleanup。
remove 会连依赖一起删:卸载被别的包依赖的包时,dnf 提示要删一大串。看清楚列表再按 y,必要时用 dnf remove --noautoremove。
卸载前问自己:这个包是系统自带的吗?删掉 base 组里的东西可能破坏环境。
install 时那两行大小是两层,不是重复:先出现的 4.3 kB 是 repomd.xml(仓库的索引目录),确认「元数据变过了 / 本地没有」之后,才会去拉 44 MB 的 primary / filelists 全量。所以第一次或 dnf clean all 之后一定会看到两行;平时只看到第一行几 kB 才是正常的。
「already installed」和「not installed」都是 info,不是错误:install 一个已经在的包,dnf 输出 Package ... is already installed 加 Nothing to do,退出码仍是 0;remove 一个不存在的包,输出 package xxx is not installed 也不是失败。判断装没装上,只认 rpm -q 的输出,不认这两句话的语气。
考试也常问「怎么撤销」
dnf history
dnf history info last
dnf history undo last
成功长什么样:history 每行一个事务 ID;undo last 输出里把刚才装的东西再删回去,最后多出一条新的 history 记录(撤销本身也是一个事务)。
为什么 undo 不是「删除那条记录」:dnf 不做时光倒流,它是再执行一次反向操作。所以如果这期间文件被手动改过,undo 只负责包层面,不负责你的数据。
versionlock
dnf install -y 'dnf-command(versionlock)'
dnf versionlock add tree
dnf versionlock list
dnf versionlock delete tree
成功长什么样:list 里出现 tree-0:1.8.0-... 这样的行,带 0: 前缀和 epoch。
'dnf-command(versionlock)' 是一种虚拟 provides——用「我需要的能力」而不是包名去装,这在不知道插件包叫什么时特别有用。同类的还有 dnf-command(config-manager)。
锁住之后 dnf update 是「静默跳过」,不报错:命中时 dnf 直接把候选从求解过程里去掉,输出 Nothing to do. / Complete!,退出码 0。它不会被当成失败——所以别指望用「有没有报错」来判断锁生效了没有。
★ 实测最反直觉的一点:锁对「查询」无效,只对「求解」有效
用 grub2-tools(本机 126,仓库有 127)实测:
· dnf list --upgrades grub2-tools → 锁着也照样显示 127
· dnf update grub2-tools → Nothing to do.
list / repoquery 走的是包集合查询,update / install 走的是事务求解器,versionlock 只塞进后者。
⇒ 所以「锁前 / 锁后跑同一条 list」这个对照是无效的,别用。
正确判据(两条合起来才成立):① 用不受锁影响的命令确认候选确实存在 —— dnf list --upgrades <包> 或 dnf check-update;② 看 dnf update <包> 是否真的不动。「有候选」+「不动」= 锁生效。dnf update <包> --assumeno 会自动答 No,只解析不执行,拿来复核很安全。
★ --disableexcludes=all 绕不过 versionlock(实测):它只管 .repo 里的 excludepkgs,versionlock 的过滤走的是另一条通道。锁后执行 dnf update <包> --disableexcludes=all --assumeno,照样 Nothing to do.。只有 versionlock delete / clear 能解。
另外两个易错点:
① 显式写全 NVRA 也装不上:dnf install 包-新版本 会报 All matches were filtered out by exclude filtering。锁不是「只挡自动选版本」。
② 对没安装的包执行 add 是陷阱:会把当时仓库里所有可用版本全部加锁(实测 5 条),以后新版本永远进不来。动手前先 versionlock list 看清楚。
锁文件在哪:/etc/dnf/plugins/versionlock.list,由 versionlock.conf 里的 locklist= 指定,dnf versionlock list 读的就是它。锁对 reinstall 无效 —— 锁的是当前版本,重装同一版本不算升级,照常执行。
另外:拿一个「本来就是最新版」的包测锁,是无效实验。锁生效与没得升,两条路都是 Nothing to do.。先用 dnf check-update 找一个真正有更新的包(本机当时是 grub2 系列 126→127)。
provides
dnf provides */semanage
dnf provides /usr/bin/htop
dnf provides '*/nc'
成功长什么样:输出 policycoreutils-python-utils-... : 提供 semanage 这类结果,带 Repo 和 Matched from。

这是本域最该练熟的一条:考试说「安装提供 semanage 命令的包」时,你不需要背包名;真实运维里遇到 command not found,第一反应也应该是它,而不是去搜网页。

5 · 组与模块 RHEL 9 特有的一层

group
dnf group list
dnf group list --installed
dnf group info "Development Tools"

# 只在这些情况下才真的装(本机未执行,Development Tools 约 100+ 包)
# dnf group install -y "Development Tools"
成功长什么样:group list 分「已安装组 / 可用组」两段;group info 列出 Mandatory / Optional 包清单。
组名带空格必须加引号,且大小写要与 group list 输出的完全一致。写错会报 No group marked for installation。
组其实有两层,group list 的输出会分三段告诉你:
Available / Installed Environment Groups —— 环境组,描述的是「整台机器装成什么样」,如 Minimal Install(本机已装的就是它)、Server with GUI。
Available Groups —— 功能组,是「一堆包打包成一份清单」,如 Development Tools、System Tools。
装环境组用 dnf group install,和功能组同一条命令;差别只在你选的是哪一类名字。
同一软件的多个版本并存
dnf module list nodejs
dnf module enable nodejs:18
dnf module install -y nodejs:18
dnf module list --installed
dnf module reset nodejs
成功长什么样:module list 里每个流有 [d](默认)/ [e](已启用)/ [i](已安装)标记;enable 之后状态变成 [e]。
同一个模块同时只能启用一个流。想换流必须先 dnf module reset nodejs,否则报 Cannot enable ... conflicting requests。
模块和普通包不要混装:模块管理的包用 dnf install 装,后续升级会被模块流限制。
版本差异:RHEL 9 / Rocky 9 仍有 module;RHEL 10 已经移除这套机制。考 EX200(RHEL 9)要会。

6 · 直接处理 rpm 文件 选做:不在考纲里,用来解释 dnf 的报错

先说清楚定位:EX200 考纲只写到「通过红帽 CDN、远程存储库或本地文件系统安装和更新软件包」,标准做法是 dnf install /path/x.rpm;rpm -V / rpm2cpio 都不在考纲。这一章是「深层理解 + 排障」内容,可以先跳过,等 dnf 报错看不懂时再回来。
一个包的全部身份
rpm -q openssh-server
rpm -qi openssh-server | head -12
rpm -ql openssh-server | wc -l
rpm -qc openssh-server
rpm -qd openssh-server
rpm -qf /etc/ssh/sshd_config
rpm -qR openssh-server | head -8
rpm -qpl /opt/localrepo/tree-1.8.0-10.el9.x86_64.rpm | head
成功长什么样(Rocky 9 实测):
· -q → 一行 openssh-server-9.9p1-9.el9_8.rocky.0.1.x86_64
· -qi → Name / Version / Release / Install Date / Signature(签名行带 Key ID)
· -ql → 27 行
· -qc → 4 行(/etc/pam.d/sshd、/etc/ssh/sshd_config、/etc/ssh/sshd_config.d/50-redhat.conf、/etc/sysconfig/sshd)
· -qd → 4 个 man 页
· -qf → 反查回 openssh-server-9.9p1-...
· -qR → 依赖清单,/bin/sh 会重复出现多次(每个能力各要一次)
· -qp(加 -p)→ 查还没装的 rpm 文件
四个易混点:
① -qc 出 4 行,rpm -ql | grep ^/etc 出 5 行 —— 少的那行是目录 /etc/ssh/sshd_config.d。-qc 只列文件级、且带 config 标记的条目,目录永远不列。
② -ql 含目录,-qc / -qd 不含。
③ -qf 的路径必须真实存在;软链接要想到它可能归属另一个包。
④ 一次查多个包时,只要有一个没装,退出码就是 3(各包状态仍逐条打印)。
退出码别乱用:rpm -q 单包 → 0 装了 / 1 没装;多包 → 3 表示有没装的。脚本里先 rpm -q pkg >/dev/null,别接管道(管道会把退出码换成后一个命令的)。
同一个文件三种破坏
rpm -V openssh-server
echo "# lab" >> /etc/ssh/sshd_config
rpm -V openssh-server
chmod 644 /etc/ssh/sshd_config
rpm -V openssh-server
chmod 600 /etc/ssh/sshd_config
成功长什么样(实测逐步对照):
· 第一步 → 无输出,echo $? = 0
· 追加一行后 → S.5....T.  c /etc/ssh/sshd_config
· 改成 644 后 → .M.....T.  c /etc/ssh/sshd_config
· 改回 600 → 只剩 .......T.  c /etc/ssh/sshd_config
九位顺序(对不上就是没记住,按这个背):
S Size 大小 · M Mode 权限 · 5 MD5 内容摘要 · D Device 设备号 · L Link 链接指向 · U User 属主 · G Group 属组 · T mTime 修改时间 · P caPabilities
行尾的 c = config 文件,d = doc,什么都没写 = 普通文件。
rpm -V 有输出 ≠ 文件坏了。改完再改回来,内容与权限都复原了,T 位照样亮 —— 因为 mtime 被永久改掉了,而 mtime 也是被校验的。判断「是否被篡改」要逐位看:只有 5(内容)位亮才是真的内容不对。
无仓库时的唯一救命手段
mkdir -p /tmp/fix && cd /tmp/fix
dnf download --destdir /tmp/fix openssh-server-9.9p1-9.el9_8.rocky.0.1
rpm2cpio openssh-server-9.9p1-9.el9_8.rocky.0.1.x86_64.rpm | cpio -idmv ./etc/ssh/sshd_config
ls --full-time /tmp/fix/etc/ssh/sshd_config
touch -r /tmp/fix/etc/ssh/sshd_config /etc/ssh/sshd_config
rpm -V openssh-server
成功长什么样:cpio 打印 ./etc/ssh/sshd_config 和 2992 blocks;ls --full-time 看到包内原始 mtime 2026-07-30 20:05:41;touch -r 之后 rpm -V 无输出、退出 0。
三个必踩的坑:
① dnf download openssh-server(不写版本)下到的是仓库最新版 13.el9,系统装的是 9.el9。版本不对,提取出来的是另一版配置 —— 必须写全 NVRA(名字-版本-发行版-架构)。
② cpio 的模式必须带 ./(包内路径就是 ./etc/...)。写成 etc/ssh/sshd_config 会一个文件都不提取,只打印 2992 blocks 然后退出 0 —— 看着像成功,实际什么都没做。
③ 只复制内容不够:mtime 不同,rpm -V 会一直报 .......T.。要 touch -r 借用包内文件的时间戳,别手写一个近似时间(20:05:00 是错的,真值是 20:05:41)。
还有更省事的路:仓库可用时直接 dnf reinstall openssh-server。rpm2cpio 是没有仓库、只能拿到一个 .rpm 文件时的兜底。
dnf 与 rpm 的本质区别
rpm -ivh --test /opt/localrepo/tree-1.8.0-10.el9.x86_64.rpm
rpm -ivh --test --replacepkgs /opt/localrepo/tree-1.8.0-10.el9.x86_64.rpm
mkdir -p /tmp/t && dnf download --destdir /tmp/t httpd
rpm -ivh --test /tmp/t/httpd-*.rpm
rpm -ivh --test --nodeps /tmp/t/httpd-*.rpm
dnf install -y /tmp/t/httpd-*.rpm
成功长什么样(实测):
· 第 1 条 → 先打印 Verifying... Preparing... 两行进度条,最后一行才是 package tree-1.8.0-10.el9.x86_64 is already installed
· 第 2 条 → 只有两行进度条,无报错、退出 0
· 第 4 条 → Failed dependencies: httpd-core = 0:2.4.62-13.el9_8.9 is needed by ...; system-logos-httpd is needed by ...
· 第 5 条加 --nodeps → 通过(因为 --test 不真装)
· 最后 dnf install ./x.rpm → 自动把 httpd-core、system-logos-httpd 一起解出来装上
--test 是唯一安全的试穿:依赖检查、文件冲突检查全跑一遍,一个文件都不写。所有不确定的 rpm 装卸,先加 --test。
★ 进度条会骗人:Verifying... Preparing... 出现不代表要装成了,它只是 rpm 在解包校验。只认最后一行和退出码。
本质区别:dnf = 仓库元数据 + 依赖求解;rpm = 单个文件的事务执行器,不联网、不解依赖。
实践:能用 dnf install ./x.rpm 就别用 rpm -ivh;只有依赖确定齐全(或纯离线批量)时才用 rpm。
-i 装(已存在则报冲突)· -U 升级或安装 · -F 只升级已装的 · --replacepkgs 允许重装同版本。
--whatrequires 才是判据
rpm -e --test glibc 2>&1 | head -3
rpm -q --whatrequires openssh-server
rpm -e --test openssh-server
rpm -ivh --test /tmp/nonexist.rpm | head -2
echo $?
rpm -ivh --test /tmp/nonexist.rpm > /tmp/e.txt 2>&1
echo $?
成功长什么样(实测):
· rpm -e --test glibc → 刷屏,3428 条 is needed by (installed) ...
· --whatrequires openssh-server → no package requires openssh-server
· rpm -e --test openssh-server → 无输出、退出 0
· | head 之后 echo $? → 0;改成重定向后 echo $? → 1
三件必须分开记的事:
① 没有输出 ≠ 没有依赖者。openssh-server 确实没有包依赖它(--whatrequires 空),所以 rpm -e --test 干净退出。要判断"能不能卸",先问 --whatrequires。
② sshd 正在运行 / 你正连着它,rpm 完全不看 —— rpm 只查 rpm 数据库里的依赖关系。
③ | head 会吞掉退出码(拿到的是 head 的 0)。脚本里要判成败,重定向到文件再 echo $?,这一条对所有命令都成立。
--nodeps 是拆系统:rpm -e --nodeps 不管依赖硬删,删完系统可能"能开机但功能残缺"。真正需要时先用 --test 看清后果,再决定。

7 · 附录一:命令速查 按「我想干什么」查

我想…命令注意
看仓库列表dnf repolist --all读本地缓存,不联网
验证仓库可用dnf clean all && dnf makecache唯一算数的验收
临时关掉其它仓库--disablerepo='*' --enablerepo=X优先级高于 .repo
找命令属于哪个包dnf provides */命令不用背包名
只下载不安装dnf download --destdir=DIR 包名需 dnf-plugins-core
撤销上一次操作dnf history undo last是反向执行,不是删记录
锁版本dnf versionlock add 包装 dnf-command(versionlock)
装一个本地 rpmdnf install ./x.rpm比 rpm -ivh 会解依赖
看文件属于哪个包rpm -qf /path比 dnf provides 更快
看包装了哪些文件rpm -ql 包名配 -qc 只看配置

8 · 附录二:报错速查 看到这行字该想到什么

报错真实原因先查这个
Cannot download repomd.xml仓库地址不可达 / 路径写错 / DNS 不通curl -I <baseurl>
Curl error 6: Could not resolve hostDNS 或网络ping / cat /etc/resolv.conf
repodata/repomd.xml ... not found没跑 createrepo_cls 目录/repodata/
No package X available仓库里没有,或仓库没启用dnf --enablerepo=X list available
nothing provides ...缺依赖包,仓库不全dnf provides */缺失的命令
GPG key retrieval failed没导入 key 但 gpgcheck=1rpm --import <keyurl>
Failed dependencies(rpm 命令)用了 rpm -ivh,它不解依赖改用 dnf install ./x.rpm
Cannot enable ... conflicting requests模块流已被别的流占用dnf module reset X

9 · 附录三:自测 先自己答,再展开

1. 一个装了 100 个 rpm 的目录,为什么 dnf 看不见里面任何包?

因为仓库的入口是 repodata/repomd.xml,不是 rpm 文件本身。没有索引,dnf 不知道目录里有什么、依赖关系如何。createrepo_c 就是生成这份索引的工具。

2. dnf repolist 显示两个仓库都很正常,为什么还可能装不上包?

因为它读的是本地缓存 /var/cache/dnf/,不联网。要证明仓库活着必须 dnf clean all && dnf makecache。「配置读通了」和「仓库可用」是两件事。

3. file:///opt/repo 和 file://opt/repo 差在哪?

前者是本地路径 /opt/repo;后者把 opt 当成主机名,会尝试走网络。本地路径必须三个斜杠。

4. 往本地仓库里新加了几个 rpm,需要做什么?

重新生成索引:createrepo_c --update /opt/localrepo,然后 dnf clean all。不重跑的话,新包在 repodata 里不存在,dnf 依然看不见。

5. 怎么只从一个仓库装包,而不改动任何配置文件?

dnf --disablerepo='*' --enablerepo=local install 包名。命令行参数优先级高于 .repo 的 enabled=,且只对本次生效。

6. 考试说「安装提供 semanage 命令的包」,你怎么找?

dnf provides */semanage。这比记包名(policycoreutils-python-utils)可靠得多,也是遇到 command not found 时的标准动作。

7. dnf install ./x.rpm 和 rpm -ivh ./x.rpm 的本质区别?

dnf 会连仓库、做依赖求解;rpm 只执行单个文件的事务,不解依赖、不联网。缺依赖时 rpm 直接 Failed dependencies。优先用 dnf 装本地包。

8. 装错了包,怎么回退?为什么说 undo 不是「时光倒流」?

dnf history undo last。它实际是再执行一次反向事务,不是删除历史记录;期间被手动改过的文件不会恢复。

9. 为什么 dnf module enable a:1 之后想换 a:2 会失败?

同一个模块同时只能启用一个流。要先 dnf module reset a 再 enable 另一个流。模块流是「整个系统只用这个版本」的承诺。

10. rpm -V 没有输出,说明什么?

说明该包所有文件的校验信息与安装时记录一致,没有被改动。没输出才是好结果——和 mount -a、parted 的成功行为一致。

11. BaseOS 和 AppStream 为什么要分成两个仓库?

BaseOS 放系统底座(内核、glibc、systemd 这类,只有一个版本),AppStream 放应用与多版本组件(可以用 module 选流)。分仓是为了让「底座稳定」和「应用可选版本」互不干扰。

12. 仓库文件里 enabled=0 的仓库,dnf install 会用吗?

不会,除非命令行临时 --enablerepo=那个仓库,或用 dnf config-manager --set-enabled 永久打开。