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 のことを調べてもよくわかっていなかったのでスッキリました
教えていただき、ありがとうございます