构建PHP
本章介绍如何以适合开发扩展或修改核心的方式编译 PHP。我们将只介绍在 Unixoid 系统上的编译。如果希望在 Windows 上编译 PHP,则应查看 PHP wiki 中的 分步编译 说明。
本章还概述了 PHP 构建系统的工作原理及其使用的工具,但详细描述不在本书范围之内。
为什么不使用软件包?
如果你正在使用 PHP,很可能是通过软件包管理器使用 sudo apt-get install php
这样的命令安装的。在解释实际编译之前,你首先应该明白为什么需要自己编译,而不能直接使用预编译包。原因是多方面的:
首先,预编译包只包含生成的二进制文件,而忽略了编译扩展所需的其他内容,例如头文件。安装开发包(通常称为 php-dev
)可以轻松解决这个问题。为了便于使用 valgrind
或 gdb
调试,可以额外安装调试符号,这通常作为另一个名为 php-dbg
的软件包提供。
不过,即使安装了头文件和调试符号,你仍将使用 PHP 的发行版本。这意味着它将以高优化级别构建,这可能会使调试变得非常困难。此外,发行版不会启用断言,也不会生成内存泄漏警告。此外,预编译包不会启用线程安全,这可能有助于确保扩展以线程安全的配置编译。
另一个问题是,几乎所有的发行版都会给 PHP 打上额外的补丁。在某些情况下,这些补丁只包含与配置相关的小改动,但有些发行版会使用像 Suhosin 这样侵入性很强的补丁。众所周知,其中一些补丁会引入与 opcache 等低级扩展的不兼容性。
PHP 只支持 php.net 上提供的软件,而不支持发行版修改后的版本。如果您想报告错误、提交补丁或使用我们的帮助渠道进行扩展编写,则应始终使用 PHP 官方版本。当我们在本书中谈到 “PHP” 时,我们总是指官方支持的版本。
获取源码
两种方式的构建过程略有不同:git
仓库没有捆绑 configure
脚本,因此您需要使用 buildconf
脚本生成 configure
脚本,该脚本使用 autoconf
。此外,git 仓库不包含预生成的词法和解析器,因此还需要安装 re2c
和 bison
。
我们建议您从 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 版本和配置测试扩展或更改,这可能非常有用。
在继续之前,您还应该使用包管理器安装一些基本的构建依赖项(默认情况下您可能已经安装了前三个):
-
gcc
和g++
或其他一些编译器工具链。 -
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 构建将需要 libxml
和 libsqlite3
,您可以通过 libxml2-dev
和 libsqlite3-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/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 构建,安装 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
将在调用 autoconf
和 autoheader
时禁用警告抑制。除非您想在构建系统上工作,否则您对这个选项没什么兴趣。
第二个选项是 --force
,它将允许在发布包中运行 ./buildconf
(例如,如果您下载了打包的源代码并想要生成新的 ./configure
),并另外清除配置缓存 config.cache
和 autom4te.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/php
和 sapi/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 二进制文件(php
和php-cgi
)以及phpize
和php-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/
目录还包含两个重要脚本: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 源代码树的不同测试/目录中。默认情况下,编译时要运行 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 忽略的未跟踪文件和目录。请谨慎使用。