1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

複数言語を扱うプロジェクト、ビルドはどうする

Posted at

どうも。日曜プログラマのUBinKitteです。
最近、自分のために画像のブックマークをするアプリを作ったのですが、
不意に、いくつか言語使って開発したらカッコよくねと思い立って、Javascript、PythonとRustが混在するプロジェクトを作ってしまいました。

こうなると手動でビルドするのは面倒です。そこで、色々とタスクランナーを調べたり、ChatGPTに聞いたりしました。

※この記事での「タスクランナー」はmakeの様なビルドの自動化をするものの事を指してるいます。

前提

  • Windows環境 + MSYS2を使用したgccとrust

この記事では、例としてこんなのをビルドしてみます。

ディレクトリ構成

\---project
    +---c
    |       main.c
    |       
    +---dist
    \---rust
        |   .gitignore
        |   Cargo.lock
        |   Cargo.toml
        |   
        \---src
                main.rs

また、中身は、

./c/main.c
#include <stdio.h>

int main() {
    printf("Hello world!\n");
}

./rust/src/main.rs
use std::process::Command;

fn main() {
    let _ = Command::new(r".\a.exe").spawn().expect("error!");
}

となっています。

a.exemain.cgccで、rust.exemain.rscargoでそれぞれコンパイルしたものです。
で、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。お恥ずかしながら私が唯一使えるものです(笑)。
圧倒的な知名度と歴史があるので、解説は省きます。各々調べてください。

makefile
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のRakeSConsなどありますが、ここではPythonとinvokeの例を紹介します。

build.py
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のリリースページからダウンロードしてください。

Makefile.toml
[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のように小回りが利く
  • シェル語以外にも、Pythonjsperlまで召喚できる

デメリット

  • 情報が少ない 
  • 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を書きます。

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を作成します。
中身を、このように編集します。

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'のように指定するとなんとコマンドをそのまま使えるようです。

これを活かして、ちょっとこんなことをしてみました。

build.gradle
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を作ります。そして中身を、

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な気がします。
大きくなってきたらBazelMesonだったりをおすすめしたいです。今回は動きませんでしたが...。

おれはいつまでもMakefileをあいしているぜ!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?