
任何一个编译过 PHP 扩展的人几乎都跑过这样一条命令链phpize ./configure --with-php-config$(which php-config) make sudo make install跑归跑真正被问住的时候也不少phpize 凭什么知道 PHP 的头文件在哪个目录为什么用同一份扩展源码在不同机器上编译出来的.so加载行为能完全不一样更诡异的是明明编译成功了php -m里就是看不到新扩展甚至直接报undefined symbol。这些问题的答案全都藏在 phpize 和 php-config 的依赖关系里。既然说庖丁解牛那就不能只看成品。这篇文章顺着这条依赖链把 phpize 的脚本逻辑一层层剥开看看它到底怎么利用 php-config 获取 PHP 信息以及在多版本混装、Docker 容器、源码编译这些真实场景里这些信息又是怎样决定扩展生死的。看完之后你会明白为什么文档里总强调phpize 和 php-config 必须配套使用也会知道下次遇到诡异报错时该去哪里找答案。1. 四联命令背后phpize 的活其实是生成配置1.1 phpize 不是编译器而是配置生成器很多人以为phpize是某种编译器其实它连编译动作都不碰。你解开一个 PECL 扩展的源码包里面通常有config.m4、php_xxx.h、xxx.c但大概率没有configure脚本也没有Makefile.global。没有这两个文件后面的./configure和make根本无从谈起。phpize 做的事情就是把 PHP 源码包build/目录里的 autoconf 辅助文件复制到当前扩展目录再读取扩展的config.m4生成一份独立的configure脚本。执行完后当前目录下会多出configure、Makefile.global、autom4te.cache、config.h.in、build/等一整套文件。这一步本质上是把 PHP 当初编译时的 autoconf 环境搬到你手头这个扩展目录里重新跑一遍。所以 phpize 的正确使用姿势是必须在扩展源码的根目录里执行因为它要的就是那个config.m4。你要是跑到/tmp或者$HOME下面执行它立刻还你一句Cannot find config.m4。这个坑新手踩得最多但根因其实很清晰phpize 不是全局工具它是一把针对当前目录里的扩展项目的钥匙。1.2 为什么扩展不能脱离 PHP 本体单独编译更深一层的问题是为什么不能像编译普通 C 程序那样直接gcc xxx.c -o xxx.so非要绕这么大一圈去生成 configure原因在于 PHP 扩展不是一个独立程序它是要被加载进 PHP 解释器进程里的动态模块。扩展的 C 代码里会#include php.h、#include zend.h这些头文件定义了大量 PHP 内部结构体和宏扩展还必须按照 PHP 编译时确定的 ABI 规则来编译否则符号对不上加载时就崩给你看。用汽车来类比扩展像是给特定型号汽车加装的配件phpize 是负责按原厂图纸准备安装流程的车间而 php-config 就是那份写着这辆车出厂时用了什么螺丝、什么扭矩、什么油路规格的档案。没有档案车间再怎么折腾也是瞎蒙。2. 打开 phpize 脚本它向 php-config 索取了四样关键情报2.1 定位 php-config 的优先级逻辑phpize 本身是个 shell 脚本不同发行版、不同 PHP 版本里脚本内容略有出入但主干逻辑几乎一致。第一步永远是找 php-config。它按这个优先级来找环境变量PHP_CONFIG是否被显式指定PATH里能否找到php-config内置的默认路径通常是 PHP 安装时写死的$includedir/../bin/php-config以我手头这台 Debian PHP 8.2 的环境为例phpize 脚本里相关的片段大致长这样if test -z $PHP_CONFIG; then PHP_CONFIGwhich php-config 2/dev/null if test -z $PHP_CONFIG; then PHP_CONFIG$includedir/../bin/php-config if test ! -x $PHP_CONFIG; then PHP_CONFIG$datarootdir/php-config fi fi fi这段逻辑解释了很多人遇到的一个怪象明明系统里装了 PHP 和 phpize但执行sudo phpize时总是报错找不到 php-config。原因就是sudo会把 PATH 重置成系统默认值而/usr/local/bin这类目录往往被排除在外which php-config就找不到了。后面我还会专门讲这个坑的解法。2.2 版本、头文件路径、链接参数、configure 选项定位到 php-config 之后phpize 会像连珠炮一样向它询问各种信息。我把它要的关键情报整理成了一张表你以后编译扩展时可以对着看php-config 参数返回内容示例在扩展编译中的作用--version8.2.10确认 PHP 版本号作为基础校验--includes-I/usr/include/php/20220829 -I/usr/include/php/20220829/main ...编译扩展时需要的头文件搜索路径会写进 Makefile 的 INCLUDES--include-dir/usr/include/phpPHP 主要头文件所在目录phpize 要从中提取 API 版本宏--extension-dir/usr/lib/php/20220829make install 时把 .so 装到哪个目录--configure-options--prefix/usr --with-apxs2...保留 PHP 编译时的原始配置供扩展 configure 参考--ldflags-L/usr/lib/php/20220829扩展链接时需要的库路径--libs-lcrypt -lresolv -lc ...扩展链接时依赖的系统库你随便在终端里敲一下php-config --includes会看到一长串-I参数。这串东西非常关键它直接决定了编译器在#include php.h时会去哪些目录里找文件。如果这个路径指向了错误的 PHP 版本后面编译出来的扩展基本就是废品。2.3 情报到手之后去了哪里你可能好奇phpize 拿到这些信息后除了在屏幕上打印那几行字到底把它们用到了哪里首先php-config --includes返回的头文件路径会被写进生成的Makefile.global里具体是INCLUDES变量。后面make编译.c文件时编译器会带着这些-I参数去搜索头文件。其次phpize 启动时打印的这三行信息就是它从 php-config 提供的头文件里解析出来的宏值Configuring for: PHP Api Version: 20220829 Zend Module Api No: 20220829 Zend Extension Api No: 420220829这三个数字分别对应PHP_API_VERSION、ZEND_MODULE_API_NO、ZEND_EXTENSION_API_NO它们定义了扩展和 PHP 核心之间的 ABI 契约。同一份源码用不同版本的 PHP 头文件编译打印出来的这三行数字完全不同。这也是我反复强调phpize 必须和运行版本的 PHP 配套的原因这三行数字写进php_xxx.h和zend_module_entry加载时 PHP 会拿它们和自身 API 编号做比对对不上就拒绝加载。3. php-config 本身一份 PHP 安装时代留下的出厂档案3.1 一个普通得不能再普通的 shell 脚本php-config 并不是什么神秘二进制它就是一个由 configure 生成的 shell 脚本。PHP 源码里有个模板叫php-config.in安装时里面的prefix、version、includes等占位符会被 configure 替换成真实值最终输出到/usr/bin/php-config或/usr/local/bin/php-config这类位置。打开它你会发现前面就是一堆变量赋值#!/bin/sh prefix/usr datarootdir/usr/share exec_prefix/usr version8.2.10 vernum80210 includes-I/usr/include/php/20220829 -I/usr/include/php/20220829/main -I/usr/include/php/20220829/TSRM -I/usr/include/php/20220829/Zend -I/usr/include/php/20220829/ext ldflags-L/usr/lib/php/20220829 libs-lcrypt -lresolv -lc extension_dir/usr/lib/php/20220829下面就是一个巨大的case分支根据传入参数把对应变量打印出来。所以它本质上只是把 PHP 编译安装时的参数做了个固化存档。PHP 是什么时候编译的、头文件装在哪、扩展目录在哪、链接用了什么库全部记录在这个脚本里。扩展编译时如果不去读它就等于闭着眼睛组装机器。3.2 带 API 版本号的头文件目录为什么不能错注意看前面那些路径比如/usr/include/php/20220829。这个数字不是随便起名的它对应 PHP 8.2 的 Zend Module API 编号。PHP 每次更新主版本API 编号都会变头文件目录也随之变化。这个设计是有实际意义的系统里完全可以同时存在 PHP 7.4、8.1、8.2 的头文件分别位于/usr/include/php/20190902、/usr/include/php/20210902、/usr/include/php/20220829。每个 php-config 都忠实记录自己对应的那套路径phpize 按这个路径去拿头文件就能保证编译时用的是和运行时 PHP 同一套 API。如果你手动指定了一个不匹配的 php-config比如用 PHP 8.2 的 phpize却非要用 PHP 7.4 的 php-config那 configure 阶段或许还能糊弄过去但编译出的扩展在运行时几乎必然崩溃。轻则undefined symbol重则直接段错误连 PHP 进程都带崩。4. 当系统里不止一个 PHP--with-php-config 的实战场景4.1 混装多版本时的正确指定姿势前面讲的是单环境真实生产环境里最折磨人的其实是多版本混装。我之前排查过一个案例一台服务器上系统源里装了/usr/bin/php7.4后来运维又用源码编译装了/usr/local/php82/bin/php8.2。同事要给 8.2 装一个扩展照着网上的教程敲phpize ./configure make sudo make install结果编译倒是成功了php -m里却看不到扩展php -v之后还跟了一堆undefined symbol警告。我过去一看问题一目了然PATH 里排在前头的是 7.4 的 phpize 和 php-config于是整个扩展都按照 7.4 的 API 编译装到了/usr/lib/php/20190902/目录。8.2 的 PHP 加载它自然是鸡同鸭讲。正确的做法是让这一套命令里的 phpize、php-config、php 全都指向同一个版本/usr/local/php82/bin/phpize ./configure --with-php-config/usr/local/php82/bin/php-config make -j$(nproc) sudo make install编译完最好再做一次核对。先看扩展装到哪了/usr/local/php82/bin/php-config --extension-dir再看目标 PHP 实际加载的扩展目录/usr/local/php82/bin/php -i | grep extension_dir这两条命令的输出放在一起对照如果路径不一致后面八成要出事。这个习惯我后来一直保留着每次编译第三方扩展前都先校验这一步省掉了大量返工。4.2 不碰 configure 也能指定 php-configPHP_CONFIG 环境变量除了在 configure 阶段用--with-php-config...phpize 阶段其实也能强制指定。因为脚本开头有一段if test -z $PHP_CONFIG的逻辑所以你可以直接用环境变量喂给它PHP_CONFIG/usr/local/php82/bin/php-config /usr/local/php82/bin/phpize这个用法在很多文档里都一笔带过实际却非常有用。比如你不想登录到 root 用户又想让 sudo 在执行时保留环境变量可以配合sudo env PHP_CONFIG...来用sudo env PHP_CONFIG/usr/local/php82/bin/php-config /usr/local/php82/bin/phpize另外前面提到的sudo phpize找不到 php-config 的坑也可以用全路径解决sudo /usr/local/php82/bin/phpize。因为脚本内部使用which php-config失败后会走内置默认路径而源码编译安装 PHP 时内置默认路径会被替换成/usr/local/php82/bin/php-config这样就不会受 sudo 的 PATH 重置影响了。5. 现场排障按这条链路把常见报错逐一定位理解了依赖链之后再看常见报错就顺理成章了。我把这些年踩过的、看见别人踩过的坑按症状 - 根因 - 解决的方式整理出来你可以当排查手册用。5.1 phpize: command not found —— 缺的不是源码是 dev 包这是最基础也最常见的。很多人以为装了 PHP 就一定有 phpize其实发行版默认安装的通常是 PHP 运行时不包含扩展开发所需的工具链。Debian/Ubuntu 下要装php-dev或对应版本的php8.x-devCentOS/RHEL 下要装php-devel。装完之后phpize和php-config会一起出现在/usr/bin/下配套好。5.2 Cannot find config.m4 —— 在错误的目录里执行了 phpize这个错误信息很直接。phpize 必须在扩展源码根目录运行因为它要找config.m4。你从 PECL 下载的扩展包解压后目录里通常就有config.m4但如果你解压完又cd到子目录里去执行照样会报这个错。遇到这种情况先ls config.m4看一眼确认自己确实待在正确的位置。5.3 Cannot find autoconf / libtool —— 纯净容器里最容易卡住phpize 生成 configure 的过程需要 autoconf 和 libtool 系列工具。在精简的 Docker 容器里跑phpize经常会看到Cannot find autoconf. Please check your autoconf installation解决办法是在容器里先把工具装上apt-get update apt-get install -y autoconf libtool make pkg-config很多人以为这是 PHP 的问题其实只是容器基础镜像没带完整编译工具链。用docker-php-ext-install时内部已经处理好了但手动编译扩展时就得自己补这一课。5.4 扩展编译成功但加载失败API 版本对不上这是最隐蔽的一类问题因为编译过程完全正常make install也没报错但php -m里就是看不到或者 PHP 启动时弹出这样的警告PHP Warning: PHP Startup: Unable to load dynamic library xxx.so (tried: /usr/lib/php/20220829/xxx.so (/usr/lib/php/20220829/xxx.so: undefined symbol: xxx), ...)看到undefined symbol基本可以断定是版本错配。排查时按顺序做三件事运行phpize --version看输出的 API 版本号是否等于php -i | grep PHP API显示的编号运行php-config --extension-dir确认 .so 被装进了当前 PHP 真正会扫描的目录查看 Makefile 里INCLUDES变量的路径看头文件是否来自预期 PHP 版本的 include 目录三件事都一致了扩展基本就能正常加载。我也曾经在第三步发现问题Makefile 里包含了两个不同版本的头文件路径gcc 按顺序解析时先吃进了旧版本头文件里的宏定义导致编译出的符号在新运行时里找不到对应的实现。后来我清理干净旧的phpize、configure缓存文件重新生成整个配置才解决。最后说点个人的体会这套链路我最早也没当回事直到在容器和混装环境里连续栽了几个跟头才老老实实把 phpize 脚本打开逐行读过一遍。读完之后最大的感受是文档里写的请确保 phpize 与 php-config 匹配并不是套话它背后就是一套非常具体的路径查找和参数传递逻辑。以后遇到扩展编译相关的环境问题我第一反应不是翻长篇文档而是先跑phpize --version和php-config --includes用输出告诉自己是哪一环掉了链子。这两个脚本不会说谎它们比任何二次转述的教程都诚实。