开源软件源码编译指南

之前在博客介绍了 Linux From Scratch, 最近发现 LFS 已经有新的版本 10.1 了,周末打算重新编译一下。

LFS 在首页推荐了两个教程,可以学习编译软件的一些基础常识,一个是 TLDP 的 Building and Installing Software Packages for Linux另一个是 Beginner’s Guide to Installing from Source,相当于是 TLDP 的简略版。边学习边记了一些笔记和总结。那这篇博客,就相当于是简略版的简略版吧。

软件的开发和发行

一个操作系统除了内核,还需要成千上万的软件才能真正去完成一些工作。专有系统一般会帮你管理这些软件,开源系统的软件则是由世界各地的开发者贡献的。这些软件通常不仅可以在 Linux 上使用,只要版权允许,也可以在 BSD 等系统使用,如果版权允许,有些还可以在专有系统运行。比如 Mac, Solaris 系统等。

开源软件发行中有两个常见的术语:

  • 上游(Upstream): 通常指的是软件的原作者;
  • 下游(Downstream): 通常指的是发行版,和用户;

大部分的开源软件都会开放一个版本控制系统管理的代码,任何人可以匿名下载代码,比如从 Github clone. 使用版本管理软件下载代码包含软件的开发历史(commit),可能会比较慢,软件在发布(release)的时候通常会将代码打包提供下载,这样下载就不包含代码提交历史,所以下载会更快一些,也能给世界节省一些带宽,减缓全球气候变暖。也有一些软件没有开放版本控制,只有定期打包开放的代码,这种就只能下载不带历史的源代码了。

发行版,Binary,和编译

Linux (以及其他的操作系统)有很多的发行版,这些发行版所帮你做的一个重要的事情就是给流行的软件打包。让你可以直接从 apt/dnf 等 package manager 下载安装软件。这些 package manager 有些是直接分发 binary 的,这意味着你在安装的时候直接下载下来放到 $PATH 下面就好了,并不需要自己编译。

Package manager 带来的好处是:

  • 方便:要找什么软件直接从它们提供的 repository 下载就好了;
  • 安全:package manager 在下载的时候会自动校验 md5,发行版的开发者也会时刻关注安全问题,所维护的打包软件会及时地做安全更新;

但是另一方面,发行版要维持一定的稳定性。比如你在特定版本的 Ubuntu 上,从官方的 Repository 就只能安装到特定版本的 Python,而不能安装最新的。(当然也有发行版的策略是永远提供最新版的软件。)

在这种情况下如果你想使用最新版的软件,就需要自己编译了。

另一种可能需要自己编译软件的情况是定制。一般开源软件会提供一些参数,比如删掉不必要的功能。这些定制都是要在编译阶段配置的,发行版要兼顾大部分的用户,所以一般都会尽量打开所有的 Feature,如果你需要特定的配置,就需要自己编译了。

自己编译软件的时候也要注意不要搞乱了你的 package manager, 后面可能出现意想不到的错误。最好的方法是阅读对应的 package specification 然后使用 package manager 来编译和安装软件。

源码下载和安全

一般的软件会提供 http 服务或者 ftp 服务让大家下载。大部分人应该都会从网上下载东西,在浏览器里面使用右键,另存为,就可以了。在终端上可以使用 wget 程序或者 curl 来下载。

除了从官网下载之外,一般还会有很多 mirror,其实就是散步在世界各地的备份。这样一来可以备份软件,而来可以让用户就近下载,节省原服务器的带宽,也节省整个世界的带宽。

tarball

之前介绍过文件系统。可以知道,不同的文件系统对于目录(文件夹)和文件的组织方式并不相同。所以在互联网上传输的一般是打包成一个的文件而不是文件夹。

文件打包说白了就是将所有的文件 append 到一个文件中,然后创建一个 “index”,里面包含了所有的文件信息,比如每个文件的名字,offset,length,index 等等,方便不必解包就可以知道里面都有什么,甚至可以从中提取出来某一个文件而不必完全解包。

tar 程序是最常见的打包程序(参考之前的这篇博客)。名字的意思是 “tape archive”,虽然现在文件已经都不存储在 tape 上,而是 disk 上了…… 用 tar 打包的文件通常叫做 tarball. 用 tar 打包可以保留操作系统的权限信息,以及文件的 owner id 等信息。

因为源代码都是文本,压缩比率非常高,很适合压缩之后传输。tar 仅仅是打包,一般打包之后还会进行压缩。常见的压缩有:

  • gzip: 文件后缀是 .tar.gz 表示是一个使用 gzip 压缩之后的 tarball
  • bzip2: 后缀一般是 .tar.bz2
  • xz: 后缀是 .tar.xz

tar 现在的版本已经非常智能了,可以自动识别文件然后调用对应的解压工具进行解压,比如这么使用:

但是 tar 是一个非常古老的程序了,是否支持自动解压取决于版本,参数的使用也取决于版本。本着 Unix 的哲学:一个工具只做一件事情。我比较喜欢这么用,解压交给解压工具,解包交给解包工具:

tar 在解压的时候,如果在当前目录存在重名文件,会直接覆盖掉。一般在软件打包的时候,会将所有的内容放到一个目录内,这样你解包的时候所有的内容也会都在一个新的目录内。但不排除有些软件在打包的时候没有这么做,所以盲目地解压网上下载回来的软件可能搞乱你的当前目录。有两种方法可以避免:

一个在解压之前先看看里面都有些啥:

另一个方法是创建一个新的目录,在里面解压:

解压的时候最好不要盲目地使用这些参数,这些参数可以改变解压的目标目录,很可能覆盖你的系统上的文件:

Windows 和 DOS 上经常用的一种压缩格式是 zip. 可以理解为 zip 是 tar + gzip. zip 不会保留文件系统的权限信息。对应的解压工具是 unzip.

即使使用 tar 解压,如果要保留文件的 owner 信息的话,需要使用 root 用户解压才行。但是有这种需求的情况非常少。非常不推荐使用 root 解包文件!这样可能覆盖重要的系统文件。

需要注意的是,http 服务和 ftp 服务都可能被劫持(即使使用 https 也有被劫持的可能),mirror 上存放的软件可能被篡改。所以下载回来的软件在安装之前一定要验证!

使用 md5sum 校验文件

大部分的文件下载都会提供一个 md5 校验码,我们将文件下载回来之后,只要将文件计算一个 md5sum,然后和官方提供的 md5 对比就可以了。注意这个 md5sum 必须从官方渠道获得,不能从 mirror 获得,否则就没有意义了。

以 Python 为例,在这个下载地址( https://www.python.org/downloads/release/python-395/ ) 有提供 md5 码:

下载这个 XZ compressed source tarball 然后校验:

md5 一样,就可以认为文件是和官方完全一致的。

有些软件要下载多个文件,会提供一个 checksum-file, 可以使用这个命令,会自动根据 checksum-file 里面的文件逐个检查 md5:

使用 GPG key 校验文件

有些软件是使用 GPG key 签名来证明软件来源。作者用自己的 key 对文件签名,然后将文件与签名文件一同发布,用户需要下载四个东西:

  1. 作者的 public key
  2. 作者的 gpg key 指纹,证明 public 的来源真实
  3. 软件包
  4. 软件包的签名

校验的步骤是:

  1. 导入作者的 public key
  2. 检查 public key,确认指纹,然后信任此 key
  3. 通过软件包的签名来验证软件包

这里依然以上面的 Python 源码包为例,来通过 gpg key 进行校验。

第一步,找到作者的 public key。在 https://www.python.org/downloads/ 可以看到,Python 3.8 和 3.9 的 Release Manager 是 Łukasz,我们根据这个 key 的地址将这个 key 下载下来。

Python 官方开发者的 gpg key



然后确认这个 key 的指纹,与作者公布的指纹做对比。发现一致,即可信任。

Łukasz 公布在 keybase 上的 GPG public key



或者可以直接通过指纹 sign 这个 key,比上面的步骤要简单一些,是等效的。

最后,就可以下载代码以及 .asc 后缀的签名文件了。

阅读文档!

源码文件在解压之后,一般在最上层的目录下会有 README 或者 INSTALL 类似的文件,这是安装的文档说明。解压之后需要做的第一件事就是阅读这些文件。

文档中会有两个地方比较重要:

  1. 编译和安装这个软件需要的依赖,如果没有依赖就安装这个软件,要么编译有问题,要么会运行有问题;
  2. 软件编译的时候接收的一些参数;

Patching

某些软件可能在一些环境下无法正常工作,如果软件流行的话,在网上搜一下解决办法,一般可以找到一些人发布的解决办法。通过修改一些参数或者代码就可以了。叫做 Patch。

Patch 有两种:

  1. 修改脚本:比如 sed/awk 脚本,运行这些脚本可以修改源代码。例子
  2. Patch 文件:diff 的形式,人类可读。需要使用 Patch 程序去“应用”这个 diff,即“让这个 diff 生效” 例子

常见的编译系统

简单点说,编译就是将下载回来的代码变成可以在操作系统执行的 binary 文件。所以分成两步:

  1. 编译(make)
  2. 将编译好的文件放到系统的 $PATH 下面,这样可以在任意路径执行软件 (make install)

作者会将编译所用到的一些文件都放在 tarball 里面。同时,作者会希望有更多的人使用他的软件(谁不想呢?),所以会尽量让编译过程变简单一些。大部分的软件会使用 make 软件作为编译的工具。

这里不展开讲 make 了,简单点说,你提供给 make 一个 makefile,里面按照 make 的格式告诉 make:“如果目标文件不是基于最新的源码,就执行这个动作”,其中,目标文件可以是最终的软件,也可以是中间的产物。make 会自动帮你解决好先后顺序问题。

然而,make 的描述能力有限。但是想要处理起来所有的配置项,也捉襟见肘。所以这里又出现了另外一个工具:configure. 通常,软件包不会直接包含一个 makefile,而是会包含一个 configure 脚本以及一个 makefile.in 模板文件。执行 ./configure 就可以生成一个 makefile 文件,然后再执行 make.

所以,一般的编译流程如下:

另外,configure 也不是手写的,是由另一叫做 autotools 的 GNU 软件生成的。但是这个不需要用户使用,而是由软件作者来生成好。

最后一步 make install 的作用是将编译好的 binary 复制到系统的 $PATH 下。除了这一步以外,其他的步骤最好都使用普通用户来执行。

总体的流程如下:

有些软件没有使用 configure 脚本。因为它们足够简单,将 configure 的逻辑直接写到 makefile 里面了。比如 Redis,就没有。安装的时候只要 make 就可以了。

configuremake 生成的产物可能会和源代码文件混合在一起,弄得比较混乱,可以使用 make clean 清理编译产物。

但是对于每次编译,都在一个临时文件夹里面进行,或许是一个更好的办法。

其他的一些编译方式

cmake 现在也比较流行了,cmakeconfigure 类似,会基于传入的参数生成一个 makefile。除了使用下面这个命令替换 ./configure 之外,其他的步骤基本一样。

再次提醒一下,编译软件之前先看文档是一个好习惯。

有一些软件使用基于 Python/Perl 的编译工具,这些具体就参考文档好了。

还有一些工具直接是脚本语言编写的,这样的软件不需要编译,只要将软件拷贝到解释器读取的目录即可。不过一般这些语言都会自带一些包管理工具。

环境变量

configure 接受的配置项可以通过两种方式指定。

一种是环境变量。

或者直接 export 到当前的 shell 中然后执行。

另一种是通过执行 configure 时候的命令行参数。具体支持的参数,可以通过 ./configure --help 来查看。

编译安装文档

软件附带的文档没有统一的形式,常见的一些有:

  • 提供在线的 HTML 形式的文档;
  • 提供标准的 archive file, 可以直接下载;
  • 单独提供文档下载,可以通过 man/info 等查看;
  • make installdoc 安装
  • ……

总之还是参考文档来安装文档吧!

错误排查

编译是会很容易出错的一个过程。发生错误的时候一般根据编译错误一步一步排查就好了。要点是不要乱猜,要根据事实去推测。

举个最近遇到的问题。在一台机器上编译 Python3 失败了。./configure 的错误提示如下:

根据提示去查看 config.log 日志文件,在文件中发现这么一段:

可以知道是 gcc 尝试测试编译一段简单的程序失败了。失败原因是 ld 不支持 -p 参数。

然后查看机器上的 ld 为什么不支持。

通过这几个命令可以看到这个机器上有很多个 ld ,而使用的 ldelfutils 里面的 ld,并不是 GNU 的 ld。编译的时候将 /usr/bin 这个路径提前就好了。

大部分错误的情况是缺失了一些依赖,喜欢走弯路的话可以去网上搜索缺失的文件的名字,运气好找到缺失的依赖安装了就好了,不喜欢走弯路的话可以参考安装文档。也有时候缺失的依赖是可选的,可以通过 ./configure 的参数忽略掉。

编译器一般有一些高级的参数可以优化性能,但是如果不是很了解自己在做啥的话,最好还是不要碰。(既然你还在读这篇文章,还是不要碰了吧。)

编译出来的文件一般会带有一些 debug 参考的信息,一般用户用不着,可以通过 strip 命令来剪掉。

这是在 Mac 系统上编译出来的 Python3.9,strip 之后少了0.8M.

不过今天的硬盘已经这么不值钱了,也没什么必要其实。

最后有些软件可能在安装之后需要一些特定的配置才能运行。还是那句话,读文档。

练习

看到这里,编译下来 LFS 基本就没什么问题了,可以按照 LFS 从头开始编译一些 Linux 的工具试试,这里列出了一个能工作的 Linux 需要的基本软件。



开源软件源码编译指南”已经有5条评论

    • 嗯,应该是可以。但其实 LFS 是本书,意义就是体验手动编译的过程,学习 Linux. 要是自动化编译的话就没有什么意义了……可以直接选择其他的发行版。如果 Dockerfile 只是为了一次性的编译,那成本有些大……

      • 其实可以考虑gentoo

        Gentoo也是和lfs一样可以tweak flags和features,但是它的portage帮你standardlize了这些,装软件就和正常apt一样简单

        我现在就天天用gentoo

        • 同意。LFS 主要的意义是完全从头开始编译一个 Linux,包括编译的工具链也是需要从新编译来自举的。所以学习的功能更多一些。要是自己使用的话,我不会选择 LFS 的。

Leave a comment

您的电子邮箱地址不会被公开。 必填项已用 * 标注