jpmens/joをRustに移植してみた
初めに
最近Rustを勉強しています。
ひととおり、Rust入門をザーッと読んである程度把握したので、jpmens/joをRustで移植してみました。
joとは
joはJSONを簡単に作成できるCLIです。
たとえば{"name":"gorilla"}というオブジェクトを作る時は次のようになります。
$ rjo name=gorilla
{"name":"gorilla"}
もちろん配列も可能ですし、joを組み合わせて使うことも可能です。
$ rjo name=gorilla like=$(rjo -a Go Vim Docker Kubernetes Rust)
{"like":["Go","Vim","Docker","Kubernetes","Rust"],"name":"gorilla"}
実装
実装自体もシンプルに引数で受け取った値をパースしていくだけです。
コマンドライン引数自体はclapを使ってパースしました。
fn parse_value(value: &str) -> serde_json::Value {
    match value {
        "true" => serde_json::Value::Bool(true),
        "false" => serde_json::Value::Bool(false),
        "null" => serde_json::Value::Null,
        _ => match value.chars().next() {
            Some(char) => match char {
                '{' | '[' | '"' => serde_json::from_str(value).unwrap(),
                '0'..='9' | '-' => serde_json::Value::Number(value.parse().unwrap()),
                '+' => serde_json::Value::Number(value.trim_start_matches('+').parse().unwrap()),
                _ => serde_json::Value::String(value.to_string()),
            },
            None => panic!("invalid value: {}", value),
        },
    }
}
Rustの場合、serde_jsonというクレートを使用することでマップや配列をJSON文字列に変換できます。
fn do_object(args: Args) -> String {
    let mut obj = BTreeMap::<&str, serde_json::Value>::new();
    for el in &args.values {
        let kv: Vec<&str> = el.split('=').collect();
        if kv.len() != 2 {
            panic!("'{}' must be key=value", el);
        }
        obj.insert(kv[0], parse_value(kv[1]));
    }
    to_string(args.pretty, &obj)
}
クロスコンパイル
今回移植したCLIのクロスコンパイルで一苦労しました。
Goの場合GOOS=windows GOARCH=amd64 go buildで手軽にクロスコンパイルできますが、
Rustの場合は違うOSのバイナリをビルドする場合はどうやらqemuでクロスコンパイルをする必要があるようです。
qemuは重たいため、できれば避けたいなって思っていました。
そこで、先日mattnさんがzigを使ってこんなことをやっていたことを思い出しました。
Rustでzigを使う方法について調べたらところ、ちょうどcargo-zigbuildというのを見つけました。
原理はmattnさんがやっていることと同じで、zigをCのコンパイラとリンカー代わりにすることで、qemuを使わずクロスコンパイルができます。
zigをCC代わりにする詳細はこちらの記事を読めば分かると思います。
cargo-zigbuildの使い方はシンプルで、これだけであとはよしなにクロスコンパイルしてくれます。
$ cargo zigbuild --release --target ${target}
リリース
タグを打つと、クロスコンパイルしたバイナリをリリースにアップロードしてくれるようにしています。
具体的に、ターゲット一覧が載っているtarget.txtを用意して、各ターゲットをコンパイルしたバイナリをアーカイブしてそれをリリースにアップロードしています。
TARGET=$(shell cat target.txt)
.PHONY: target-add
target-add:
	@rustup target add $(TARGET)
.PHONY: target-build
target-build: target-add
	@for target in $(TARGET); do echo $$target; cargo zigbuild --release --target $$target; done
.PHONY: target-archive
target-archive: target-build
	@bash archive.sh $(TARGET)
#!/bin/bash
version=$(git tag --points-at HEAD)
if [[ "${version}" == "" ]]; then
  echo "cannot get version tag from HEAD commit"
  exit 1
fi
bin=$(basename "$(git rev-parse --show-toplevel)")
target="$*"
if [[ ! -d "release" ]]; then
 mkdir "release"
fi
for target in "$@"; do
  archive="${bin}_${version}_${target}"
  echo "## make archive ${archive}"
  if [[ "$target" == *"windows"* ]]; then
    zip "release/${archive}.zip" "target/${target}/release/${bin}.exe"
  else
    tar -zcvf "release/${archive}.tar.gz" "target/${target}/release/${bin}"
  fi
done
CIはこんな感じになっています。
name: Release
on:
  push:
    tags:
      - 'v*'
  workflow_dispatch:
jobs:
  release:
    name: Release
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
        with:
          fetch-depth: 0
      - name: Cache toolchain
        uses: Swatinem/rust-cache@v1
      - name: Update toolchain
        run: |
          rustup update stable
          rustup default stable
      - name: Install zig
        uses: goto-bus-stop/setup-zig@v1
        with:
          version: 0.9.1
      - name: Install cargo-zigbuild
        run: |
          cargo install cargo-zigbuild
      - name: Build and archive
        run: |
          make target-archive
      - name: Create release
        uses: ncipollo/release-action@v1
        with:
          omitBody: true
          artifacts: 'release/*'
          token: ${{ secrets.GITHUB_TOKEN }}
最後に
Rustの入門としてひととおり実装とCICDをやってみましたが、クロスコンパイルはちょっと面倒だなっていう印象でした。
ただ、パターンマッチは結構便利だなと思いました。特に範囲パターンマッチができると知ったときは感動しました。
まだ所有権周り慣れていないんですが、もう少し深堀りするため、次は簡単なAPIを書いてみたいと思います。
Discussion
qemuは基本的にエミュレーションを行うためのソフトウェアで、リンクには関係ないはずです。例えばx86_64マシン上でARM向けのバイナリを作った場合にそれを実行したりするために使います。
参照している記事にもあるとおり、Rustにはcross compile機能がありますが、リンカ自体は配布していないためもう一手間必要な事が多いです。これは組み込みなどのケースでリンカが自由に配布できる形で提供されていない場合にも使うためにこうなっているはずです。
そこでリンカや代表的なCライブラリをあらかじめ用意してしまって、それをコンテナで配布することによって簡単に開発するためのツールがcrossです。基本的に
cargoをcrossに置きかれば使えます。ここにはQEMU User mode emulationを使って実行する為のものも同梱されています。User mode emulationでは仮想マシンのようにシステム全体を仮想化せずに、主にCPU命令だけを置換して実行するのでコンテナ内のプロセスとして実行できます。rustup target install target-tripleでツールチェインインストールしてcargo build --target target-tripleでクロスコンパイル出来た気がします。target-tripleはrustup target listの中から選びます。(最近クロスコンパイルしてないので間違ってたらごめんなさい)リンカはクロスコンパイル用のリンカを設定する必要があった気もします。
既に似たようなコメントありましたね
@termoshtt @Toru3
なるほど、そういうことだったんですね
cross のことを調べてもよくわかっていなかったのでスッキリました
教えていただき、ありがとうございます