TechQuant Blog

just (justfile) を Makefile の代わりに使う — 乗り換えて半年経った所感とハマり所

7分で読める

金曜の深夜、auto_daily_trader のリポジトリで make backup-db を打とうとして make backupp-db と打ち間違えた。当然エラー。次に Tab 補完を叩いたら候補が 4 つ出た。Makefile に書いたターゲットが 30 個近くあって、自分で書いたはずの名前を毎回 README で確認している。さすがに馬鹿らしくなった。

その夜、just を試した。1 時間で全部のターゲットを移植して、Makefile を git rm した。半年経った今も後悔はない。

本記事は just 1.27 系で動作確認している。インストールは brew install justcargo install just、もしくは公式のインストーラから。

なぜ Makefile を捨てたのか

Makefile はもともと C のビルド用ツール。依存解決と「最終生成物のキャッシュ」が本業で、シェルスクリプトを並べる用途は二次的なものだ。それを承知のうえで自分も長く使ってきた。以前書いた Python での Makefile 自動化ガイド でも触れたように、シェル代わりに使う分には十分動く。

ただ、長く使っていると小さな不満が積もる。

  • インデントは Tab 必須。Space を混ぜると無言で壊れる
  • $(VAR)${VAR} を Make の世界とシェルの世界で混同する
  • .PHONY を書き忘れて、同名のファイルがあると突然動かなくなる
  • 引数を渡したいだけで MAKEFLAGS の沼にハマる

毎回詰まるたびに「Make の本質的な強みを、自分は実は使っていないのでは」という疑念があった。生成物のキャッシュなんて pyproject.toml の編集が走った瞬間に意味を失うし、依存解決もせいぜい 2、3 階層しか書いていない。

just はそこに刺さる。依存解決もキャッシュも要らない、ただターゲットを定義して引数を取りたいだけ — そのユースケースに振り切った命令ランナーだ。シンプルなぶん、Makefile の罠の半分以上が消える。

最初の 5 分で書ける justfile

インストール:

# macOS
brew install just

# Debian / Ubuntu (cargo 経由)
cargo install just

# 公式インストーラ (~/.local/bin に置く)
curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh \
  | bash -s -- --to ~/.local/bin

リポジトリ直下に justfile(Justfile でも可)を置く。

default:
    @just --list

backup-db:
    pg_dump -U trader trading > backups/$(date +%Y%m%d).sql

deploy:
    rsync -av --delete public/ vps:/var/www/blog/

just を引数なしで叩くと default が走る。これに just --list を書いておくと、ターゲット一覧が勝手に整形されて出る。make help 用のレシピを毎回手で書いていた頃の自分を労いたい気持ちになる。

Tab とインデントの呪縛から解放される

Makefile はインデントが Tab 必須で、エディタが勝手に Space に変換すると死ぬ。just はインデントが揃っていればよく、Space でも Tab でも動く。これだけで「あの謎エラー」を踏むことが無くなった。

引数・依存・条件分岐

引数を取るのが一番楽だ。Makefile だと make foo ARG=bar のような書き方になるが、just は普通の関数呼び出しに近い。

deploy env="staging":
    rsync -av public/ {{env}}:/var/www/blog/

# 呼び方
just deploy production

引数のデフォルト値も書ける。位置引数なので「どの順番だっけ」と悩むこともない。

依存関係は : の右側に並べる。

build:
    npm run build

test: build
    npm test

deploy env="staging": test
    rsync -av public/ {{env}}:/var/www/blog/

just deploy production を叩くと、build → test → deploy の順で解決される。Make と挙動はほぼ同じだが、書式が読みやすい。

レシピのシェルは set 文で固定できる。

set shell := ["bash", "-euc"]

clean:
    rm -rf dist/
    echo "cleaned"

-e で途中失敗時に止まり、-u で未定義変数を即エラーにできる。Makefile でこれをやるのは結構面倒で、結局 SHELL := bash -euc を書いて、それでも一行ごとに別シェルが立ち上がる仕様で混乱する。just だと「set 一行で全レシピに効く」が直感に合う。

環境変数と dotenv の付き合い方

.env を自動で読みたい」というシーンは多い。just は標準で対応している。

set dotenv-load := true

deploy:
    @echo "DB_URL is $DB_URL"
    rsync -av public/ $DEPLOY_HOST:/var/www/blog/

.envDB_URL=postgres://... と書いておけば、レシピ内で $DB_URL がそのまま展開される。シェルスクリプトと同じ感覚で書けるのが良い。

ちなみに自分は direnv での環境変数管理 も併用していて、対話開発には direnv、CI から叩く一発タスクには .env、というすみ分けにしている。両者は競合しないので、好みで混ぜていい。

Makefile から移行するときに詰まった所

1. レシピは 1 行ごとに別シェル

これは Makefile と同じ仕様だが、見落としやすい。

bad:
    cd /tmp/foo
    pwd       # /tmp/foo ではない (別シェル)

set shell := ["bash", "-euc"] を入れたうえで、複数行をまたぐ処理は && でつなぐか、レシピ全体を 1 行に書く。

good:
    cd /tmp/foo && pwd

2. {{}} と $() の使い分け

{{var}} は just のテンプレート展開、$(...) はシェルのコマンド置換。最初の 1 週間はだいたいここで詰まる。「コンパイル時に決まる値は {{}}、実行時に呼ぶシェルの値は $()」と覚えるとブレない。

3. .PHONY が無い代わりに、常にレシピが走る

just はターゲットがファイルかどうかを見ない。常にレシピを実行する。.PHONY 書き忘れ事故は起きないが、Make の癖で「これ毎回ビルドし直しちゃまずい」と思うかもしれない。安心していい。本当にキャッシュが要るタスクは just の責務外なので、シェル側で if [ -f dist/foo ] 等を書く。

日常で叩いている自分の justfile

最後に、ブログ用と auto_daily_trader 用に書いた justfile の抜粋。コメントを書くと just --list の出力に説明文として並ぶので、README 代わりにもなる。

set dotenv-load := true
set shell := ["bash", "-euc"]

default:
    @just --list

# ブログ記事を1本ビルドして公開する
publish slug:
    python3 seoblog/scripts/build_one.py {{slug}}
    bash scripts/deploy_seoblog.sh

# 直近 N 日の取引ログを集計
weekly-pnl days="7":
    python3 tools/aggregate_pnl.py --days {{days}}

# Raspberry Pi 5 から VPS にデータをミラー
mirror-to-vps:
    rsync -avz --delete data/ vps:/srv/trading/

# 全テスト+lint
check:
    pytest -q tests/
    ruff check .
    ruff format --check .

VPS 側の自動化も同じ流れで justfile に統一した。ちなみに自分は本番側のホストに お名前.comの高性能VPS を使っていて、ピーク時の安定度はかなり良い。「書きやすさ」が長く効くのは、無人運用に組み込んだときに一番効いてくる。

以前書いた Claude Code を cron で自動化する話 のような無人パイプラインに組み込むときも、justfile に集約しておくとデバッグが格段に楽になる。タスクが標準化されるので、ログを追うときに「どこから叩かれたタスクか」が一目で分かる。

結局、移行する価値はあるか

動いている Makefile を捨てるコストに見合うかは、リポジトリのターゲット数による。10 個以下ならわざわざ移行しなくていい、というのが正直な所感。20 個を超えたあたりから、justfile の --list と引数の取りやすさが効いてくる。

悩むくらいなら、1 つのリポジトリだけ試しに移植してみるといい。1 時間で終わる。半月後に Makefile に戻りたい気が起きるかどうかで判断すれば、自分の答えが出る。少なくとも自分はもう戻る気は無い。