openSUSE:Specfile guidelines

跳转至: 导航, 搜索

什么是 spec(配置规范文件)?RPM 编译过程的核心是处理 .spec 文件。它说明了软件包怎样被配置,补缀哪些补丁,安装哪些文件,被安装到哪里,在安装该包之前或之后需要运行那些系统级别的活动。它必须手写,但更简单的办法是拿来他人写好的,在此基础上修改。RPM 自身对于你能在 spec 文件中做什么没有太多限制,所以你可以搞的很复杂。本指南试图减少一些特殊区域的差异以使它易于维护。

通用指南

所有的 spec 文件必须易读。如果其他的打包者看不懂你的 spec 文件,他们就不可能去复核你的包或者与你协作。

Spec 模板

当你从零开始打包一个软件的时候,你应该基于 spec 模板来写你的 spec 文件,(参见 rpmdevtools)。下面是一个针对名为 我的软件 的软件包的基本配置:

 # 添加开发工具源
 sudo zypper -p http://download.opensuse.org/repositories/devel:/tools/openSUSE_12.1 -v in osc rpmdevtools  
 # 定位到你的项目文件夹
 cd .../MYPROJECT
 # 在构建服务官网上注册你的包
 osc mkpac MYPACK
 # 定位到你的软件包根目录
 cd MYPACK
 # 用wget下载你的软件包的源代码
 wget http://upstream.example.org/source/.../MYPACK-1.0.tar.gz
 # 使用 rpmdev 工具生成你的软件包的 spec 模板文件
 rpmdev-newspec -t lib MYPACK
 (或者直接使用 vi MYPACK.spec 即可。项目文件夹下的任何空白 spec 文件都会被模板自动填充)
 # 生成软件更新日志
 osc vc
 # 编辑你的spec文件
 vi MYPACK.spec
 # 之后可以用:
 # osc build 编译
 # osc ci 扫描文件改动并上传到构建服务器

请把你的注意力从 spec 文件的格式和组织方式上移开,尽可能的顺着这个模板一步一步往下填。这么做不是因为我们认为这是唯一正确的写 spec 文件的方法,而是让在你提问的时候,帮助你的那个人能更方便的定位错误和很快就明白你试图做什么:-)。

针对特殊编程语言的特殊 spec 文件也可以被特殊的工具制作出来,例如 cpanspec 或者 gem2rpm-opensuse。想了解更多就去瞧瞧:

Spec 文件的编码

如果不需要使用 ASCII字符集以外的字符,那就不用关心 spec 文件的编码。如果使用了 ASCII 字符集以外的字符,请把 spec 文件以 UTF-8 编码保存。如果不确定一个字符是不是属于 ASCII 字符集,请参见: ACII字符地图

Spec 文件的授权

由于一些法律上的原因,spec 文件必须有一个授权说明的头部。请注意,如果你不写,开放式构建服务就会把它自己默认的加给你。如果这不是你想要的那样子,你可以参考下面的模板写你自己的。

 #
 # spec file for package python-$FOO
 #
 # Copyright (c) $CURRENT_YEAR $YOUR_NAME_WITH_MAIL_ADDRESS
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
 # upon. The license for this file, and modifications and additions to the
 # file, is the same license as for the pristine package itself (unless the
 # license for the pristine package is not an Open Source License, in which
 # case the license is the MIT License). An "Open Source License" is a
 # license that conforms to the Open Source Definition (Version 1.9)
 # published by the Open Source Initiative.
 
 # Please submit bugfixes or comments via http://bugs.opensuse.org/
 #
 #中文版
 #
 #
 # 软件包 软件包名写这里 的 spec 文件
 # 版权所有 (c) 年份写这里 你的名字 你的电邮
 #
 # 第三方修订者拥有对本文件的任何修正和增补的版权,除非他们宣布放弃。这份文件本身的授权,
 # 修复和增补的授权,都和软件包的授权相同(除非软件包不是以开源协议发布,例如MIT协议)。
 # 开源协议是一种遵守开源行动制定的开源定义的授权许可。
 # 请通过 http://bug.opensuse.org 提交错误报告和评论。
 #

请使用宏而不是硬编码的文件夹名 (可用宏请参见 打包指南之打包惯例:RPM宏). 在 Source: 或者 Patch: 行使用宏是高手的象征。有些人认为在 Source: 行不使用宏会更加易读。另一些人喜欢用宏,这样升级版本的时候他们就不用手动改这里了。两种都可以,但请统一风格并书写正确。如果你需要查看包含宏的字符串背后的真实内容的话,你可以使用rpm命令。例如,查看 Source: 背后的真实内容,你可以用:

rpm -q --specfile foo.spec --qf "$(grep -i ^Source foo.spec)\n"

注意:以上方法对于 %name%version 都可用,但是用户自定的宏可能会失败。

宏 vs RPM 变量

在 spec 文件中有两种定义编译根目录和优化参数的风格。

宏风格 变量风格
编译根目录  %buildroot $RPM_BUILD_ROOT
优化参数  %optflags $RPM_OPT_FLAGS

两者都是正确的,最终解析成的也是同样的值,但打包者应该选取一种风格并持续的在打包生涯中使用。混用,本身是合法的,但是从与其他人互动和易用性的角度看,是不应该在 openSUSE 中这么用的。

RPM 变量在包含它的命令脚本执行前被解析。由于 sh 的执行是臭名狼籍的慢,因此%-宏应该优先考虑,此外你其实已经使用了宏 %_bindir ,所以也不得不把其他的都写成宏,因为上面那些是没有 RPM 变量来替代的。

条件依赖

这章是MargueriteSu 讨论 - 贡献重写的,因为英文版是直接复制 Fedora 的,而 Fedora 的 Koji 构建服务是要上传 SRPM 和 spec 文件的,对 openSUSE 来讲 SRPM 就驴唇不对马嘴了。

详情见 RPM 条件依赖,这是目前互联网上最详细的资料。

报头

以下是对 spec 文件报头的要求。

命名 / 版本信息

包命名指南

元数据标签

  • Packager(打包者) 标签,一律不能在 spec 文件中使用。从源代码 RPM 重编译会强制替换这个标签的内容为重编译的家伙的,这样你和最终用户之间有了个第三者,出问题最终用户就不知道联系谁了。打包者的信息可以在 修改日志 中用特殊格式进行说明。
  • Vendor(厂商) 标签,也是同样的原因一律不准用。在开放式构建服务中,它还是自动设定的;如果你想要覆盖它,请在项目级别的元数据信息中使用 %define,去osc meta prj -e ... 得到的元数据信息里改。
  • Copyright(版权) 标签,早过时了。使用 License(授权许可) 标签, 这儿有详细的格式和解释: openSUSE:Packaging_guidelines#Licensing
  • Source(源代码) 标签,应该是指向上游开发者提供的压缩包的一个完整链接。然而如果压缩包已经被他们变更了压缩格式,这就行不通了。最好也在注释中提供一个下载页面。
  • Group(组别) 标签,只可以使用 RPM Group 组指南 里预定义的组。
  • BuildRoot(编译根目录) 标签,必须有,即使之后的更新包会覆盖它。合适的目录是: %_tmppath/%name-%version-build
  • AutoReqProv(自动查找 Requires 运行依赖) 标签,默认已经开了,所以不是想要关掉它的话,再写一遍就多余了。

概要(summary)标签

  • 最结尾处不能用任何语言的句号。如果这让语法洁癖的你很难接受,想想生活就像被XX那个调侃吧。出问题的是你。
  • 它必须简明扼要的描述软件包,最长80字节。
  • 语言必须大众,别让人查字典。比如ASAP每个人都知道是「尽快」,WYSIWYG就不是每个人都知道是「所见即所得」,所以不要用。
  • 只看它不看别的也能提供帮助。可以附带按字母排序或乱序的部分子包/全部子包名。
  • 它应该描述了软件包的主要功能,并指出和其他相似软件相比它的出彩之处。例如,「浏览器」 指代了一整大类,但是加上额外的形容词(如极简的、字符界面的或者快捷的)能给你的软件包一个更有性格的定位。

描述(description)标签

描述是在概要的基础上的展开。在描述标签里不要收纳任何安装说明样子的东西,它不是用户手册。如果软件包要求手动配置或有其他重要说明要告之用户,在软件文档里告诉他们。觉得有必要的话可以添加一个 README.SUSE 或其他类似的文件。另外请确保你的描述每行不超过70个字符(受 YaST 软件管理器在使用 ncurses 库进行高级终端处理时描述窗口的大小所限,不是指你在图形界面打开的那个 YaST,是你 DVD 安装时的那个字符界面的 YaST) 。描述也不该超出合理的长度,多于20行可能就太多了。

软件包的描述应该辅助并使用户不用安装其他类似的软件包就找到对的符合他目的的包。这里才是更详细的告知用户一个软件的功能的地方。它应该包含更多的软件特性的信息和与其他类似特性的软件包的不同之处。如果一个包有可能破坏用户的安装,那描述就应该明确警告这个包的潜在风险和副作用。

请放下你的个人喜好,在概要和描述中使用美式英语的拼写吧。出于对整个 RPM 数据库大小的考虑,RPM 的 spec 文件只包含了英语版本的概要和描述。不靠谱的本地化是由 YaST 来控制的。

2011年夏天之后,描述结尾部分的软件作者列表再也不应该被收纳进来了。我们使用了一个独立的 AUTHORS 文件来给他们表功。如果上游的源代码包没有这部分,你可以创建一个,然后像下面这样写:

Source2:        AUTHORS
...
%setup
cp %{S:2} . <= 这个缩写值得学习,全文是 %{SOURCE2},最后那个.表示当前文件夹,也就是 %{buildroot}
...
%files
%doc AUTHORS

概要或描述标签中用到商标

打包者应该注意在概要或描述中使用商标的方式。下面是几条需要遵守的规则:

  • 永远不要使用 "(TM)" 或者 "(R)" (或者这些通用字符集里面的等价字符, ™/®)。用对它们是个复杂到不可思议的问题(因为那个写法有大半本专利法得学),所以对我们来说更安全的就是根本不要用。
  • 不要含糊不清的提到商标。别用 「貌似」 或者 「类似」 这样的词,例如:
  • 不妥当: 它和 Adobe Photoshop 挺像(明天他们的律师团就找上你了)。
  • 恰当的: 它支持 Adobe Photoshop 的 PSD 格式文件, ...
  • 不妥当: Microsoft Office 的 Linux 版本
  • 恰当的: 一个支持 Microsoft Office 的 DOC 格式文件的文字处理器

如果你不确定的话,问问自己,有没有可能让别人误以为这个包是官方版?如果还有犹豫,干脆就把商标从你的表述中去掉。

依赖关系 Dependencies

运行依赖 Requires

RPM 有超级牛力去自动寻找函数库和诸如 Perl 模块的运行间依赖关系。简单说,别重造轮子,这活儿交给 RPM 就好。通常没有必要去单独列示像 Requires: libqt4 这样的字段,RPM 发现你依赖了 libqt4 包的共享库就会自动帮你选取这个运行依赖。

如果你的包分成了几个子包,那么子包需要用 Requires: %name = %version 代码来请求对主包的附带版本号的运行依赖。

-devel 子包需要明确的依赖这些开发工程包做了软链接的共享库所在的库包,例如 libfoobar-devel 需要 Requires: libfoo2 = %version, libbar5 = %version

小提示: 在 %files 字段中只有 libkde4.0.473.1.so 这样的名称最全的库文件才应该被放到 %{name}-libs / lib%{name}? 这样的共享库包中,其他的 libkde4.0.so, libkde4.0.473.so 这样的文件大部分都是做的软链接,应该放到 -devel 开发工程包中。

前期依赖 PreReq

软件包不应该使用 PreReq 标签。曾几何时,在查找依赖关系时的循环里 PreReq 用来在 RPM 决定安装顺序时 「获得」 一些普遍的运行依赖(那时 RPM 处理能力不强,包也不标准,你运行时依赖 libqt4, 但是你想编译发生就需要一些基础系统包比如 sysvinit 这种,等它一个一个依赖解决到这个层面是很慢的)。现在这个标签再也用不到了。

编译依赖 BuildRequires

不同于运行依赖 Requires,编译依赖 BuildRequires 是没有自动查找程序的。你必须单独列出你的软件包成功编译所需要的包。写个好的编译依赖会节省所有开发者和测试者的时间,因为他们不用再去手动查找缺失的编译依赖。这也保证了编译是可再生的,输出的包是永远有相同特性的。例如,如果编译依赖里没有 libpng-devel 包的话,configure 脚本,就有可能找不到 PNG 图像的支持。通过这样的代码 BuildRequires: libpng-devel (或者 openSUSE 11.4 之后才有的: BuildRequires: pkgconfig(libpng14),在没有安装 libpng-devel 编译依赖时,你可以让编译抛错并中止。

对于我们打包者来说,最重要的字段就是编译依赖 BuildRequires,但由于它的手动属性,留给我们的方法不多,如果不是修补他人 spec 文件里的编译依赖的话,试错是唯一通向成功的路:

  • 去软件的官方主页(一些中式软件的官方主页可能就是一个论坛的置顶帖子,甚至都没置顶,这些坏毛病老外样样不缺的,但好在一般都在搜该软件名的 google 返回结果前两页),开源软件一般都有或多或少这样的信息(如果没有,那就是软件开发者还没有准备好把软件推送给我们,或者是你找错找到他们的源代码暂存地诸如 github 这样的地方了,或者他们单独有一个给开发者准备的网站),一般在 Download/Develop 页面的 build/compile/make/source/svn/git/unstable 章节下面,或者单独的 Howto 或 Wiki 页面(常见于 google code)。一般情况下,这会解决软件包大部分的依赖。
  • 如果开发者提供的依赖包名,你无法在 openSUSE 中找到(在打开用户私人车库的情况下),那一般是由于 openSUSE 不叫这个名字,这时你可以用 openSUSE + 开发者提供的依赖包名在 google 搜索中查找,一般你都会有答案,比如 Dnsutils 这个包在 openSUSE 中就叫做 Bind-Utils; 如果在他人的私人车库里,你可以链入该包到你的源; 如果真的不存在,那恐怕你要先去编译这个包了。
  • 如果以上都做过了,编译时依然会抛错:
    • 首先检查你的语法,比如 BuildRequires 是不是少写了一个 s,最好使用有语法高亮的编辑器,命令行的 vi 或者 kwrite/kate,当没有变颜色时通常是你写错了。
    • 直接贴 软件名 + error 开头的或者 Make Error 这样的错误信息到 google 去搜,一般论坛、邮件列表、该软件 Q&A 会有你要的答案甚至是补丁。没有结果的话可以逐步精简搜索字段以扩大范围,例如,gegl error no videodev.h found ,我精简到了 gegl videodev.h 一下就找到了补丁告诉我说是 2.6.38 之后的内核不再支持 v4l1 ,删了这个文件。
    • 排查你的 configure 日志记录,找诸如 git ....no 这样的字段,它们完全有可能提示你缺失的依赖,然后再用上述方法找到 openSUSE 下对应的包名,装上它的 -devel 就好。一些老软件的新版本可能引入了新的依赖,升级打包的时候最好也查一下。
  • 如果到这里依然行不通,如果你是程序员(码农),那就是你大显神通的时候了; 如果不是,就当是进行了一场心灵的旅行吧,报个 Bug,重新上路。毕竟,漫漫长路,最美总是在路上不是咩?
针对 宏嵌套 的 OBS 警告

条件编译依赖仅限于简单的变量。RPM 一般通过这样的 %if 表达式来支援复杂的构造:

%if 0%(test "%something" = "enabled" && echo 1)

(上面是一个有两层宏嵌套的例子,第一层是 0%(),第二层是 %something。)

但是当评估编译依赖的时候,构建服务的 RPM 解析器是运行在一个 宏嵌套展开 被禁止的环境中的。你会看到这样子的警告:

Warning: spec file parser line 109: can't expand %(...)

下面这个例子只使用了一个简单变量,因此和 编译依赖 兼容:

%if ! 0%?have_own_mpfr
BuildRequires: mpfr-devel
%endif
豁免情况

不需要把下列包或者他们的依赖作为 BuildRequires,因为他们出现的太频繁了。它们已经被作为开放构建服务的最小化编译环境被预装了。之所以构建服务禁用了 %PreReq 宏,原因就在于此。 你可以用此命令得到这个列表:

osc meta prjconf openSUSE:Factory | egrep '^(Preinstall:|Support:|Required:)'
Preinstall: aaa_base acl attr bash coreutils diffutils
Preinstall: filesystem fillup glibc grep insserv libacl libattr
Preinstall: libbz2-1 libgcc%{gcc_version} libxcrypt m4 libncurses5 pam
Preinstall: permissions libreadline6 rpm sed tar zlib libselinux1
Preinstall: liblzma5 libcap2 libpcre0
Preinstall: libpopt0 libelf1 liblua5_1
Required: gcc gcc%{gcc_version} glibc perl rpm tar patch
Support: autoconf automake binutils bzip2 gcc gcc%{gcc_version}
Support: gettext-runtime glibc libtool perl rpm zlib
Support: libncurses5
Support: libaudit1 cpio cpp cpp%{gcc_version} cracklib cvs
Support: file findutils gawk gdbm gettext-tools
Support: glibc-devel glibc-locale groff gzip info less
Support: libbz2-devel libdb-4_8
Support: libstdc++%{gcc_version}
Support: udev
Support: libxcrypt libzio
Support: linux-glibc-devel make man netcfg
Support: net-tools pam-modules patch perl-base sysvinit-tools
Support: texinfo timezone util-linux libmount1 login
Support: libgomp%{gcc_version} libuuid1 psmisc
Support: terminfo-base update-alternatives pwdutils build-mkbaselibs
Support: brp-check-suse post-build-checks rpmlint-Factory
Support: build-compare
Support: libunwind libunwind-devel

依赖冲突

openSUSE 会尽可能的去自动避免包之间的依赖冲突。但不幸的是,目前还达不到完全自动避免。openSUSE 的依赖冲突政策详见: 依赖冲突

文件依赖

除了依赖包,RPM 还赐予了你依赖文件的能力,但这种既懒惰又不标准的能力即使不会让你深陷泥潭,也会抹杀掉你大部分的创造力。尽量使用:

  • whereis 文件名 来获得你要依赖的文件的完整路径,如果返回为空白,那证明你系统中没有安装该文件,请跳过下一条直接到第三条
  • sudo rpm -qf 文件路径如 /usr/bin/nslookup 获得文件所在的rpm包名
  • 如果没有安装该文件,用 openSUSE + 文件名 的方法去 google 搜索,一般在 OBS 开放式构建服务自身/rpmfind/pkgs.org/rpm.pbone.net 都会找到文件所在的rpm包名
  • 去 BuildRequires 这个包

只要有可能性,你都要去避免依赖 /etc, /bin, /sbin, /usr/bin, 或者 /usr/sbin 以外的文件。依赖这些文件夹以外的文件要求 zypper(和其他使用「软件源」这种方式的依赖解决工具,其实就是命令行的软件在线安装工具啦)去下载和解析一个庞大的 XML 文件,在里面查找你依赖的文件所在的包。通过依赖包而不是依赖文件的方式来帮助依赖解决工具避免这个过程会节省最终用户大把的时间。

很多时候,其他一些技术上的考虑会压倒这些考虑。一个特殊例子就是安装路径在 %_libdir/mozilla/plugins 的包。在本例中,授权你包中的某个特殊的小浏览器程序作为这个文件夹的所有者(通过 %files 宏的子宏 %dir</dir>),会给你拖上一大堆不需要的包,至少 mozilla 那一堆就躲不过。两害相权取其轻,下载一堆包还是下载一个大 XML 文件并占用点处理器运算时间,相信请求这个文件夹作为文件依赖会是更好的选择。

补丁

所有 openSUSE spec 文件中提到的补丁都应该在最上面有一条说明它们状态的评论。详见 打包指南之补丁 页面。

预备处理 (%prep)

以下是对 spec 文件的预备处理标签的要求

%setup 消音

你应该传送 <tt>-q 参数给 %setup 宏。这会显著减少编译日志文件的输出,尤其是源代码包会解压出一堆文件的时候。

编译 (%build)

以下是针对 spec 文件的编译章节的规则

编译器参数

用来编译软件包的编译器应该尊重系统 RPM 配置中设定的可用编译器参数。现实点说就是 C,C++ 和 Fortran 编译器应该尊重 %optflags 宏(等价 RPM 变量: $RPM_OPT_FLAGS)。尊重的意思是说在软件包编译时使用此宏/变量对应的真实内容作为编译器实际使用的参数。如果理由够充分,附加额外参数,重定义覆盖一些参数以及过滤这些参数中的一部分都是允许的,但这么做之前应该再三思量并把理由注释进 spec 文件,尤其是当你要覆盖或过滤参数的时候。

并行 make

可能的话, 应该这样调用 make

make %{?_smp_mflags}

通常这都会加快编译,尤其是在有对称多处理器(即双核心/四核心)的机器上。这比 %{?jobs:-j%jobs} 更受欢迎,因为它允许使用交叉编译参数,make -lN,而不是通过多线程编译参数硬性指定线程 -jN。然而,请务必确认,你的软件包可以 干净的 这么编译,因为有些作者的 Makefile 不支持并行编译,或者这么做会搞坏你的依赖关系。

如果源代码包不支持并行编译的话,请在 spec 文件中用注释提示这点,并且最好用 -j1 参数,这样其他人用 grep 命令会方便的知道该包不支持并行编译。

安装 (%install)

以下是对 spec 文件中安装章节的规则:

因为 rpmbuild 是以一个非特权用户运行的(就是最常见的那个 # no root for build),运行 make install 的时候必须不能试图去拥有文件的所有权(无论是直接使用 chown 命令,还是利用这样的命令如, install -o root)。文件的所有权应该在下面的 %files 部分来设定。

原版 RPM 的宏包和 SUSE 的 rpm 包里面带的宏包提供了两种名字相近所以特别爱混淆的安装宏:

# 在原版 RPM 的宏包中:
# 这种模拟方法套用了现在的 autotools 里面的 %configure:
%make_install 相当于 make install DESTDIR=%{?buildroot} 
#------------------------------------------------------------------------------
# 这种模拟方法是为了和老的不支持 DESTDIR 或者用 DESTDIR 会崩溃的包兼容
%makeinstall \
相当于  make \\\
        prefix=%{?buildroot:%{buildroot}}%{_prefix} \\\
        exec_prefix=%{?buildroot:%{buildroot}}%{_exec_prefix} \\\
# 为什么叫模拟呢,因为真正的 make install 是在命令行里面用的。
[...]

SUSE 的 rpm 的宏包,在 /usr/lib/rpm/suse_macros 里,把上面两种都重新定义为其实是一回事的宏了(因此不再支持老的 autotools 方式了):

%make_install           make install DESTDIR=%{?buildroot}
%makeinstall            make DESTDIR=%{?buildroot:%{buildroot}} install

因此,你最好用且只用 %make_install

移除编译根目录 buildroot

openSUSE 把在 %install 的头部使用 rm -rf %{buildroot}rm -rf $RPM_BUILD_ROOT 标记为「坏的写法」:

  %install
  rm -rf $RPM_BUILD_ROOT
  mkdir -p $RPM_BUILD_ROOT/usr/...  或 make install

为什么呢?

RPM_BUILD_ROOT 一般是在 /var/tmp,你造成了一个「竞速危机」,把自己暴露给同一台计算机上的本地攻击者,他可能控制你的账户(甚至是根账户,如果你用它编译的话)。最好在 %install 章节完全不要用 "rm -rf $RPM_BUILD_ROOT",而是依靠 %clean 来做。

如果一定要做,使用:

  %install
  rm -rf $RPM_BUILD_ROOT
  mkdir $RPM_BUILD_ROOT
  mkdir -p $RPM_BUILD_ROOT/usr ... or make install

这样如果攻击者想要用他自己的磁盘链接来替换你的 buildroot 的话,"mkdir $RPM_BUILD_ROOT" 会出错,整个编译也会中断。

PS:「竞速危机」是指你马上要做的事依赖前一个你发起却管不了过程的事的返回结果,如果有人在结果出来前伪造了一个结果给你,你要做的事实际上帮助了攻击者钓鱼。意即你前一件事的运行时间必须和伪造结果所需的时间赛跑。

清理 (%clean)

%clean 章节,如果有的话,会在 RPM 二进制包和 RPM 源码包生成后运行。在开放构建服务上这是没必要的,因为无论如何用来编译这个包的 chroot 环境或虚拟化环境都会被抹去。openSUSE 通常不支持在已有环境中编译软件包。(cf. bnc#176528 c4) 从 rpm-4.7/openSUSE_11.3 开始, 如果 %clean 字段在 spec 文件中没有的话,rpm 会默认使用 "%clean: rm -Rf %buildroot"。

很早以前,在 %clean 删除一些包前会检测 %{buildroot} 不是 /。 这在 openSUSE 中也不需要了。

代码片段 (%post* / %pre*)

这里是关于 spec 文件中 「代码片段」 章节的规则。使用代码片段应该慎之又慎。只有在合情合理的情况下才使用它。一些常见的代码片段被归档在:openSUSE:Packaging scriptlet snippets.

代码片段的运行依赖 (Requires)

你的包必须声明你在代码片段中使用的任何东西。表示法如下:

Requires(pre): ...
Requires(post): ...

注意:Requires 和 Requires(pre) 的内容不能有相同的部分,即 Requires(pre) 要求的包只用在 %pre 章节,也即它是「代码片段」的依赖而不是包的依赖。如果有相同,会造成编译成功的 rpmlint.log (本地编译会产生在 /var/tmp/build-root/home/abuild/rpmbuild/OTHER, 开放构建服务官网点击"success"字样后在编译日志“buildlog”的最后一章就是它的内容,也可以点击源名称如”openSUSE_Factory“后看到生成的二进制 RPM 包和源代码 SRPM 包,最下面的下载链接就是 rpmlint.log,另外不同的源会生成不同的 rpmlint.log)里产生一个 duplicate requires(依赖冗余) 警告。

只允许针对特定目录写代码片段

根据下标,包编译指令 (%prep, %build, %install, %check%clean) 里只允许变动 (创建、修改、删除) %buildroot%_builddir%_sourcedir 和有效的临时位置如 /tmp, /var/tmp (或 rpm 编译过程设定的 $TMPDIR (valid?) 或 %_tmppath) 中的文件。

/tmp, /var/tmp, $TMPDIR, %{_tmppath}  %{_builddir}  %{buildroot}
%prep
%build
%install
%check
%clean

上表中规则不管你用什么用户编译都必须被遵守。

文件 (%files)

这里是针对 spec 文件的「文件」章节的规则。openSUSE 沿袭了 文件系统层级标准 对文件系统布局的安排。该标准定义了文件应该被放置在系统的哪个位置,任何对该标准的背离都应该在 spec 文件中注释并解释为什么这么做是合理的。

所有权

您的软件包应该拥有在 %install 章节安装的全部文件。软件包不能拥有已经被其他软件包拥有的文件。这里的经验法则是第一个安装的软件包应该拥有其他包可能依赖的文件。如果你认为你有充分的理由去拥有一个其他软件包已拥有的文件,那么请在软件包提交到工厂版的复核请求里指出它。

文件夹的拥有权比文件的拥有权稍微复杂一点。尽管经验法则是相同的:拥有你的软件包在安装过程中创建的全部文件夹,不要拥有你的软件包依赖的软件包的文件夹,在一些特殊情况下,多个软件包拥有同一个文件夹也是可以的。这些特例如下:

  1. 你的软件包依赖的软件包提供的文件夹可能在该依赖包的后续版本中提供不同的文件夹,但你的软件包仍会使用原来提供的文件夹。一个普遍的例子是 Perl 模块。假设 perl-A-B 依赖 perl-A 并且把文件安装到 /usr/lib/perl5/vendor_perl/5.8.8/i386-linux-thread-multi/A/B. Perl 基础软件包保证了它会拥有 /usr/lib/perl5/vendor_perl/5.8.8/i386-linux-thread-multi ,只要它还与 5.8.8 版本兼容,但未来升级 perl-A 依赖包的时候可能会拥有并安装文件到 /usr/lib/perl5/vendor_perl/5.9.0/i386-linux-thread-multi/A. 这时 perl-A-B 软件包就需要去拥有 /usr/lib/perl5/vendor_perl/5.8.8/i386-linux-thread-multi/A/usr/lib/perl5/vendor_perl/5.8.8/i386-linux-thread-multi/A/B 以便保持合理的所有权。
  1. 多个软件包都安装了文件到一个常见文件夹中,但这些文件互不依赖。一个例子:
Foo-Animal-EMu 把文件放到 /usr/share/Foo/Animal/Emu
Foo-Animal-Llama 把文件放到 /usr/share/Foo/Animal/Llama

两个包互不依赖。没有其中一个软件包需要另一个软件包去拥有 /usr/share/Foo/Animal 文件夹的情况。在这种情况下,它们都必须宣称拥有 /usr/share/Foo/Animal 文件夹。

无论如何,我们都不能让系统中出现无人拥有的文件夹。详情见 Packaging/Unowned_Directories.

一个 openSUSE 软件包必须不能在 %files 列表里包含任何的冗余文件,冗余文件不但指一个文件被打包了两次或以上(在两个或多个子软件包中),还指文件内容上的雷同。可以使用 %fdupes %buildroot (需要添加 BuildRequires: fdupes) 来巧妙的通过硬链接来清理冗余文件。

权限

文件权限必须妥善设定,例如可执行文件需要给它设定执行权限。每个 %files 章节必须包括一行 %defattr(...)。下面是默认的设定:

%files
%defattr(-,root,root)

如果你没有一个非常好的原因去背离它,你应该对你软件包中所有的 %files 章节使用 %defattr(-,root,root) 设定。

访问授权 (SUID bits)

待修正 本章节基于「试错」法写作,还没有找到任何有关于此的文档。

在 openSUSE 下面,安全相关的权限是通过 /etc/permissions* 来处理的。软件包特有的权限可以通过在 /etc/permissions.d/ 里放文件来定义。阅读 /etc/permission* 的例子来取得这部分知识。

如果你想要创建一个需要设定用户和组权限的程式,你应该如下去做:

  • 添加 Requires(post): permisssions 代码片段依赖到你的 spec 文件。
  • 为你的软件包创建 permissions{,.easy,.secure,.paranoid} 文件。如果没有满足系统现在的安全设定的 permissions.*,就会回溯到 permissions。一般来说,permissions.easy 应该包含 make install 安装软件所需要的权限,permissions.paranoid 应该移除所有对非系统用户的授权(即使这会破坏功能),permissions.secure 的严格性介于两者之间。
  • 在 spec 文件中把这些 权限文件 作为源文件添加,在 %install 部分安装它们到 %{buildroot}%{_sysconfdir}/permissions.d/"软件包名"[."后缀"].
  • %files 部分,定义你权限文件中列示的文件的属性, 因为它们应该在permissions.secure 中定义。配置 rpmverify 不要 去检查它们的所有权或者权限。
  • 添加一个 %post 脚本根据系统现在的安全级别来设定你软件包的权限。
  • 添加一个 %verifyscript 脚本根据系统现在的安全级别来检查你软件包的权限。
  • 为了避免 rpmlint 对不允许的权限文件报错,创建一个名为 "软件包名"-rpmlintrc 的文件,里面写上 setBadness('permissions-unauthorized-file',333), 并且在 spec 文件中定义其为源代码。(如果你有少于或者多于 3 个不允许的权限文件,请自行修改 "333")

由于上面所说的包含一些不显著的细节,下面给出一个简化的例子:

[...]
Source2:        permissions
Source3:        permissions.easy
Source4:        %{name}-rpmlintrc
[...]
Requires(post):         permissions
[...]

%install
[...]
mkdir -p %{buildroot}%{_sysconfdir}/permissions.d/
install -m 644 %{S:2} %{buildroot}%{_sysconfdir}/permissions.d/%{name}
install -m 644 %{S:3} %{buildroot}%{_sysconfdir}/permissions.d/%{name}.easy

%if 0%{?suse_version} >= 1120
%verifyscript
%verify_permissions -e %{_bindir}/mysuidprogram
%endif

%post
%if 0%{?set_permissions:1} > 0
    %set_permissions %{name}
%else
    %run_permissions
%endif

%files
%defattr(-,root,root,-)
[...]
%verify(not user group mode) %attr(0711,root,root) %{_bindir}/mysuidprogram
[...]

这至少对你 私人车库 下的软件包是适宜的。

文档 文件

任何分发的源代码中附带的相关文档都应该被附加到软件包中去。不相关的文档包括像「编译指南」,无处不在的写有一般编译指南的 INSTALL 文件,和给非 Linux 系统的文档,比如 README.MSDOS。另外还需注意你应该把文档放在哪个子软件包,例如 API 文档归属于 -devel 子软件包,而不应该是主软件包。或者如果有好多文档,那可以考虑把它们单独切分为一个子文档包。在这种情况下,推荐使用 *-doc 作为子软件包名,Documentation 作为 Group 标签的值。

还有,如果一个软件包包含了一些 %doc 的东西,它必须不能影响程式的运行环境。简单说:如果它在 %doc 里,那么即使它没有程序也必须能运行。

配置 文件

配置文件必须在软件包中被标记为配置文件。这里的经验法则是,如果你大胆猜测单纯使用简单的 %config 会破坏东西,那么就用 %config(noreplace) 来替换它。换句话讲,当软件包升级需要覆盖本地配置文件中的更改时请一定三思而后行。一个不使用 %config(noreplace) 的例子是当一个软件包的配置文件更改过以至于新版本的包不能和它一起工作了的时候。无论何时使用 %config ,最好在 spec 文件里加注简短注释说明为什么那样子做。

不能在 /usr 目录下使用 %config 或者 %config(noreplace)。 在 openSUSE 中 /usr 是约定俗成的不放配置文件的。

开发 文件

如果被打包的程式包含只用于开发的文件,这些文件就应该被放到一个 -devel 子包里去。下面举例说明什么类型的文件应该放到 -devel 中去:

  • 头文件(例如,.h 文件)
  • 无版本号的共享库文件(例如,libfoo.so)。有版本号的共享库文件,例如,libfoo.so.3, libfoo.so.3.0.0 不应该被放到 -devel 中去。
  • pkgconfig 文件。一个特例是主包本身就是例如 gcc 或者 gdb 这样的纯开发工具,那就不用放到 -devel 子包。

包含 pkgconfig (.pc) 文件的软件包必须初始化 BuildRequires: pkg-config 以便于能够自动加上运行依赖 Requires: pkg-config

区域和语言 文件

openSUSE 包含了一个 RPM 宏变量叫做 %find_lang. 这个宏变量可以通过名称来定位所有属于你的软件包的区域和语言文件,并且把这个列表写进一个文件。你稍后可以用那个文件来导入它们到「文件」章节。%find_lang 应该在你的 spec 文件的 %install 章节运行,运行次序应该在所有的文件已经被安装到 %{buildroot} 之后。使用 %find_lang 可以使得 spec 文件更加简明,并且绕开了容易犯的打包错误。

  • 使用 %{_datadir}/* 来撷取所有的区域和语言文件到一行的方法的软件包也同时撷取了这些区域和语言文件夹的所有权,这是不允许的。
  • 大部分包含区域和语言文件的软件包都有好多该类文件。使用 %find_lang 可以不用去写诸如:
%{_datadir}/locale/ar/LC_MESSAGES/%{name}.mo
%{_datadir}/locale/be/LC_MESSAGES/%{name}.mo
%{_datadir}/locale/cs/LC_MESSAGES/%{name}.mo
%{_datadir}/locale/de/LC_MESSAGES/%{name}.mo
%{_datadir}/locale/es/LC_MESSAGES/%{name}.mo
...

这样的内容,打包变得更容易了。

  • 只要新的区域和语言文件出现在后续的软件包版本中,%find_lang会在运行时自动撷取到它们,使你不用每次都去更新 spec 文件。

非 ASCII 文件名

包含非 ASCII 字符的文件名必须编码为 UTF-8. 因为没有方法来识别文件名的编码,对所有文件名使用同一种编码是让用户可以正常读文件名的最好办法。如果上游提供了不使用 UTF-8 编码的文件名,你可以使用类似 convmv 软件包里的 convmv%install 部分转换文件名。

可执行库 文件夹 (Libexecdir)

文件系统层级标准 并没有提供任何「可执行库」文件夹, 但是 openSUSE 软件包可以在那里存放恰当的文件。可执行库文件夹 (在 openSUSE 上被重定义到/usr/lib) 可以作为设计目的是由「其他程序」而不是「用户」来执行的可执行文件的目录。

openSUSE rpm 包含了一个可执行库文件夹的宏变量, %{_libexecdir}。强烈建议打包者在一个软件包特定的子文件夹中存放可执行库文件,例如 %{_libexecdir}/%{name}

修订日志 (%changelog)

每次你修订了软件包,你必须添加一条修订日志。这对于不管是记录软件包历史,还是让用户、依赖你的包的打包者和「客服人员」能够轻松的定位到你做出的修订,都是蛮重要的。

OBS 使用一个单独的文件来记录软件包修订。这个文件像 spec 文件一样,但它的后缀是 .changes 而不是 .spec。(实际上最终编译发生时会自动使用一个 changes2spec 的工具把 .changes 写入到 .spec 的 %changelog 章节)

修订日志条目必须以时间顺序排列。新的修订日志条目在老的之上,因此最上面一条是最近期的一条。如果一个软件包已经在官方源里「注册」了那么就不允许再修正之前的修订日志条目了。

修订日志单行不允许超过 80 个字节,跟 %description 描述的规定相同,这是因为超出的部分在命令行显示时会被 overflow 略去。

在修订日志中的条目遵守如下结构:

-------------------------------------------------------------------
Tue Apr 22 20:54:26 UTC 2011 - your@email.com

- bullet point 1
  * bullet point 1.1 with long
    long long description
  * bullet point 1.2
- bullet point 2 with long long long
  description
  * bullet point 2.1

修订日志文件中的补丁应该遵守 补丁指南 (尤其是「缩写」部分,因为 OBS 会自动链接格式正确的 bug 缩写到 bugzilla 网址,便于 reviewer 审核者调阅)。

还需要注意这里的「修订日志」针对的是软件包的修订,而不仅仅是 spec 文件的修订。因此仅仅像 deb 软件包那样无节操的使用一句 update 1.0 是坚决不能允许的。

-------------------------------------------------------------------
Tue Apr 22 20:54:26 UTC 2011 - your@email.com

- updated version 2.0 (或者 initial package 1.0) // 上游修订日志
  * many bug fixes ┓
  * new feature 1  ┫-- (从软件包归档中的 ChangeLog/News 以及上游网站的
  * new feature 2  ┛   commit log/release notes 等信息中寻找)
- regenerated patch #1, #3 against latest source. -- 关于补丁
- fixed categories in desktop file. -- 关于其他 Source,没有可不写
- add Requires for %{name}-%{version} -- 关于 spec 的修订
- License changes to GPL-3.0+ -- 关于授权协议的修订,注明原因,没有可不写 
  * Upstream update

关于上游修订日志:

  • 可以抄,但不可以全盘复制粘贴,还需注意修订日志的格式。
  • 不能使用任何格式的差异(diff)文件,因为 OBS 可以直接以 diff 格式查看你的补丁和修订。
  • 要把握好尺度,不要像议论文一样搞得又臭又长,因为修订日志是你给用户的简短附言。上游的修订日志太长,你可以引导用户直接去看 NEWS/ChangeLog(这里有个潜在的共识就是我们认为用户应该知道一个软件的文档是放在 /usr/share/doc/packages/%{name}),例如:
- update version 2.0
  * details see ChangeLog 

或者是上游网站,例如:

- update version 2.0
  * details see https://is.gd/blabla
  • 通过 scm 方式如 svn/git/hg 获取的没有版本号的源代码,你需要多提一句为什么要从哪里获取源代码,比如updated from git, fixed bug #78708/API changed
  • openSUSE:Factory 源的软件包要求每次上游发布新版本都附加发行说明,上游没提供你也得提供,例如"update to 1.2.3: 无可用发行说明(no changelog available)"
  • openSUSE 是业内标准的典型。因为我们作为它的打包者有义务督促上游开发者养成维护 ChangeLog 的习惯,尤其是对于 Ubuntu 上的开发者,他们的打包教学有很大的问题,ChangeLog 教的比较模糊。造成了业内俗称的 Ubuntu dev never know what a changelog is。多见于 launchpad 上开发的软件。唯一例外是 NVIDIA 显卡这种私有软件,它们官方的打包者在里面写情感日记你都没有丝毫办法,实在没有请参考上一条「无可用发行说明」,但很遗憾现在是 NVIDIA 有,Ubuntu 的 hack kids(coolo 语)没有。

关于 spec 文件的修订:

  • 出现 spec 文件的修订大部分都是修复 openSUSE:Packaging_checks 文章里提到的 rpmlint 警告/错误,以及一些其他错误,因此你需要告知后续维护者和你的用户错在哪里,你修了什么。

最后,善意的提醒大家,.changes 是通过命令行 osc vc 命令或网页的 insert new changelog template 按钮来自动生成的,不要用 vi 创建文件然后自己画线哦。