11 個 Makefile 實戰技巧

本文首次發表在 11 個 Makefile 實戰技巧 -- 泰曉科技linux

過去數月,筆者一直在重構開發了數年的開源項目:Linux Lab(Linux 內核實驗室)。git

在開發過程當中,須要跟 Makefile 打交道,完善功能,優化速度,提高體驗。shell

數月下來,積累了很多 Makefile 實戰技巧。回過來看,以前對 Makefile 的熟知程度只能說是幼兒園託班水平 ;-)bash

本文篇幅較長,請先看大綱。建議從大綱中選擇感興趣的部分閱讀(文末有彩蛋 ^_^):ide

1. 當即賦值(:=)和延遲賦值(=)
2. 變量賦值 和 目標執行 之間的時序關係
3. 如何獲取 Make 傳遞的全部參數和編譯目標
4. Makefile 調試與跟蹤方法一覽
5. Makefile 與 Shell 中的文件名處理差別
6. 在 Makefile 表達式中使用逗號和空格變量
7. 在 Makefile 中對軟件版本號作差別化處理
8. 修改默認執行目標的簡單方法
9. 檢查文件是否存在的兩種方法
10. 如何相似普通程序同樣把目標當變量使用
11. Makefile 實例模板
複製代碼

本文彙總了諸多 Makefile 進階用法,便於提高 Makefile 閱讀和編寫效率。工具

當即賦值(:=)和延遲賦值(=)

  • :=: 強制按前後順序執行,當即賦值。
  • =:賦值的結果會等到整個路徑執行完再決定,後面的會覆蓋前面的,延遲賦值。

按照常規邏輯,建議默認選用 ":="。優化

實例以下:ui

$ cat Makefile

a = foo
b1 := $(a) bar
b2 = $(a) bar
a = xyz

all:
	@echo b1=$(b1)
	@echo b2=$(b2)

$ make
b1=foo bar
b2=xyz bar
複製代碼

變量賦值 和 目標執行 之間的時序關係

這裏再看看變量賦值和編譯目標之間的關係,以及不一樣的變量傳遞和設置方式。this

先看看一般可能會傳遞參數的方式,你們以爲哪一個會生效呢?spa

$ make a=b target
$ make target a=b
$ a=b make target
$ export a=b && make target
複製代碼

另外,這種狀況下,target1 和 target2 打印的變量同樣嗎?

a = aaa

test1:
	echo $a

a = bbb

test2:
	echo $a
複製代碼

下面看一個案例(注意:target 下命令縮進必須是一個 TAB)。

$ cat Makefile

a ?= aaa
b := $(a)
c = $(a)

a_origin = $(origin a)
b_origin = $(origin b)
c_origin = $(origin c)

all:
	@echo all:$(a)
	@echo all:$(b)
	@echo all:$(c)
	@echo all:$(a_origin)
	@echo all:$(b_origin)
	@echo all:$(c_origin)

a = bbb
b := $(a)
c = $(a)

test1:
	@echo test1:$(a)
	@echo test1:$(b)
	@echo test1:$(c)
	@echo test1:$(a_origin)
	@echo test1:$(b_origin)
	@echo test1:$(c_origin)

a = ccc
b := $(a)
c = $(a)

test2:
	@echo test2:$(a)
	@echo test2:$(b)
	@echo test2:$(c)
	@echo test2:$(a_origin)
	@echo test2:$(b_origin)
	@echo test2:$(c_origin)

a = ddd
複製代碼

看看執行狀況。

關於 變量賦值 和 目標中的變量引用 的順序

首先,執行默認 target,也就是第一個出現的 target,這裏是 "all":

$ make
all:ddd
all:ccc
all:ddd
all:file
all:file
all:file
複製代碼

比較奇怪的是?爲何 "all" 目標恰好在這三條以後,卻拿到了 ddd, ccc 和 ddd 呢?

a ?= aaa
b := $(a)
c = $(a)
複製代碼

爲何不是 aaa, aaa 和 aaa 呢?

接着,執行 test1, test2:

$ make test1
test1:ddd
test1:ccc
test1:ddd
test1:file
test1:file
test1:file

$ make test2
test2:ddd
test2:ccc
test2:ddd
test2:file
test2:file
test2:file
複製代碼

發現,test1, test2 都同樣?因此,結論是,Makefile 中全部變量賦值的語句在全部 target 以前完成,跟變量賦值與 target 的相對位置無關。

另外,咱們能夠看到 b 沒有跟上 c 的節奏,拿到 ccc 就再也不跟 c 同樣去拿最後設置的 ddd 了,體現了 「:=」 的 「當即賦值」,而 c 一直等到了 Makefile 最後的 a。另外,三個變量最後的值都是文件內部賦值,因此 origin 是 file.

經過命令行賦值

$ make a=fff
all:fff
all:fff
all:fff
all:command line
all:file
all:file
複製代碼

發現命令行覆蓋了 Makefile 中全部的變量賦值,a 的優先級很高。

$ make b=fff
all:ddd
all:fff
all:ddd
all:file
all:command line
all:file
複製代碼

因爲 a 和 c 沒用引用 b,因此這裏只有 b 發生了變化。

$ make c=fff
all:ddd
all:ccc
all:fff
all:file
all:file
all:command line
複製代碼

一樣,a 和 b 沒有引用 c,只有 c 發生了變化。

經過環境變量賦值

$ a=xxx make
all:ddd
all:ccc
all:ddd
all:file
all:file
all:file
複製代碼

發現並無生效,仍是用的 make 的內部賦值語句。

$ a=xxx make -e
all:xxx
all:xxx
all:xxx
all:environment override
all:file
all:file
複製代碼

確實都改了,因此要讓環境變量生效,得給 make 傳遞 -e

$ b=xxx make -e
all:ddd
all:xxx
all:ddd
all:file
all:environment override
all:file
複製代碼

這個的效果一樣:

$ export b=fff
$ make -e
all:ddd
all:fff
all:ddd
all:file
all:environment override
all:file
複製代碼

只是建議不要隨便用 -e,萬一有人在 .bashrc 或者 .profile 提早 export 了一個環境變量,本身沒有主動設置的話,可能就會懷疑人生了,程序行爲可能會出人意料而很難 debug。

環境變量和命令行哪一個優先

$ b=xxx make -e b=yyy
all:ddd
all:yyy
all:ddd
all:file
all:command line
all:file
複製代碼

能夠看到 命令行 優先。

小結一下:

  • 全部變量語句的執行在 target 下的語句以前(每一個 target 所屬語句有一個 TAB 的縮進)。
  • 變量 override 優先級:command line > environment override > file

最後佈置一個小做業?這個的結果是什麼呢?

$ b=xxx make -e b=yyy all b=zzz test2 b=mmm
複製代碼

如何獲取 make 傳遞的全部參數和編譯目標

先來看看這樣一個問題:

$ make test1 test2 test3 a=123 b=456
複製代碼

如何在 Makefile 中獲取 make 命令後面的全部參數呢?

在 Shell 腳本里頭這個是很經常使用的,參數列表:$1, $2, $3, $4 ... $@

一樣地,在 Makefile 中有這樣的需求,好比說想看看到底有沒有傳進來某個參數,根據參數不一樣作不同的動做。

make 後面的參數有兩種類型,一種是命令行變量,一種是編譯目標。

這兩個分別存放在 MAKEOVERRIDESMAKECMDGOALS 變量中。

判斷有沒有傳遞某個編譯目標,能夠這麼作:

ifeq ($(filter test1, $(MAKECMDGOALS)), test1)
    do something here
endif
複製代碼

上述代碼實際是也至關於能夠用來把一些變量賦值放到目標相關的代碼塊中。這個能夠大幅提高大型 Makefile 的執行效率,在執行特定的目標時,不去執行無關的代碼塊。

判斷有沒有傳遞某個參數,能夠這麼作:

ifeq ($(origin a), command line)
    do something here
endif
複製代碼

固然,也能夠從 MAKEOVERRIDES 中作 findstring 檢查,只是沒有用 origin 來得簡單。

Makefile 調試與跟蹤方法一覽

Debugging

$ make --debug xxx
複製代碼

展開整個 make 解析和執行 xxx 的過程。

Tracing

$ make --trace xxx
複製代碼

展開 xxx 目標代碼的執行過程,有點像 Shell 裏頭的 set -x。該功能在 make 4.1 及以後才支持。

Logging

$(info ...)
$(warning ...)
$(error ...)
複製代碼

error 打印日誌後當即退出,很是適合已經復現的錯誤。

Environment dumping

$ make -p xxx > xxx.data.dump
複製代碼

打開 xxx.data.dump 找到 xxx 的位置能夠查看相關變量是否符合預期。

Makefile 與 Shell 中的文件名處理差別

Makefile 中有相似 Shell 的 dirnamebasename 命令,它們是:dir, basename, notdir,可是用法有差別,千萬別弄混,下面來一個對比。

$ cat Makefile
makefile:
	@echo $(dir $a)
	@echo $(basename $a)
	@echo $(notdir $a)

shell:
	@echo $(shell dirname $a)
	@echo $(shell basename $a)

$ make makefile a=/path/to/abc.efg.tgz
/path/to/
/path/to/abc.efg
abc.efg.tgz
$ make shell a=/path/to/abc.efg.tgz
/path/to
abc.efg.tgz

$ make makefile a=/path/to/
/path/to/
/path/to/

$ make shell a=/path/to/
/path
to

$ make makefile a=/path/to
/path/
/path/to
to
複製代碼

經過對比,能夠看到,Makefile 的 dirbasename 跟 Shell 中的 dirnamebasename 有很是微妙的差別。若是理解成等價,那就很麻煩了,由於拿到的結果並不如預期。

對於文件,有以下等價關係:

參數 動做 Makefile Shell
/path/to/abc.efg.tgz 取目錄 dir dirname
同上 取文件名 notdir basename

而且須要注意,Makefile 的 dir 取到的目錄帶有 / 後綴,而 Shell 的 dirname 結果不帶 /。對於目錄,二者的認知千差萬別,Makefile 的 dirbasename 拿到的都是目錄,而 Shell 可以拆分出父目錄和字目錄的文件名。若是要對齊到 Makefile,用 dirnotdir 起到相似 Shell dirnamebasename 的效果,得先 strip 掉後面的 '/'。

下面改造一下:

$ cat Makefile
makefile:
	@echo $(patsubst %/,%,$(dir $(patsubst %/,%,$a)))
	@echo $(notdir $(patsubst %/,%,$a))

shell:
	@echo $(shell dirname $a)
	@echo $(shell basename $a)

$ make makefile a=/path/to/abc.efg.tgz
/path/to
abc.efg.tgz
$ make shell a=/path/to/abc.efg.tgz
/path/to
abc.efg.tgz

$ make shell a=/path/to/
/path
to
$ make makefile a=/path/to/
/path
to
複製代碼

能夠看到,改造完之後,結果跟 Shell 結果對齊了。

在 Makefile 表達式中使用逗號和空格變量

逗號和空格是 Makefile 表達式中的特殊符號,若是要用它們的願意,須要特別處理。

empty :=
space := $(empty) $(empty)
comma := ,
複製代碼

在 Makefile 中對軟件版本號作差別化處理

Makefile 一般須要根據軟件版本傳遞不一樣的參數,因此常常須要對軟件版本號作比較。

例如,在 Linux 4.19 以後刪除了 oldnoconfig,並替換爲了 olddefconfig,因此以前用到的 oldnoconfig 在新版本用不了,直接改掉老版本又用不了,得作差別化處理。

你們以爲應該怎麼處理呢?先思考一下再看答案吧。

下面貼出關鍵片斷:

LINUX_MAJOR_VER := $(subst v,,$(firstword $(subst .,$(space),$(LINUX))))
LINUX_MINOR_VER := $(subst v,,$(word 2,$(subst .,$(space),$(LINUX))))

ifeq ($(shell [ $(LINUX_MAJOR_VER) -lt 4 -o $(LINUX_MAJOR_VER) -eq 4 -a $(LINUX_MINOR_VER) -le 19 ]; echo $$?),0)
    KERNEL_OLDDEFCONFIG := oldnoconfig
else
    KERNEL_OLDDEFCONFIG := olddefconfig
endif
複製代碼

相似地,若是要同時兼容不一樣版本的 GCC,得根據 GCC 版本傳遞不一樣的編譯選項,也能夠像上面這樣去作識別,Linux 源碼下就有不少這樣的需求。

不過它用了 try-run 的方式實現了一個 cc-option-yn (見 linux-stable/scripts/Kbuild.include),它是試錯的方式,避免了堆積大量的判斷代碼,不過這裏用的版本判斷很少,並且調用這類 target 開銷較大,不必,直接加判斷便可。

須要注意的是,考慮到版本號命名的潛在不一致性,好比說,後面加個 -rc1,再加點別的什麼,判斷的複雜度會增長很多,因此,這類邏輯能夠替換爲其餘方式,好比說,這裏能夠直接去 linux-stable/scripts/Makefile 下用 grep 查詢 olddefconfig 是否存在:

KCONFIG_MAKEFILE := $(KERNEL_SRC)/scripts/kconfig/Makefile
KERNEL_OLDDEFCONFIG := olddefconfig
ifeq ($(KCONFIG_MAKEFILE), $(wildcard $(KCONFIG_MAKEFILE)))
  ifneq ($(shell grep olddefconfig -q $(KCONFIG_MAKEFILE); echo $$?),0)
    ifneq ($(shell grep oldnoconfig -q $(KCONFIG_MAKEFILE); echo $$?),0)
      KERNEL_OLDDEFCONFIG := oldconfig
    else
      KERNEL_OLDDEFCONFIG := oldnoconfig
    endif
  endif
endif
複製代碼

修改默認執行目標的簡單方法

若是不指定目標直接敲擊 make 的話,Makefile 中的第一個目標會被執行到。這個是比較天然的邏輯,可是有些狀況下,好比說,在代碼演化之後,若是須要調整執行目標的話,得把特定目標以及相應代碼從 Makefile 中搬到文件開頭,這個改動會比較大,這個時候,就能夠用 Makefile 提供的機制來修改默認執行目標。

來看看上面那個例子:

$ make -p | grep makefile | grep -v ^#
.DEFAULT_GOAL := makefile
makefile:
複製代碼

能夠看到,makefile 被賦值給了 .DEFAULT_GOAL 變量,經過 override 這個變量,就能夠設置任意的目標了,把默認目標改成 shell 看看。

$ make -p .DEFAULT_GOAL=shell a=/path/to/abc.efg.tgz | grep ^.DEFAULT_GOAL
.DEFAULT_GOAL = shell
複製代碼

確實能夠改寫,這個要永久生效的話,直接加到 Makefile 中便可:

override .DEFAULT_GOAL := shell
複製代碼

檢查文件是否存在的兩種方法

在 Makefile 中,一般須要檢查一些環境或者工具是否 Ready,檢查文件是否存在的話,能夠用 wildcard 展開再匹配,也能夠用 Shell 來作判斷。

ifeq ($(TEST_FILE), $(wildcard $(TEST_FILE)))
    $(info file exists)
endif

ifeq ($(shell [ -f $(TEST_FILE) ]; echo $$?), 0)
    $(info file exists)
endif
複製代碼

第二種方法比較自由,能夠擴展用來檢查文件是否可執行,也能夠調用 grep 作更多複雜的文本內容檢查。在複雜場景下,經過第二種方法調用 Shell 是比較好的選擇。

如何相似普通程序同樣把目標當變量使用

若是執行 make test-run arg1 arg2 想達到把 arg1 arg2 做爲 test-run 目標的參數這樣的效果該怎麼作呢?能夠用 eval 指令,它可以動態構建編譯目標。

經過 eval 指令把 arg1 arg2 這兩個目標變成空操做,即便有 arg1 arg2 這樣的目標也再也不執行, 而後執行 test-run 運行。

大概實現爲:

$ cat Makefile

# Must put this at the end of Makefile, to make sure override the targets before here
# If the first argument is "xxx-run"...
first_target := $(firstword $(MAKECMDGOALS))
reserve_target := $(first_target:-run=)

ifeq ($(findstring -run,$(first_target)),-run)
  # use the rest as arguments for "run"
  RUN_ARGS := $(filter-out $(reserve_target),$(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS)))
  # ...and turn them into do-nothing targets
  $(eval $(RUN_ARGS):;@:)
endif

test-run:
    @echo $(RUN_ARGS)


$ make test-run test1 test2
複製代碼

這個的實際應用場景有,好比說想在外面的目標中調用內核的編譯目標,一般得進入內核源碼,再執行 make target,可能得寫不少條這樣的目標:

kernel-target1:
	@make target1 -C /path/to/linux-src

kernel-target2:
	@make target2 -C /path/to/linux-src
複製代碼

有了上面的支持,就能夠實現成這樣:

kernel-run:
	@make $(arg1) -C /path/to/linux-src
複製代碼

使用時也不復雜,內核的各類目標均可以做爲參數傳遞進去:

$ make kernel-run target1
$ make kernel-run target2
複製代碼

雖說,上述 arg1,也能夠這樣寫:

$ make kernel-run arg1=target1
$ make kernel-run arg1=target2
複製代碼

可是在使用效率上明顯不如前者來得直接。

Makefile 實例模板

本文的內容大部分都彙整到了 Linux Lab: examples/makefile/template

送您一枚免費體驗卡

更多 Linux 精彩歡迎透過下方免費體驗卡訪問『Linux 知識星球』:

『Linux 知識星球』免費體驗卡
相關文章
相關標籤/搜索