Rust on i586

#はじめに
Rustの公式32bitバイナリはi686向けになっています。今時ほとんどのx86/32bitはi686なので妥当な選択ではありますが、ごくまれにi586（世代的には初代Pentium~MMX Pentiumあたり）上でRustを動かしたいことがあります。i586上で公式バイナリの`rustc`などをそのまま動かしても`illegal hardware instruction`となり実行できません。このエラーについては[こちら](https://users.rust-lang.org/t/i586-support-illegal-hardware-instruction/2694)で報告されていて、i586向けの独自ビルドを公開している方がいますので、基本的にはそれを使えば問題ありません。
だた、公開先がdropboxということと、stable(1.5.0)しかないということで今回は自分でビルドしてみることにしました。また、ビルドスクリプトも公開し、CircleCIでビルドしてGitHubにリリースするようにしました。

※ちなみにRustの公式パッケージについては[こちら](https://internals.rust-lang.org/t/perfecting-rust-packaging/2623)で議論されているようで、i586についても言及があります。そのうち公式にi586バイナリが提供されるようになるかもしれません。

#リンク

- [release](https://github.com/dalance/rust-i586/releases/tag/v1.0.0)
- [build script](https://github.com/dalance/rust-i586)

#動作確認環境
CPU: AMD Geode LX 800
Distribution: Arch Linux

#使い方
適当なところに展開してパスを通せばいいのですが、[`multirust`](https://github.com/brson/multirust)を使うと便利です。ただし、i586上で普通にインストールを試みると`unknown CPU type`といわれてしまいますので、以下のようにCPUチェックをごまかします。

```
curl -sf https://raw.githubusercontent.com/brson/multirust/master/blastoff.sh | sed 's/i486/i586/g' | sh
```

インストール中にエラーが発生して終了しますが無視して問題ありません。`multirust`コマンドが使えるようになったら以下のようにtoolchainを登録します。`--link-local`で指定するのはi586版を展開したディレクトリ（`rustc`バイナリのパスが`$path-to-rust-i586-root/bin/rustc`となるように設定）です。

```
multirust update nightly-i586 --link-local $path-to-rust-i586-root
```

これでtoolchainとしてnightly-i586（この名前は適当につけて下さい）が使えるようになるので、あとは普通にdefault/override等で指定すれば使えるようになります。

#ビルドスクリプト解説
`-march`を変えてコンパイルするだけかと思いきやいろいろと小細工が必要だったので一応解説を。CircleCIでビルドするのでメインは`circle.yml`のtest/overrideあたり。

```circle.yml
machine:
    environment:
        CFLAGS: -m32 -march=i586
        CXXFLAGS: -m32 -march=i586
        LD_LIBRARY_PATH: /usr/local/lib

dependencies:
    post:
        - sudo apt-get update
        - sudo apt-get install gcc-multilib g++-multilib cmake curl:i386 lib32z1
        - sudo ln -s /lib/i386-linux-gnu/libcrypto.so.1.0.0 /usr/lib32/libcrypto.so
        - sudo ln -s /lib/i386-linux-gnu/libssl.so.1.0.0 /usr/lib32/libssl.so
        - sudo ln -s /usr/lib/i386-linux-gnu/libcurl.so.4 /usr/lib32/libcurl.so
        - sudo ln -s /usr/lib32/libz.so.1 /usr/lib32/libz.so

test:
    override:
        - ./env.sh nightly
        - ./setup_rust.sh
        - make -C rust rustc-stage1
        - make -C rust
        - sudo make -C rust install
        - ./setup_cargo.sh
        - make -C cargo
        - ./clean.sh
```

この後も続きますが、基本的に`env.sh`の引数を変えて同じことをしてるだけです。このあたりの重複はなんとかしたいのですがうまい書き方があれば教えてください。（後述する1コマンド2時間制限があるので、単純にスクリプトにまとめるのはうまくいきません）
また、`-march`指定は環境変数経由で行います。（`-march`を指定する箇所はここと、`src/i586_unknown_linux_gnu.rs`の2か所です）

##小細工その１
`setup_rust.sh`の中でソースの展開後、いくつかのファイルを追加・上書きしています。

```setup_rust.sh
#!/bin/sh

git clone https://github.com/rust-lang/rust.git

cd rust
git checkout `cat ../branch`

cp ../src/i586-unknown-linux-gnu.mk ./mk/cfg
cp ../src/i586_unknown_linux_gnu.rs ./src/librustc_back/target
echo ./src/librustc_back/target/mod.rs | xargs sed -i.bak -e 's/i686_unknown_linux_gnu/i586_unknown_linux_gnu/g'

cp ../src/get-snapshot.py ./src/etc
echo ./src/etc/snapshot.py | xargs sed -i.bak -e 's/i686/i586/g'

./configure --target=i586-unknown-linux-gnu --host=i586-unknown-linux-gnu --build=i586-unknown-linux-gnu

cd ..
```

`i586-unknown-linux-gnu.mk`と`i586_unknown_linux_gnu.rs`がrustのtarget追加用ファイルです。また、`src/librustc_back/target/mod.rs`にtarget一覧があるので、そこの`i686-unknown-linux-gnu`を`i586-unknown-linux-gnu`に変更します。`snapshot.py`/`get-snapshot.py`は後述します。

##小細工その２
続いてスナップショット関連です。RustコンパイラはRustで書かれているのでコンパイルするにはRustコンパイラが必要です。ここで必要なRustコンパイラはビルドプロセスの途中でダウンロードされていて、これを行っているのが`get-snapshot.py`です。また、その中でダウンロードするバイナリのURLなどを決めているのが`snapshot.py`になります。`snapshot.py`にはi686用の記述があるのでi586に変更します。さらに、`get-snapshot.py`では`rustc`の置き換えを行います。

```get-snapshot.py
    unpack_snapshot(triple, dl_path)
    os.rename("/home/ubuntu/rust-i586/rust/i586-unknown-linux-gnu/stage0/bin/rustc", "/home/ubuntu/rust-i586/rust/i586-unknown-linux-gnu/stage0/bin/rustc.org")
    shutil.copy("/home/ubuntu/rust-i586/src/rustc", "/home/ubuntu/rust-i586/rust/i586-unknown-linux-gnu/stage0/bin")
```

`unpack_snapshot`までがオリジナルの記述で、ダウンロードしてきたRustコンパイラを展開しています。その後の2行が追記分で、展開された`rustc`を`rustc.org`にリネームし、`rustc`のラッパースクリプトを置きます。

```rustc
#!/bin/sh

args=`echo "$*" | sed s/--target=i586-unknown-linux-gnu/--target=i686-unknown-linux-gnu/g`
$(cd $(dirname $0) && pwd )/rustc.org $args -L i586-unknown-linux-gnu/stage0/lib/rustlib/i586-unknown-linux-gnu/lib
```

わざわざ`rustc`を置き換えるのはダウンロードしてきた`rustc`がi686用であるためです。最初の`setup_rush.sh`内で`./configure --target=i586-unknown-linux-gnu`を指定すると、この`rustc`に渡されるtargetも`i586-unknown-linux-gnu`になりますが、i686用ではその指定は無効です。そのためラッパースクリプトでtargetをi686に置き換えています。また、rustlibへのパスもi686用しか通っていないのでi586用のパスを指定しておきます。

##小細工その３
CircleCIは無料版でもビルド時間は1500分/月ということでビルドに4~5時間かかっても問題ないと思っていたのですが、1コマンドあたり2時間の制限があるそうです。（[参考](https://discuss.circleci.com/t/timeout-more-than-120-minutes/640)）
当初リポジトリのクローンから`make`まで1つのスクリプトで行ったところ、この2時間制限に引っかかってしまい、やむなく分割しました。しかし`make`だけでも2時間近く（nightlyの場合で1時間50分台でした）かかってしまい、`make`も分割したほうが安全そう、ということで以下のように2段階に分けています。今のところ前半で1時間、後半で45分程度なので大丈夫そうですが、これでも足りなくなるとLLVMのコンパイルも分ける必要がありそうです。

```circle.yml
        - make -C rust rustc-stage1
        - make -C rust
```

##小細工その４
最後はcargoのビルドです。

```setup_cargo.sh
#!/bin/sh

git clone https://github.com/rust-lang/cargo.git

cd cargo
git submodule update --init
python -B src/etc/install-deps.py
./configure --target=i586-unknown-linux-gnu --build=i586-unknown-linux-gnu --host=i586-unknown-linux-gnu --local-rust-root=/usr/local
make
mkdir ../mods
cp -r ~/.cargo/registry/src/github.com-*/* ../mods/
mkdir .cargo
grep -lr target.i686-unknown-linux-gnu.dependencies `pwd`/../mods | awk 'BEGIN{print "paths = ["}//{out=$0;gsub("Cargo.toml","",$out);print "\""$out"\","}END{print "]"}' > .cargo/config
grep -lr target.i686-unknown-linux-gnu.dependencies ../mods | xargs sed -i.bak -e 's/target.i686-unknown-linux-gnu.dependencies/target.i586-unknown-linux-gnu.dependencies/g'
grep -lr "curl-sys = { path = \"curl-sys\", version = \"0.1.0\" }" ../mods | xargs sed -i.bak -e 's/curl-sys = { path = \"curl-sys\", version = \"0.1.0\" }/curl-sys = \"0.1.0\"/g'
make
grep -lr rustc-link-search=native=/usr/lib/x86_64-linux-gnu ./ | xargs sed -i.bak -e 's/rustc-link-search=native=/usr/lib/x86_64-linux-gnu/rustc-link-search=native=/usr/lib32/g'
cd ..
```

結構長いですが、ポイントはこの2行です。

```
grep -lr target.i686-unknown-linux-gnu.dependencies `pwd`/../mods | awk 'BEGIN{print "paths = ["}//{out=$0;gsub("Cargo.toml","",$out);print "\""$out"\","}END{print "]"}' > .cargo/config
grep -lr target.i686-unknown-linux-gnu.dependencies ../mods | xargs sed -i.bak -e 's/target.i686-unknown-linux-gnu.dependencies/target.i586-unknown-linux-gnu.dependencies/g'
```

cargoのビルドはcargoを使って行われるのですが、依存しているライブラリの`Cargo.toml`に`[target.i686-unknown-linux-gnu.dependencies]`という記述がある場合があります。これは特定のプラットフォームでのみ依存関係を記述する場合の指定ですが、今回の場合プラットフォームは`i586-unknown-linux-gnu`なので先ほどの記述には該当せず、依存ライブラリが見つからないことになってしまいます。
そのためこの`Cargo.toml`は書き換える必要があるのですが、単に`~/.cargo/registry/src/github.com-...`以下にある`Cargo.toml`を書き換えてもビルドを走らせると元に戻ってしまいます。
そこで

```
mkdir ../mods
cp -r ~/.cargo/registry/src/github.com-*/* ../mods/
```

のように、依存ライブラリのディレクトリをコピーして

```
grep -lr target.i686-unknown-linux-gnu.dependencies `pwd`/../mods | awk 'BEGIN{print "paths = ["}//{out=$0;gsub("Cargo.toml","",$out);print "\""$out"\","}END{print "]"}' > .cargo/config
```

cargoが`~/.cargo/registry/src/github.com-...`ではなくコピー先を見に行くように`.cargo/config`を生成します。
（`.cargo/config`に`paths = ["dir1","dir2"]`と書くとそのパスを優先して見に行くようになります）

```
grep -lr target.i686-unknown-linux-gnu.dependencies ../mods | xargs sed -i.bak -e 's/target.i686-unknown-linux-gnu.dependencies/target.i586-unknown-linux-gnu.dependencies/g'
```
このコピーに対して`Cargo.toml`を書き換えると、書き換えた`Cargo.toml`を維持したままビルドすることができます。