1. createrepo_c 基础
1.1 核心定义
1.1.1 工具本质
createrepo_c 是一款基于 C 语言开发的命令行工具,核心功能是扫描指定目录下的 RPM 包,提取每个包的元数据(如包名、版本、依赖、架构、文件列表等),并按照 rpm-md(RPM Metadata)标准,生成结构化的元数据文件(repodata 目录),供 yum/dnf 等包管理器解析使用。
1.1.2 核心相关工具套件
createrepo_c 并非独立工具,而是一个完整的仓库元数据管理套件,包含以下核心组件,协同实现仓库的全生命周期管理:
- createrepo_c:核心工具,负责生成和更新仓库元数据;
- mergerepo_c:用于合并多个 RPM 仓库的元数据,生成一个统一的仓库,适用于多源仓库整合场景;
- modifyrepo_c:用于修改已生成的 repodata 元数据(如添加/删除元数据文件、修改元数据属性),无需重新生成整个仓库元数据;
- sqliterepo_c:将 repodata 中的 XML 元数据转换为 SQLite 数据库索引,提升 yum/dnf 包搜索、依赖解析的速度;
1.2 与 createrepo(Python 版)的核心差异
createrepo_c 作为 createrepo 的替代品,在保留核心功能兼容性的同时,在性能、依赖、功能扩展上有显著提升。在CTyunOS全系列中createrepo是指向createrepo_c的软连接。
1.3 核心依赖与运行环境
1.3.1 系统依赖
createrepo_c 依赖以下系统级库,安装时需确保这些依赖已存在(不同 Linux 发行版名称略有差异):
- rpm-devel:提供 RPM 包解析的核心接口,用于提取 RPM 包内的元数据;
- zlib-devel:用于元数据文件的压缩(gzip 格式);
- xz-devel:用于元数据文件的高压缩比压缩(xz 格式);
- zstd-devel:用于现代高压缩效率的 zstd 格式压缩(新版本推荐使用,
createrepo-1.1.4 默认类型); - openssl-devel:用于生成元数据的哈希校验值(sha256、sha512 等);
- libxml2-devel:用于 XML 元数据的解析与生成(rpm-md 元数据基于 XML 格式)。
1.3.2 支持的操作系统
createrepo_c 兼容所有基于 RPM 包管理的 Linux 发行版,包括但不限于:
- Red Hat 系列:RHEL 7+、CentOS 7+、Rocky Linux、AlmaLinux;
- Fedora 系列:Fedora 28+(默认预装 createrepo_c);
- 国产化 Linux:CTyunOS、麒麟操作系统、欧拉操作系统(openEuler);
- 其他:SUSE Linux Enterprise Server(SLES)12+。
2. 核心架构与工作原理
2.1 整体架构设计
createrepo_c 的架构采用模块化设计,分为输入层、解析层、处理层、输出层四个核心模块,各模块协同工作,实现元数据的生成与更新,架构如下:
- 输入层:接收用户传入的参数(如 RPM 目录、分组文件、压缩格式等),读取指定目录下的 RPM 包文件,验证 RPM 包的合法性(如校验包的完整性、格式正确性);
- 解析层:通过 rpm-devel 库解析每个 RPM 包的头部信息(header),提取核心元数据,包括包名(name)、版本(version)、发布号(release)、架构(arch)、依赖关系(requires)、提供能力(provides)、文件列表(filelists)、变更日志(changelog)等;
- 处理层:对解析后的元数据进行清洗、去重、排序,整合分组信息(若指定 comps.xml 文件),计算每个元数据文件的哈希值(用于校验),并按照 rpm-md 标准组织元数据结构;
- 输出层:将处理后的元数据生成 XML 文件(primary.xml、filelists.xml、other.xml 等),进行压缩处理,生成 repomd.xml(元数据索引文件),最终输出到指定的 repodata 目录中。
核心优势:模块化设计使得各模块可独立扩展(如新增压缩格式、支持新的元数据字段),同时 C 语言的指针操作和内存优化,大幅提升了元数据解析与处理的效率,减少了资源占用。
2.2 元数据生成的核心流程
createrepo_c 生成元数据的流程可分为初始化、解析、处理、生成四个步骤,每个步骤的核心逻辑如下:
2.2.1 步骤1:初始化(初始化阶段)
用户执行 createrepo_c 命令后,工具首先进行初始化操作:
- 解析命令行参数,确定 RPM 包目录、输出目录(默认与 RPM 目录一致)、压缩格式、分组文件路径、增量更新标识等参数;
- 检查输出目录是否存在,若不存在则创建;若存在且未指定增量更新,则删除原有 repodata 目录(避免旧元数据干扰);
- 加载系统依赖库(如 rpm、zlib 等),初始化解析器和压缩器。
2.2.2 步骤2:解析(RPM 包解析阶段)
此阶段是元数据生成的核心,工具会遍历指定目录下的所有 .rpm 文件,对每个 RPM 包进行解析:
- 通过 rpmReadPackageFile 接口读取 RPM 包头部信息,跳过损坏、格式错误的 RPM 包(并输出警告信息);
- 提取 RPM 包的核心元数据字段,存储到内存中的临时数据结构(链表或哈希表),便于后续处理;
- 若指定了分组文件(comps.xml),则解析该文件,提取软件分组信息(如组名、组描述、组内包含的 RPM 包),与 RPM 包元数据关联。
关键优化:createrepo_c 采用批量解析机制,避免单次解析单个 RPM 包的频繁 I/O 操作,同时利用内存缓存存储已解析的元数据,减少重复解析,提升效率。
2.2.3 步骤3:处理(元数据处理阶段)
对解析后的元数据进行整理,确保元数据的规范性和一致性:
- 去重处理:删除重复的 RPM 包元数据(按包名+版本+架构去重,保留最新版本);
- 排序处理:对元数据按包名、版本号进行排序,便于 yum/dnf 快速搜索;
- 依赖关系整理:解析 RPM 包的依赖表达式(如 libc.so.6()(64bit)),标准化依赖格式,确保依赖解析的准确性;
- 哈希计算:对每个 RPM 包文件和生成的元数据文件,计算哈希值(默认 sha256,可通过参数指定),用于校验文件完整性。
2.2.4 步骤4:生成(元数据输出阶段)
将处理后的元数据生成标准化的 rpm-md 格式文件,输出到 repodata 目录:
- 生成 primary.xml:包含所有 RPM 包的核心元数据(包名、版本、依赖、哈希值等),是 yum/dnf 依赖解析的核心文件;
- 生成 filelists.xml:包含每个 RPM 包安装的所有文件路径、文件类型、权限等信息,用于 yum/dnf 的文件搜索功能;
- 生成 other.xml:包含 RPM 包的变更日志、作者、厂商等附加信息;
- 生成 repomd.xml:元数据索引文件,记录上述所有 XML 文件的路径、哈希值、修改时间,供 yum/dnf 快速定位和校验元数据;
- 压缩处理:对上述 XML 文件进行压缩(默认 gzip,可指定 xz、zstd 等格式),减少文件体积,节省存储和传输带宽。
2.3 增量更新原理
createrepo_c 的 --update 参数是其核心优化特性之一,用于增量更新仓库元数据,避免每次都重新生成整个 repodata 目录,大幅提升更新效率。其原理如下:
- 读取原有 repodata 目录中的 repomd.xml 文件,获取上次生成的元数据信息(如各 XML 文件的路径、哈希值、修改时间);
- 遍历指定目录下的 RPM 包,对比每个包的修改时间、哈希值与原有元数据中的记录,判断该包是否为新增、修改或删除状态;
- 仅对新增、修改的 RPM 包重新解析元数据,对删除的 RPM 包从元数据中移除,未变更的 RPM 包元数据直接复用;
- 更新 primary.xml、filelists.xml、other.xml 等文件,重新计算哈希值,更新 repomd.xml 索引,生成新的 repodata 目录(覆盖原有目录,但仅修改变更部分)。
注意:增量更新仅适用于 RPM 包目录中新增、修改、删除少量 RPM 包的场景;若大量 RPM 包变更(如超过 50%),建议直接重新生成元数据,效率更高。
3. 功能讲解与实操
3.1 基础功能:创建与更新仓库
3.1.1 安装 createrepo_c
sudo yum install createrepo_c
3.1.2 基础命令:创建仓库
最基础的用法的是为指定目录下的 RPM 包生成元数据,命令格式如下:
createrepo_c [选项] /path/to/rpm_directory
示例:为 /var/www/html/repo 目录下的 RPM 包创建仓库:
createrepo_c /var/www/html/repo
执行后,会在 /var/www/html/repo 目录下生成 repodata 目录,包含 primary.xml.gz、filelists.xml.gz、other.xml.gz、repomd.xml 等文件。
3.1.3 增量更新仓库
当 RPM 包目录中新增、修改或删除少量 RPM 包时,使用 --update(缩写 -u)参数进行增量更新,命令如下:
createrepo_c --update /var/www/html/repo
优势:仅更新变更的元数据,比重新生成仓库节省 80% 以上的时间,适合日常维护场景。
3.2 进阶功能:分组与updateinfo支持
3.2.1 导入软件分组信息(comps.xml)
comps.xml 是 RPM 仓库的分组配置文件,用于定义软件包的分组(如 “Base”“Development Tools” 等),方便用户按分组安装软件。createrepo_c 通过 -g(--groupfile)参数导入分组信息:
# 导入 comps.xml 文件,创建带分组的仓库
createrepo_c -g /path/to/comps.xml /var/www/html/repo
# 增量更新时保留分组信息
createrepo_c -g /path/to/comps.xml --update /var/www/html/repo
说明:comps.xml 文件可从系统镜像中提取(如 /mnt/cdrom/repodata/xxx-comps.xml),也可手动编写,格式需符合 rpm-md 标准。
3.2.2 导入仓库更新信息(updateinfo.xml)
updateinfo.xml是 RPM 仓库的更新元数据文件,用于向客户端发布软件包的安全补丁、缺陷修复及功能增强等更新公告。通过 modifyrepo_c工具的 --mdtype=updateinfo参数,可将此信息集成到仓库中。
# 通用方法:在已有仓库的repodata目录中导入或更新updateinfo
modifyrepo_c --mdtype=updateinfo updateinfo.xml repodata/
# 创建仓库时一并加入updateinfo
createrepo_c --updateinfo=updateinfo.xml /var/www/html/repo
注意:单独执行createrepo /var/www/html/repo并不会处理updateinfo.xml,即使之前有,新生成的也是没有的。
3.3 优化功能:压缩与哈希配置
3.3.1 配置压缩格式
createrepo_c 支持多种压缩格式,可通过 --compression-type 参数指定,不同压缩格式的对比与用法如下:
- gzip:默认压缩格式,压缩速度快,兼容性好,适合大多数场景;
- xz:压缩比高,文件体积小,但压缩/解压速度较慢,适合仓库存储(节省空间);
- zstd:现代压缩格式,压缩比接近 xz,速度接近 gzip,是推荐的优化选择(需要新版本支持,createrepo-1.1.4的默认类型)。
示例:使用 zstd 压缩格式创建仓库:
--compress-type=COMPRESSION_TYPE Which compression type to use for additional metadata files (comps, updateinfo, etc). Supported values are: bz2, gz, zstd, xz.
--general-compress-type=COMPRESSION_TYPE Which compression type to use (even for primary, filelists and other xml). Supported values are: bz2, gz, zstd, xz.
第一个是updateinfo、comps, 第二个是其他通用的。
3.3.2 配置哈希算法
元数据文件和 RPM 包的哈希值用于校验文件完整性,createrepo_c 支持多种哈希算法,通过 --checksum 参数指定,常用算法如下:
- sha256:默认算法,安全性高,兼容性好;
- sha512:安全性更高,适合对安全性要求高的场景;
- md5:兼容性好,但安全性较低,不推荐用于生产环境。
示例:使用 sha512 哈希算法创建仓库:
createrepo_c --checksum sha512 /var/www/html/repo
3.4 辅助功能:元数据修改与仓库合并
3.4.1 使用 modifyrepo_c 修改元数据
modifyrepo_c 用于修改已生成的 repodata 元数据,无需重新生成整个仓库,常用场景如下:
# 向 repodata 中添加自定义元数据文件(如 custom.xml)
modifyrepo_c /path/to/custom.xml /var/www/html/repo/repodata
# 删除 repodata 中的某个元数据文件(如 other.xml.gz)
modifyrepo_c --remove other.xml.gz /var/www/html/repo/repodata
# 修改 repomd.xml 中的元数据描述信息
modifyrepo_c --set-repomd-desc "Custom RPM Repository" /var/www/html/repo/repodata
3.4.2 使用 mergerepo_c 合并仓库
当需要将多个 RPM 仓库合并为一个统一仓库时,使用 mergerepo_c 命令,示例如下:
# 将 repo1 和 repo2 合并,输出到 merged_repo 目录
mergerepo_c -o /var/www/html/merged_repo /var/www/html/repo1 /var/www/html/repo2
# 合并时去重,保留最新版本的 RPM 包
mergerepo_c -o /var/www/html/merged_repo --unique /var/www/html/repo1 /var/www/html/repo2
注意:合并的仓库需采用相同的压缩格式和哈希算法,否则可能出现元数据不兼容的问题。
3.5 性能优化参数汇总
createrepo_c 提供多个参数用于优化性能,适用于大规模仓库场景,核心参数如下:
| 参数 | 功能说明 | 适用场景 |
|---|---|---|
| --update(-u) | 增量更新元数据,仅处理变更的 RPM 包 | 日常维护、少量 RPM 包更新 |
| --workers N(-w N) | 指定并行解析 RPM 包的线程数,N 为线程数(如 4、8) | 大规模仓库(万级 RPM 包),利用多核 CPU 提升效率 |
| --skip-symlinks | 忽略 RPM 包目录中的符号链接,避免重复解析 | RPM 包目录存在大量符号链接的场景 |
| --no-database | 不生成 SQLite 索引文件,减少元数据体积 | 仓库仅用于 yum/dnf 基础安装,无需快速搜索 |
| --keep-all-metadata | 保留所有旧版本的元数据,便于回滚 | 生产环境,需要元数据回滚能力的场景 |
示例:使用 8 线程、zstd 压缩、增量更新,处理大规模仓库:
createrepo_c --update --workers 8 /var/www/html/large_repo
4. 关键逻辑解析
4.1 primary
4.1.1 运算符号的转换
根据rpm二进制中数值标识转换成比较符号(例如LT(less then,<),GE(Greater than or Equal to,>=))
const char *
cr_flag_to_str(gint64 flags)
{
flags &= 0xf;
switch(flags) {
case 0:
return NULL;
case 2:
return "LT";
case 4:
return "GT";
case 8:
return "EQ";
case 10:
return "LE";
case 12:
return "GE";
default:
return NULL;
}
}
注意里面的flags &= 0xf;,即逻辑里只计算后面4个bit,若漏了此逻辑会有很多空值。
4.1.2 primary里的file
filelists结构中写入了全量的提供file,primary中也写入了file但加了过滤条件,过滤后只有很小一部分。
过滤逻辑:
/** Check if the filename match pattern for primary files (files listed
* in primary.xml).
* @param filename full path to file
* @return 1 if it is primary file, otherwise 0
*/
static inline int cr_is_primary(const char *filename) {
if (!strncmp(filename, "/etc/", 5))
return 1;
if (!strcmp(filename, "/usr/lib/sendmail"))
return 1;
if (strstr(filename, "bin/"))
return 1;
return 0;
};
4.1.3 本包提供的primary file不算入依赖
过滤逻辑:
// Skip package primary files
if (*filename == '/' && g_hash_table_lookup_extended(filenames_hashtable, filename, NULL, NULL)) {
if (cr_is_primary(filename)) {
continue;
}
}
注意这里不是用filelist里的全量数据,加了cr_is_primary的判断。
4.1.4 本包提供的provide不算入依赖
过滤逻辑:
// Skip files which are provided
if (g_hash_table_lookup_extended(provided_hashtable, depnfv, NULL, NULL)) {
continue;
}
注意这里不是只用provide名,加了flag、version、release,不包含epoch:
_cleanup_free_ char *depnfv = NULL; // Dep NameFlagsVersion
depnfv = g_strconcat(filename,
flags ? flags : "",
full_version ? full_version : "",
NULL);
对应的Go代码:
//Skip files which are provided 本包提供的不算入依赖
depnfv := d.Name + "~" + d.Flags.String() + "~" + d.Version + "~" + d.Release
if _, ok := filterProvidesMap[depnfv]; ok {
continue
}
4.1.5 过滤重复的依赖
过滤逻辑:
// Skip duplicate files
gpointer value;
if (g_hash_table_lookup_extended(ap_hashtable, filename, NULL, &value)) {
struct ap_value_struct *ap_value = value;
if (!g_strcmp0(ap_value->flags, flags) &&
!strcmp(ap_value->version, (full_version ? full_version : "")) &&
(ap_value->pre == pre))
{
continue;
}
}
注意这里不是说相同filename、flag、version、pre的只能出现一次,是相同name,其他值也一样就跳过,但其他值不一样重新写入map了,后面可能还会出现前面的值,例如下面是正常的:

对应Go代码:
if exist, ok := filterDupRequiresMap[d.Name]; ok {
if exist.Flags == e.Flags && exist.Ver == e.Ver && exist.Rel == e.Rel && exist.Pre == e.Pre {
continue
}
}
filterDupRequiresMap[e.Name] = *e
4.1.6 依赖(requires)的结构多了pre字段
不同于其他的Dependence,requires多了一个pre字段。
if (!strcmp(deps[i], "requires")) {
prereq = ", pre BOOLEAN DEFAULT FALSE";
} else
prereq = "";
4.1.7 依赖(requires)pre字段bool值的入库
requires的pre字段定义的bool值,但实际入库是按字符串入库的:
定义:
prereq = ", pre BOOLEAN DEFAULT FALSE";
入库:
if (isRequirement) {
if (dep->pre)
cr_sqlite3_bind_text (handle, 7, "TRUE", -1, SQLITE_TRANSIENT);
else
cr_sqlite3_bind_text (handle, 7, "FALSE", -1, SQLITE_TRANSIENT);
}
4.1.8 epoch的处理
epoch是数值型,但不能简单的>0就不输出,因为确实有很多rpm包的epoch是0,在有比较flag的时候,应该至少给epoch一个0值。
对应逻辑:
if d.Epoch > 0 {
sd.Epoch = fmt.Sprintf("%d", d.Epoch)
} else if sd.Flags != "" {
sd.Epoch = "0"
}
sqlite对字段的定义不严谨。
4.2 filelist
4.2.1 filelist存储全量数据
filelist中存储了全量的文件列表。
4.3 other(changelog)
4.3.1 仅保留最新的10条更新日志
超过10条的,只保留最新的10条更新日志。
4.3.2 对于相同日期的日志按倒序保留
rpm中的changelog时间只到日期,示例:
rpm -q --changelog expat-help-2.2.9-2.ctl2.noarch.rpm
warning: expat-help-2.2.9-2.ctl2.noarch.rpm: Header V3 RSA/SHA1 Signature, key ID 421e729f: NOKEY- quality enhancement synchronization github patch
- Type:requirement
- ID:NA
- SUG:NA
- DESC:update to 2.2.9
- Type:NA
- ID:NA
- SUG:NA
- DESC:modify the directory of AUTHORS
- Type:NA
- ID:NA
- SUG:NA
- DESC:move AUTHORS to license directory
程序处理时会将日期转成时间戳,会保留+1的偏移,上例的2.2.6-4转成时间戳是1571659200,2.2.6-5那条会转换成1571659201,如果有其他的依次递增。注意如果大于10条,是先保留10条再修改时间,如果先+1后截断10条会出现几个大一些的数。效果:


4.4 通用
4.4.1 xml中双引号转义
"转义成",实际不转义对后续的展示也不影响。
4.4.2 sqlite空字符串存储为null
写入时空字符串存储的null,存储为空字符串对展示也不影响。
4.4.3 sqlite压缩比
createrepo_c使用bzip2库压缩:
#define BZ2_BLOCKSIZE100K 5 // Higher gives better compression but takes
if (mode == CR_CW_MODE_WRITE) {
file->FILE = (void *) BZ2_bzWriteOpen(&bzerror,
f,
BZ2_BLOCKSIZE100K,
BZ2_VERBOSITY,
BZ2_WORK_FACTOR);
}
使用golang(bzip2)生成的sqlite.bz2要比C大20%~30%。压缩级别都为默认的9。不影响,未详细研究。
5. 参考资料
- createrepo_c 官方文档。