为了维持软件包质量和一致性,一系列帮助抓出常见打包错误或在打包时强制满足一些策略的自动化检查默认会在所有使用 OBS 打包的软件包上执行。
以下将会涉及三种自动打包检查的方式:
由于 rpmlint 的灵活性,它成为了包检查的最优方式。如果有必要的话 SuSE 团队也会在 rpmlint 中加入新的检查代码或移植一些老的由别的方式执行的检查到 rpmlint 中。本文主要讲述 rpmlint 的打包检查。
rpmlint 可以辨认出三类结果:信息,警告和错误。不是每个错误都严重到会导致编译失败。因此会给一些错误加上权重分数。在编译时这些分数会累加。只有这些分数超过了某一设定的下限(10000分)时,编译才会失败。
当然 rpmlint 也不总是对的,所以忽略一些警告信息或调低错误的权重分数有时候是有必要的。软件包可以通过添加一个 rpmlintrc 来覆盖全域设置。
首先添加一个名为 %name-rpmlintrc 的文件到你 spec 文件的 source 部分。
例如:
[...] Source5: perl-rpmlintrc [...]
addFilter 命令可以用来配合正则表达式过滤警告信息:
addFilter("perl.* devel-file-in-non-devel-package")
本例过滤了所有包名带 perl 的包的名为 devel-file-in-non-devel-package 的警告提示。总的来讲,这种忽略最好写的越详细越好,只有在非常必要时才使用正则表达式。因此,你应该如下写来忽略掉一个特殊文件的警告提示:
addFilter("spurious-executable-perm .*/usr/share/doc/packages/cups/PrintAnalyzer")
总的来讲,你可以使用任何有效的 Perl/Python 正则表达式来过滤警告,它们会在 rpmlint 打印输出时直接匹配。
然而这种覆盖文件不会成为二进制包的一部分。因此手动在生成的 RPM 上运行 rpmlint 还是会警告那些已经被忽略掉的警告提示。这样做是故意的。如果任何系统范围的 rpmlint 警告应该被移除,请提交 bug。
有些打包检查,比如安全性检查会有一个非常高的错误权重分数,一旦发生就会导致编译中止。不是提交给 openSUSE Factory 的一些包可能需要把这些致命错误变成警告。可以使用 'setBadness' 命令来做。
例如允许在编译的软件包里包含一个未授权的权限文件,添加下面一行到 rpmlintrc:
setBadness('permissions-unauthorized-file', 0)
软件包安装了特定的依赖 CPU 架构的可执行二进制文件到 /usr/share 目录,该目录应该是被不依赖 CPU 架构的文件注册使用的。只可以在特定 CPU 架构上使用的二进制文件(比如编译好的 C 代码),应该放到 %{_libdir}/包名 目录。
某些包也有特例,如果安装路径就是指定了架构的,本检查会自动避免输出警告,例如/usr/share/tetex/bin/linux/x86/tetex。但是使用这种手段来规避检查是不推荐的。
代码在它的 shebang(#!blabla)中使用了错误的解释器。代码可以使用的解释器应该是一个打包好的解释器的完整路径,也就是说不能只指向 /usr/bin 而应该是/usr/bin/python 这样。
如果你标记了你的软件为 noarch(独立于操作系统架构),然后还在里面捆绑了一个只适用于特定架构的二进制包或库文件,就会触发此警告。解决办法是删除 spec 文件中的 BuildArch: noarch 字段,此错误常发生在新手打包 perl/python 的包时,那两类包有时会 rpmlint 提示让你添加 BuildArch: noarch,但实际上并不需要这么做。
软件包安装了库文件,却没有在 %post 和 %postun 字段调用 ldconfig。不更新 ldconfig 缓存,系统就不能正确的辨认出库文件。这是安装时很常见的错误,因为打包者事先并不知道会生成库文件因此可能忘记了在他们的 %post 代码片段中这么做。
%post -p /sbin/ldconfig %postun -p /sbin/ldconfig
文件中的某字段包含了编译根目录的完整路径。软件包中的文件不应该包含这样的打包专用信息。
一个常见的例子是你修复了 %{python_sitelib} 下面安装的 .py 文件的 shebang(这是另一个警告,安装在 %{_libdir} 中的文件不应该带有可执行代码,#!/usr/bin/python 表示这个文件是可以直接执行的,因此应该去掉这一行),然后会发生 python-bytecode-incosistent-mtime 警告,也就是说 python 编译器生成的供操作系统直接调用的预编译机器码文件 .pyc 和你改动过的 python 源文件 .py 的生成时间不一致,因为一个是作者写作的时间,一个是你编译的时间。这时就需要执行 %py_compile 来重新生成全部的 .pyc 文件。这时如果你只是这样执行:
%py_compile %{buildroot}%{python_sitelib}/%{name}/
就会把完整的 %{buildroot} 路径给输入到生成的 .pyc 文件中去,就会造成本警告。正确的做法是:
pushd %{buildroot}%{python_sitelib}/%{name}/ %py_compile ./ popd
这样就绕过了编译根目录 %{buildroot}。
软件包包含了冗余文件,这样会浪费安装空间和增大软件包体积。可以使用 %fdupes 宏变量来处理:
[...] # 这个包只有 openSUSE 才有,跨发行版的 spec 文件应该包含括号中内容 (%if 0%{?suse_version}) BuildRequires: fdupes (%endif) [...] %install blabla... %fdupes %{buildroot} %post -p /sbin/ldconfig # 一般写在 %install 章节的最末。 [...]
软件包的 Summary 的首字母应该大写,结尾不应该有句号 (.)。
在 %{_docdir} 的文档包含了可执行文件。文档不应该有可执行权限。
常见的例子是把某些软件的 examples 放到文档目录中去了。这样的 examples 应该放到 /usr/share/%{name}/。或者在 spec 文件的 %install 章节这样做:
chmod 644 %{buildroot}%{_docdir}/%{name}/*
看起来不像是可执行权限的文件比如 .txt 被赋予了可执行权限。这会在执行时发生错误,因此会产生警告提示。解决办法是仔细看一下文件的内容该不该是可执行的,不是的话用上面的 chmod 方法来修复它。
软件包包含了应该被放到 -devel 子软件包的文件。清理你的软件包并切分出一个 -devel 子包有如下两个好处:
一些原因会触发本警告:
多数时候,本错误是由于 libtool 的版本生成模块造成的。软件的插件应该在 LDFLAGS 中使用 -avoid-version 和 -module 参数来编译,这样也能避免触发不必要的本警告。
例如,如果你的某个共享库文件的唯一存在目的就是作为一个插件以供其他程序加载(这样就不应该被直接安装到 %{_libdir} 下面,而是它的一个子目录比如 %{_libdir}/%{name}/, 这个子目录也不应该被放到环境变量 /etc/ld.so.conf 的查找路径中去),那么你就应该检查用来编译该模块的 LDFLAGS,确保使用了 -module 和-avoid-version。然后还想明确插件的版本,那么应该在它的安装目录 %{_libdir}/%{name}-%{version}/ 上动手脚。
有些文件使用了 DOS 风格 (CRLF)的行末结束符。通常这是因为开发者使用 Windows 来写文档。在某些情况下,这会让文件查看程序变得不兼容。本错误的权重分值极低,因此你可以安全的忽略它。如果你愿意的话,你也可以转换这类文件到 Unix 风格的 LF 行末结束符。
你坚决不能在 iso 模式下使用 dos2unix,因为这不仅会变更 CRLF 到 LF,还会把编码从 CR437 改成 ISO-8859-1。
在 %prep 章节使用如下几个命令中的任何一个:
dos2unix -c ascii 文件
perl -i -pe 's/\r\n/\n/gs' 文件
sed -i 's/\r$//' 文件
sed -i 's/\r//' 文件
(这种方法判断的不精确,可能会删除前面没有 '\n' 的 '\r'。)(其他章节中做也可以,比如,只对 %install 章节的安装文件那么做,因为不安装的文件对用户没用。)
E: foo-package wrong-script-end-of-line-encoding /var/www/foo-package/plugins/foo.php
脚本使用了错误的行末结束符,这通常是由于脚本在非 Unix 系统上创建或修改过而造成的。这个错误会让脚本无法执行。解决方法是在 Linux 上重制脚本。不要在非 Unix 环境中创建文件,然后拿来为 Linux 系统来打包。
你的软件包包含了两个明显是硬链接的文件,这两个文件似乎处在不同的分区。安装这样的 RPM 可能会失败,因为 RPM 不能解压这样的跨分区的硬链接。一般来说,不要在 spec 文件中做跨越路径的前两个目录那样的链接,例如 /srv/ftp 和 /srv/www,或者 /etc 和 /usr,因为你并不知道最终使用它的用户是怎么分区的,那些目录可能不在一个硬盘分区上面。
另外该例子还常见于某些使用 %fdupes %{buildroot} 来清理冗余文件的软件包中。解决办法是删除两个相同内容的文件中的一个,然后使用软链接:
ln -sf %{_datadir}/%{name}/1.conf %{buildroot}%{sysconfdir}/%{name}/2.conf # 注意,软链接的前面一个路径中一定不能出现 %{buildroot},会导致编译失败。 # 因为 -f 是强制创建软链接,编译主程序 rpmbuild 无法检测你系统的 /usr/share 有没有那个文件; # 却是可以检测 %{buildroot} 中有没有的。系统检测不到,可以强制创建; 编译根目录检测不到,那就 # 叫错误了。
spec 文件的文件名必须和 spec 文件中的 Name 一样。
spec 文件包含了 "Packager" 标签。这是一个过时的标签。openSUSE 中的 spec 文件不应该包含它,这会让用户在报 bug 的时候很困扰,如果他是安装的他人重编译的你的包的时候。(RPM 会自动替换这个标签为那个人计算机上管理员名字和电子邮件。)
你软件包中包含的一个静态函数库没有 debug 信息,通常静态函数库都是没什么用的,使用 --disable-static configure 选项来禁用编译出静态函数库或者如果没有这样的选项可是在 %install 章节的 %make_install 之后使用 rm %{buildroot}%{_libdir}/*.a
来删除它。
你的函数库包不满足 SUSE library packaging policy 的命名要求。
有些文件本身并不在 RPM 包中,而是在包创建过程的安装环节中被动态的创建出来或者更常见的是在打包的自动测试运行时守护程序 deamon 运行时被动态的创建出来的 status 状态文件或 log 日志文件。它们理论上会在自动测试运行结束时的 rpm -e
里被删除掉。因此不必担心,你也不要把它写到 %files 章节里去。
启动脚本应该在它的 INIT INFO 章节里包含一个非空的 "Default-Start" 标签。可以参照 运行脚本指南 来添加上。
启动脚本使用了第 4 级运行级别,那是给管理员用。发行版的启动脚本不应该使用该级别。从 'Default-Start' 标签里面把 '4' 移除掉。
dbus 软件包过去使用了过于宽松的配置,会导致安全问题(CVE-2008-4311)。在调查这个问题的过程中,发现许多软件包的 dbus 配置都包含了无用的设置,一些会损失其他服务的安全性的设置,或在 dbus 安全更新之后会崩溃的设置。
解决方案:修复软件包的 dbus 配置文件。大多数情况下,配置文件可以精简为几行。详见https://bugs.launchpad.net/ubuntu/+source/avahi/+bug/318783
之所以会触发本警告,是因为 dbus 配置文件需要一行 <allow send_destination="org.foo.bar"/> 或类似的代码。如果缺失本行,当 dbus 软件包默认使用 拒绝 作为默认策略时软件的服务就不能运行。
Dbus 的 '拒绝' 指令必须指定一个 'send_destination' 参数否则传递给其他与本软件无关的服务的 dbus 信息也会被封禁。
解决方案:给相关的 dbus 配置加上 'send_destination' 参数。
Dbus 的 '允许' 指令必须指定一个 'send_destination' 参数否则传递给其他与本软件无关的服务的信息也会无意中被允许。
解决方案:给相关的 dbus 配置文件加上 'send_destination' 参数。
Dbus 配置文件包含了多余的 "allow receive_..." 指令。
解决方法:删除它。
rpmlint 输出: “The binary declares the stack as executable. Executable stack is usually an error as it is only needed if the code contains GCC trampolines or similar constructs which uses code on the stack. One common source for needlessly executable stack cases are object files built from assembler files which do not define a proper .note.GNU-stack section.”
解决方法:
osc build --debug
` 命令。rpmlint 输出: “The generic name is not in a filelist of package, add it to list marked as %ghost. Note: this error will be raised, if you use a hash ($) in file name, use rpm macros in spec file instead.”
解决方法:和下面的警告的方法一样。
rpmlint 输出: “The generic name is not marked as a ghost, which may cause a problems during update. Mark it as a %ghost in %files section.”
解决方法:通用文件 (比如某个 java 虚拟机包中指向的 /usr/bin/java 文件) 应该在文件列表中标记为 %ghost %{_bindir}/java 虚拟文件,这样升级或替代进程才不容易出错。某些实例中,RPM 在升级时会从文件系统中删除掉通用名而替换为 /usr/bin/java-1.7 这样的文件,这就会导致你的软件包中的链接失效。
警告:这个策略是针对 openSUSE 11.2 以上的版本。之前的 openSUSE 版本中的 RPM 是禁止一个通用名被多个包宣称占用的,那样在安装时就会出错根本轮不到升级。
zabbix 包的实例 (server:monitoring 源里):
%install ... # 虚拟文件必须存在于编译根目录中 touch %buildroot/%_sbindir/zabbix-server touch %buildroot/%_sbindir/zabbix-proxy
%post server-mysql %_sbindir/update-alternatives \ --install %_sbindir/zabbix-server \ zabbix-server \ %_sbindir/zabbix-server-mysql 11
%files server-mysql %defattr(-,root,root) %if 0%{?suse_version} >= 1120 %ghost %_sbindir/zabbix-server %endif %_sbindir/zabbix-server-mysql
注意在编译根目录里必须存在这个虚拟文件,可以使用 touch 命令来创建一个同名的空白符号。
E: foo-package no-binary
软件包应该被标记为独立于操作系统架构的,因为它没有任何二进制可执行文件。解决方法:在 spec 文件中添加 "BuildArchitectures: noarch
" 代码 — 除非软件包本身是一个 Python 软件包,这种情况下你应该忽略这个警告。
E: foo-package standard-dir-owned-by-package /usr/share
软件包宣称占用了一个标准目录比如 /usr/bin,这会导致该文件夹的访问权限或所有权被修改得不标准。因此不要在 %files 文件列表里使用 %dir 标签来宣称占用标准目录。
W: foo-package non-conffile-in-etc /etc/xdg/menus/applications-merged/foo-package.menu
一个不可执行文件被安装到了 /etc,但该文件并不是一个配置文件。所有在 /etc 目录中的不可执行文件都应该是配置文件。在 spec 文件的 %files 文件列表标记这个文件为 %config %{_sysconfdir}/%{name}.conf 或者 %config(noreplace) %{_sysconfdir}/%{name}.conf。
W: foo-package summary-ended-with-dot A content management system for foo package.
删掉 Summary 结尾的英文句号。
E: foo-package script-without-shebang /var/www/foo-package/plugins/foo.php
执行脚本没有 shebang(#!blabla..),因此执行时可能会出问题。通常,这意味着一个不是脚本的文件里有多余的可执行代码导致其被误认为脚本,但也可能真的是缺失 shebang。应该检查下文件看看到底是哪种情况,前者就把可执行代码删除,后者就添加上 #!/usr/bin/python 这样的 shebang:
sed -i "1i#!\/usr\/bin\/python" 文件
E: foo-package version-control-internal-file /var/www/foo-package/CVS/Entries
你安装了被软件作者的版本控制系统使用的文件,这些文件对软件包本身和用户都是没有意义的。删除它们。
E: foo-package zero-length /var/www/foo-package/foo.js
空文件。大多数情况下是作者无意中留下的,应该被删除。
W: foo-package non-standard-group Networking/Other
spec 文件中所使用的 Group 标签的值是没有被本发行版定义过的。这常见于从别的发行版捡来的 spec。在这里选个 openSUSE 定义过的 Group 值。
W: foo-package strange-permission foo-package.spec 0744
你软件包中的某个文件有着一个奇怪的权限。通常文件的权限应该是 0644 (rw-r--r--),文件夹的权限应该是 0755 (rwxr-xr-x)。
E: foo-package hardcoded-library-path in %{buildroot}/usr/lib/menu/
库文件的路径使用了硬编码的 /lib, /usr/lib。替换它们为 /%_lib 或者 %_libdir。
E: suse-filelist-forbidden-noarch /usr/lib64/aspell-0.60/english-huge.multi is not allowed in a noarch package
软件包本身被标记为操作系统架构独立的,然后安装路径确是只适用于某些架构的。要么把文件移动到操作系统架构独立的目录中去,要么删除软件包的 BuildArch: noarch 标签。
W: incoherent-init-script-name haldaemon ('hal', 'hald')
启动脚本应该是软件包名字的小写,或者也可以在后面多上一个 'd' 表明它是一个进程。根据 rpmlint 的建议重命名它
这个错误实际上不应该在编译服务中出现。编译脚本会根据源的全局设置自动的从二进制文件或函数库文件中剥离出调试信息 debuginfo。在自动剥离时遗漏了文件意味着这是一个 bug,因此请报 bug 而不是手动的
%{__strip} %{buildroot}%{_libdir}/%{name}.so
这样会导致自动化的 debuginfo 子包创建出现问题。(当然,不出问题还是可以做的,:P)
gethostbyname*() 和 gethostbyaddr*() 函数因为它们不能作用于 IPv6 所以和其他一些东西一样都过时了。程序应该使用 getaddrinfo(3) 和 getnameinfo(3) 替换它们。因此请和软件开发的上游协调来移植程序到当代接口上。 Ulrich Drepper 更详细的解释了这个问题。
W: file-not-in-%lang /usr/share/sarg/sarg-php/locale/en_EN/LC_MESSAGES/messages.mo
gettext 翻译文件(以 .mo 结尾) 在 %files 章节没有以 %lang 前缀标记。未来想要在安装过程中去除特定语言时该信息会很有用。%find_lang 宏可以自动的标记翻译文件。
%install [...] %find_lang %{name} %no_lang_C # %no_lang_C 不包含一般情况下的默认语言,英语 %fdupes %{buildroot} %post [...] %files -f %{name}.lang
另外值得注意的是在 cmake 系统中,%install 安装的第一条命令会是
cd build
在执行 %find_lang 时请退回到编译根目录 %build
cd ../
否则下面的 %files 行会提示找不到 %{name}.lang,因为它们被创建到了 build 文件夹,或者使用:
%files -f ./build/%{name}.lang
来做,但是这种方法不推荐。
我们经常碰到翻译 bug 因为 .desktop 文件没有被 SUSE 翻译过。你最好对每个 .desktop 文件都运行 %suse_update_desktop_file。这样会自动为你的软件包添加多语言翻译,如果没有这样的翻译会自动发送翻译请求给 openSUSE 翻译家们和 SuSE 的翻译团队,要是你自己搞了,其他的软件包可能还会遇到同样的问题。
BuildRequires: update-desktop-files [...] %install %make_install [...] %suse_update_desktop_file %{name} %suse_update_desktop_file %{buildroot}%{_kde4_applicationsdir}/%{name}-config.desktop
注意如果是 KDE3 或者 KDE4 的包,触发该警告还可能是没有在 %install 章节末调用 %kde_post_install 宏。
包含了 /etc/init.d 脚本的软件包没有在 %preun 代码片段中调用 %stop_on_removal。参考 打包常用的 RPM 宏
包含了 /etc/init.d 脚本的软件包没有在 %postun 代码片段中调用 %insserv_cleanup 宏。参考 打包中常用的 RPM 宏
软件包中的 %pre, %post, %preun 或 %postun 代码片段使用了外部的库或可执行文件却没有明确的通过 Requires(pre):, Requires(post):, Requires: 或 Requires: 来在 spec 开头宣称需要它们。添加上这些标签。
这样会让软件包安装/卸载过程的次序更加的通顺。比如,在 %post 执行时需要的包会在本软件包被安装前就装上。
/var/run 在新式发行版上是作为一个缓存文件系统被挂载的,它里面的文件是一次性的,因此安装文件到该目录很不科学。
解决方法:将这类文件在 spec 文件中标记为 %ghost 虚拟文件,只在运行时创建它。例如,在启动脚本中 touch 它,或者如果软件包没有启动脚本也可以通过在 /usr/lib/tmpfiles.d 目录下面放脚本来做,这些脚本会在运行时创建相关文件。
/var/lock 在新式发行版上是作为缓存文件夹被挂载的,它里面的文件是一次性的,所以安装文件到该目录是不科学的。另外,该目录是被设计成供老式的受 lockdev 管理的设备锁定文件 (例如 LCK..ttyS0) 使用的。不要将它用在其他目的上。
/var/lock/subsys 在 openSUSE 中没用,因此也不被支持。其他发行版可能使用它来标记启动脚本的运行状态。在 openSUSE 中也没有替代品。因此多数情况下指向 /var/lock/subsys/ 的文件或链接都可以被移除掉。如果启动脚本确实需要保存一些它的状态信息请使用例如 /var/run/rc脚本名 这样的目录。为什么不使用 /var/lock 的原因见上面。
按照发行版设定的策略,有些可执行二进制文件,比如重要的联网程序,用户和组策略设定工具,应该以位置独立可执行文件(PIE)的方式被编译。PIE 文件的启动路径是随机的,这样黑客就更难破解它们。添加 -fPIE 和 -pie 参数到相应的二进制文件的编译变量 CFLAGS 或 LDFALGS 中。
例如 util-linux 添加了如下代码到 Makefile.am 中来解决这个问题:
write_CFLAGS = $(SUID_CFLAGS) $(AM_CFLAGS) write_LDFLAGS = $(SUID_LDFLAGS) $(AM_LDFLAGS)
然后在 spec 文件中使用
export SUID_CFLAGS=-fPIE export SUID_LDFLAGS=-pie %configure ...
软件包允许普通用户不经授权操作就执行需要特权的系统操作。如果没做好会导致安全问题。如果软件包准备在任何 SuSE 产品中这么做请提交个 bug 报告让安全团队来判断是否要给予你的软件包这种权限。学姐就 recorditnow 提交过这样的报告,通过的很快。
软件包安装了 dbus 系统服务文件。由于由普通用户运行可能导致安全问题,dbus 服务通常是由根用户 root 来运行的。所以如果软件包准备在任何 SuSE 产品上这么做请提交一个 bug 报告来让安全团队判断是否要给与你的软件包这种权限,审核的很快。
软件包没有变更日志,请添加 %changelog 章节到 spec 文件的结尾,然后使用 osc vc 来创建一个 .changes 文件。
/etc/logrotate.d/* 中的文件里面使用的目录是可以由普通用户和组来写入的。由于 logrotate 是由根用户 root 运行的,一个被破解的用户账户可以在 logrotate 试图生成 log 日志文件时做坏事(例如,通过软链接, CVE-2011-1155)。完美的修复是只允许根用户 root 有日志文件夹的写入权限(例如 0755 root:root),其他用户可以拥有日志文件。在这种情况下 logrotate 的创建选项会保证生成的日志文件的正确权限。不管出于什么原因服务需要不同的目录所有权,你都可以在 logrotate 配置文件中使用 'su' 选项来做(详细参考 man logrotate)。但这种方法应该是在设定合适的文件夹权限不可能的情况下作为最后的手段而不是一上来就那么干。
/etc/logrotate.d/* 的文件里提到的目录,没有包含在软件包的 %files 文件列表章节。因此 rpmlint therefore 不能检查该目录的权限设置是否安全。因此请添加那个目录到 %files 文件列表。