概要
Makefile で eval で変数が空になる現象に悩まされていたのだが、原因が分かったのでまとめておく。
環境
- Ubuntu 20.04 LTS
- GNU Make 4.2.1
NG例
KubernatesでPodが起動するのを待ってPostgresのコンテナに対してSQL実行する例。
# NG: make create
create:
kubectl apply -f postgres.pod.yaml
kubectl wait --for condition=Ready --timeout=90s pod postgres-pod
# NG: ↓empty result
$(eval TARGET_HOST := $(shell kubectl get services postgres-service -o jsonpath='{.status.loadBalancer.ingress[*].ip}'))
psql -h $(TARGET_HOST) -U $(DB_USER_NAME) -d fruits -f test.sql
# OK: make exec-sql
exec-sql:
# OK: -> TARGET_HOST=xxx.xxx.xxx.xxx
$(eval TARGET_HOST := $(shell kubectl get services postgres-service -o jsonpath='{.status.loadBalancer.ingress[*].ip}'))
psql -h $(TARGET_HOST) -U $(DB_USER_NAME) -d fruits -f test.sql
ポイント
- Makefileの変数の評価は Makefile が読み込まれたときに行われる
解説
-
make create
の実行前ではpostgres-service
が存在していないため、TARGET_HOST
の中身は空文字になった -
make exec-sql
は、make create
でpostgres-service
が作成し終わった後に実行するものであり、どのタイミングでも期待通り動作する
eval/shellの実行例
起動時評価の例1: shell
上記の動作が分かる例
test1:
@date '+%T' # <1>
sleep 3
@echo $(shell date '+%T')
実行結果
2つ目の $(shell date '+%T')
が<1>と同時刻になっている。
$ make test1
20:09:43
sleep 3
20:09:43
起動時評価の例2: eval + shell
上記の shell の動作に準じる動きをします。
test2:
@date '+%T' # <1>
sleep 3
$(eval T1=$(shell date '+%T'))
@echo $(T1)
実行結果
2つ目の $(eval T1=$(shell date '+%T'))
が<1>と同時刻になっている。
$ make test2
20:12:46
sleep 3
20:12:46
遅延評価の例1: eval + date '+%T'
test3:
@date '+%T' # <1>
sleep 3
$(eval T1=`date '+%T'`) # <2>
@echo $(T1)
実行結果
<2>にはsleepした後の時刻が入る。
$ make test3
20:18:57
sleep 3
20:19:00
最初のNG例の修正例
-
eval
で変数を作るまでもなかったので、eval
は使わなかった -
$(shell ...)
を使うのをやめて、shell実行時の評価に変更した。psql -h $$(...)
の行が評価され$$
→$
となり、psql -h $(...)
のコマンドが実行される。
create:
kubectl apply -f postgres.pod.yaml
kubectl wait --for condition=Ready --timeout=90s pod postgres-pod
psql -h $$(kubectl get services postgres-service -o jsonpath='{.status.loadBalancer.ingress[*].ip}') -U $(DB_USER_NAME) -d fruits -f test.sql
参考
- Eval Function (GNU make)
- gnu make - Why does makefile lazy evaluation find a file in a "parent" recipe but not the current one? - Stack Overflow
あとがき
普通は遅延評価が必要になることはないが、必要になるときは Makefile ではなく、Shellの世界で考えることになる。
遅延評価が必要なケースでは、待ち合わせ処理をシェルで実装することになる。今回は Kubernetes で kubectl wait
を使ったが shell の世界で頑張る方法もある。
以下は、while ~ sleep 1 で、空文字でなくなるか 30秒のタイムアウト超過するまで待ち合わせを行う例となる。遅延評価をしたいので $$
を使っていることに留意する。
create2:
kubectl apply -f postgres.pod.yaml
timeout 30 bash -c "while [ -z $$(kubectl get services postgres-service -o jsonpath='{.status.loadBalancer.ingress[*].ip}') ]; do sleep 1; done"
psql -h $$(kubectl get services postgres-service -o jsonpath='{.status.loadBalancer.ingress[*].ip}') -U $(DB_USER_NAME) -d fruits -f test.sql