构建PHP

本章介绍如何以适合开发扩展或修改核心的方式编译 PHP。我们将只介绍在 Unixoid 系统上的编译。如果希望在 Windows 上编译 PHP,则应查看 PHP wiki 中的 分步编译 说明。

本章还概述了 PHP 构建系统的工作原理及其使用的工具,但详细描述不在本书范围之内。

为什么不使用软件包?

如果你正在使用 PHP,很可能是通过软件包管理器使用 sudo apt-get install php 这样的命令安装的。在解释实际编译之前,你首先应该明白为什么需要自己编译,而不能直接使用预编译包。原因是多方面的:

首先,预编译包只包含生成的二进制文件,而忽略了编译扩展所需的其他内容,例如头文件。安装开发包(通常称为 php-dev)可以轻松解决这个问题。为了便于使用 valgrindgdb 调试,可以额外安装调试符号,这通常作为另一个名为 php-dbg 的软件包提供。

不过,即使安装了头文件和调试符号,你仍将使用 PHP 的发行版本。这意味着它将以高优化级别构建,这可能会使调试变得非常困难。此外,发行版不会启用断言,也不会生成内存泄漏警告。此外,预编译包不会启用线程安全,这可能有助于确保扩展以线程安全的配置编译。

另一个问题是,几乎所有的发行版都会给 PHP 打上额外的补丁。在某些情况下,这些补丁只包含与配置相关的小改动,但有些发行版会使用像 Suhosin 这样侵入性很强的补丁。众所周知,其中一些补丁会引入与 opcache 等低级扩展的不兼容性。

PHP 只支持 php.net 上提供的软件,而不支持发行版修改后的版本。如果您想报告错误、提交补丁或使用我们的帮助渠道进行扩展编写,则应始终使用 PHP 官方版本。当我们在本书中谈到 “PHP” 时,我们总是指官方支持的版本。

获取源码

在编译 PHP 之前,首先需要获取其源代码。有两种方法: 您可以从 PHP 的下载页面 下载压缩包,或者从 Github 克隆 git 代码库。

两种方式的构建过程略有不同:git 仓库没有捆绑 configure 脚本,因此您需要使用 buildconf 脚本生成 configure 脚本,该脚本使用 autoconf。此外,git 仓库不包含预生成的词法和解析器,因此还需要安装 re2cbison

我们建议您从 git 签出源代码,因为这将为您提供一种简便的方式来更新您的安装,并在不同版本中试用您的代码。如果要为 PHP 提交补丁或 pull 请求,也需要使用 git 签出。

要克隆仓库,请在 shell 中运行以下命令:

~> git clone https://github.com/php/php-src.git
~> cd php-src
# by default you will be on the master branch, which is the current
# development version. You can check out a stable branch instead:
~/php-src> git checkout PHP-8.1

如果您对 git checkout 有疑问,请查看 PHP wiki 上的 Git FAQ 常见问题解答。Git 常见问题解答还解释了如果您想为 PHP 本身做出贡献,如何设置 git。此外,它还包含有关为不同 PHP 版本设置多个工作目录的说明。如果您需要针对多个 PHP 版本和配置测试扩展或更改,这可能非常有用。

在继续之前,您还应该使用包管理器安装一些基本的构建依赖项(默认情况下您可能已经安装了前三个):

  • gccg++ 或其他一些编译器工具链。

  • 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 词法分析器。

    • 对于 PHP <= 7.3 是可选的。

    • 0.13.4 或更高版本(适用于 PHP 7.4 及更高版本)

在 Debian/Ubuntu 上,您可以使用以下命令安装所有这些:

~/php-src> sudo apt-get install build-essential autoconf libtool bison re2c pkg-config

根据您在 ./configure 阶段启用的扩展,PHP 将需要许多附加库。安装它们时,检查是否有以 -dev-devel 结尾的软件包版本,然后安装它们。没有 dev 的包通常不包含必要的头文件。例如,默认的 PHP 构建将需要 libxmllibsqlite3,您可以通过 libxml2-devlibsqlite3-dev 包安装它们。

构建概览

在仔细查看各个构建步骤的作用之前,以下是 “默认” PHP 构建需要执行的命令:

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

为了快速构建,请将 N 替换为可用的 CPU 核心数(您可以运行 nproc 来确定这一点)。

默认情况下,PHP 将为 CLI 和 CGI SAPI 构建二进制文件,分别位于 sapi/cli/phpsapi/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 构建,安装 PHP 会很方便。

现在让我们仔细看看各个构建步骤!

./buildconf 脚本

如果您从 git 存储库构建,您要做的第一件事就是运行 ./buildconf 脚本。该脚本只不过是调用 build/build.mk makefile,后者又调用 build/build2.mk

这些 makefile 的主要工作是运行 autoconf 来生成 ./configure 脚本和 autoheader 来生成 main/php_config.h.in 模板。后一个文件将被 configure 用来生成最终的配置头文件 main/php_config.h

这两个工具都从 configure.ac 文件(它指定了 PHP 的大部分编译过程)、build/php.m4 文件(它指定了大量特定于 PHP 的 M4 宏)以及单个扩展和 SAPI 的 config.m4 文件(以及大量其他 m4 文件)中生成结果。

好消息是编写扩展甚至进行核心修改不需要与构建系统进行太多交互。稍后您将不得不编写小的 config.m4 文件,但这些文件通常只使用 build/php.m4 提供的两个或三个高级宏。因此,我们不会在这里进一步详细介绍。

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

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

如果您使用 git pull(或其他命令)更新 git 存储库,并在 make 步骤期间遇到奇怪的错误,这通常意味着构建配置中的某些内容发生了更改,您需要重新运行 ./buildconf

./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 7.4 起,大多数扩展都使用 pkg-config,在这种情况下,向 --with 传递目录没有任何作用。在这种情况下,有必要将库添加到 PKG_CONFIG_PATH

export PKG_CONFIG_PATH=/path/to/library/lib/pkgconfig:$PKG_CONFIG_PATH

默认情况下,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
hash
json
pcre
Reflection
SPL
standard

如果你希望快速编译,又不需要太多的功能(例如在执行语言更改时),那么 --disable-all 选项就非常有用。此外,你还可以指定 --disable-cgi 开关,以便只生成 CLI 二进制文件,从而尽可能缩小编译范围。

还有三个开关,通常在开发扩展或使用 PHP 时需要指定:

--enable-debug(启用调试)启用调试模式,它有多种效果: 编译时将使用 -g 生成调试符号,并使用最低优化级别 -O0。这将使 PHP 的运行速度大大降低,但使用 gdb 等工具进行调试却更容易预测。此外,调试模式还定义了 ZEND_DEBUG 宏,它可以使用断言并在引擎中启用各种调试辅助工具。除其他外,内存泄漏以及某些数据结构的错误使用都会被报告。使用 --enable-debug-assertions 可以在不关闭优化的情况下启用调试断言。

--enable-zts(或 PHP 8.0 之前的 --enable-maintainer-zts)启用线程安全。该开关将定义 ZTS 宏,进而启用 PHP 使用的整个 TSRM(线程安全资源管理器)机制。从 PHP 7 开始,持续启用该开关的重要性已大大低于以前的版本。最重要的是确保包含了所有必要的模板代码。如果需要更多有关 PHP 中线程安全和全局内存管理的信息,请阅读 【全局管理一章】。

--enable-werror(自 PHP 7.4 起)启用 -Werror 编译器标志,它将把编译器警告提升为错误。启用该标志可确保 PHP 编译过程中不会出现警告。不过,生成的警告取决于所使用的编译器、版本和优化选项,因此某些编译器可能无法使用该选项。

另一方面,如果想对代码执行性能基准测试,则不应使用 --enable-debug 选项。--enable-zts 也会对运行时性能产生负面影响。

请注意,--enable-debug--enable-zts 会改变 PHP 二进制的 ABI,例如为函数添加额外的参数。因此,以调试模式编译的共享扩展与以发布模式构建的 PHP 二进制不兼容。同样,线程安全扩展(ZTS)与非线程安全 PHP 构建(NTS)也不兼容。

由于 ABI 不兼容,make install(和 PECL install)会根据这些选项将共享扩展放到不同的目录中:

  • $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 传递选项,还可以指定一些环境变量。一些更重要的变量在 configure 帮助输出(./configure --help | tail -25)的末尾有详细说明。

例如,你可以使用 CC 来使用不同的编译器,使用 CFLAGS 来更改使用的编译标志:

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

在这种配置下,编译将使用 clang(而不是 gcc),并使用非常高的优化级别(-O3 -march=native)。

对于开发而言,一个特别有用的选项是 -fsanitize,它允许你在运行时检测内存损坏和未定义的行为:

CFLAGS="-fsanitize=address -fsanitize=undefined"

这些选项只有在 PHP 7.4 之后才能可靠地工作,并且会大大降低生成 PHP 二进制文件的速度。

make 和 make install

一切配置完成后,就可以使用 make 进行实际编译了:

~/php-src> make -jN    # where N is the number of cores

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

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

make install 只会复制一些文件到新的位置。如果你在配置时指定了 --with-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 二进制文件(phpphp-cgi)以及 phpizephp-config 脚本。该目录还包含各种 PEAR/PECL 脚本。

  • etc/ 包含配置文件。请注意,默认的 php.ini 目录不在这里。

  • include/php 包含头文件,这是构建附加扩展或在自定义软件中嵌入 PHP 所必需的。

  • lib/php 包含 PEAR 文件。lib/php/build 目录包含构建扩展所需的文件,例如包含 PHP 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/ 目录还包含两个重要脚本:phpizephp-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 源代码树的不同测试/目录中。默认情况下,编译时要运行 10000 多个测试代码(最小编译时更少,启用附加扩展后会更多),这可能需要几分钟时间。

make test 命令在内部使用 CLI 二进制调用 run-tests.php 文件。为获得更多控制权,建议直接调用 run-tests.php。例如,这将允许你启用并行测试运行器:

~/php-src> sapi/cli/php run-tests.php -jN

并行测试仅在 PHP 7.4 中可用。在较早的 PHP 版本中,并行性是不可用的,必须额外通过 -P 选项:

~/php-src> sapi/cli/php run-tests.php -P

你也可以将某些目录作为参数传递给 run-tests.php,将其限制在特定范围内,而不是运行整个测试套件。例如,只测试 Zend 引擎、反射扩展和数组函数:

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

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

你可以运行 sapi/cli/php run-tests.php --help 来显示测试运行器接受的全部选项列表。一些特别有用的选项如下

  • -c php.ini 用于指定要使用的 php.ini 文件。

  • -d foo=bar 可用于设置 ini 选项。

  • -m 在 valgrind 下运行测试以检测内存错误。请注意,这样做速度极慢。

  • --asan 应在使用 -fsanitize=address 编译 PHP 时设置。这些选项加在一起大约相当于在 valgrind 下运行,但性能要好得多。

你无需明确使用 run-tests.php 来传递选项或限制目录。相反,你可以使用 TESTS 变量通过 make test 传递额外的参数。例如,与上一条命令等价的命令是:

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

稍后我们将更详细地介绍 run-tests.php 系统,特别是如何编写自己的测试以及如何调试测试失败。 请参阅专门的测试章节

修复兼容问题和 make clean

正如你可能知道的,make 会执行增量编译,也就是说,它不会重新编译所有文件,而只会重新编译上次调用后发生变化的 .c 文件。这是一种缩短编译时间的好方法,但并不总是很有效:例如,如果你修改了一个头文件中的结构,make 不会自动重新编译所有使用该头文件的 .c 文件,从而导致编译失败。

如果在运行 make 时出现奇怪的错误,或者生成的二进制文件被破坏(例如,如果 make test 在运行第一个测试之前就崩溃了),你应该尝试运行 make clean。这会删除所有编译对象,从而迫使下一次 make 调用执行完整的编译。(你可以使用 ccache 来减少重建的代价)。

有时,在更改 ./configure 选项后也需要运行 make clean。如果只启用了额外的扩展,增量编译应该是安全的,但更改其他选项可能需要完全重建。

编译问题的另一个来源是修改 config.m4 文件或属于 PHP 编译系统一部分的其他文件。如果修改了此类文件,就必须重新运行 ./buildconf./configure 脚本。如果是自己修改,可能会记得运行该命令,但如果是 git pull(或其他更新命令)的一部分,问题可能就不那么明显了。

如果遇到任何无法通过 make clean 解决的编译问题,运行 ./buildconf 就有可能解决问题。为了避免事后键入之前的 ./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 忽略的未跟踪文件和目录。请谨慎使用。