php-buildでの複数PHPバージョンビルドを自動化する

BASE Advent Calendar 2019 Day 16

この記事はBASE Advent Calendar 2019の16日目の記事です。

devblog.thebase.in

エンジニアの田中(@tenkoma)です。

あなたのマシンにインストールされているPHPのバージョンは何ですか?

仮想マシンやコンテナで開発環境を作ることが増えているので、ホストOSにはPHPが入ってない・気に掛けたことがない、ということも多いかもしれません。 僕は、新しいバージョンを試すためにphp-buildを使ってmacOSでビルド・インストールしています。(また、プロジェクト毎にバージョンの切り替えがしやすいようdirenvを使っています)

今回はphp-buildを使った複数バージョンビルドを、コードを書いて少し省力化してみたので紹介します。

f:id:tenkoma:20191210151523p:plain
多くのバージョンのPHPをそろえてみました。ただし、Catalinaでは7.0.19未満の動作が実現できていません

前提

この記事で紹介するコードは以下の環境で実行しています。

  • OS: macOS 10.15 Catalina
  • 依存ライブラリのインストールはHomebrew
  • php-buildはmotemen/ghqでローカル環境にclone

php-buildの導入・トラブルシュートは以下の記事が参考になります。

作った理由

php-buildを使うと、自分でソースコードをダウンロードしてビルドするよりは楽に、ビルド・インストールができます。(ただし、ビルドエラー時に必要な依存ライブラリについて調査したりするので、導入時にある程度の知識や調査の時間が必要です)

例えば、以下のようなコマンドでビルド+インストールします。

$ php-build -i development 7.3.12 ~/local/php/7.3.12/

これでインストールできたらめでたいのですが、macOS をアップグレードしていくと、なぜか依存ライブラリが見つからなくなるようになってきたので、ビルドのためのオプションを付けて以下のように実行しています。(macOS Catalina 10.15.1にて実行)

$ PHP_BUILD_CONFIGURE_OPTS="--with-zlib-dir=$(brew --prefix zlib) --with-bz2=$(brew --prefix bzip2) --with-iconv=$(brew --prefix libiconv) --with-libedit=$(brew --prefix libedit) --with-openssl=$(brew --prefix openssl) --with-libxml-dir=$(brew --prefix libxml2) --with-curl=$(brew --prefix curl) --without-tidy" YACC=$(brew --prefix bison)/bin/bison PHP_BUILD_EXTRA_MAKE_ARGUMENTS=-j4 php-build -i development 7.3.12 ~/local/php/7.3.12/

さて、PHP の新しいバージョン(ポイントリリース)はだいたい1〜2ヶ月に1度リリースされているようですが、このとき、7.3と7.2と7.1の新しいバージョンがほぼ同時にリリースされるという感じなので、そのたびに以下のようなコマンドを実行することになります。

$ ghq look php-build
$ git pull
$ ./install.sh
$ exit
$ export PHP_BUILD_CONFIGURE_OPTS="--with-zlib-dir=$(brew --prefix zlib) --with-bz2=$(brew --prefix bzip2) --with-iconv=$(brew --prefix libiconv) --with-libedit=$(brew --prefix libedit) --with-openssl=$(brew --prefix openssl) --with-libxml-dir=$(brew --prefix libxml2) --with-curl=$(brew --prefix curl) --without-tidy" 
$ export YACC=$(brew --prefix bison)/bin/bison
$ export PHP_BUILD_EXTRA_MAKE_ARGUMENTS=-j4
$ php-build -i development 7.3.12 ~/local/php/7.3.12/
$ php-build -i development 7.2.25 ~/local/php/7.2.25/
$ php-build -i development 7.1.33 ~/local/php/7.1.33/

php-build コマンドを打つこと自体は対して大変ではありませんが、マイナーバージョンごとの最新バージョン番号を確認するのが面倒ですし、自動化出来そうだったのでやってみました。

複数のPHPバージョンをビルドするスクリプト

以下のスクリプトを作りました。

php-build-auto.sh

#!/usr/bin/env bash
function usage_exit() {
  echo "Usage: $0 [OPTIONS] <version1> [<version2> [...]]"
  echo
  echo "Options:"
  echo "  -h, --help"
  echo "  --parallel num (default: CPU physical core number)"
  echo "  --install-root-path path (default: \$HOME/src/local/php"
  echo "  --override"
  echo "  --show-versions"
  echo
  exit 1
}

# option defalut
PARALLEL=$(sysctl -n hw.physicalcpu_max)
INSTALL_ROOT_PATH="$HOME/local/php"
OVERRIDE=false
SHOW_VERSIONS=false

param=()
for OPT in "$@"
do
  case $OPT in
    -h | --help)
      usage_exit
      exit 1
      ;;
    --parallel)
      PARALLEL=$2
      shift 2
      ;;
    --install-root-path)
      INSTALL_ROOT_PATH=$2
      shift 2
      ;;
    --override)
      OVERRIDE=true
      shift 1
      ;;
    --show-versions)
      SHOW_VERSIONS=true
      shift 1
      ;;
    *)
      if [[ -n "$1" ]] && [[ ! "$1" =~ ^-+ ]]; then
        param+=( "$1" )
        shift 1
      fi
      ;;
  esac
done

if [ -p /dev/stdin ]; then
  IFS=$'\n'
  for line in $(cat -)
  do
    param+=( "$line" )
  done
fi

BUILD_VERSIONS=()
SKIP_VERSIONS=()
if [ $OVERRIDE = true ]; then
  BUILD_VERSIONS=$param
else
  for VERSION in "${param[@]}" ; do
    if [ -e "$INSTALL_ROOT_PATH/$VERSION/bin/php" ]; then
      SKIP_VERSIONS+=($VERSION)
    else
      BUILD_VERSIONS+=($VERSION)
    fi
  done
fi
echo "skip versions:"
echo "${SKIP_VERSIONS[@]}"
echo "build versions:"
echo "${BUILD_VERSIONS[@]}"

if [ $SHOW_VERSIONS = true ]; then
  exit 0
fi

export PHP_BUILD_CONFIGURE_OPTS="--with-zlib-dir=$(brew --prefix zlib) --with-bz2=$(brew --prefix bzip2) --with-iconv=$(brew --prefix libiconv) --with-libedit=$(brew --prefix libedit) --with-openssl=$(brew --prefix openssl) --with-libxml-dir=$(brew --prefix libxml2) --with-curl=$(brew --prefix curl) --without-tidy"
export YACC="$(brew --prefix bison)/bin/bison"
export "PHP_BUILD_EXTRA_MAKE_ARGUMENTS=-j$PARALLEL"
echo "${BUILD_VERSIONS[@]}" | xargs -n1 -t -I@ php-build -i development @ "$INSTALL_ROOT_PATH"/@/

使い方ですが、引数でPHPバージョンを指定すると、まとめてビルドしてくれます。

$ ./php-build-auto.sh 7.0.33 7.1.33 7.3.12
skip versions:
7.0.33
build versions:
7.1.33 7.3.12
[Info]: Loaded extension plugin
[Info]: Loaded apc Plugin.
(以下略)

すでにインストール済みのバージョンがあれば、ビルドはスキップされます。もし再ビルドしたい場合は --override オプションを付けます。

$ ./php-build-auto.sh --override 7.0.33 7.1.33 7.3.12

このスクリプトですが、作りはじめたときは xargs -P を使って php-build コマンドを並列実行させるのが最大の特徴でした。しかし、PHPビルド後のXdebugビルドは、同じディレクトリで実行されるので、複数のXdebugビルドを1ディレクトリで同時にやってしまい、エラーになってしまったのでその機能を削除しています。

マイナーバージョン毎の最新バージョン番号を列挙する

php-build-auto.sh は、新しいポイントリリースが出たときにまとめてビルドしたいときに使います。そこで、最新のポイントリリースバージョンを列挙するスクリプトを別途作りました。

listversion.php

#!/usr/bin/env php
<?php
declare(strict_types=1);
/**
 * usage: php listversion.php [--filter stable|minor-head] [--oldest-version version] [--definitions-path path]
 */
class PhpVersion
{
    const VERSION_PATTERN = '/(?P<major>\d+)\.(?P<minor>\d+)\.(?P<point>\d+)/';
    public static function getMinorVersion(string $version)
    {
        preg_match(self::VERSION_PATTERN, $version, $matches);
        return sprintf('%s.%s', $matches['major'], $matches['minor']);
    }

    public static function isStable(string $version)
    {
        return preg_match(self::VERSION_PATTERN, $version) === 1;
    }
}

$argvOptions = getopt('', ['filter:', 'oldest-version:', 'definitions-path:']);
$options = [
    'filter' => !empty($argvOptions['filter']) ? $argvOptions['filter'] : 'minor-head',
    'oldest_version' => !empty($argvOptions['oldest-version']) ? $argvOptions['oldest-version'] : '5.6.0',
    'definitions_path' => !empty($argvOptions['definitions-path'])
        ? $argvOptions['definitions-path']
        : '/usr/local/share/php-build/definitions/',
];

$definitionsIter = new DirectoryIterator($options['definitions_path']);
$versions = [];
foreach ($definitionsIter as $definition) {
    if ($definition->isDot()) {
        continue;
    }
    $version = $definition->getFilename();
    if (! PhpVersion::isStable($version)) {
        continue;
    }
    if (version_compare($version, $options['oldest_version'], '<')) {
        continue;
    }
    if ($options['filter'] === 'minor-head') {
        $minorVersion = PhpVersion::getMinorVersion($version);
        if (!isset($versions[$minorVersion])
            || version_compare($version, $versions[$minorVersion], '>')) {
            $versions[$minorVersion] = $version;
        }
    } else {
        $versions[] = $version;
    }
}
echo implode("\n", $versions) . PHP_EOL;

実行すると、各マイナーバージョン毎の最新バージョンを列挙します。

$ ./listversion.php 
7.2.25
7.3.12
7.1.33
7.0.33

僕の環境だと 7.0.19 未満のバージョンはビルドエラーになったので、列挙しないようにしています。列挙したい場合は --oldest-version オプションを使います。

 $ ./listversion.php --oldest-version=5.3
7.2.25
5.4.45
7.3.12
7.1.33
5.3.29
5.5.38
7.0.33
5.6.40

php-build-auto.sh は引数と標準入力の両方でビルド対象を指定できるので、 listversion.php と組み合わせて、「マイナーバージョン毎の最新バージョンをまとめてビルド」、が実現できます。

$ ./listversion.php | ./php-build-auto.sh
skip versions:
7.1.33 7.0.33
build versions:
7.2.25 7.3.12
php-build -i development 7.2.25 /Users/kojitanaka/local/php/7.2.25/
[Info]: Loaded extension plugin
[Info]: Loaded apc Plugin.
(以下略)

2つのスクリプトを作った結果、PHPのバージョンアップ時の作業は以下のように単純化できました。

$ ghq look php-build
$ git pull
$ ./install.sh
$ exit
$ cd ~/src/github.com/tenkoma/php-build-tools
$ ./listversion.php | ./php-build-auto.sh

今回実装したスクリプトはtenkoma/php-build-toolsにて公開しています。

まとめ

複数のPHPバージョンを手元にそろえるときに使えるphp-buildの運用を多少楽にするためにコードを書き、バージョンアップ時に考える要素を減らしました。最初やりたかった複数バージョンの並列ビルドはできていません。

また、Homebrew でライブラリをアップグレードすると、php-build でビルドしたPHPが動作しなくなることもあります。2年前は5.3〜7.1まで揃えられましたが、Catalinaではまだ5.5, 5.6 のビルドができていません。

最新のmacOSで古いPHPを動かしにくくなってきている気がするので、PHPの古いバージョンを揃えたい場合は、phpallのDocker版を作ってみた話 - hamacoの日記のように、Docker を使った方がいいかもしれません。

明日はPlatform Devマネージャーの大窪さんとData Strategyの杉さんです。