ソースコード(php-src)からPHPをビルドする流れと仕組みを手を動かしながら理解する

公開:2020/11/21
更新:2020/12/11
20 min読了の目安(約18000字TECH技術記事

PHP8がリリースされました。

さまざまな言語仕様が追加される中、やはりそれを触る最速の方法は手元でビルドして確かめることでしょう。本記事では、手元のPC[1]で、PHPのソースコード(php-src)からビルドし、テスト・CLIとして実行するまで、実際に手を動かせる形で順序立てて解説しています[2]

ただの手順書とするだけではあまり面白くないので、都度「ここではなにが行われているのか」について解説を含めていきます。具体的には、PHPコードの実行プロセスのために必要なライブラリや、CやC++の開発文脈で当たり前のように出てくるconfigureなどの基礎知識・CLI実行時の処理構造(SAPIなど)について触れることで、PHPのソースコードの前提知識をインプットできる時間とします。

そしてこちらは、PHP Advent Calendar 2020の5日目の記事となります。

手順

途中途中で説明を挟むため、まず全体像としてどういう手順を踏むかについて抑えておきます。

  1. PHPソースコードをcloneする
  2. 依存ソフトウェアを事前インストール
  3. configureスクリプトを生成・実行
  4. makeでビルドする
  5. テストを実行する
  6. ビルドしたものをinstallする
  7. CLIでバージョンを表示して動作確認

1. PHPソースコードをcloneする

PHPのソースコードはこちらです。cloneするとmasterブランチを指したソースコードを手に入れることができます。

2. 依存ソフトウェアを事前インストール

PHPのビルドに必要なものを事前にインストールします。具体的には、次のページにコードからビルドしたい場合に必要なものが記載されています。

今回の動作環境が注記したとおりMacOSであるため、UNIXのページを参照していますがお使いの環境に応じて参照するべきページは異なります。たとえば、Windows PCの場合は、Windows システムへのインストールを参照いただくことになります。

以降、UNIXを前提としますが、UNIXの場合は次のものが必要になる点が記載されています。

  • autoconf
  • automake
  • libtool
  • re2c
  • flex
  • bison

ここは環境に依存する可能性がありますが、自分が手元のMacbook Proでビルドする際に実際にHomebrewからインストールしたものは以下です。

brew install autoconf
brew install re2c
brew install bison
brew install oniguruma
brew install libiconv

これらが具体的にどういう役割・経緯で必要なのかをそれぞれ記します。

autoconf

PHPのソースコードを実行ファイルにビルドする過程で、configureスクリプトを生成する過程があります。その過程において、buildconfという実行スクリプトによって生成するのですが、このbuildconfはAutoconfのラッパーとなっています。

A wrapper around Autoconf that generates files to build PHP on *nix systems.

Autoconfの説明は以下の公式ページのとおりですが、まさにconfigureスクリプトの自動生成ツールとなっています。

Autoconf is an extensible package of M4 macros that produce shell scripts to automatically configure software source code packages.

configureは次の記事にもありますが、手動で作るのではなくGNU Autotoolsと呼ばれるパッケージによって自動生成します。

re2c

Its main goal is generating fast lexers: at least as fast as their reasonably optimized hand-coded counterparts.

lexer とは字句解析のことを指します。日本語では知っていますか? あなたの書いたPHPのコードが実行される4つのプロセスあたりでPHPコードの実行プロセスを4つに分解して説明しています。

  1. 字句解析(Lexing)
  2. 構文解析(Parsing)
  3. コンパイル(Compilation)
  4. 実行(Interpretation)

PHPはre2cを使用して、zend_language_scanner.l定義ファイルからレキサー(字句解析器)を生成します。

そのため必要になります。

bison

こちらは、字句解析後の構文解析に用いられるものです。PHPはBisonを用いています。

Bison is a general-purpose parser generator that converts an annotated context-free grammar into a deterministic LR or generalized LR (GLR) parser employing LALR(1) parser tables.

$ brew install bison

==> Downloading https://homebrew.bintray.com/bottles/bison-3.7.4.catalina.bottle
==> Downloading from https://d29vzk4ow07wi7.cloudfront.net/6252edf4d591cf1de3e94
######################################################################## 100.0%
==> Pouring bison-3.7.4.catalina.bottle.tar.gz
==> Caveats
bison is keg-only, which means it was not symlinked into /usr/local,
because macOS already provides this software and installing another version in
parallel can cause all kinds of trouble.

If you need to have bison first in your PATH run:
  echo 'export PATH="/usr/local/opt/bison/bin:$PATH"' >> ~/.zshrc

For compilers to find bison you may need to set:
  export LDFLAGS="-L/usr/local/opt/bison/lib"

==> Summary
🍺  /usr/local/Cellar/bison/3.7.4: 94 files, 3.3MB

また、「OSXで最新のPHPをビルドする方法」にて説明がありましたが、後ほどbisonのパスを用いるためダウンロード先を確認・メモしておきます(/usr/local/Cellar/bison/3.7.4)。

oniguruma

これは後のconfigureにてconfigure: error: Package requirements (oniguruma) were not met:というエラーが発生したためインストールしています。

dockerでのビルドでは様々なケースが報告されていて対応策が明示されていました。


Mac OSの場合は、MacOS に anyenv + phpenv で PHP 7.4.1 をインストールするにて、Homebrewからinstallしています。

libiconv

configure: error: Please specify the install prefix of iconv with --with-iconv=<DIR> というエラーが後のconfigureで発生したためインストールしています。

でのコメント内にて紹介されている、Homebrew で libiconv をインストールする方式を取りました。

3. configureスクリプトを生成・実行

必要なものがインストールできたらコマンドを打っていきます。

まずはbuildconfスクリプト(前述したとおりAutoconfのラッパースプリプトです)を実行することでAutoconfを通じてconfigureスクリプトを生成します。

$ ./buildconf

buildconf: Checking installation
buildconf: autoconf version 2.69 (ok)
buildconf: Cleaning cache and configure files
buildconf: Rebuilding configure
buildconf: Rebuilding main/php_config.h.in
buildconf: Run ./configure to proceed with customizing the PHP build.

Run ./configure to proceed with customizing the PHP build.というメッセージの通り、生成したconfigureスクリプトを実行(./configure)していきます。なお、ここでbisonのパス指定が必要なので、環境変数YACCにはbisonのパスを指定します。
また、onigurumaのpkgconfigへのパスを環境変数に設定します(これは後に説明するエラーを回避するためです)。

$ YACC="/usr/local/Cellar/bison/3.7.4/bin/bison" \
PKG_CONFIG_PATH="/usr/local/opt/oniguruma/lib/pkgconfig" \
./configure --prefix="/opt/php-master" \
--enable-cli \
--enable-mbstring \
--enable-debug \
--with-iconv=$(brew --prefix libiconv)


checking for grep that handles long lines and -e... /usr/bin/grep
checking for egrep... /usr/bin/grep -E
checking for a sed that does not truncate output... /usr/bin/sed
checking build system type... x86_64-apple-darwin19.6.0
checking host system type... x86_64-apple-darwin19.6.0
checking target system type... x86_64-apple-darwin19.6.0
checking for pkg-config... /usr/local/bin/pkg-config
checking pkg-config is at least version 0.9.0... yes
checking for cc... cc
checking whether the C compiler works... yes
checking for C compiler default output file name... a.out
checking for suffix of executables...
checking whether we are cross compiling... no
checking for suffix of object files... o
checking whether we are using the GNU C compiler... yes
checking whether cc accepts -g... yes
checking for cc option to accept ISO C89... none needed

...(省略)

+--------------------------------------------------------------------+
| License:                                                           |
| This software is subject to the PHP License, available in this     |
| distribution in the file LICENSE. By continuing this installation  |
| process, you are bound by the terms of this license agreement.     |
| If you do not agree with the terms of this license, you must abort |
| the installation process at this point.                            |
+--------------------------------------------------------------------+

Thank you for using PHP.

configureに対するオプションは、エラーに対するトラブルシューティングの結果次のコマンドになっています。どのようなエラーが発生しうるかについて以下に列挙いたします。

trouble shooting: Please specify the install prefix of iconv

次のようなエラーが発生する場合があります。

Please specify the install prefix of iconv

こちらのエラーについてはすでに前述していますが、必要なものをインストールしてオプションで指定する必要があります。

configure: error: Please specify the install prefix of iconv with --with-iconv=<DIR>

また、上の記事で --enable-debug というオプションがビルドデバックに役立つという情報があります。デバックで詰まった場合は--enable-debugを有効にして情報量を増やしてみてください。

対応策としては、

  1. --without-iconv
  2. brew install libiconvして、--with-iconv=$(brew --prefix libiconv)を設定

があり、2を選択しています。

trouble shooting: No package 'oniguruma' found

これも前述していますが、onigurumaが足りないというエラーです。

configure: error: Package requirements (oniguruma) were not met:

No package 'oniguruma' found

Consider adjusting the PKG_CONFIG_PATH environment variable if you
installed software in a non-standard prefix.

Alternatively, you may set the environment variables ONIG_CFLAGS
and ONIG_LIBS to avoid the need to call pkg-config.
See the pkg-config man page for more details.

そのため、brew install onigurumaにてインストールし、PKG_CONFIG_PATHに追加しました。

4. makeでビルドする

configureスクリプトを実行し各種チェック成功後、Makefile等が生成されます。続けて make の実行に続きます。

$ make

/bin/sh /Users/kazukihigashiguchi/src/github.com/php/php-src/libtool --silent --preserve-dup-deps --mode=install cp ext/opcache/opcache.la /Users/kazukihigashiguchi/src/github.com/php/php-src/modules

...(省略)


Build complete.
Don't forget to run 'make test'.

Build complete.と出れば成功です。

Makefileの仕様は最初のターゲットを対象とするのが仕様です[3][4]

make starts with the first target (not targets whose names start with ‘.’)

生成されたMakefileの最初のターゲットはallなため、次のようなスクリプトが実行されます。

all: $(all_targets)
	@echo
	@echo "Build complete."
	@echo "Don't forget to run 'make test'."
	@echo

$(all_targets)が指す先で、各モジュールのビルドが定義されています。

5. テストを実行する

$ make test

これでテストスクリプト phpt が実行されます。

次のようなメッセージが出ればテスト終了です。

=====================================================================
TIME END 2020-11-21 22:49:38

=====================================================================
TEST RESULT SUMMARY
---------------------------------------------------------------------
Exts skipped    :   45
Exts tested     :   27
---------------------------------------------------------------------

Number of tests : 16033             11240
Tests skipped   : 4793 ( 29.9%) --------
Tests warned    :    0 (  0.0%) (  0.0%)
Tests failed    :    4 (  0.0%) (  0.0%)
Expected fail   :   31 (  0.2%) (  0.3%)
Tests passed    : 11205 ( 69.9%) ( 99.7%)
---------------------------------------------------------------------
Time taken      :  678 seconds
=====================================================================

今回は開発時点最新のブランチを利用している&&とくに実運用のサーバーに突っ込むことを前提としていないのでTests failedが4つ(0.0%)あることはスルーしてインストールへ進みます。
なお、テスト失敗したときにメーリングメストに連絡するフローがあります。必要であればレポーティングしましょう。

You may have found a problem in PHP.
This report can be automatically sent to the PHP QA team at
http://qa.php.net/reports and http://news.php.net/php.qa.reports
This gives us a better understanding of PHP's behavior.
If you don't want to send the report immediately you can choose
option "s" to save it.	You can then email it to qa-reports@lists.php.net later.
Do you want to send this report now? [Yns]: 

PHP内部コードのテスト実行構造

さて、make testした際の挙動ですが簡略化した図にするとこのような実行フローとなっています。

make test時の挙動ですがまずPHPが実行可能かどうかを検証しています。ここでいうとPHPが実行可能かどうかは、CLI SAPIがあるかを確かめています。後ほど詳述しますが、SAPIとはPHPと「外」の間を取り持つメカニズムです。CLI SAPIはその中の一つで、「CLIという外」とPHPの間を取り持っています。

PHP_EXECUTABLE = $(top_builddir)/$(SAPI_CLI_PATH)

# (省略)

test: all
	@if test ! -z "$(PHP_EXECUTABLE)" && -x "$(PHP_EXECUTABLE)"; then \
		# (省略)テスト実行
	else \
		echo "ERROR: Cannot run tests without CLI sapi."; \
	fi

CLI SAPI有効でない場合はその時点でテスト実行は不可能として終了しています。
CLI SAPIが有効でありテストが実行できる場合ifの中でCLI SAPIを通じてrun-tests.phpというPHPコードを実行します。

Makefileでは次のようなコードがそれに該当します。

TEST_PHP_EXECUTABLE=$(PHP_EXECUTABLE) \
TEST_PHP_SRCDIR=$(top_srcdir) \
CC="$(CC)" \
$(PHP_EXECUTABLE) -n -c $(top_builddir)/tmp-php.ini $(PHP_TEST_SETTINGS) $(top_srcdir)/run-tests.php -n -c $(top_builddir)/tmp-php.ini -d extension_dir=$(top_builddir)/modules/ $(PHP_TEST_SHARED_EXTENSIONS) $(TESTS); \

様々な環境変数を設定して長くなっていますが、超絶簡略化するとこのコードは次のようなコマンドを実行しているのと同義です。

$ ./sapi/cli/php ./run-tests.php

CLI SAPIの実行ファイルである./sapi/cli/phpを用いてrun-tests.phpを実行しています。Makefileで発行されるコマンドはこれに対して様々なオプションを付けています。

$ ./sapi/cli/php -n \
-c ./tmp-php.ini \
-d open_basedir= -d output_buffering=0 \
-d memory_limit=-1 \
./run-tests.php -n -c ./tmp-php.ini \
-d extension_dir=./modules/ \
-d zend_extension=./modules/opcache.so

(※ 正確には./sapi/cli/phpなどの箇所は絶対パスですが、記事内での視認性のため相対パスにしています。)

そして、run-tests.phpは「*.phptファイルをあつめ」、「phptファイル形式を解釈してテスト対象の実行」、「検証」・「結果をレポート」しています。

run-tests.php内のphptファイルをあつめる箇所
            if (!$testfile && strpos($argv[$i], '*') !== false && function_exists('glob')) {
                if (substr($argv[$i], -5) == '.phpt') {
                    $pattern_match = glob($argv[$i]);
                } else {
                    if (preg_match("/\*$/", $argv[$i])) {
                        $pattern_match = glob($argv[$i] . '.phpt');
                    } else {
                        die('Cannot find test file "' . $argv[$i] . '".' . PHP_EOL);
                    }
                }

*.phptファイルはこのような形式のテストスクリプトです。

--TEST--
strtr() function - basic test for strtr()
--FILE--
<?php
/* Do not change this test it is a README.TESTING example. */
$trans = array("hello"=>"hi", "hi"=>"hello", "a"=>"A", "world"=>"planet");
var_dump(strtr("# hi all, I said hello world! #", $trans));
?>
--EXPECT--
string(32) "# hello All, I sAid hi planet! #"

このファイルをPHPで解釈しファイルを実行・var_dump()等で出力した内容を期待値(EXPECT)と突き合わせます。

run-tests.phpにて結果比較しているコード例
        // compare and leave on success
        if (!strcmp($output, $wanted)) {
            $passed = true;

そして、最後この結果はサマリーとして出力されます。

run-tests.phpでテスト結果サマリーを出力している
    $summary .= '
Tests passed    : ' . sprintf('%4d (%5.1f%%)', $sum_results['PASSED'], $percent_results['PASSED']) . ' ' . sprintf('(%5.1f%%)', $x_passed) . '
---------------------------------------------------------------------
Time taken      : ' . sprintf('%4d seconds', $end_time - $start_time) . '
=====================================================================
';

このような設計・実装によって、PHPの内部コードはテストされています。

詳述: CLI SAPIとはなにか

さきほど、ERROR: Cannot run tests without CLI sapi.とある通り、CLI SAPIがないとテストは実行できません。そもそも、CLI SAPIとはなにかを説明します。

PHPユーザーに身近な場所ではここに出ています。

$ /opt/php-master/bin/php -v

PHP 8.1.0-dev (**cli**) (built: Nov 22 2020 07:37:38) ( NTS DEBUG )
Copyright (c) The PHP Group
Zend Engine v4.1.0-dev, Copyright (c) Zend Technologies

「バージョンを確認するぞ」とコマンドを打ったときに表示されてるcliを指します。これは echo php_sapi_name() で出てくる値と同一です。

意外と身近なところで出ているこちらの概念ですが、そもそもSAPIとはServer Application Program Interfaceの略です。

PHPにおいては、PHP/Zend Engineと「外」の相互作用を制御するメカニズムです。SAPIには、apachecgiなどさまざまなSAPIのバリエーションがあります。具体的には次のリストです。

  • apache
  • apache2handler
  • cgi (until PHP 5.3)
  • cgi-fcgi
  • cli
  • cli-server
  • embed
  • fpm-fcgi
  • litespeed
  • nsapi
  • phpdbg

PHPではCLIも提供しており、それがCLI SAPIです。さらに細かい点ではCLI SAPIは他のSAPIとはいくつか相違点があることを公式ドキュメントでは示しています。

  • CGI SAPIと異なりヘッダに出力を書き込まない点
  • CGI SAPIにはHTTPヘッダを抑制する方法を提供しているが、CGI SAPIにはその有効スイッチがない。
  • 出力はHTMLフォーマットではなくプレインテキストである。
  • html_errorsmax_execution_timeなどWebベースのスクリプト用途の php.ini のディレクティブはCLI SAPIによって上書きされる

長々と来たが、ゆえにconfigureで作成されるMAKEFILE内のSAPI_CLI_PATH定義にはと、デフォルト例として示したsapi/cli/phpが設定されている。

さきほどconfigureスクリプトを実行する際に--enable-cliオプションをつけましたが、その設定によってCLI SAPIが有効化されます[5]

./configure --prefix="/opt/php-master" \
--enable-cli \
--enable-mbstring \
--enable-debug \
--with-iconv=$(brew --prefix libiconv)

The CLI SAPI is enabled by default using --enable-cli, but may be disabled using the --disable-cli option when running ./configure.

6. ビルドしたものをinstallする

ビルドが完了したら最後インストールします。--prefix="/opt/php-master"にて指定した際にインストールされます。

$ sudo make install

Installing shared extensions:     /opt/php-master/lib/php/extensions/debug-non-zts-20201009/
Installing PHP CLI binary:        /opt/php-master/bin/
Installing PHP CLI man page:      /opt/php-master/php/man/man1/
Installing phpdbg binary:         /opt/php-master/bin/
Installing phpdbg man page:       /opt/php-master/php/man/man1/
Installing PHP CGI binary:        /opt/php-master/bin/
Installing PHP CGI man page:      /opt/php-master/php/man/man1/
Installing build environment:     /opt/php-master/lib/php/build/
Installing header files:          /opt/php-master/include/php/
Installing helper programs:       /opt/php-master/bin/
  program: phpize
  program: php-config
Installing man pages:             /opt/php-master/php/man/man1/
  page: phpize.1
  page: php-config.1
/Users/kazukihigashiguchi/src/github.com/php/php-src/build/shtool install -c ext/phar/phar.phar /opt/php-master/bin/phar.phar
ln -s -f phar.phar /opt/php-master/bin/phar
Installing PDO headers:           /opt/php-master/include/php/ext/pdo/

7. CLIでバージョンを表示して動作確認

ビルド先でPHPのバージョンが表示されれば完了です。

% /opt/php-master/bin/php -v
PHP 8.1.0-dev (cli) (built: Nov 22 2020 07:37:38) ( NTS DEBUG )
Copyright (c) The PHP Group
Zend Engine v4.1.0-dev, Copyright (c) Zend Technologies

おわりに

当記事では、実際にPHPソースコード php-src をクローンしてきてからビルドするまでの流れとその過程で現れる概念について解説いたしました。

PHP8をきっかけに「PHPの内部コードをちょっと覗いてみよう」といった好奇心や「自分でビルドしてみよう」といったお試し心ができた際に参考になれば幸いです。

脚注
  1. 動作環境は、macOS Catalina (Version 10.15.7)です ↩︎

  2. 私が初めてPHPをビルドした際にはこちらの記事( https://qiita.com/DQNEO/items/9aed9ee3136d5f3b7fbd )を非常に参考にしました ↩︎

  3. .から始まるものを除きます ↩︎

  4. Makefileの仕様上は、.DEFAULT_GOALを指定することでターゲットを変更することも可能です ↩︎

  5. この設定はデフォルトで有効なので今回のケースでは特段指定しなければならないわけではありません。 ↩︎