
# 构建 PHP

本章将说明如何以一种适合开发扩展或者内核修改的方式编译 PHP。我们将仅介绍 Unixoid 系统的构建。如果你希望在 Windows 构建 PHP，你可以在 PHP 维基上看下这个[逐步构建说明](https://wiki.php.net/internals/windows/stepbystepbuild)。

本章也概述了 PHP 构建系统的工作方式和工具使用，但是详细的说明不在本书的范围之内。

免责声明：我们对因尝试在 Windows 编译 PHP 而造成不利健康的影响概不负责。 

## 为什么不使用软件包？

如果您目前正在使用 PHP ，则可能使用 `sudo apt-get install php` 之类的命令通过软件包管理器进行了安装。在解释实际的编译之前，您应该首先理解为什么自己编译是必要的，而不仅仅是使用预编译的程序包。原因有很多：

首先，预构建包只包含生成的二进制文件，但缺少编译扩展所必需的其他东西，例如头文件。这可以通过安装开发包来轻松解决，这个开发包通常被称为 `php-dev`。为了便于使用 valgrind 或 gdb 进行调试，可以另外安装调试符号，这些符号通常作为另一个名为 `php-dbg` 的软件包提供。

但是，即使您安装标头和调试符号，您仍将使用PHP的发行版。这意味着它将以较高的优化级别构建，这会使调试变得非常困难。此外，发行版本不会生成有关内存泄漏或数据结构不一致的警告。此外，预构建的包不支持线程安全，这在开发过程中非常有帮助。

另一个问题是几乎所有的发行版都会向PHP应用额外的补丁。在某些情况下，这些补丁只包含与配置相关的微小更改，但有些发行版使用像 Suhosin 这样的高侵入性补丁。已知其中一些补丁会引入与低级扩展(例如 opcache )的不兼容性。

PHP 仅提供对[php.net](http://www.php.net/)上提供的软件的支持，不对发行版修改的版本提供支持。如果要报告错误，提交补丁或利用我们的帮助渠道进行扩展编写，则应始终对照官方的PHP版本进行工作。当我们在本书中谈论「PHP」时，我们总是指受官方支持的版本。

## 获取源代码

在构建 PHP 之前，你必须先获得源代码。有两种方式可以获取：一种是从 [PHP 下载页面](http://www.php.net/downloads.php) 下载，一种是从 [Git 仓库](http://git.php.net/) 克隆（或者 [Github](http://www.github.com/php/php-src)的镜像）。

这两种情况下构建 PHP 的过程有些许差异：Git 仓库未捆绑 `configure` 脚本，所以你需要使用 `buildconf` 脚本来生成自动配置。此外，Git 仓库不包含预生成解析器，所以你还需要安装Bison。

我们推荐你从 Git 上检出源代码，因为这样方便安装更新和尝试不同版本的代码。如果你想要提交修改或者拉取 PHP 的请求，Git 同样需要检出。

要克隆仓库，在你的Shell中运行一下命令：

```
~> git clone http://git.php.net/repository/php-src.git
~> cd php-src
# 默认情况下是在master分支上
# 开发版本。你可以改为检出稳定分支：
~/php-src> git checkout PHP-7.0

```

如果你对 Git 检出有疑问，看下 PHP 维基上 [Git 常见问题](https://wiki.php.net/vcs/gitfaq)。Git 常见问题也说明了如果你想要为 PHP 本身做贡献的话，如何设置 Git。此外，它包含为不同 PHP 版本设置多种工作目录的说明。如果你需要测试扩展或更改多种 PHP 版本和配置的话，这对你非常有用。



在继续之前，你应该用你的包管理下载了一些基础构建依赖库（你可能已经默认安装了前三个）：

-   `gcc` 或者其它的编译套件。
-   `libc-dev`，提供 C 的标准库，包含头文件。
-   `make`，这是 PHP 使用的构建管理工具。
-   `autoconf`，用于生成 `configure` 脚本。
    -   2.59或更高版本（对于 PHP 7.0-7.1）
    -   2.64或更高版本（对于 PHP 7.2）
    -   2.68或更高版本（对于 PHP 7.3）
-   `libtool`，帮助管理共享库。
-   `bison`，用于生成 PHP 解析器。
    -   2.4或更高版本（对于 PHP 7.0-7.3）
    -   3.0或更高版本（对于 PHP 7.4）
-   `re2c`，用于生成 PHP 词法解析器。当从 Git 仓库构建 PHP 时，re2c 词法生成器曾是可选的依赖项。在 PHP > 7.3 分支上，Git 仓库不再捆绑生成词法分析器文件。

在 Debian/Ubuntu 上，你可以使用以下命令安装所有这些文件：

```
~/php-src> sudo apt-get install build-essential autoconf libtool bison re2c

```

根据你在 `./configure` 阶段启用的扩展， PHP 需要很多额外的库。当安装这些，请检查软件包版本是否以 `-dev` 或者 `-devel` 结尾，然后安装它们。没有 `dev` 的包通常不包含必要的头文件。例如，默认的 PHP 构建会需要libxml，你可以通过 `libxml2-dev` 软件包进行安装。

如果你使用 Debian 或者 Ubuntu，你可以使用 `sudo apt-get build-dep php7`一次性安装大量的可选构建依赖项。如果你只是默认构建，这其中的很多都是不需要考虑的。



## 构建概述

在仔细研究各个构建步骤前，需要你执行这里的“默认” PHP 构建命令：

```
~/php-src> ./buildconf     # only necessary if building from git
~/php-src> ./configure
~/php-src> make -jN

```

为了快速构建，请用可用的 CPU 内核数替换 `N` （请见 `grep "cpu cores" /proc/cpuinfo`）。

默认 PHP 构建将会为 CLI 和 CGI SAPI 构建二进制文件，它们分别位于 `sapi/cli/php` 和 `sapi/cgi/php-cgi` 中。若要检查一切是否正常，可尝试运行 `sapi/cli/php -v`。

另外你可以运行 `sudo make install` 安装 PHP 到 `/usr/local`。在配置阶段，目标路径可以通过指定的 `--prefix` 更改：

```
~/php-src> ./configure --prefix=$HOME/myphp
~/php-src> make -jN
~/php-src> make install

```

这里 `$HOME/myphp` 是将在 `make install` 步骤中使用到的安装位置。注意不必安装 PHP，但是如果你想要在扩展开发之外使用 PHP 构建，则会更方便。

现在，让我们仔细看看各个构建步骤！

##  `./buildconf` 脚本

如果你从 Git 仓库构建，第一件事就是运行 `./buildconf` 脚本。这个脚本除了调用 `build/build.mk` 文件之外没有什么作用，而该文件又调用了 `build/build2.mk`。

这些生成文件的主要工作是运行 `autoconf` 生成 `./configure` 脚本和 `autoheader` 生成 `main/php_config.h.in` 模板。后一个文件将会被 configure 生成最终配置头文件 `main/php_config.h`。

 这两个实用程序均从 `configure.in` 文件（指定大多数的 PHP 构建过程）， `acinclude.m4` 文件（指定大量特定于PHP 的M4宏）和单个扩展名和 SAPI  的 `config.m4` 文件（以及一堆其它 [m4 文件](http://www.gnu.org/software/m4/m4.html)）生成的。

好消息是编写扩展甚至修改内核都不需要与构建系统进行太多交互。而在这之后，你必须编写小的 `config.m4` 文件，但是这些文件通常仅使用 `acinclude.m4` 提供的两或三个高级宏。因此，我们不在这里做进一步详细介绍。

`./buildconf` 脚本只有两个选项： `--debug`， 当你调用 autoconf 和 autoheader 时会禁用警告抑制。除非你想要在构建系统上工作，否则你对这个选项没什么兴趣。

第二个选项是 `--force`，在发行包中将会允许运行 `./buildconf`（例如，如果你下载了打包的源代码，并生成一个新的 `./configure` 文件）并另外清除配置缓存 `config.cache` 和 `autom4te.cache/`。

如果你使用 `git pull` （或其他一些命令）更新你的 Git 仓库，并且在 `make` 步骤中出现奇怪的错误，这通常意味着在构建配置中某些东西已更改，你需要运行  `./buildconf --force`。



##  `./configure` 脚本

一旦生成 `./configure` 脚本，你便可以使用它去定制你的 PHP 构建。你可以使用 `--help` 列出所有已支持的选项： 

```
~/php-src> ./configure --help | less

```

帮助的第一部分会列出各种通用选项，所有基于 autoconf 的配置脚本均支持这些选项。 其中一个便是已经提到过的 `--prefix=DIR` ，它更改了 `make install` 的安装路径。另一个有用的选项是 `-C`， 它在 `config.cache` 文件中缓存了各种测试结果并加快了后面的 `./configure` 调用。仅当你已经具有可用的构建并且想要在不同配置之间快速更改时，这个选项才有用。

除了通用的 autoconf 选项之外，PHP 也有一些特定的设置。例如，你可以选择使用  `--enable-NAME` 和 `--disable-NAME` 开关来选择应编译的扩展和 SAPI。如果扩展或 SAPI 有外部依赖，你必须使用 `--with-NAME` 和 `--without-NAME` 代替。如果 `NAME` 所需要的库不在默认位置（例如，因为你自己编译），你可以使用 `--with-NAME=DIR` 指定其位置。

PHP 会默认构建 CLI 和 CGI SAPI，以及许多扩展。你可以使用 `-m` 选项查出你的 PHP 库包含了哪些扩展。对于默认的 PHP 7.0构建，结果将如下所示：

```
~/php-src> sapi/cli/php -m
[PHP Modules]
Core
ctype
date
dom
fileinfo
filter
hash
iconv
json
libxml
pcre
PDO
pdo_sqlite
Phar
posix
Reflection
session
SimpleXML
SPL
sqlite3
standard
tokenizer
xml
xmlreader
xmlwriter

```

如果你现在想要停止编译 CGI SAPI，以及 *tokenizer* 和 *sqlite3* 扩展，启用 *opcache* 和 *gmp*，相应的 configure 命令将是：

```
~/php-src> ./configure --disable-cgi --disable-tokenizer --without-sqlite3
                       --enable-opcache --with-gmp

```

默认情况下，大多数的扩展都是静态编译的，即它们将成为生成的二进制文件的一部分。默认只有 opcache 扩展共享，即它将在 `modules/` 目录生成一个 `opcache.so` 共享对象 。你可以通过 `--enable-NAME=shared` 或者 `--with-NAME=shared` 将其他扩展编译成共享对象（但不是所有的扩展支持这个）。我们将在下一节讨论如何利用共享扩展。



了解你需要使用哪个开关和是否默认启用扩展，请检查 `./configure --help` 。如果开关是 `--enable-NAME` 或 `--with-NAME` ，则该扩展默认不编译，需要显式启用它。另一方面 `--disable-NAME` 或 `--without-NAME` 表明该扩展默认情况下已编译，但可以显式禁用。

一些扩展总是会被编译并启用。使用 `--disable-all` 选项，则会创建一个包含最少扩展的构建：

```
~/php-src> ./configure --disable-all && make -jN
~/php-src> sapi/cli/php -m
[PHP Modules]
Core
date
pcre
Reflection
SPL
standard

```

如果你想要快速构建并且不需要很多功能（例如，实现语言更改）时，`--disable-all` 选项非常有用。对于尽可能最小的构建来说，你可以另外使用 `--disable-cgi`开关，这仅生成 CLI 二进制文件。

还有两个开关，在开发扩展或使用 PHP 时，你应 **始终** 指明：

`--enable-debug` 启用调试模式，它有多种作用：编译将以 `-g` 运行生成调试符号，且使用最低优化级别 `-O0`。这将使 PHP 变得很慢，但是使用 `gdb` 之类的工具使调试变得更加可预测。另外调试模式定义了 `ZEND_DEBUG` 宏，它将在引擎中启用各种调试助手。除其他事外，还将报告内存泄露以及一些数据结构的不正确使用。

`--enable-maintainer-zts` 启用线程安全。该开关将定义 `ZTS` 宏， 这将启用 PHP 使用的整个 TSRM （线程安全资源管理）机制。PHP 的线程安全编写非常简单，但是前提是确保启用了该开关。如果你需要更多关于 PHP 线程安全和全局内存管理的信息，可阅读 [全局管理章节](http://www.phpinternalsbook.com/php7/extensions_design/globals_management.html)



另一方面，如果你想要为你的代码执行性能基准测试，你不应该使用这两个选项，因为这两者都会导致明显且不对称的减速。

注意 `--enable-debug` 和 `--enable-maintainer-zts` 会改变 PHP 二进制文件的 ABI，例如，给很多函数添加额外的参数。因此，在调试模式下编译的共享库与在发行模式下构建的 PHP 二进制文件将会不兼容。类似线程安全扩展（ZTS）与 PHP 构建的非线程安全扩展（NTS）不兼容。

由于 ABI 不兼容， `make install` （和 PECL 安装）会根据这些选项，将共享库放在不同的目录中：

-   `$PREFIX/lib/php/extensions/no-debug-non-zts-API_NO` 用于无 ZTS 的发行版本
-   `$PREFIX/lib/php/extensions/debug-non-zts-API_NO` 用于无 ZTS 的调试版本
-   `$PREFIX/lib/php/extensions/no-debug-zts-API_NO` 用于 ZTS 的发行版本
-   `$PREFIX/lib/php/extensions/debug-zts-API_NO` 用于 ZTS 的调试版本

上面的 `API_NO` 占位符指的是 `ZEND_MODULE_API_NO`，它只是类似于 `20100525` 的日期，用于内部 API 版本控制。

上述的配置开关，对于大多数用途来说已经足够了，但是，`./configure` 当然提供了更多的选项，你可在帮助中找到这些选项。

除了给配置传递选项外，你也可以指定许多环境变量。一些更重要的信息记录在配置帮助输出的末尾（`./configure --help | tail -25`）。

例如，你可以使用 `CC` 去使用其他编译器，使用 `CFLAGS` 去更改使用的编译标志：

```
~/php-src> ./configure --disable-all CC=clang CFLAGS="-O3 -march=native"

```

在这个配置中，构建将使用 clang （而不是 gcc），并使用一个很高级别的优化（`-O3 -march=native`）。

你可以使用另外的编译器警告标志，这可以帮助你发现一些错误。对于 GCC，你可以阅读它们[ 在 GCC 手册中](https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html#Warning-Options)



## `make` 和 `make install`

在一切都配置好后，你可以使用 `make` 去执行实际的编译：

```
~/php-src> make -jN    # N 是内核的数量

```

这个操作最主要的结果是启用 SAPI 的 PHP 二进制文件（默认 `sapi/cli/php` 和 `sapi/cgi/php-cgi`），以及 `modules/` 目录下的 共享扩展。

现在你可以运行 `make install` 安装 PHP 到 `/usr/local` （默认）或者你使用  `--prefix` 配置开关指定的任何目录。

`make install` 只是复制大量的文件到新的位置。除非你在配置中指定 `--without-pear`，否则它将下载和安装 PEAR。这里是默认 PHP 构建的结果树：

```
> tree -L 3 -F ~/myphp

/home/myuser/myphp
|-- bin
|   |-- pear*
|   |-- peardev*
|   |-- pecl*
|   |-- phar -> /home/myuser/myphp/bin/phar.phar*
|   |-- phar.phar*
|   |-- php*
|   |-- php-cgi*
|   |-- php-config*
|   `-- phpize*
|-- etc
|   `-- pear.conf
|-- include
|   `-- php
|       |-- ext/
|       |-- include/
|       |-- main/
|       |-- sapi/
|       |-- TSRM/
|       `-- Zend/
|-- lib
|   `-- php
|       |-- Archive/
|       |-- build/
|       |-- Console/
|       |-- data/
|       |-- doc/
|       |-- OS/
|       |-- PEAR/
|       |-- PEAR5.php
|       |-- pearcmd.php
|       |-- PEAR.php
|       |-- peclcmd.php
|       |-- Structures/
|       |-- System.php
|       |-- test/
|       `-- XML/
`-- php
    `-- man
        `-- man1/

```

目录结构的简短概述：

-   *bin/* 包含了 SAPI 二进制文件（`php` 和 `php-cgi`），以及 `phpize` 和 `php-config` 脚本。它同样是各种 PEAR/PECL 脚本的所在地。
-   *etc/* 包含了配置。请注意，默认的 *php.ini*  文件**不在**这里。
-   *include/php* 包含了头文件，在自定义软件中，这些是构建附加扩展或者 PHP 嵌入所必需的。
-   *lib/php* 包含了 PEAR 文件。*lib/php/build* 目录包含了构建扩展所必需的文件，例如 `acinclude.m4` 文件包含了 PHP 的 M4 宏。如果我们编译了任何共享扩展，则这些文件将位于 *lib/php/extensions* 的子目录下。
-   *php/man* 显然包含了 `php` 命令的手册。

如上所述，默认的 *php.ini* 不在 *etc/*.。您可以使用PHP二进制文件的 `--ini` 选项显示位置：

```
~/myphp/bin> ./php --ini
Configuration File (php.ini) Path: /home/myuser/myphp/lib
Loaded Configuration File:         (none)
Scan for additional .ini files in: (none)
Additional .ini files parsed:      (none)

```

如您所见，默认的* php.ini *目录是`$ PREFIX / lib`(libdir)，而不是`$ PREFIX / etc`(sysconfdir)。您可以使用`-with-config-file-path = PATH`配置选项来调整默认的* php.ini *位置。



同样也要注意一下 `make install` 不会创建 ini 文件。如果你想要使用 *php.ini* 文件，你需要自己创建一个。例如，你可以复制默认的开发配置文件：

```
~/myphp/bin> cp ~/php-src/php.ini-development ~/myphp/lib/php.ini
~/myphp/bin> ./php --ini
Configuration File (php.ini) Path: /home/myuser/myphp/lib
Loaded Configuration File:         /home/myuser/myphp/lib/php.ini
Scan for additional .ini files in: (none)
Additional .ini files parsed:      (none)

```

除了 PHP 二进制文件， *bin/* 目录下同样有两个重要的脚本： `phpize` 和 `php-config`。

`phpize` 相当于 `./buildconf` 的扩展。它会从 *lib/php/build* 复制各种文件，并调用 autoconf/autoheader。在下一节，你将会学习更多关于这个工具的知识。

`php-config` 提供有关于 PHP 构建的配置的信息。试试看：

```
~/myphp/bin> ./php-config
Usage: ./php-config [OPTION]
Options:
  --prefix            [/home/myuser/myphp]
  --includes          [-I/home/myuser/myphp/include/php -I/home/myuser/myphp/include/php/main -I/home/myuser/myphp/include/php/TSRM -I/home/myuser/myphp/include/php/Zend -I/home/myuser/myphp/include/php/ext -I/home/myuser/myphp/include/php/ext/date/lib]
  --ldflags           [ -L/usr/lib/i386-linux-gnu]
  --libs              [-lcrypt   -lresolv -lcrypt -lrt -lrt -lm -ldl -lnsl  -lxml2 -lxml2 -lxml2 -lcrypt -lxml2 -lxml2 -lxml2 -lcrypt ]
  --extension-dir     [/home/myuser/myphp/lib/php/extensions/debug-zts-20100525]
  --include-dir       [/home/myuser/myphp/include/php]
  --man-dir           [/home/myuser/myphp/php/man]
  --php-binary        [/home/myuser/myphp/bin/php]
  --php-sapis         [ cli cgi]
  --configure-options [--prefix=/home/myuser/myphp --enable-debug --enable-maintainer-zts]
  --version           [5.4.16-dev]
  --vernum            [50416]

```

该脚本类似于由 Linux 发行版使用的 `pkg-config` 脚本。在扩展构建过程，调用它以获得有关编译器选项和路径的信息。你也可以利用它快速获得有关你的构建的信息，例如，你的配置选项或默认扩展目录。 `./php -i`（phpinfo）同样也可以提供这些信息，但是 `php-config` 以一种更简单的形式提供此信息（可以由自动化工具轻松使用）。



## 运行测试套件

如果你的 `make` 命令成功完成，它会打印一条信息鼓励你去运行 `make test`：

```
Build complete.
Don't forget to run 'make test'

```

`make test` 会针对我们的测试套件运行 PHP CLI 二进制文件，它位于不同的 PHP 资源树下的 *tests/* 目录。默认的构建下是运行大约 9000 个测试 (对最小构建来说更少，对启用附加扩展来说则更多），这可能需要几分钟。 `make test` 命令当前是非并行的，所以指定 `-jN` 选项也不会让它变快。

如果你的平台是第一次编译 PHP，我们希望你能运行测试套件。根据你的系统和构建环境，在运行测试时你可能会找到错误。如果没有任何错误，该脚本会问你是否要发送一份报告给我们的质量检查平台，这将使贡献者能够分析错误。请注意，有一些失败的测试是相当正常的，只要你没有看到十几个错误，你的构建仍可能正常工作。

`make test` 命令使用你的 CLI 二进制文件在内部调用 `run-tests.php` 文件。那你可以运行 `sapi/cli/php run-tests.php --help` 显示该脚本接受的选项列表。

如果你手动运行 `run-tests.php`，你必须指定 `-p` 或 `-P` 选项（或者一个难看的环境变量）：

```
~/php-src> sapi/cli/php run-tests.php -p `pwd`/sapi/cli/php
~/php-src> sapi/cli/php run-tests.php -P

```

`-p` 是测试使用的显式指定一个二进制文件。请注意，为了正确地运行所有测试，它应该是一个绝对路径（或者独立于它调用的目录）。 `-P` 是调用 `run-tests.php` 的二进制文件的快捷方式。在上面的例子中，这两种方式都是相同的。

除了运行整个测试套件，你也可以通过将它们作为参数传递给 `run-tests.php` ，使其限制在某些目录中。例如，只测试 Zend 引擎、reflection 扩展和数组函数：

```
~/php-src> sapi/cli/php run-tests.php -P Zend/ ext/reflection/ ext/standard/tests/array/

```

这非常有用，因为它允许你快速运行只与你的更改有关的测试套件部分。例如，如果你做了语言的修改，你可能不关心扩展的测试，只想要验证 Zend 引擎是否仍然正确的工作。

使用 `run-tests.php` 时，你不需要传递选项或限制目录。除非你可以通过 `make test` 使用 `TESTS` 变量去传递另外的参数。例如，与先前的命令相等的是：

```
~/php-src> make test TESTS="Zend/ ext/reflection/ ext/standard/tests/array/"

```

在之后，我们将会更详细地查看 `run-tests.php` 系统，尤其是会讨论怎么编写我们的测试和调试失败的测试。[查看专用测试章节](http://www.phpinternalsbook.com/tests/introduction.html)。



## 修复编译问题并 `make clean`

你可能知道 `make` 是增量构建的，即不会重新编译所有文件，而是重新编译那些在最后调用中改变的 `.c` 文件。这是一个很好的缩短构建时间的方式，但是它并不是总能做好：例如，如果你在头文件修改了一个结构， `make` 不会自动重新编译使用该头文件的所有 `.c` 文件，从而导致构建失败。

如果运行 `make`时遇到奇怪的错误或生成的二进制文件损坏（例如，在运行第一次测试之前， `make test` 就崩溃了），你应该尝试运行 `make clean`。它会删除所有已编译的对象，强制下一次 `make` 调用运行完整构建。

有时候，你必须在更改 `./configure` 选项之后运行 `make clean`。 如果只是启用额外的扩展，则增量构建应是安全的，但是改变其他的选项可能需要完全重建。

通过 `make distclean` 命令可以达到更强效的清理目标。它除了运行正常的清理，还会回滚所有 `./configure` 命令调用带来的的文件。它会删除配置缓存、make文件、配置头文件和其他各种文件。顾名思义，该目标是“分布清理”，所以通常由发行管理者使用。

另一个编译问题的来源是 `config.m4` 文件或 PHP 构建系统中的其他文件的修改。如果像这样的文件被修改，则必须运行重新 `./buildconf` 脚本。如果你自己做了修改，你可能会记得运行该命令，但如果它是作为 `git pull`（或其他一些更新命令）的一部分发生的，则问题可能不会很明显。

如果你遇到一些奇怪的编译问题，但是通过 `make clean` 不能解决，运行 `./buildconf --force` 有机会修复这个问题。避免优先命令 `./configure` 在后面输入，你可以使用 `./config.nice` 脚本（它包含了你的最后一次 `./configure` 调用）：

```
~/php-src> make clean
~/php-src> ./buildconf --force
~/php-src> ./config.nice
~/php-src> make -jN

```

PHP 提供的最后一个清理脚本是 `./vcsclean`。它只有在你从 Git 检出源代码才有效。 它有效地归结为对 `git clean -X -f -d` 的调用，它会移除所有 Git 忽略的未跟踪文件和目录。你应该小心使用。