2.1. 编译 PHP

构建 PHP

本章将说明如何以一种适合开发扩展或者内核修改的方式编译 PHP。我们将仅介绍 Unixoid 系统的构建。如果你希望在 Windows 构建 PHP,你可以在 PHP 维基上看下这个逐步构建说明

本章也概述了 PHP 构建系统的工作方式和工具使用,但是详细的说明不在本书的范围之内。

免责声明:我们对因尝试在 Windows 编译 PHP 而造成不利健康的影响概不负责。

为什么不使用软件包?

如果您目前正在使用 PHP ,则可能使用 sudo apt-get install php 之类的命令通过软件包管理器进行了安装。在解释实际的编译之前,您应该首先理解为什么自己编译是必要的,而不仅仅是使用预编译的程序包。原因有很多:

首先,预构建包只包含生成的二进制文件,但缺少编译扩展所必需的其他东西,例如头文件。这可以通过安装开发包来轻松解决,这个开发包通常被称为 php-dev。为了便于使用 valgrind 或 gdb 进行调试,可以另外安装调试符号,这些符号通常作为另一个名为 php-dbg 的软件包提供。

但是,即使您安装标头和调试符号,您仍将使用PHP的发行版。这意味着它将以较高的优化级别构建,这会使调试变得非常困难。此外,发行版本不会生成有关内存泄漏或数据结构不一致的警告。此外,预构建的包不支持线程安全,这在开发过程中非常有帮助。

另一个问题是几乎所有的发行版都会向PHP应用额外的补丁。在某些情况下,这些补丁只包含与配置相关的微小更改,但有些发行版使用像 Suhosin 这样的高侵入性补丁。已知其中一些补丁会引入与低级扩展(例如 opcache )的不兼容性。

PHP 仅提供对php.net上提供的软件的支持,不对发行版修改的版本提供支持。如果要报告错误,提交补丁或利用我们的帮助渠道进行扩展编写,则应始终对照官方的PHP版本进行工作。当我们在本书中谈论「PHP」时,我们总是指受官方支持的版本。

获取源代码

在构建 PHP 之前,你必须先获得源代码。有两种方式可以获取:一种是从 PHP 下载页面 下载,一种是从 Git 仓库 克隆(或者 Github的镜像)。

这两种情况下构建 PHP 的过程有些许差异:Git 仓库未捆绑 configure 脚本,所以你需要使用 buildconf 脚本来生成自动配置。此外,Git 仓库不包含预生成解析器,所以你还需要安装Bison。

我们推荐你从 Git 上检出源代码,因为这样方便安装更新和尝试不同版本的代码。如果你想要提交修改或者拉取 PHP 的请求,Git 同样需要检出。

要克隆仓库,在你的Shell中运行一下命令:

  1. ~> git clone http://git.php.net/repository/php-src.git
  2. ~> cd php-src
  3. # 默认情况下是在master分支上
  4. # 开发版本。你可以改为检出稳定分支:
  5. ~/php-src> git checkout PHP-7.0

如果你对 Git 检出有疑问,看下 PHP 维基上 Git 常见问题。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 上,你可以使用以下命令安装所有这些文件:

  1. ~/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 构建命令:

  1. ~/php-src> ./buildconf # only necessary if building from git
  2. ~/php-src> ./configure
  3. ~/php-src> make -jN

为了快速构建,请用可用的 CPU 内核数替换 N (请见 grep "cpu cores" /proc/cpuinfo)。

默认 PHP 构建将会为 CLI 和 CGI SAPI 构建二进制文件,它们分别位于 sapi/cli/phpsapi/cgi/php-cgi 中。若要检查一切是否正常,可尝试运行 sapi/cli/php -v

另外你可以运行 sudo make install 安装 PHP 到 /usr/local。在配置阶段,目标路径可以通过指定的 --prefix 更改:

  1. ~/php-src> ./configure --prefix=$HOME/myphp
  2. ~/php-src> make -jN
  3. ~/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 文件)生成的。

好消息是编写扩展甚至修改内核都不需要与构建系统进行太多交互。而在这之后,你必须编写小的 config.m4 文件,但是这些文件通常仅使用 acinclude.m4 提供的两或三个高级宏。因此,我们不在这里做进一步详细介绍。

./buildconf 脚本只有两个选项: --debug, 当你调用 autoconf 和 autoheader 时会禁用警告抑制。除非你想要在构建系统上工作,否则你对这个选项没什么兴趣。

第二个选项是 --force,在发行包中将会允许运行 ./buildconf(例如,如果你下载了打包的源代码,并生成一个新的 ./configure 文件)并另外清除配置缓存 config.cacheautom4te.cache/

如果你使用 git pull (或其他一些命令)更新你的 Git 仓库,并且在 make 步骤中出现奇怪的错误,这通常意味着在构建配置中某些东西已更改,你需要运行 ./buildconf --force

./configure 脚本

一旦生成 ./configure 脚本,你便可以使用它去定制你的 PHP 构建。你可以使用 --help 列出所有已支持的选项:

  1. ~/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构建,结果将如下所示:

  1. ~/php-src> sapi/cli/php -m
  2. [PHP Modules]
  3. Core
  4. ctype
  5. date
  6. dom
  7. fileinfo
  8. filter
  9. hash
  10. iconv
  11. json
  12. libxml
  13. pcre
  14. PDO
  15. pdo_sqlite
  16. Phar
  17. posix
  18. Reflection
  19. session
  20. SimpleXML
  21. SPL
  22. sqlite3
  23. standard
  24. tokenizer
  25. xml
  26. xmlreader
  27. xmlwriter

如果你现在想要停止编译 CGI SAPI,以及 tokenizersqlite3 扩展,启用 opcachegmp,相应的 configure 命令将是:

  1. ~/php-src> ./configure --disable-cgi --disable-tokenizer --without-sqlite3
  2. --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 选项,则会创建一个包含最少扩展的构建:

  1. ~/php-src> ./configure --disable-all && make -jN
  2. ~/php-src> sapi/cli/php -m
  3. [PHP Modules]
  4. Core
  5. date
  6. pcre
  7. Reflection
  8. SPL
  9. standard

如果你想要快速构建并且不需要很多功能(例如,实现语言更改)时,--disable-all 选项非常有用。对于尽可能最小的构建来说,你可以另外使用 --disable-cgi开关,这仅生成 CLI 二进制文件。

还有两个开关,在开发扩展或使用 PHP 时,你应 始终 指明:

--enable-debug 启用调试模式,它有多种作用:编译将以 -g 运行生成调试符号,且使用最低优化级别 -O0。这将使 PHP 变得很慢,但是使用 gdb 之类的工具使调试变得更加可预测。另外调试模式定义了 ZEND_DEBUG 宏,它将在引擎中启用各种调试助手。除其他事外,还将报告内存泄露以及一些数据结构的不正确使用。

--enable-maintainer-zts 启用线程安全。该开关将定义 ZTS 宏, 这将启用 PHP 使用的整个 TSRM (线程安全资源管理)机制。PHP 的线程安全编写非常简单,但是前提是确保启用了该开关。如果你需要更多关于 PHP 线程安全和全局内存管理的信息,可阅读 全局管理章节

另一方面,如果你想要为你的代码执行性能基准测试,你不应该使用这两个选项,因为这两者都会导致明显且不对称的减速。

注意 --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 去更改使用的编译标志:

  1. ~/php-src> ./configure --disable-all CC=clang CFLAGS="-O3 -march=native"

在这个配置中,构建将使用 clang (而不是 gcc),并使用一个很高级别的优化(-O3 -march=native)。

你可以使用另外的编译器警告标志,这可以帮助你发现一些错误。对于 GCC,你可以阅读它们 在 GCC 手册中

make 和 make install

在一切都配置好后,你可以使用 make 去执行实际的编译:

  1. ~/php-src> make -jN # N 是内核的数量

这个操作最主要的结果是启用 SAPI 的 PHP 二进制文件(默认 sapi/cli/phpsapi/cgi/php-cgi),以及 modules/ 目录下的 共享扩展。

现在你可以运行 make install 安装 PHP 到 /usr/local (默认)或者你使用 --prefix 配置开关指定的任何目录。

make install 只是复制大量的文件到新的位置。除非你在配置中指定 --without-pear,否则它将下载和安装 PEAR。这里是默认 PHP 构建的结果树:

  1. > tree -L 3 -F ~/myphp
  2. /home/myuser/myphp
  3. |-- bin
  4. | |-- pear*
  5. | |-- peardev*
  6. | |-- pecl*
  7. | |-- phar -> /home/myuser/myphp/bin/phar.phar*
  8. | |-- phar.phar*
  9. | |-- php*
  10. | |-- php-cgi*
  11. | |-- php-config*
  12. | `-- phpize*
  13. |-- etc
  14. | `-- pear.conf
  15. |-- include
  16. | `-- php
  17. | |-- ext/
  18. | |-- include/
  19. | |-- main/
  20. | |-- sapi/
  21. | |-- TSRM/
  22. | `-- Zend/
  23. |-- lib
  24. | `-- php
  25. | |-- Archive/
  26. | |-- build/
  27. | |-- Console/
  28. | |-- data/
  29. | |-- doc/
  30. | |-- OS/
  31. | |-- PEAR/
  32. | |-- PEAR5.php
  33. | |-- pearcmd.php
  34. | |-- PEAR.php
  35. | |-- peclcmd.php
  36. | |-- Structures/
  37. | |-- System.php
  38. | |-- test/
  39. | `-- XML/
  40. `-- php
  41. `-- man
  42. `-- man1/

目录结构的简短概述:

  • bin/ 包含了 SAPI 二进制文件(phpphp-cgi),以及 phpizephp-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 选项显示位置:

  1. ~/myphp/bin> ./php --ini
  2. Configuration File (php.ini) Path: /home/myuser/myphp/lib
  3. Loaded Configuration File: (none)
  4. Scan for additional .ini files in: (none)
  5. 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 文件,你需要自己创建一个。例如,你可以复制默认的开发配置文件:

  1. ~/myphp/bin> cp ~/php-src/php.ini-development ~/myphp/lib/php.ini
  2. ~/myphp/bin> ./php --ini
  3. Configuration File (php.ini) Path: /home/myuser/myphp/lib
  4. Loaded Configuration File: /home/myuser/myphp/lib/php.ini
  5. Scan for additional .ini files in: (none)
  6. Additional .ini files parsed: (none)

除了 PHP 二进制文件, bin/ 目录下同样有两个重要的脚本: phpizephp-config

phpize 相当于 ./buildconf 的扩展。它会从 lib/php/build 复制各种文件,并调用 autoconf/autoheader。在下一节,你将会学习更多关于这个工具的知识。

php-config 提供有关于 PHP 构建的配置的信息。试试看:

  1. ~/myphp/bin> ./php-config
  2. Usage: ./php-config [OPTION]
  3. Options:
  4. --prefix [/home/myuser/myphp]
  5. --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]
  6. --ldflags [ -L/usr/lib/i386-linux-gnu]
  7. --libs [-lcrypt -lresolv -lcrypt -lrt -lrt -lm -ldl -lnsl -lxml2 -lxml2 -lxml2 -lcrypt -lxml2 -lxml2 -lxml2 -lcrypt ]
  8. --extension-dir [/home/myuser/myphp/lib/php/extensions/debug-zts-20100525]
  9. --include-dir [/home/myuser/myphp/include/php]
  10. --man-dir [/home/myuser/myphp/php/man]
  11. --php-binary [/home/myuser/myphp/bin/php]
  12. --php-sapis [ cli cgi]
  13. --configure-options [--prefix=/home/myuser/myphp --enable-debug --enable-maintainer-zts]
  14. --version [5.4.16-dev]
  15. --vernum [50416]

该脚本类似于由 Linux 发行版使用的 pkg-config 脚本。在扩展构建过程,调用它以获得有关编译器选项和路径的信息。你也可以利用它快速获得有关你的构建的信息,例如,你的配置选项或默认扩展目录。 ./php -i(phpinfo)同样也可以提供这些信息,但是 php-config 以一种更简单的形式提供此信息(可以由自动化工具轻松使用)。

运行测试套件

如果你的 make 命令成功完成,它会打印一条信息鼓励你去运行 make test

  1. Build complete.
  2. 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 选项(或者一个难看的环境变量):

  1. ~/php-src> sapi/cli/php run-tests.php -p `pwd`/sapi/cli/php
  2. ~/php-src> sapi/cli/php run-tests.php -P

-p 是测试使用的显式指定一个二进制文件。请注意,为了正确地运行所有测试,它应该是一个绝对路径(或者独立于它调用的目录)。 -P 是调用 run-tests.php 的二进制文件的快捷方式。在上面的例子中,这两种方式都是相同的。

除了运行整个测试套件,你也可以通过将它们作为参数传递给 run-tests.php ,使其限制在某些目录中。例如,只测试 Zend 引擎、reflection 扩展和数组函数:

  1. ~/php-src> sapi/cli/php run-tests.php -P Zend/ ext/reflection/ ext/standard/tests/array/

这非常有用,因为它允许你快速运行只与你的更改有关的测试套件部分。例如,如果你做了语言的修改,你可能不关心扩展的测试,只想要验证 Zend 引擎是否仍然正确的工作。

使用 run-tests.php 时,你不需要传递选项或限制目录。除非你可以通过 make test 使用 TESTS 变量去传递另外的参数。例如,与先前的命令相等的是:

  1. ~/php-src> make test TESTS="Zend/ ext/reflection/ ext/standard/tests/array/"

在之后,我们将会更详细地查看 run-tests.php 系统,尤其是会讨论怎么编写我们的测试和调试失败的测试。查看专用测试章节

修复编译问题并 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 调用):

  1. ~/php-src> make clean
  2. ~/php-src> ./buildconf --force
  3. ~/php-src> ./config.nice
  4. ~/php-src> make -jN

PHP 提供的最后一个清理脚本是 ./vcsclean。它只有在你从 Git 检出源代码才有效。 它有效地归结为对 git clean -X -f -d 的调用,它会移除所有 Git 忽略的未跟踪文件和目录。你应该小心使用。