rpm spec autopatch 实现

问题

rpm spec 中的 Patch(或者 Source)能否指定目录前缀?

如能否以如下的方式指定文件路径:

Source0: a/b/c
Patch0: a/b/c

代码分析

%autopatch 宏实现如下,通过 lua 内置函数 ipairs 遍历 patches table 遍历,得到每个通过 Patch 定义的补丁路径,并最终应用补丁:

## /lib/rpm/macros

# Single patch application
%apply_patch(qp:m:)\
%{lua:\
local file = rpm.expand("%{1}")\
local num = rpm.expand("%{2}")\
if posix.access(file, "r") then\
    local options = rpm.expand("%{-q} %{-p:-p%{-p*}} %{-m:-m%{-m*}}")\
    local scm_apply = rpm.expand("%__scm_apply_%{__scm}")\
    print(rpm.expand("%{uncompress:"..file.."} | "..scm_apply.." "..options.."  "..file.." "..num.."\\n"))\
else\
    print("echo 'Cannot read "..file.."'; exit 1;".."\\n")\
end}

# Automatically apply all patches
# -m<min>       Apply patches with number >= min only
# -M<max>       Apply patches with number <= max only
%autopatch(vp:m:M:)\
%{lua:\
local options = rpm.expand("%{!-v:-q} %{-p:-p%{-p*}} ")\
local low_limit = tonumber(rpm.expand("%{-m:%{-m*}}"))\
local high_limit = tonumber(rpm.expand("%{-M:%{-M*}}"))\
for i, p in ipairs(patches) do\
    local inum = patch_nums[i]\
    if ((not low_limit or inum>=low_limit) and (not high_limit or inum<=high_limit)) \
    then\
        print(rpm.expand("%apply_patch -m %{basename:"..p.."}  "..options..p.." "..i.."\\n")) \
    end\
end}

其中 patchessources 类似),是通过解析 spec 文件得到的 lua table 类型变量,来自于如下代码:

// https://github.com/rpm-software-management/rpm/blob/rpm-4.17.0-beta1/build/speclua.c

static const char * luavars[] = { "patches", "sources",
			       "patch_nums", "source_nums", NULL, };

void addLuaSource(rpmlua lua, const struct Source *p)
{
    lua_State *L = rpmluaGetLua(lua);
    const char * what = (p->flags & RPMBUILD_ISPATCH) ? "patches" : "sources";

    lua_getglobal(L, what);
    lua_pushstring(L, p->path);
    lua_rawseti(L, -2, lua_rawlen(L, -2) + 1);
    lua_pop(L, 1);

    what = (p->flags & RPMBUILD_ISPATCH) ? "patch_nums" : "source_nums";
    lua_getglobal(L, what);
    lua_pushinteger(L, p->num);
    lua_rawseti(L, -2, lua_rawlen(L, -2) + 1);
    lua_pop(L, 1);
}

然而,在解析 Patch(或者 Source)定义的路径时会自动忽略文件名前面的文件夹(即最终的文件路径 p->path%{_sourcedir}/filename):

// https://github.com/rpm-software-management/rpm/blob/rpm-4.17.0-beta1/build/parsePreamble.c#L125

static struct Source *newSource(uint32_t num, const char *path, int flags)
{
    struct Source *p = xmalloc(sizeof(*p));
    p->num = num;
    p->fullSource = xstrdup(path);
    p->flags = flags;
    p->source = strrchr(p->fullSource, '/');
    if (p->source) {
        const char *buf = strrchr(p->source,'=');
        if (buf)
            p->source = buf;
        p->source++;
    } else {
        p->source = p->fullSource;
    }
    p->path = rpmGetPath("%{_sourcedir}/", p->source, NULL);
    return p;
}

揭晓答案

以如下的 Patch 定义为例:

Patch0: a/b/c

通过上面对代码的分析可知,在 ipairs(patches) 遍历时,临时变量 p%{_sourcedir}/c ,即传递给 apply_patch 宏进行应用补丁操作时得到的路径就是 %{_sourcedir}/c

因此,答案是补丁的路径里带上了文件夹路径不会报错,但是文件名前的目录会自动忽略。

此外,需要注意 %apply_patch 宏定义中 %{basename:"..p.."}-m 选项的参数(裸的 patch 命令会直接忽略该参数),patch 命令的参数是后面的 p

%apply_patch -m %{basename:"..p.."}  "..options..p.." "..i.."\\n

扩展阅读

从 debian quilt 补丁中生成 rpm spec 补丁

首先生成 Patch 定义:

%{lua:
local i = 1
for l in io.lines('debian/patches/series') do
    print(string.format('Patch%04d:\t%s\n', i, l))
    i = i + 1
end
}

然后,重新定义 %autopatch 宏,在 %apply_patch 时传递正确的路径:

%define autopatch(vp:m:M:)\
%{lua:\
local options = rpm.expand("%{!-v:-q} %{-p:-p%{-p*}} ")\
local low_limit = tonumber(rpm.expand("%{-m:%{-m*}}"))\
local high_limit = tonumber(rpm.expand("%{-M:%{-M*}}"))\
for i, p in ipairs(patches) do\
    local inum = patch_nums[i]\
    if ((not low_limit or inum>=low_limit) and (not high_limit or inum<=high_limit)) \
    then\
        print(rpm.expand("%apply_patch -m %{basename:"..p.."}  "..options.."debian/patches/%{basename:"..p.."} "..i.."\\n")) \
    end\
end}

需要注意 p 是带 %{_sourcedir}/ 前缀的全路径,因此需要加上一个 basename 的处理。

最后,通过 rpmspec -P <name>specrpmbuild -bp <name>.spec 命令即可很容易的验证修改后的 spec 文件的正确性。

注意:rpm 的多行宏定义需要在每行行尾添加反斜杠进行多行连接,如上面的 %autopatch 宏定义,但是如果是在 spec 文件里直接写 lua 脚本,则不需要。

这里有一个最大的问题是生成 srcrpm 的时候由于找不到补丁文件,所以构建时会报错:

error: Bad file: ./0049-PVE-savevm-async-register-yank-before-migration_inco.patch: No such file or directory
error: Bad file: ./0048-PVE-whitelist-invalid-QAPI-names-for-backwards-compa.patch: No such file or directory

由于这是 rpmbuild C 源代码层面的逻辑:

// https://github.com/rpm-software-management/rpm/blob/rpm-4.17.0-beta1/build/files.c#L2675

for (srcPtr = spec->sources; srcPtr != NULL; srcPtr = srcPtr->next) {
    char * sfn = rpmGetPath( ((srcPtr->flags & RPMBUILD_ISNO) ? "!" : ""),
            srcPtr->path, NULL);
    argvAdd(&files, sfn);
    free(sfn);
}

for (ARGV_const_t fp = files; *fp != NULL; fp++) {
    if (stat(diskPath, &flp->fl_st)) {
        rpmlog(RPMLOG_ERR, _("Bad file: %s: %s\n"),
        diskPath, strerror(errno));
        fl.processingFailed = 1;
    }
}

所以唯一的规避办法就是执行 rpmbuild 的时候使用 -bb 选项忽略源代码包的构建。

rpm spec preamble

SourcePatch 定义的位置有严格的要求,对于同一个 package(spec 可以定义多个 package)所有的 preamble tag 必须集中定义,比如,如下的 spec 写法是错误的:

%description
Source1:        x1
Patch1:         1.patch

而如下的 spec 写法是正确的:

Source1:        x1
Patch1:         1.patch
%description

参考资料

rpm spec 内置宏定义
https://rpm.org/user_doc/macros.html

Lua in RPM
https://rpm.org/user_doc/lua.html

The Preamble
http://ftp.rpm.org/max-rpm/s1-rpm-specref-preamble.html


最后修改于 2021-08-10