どうも。日曜プログラマのUBinKitteです。
最近、自分のために画像のブックマークをするアプリを作ったのですが、
不意に、いくつか言語使って開発したらカッコよくねと思い立って、Javascript、PythonとRustが混在するプロジェクトを作ってしまいました。
こうなると手動でビルドするのは面倒です。そこで、色々とタスクランナーを調べたり、ChatGPTに聞いたりしました。
※この記事での「タスクランナー」はmakeの様なビルドの自動化をするものの事を指してるいます。
前提
- Windows環境 + MSYS2を使用したgccとrust
この記事では、例としてこんなのをビルドしてみます。
ディレクトリ構成
\---project
+---c
| main.c
|
+---dist
\---rust
| .gitignore
| Cargo.lock
| Cargo.toml
|
\---src
main.rs
また、中身は、
#include <stdio.h>
int main() {
printf("Hello world!\n");
}
use std::process::Command;
fn main() {
let _ = Command::new(r".\a.exe").spawn().expect("error!");
}
となっています。
a.exeはmain.cをgccで、rust.exeはmain.rsをcargoでそれぞれコンパイルしたものです。
で、distに詰めます。
Rustのコードが甘過ぎで実行が終わったあとにHello, world!が出てきますが、細かいことは気にしないでください。
シェルスクリプトorバッチファイル
学習難易度が一番低いです。というか全員履修済みでは?
加えて、どの環境でも動きます。そういう所では最強じゃないかと...。
# ウチの環境ではcargoをmsysで動くようにしてないので動作未確認です。orz
mkdir ./dist
gcc ./c/main.c -o ./dist/a.exe
cargo build --release
cp ./rust/target/release/rust.exe ./dist/rust.exe
mkdir .\dist
gcc .\c\main.c -o .\dist\a.exe
cargo build --release --manifest-path .\rust\Cargo.toml
cp .\rust\target\release\rust.exe .\dist\rust.exe
メリット
- ほとんどの人がわかる + 学習難易度が低い
- 悩んでも情報量が多い
デメリット
- 記述が単調になりがち
- Windows環境とその他Unix環境で使い回せない -> MSYS2やCygwinなど選択肢は有る
- エラーに対してめっぽう弱い
まあ、あんまり嬉しいタスクランナーではないかもです。
ただ、小規模なプロジェクトならこれでいいでしょう。
makefile (GNU Make)
おなじみGNU Make。お恥ずかしながら私が唯一使えるものです(笑)。
圧倒的な知名度と歴史があるので、解説は省きます。各々調べてください。
CC = gcc
CFLAGS =
DIST_DIR = ./dist
C_SRC = ./c/main.c
RUST_MANIFEST = ./rust/Cargo.toml
all: $(shell (mkdir ./dist)) $(DIST_DIR)/main_c $(DIST_DIR)/main_rust
$(DIST_DIR)/main_c: $(C_SRC)
@$(CC) $(CFLAGS) $(C_SRC) -o $(DIST_DIR)/a
$(DIST_DIR)/main_rust: $(RUST_MANIFEST)
@cargo build --release --manifest-path $(RUST_MANIFEST)
@cp ./rust/target/release/rust.exe $(DIST_DIR)/rust.exe
.PHONY: all
見慣れすぎて特に感想がありません。
メリット
- ほとんどの人がわかる
- 悩んでも情報量が多い
-
makeと入力するだけで実行できる ChatGPT曰く、『伝統的なビルドツール』
デメリット
- 大規模なプロジェクトに使うには動作が遅め
- 移植性は少し低め
安心感強めのタスクランナーでしょうが、モダンなプロジェクト向きではないかもです。
というかMakefileってビルドツールなんですっけ...。
小回りの利く点では最強だと思います。
簡単なものならmakeだけで実行できる手軽さもいいですね。
スクリプト言語のライブラリとか
RubyのRakeやSConsなどありますが、ここではPythonとinvokeの例を紹介します。
from invoke import task
@task
def build(c):
c.run("mkdir dist")
c.run("gcc ./c/main.c -o ./dist/a")
c.run("cargo build --release --manifest-path ./rust/Cargo.toml")
c.run("cp ./rust/target/release/rust.exe ./dist/rust.exe")
invokeはpipで入れてください。
py -m pip install invoke
また、実行するときは、
invoke buildです。
メリット
- 慣れ親しんだスクリプト言語の環境が使える
デメリット
- シェル語に依存するため、対応が必須
- 引数がうまく動作しないなど、各言語の抱える悩みがある
ChatGPTに紹介されたので使ってみましたが、あんまり良くない気がします。
スクリプト言語が使える点は良いですが、特別な理由がなければこれを使う必要はなさそうです。
cargo-make
cargo-makeはRust製のタスクランナーです。Rustを想定した作りですが、他の言語で使うのもOKな感じです。
Githubのリリースページからダウンロードしてください。
[tasks.create_dist]
command = "mkdir"
args = ["-p", "./dist"]
[tasks.build_c]
command = "gcc"
args = ["./c/main.c", "-o", "./dist/a"]
# こういう書き方もできる
[tasks.build_rust]
script_runner = "@shell"
script = '''
cargo build --release --manifest-path ./rust/Cargo.toml
cp ./rust/target/release/rust.exe ./dist/rust.exe
'''
[tasks.build_all]
dependencies = ["create_dist", "build_c", "build_rust"]
command = "echo"
args = ["Builds completed successfully!"]
メリット
-
tomlで書ける - Rustと相性が良い
- makefileのように小回りが利く
- シェル語以外にも、
Pythonやjs、perlまで召喚できる
デメリット
- 情報が少ない
-
tomlは複雑になると可読性が落ちる
RustのCargo.tomlの書き心地です。あれが好きな方にはおすすめ。
あの書き心地を求めていました私でしたが、引数の指定で泣きました。
args = ["./c/main.c", "-o", "./dist/a"]ってなんだよ(憤慨)
また、script_runnerという機能でPythonやシェル語など他言語が呼べます。なぜ。シェル語が呼べるので、上の引数問題も解決します。どういうことだ。
名前こそcargoですが、tomlが読めるなら案外使いやすいです。
Taskfile (Task)
TaskfileはGoで実装された比較的新しいタスクランナーです。
Taskfileは、makefileのように独自の書き方をせず、yamlを使用します。
インストールは各種パッケージマネージャーから、もしくはGithubのリリースページから。
# https://taskfile.dev/installation/ より一部
winget install Task.Task
brew install go-task
sudo snap install task --classic
そして、Taskfile.ymlを書きます。
version: '3'
tasks:
build_c:
cmds:
- gcc ./c/main.c -o ./dist/a
description: "Build C project"
build_rust:
cmds:
- cargo build --release --manifest-path ./rust/Cargo.toml
- cp ./rust/target/release/rust.exe ./dist/rust.exe
description: "Build Rust project"
build_all:
deps: [build_c, build_rust]
description: "Build both C and Rust projects"
yamlでタスクランナー系のものを書けるとは思いませんでしたね。
メリット
-
yamlが使える -
makefileのような小回りの良さ -
makefileより記述が簡単
デメリット
- 大規模プロジェクトは厳しい
Taskのホームページには、
Task is a task runner / build tool that aims to be simpler and easier to use than, for example, GNU Make.
と書いてあり、GNU Makeをかなり意識していることが読み取れます。
yamlでの記述ができて管理しやすいですが、新しい故に使いにくい部分もありそうです。
MakefileとDockerfileの中間みたいな感じです。yamlわかる方なら簡単に馴染むと思います。
無理やりビルド
「他言語用のツールでビルドしよう」という企みです。
gradle
皆さんこ存じ「Gradle」はJavaなんかのビルドシステムですね。
ただ、ふつーにタスクランナーとして使えます。
プロジェクトのルートに、build.gradleを作成します。
中身を、このように編集します。
task buildC(type: Exec) {
commandLine 'gcc', 'c/main.c', '-o', 'dist/a.exe'
}
task buildRust(type: Exec) {
commandLine 'cmd', '-c' 'cargo build --release --manifest-path rust/Cargo.toml'
}
stask copyRust(type: Copy) {
from file('rust/target/release/rust.exe')
into file('dist')
}
task buildAll {
dependsOn buildC, buildRust, copyRust
}
task createDistDir {
doLast {
mkdir 'dist'
}
}
buildAll.dependsOn createDistDir
依存関係の書いてないgradleファイルを見るのは初めてかもしれません。
gradle buildAllで実行できます。
'cmd', '-c' 'cargo build --release --manifest-path rust/Cargo.toml'のように指定するとなんとコマンドをそのまま使えるようです。
これを活かして、ちょっとこんなことをしてみました。
ext {
CC = 'gcc'
CFLAGS = ''
DIST_DIR = '-o ./dist'
C_SRC = './c/main.c'
}
task buildC(type: Exec) {
commandLine 'cmd', '-c', "${CC}' ${CFLAGS} ${C_SRC} ${DIST_DIR}"
}
パット見はmakefileですね(笑)。
task同士で依存関係も使えるので、makefileの代替としてアリです。
もっと言えば、VSCodeの拡張機能を使えばGUI上でtaskの確認ができるので、makefileよりも優秀かもしれません。
npm scripts
皆さんご存知、主にnode.js用のnpm scriptsです。
まず、プロジェクトのルートにpackage.jsonを作ります。そして中身を、
{
"scripts": {
"build": "mkdir dist && gcc ./c/main.c -o ./dist/a && cargo build --release --manifest-path ./rust/Cargo.toml && cp ./rust/target/release/rust.exe ./dist/rust.exe"
}
}
こうしてから、npm run buildです。
「素直にコピーして貼れよ」って思いましたが、コピペできないCLI環境などでは使えるので、考えられる中で最も悪い選択肢としては有りかもしれないです。
まあシェル語っていう選択肢があるんですが...。
その他
いろいろ見つけましたが、ウチの環境ではうまく動かなかったものたちです。
msvcを使わないようにしているので、大半動きませんでした。
まあ他の方が情報上げてるからいいかな...と。
結論
今回の例くらいの簡単なものならMakefileでいいんじゃないでしょうか?保守性を考えるならtaskfileな気がします。
大きくなってきたらBazelやMesonだったりをおすすめしたいです。今回は動きませんでしたが...。
おれはいつまでもMakefileをあいしているぜ!