y_uti のブログ

統計、機械学習、自然言語処理などに興味を持つエンジニアの技術ブログです

php-build をカスタマイズして使う

以前このブログでも紹介したことがありますが、php-build というビルドツールが便利で、愛用しています。phpenv と php-build を組み合わせて使うことで、複数のバージョンの PHP共存可能な形でインストールし、切り替えて使うことができます。バージョンごとの PHP の動作を比較したり、処理系の実装を変更して独自にビルドする用途にも便利です。
github.com

php-build には、ビルドの前後にユーザが独自の処理を実行できる仕組みが用意されています。今回は php-build のソースコードを追いながら、この仕組みを利用して php-build の処理をカスタマイズする方法を説明します。

php-build のファイル構成

はじめに php-build のファイル構成を示します。ソースツリーの一部を抜粋したものが以下になります。bin/php-build が全体の処理を実行する bash スクリプトです。php-build で PHP のビルドを行うには、このスクリプトを実行します*1

$ LANG=C tree -F php-build/
php-build/
|-- CHANGELOG.md
|-- CONTRIBUTING.md
  ...
|-- bin/
|   |-- php-build*
  ...
|-- share/
|   `-- php-build/
|       |-- after-install.d/
|       |-- before-install.d/
|       |-- default_configure_options
|       |-- definitions/
|       |   |-- 5.2.17
|       |   |-- 5.3.10
  ...
|       |-- extension/
|       |   |-- definition
|       |   `-- extension.sh
|       |-- patches/
|       |   |-- gmp.c.patch
|       |   |-- php-5.4.6-libxml2-2.9.patch
  ...
|       `-- plugins.d/
|           |-- apc.sh
|           |-- composer.sh
  ...
11 directories, 173 files

share/php-build ディレクトリ以下に、php-build の処理をカスタマイズするファイルが置かれています。bin/php-build スクリプトは、share/php-build 以下のファイルを source またはサブプロセスとして実行しながら処理を進めます。ここに独自のファイルを追加することで、php-build の動作をカスタマイズできます。

definitions

definitions ディレクトリの各ファイルは、PHP の各バージョンについて、ビルド全体の処理の流れを記述したものです。いくつかのバージョンについて内容を見てみます。

PHP 5.6.16 では次のようになります。ソースコードの URL が指定されているほか、Xdebug 2.3.3 をインストールすること、OPcache を有効にすることが指示されています。

$ cat 5.6.16
install_package "https://secure.php.net/distributions/php-5.6.16.tar.bz2"
install_xdebug "2.3.3"
enable_builtin_opcache

PHP 7.0.0 では次のようになります。PHP 7.0.0 では Xdebug のバージョンを指定せずに master ブランチから取得してインストールするようです。

$ cat 7.0.0
install_package "https://secure.php.net/distributions/php-7.0.0.tar.bz2"
install_xdebug_master
enable_builtin_opcache

古い PHP の例として 5.3.29 も見てみます。PHP 5.3 には OPcache が含まれないので enable_builtin_opcache の記述がありません。

$ cat 5.3.29
install_package "https://secure.php.net/distributions/php-5.3.29.tar.bz2"
install_xdebug "2.2.7"

さて、実はこれらのファイルは単なる bash スクリプトで、bin/php-build の以下の箇所で source されて実行される仕組みになっています。つまり、install_package や install_xdebug, enable_builtin_opcache といった記述は、それぞれ、bin/php-build またはその他のファイルで定義された関数名です。
https://github.com/php-build/php-build/blob/51dc7e50c4cd18507e8f70087bef857e6a136901/bin/php-build#L723

install_package 関数は、引数に指定された URL からソースコードをダウンロードして展開、ビルド、インストールまでを実行する関数です。この関数で php-build の最も主要な処理が実行されることになります。install_package 関数の定義は以下にあります。download 関数でソースコードのダウンロードと展開、build_package 関数でビルド、インストールが実行されます。
https://github.com/php-build/php-build/blob/14e5b43fb693ae7c0b9430859730d07e86aac378/bin/php-build#L400-L410

このような仕組みになっているので、definitions ディレクトリのファイルを変更、追加することで、独自のビルド処理を記述できます。bash スクリプトなので好きに書けるのですが、配布されているファイルにはどういう処理が書かれているのか、一覧を見てみます。

$ cat * | cut -f1 -d' ' | sort | uniq

configure_option
enable_builtin_opcache
install_package
install_package_from_github
install_xdebug
install_xdebug_master
patch_file

これらのうち、install_xdebug と install_xdebug_master 以外は bin/php-build で定義されています。configure_option は ./configure に渡される引数を指定するものです。patch_file は make コマンドの実行前に PHPソースコードにパッチを当てるもので、主に、過去の PHP を現在の環境でビルドするために使われているようです*2

install_xdebug と install_xdebug_master は、plugins.d/xdebug.sh ファイルで定義されています。plugins.d ディレクトリと extension ディレクトリには、php-build で拡張モジュールをインストールするための仕掛けがあります。これについては本記事の最後で説明します。

ところで、definitions ディレクトリのファイルを変更したり追加したりすると、git 管理下のファイルに差分が発生します。php-build は定義ファイルのパスを指定して実行することもできるので、差分の発生を避けたければ、任意のディレクトリに定義ファイルを作成して問題ありません。また、definitions ディレクトリのパスは、bin/php-build で以下のように PHP_BUILD_DEFINITION_PATH 変数に設定されています。したがって、この変数を環境変数に定義しておけば、独自に作成したディレクトリを既定の場所にすることもできます。

# Initialize the builtin definition path.
[ -z "$PHP_BUILD_DEFINITION_PATH" ] && PHP_BUILD_DEFINITION_PATH="$PHP_BUILD_ROOT/share/php-build/definitions"

https://github.com/php-build/php-build/blob/14e5b43fb693ae7c0b9430859730d07e86aac378/bin/php-build#L69-L70

default_configuration_options

default_configuration_options ファイルには ./configure に渡されるオプションの既定値が書かれています。bin/php-build ファイルの以下の箇所で CONFIGURE_OPTIONS 変数に読み込まれます。
https://github.com/php-build/php-build/blob/14e5b43fb693ae7c0b9430859730d07e86aac378/bin/php-build#L72-L74

このファイルの内容を変更すれば、PHP ビルド時の既定の設定を変更できます。たとえば私は PHP-FPM ではなく Apache モジュールを使っているので、手元の環境では --enable-fpm の行を削除して --with-apxs2 を追加しています。

./configure に渡すオプションをカスタマイズするには、default_configuration_options の内容を直接書き換える方法のほかに、先ほど説明した定義ファイルに configure_option を記述する方法があります。また、オプションを追加するだけなら、PHP_BUILD_CONFIGURE_OPTS または CONFIGURE_OPTS を指定する方法もあります。これら二つの変数は、以下の箇所で CONFIGURE_OPTIONS に追加されます。
https://github.com/php-build/php-build/blob/14e5b43fb693ae7c0b9430859730d07e86aac378/bin/php-build#L86-L92

設定された CONFIGURE_OPTIONS 変数が bin/php-build スクリプトのどこで利用されているのか、処理の流れを簡単に追っておきます。前節では、bin/php-build スクリプトから定義ファイルが source され、そこから install_package 関数が実行され、ソースコードをダウンロードした後 build_package 関数が呼ばれるところまで説明しました。build_package 関数の次の箇所でconfigure_package 関数が呼ばれます。
https://github.com/php-build/php-build/blob/14e5b43fb693ae7c0b9430859730d07e86aac378/bin/php-build#L319

configure_packages 関数で ./configure が実行されます。このとき、次のように CONFIGURE_OPTIONS 変数の内容がオプションに渡されます。

function configure_package() {
  ...

    local argv="--with-config-file-path="$PREFIX/etc" \
--with-config-file-scan-dir="$PREFIX/etc/conf.d" \
--prefix=$PREFIX \
--libexecdir=$PREFIX/libexec \
$CONFIGURE_OPTIONS"

  ...

    ./configure $argv > /dev/null

  ...
}

https://github.com/php-build/php-build/blob/14e5b43fb693ae7c0b9430859730d07e86aac378/bin/php-build#L500-L579

before-install.d

before-install.d ディレクトリには、PHP をビルドする前に実行させたいスクリプトを作成できます。bin/php-build は ./configure を実行した後、このディレクトリに置かれたファイルを順に実行します。具体的にソースコードを追ってみましょう。次の箇所で trigger_before_install 関数が呼ばれます。
https://github.com/php-build/php-build/blob/14e5b43fb693ae7c0b9430859730d07e86aac378/bin/php-build#L321

trigger_before_install 関数は次のように実装されています。この実装からわかるように、スクリプトは source されるのではなくサブプロセスとして起動されます。

function trigger_before_install() {
    export PHP_BUILD_ROOT
    export PREFIX
    export SOURCE_PATH="$1"

    local triggers_dir="$PHP_BUILD_ROOT/share/php-build/before-install.d/"
    local triggers=$(ls "$triggers_dir")

    if [ -n "$triggers" ]; then
        for trigger in "$triggers_dir"*; do
            log "Before Install Trigger" "$(basename $trigger)"
            /usr/bin/env PATH="$PREFIX/bin:$PATH" "$trigger" 2>&4
        done
    fi
}

https://github.com/php-build/php-build/blob/14e5b43fb693ae7c0b9430859730d07e86aac378/bin/php-build#L255-L269

作成したスクリプト内では、PHP_BUILD_ROOT, PREFIX, SOURCE_PATH という変数を利用できます。たとえば、php-build が phpenv のプラグインとして導入されている環境で PHP 7.0.0 をインストールする場合、次の表に示した値になります。

変数名 意味 値 (PHP 7.0.0 インストール時)
PHP_BUILD_ROOT php-build のインストールディレクトリ $PHPENV_ROOT/plugins/php-build
PREFIX PHP のインストールディレクトリ $PHPENV_ROOT/versions/7.0.0
SOURCE_PATH PHP のソースディレクトリ /tmp/php-build/sources/7.0.0

before-install.d に作成するスクリプトの例を示します。私は次のようなスクリプトを書いて利用しています。これは、Apache モジュールのインストール先を、php-build で作成される PHP のインストールディレクトリ配下に変更するものです*3。./configure によって生成された Makefile を変更する方法で実現しています。

#!/usr/bin/env bash
if type apxs &>/dev/null; then
    apxs_libexecdir=$(apxs -q LIBEXECDIR)
    sed -i -e "s|'\$(INSTALL_ROOT)$apxs_libexecdir'|$PREFIX/libexec|g" $SOURCE_PATH/Makefile
fi

なお before-install.d ディレクトリは .gitignore に含まれています。したがって、ここに作成したスクリプトは git の管理下には入りません。

patches.d

patches.d ディレクトリには、PHP のソースツリーに適用されるパッチファイルが置かれています。before-install.d ディレクトリのスクリプトが実行された後に、パッチが適用されます。これは、bin/php-build の以下の箇所から apply_patches 関数が呼ばれて処理されます。
https://github.com/php-build/php-build/blob/14e5b43fb693ae7c0b9430859730d07e86aac378/bin/php-build#L323

patches.d ディレクトリに独自のパッチファイルを追加しておけば、make の実行前に PHPソースコードを修正できます。apply_patches 関数を読むと処理の内容がわかると思いますので、ここでは説明を省略します。

after-install.d

after-install.d ディレクトリには、定義ファイルに書かれたすべての処理を終えた後に実行したいスクリプトを作成できます。たとえば PHP 7.0.0 の定義ファイルは次のとおりでしたので、PHP のビルド、インストールだけではなく、Xdebug のインストール、OPcache の有効化といった一連の処理をすべて終えた後に実行されます。

install_package "https://secure.php.net/distributions/php-7.0.0.tar.bz2"
install_xdebug_master
enable_builtin_opcache

bin/php-build の次の箇所から trigger_after_install 関数が呼ばれます。定義ファイルを source して処理を終えた直後に呼ばれていることがわかります。
https://github.com/php-build/php-build/blob/14e5b43fb693ae7c0b9430859730d07e86aac378/bin/php-build#L726

関数の実装は以下のとおりです。trigger_before_install 関数と同様に、after-install.d ディレクトリの各ファイルをサブプロセスとして実行します。

function trigger_after_install() {
    export PHP_BUILD_ROOT
    export PREFIX

    after_install "$PREFIX" 2>&4

    local triggers_dir="$PHP_BUILD_ROOT/share/php-build/after-install.d/"
    local triggers=$(ls "$triggers_dir")

    if [ -n "$triggers" ]; then
        for trigger in "$triggers_dir"*; do
            log "After Install Trigger" "$(basename $trigger)"
            /usr/bin/env PATH="$PREFIX/bin:$PATH" "$trigger" 2>&4
        done
    fi
}

https://github.com/php-build/php-build/blob/14e5b43fb693ae7c0b9430859730d07e86aac378/bin/php-build#L271-L286

trigger_after_install 関数から呼ばれている after_install 関数は、バージョンごとに異なる処理を実行させるためのスタブです。bin/php-build での実装は次のとおりで、特に何も処理されません。定義ファイル内で関数を上書きして個別の処理を実装することを意図しているようです。

# after_install(BUILD_DIR)
#
# Gets called after install has finished.
# Implement this function in your definitions.
function after_install() {
    local stub=1
}

https://github.com/php-build/php-build/blob/14e5b43fb693ae7c0b9430859730d07e86aac378/bin/php-build#L288-L294

after-install.d に作成するスクリプトの例を示します。before-install.d の例と同様に、私の手元の環境で利用しているものです。次のスクリプトでは php.ini ファイルを書き換えて memory_limit を 1024MB に、タイムゾーンを Asia/Tokyo に設定しています。

#!/usr/bin/env bash
inifile="$PREFIX/etc/php.ini"
if [ -f "$inifile" ]; then
    sed -i -e 's/^memory_limit =.*$/memory_limit = 1024M/' $inifile
    sed -i -e 's/^;date.timezone =.*$/date.timezone = Asia\/Tokyo/' $inifile
fi

after-install.d も .gitignore に含まれているので、作成したスクリプトは git の管理下には入りません。

extensions

ここまでに説明したすべての処理を終えた後に、拡張モジュールをダウンロード、インストールすることもできます。それには、次のように PHP_BUILD_INSTALL_EXTENSION 変数を設定して php-build を実行します。この例では、バイトコード命令をダンプする vld モジュールと PHP から抽象構文木を扱えるようにする ast モジュールをインストールするように指示しています。

$ PHP_BUILD_INSTALL_EXTENSION='vld=@ ast=@' phpenv install 7.0.0

実は、上記のように実行するためには、あらかじめ extensions ディレクトリにある definition という名前のファイルを編集しておく必要があります。これは次のような csv 形式のファイルで、拡張モジュールのダウンロード URL などを管理するものです。

$ cat definition
"name","url-dist","url_source","source_cwd","configure_args","extension_type","after_install"
"apc","http://pecl.php.net/get/APC-$version.tgz","git@git.php.net:/pecl/caching/apc.git",,"--enable-apc","extension",
"apcu","http://pecl.php.net/get/apcu-$version.tgz","https://github.com/krakjoe/apcu.git",,,"extension",
...

たとえば私は、vld と ast をインストールするために次のように追記しています。

...
"ast",,"https://github.com/nikic/php-ast.git",,,"extension",
"vld","http://pecl.php.net/get/vld-$version.tgz","https://github.com/derickr/vld.git",,,"extension",

各カラムの意味は次のとおりです。1 列目の name と 6 列目の extension_type は必須です。また、2 列目の url-dist と 3 列目の url_source のいずれか一方は必須です。その他のカラムは空でも構いません。これ以降では、bin/php-build のソースコードと照らし合わせながら、これらの記述がどのように利用されるのかを追ってみます。

列番号 カラム名 意味
1 name モジュール名
2 url-dist バージョン指定時のダウンロード URL
3 url_source git リポジトリの URL
4 source_cwd ビルドを実行するディレクトリ (相対パス)
5 configure_args ./configure 実行時のオプション
6 extension_type extension または zend_extension
7 after_install インストール後の追加処理を行う関数名

bin/php-build の以下の箇所から install_extensions 関数が呼ばれることで、拡張モジュールのインストール処理が始まります。これは trigger_after_install 関数の後に呼ばれます。
https://github.com/php-build/php-build/blob/14e5b43fb693ae7c0b9430859730d07e86aac378/bin/php-build#L729

install_extensions 関数の実装は以下のとおりです。PHP_BUILD_INSTALL_EXTENSION 変数からモジュール名とバージョンを切り出して install_extension_source 関数または install_extension 関数を呼び出します。右辺の 1 文字目が "@" ならリビジョン番号とみなして install_extension_source 関数が、それ以外ならバージョン番号とみなして install_extension 関数が呼ばれます。

function install_extensions() {
    # handle extensions that should be installed by defined environment variable
    # variable must be in the format: extension_name=version extension_name=version
    for extension_def in $PHP_BUILD_INSTALL_EXTENSION; do
        local extension=$(echo $extension_def | cut -d"=" -f1)
        local version=$(echo $extension_def | cut -d"=" -f2)
        local first_char=$(echo $version | cut -c1 )

        # if first character of version is an "@" it's meant to be a revision
        if [ $first_char = "@" ]; then
            local version=$(echo $version | cut -c"2-")
            install_extension_source $extension "$version"
        else
            install_extension $extension $version
        fi
    done
}

https://github.com/php-build/php-build/blob/14e5b43fb693ae7c0b9430859730d07e86aac378/bin/php-build#L376-L392

install_extension_source 関数、install_extension 関数は、どちらも extension/extension.sh に定義されています。ここで先ほどの extension/definition ファイルからモジュール名の一致する行を読み取り、_download_extension 関数または _checkout_extension 関数を呼び出してソースコードを取得します。_download_extension 関数では 2 列目の url-dist が、_checkout_extension 関数では 3 列目の url_source が利用されます。いずれの場合でも、ソースコードの取得後に _build_extension 関数を呼び出してモジュールをビルドします。
https://github.com/php-build/php-build/blob/14e5b43fb693ae7c0b9430859730d07e86aac378/share/php-build/extension/extension.sh#L9-L44

_build_extension 関数は、同じファイルの以下の箇所で実装されています。
https://github.com/php-build/php-build/blob/14e5b43fb693ae7c0b9430859730d07e86aac378/share/php-build/extension/extension.sh#L115-L161

拡張モジュールをビルドする処理は次のように実装されています。extension/definition ファイル 4 列目の source_cwd に指定されたディレクトリ内でビルドを実行します。./configure スクリプトの実行時には、5 列目の configure_args に記述されたオプションが渡されます。

  ...
    cd "$source_dir/$source_cwd"

    {
        $PREFIX/bin/phpize > /dev/null
        "$(pwd)/configure" --with-php-config=$PREFIX/bin/php-config \
            $configure_args > /dev/null

        make > /dev/null
        make install > /dev/null
    } >&4 2>&1
  ...

拡張モジュールのインストール後、次の実装によって、etc/conf.d ディレクトリに設定ファイルを作成します。このとき、6 列目の extension_type にしたがって extension または zend_extension が出力されます。

  ...
    local extension_ini="$PREFIX/etc/conf.d/$name.ini"

    if [ ! -f "$extension_ini" ]; then
        log "$name" "Installing $name configuration in $extension_ini"

        echo "$extension_type=\"$name.so\"" > $extension_ini
    fi
  ...

最後に、7 列目の after_install に関数名が指定されていれば、その関数を呼び出してインストール後の処理を実行します。たとえば Xebug では xdebug_after_install 関数が指定されています。この関数は plugins.d/xdebug.sh ファイルに定義されています。

  ...
    if [ -n "$after_install" ]; then
        # Zend extensions are not looked up in PHP's extension dir, so
        # we need to find the absolute path for the extension_dir.
        local extension_dir=$("$PREFIX/bin/php-config" --extension-dir)

        $after_install "$source_dir" "$extension_ini" "$extension_type" "$extension_dir" "$extension_home"
    fi
  ...

以上、拡張モジュールをインストールする処理の実装を駆け足で見てきました。このような仕掛けで、PHP_BUILD_INSTALL_EXTENSION 変数の指定による拡張モジュールのインストールを行えます。

拡張モジュールをインストールする別の方法として、定義ファイルに記述することもできます。たとえば vld をインストールするには、定義ファイルに次のように書くこともできます。この記述によって、extension/extension.sh の install_extension_source 関数が直接呼び出されて vld がインストールされます。

...
install_extension_source "vld"

また、plugins.d ディレクトリにファイルを作成することもできます。plugins.d/vld.sh ファイルを次のように作成しておけば、定義ファイルで install_vld_master, install_vld という記述を使えるようになります。冒頭に見た install_xdebug_master, install_xdebug などは、この方式で実装されています。

#!/usr/bin/env bash

function install_vld_master {
    install_extension_source "vld" "$1"
}

function install_vld {
    install_extension "vld" "$1"
}

なお、通常は特に問題にならないと思いますが、PHP_BUILD_INSTALL_EXTENSION 変数を指定して拡張モジュールをインストールする場合と、定義ファイルに記述してインストールする場合では、拡張モジュールがインストールされるタイミングが異なることに注意してください。PHP_BUILD_INSTALL_EXTENSION 変数を指定した場合には after_install.d ディレクトリのスクリプトの実行後にインストールされます。一方、定義ファイルに記述した場合には、after_install.d ディレクトリのスクリプトが実行されるよりも前に拡張モジュールがインストールされます。

まとめ

php-build の実装を追いながら、処理をカスタマイズする方法を紹介しました。まとめると、php-build では次の手順で PHP をビルドします。処理の各所でユーザが作成したスクリプトを呼び出せるようになっており、これを利用して php-build の処理をカスタマイズできます。

  1. definitions ディレクトリに置かれた定義ファイルが source される
  2. 定義ファイルの記述から install_package 関数が呼ばれて PHP がビルドされる
  3. ./configure と make の間で before_install.d ディレクトリのスクリプトが実行される
  4. 定義ファイルに書かれた処理の終了後に after_install.d ディレクトリのスクリプトが実行される
  5. 最後に PHP_BUILD_INSTALL_EXTENSION 変数に指定された拡張モジュールがインストールされる

*1:bin ディレクトリにあるその他のスクリプトは、php-build を phpenv のプラグインとして利用するためのものです。この記事では bin/php-build 以外のスクリプトについては説明しません。

*2:definitions ディレクトリのファイルの中で patch_file が指定されているのは、すべて PHP 5.4 以前のものでした。

*3:anyenv + phpenv + php-build で複数のバージョンの PHP を切り替える - y_uti のブログ で紹介した内容を書き直したものです。