Makefileで動的な変数をevalで作成する場合の注意点

## 概要
Makefile で eval で変数が空になる現象に悩まされていたのだが、原因が分かったのでまとめておく。

### 環境

* Ubuntu 20.04 LTS
  * GNU Make 4.2.1

### NG例
KubernatesでPodが起動するのを待ってPostgresのコンテナに対してSQL実行する例。

```Makefile
# 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

上記の動作が分かる例

```Makefile
test1:
        @date '+%T'  # <1>
        sleep 3
        @echo $(shell date '+%T')
```

**実行結果**

2つ目の `$(shell date '+%T')` が<1>と同時刻になっている。

```console
$ make test1
20:09:43
sleep 3
20:09:43
```

### 起動時評価の例2: eval + shell

上記の shell の動作に準じる動きをします。

```Makefile
test2:
        @date '+%T'  # <1>
        sleep 3
        $(eval T1=$(shell date '+%T'))
        @echo $(T1)
```

**実行結果**

2つ目の `$(eval T1=$(shell date '+%T'))` が<1>と同時刻になっている。

```console
$ make test2
20:12:46
sleep 3
20:12:46
```

### 遅延評価の例1: eval + `date '+%T'`


```Makefile
test3:
    @date '+%T'              # <1>
    sleep 3
    $(eval T1=`date '+%T'`)  # <2>
    @echo $(T1)
```

**実行結果**

<2>にはsleepした後の時刻が入る。

```console
$ make test3
20:18:57
sleep 3
20:19:00
```

## 最初のNG例の修正例

* `eval` で変数を作るまでもなかったので、`eval` は使わなかった
* `$(shell ...)` を使うのをやめて、shell実行時の評価に変更した。`psql -h $$(...)` の行が評価され `$$` → `$` となり、`psql -h $(...)` のコマンドが実行される。

```Makefile
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)](https://www.gnu.org/software/make/manual/html_node/Eval-Function.html)
* [gnu make - Why does makefile lazy evaluation find a file in a "parent" recipe but not the current one? - Stack Overflow](https://stackoverflow.com/questions/64084643/why-does-makefile-lazy-evaluation-find-a-file-in-a-parent-recipe-but-not-the-c)

## あとがき

普通は遅延評価が必要になることはないが、必要になるときは Makefile ではなく、Shellの世界で考えることになる。

遅延評価が必要なケースでは、待ち合わせ処理をシェルで実装することになる。今回は Kubernetes で `kubectl wait` を使ったが shell の世界で頑張る方法もある。

以下は、while ～ sleep 1 で、空文字でなくなるか 30秒のタイムアウト超過するまで待ち合わせを行う例となる。遅延評価をしたいので `$$` を使っていることに留意する。

```Makefile
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
```