2013年8月21日 星期三

略懂 makefile

the 1st edition: 20130821
the 2nd edition: 20170419

雖然我會自己寫 makefile, 但那只是最簡單的部份, 我通常只記得住 $@, $<, $^ 這三個, 也夠我用好上幾年。不過也該來記住一點進階的東西。


Default Compilation Rules 
有兩種:

Suffix Rule
.SUFFIXES:
.c.o:
        gcc -c -o $@ -O $*.c

Suffix Rules 目前已經被廢棄, 不要再用這種語法, 又稱為 Old-Fashioned Suffix Rules。

Pattern Rule
%.o : %.c
        gcc $(CFLAGS) -c $< 

新的 Suffix Rules 又分為 double-suffix, single-suffix, 或是 Pattern Rule, 上面展示的 double-suffix 的形式。

ref:
10.7 Old-Fashioned Suffix Rules
https://www.gnu.org/software/make/manual/html_node/Suffix-Rules.html

我老是被這樣的語法所疑惑, 原來這是兩種不同的東西。這個寫法若是 .h 改變了, source code 不會被重新編譯, 不是太好用的語法。

macro substitution

makefile
 1 files = main.aa stack.aa
 2 sources = $(files:.aa=.c)
 3 
 4 %.aa:%.zz
 5  ls $@
 6 main.zz:
 7  ls /
 8 clean:
 9  echo $(files)
10  echo $(sources)

makefile L2 把 main.aa 換成 main.c; stack.aa 換成 stack.c 很怪異的語法, 令我困惑。所以 sources 就是 main.c stack.c

執行結果
descent@descent-u:tmp$ make clean
echo main.aa stack.aa
main.aa stack.aa

echo main.c stack.c
main.c stack.c


測試 pattern rule, L4, L5 是我自己定義的 pattern rule。

執行結果
descent@descent-u:tmp$ make main.aa
ls /
bin     etc      hd_mnt      lib32     mnt   sbin      tmp      vmlinuz.old
boot    floppy   home        lib64     opt   selinux   ubiquity-apt-clone  work
cdrom   hd1_mnt  initrd.img  lost+found  proc  srv  usr
C:\nppdf32Log\debuglog.txt   hd2_mnt  initrd.img.old  material  root  sys  var
dev     hd3_mnt  lib         media     run   tftpboot  vmlinuz
ls main.aa
ls: cannot access main.aa: No such file or directory
make: *** [main.aa] Error 2

makefile 很複雜, 不過這樣已經足夠來使用, 沒有 IDE 的環境已經足夠累人, 撰寫 makefile 就不要再累死自己。其實 $@, $<, $^ 這三個也很夠用。

make -n 則可以列出 makefile 要執行的內容。


在 makefile 中使用 ifeq 和 or
$(filter ,)
名稱:過濾函數——filter。
功能:以模式過濾字符串中的單詞,保留符合模式的單詞。可以有多個模式。
返回:返回符合模式的字串。

ex:
if SELECT_A==y || SELECT_B==y
ifeq (y, $(filter $(SELECT_A) $(SELECT_B), y))

gcc -MM, 產生 makefile rule, ex:

gcc -Iinclude/ -MM process.c
process.o: process.c include/process.h include/type.h include/protect.h \
 include/k_string.h include/keyboard.h include/tty.h include/console.h \
 include/console.h include/tty.h include/romfs.h include/syscall.h \
 include/process.h include/vga.h include/k_assert.h include/k_stdio.h \
 include/k_stdio.h include/k_string.h include/k_stdlib.h \
 include/systask.h include/vfs.h include/storage.h include/storage.h



看了《Makefile header 檔的相依性檢查》這篇之後, 我決定好好把 gcc -MM 這樣的語法看懂。

list 1 是 simple compiler 的 makefile, 我修改成類似的作法, 沒有看不懂的 sed 指令。我不確定這是正規或是正確的寫法, 看來還可以用, 這是我自己亂試的版本。

要加入一個新的 c++ source code 檔案, 在 list 1. L21 加入 .o 檔名即可。ex: new_file.cpp, 把 new_file.o 加入到 OBJS 列表即可。

折磨人的是 L40, L42, L48, 一個一個來, 先看 L42, 所有副檔名 .d 的從 .cpp 製造出來, 先把 L48 include 刪掉, 鍵入以下指令:

descent@debian64:simple_compiler$ make astnode.d
g++ -g -std=c++14 -Wall -m32 -MM -MF astnode.d astnode.cpp
descent@debian64:simple_compiler$ cat astnode.d 
astnode.o: astnode.cpp astnode.h symbol_table.h mytype.h env.h token.h 

得到了 astnode.d, 裡頭已經幫我們寫好 astnode.o 的相依性, 那要怎麼補上 g++ 這個編譯動作呢? 加入 L45, 每個 .o 檔, 由 .cpp 製造出來, 用
$(CXX) $(CXXFLAGS) -c $<
這樣的指令製造出來。

最後結果大概是這樣:
astnode.o: astnode.cpp astnode.h symbol_table.h mytype.h env.h token.h 
        g++ -c astnode.cpp

另外一個問題是怎麼讓這些 .d 檔案變成 makefile 的內容呢? 這樣我就不用複製其內容, 貼到 makefile 裡頭。

用 L48 include, 所以我要
include c_parser.d
include astnode.d
include env.d
include lexer.d
include op.d
include symbol_table.d
include token.d
好啦! 我有 c_parser.o astnode.o env.o lexer.o op.o symbol_table.o token.o 這麼多 .o 檔, 難道要一個一個寫嗎? 看起來一點都不高明是吧! L48 就是在做這件事情, 所以 L48 那行你不喜歡, 也可以像上面那樣, 自己寫上所有要 include 的 .d 檔案。

include qq, qq 是一個檔案, 當這個檔案不存在時, 會怎麼樣,

makefile:42: qq: No such file or directory

會這樣, 很合理的, 但 gnu make 有個特異功能, 如果加上 qq 這個 target, makefile 會去執行這個 rule。

qq:
        echo "i am qq"

執行結果
makefile:42: qq: No such file or directory
echo "i am qq"
i am qq

而 qq 存在時, 就不會執行這個 rule。

所以當 astnode.d 不存在時, 便會執行
g++ -g -std=c++14 -Wall -m32 -MM -MF astnode.d astnode.cpp 
產生 astnode.d。

再來是《Makefile header 檔的相依性檢查》提到的 .h 改名或是不存在了, 會有

descent@debian64:simple_compiler$ make astnode.d
g++ -g -std=c++14 -Wall -m32 -MM -MF astnode.d astnode.cpp
astnode.cpp:2:16: fatal error: op.h: No such file or directory
compilation terminated.
makefile:47: recipe for target 'astnode.d' failed
make: *** [astnode.d] Error 1

相信比以下的訊息容易看懂。
make: *** No rule to make target 'name.h', needed by 'name.d'. Stop.

剩下那個 L40 是幹麻的, 很簡單, 我測試產生 .d 用的, 沒幹麻。

list 1. simple compiler makefile
 1 # test parser
 2 # make clean; make DPARSER=1 c_parser
 3 # make clean; make DPARSER=1 
 4 # make clean; make DPARSER=1 parser_4op
 5 
 6 # test lexer
 7 # make clean; make DLEXER=1 lexer
 8 
 9 CXXFLAGS=-g -std=c++14 -Wall -m32
10 ifdef DLEXER
11 CXXFLAGS+= -DDEBUG_LEXER
12 endif
13 
14 ifdef DPARSER
15 CXXFLAGS+= -DDEBUG_PARSER
16 endif
17 
18 
19 CXX=g++
20 
21 OBJS = c_parser.o astnode.o  env.o  lexer.o  op.o  symbol_table.o  token.o
22 
23 all: 
24       make DPARSER=1 c_parser
25 
26 parser: parser.o astnode.o token.o lexer.o op.o
27       $(CXX) $(CXXFLAGS) -o $@ $^
28 
29 parser.o: parser.cpp astnode.h mytype.h token.h parser.h op.h lexer.h
30       $(CXX) $(CXXFLAGS) -c $<
31 
32 parser_4op: parser_4op.o astnode.o token.o lexer.o op.o
33       $(CXX) $(CXXFLAGS) -o $@ $^
34 parser_4op.o: parser_4op.cpp astnode.h mytype.h token.h parser.h op.h
35       $(CXX) $(CXXFLAGS) -c $<
36 
37 c_parser: $(OBJS)
38       $(CXX) $(CXXFLAGS) -o $@ $^
39 
40 dep: $(OBJS:.o=.d)
41 
42 %.d: %.cpp
43       $(CXX) $(CXXFLAGS) -MM -MF $@ $<
44 
45 %.o: %.cpp
46       $(CXX) $(CXXFLAGS) -c $<
47 
48 include $(OBJS:.o=.d)
49       
50 
51 doc_html:
52       make -C doc
53 clean:
54       rm parser lexer *.o *.d ; make -C doc clean

列出 makefile target
https://gist.github.com/pvdb/777954
make -rpn | sed -n -e '/^$/ { n ; /^[^ ]*:/p }'

來試試看 list 1 的 makefile 會有什麼結果:

make -rpn | sed -n -e '/^$/ { n ; /^[^ ]*:/p }'
 1 %.d: %.cpp
 2 %.o: %.cpp
 3 all:
 4 token.d: token.cpp
 5 parser_4op.o: parser_4op.cpp astnode.h mytype.h token.h parser.h op.h
 6 parser_4op: parser_4op.o astnode.o token.o lexer.o op.o
 7 symbol_table.d: symbol_table.cpp
 8 op.o: op.cpp op.h
 9 astnode.d: astnode.cpp
10 dep: c_parser.d astnode.d env.d lexer.d op.d symbol_table.d token.d
11 doc_html:
12 c_parser.d: c_parser.cpp
13 token.o: token.cpp token.h mytype.h
14 symbol_table.o: symbol_table.cpp symbol_table.h mytype.h env.h astnode.h token.h
15 astnode.o: astnode.cpp astnode.h symbol_table.h mytype.h env.h token.h op.h
16 parser.o: parser.cpp astnode.h mytype.h token.h parser.h op.h lexer.h
17 lexer.d: lexer.cpp
18 parser: parser.o astnode.o token.o lexer.o op.o
19 c_parser.o: c_parser.cpp lexer.h token.h mytype.h parser.h astnode.h symbol_table.h env.h op.h
20 clean:
21 c_parser: c_parser.o astnode.o env.o lexer.o op.o symbol_table.o token.o
22 env.d: env.cpp
23 lexer.o: lexer.cpp mytype.h lexer.h token.h
24 op.d: op.cpp
25 env.o: env.cpp env.h mytype.h

ref:

沒有留言:

張貼留言

使用 google 的 reCAPTCHA 驗證碼, 總算可以輕鬆留言了。

我實在受不了 spam 了, 又不想讓大家的眼睛花掉, 只好放棄匿名留言。這是沒辦法中的辦法了。留言的朋友需要有 google 帳號。