👀

cargo watch で .env ファイルの変更も監視する

2023/06/14に公開

TL;DR

.env と .trigger を .gitignore に追加して、以下のスクリプトを cargo run の代わりに実行する。

run.sh
#!/bin/bash

set -e -u -o pipefail

# kill all background child processes when exit.
trap "exit" INT TERM
trap "kill 0" EXIT

realpath() {
  [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"
}

SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE:-$0}")")"

cd "${SCRIPT_DIR}"

# .trigger ファイルがないときは作成する
if [ ! -f .trigger ]; then
  touch .trigger
fi

cargo watch -x fmt -x build -s "touch .trigger" &
cargo watch --no-ignore -w .env -s "touch .trigger" &
cargo watch --no-ignore -w ".trigger" -x run

cargo-watch について

Rust で Web サーバーを開発する際、ソースコードの変更を検知して再ビルド & サーバー再起動ができると便利です。

cargo-watchを使用するとそれが実現できます。

具体的には、 cargo-watch をインストールして以下のようにコマンドを実行すると、

cargo watch -x fmt -x run

ソースコードを変更したタイミングで自動で cargo fmt と cargo run コマンドを実行してくれるようになります。

.env ファイルを使用するプロジェクト

さて、サーバー開発をする際、環境変数の変更に伴ってサーバーを再起動したいことがあります。

たとえば、以下のような内容で各開発者のローカル環境向けの環境変数を .env ファイルとして定義しておき、

.env
GREETING_TEXT="Good morning"
PORT=3030

rust のソースコード側では dotenv クレートを使用して、サーバー起動時に環境変数を読み込んでいるという構成を考えます。

main.rs
use dotenv::dotenv;
use std::env;
use warp::Filter;

#[tokio::main]
async fn main() {
    dotenv().ok(); // .env ファイルから環境変数を読み込む

    let greeting_text = env::var("GREETING_TEXT").unwrap();
    let port = env::var("PORT").unwrap().parse::<u16>().unwrap();

    let greeting =
        warp::path!("greeting" / String).map(move |name| format!("{}, {}!", greeting_text, name));

    warp::serve(greeting).run(([127, 0, 0, 1], port)).await;
}

この構成のとき、ソースコードが変更された場合はもちろん、 .env ファイルが変更された場合もそれを検知してサーバーが再起動されると嬉しいです。

ただし、 .env ファイルのような各開発者向けの情報が含まれたファイルは普通は .gitignore で無視されるようになっているはずです。 cargo-watch は .gitignore で無視されたファイルをデフォルトでは監視対象にしないため、そのままでは .env ファイルの変更が検知できません。

また、 .env ファイルを監視したとしてもそのファイルが変更されたタイミングでわざわざ fmt, build 処理を呼び出すのは無駄になります。

これに対処するのが記事の冒頭の run.sh スクリプトです。

run.sh スクリプトについて

run.sh スクリプトの肝になっているのは次の3行です

cargo watch -x fmt -x build -s "touch .trigger" &
cargo watch --no-ignore -w .env -s "touch .trigger" &
cargo watch --no-ignore -w ".trigger" -x run

1行目では cargo-watch の -s オプションを使用して、 fmt, build 処理に次いで .trigger ファイルの日付を更新しています。ここでは監視対象のファイルを特に指定していないため、 .gitignore に含まれないようなローカルのすべてのファイルが監視対象になります。

2行目では -w オプションを使用して .env ファイルの変更を監視し、同じく .trigger ファイルを更新しています。ここで --no-ignore オプションを設定しているのは、 .env のような .gitignore ファイルで無視されているファイルを監視対象にするためです。

3行目では1, 2行目で更新される .trigger ファイルを監視して、変更があった場合にサーバーを起動するようにしています。

基本的な仕組みは以上ですが、この処理だけをスクリプトとして実装すると、スクリプトを終了させてもバックグラウンドで起動している1, 2行目の処理が動き続けたままになってしまって不便です。

そのため、 run.sh スクリプトではスクリプトの終了処理をトラップして、バックグラウンドで実行している1, 2行目の処理も一緒に終了させる仕組みを入れています。

Discussion