2017年6月18日 星期日

Dynamical Variable Assignment in Makefile

上星期遇到奇怪問題卡了我一天:第一次 make 成功,直接重新 make 第二次結果失敗了,第三次 make 又成功?為什麼 make 結果會不一樣?

一開始找資料發現 Makefile 針對變數指定方式的不同,會有兩種展開方式:

Two Flavors of Variables

Recursively expanded variable v.s. Simply expanded variable
一種是使用=,之後執行時都會回來改變等號左邊的變數(recursively),而另一種使用:=,將當下的值指定給等號左邊的變數(simply),之後不再更動。
Recursively 範例:
x = foo
y = $(x) bar
x = later
echo $(y) 的輸出結果會是 later bar
Simply 範例:
x := foo
y := $(x) bar
x := later
echo $(y) 的輸出結果是 foo bar
認知升級後,馬上將 Makefile 的等號改成 :=,結果根本不是這個原因…只好繼續找資料,深追問題。
後來發現問題是在 rule 中使用 $(shell ... ) 動態指派變數的話,rule 在執行前就會先展開 $(shell ... ) 了,意思是 $(shell ...) 比 rule 還早被執行。

使用 $eval(…) 搭配 $(shell …) 動態執行指令

以下透過 Makefile 範例來解釋:
執行 make 後會先砍掉所有 rom 檔,並建立 test.rom,確保當前目錄只有 test.rom,接著透過 find 指令查找 rom 檔,將檔名 assign 給變數 ROM
all:
    @rm -rf *.rom
    @touch test.rom
    $(eval ROM := $(shell find ./ -name *.rom))
    @echo name=$(ROM)
情境一、當前目錄不存在 *.rom 時:
[user][linux][~/test]% ls *.rom
ls: No match.
[user][linux][~/test]% make all
name=
情境二、當前目錄存在 *.rom 時:
[user][linux][~/test]% touch existed.rom
[user][linux][~/test]% ls *.rom
existed.rom
[user][linux][~/test]% make all
name=./existed.rom
造成上面兩個結果不同的原因,因為 $(shell ...)先執行了!有注意到情境二的 name 是 existed.rom 而非 Makefile 中建立的 test.rom 嗎?

改用 Backquote 如何?

想說好吧,不呼叫 shell 了,用另一種方式解看看。於是使用 backquote (或稱 backtick) ` 執行 find 指令,例如:
all:
    @rm -rf *.rom
    @touch test.rom
    $(eval ROM := `find ./ -name *.rom`)
    echo "before mv: name=$(ROM)"
    mv $(ROM) newname.rom
    echo "after mv: name=$(ROM)"
輸出結果:
[user][linux][~/test]% make all
echo "before mv: name=`find ./ -name *.rom`"
before mv: name=./test.rom
mv `find ./ -name *.rom` newname.rom
echo "after mv: name=`find ./ -name *.rom`"
after mv: name=./newname.rom
的確,使用 backquote 變成當下才去執行指令,符合我的預期。但其實我是想把 find 的輸出結果當成 constant string 來使用,而實際上 $(ROM) 存的卻是完整的指令,不是指令執行完的輸出結果…
所以最後,我放棄在 Makefile 動態指派變數的值了,把想要執行的指令直接寫成 script 讓 Makefile 呼叫,省去不必要的麻煩。

Reference

  1. The Two Flavors of Variables
    https://ftp.gnu.org/old-gnu/Manuals/make-3.79.1/html_chapter/make_6.html#SEC59
  2. Stackoverflow
    https://stackoverflow.com/questions/34935544/how-to-time-the-execution-of-a-gnu-make-rule