各言語環境を Linux (WSL) に asdf で構築

サーバ上でアプリケーションを動作させたり、バイナリをビルドするために、特定のバージョンのプログラム言語プラットフォームを必要とすることがある。
あらかじめ Docker で動くように設計されているのであれば Docker で十分だが、全てが対応しているわけではない。

そんな時良く使われるのが、`***env` 系のバージョン管理ツール。

# env 系ツール

元々は rbenv が始まりらしいが、今では様々な言語向けに移植されている。
できることは大体同じで、

- 特定のバーションを取得 (ビルド)
- グローバルに利用するバージョンの管理
- フォルダ毎にバージョンを固定
    - ローカルに何かしらの dotfile を置き、そこに設定する
    - 環境変数で切り替える
- shims の管理

ただ、言語ごとに `**env` をいちいち取ってきてシェルの設定するのが面倒なので、その場合は `**env` を管理する [anyenv](https://github.com/riywo/anyenv) がよく使われる。

とはいえ、`**env` 系はメンテナンスが止まっているものもあったりと、不満もあった。
そこで、自分は asdf というツールに乗り換えた。

# asdf

asdf は、様々なプログラム言語やツールの実行環境・SDK のバージョン管理を行う為のツール。
https://github.com/asdf-vm/asdf

管理できる対象は以下で一覧できる。
https://github.com/asdf-vm/asdf-plugins#plugin-list

一覧を見れば分かるが、プログラム言語の管理だけを目的としているわけではない。
kubectl や bazel などの CLI ツールや、Elasticsearch や MongoDB などのミドルウェアも何故か扱える。
が、今回はプログラム言語プラットフォームだけを対象とする。

## 対象言語

自分が知っている言語を抜き出してみた。

| Language  | Repository  | 
|-----------|-------------|
| Clojure   | [halcyon/asdf-clojure](https://github.com/halcyon/asdf-clojure) |
| Coq       | [gingerhot/asdf-coq](https://github.com/gingerhot/asdf-coq) |
| Crystal   | [marciogm/asdf-crystal](https://github.com/marciogm/asdf-crystal) |
| D (DMD)   | [sylph01/asdf-dmd](https://github.com/sylph01/asdf-dmd) |
| Elixir    | [asdf-vm/asdf-elixir](https://github.com/asdf-vm/asdf-elixir) |
| Elm       | [vic/asdf-elm](https://github.com/vic/asdf-elm) |
| Erlang    | [asdf-vm/asdf-erlang](https://github.com/asdf-vm/asdf-erlang) |
| Go        | [kennyp/asdf-golang](https://github.com/kennyp/asdf-golang) |
| GraalVM    | [vic/asdf-graalvm](https://github.com/vic/asdf-graalvm) |
| Haskell   | [vic/asdf-haskell](https://github.com/vic/asdf-haskell) |
| Idris     | [vic/asdf-idris](https://github.com/vic/asdf-idris) |
| Java      | [skotchpine/asdf-java](https://github.com/skotchpine/asdf-java) |
| Julia     | [rkyleg/asdf-julia](https://github.com/rkyleg/asdf-julia) |
| Kotlin    | [missingcharacter/asdf-kotlin](https://github.com/missingcharacter/asdf-kotlin) |
| Logtalk   | [LogtalkDotOrg/asdf-logtalk](https://github.com/LogtalkDotOrg/asdf-logtalk) |
| Lua       | [Stratus3D/asdf-lua](https://github.com/Stratus3D/asdf-lua) |
| LuaJIT    | [smashedtoatoms/asdf-luaJIT](https://github.com/smashedtoatoms/asdf-luaJIT) |
| Nim       | [rfrancis/asdf-nim](https://github.com/rfrancis/asdf-nim) |
| Node.js   | [asdf-vm/asdf-nodejs](https://github.com/asdf-vm/asdf-nodejs) |
| OCaml     | [vic/asdf-ocaml](https://github.com/vic/asdf-ocaml) |
| PHP       | [odarriba/asdf-php](https://github.com/odarriba/asdf-php) |
| Python    | [danhper/asdf-python](https://github.com/danhper/asdf-python) |
| R         | [iroddis/asdf-R](https://github.com/iroddis/asdf-R) |
| Racket    | [vic/asdf-racket](https://github.com/vic/asdf-racket) |
| Ruby      | [asdf-vm/asdf-ruby](https://github.com/asdf-vm/asdf-ruby) |
| Rust      | [code-lever/asdf-rust](https://github.com/code-lever/asdf-rust) |
| Scala     | [mtatheonly/asdf-scala](https://github.com/mtatheonly/asdf-scala)|
| .Net Core | [emersonsoares/asdf-dotnet-core](https://github.com/emersonsoares/asdf-dotnet-core) |

2018/12 現在、メンテナンスもアクティブになされており、新しい言語にも対応していきそうだ。
新し目で言うと、`GraalVM` なんかもある。

## インストール

手順は簡単

```bash
$ git clone https://github.com/asdf-vm/asdf.git ~/.asdf

# ついでに、プラグインでよく使われるパッケージをインストールしておく
$ sudo apt-get install -y automake autoconf libreadline-dev libncurses-dev libssl-dev libyaml-dev libxslt-dev libffi-dev libtool unixodbc-dev
```

あとは、お使いの Shell に設定追加するだけ。私は fisher です。

```bash
$ echo 'source ~/.asdf/asdf.fish' >> ~/.config/fish/config.fish
$ mkdir -p ~/.config/fish/completions; and cp ~/.asdf/completions/asdf.fish ~/.config/fish/completions

$ source ~/.asdf/asdf.fish
```

これで終わり。

```bash
$ asdf --version
# v0.6.2
```

# 使ってみる

## Node.js

まずは、分かりやすく Node.js で挑戦。

```bash
$ asdf plugin-add nodejs
# 既に入っているなら、アップデート
$ asdf plugin-update nodejs

# バージョンリストを取得
$ asdf list-all nodejs
# 0.10.0
# 0.10.1
# ....

# インストール
$ asdf install nodejs 10.10.0
$ asdf install nodejs 11.3.0

# 不要ならアンインストール
$ asdf uninstall nodejs 9.11.0
```

グローバルに選択してみる

```bash
$ asdf global nodejs 11.3.0
$ node -v
# v11.3.0

# 現在のグローバルなバージョン
$ asdf current  nodejs
# 11.3.0   (set by /home/username/.tool-versions)

# インストールされたリストを一覧
$ asdf list nodejs
# 10.10.0
# 11.3.0
```

今度はプロジェクト毎に選択してみる

```bash
$ cd ./some_project_folder
$ asdf local nodejs 10.10.0
$ node -v
# v10.10.0

# ローカル設定は .tool-versions に保存
$ cat .tool-versions
# nodejs 10.10.0

# ディレクトリを抜ければ、もとに戻る
$ cd ..
$ node -v
# v11.3.0
```

ちなみに、shims はどうなってるかと言うと、

```bash
$ which node
# /home/username/.asdf/shims/node

$ cat /home/username/.asdf/shims/node
# #!/usr/bin/env bash
# # asdf-plugin: nodejs
# # asdf-plugin-version: 11.3.0
# # asdf-plugin-version: 10.10.0
# exec /home/username/.asdf/bin/private/asdf-exec nodejs bin/node "$@"

# 元のフォルダの位置
$ asdf where nodejs
# /home/username/.asdf/installs/nodejs/11.3.0

# 貼り直し
$ asdf reshim nodejs
```

これだけできれば十分。

## Go

次は、Go でやってみる。

```bash
$ asdf plugin-add golang

# バージョンリストを取得
$ asdf list-all golang
# 1.2.2
# 1.3
# ....

# インストール
$ asdf install golang 1.11.2

$ asdf global nodejs 1.11.2
$ go version
# go version go1.11.2 linux/amd64

$ go env
# GOARCH="amd64"
# GOBIN=""
# GOCACHE="/home/username/.cache/go-build"
# GOEXE=""
# GOFLAGS=""
# GOHOSTARCH="amd64"
# GOHOSTOS="linux"
# GOOS="linux"
# GOPATH="/home/username/.asdf/installs/golang/1.11.2/packages"
# GOPROXY=""
# GORACE=""
# GOROOT="/home/username/.asdf/installs/golang/1.11.2/go"
# GOTMPDIR=""
# GOTOOLDIR="/home/username/.asdf/installs/golang/1.11.2/go/pkg/tool/linux_amd64"
# GCCGO="gccgo"
# CC="gcc"
# CXX="g++"
# CGO_ENABLED="1"
# GOMOD=""
# CGO_CFLAGS="-g -O2"
# CGO_CPPFLAGS=""
# CGO_CXXFLAGS="-g -O2"
# CGO_FFLAGS="-g -O2"
# CGO_LDFLAGS="-g -O2"
# PKG_CONFIG="pkg-config"
# GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build209186154=/tmp/go-build -gno-record-gcc-switches"# 
```

`GOROOT` や `GOPATH` が存在しない場合、shims の中の `asdf-exec` 実行時に、`$ASDF_INSTALL_PATH/go` と  `$ASDF_INSTALL_PATH/packages` に設定される。
https://github.com/kennyp/asdf-golang/blob/master/bin/exec-env

実際にやってみると、`$ASDF_INSTALL_PATH` は `/home/username/.asdf/installs/golang/1.11.2/` になった。
普通に `set -x GOPATH $HOME/go` と設定すればそっちが優先される。

## Python

Python は、[内部では pyenv を利用し、asdf のインターフェイスに合わせただけのようだ](https://github.com/danhper/asdf-python/blob/master/bin/utils.sh)

## Java (JDK)

Java は、`list-all` で表示するバージョンが [固定となっており](https://github.com/skotchpine/asdf-java/blob/master/bin/list-all)、選べる OpenJDK は、`openjdk-10.0.2` `openjdk-11` `openjdk-11.0.1` だけとなっている。

まぁ、サポート期間を考えて 10 未満を切り捨てているのであれば仕方ないのかも知れないが。

ちなみに、OracleJDK ではまだ 8 が存在している。

# Rust や Haskell はどうすべきか問題

例えば、Rust には rustup があり、Haskell には stack がある。

これらは、

- 公式 or デファクトスタンダードなプラットフォームバージョン管理ツール
- ツールは安定し、使いやすく、メンテナンスもアクティブに続いている
- ネット上の技術情報がそのツール利用を前提とするものが多い
- バージョン管理以外にも、重要な役割が存在する

という状況で、無理をして asdf のような外部のツールで管理する必要はあるか。

ということで、Rust と Haskell は大人しく rustup と stack を使っている。

# サードパーティ製プラグイン

好きな言語が対応していない場合には、自分でプラグインを書いて追加もできる。

```bash
$ asdf plugin-add <name> <git-url>
```
最低限、`list-all` と `install` があれば動くので、サクッと作れる。

# 感想

使っていて、手に馴染む感覚はある。
バージョン情報のコンプリーションもしっかりしてくれるし、対応言語も多い。
そういえば、[scoop](https://github.com/lukesampson/scoop) に使い心地も似ているのもあるかも。

# あとがき

※ この記事は個人の見解であり、所属する組織を代表するものではありません。