2020-06-28に更新

cargo-makeによるプロジェクト・ビルド入門

読了目安:14分

cargo-makeによるプロジェクト・ビルド

モチベーション

cargoはrustのパッケージ管理ツール兼ビルドツールである.これい自体非常に便利なのだが, Web標準は無視できない. 特にSeedのようなWebフロントエンド・フレームワークによる開発ではWeb標準に合わせる必要も出てくる. 単純なケースではQuickstartに従ってindex.htmlに直接wasmモジュールを導入すれば良いのですが, 複雑なアプリなどはwebpackなどが使えた方が便利だと思います. 今回のケースではTailwindの導入などがそれに当たります.

目標

CSSフレームワークであるtailwindcssをSeedプロジェクトで利用する.

前提条件

  • rustupの導入

とりあえずこれを導入しておけば諸々の開発環境の導入・管理が行えるようになります.

NPM vs Cargo

機能 NPM Cargo 備考
パッケージ管理 ⭕️ ⭕️ パッケージのインスタール・公開などができる
依存性管理 ⭕️ ⭕️ lockファイルがある点など共通点が多い
タスク・ランナー ⭕️ Cargoではカスタム・コマンドが開発できる(はず)
ビルド ⭕️ NPMでは代わりにタスク・ランナーを使う
コマンド拡張 ⭕️ タスク・ランナーから呼び出せば良い

Cargoにはnpm-scriptsのようなタスク・ランナーがありませんが, カスタム・コマンドで機能を拡張することができます. その一つがcargo-makeです.

cargo-makeによるプロジェクト管理

seed-quickstartのMakefile.toml内容を解説する感じです. wachモードなどはEvaQL/ui/Makefile.tomlを参照してください.

cargo-makeの導入

cargo-makeの実行にはcargo-makeバイナリが必要になるのでインストールしておきます.

cargo install --force cargo-make

次にプロジェクトを作成します. これもコマンド一つでできます. 今回はwasmモジュールとして読み込まれるので--libオプションをつけます.

cargo new --lib project-name

できたらプロジェクト・ルートに移動し実行してみましょう.

cago make

この時点ではデフォルトのtomファイルが参照されます. 次にMakefile.tomlファイルを作成します.

cd project-name
touch Makefile.toml

同様に実行すると今度はMakefile.tomlをもとに実行が行われます. 任意のmakefileを指定するには--makefileオプションを使います.

cargo make --makefile ./my_build.toml test

Makefile.tomlの書き方

seed-quickstart/Makefile.tomlの解説です. 必要ない方は飛ばしましょう.

タスク

実行するコマンドはタスクという単位で管理します. 何もしないタスクは以下のようになります.

[tasks.do_nothing]
# do nothing

基本

cargoのbuildサブコマンドを呼び出してみましょう.

# cargo make compile
[tasks.compile]
description = "Build"
workspace = false
command = "cargo"
args = ["build"]

それぞれの意味は以下のようになります.

セクション 意味
description このタスクの内容
workspace workspaceでタスクを実行するかどうか
command 実行するメイン・コマンド
args 引数の指定

依存タスク

dependencies属性を指定するとコマンドの依存性を指定できます. 要するに呼び出し順序です.

# cargo make start
[tasks.start]
description = "Combine the build and serve tasks"
workspace = false
dependencies = ["build"]

これでcargo startを実行するとbuildタスクが実行されます.

開発サーバーと環境変数

開発サーバーとしてmicroserverというクレートを使います. 簡単なWeb UIの開発には便利そうなのとクレート導入の例として紹介しておきます. 開発の趣旨を開発者の人がブログに書いています.

Microserver: local http server with SPA support

今回のようにあるタスクの前提となるバイナリ・クレートのインストールも記述できます.

[tasks.serve]
description = "Start server"
install_crate = { crate_name = "microserver", binary = "microserver", test_arg = "-h" }
workspace = false
command = "microserver"
args = ["--port", "${PORT}"]

サーバーということでポートの指定もしています. 環境変数もenvセクションで指定できます.

[env]
PORT = "8000"

別ファイルに指定して読み込むこともできます.

[env]
env_files = [
    "./my_env.env",
]

conditionによる条件設

ある条件を満たすときにタスクを実行することもできます. 環境変数がきちんと指定されている場合だけ実行するという条件ならconditionセクションをタスクに追加します.

[tasks.start]
condition = { env_set = [ "PORT" ] }

あるいは特定の環境変数を条件にして新しい変数を定義することができる.

[env]
PORT_EXISTING = { value = "true", condtion = { env_set = ["PORT"] } }
PORT = { value = "8000", condition = { env_not_set = ["PORT"] } }

条件によって読みやすい環境変数に変換したり, 環境変数が定義されていない場合に設定したりということができそうです.

profileによるモードの切り替え

webpackのモードの指定ののようなこともできます.

[env]
env_files = [
    { path = "./development.env", profile = development },
    { path = "./production.env", profile = "production }
]
cargo make --profile production some_task

developmentはデフォルト値なので指定する必要はないです.

タスクの拡張とリリース・ビルド

タスク名をextend属性で指定するとタスクを拡張できます. 例えばcompileタスクをリリース・モードでビルドするように拡張すると以下のようになります.

[tasks.compile_release]
description = "Release Build "
extend = "compile"
args = ["build", "--release"]

プラットフォームごとの拡張も簡単にできます.

[tasks.hello-world]
script = [
    "echo \"Hello World From Unknown\""
]

[tasks.hello-world.linux]
script = [
    "echo \"Hello World From Linux\""
]

[tasks.hello-world.mac]
script = [
    "echo \"Hello World From macOS\""
]

スクリプティング

シェルスクリプトを指定して実行することもできます.

[tasks.echo]
script = [
    "echo hello world"
]

script_runner属性を指定することでpythonなどスクリプトのランナーを指定できます.

[tasks.python]
script_runner = "python"
script_extension = "py"
script = [
'''
print("Hello, World!")
'''
]

またファイルを指定して実行することもできます.

[tasks.run_from_script]
script = { file = "hello.py" }

@rustの指定でRustを実行することもできます.

run_tasks

実行するタスクを指定します. dependenciesで指定したタスクは事前に実行されますが, run_task属性で指定したタスクは事後に実行されます.

[tasks.pre_task]
script = [ "echo pre task"] 

[tasks.post_task]
script = [ "echo post task"] 

[tasks.do_something]
dependencies = ["pre_task"]
run_task = "post_task"

この例の場合pre_task -> do_something -> post_taskの順で実行されます. 並列実行やフォークなど細かいタスクのフローを設定することできます. 詳しくはSub Taskを参照してください.

エイリアス

タスクを別名で参照できます.

[tasks.build]
alias = "default_build"

Seedのexamplesフォルダは複数のクレートが含まれており, そこにはMakefile.tomlが存在します. それぞれのクレートではプロジェクト・ルートのMakefile.tomlを拡張する形でルートのタスクを参照しています. つまり実行する処理は同じといことです.

条件付き実行

条件を満たした場合にタスクが実行されます.

[tasks.test-condition]
condition = {
    platforms = ["windows", "linux"],
    channels = ["beta", "nightly"],
    profiles = ["development", "production"],
    rust_version = { min = "1.39.0", max = "1.42.0" }
}
script = [
    "echo \"condition was met\""
]

このタスクはmacでstableチャネルを利用している人は実行されません. またscriptの代わりにrun_taskで他のタスクを条件を満たした時だけ実行するということもできます.

watchモード

watch属性をつけるとwatchモードで実行できます.

[tasks.run_from_script]
script = { file = "hello.py" }
watch = true

監視対象からの除外のような設定もできます.

[tasks.watch]
description = "Start building project in watch mode"
workspace = false
dependencies = ["build", "build_wasm"]
watch = { ignore_pattern="pkg/*" }

watchモードでサーバーを起動することはできません. この場合run_taskのparallelを使うとファイルの変更を監視しながら配信もで可能です.

[tasks.dev]
description = "Build in watch mode while serving file"
run_task = [
    { name = ["watch", "serve"], parallel = true }
]

tailwindcssの導入

npmを使います.

npm init # if needed
npm install tailwindcss

これでtailwindというコマンドがパッケージ上で使えるようになります. cssフォルダを作成して以下の内容をstyle.cssファイルを新規作成します.

@tailwind base;

@tailwind components;

@tailwind utilities;

これをビルドして利用します. publicフォルダを同じ階層に作っておいて, css用のフォルダを作ります. carg-makeのタスクを追加しましょう.

# cargo make tailwind
[tasks.tailwind]
script = [
    "npx tailwind build ./css/style.css -o ./public/css/style.css",
]

style.cssからstyle.cssが出力されますが中身を見ると見れ慣れたCSSファイルです. 出力されたファイルをpublic/index.htmlに読み込めばtailwindが提供するユーティリティ・クラスを利用できます.

Seedでtailwindを使ってみましょう. Seedの説明はしませんがRustのマクロを使って要素を記述できます. 注目するのはclassマクロです. ここに指定された文字列がtailwindcssのユーティリティ・クラス名です.

fn view(model: &Model) -> impl View<Msg> {
    let button_class = class!["bg-gray-400", "px-8", "py-4"];
    div![
        class![
            "flex",
            "flex-col",
            "justify-center",
            "items-center",
            "h-screen",
            "text-gray-600"
        ],
        button![
            button_class,
            simple_ev(Ev::Click, Msg::Increment),
            format!("Click Me!")
        ],
        div![
            class!["w-56", "text-center", "mt-2"],
            format!("Click {} times", model.counter)
        ]
    ]
}

こんな感じの表示になりちゃんと表示されました(クリック時にカウント数を表示するラベルが動くバグがありますが・・・)

Seed with tailwindcss

まとめ

もうちょっとまとまりがあれば良いと思ったのですが, 意外と機能が多く詳細は公式のREADME.mdを読むのがいいと思います. examplesフォルダに例が豊富なので参考になると思います.

tailwindcssはかなり使いやすいですしSeedもいい感じです(ただビルドが遅いですが・・・).

補足

WorkspaceとWorkspace Flow

タスクにworkspace属性を指定できました. Workspaceとは何でしょうか?

A workspace is a set of packages that share the same Cargo.lock and output directory.

Cargo Workspaces - The Rust Programming Language

要するに複数のパッケージを一つにまとめたものです. ただし単一のプロジェクトとして管理される前提なので最終生成物やCargo.lockなどで全体のクレートのバージョンなどは共通化されています. 実態は以下のような内容のCargo.tomlとmembers属性で指定されたメンバーとなるパッケージが存在するフォルダです.

[workspace]

members = [
    "client",
    "server",
]

こうした構成はSeedのserver_integrationという例が参考になると思います. 例えば適当なウォークスペースにmakefileを作りworkspace属性を指定します.

[tasks.do_something]
workspace = false

なぜこのような設定が必要なのでしょうか. 通常cargo-makeのタスクはworkspace直下では実行されません. タスクの要求はメンバー・クレートで実行されます(workspace flow). このおかげでウォークスペースで実行したビルド処理が各クレートで実行されることになり, 共通の処理をウォークスペースにまとめられるので構成ファイルを小さくできます.

しかしウォークスペースで実行したい場合もあるでしょう. その場合にこの機能をオフにするのがworkspace属性の意味です. この値はデフォルトでtrueになっています.

[config]
default_to_workspace = false

のようにも指定するとデフォルト値をfalseに上書きできます.

あるいはコマンド実行時にオプションとして渡すこともできます.

cargo make --no-workspace mytask

CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILEフラグ

CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILEフラグはウォークスペース直下のmakefileに指定します. そうすると自動的に個々のメンバー・クーレトが持つmakefileはルートのmakefileをextendで読み込み参照できるようになるようです.

WASM in A Nutshell

WebAssemblyという技術の略称がWASMでコードの拡張子にもなっている. 通常ブラウザはJavaScriptのランタイムを備えており(V8やスパイダーモンキー)JavaScriptのみを実行できる. JIT(Just In Time)コンパイラによる最適化など高速化されたが, 原理的にはランタイムはJavaScriptを逐次解釈してマシンコードに翻訳しそれを実行するために遅い. このプロセスを飛ばせれば, ネットーワークにるRTT(Round-Trip Time)を無視すればネイティブ並みに高速化できるわけです. これはPythonやRubyなどのインタプリタ言語がC/C++やRustなどの言語より遅い事と基本的には同じ関係と言えそうです.

そこでWeb版のアセンブラを導入しようという話になるわけです. 通常アセンブリ言語はマシン語と1対1に対応するニーモニックを用いて表現されますが, WASMがターゲットとするのは複数のマシンを抽象化したマシンになります.

So WebAssembly is a little bit different than other kinds of assembly. It’s a machine language for a conceptual machine, not an actual, physical machine.

image

Creating and working with WebAssembly modules

この説明を聞くとJavaに近い感じを受ける. 実際に(この比較はおかしいけど)WASIとJavaの類似性を指摘した記事なんかもある.

MozillaがWASIイニシアティブを発表、WebAssemblyをすべてのデバイス、コンピュータ、オペレーティングシステムで動作可能に

また公式ではWASMを以下のように定義している.

WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine.

WebAssembly

スタック・マシンは良くわからないけどWikipediaによるとJava仮想マシンも似たような定義で紹介されている.

Java仮想マシン (Java virtual machine、Java VM、JVM) は、Javaバイトコードとして定義された命令セットを実行するスタック型の仮想マシン。

Reference

  1. The Cargo Book
  2. cargo-make
  3. Makefile.toml - seed-quickstart
  4. View - Seed
  5. Installation - tailwindcss

例題

タスクの依存関係, watchモードやcrateの導入などcargo-makeの基本的な使い方を学べる.

seed-quickstart

examplesフォルダからルート・フォルダにあるMakefile.tomlの参照法などが参考になりました.

examples -seed

課題

SeedのようなWebフロントエンドの開発では, プロジェクトをcargoパッケージとしてマインに構成するのかnpmパッケージとしてメインに構成するのかが問題になる. cargo-makeがない場合はnpmパッケージ以下にcargoパッケージを作らないとビルド・プロセスが自動にできない. seed-quickstart-webpackもwebpackを使ってrustライブラリのビルドからwasmモジュールの読み込みなどを行なっている. これをcargo-makeベースに置き換えたい. npm-scriptでコマンド化しておけば, cargo-makeから呼び出せる.

seed-quickstart-webpack

Further Reading

A crash course in assembly
A cartoon intro to WebAssembly
What makes WebAssembly fast?

ツイッターでシェア
みんなに共有、忘れないようにメモ

ブレイン

Androidアプリ開発者を目指しています. 興味あることリスト: https://t.co/ew3bb6grdJ Github: https://t.co/9btqysHqWr Qiita: https://t.co/ZVRhjouauX

Crieitは個人で開発中です。 興味がある方は是非記事の投稿をお願いします! どんな軽い内容でも嬉しいです。
なぜCrieitを作ろうと思ったか

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

有料記事を販売できるようになりました!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください!
ボードとは?

関連記事

コメント