Dockerを使ってcomposerをHHVMで動かす

# TL;DR

- brewのcomposerはcomposer.pharではないので`hhvm /usr/local/bin/composer`してもPHPで動いてしまう
- コンテナ上にHHVMとcomposerを用意して解決したので、Dockerfileとその使い方を公開します

# 動作環境

- macOS High Sierra(version 10.13.3)
- Docker Community Edition(version 18.03.0-ce-rc1-mac54)

# 経緯

[PHPerKaigi 2018](https://phperkaigi.jp/2018/)で[Hackの話](https://speakerdeck.com/ytake/hackdezuo-rumaikurohuremuwaku)を聞いて触発されてHackを触ってみたものの、[hhvm/hhvm-autoload](https://packagist.org/packages/hhvm/hhvm-autoload)をインストールしようとすると、HHVMで実行してるのにもかかわらず「HHVMで動かせ」って怒られてしまいました。

```shell
$ which composer
/usr/local/bin/composer

$ hhvm /usr/local/bin/composer require hhvm/hhvm-autoload
Using version ^1.6 for hhvm/hhvm-autoload
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - hhvm/hhvm-autoload v1.6.2 requires hhvm ^3.23 -> you are running this with PHP and not HHVM.
    - hhvm/hhvm-autoload v1.6.1 requires hhvm ^3.23 -> you are running this with PHP and not HHVM.
    - hhvm/hhvm-autoload v1.6.0 requires hhvm ^3.23 -> you are running this with PHP and not HHVM.
    - Installation request for hhvm/hhvm-autoload ^1.6 -> satisfiable by hhvm/hhvm-autoload[v1.6.0, v1.6.1, v1.6.2].


Installation failed, reverting ./composer.json to its original content.
```

# 原因

このcomposerは`brew install composer`でインストールしたものなので、`/usr/local/bin/composer`は`composer.phar`とは別物のようです。

```shell
$ cat /usr/local/bin/composer
#!/usr/bin/env php
<?php
array_shift($argv);
$arg_string = implode(' ', array_map('escapeshellarg', $argv));
$arg_prefix = preg_match('/--(no-)?ansi/', $arg_string) ? '' : '--ansi ';
$arg_string = $arg_prefix . $arg_string;
passthru("/usr/bin/env php -d allow_url_fopen=On -d detect_unicode=Off /usr/local/Cellar/composer/1.6.3/libexec/composer.phar $arg_string", $return_var);
exit($return_var);
```

Cellarの中にある`composer.phar`をHHVMで実行して解決することもできそうですが、なるべく触りたく無い(個人の感想)のでDocker環境を用意することにしました。

# できたもの

```dockerfile:Dockerfile
FROM hhvm/hhvm

## get composer from composer official image
COPY --from=composer /usr/bin/composer /usr/local/bin/composer

## create non-root user
ARG PUID=1000
ARG PGID=1000

RUN groupadd -g ${PGID} docker && \
  useradd -u ${PUID} -g docker -m docker

## use non-root user
USER docker

# --------------------------------------------------
# additional composer settings
# --------------------------------------------------

ARG USE_PACKAGIST_JP=false
RUN if [ ${USE_PACKAGIST_JP} = true ]; then \
  hhvm /usr/local/bin/composer config -g repos.packagist composer https://packagist.jp \
  ;fi

ARG INSTALL_PRESTISSIMO=false
RUN if [ ${INSTALL_PRESTISSIMO} = true ]; then \
  hhvm /usr/local/bin/composer global require hirak/prestissimo \
  ;fi

# --------------------------------------------------
# final touches
# --------------------------------------------------

WORKDIR /app

## set -d option to control "SlowTimer" warning
ENTRYPOINT [ "hhvm", "-d", "hhvm.http.slow_query_threshold=30000", "/usr/local/bin/composer" ]

CMD [ "--help" ]
```

ライセンスは放棄しますので、ご自由にお使いください。

# 解説

## ベースイメージ

```dockerfile
FROM hhvm/hhvm
```
ベースイメージは`hhvm/hhvm`(ubuntu 14.04ベース)です。450MBほどあります。multi-stage buildsを利用すればalpineやbusyboxをベースイメージとして作れるのでは？と思って試行錯誤したのですがうまくいかず、時間の無駄だと判断してやめました。

## composerを持ってくる

```dockerfile
COPY --from=composer /usr/bin/composer /usr/local/bin/composer
```

composerはオフィシャルのイメージからもらってきています。`/usr/bin/composer`とpathを決め打ちしているのが気になりますが見なかったことにします。multi-stage buildsは便利ですね。

### 参考

- [Use multi-stage builds - Docker Documentation](https://docs.docker.com/develop/develop-images/multistage-build/)

## ルート権限のないユーザを用意する

```dockerfile
ARG PUID=1000
ARG PGID=1000

RUN groupadd -g ${PGID} docker && useradd -u ${PUID} -g docker -m docker

USER docker
```

composerはroot権限を持ったユーザで実行すると怒られるので、適当なユーザを用意しています。

## composerの設定をする

```dockerfile
ARG USE_PACKAGIST_JP=false
RUN if [ ${USE_PACKAGIST_JP} = true ]; then \
  hhvm /usr/local/bin/composer config -g repos.packagist composer https://packagist.jp \
  ;fi

ARG INSTALL_PRESTISSIMO=false
RUN if [ ${INSTALL_PRESTISSIMO} = true ]; then \
  hhvm /usr/local/bin/composer global require hirak/prestissimo \
  ;fi
```

このあたりは趣味です。なのでデフォルトではインストールしたりしないようにしています。  
インストールしたい方は、イメージをビルドするときに`--build-arg`オプションで変えてください。(後述)

### 参考

- [composerの遅さをまじめに考える #phpstudy - SlideShare](https://www.slideshare.net/hinakano/composer-phpstudy)

## 残り

```dockerfile
WORKDIR /app

ENTRYPOINT [ "hhvm", "-d", "hhvm.http.slow_query_threshold=30000", "/usr/local/bin/composer" ]

CMD [ "--help" ]
```

コンテナを起動するときにワーキングディレクトリに`composer.json`のあるディレクトリをマウントすることで、コンテナの中で実行した`composer`の成果物がホスト側に反映されます。

エントリーポイントでは、composerをHHVMで実行すると発生する「SlowTimer」を抑制するためのオプションをつけています。

### 参考

- [HHVMでComposerを実行したときのSlowTimer対策 - Qiita](https://qiita.com/unknown_soldier/items/71ff19a4fd809c6523e9)
- [Configuration: INI Settings - HHVM and Hack Documentation](https://docs.hhvm.com/hhvm/configuration/INI-settings)

# 使い方

## イメージをビルドする

とりあえずビルドするならこのコマンドを打ってください。

```shell
docker build -t hh-composer /path/to/Dockerfile
```

### オプション

`--build-arg`オプションで変えられます。

- `USE_PACKAGIST_JP=true|false` - packagist.jpを参照するようにするかどうか。デフォルトは`false`
- `INSTALL_PRESTISSIMO=true|false` - composerを早くするプラグイン([hirak/prestissimo](https://packagist.org/packages/hirak/prestissimo))をインストールするかどうか。デフォルトは`false`

#### 設定例

packagist.jpを使い、`hirak/prestissimo`をインストールする場合。

```shell
docker build --build-arg USE_PACKAGIST_JP=true --build-arg INSTALL_PRESTISSIMO=true -t hh-composer /path/to/Dockerfile
```

変更する数だけ`--build-arg`オプションを書かないといけないのは少し面倒くさいですね。

## コンテナを起動してcomposerを実行する

```shell
// 長いオプションだとこう
docker run --rm --tty --interactive --volume $(pwd):/app hh-composer

// 短いオプションだとこう
docker run --rm -it -v $(pwd):/app hh-composer
```

`hh-composer`に何も引数を付けないと、`--help`が呼ばれるようになってます。

それでは、当初の目的であった`hhvm/hhvm-autoload`をインストールしてみます。

```shell
$ docker run --rm -it -v $(pwd):/app hh-composer require hhvm/hhvm-autoload
Using version ^1.6 for hhvm/hhvm-autoload
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 2 installs, 0 updates, 0 removals
  - Installing fredemmott/hack-error-suppressor (v1.0.1): Downloading (100%)
  - Installing hhvm/hhvm-autoload (v1.6.2): Downloading (100%)
Writing lock file
Generating autoload files
```

いい感じですね:yum:

## 使いやすくする

いちいち長いコマンドを打つのはめんどくさいので、shellの設定ファイル(`.bashrc` `.zshrc`など)に関数を追加します。

```shell
hh-composer () {
  tty=
  tty -s && tty=--tty
  docker run \
    $tty \
    --interactive \
    --rm \
    --volume $(pwd):/app \
    hh-composer "$@"
}
```

これで`hh-composer`として実行することができるようになります:tada:

```shell
$ hh-composer --version
Composer version 1.6.3 2018-01-31 16:28:17
```

### 参考

- [library/composer - Docker Hub](https://hub.docker.com/_/composer/)

# 注意点

- composerとHHVMはコンテナ上で動いていて、コマンド実行後すぐに破棄されてしまうため、`composer global install`したい場合はDockerfileをいじるしかありません
- composerとHHVMのバージョンがコンテナ側とホスト側で一致してないことでエラーが起きる可能性があります
- 一部のpackageはインストール時にscriptを実行しますが、それが反映されるのはコンテナ側になります。それによって問題が起きる可能性があります

# 最後に

誤字脱字、内容の間違いなどは編集リクエスト、コメント、Twitter([@shuymn](https://twitter.com/shuymn))など何らかの方法で教えていただければと思います。