[Day 14] 看過有一個makefile的專案了? 那我們來看看兩個以上makefile專案怎麼寫
上一篇講了如何建構含有多個makefile的專案,今天我們會繼續學會怎麼解讀子資料夾calc
以及app
裡面的訊息。
Day14_recursive_make/
├── Makefile # 頂層:遞迴呼叫子專案
├── calc/
│ ├── Makefile # 產生 libcalc.a
│ ├── inc/
│ │ └── calc.h
│ └── src/
│ ├── add.c
│ ├── sub.c
│ └── mul.c
└── app/
├── Makefile # 連結 libcalc.a → app
└── main.c
make all
│
├── target: calc
│ └── (進入 ./calc → build libcalc.a)
│
└── target: app (依賴 calc)
└── (進入 ./app → build bin/app,連結 libcalc.a)
.o
→ 物件檔 (object file) : 編譯器 gcc -c 的輸出結果
把 xxx.c 轉成機器碼,但還沒連結成最終程式,一個 .o 只對應一個 .c
如果只改了一個檔案,只需要重編那個 .c → .o,不用重建整個專案。
之後所有 .o 再一起交給連結器 (ld 或 gcc) → 產生執行檔或靜態/動態庫gcc -c src/add.c -o obj/add.o
輸出就是 obj/add.o
.d
→ 相依檔 (dependency file): 編譯器用 -MMD -MP 自動產生的副產品
主要是用來記錄這個 .o 需要哪些 .h 檔
目的是讓 Makefile 知道:如果某個 header 有改動,要重編依賴它的 .o,意思是:只要 src/add.c 或 inc/calc.h 有修改,obj/add.o 就要重編。obj/add.o: src/add.c inc/calc.h
所以makefile通常會有這行:-include $(DEPS)
: 主要是把所有.d
列出來,讓 make 自動追蹤誰依賴誰
CC := gcc
CFLAGS := -Wall -Wextra -O2 -g -Iinc -MMD -MP
AR := ar
ARFLAGS := rcs
SRCS := $(wildcard src/*.c)
OBJS := $(patsubst src/%.c,obj/%.o,$(SRCS))
DEPS := $(patsubst src/%.c,dep/%.d,$(SRCS))
LIBDIR := ../bin
LIB := $(LIBDIR)/libcalc.a
.PHONY: all clean distclean
all: $(LIB)
$(LIB): $(OBJS) | $(LIBDIR)
$(AR) $(ARFLAGS) $@ $(OBJS)
obj/%.o: src/%.c | obj dep
$(CC) $(CFLAGS) -c $< -o $@
@mv $(@:.o=.d) dep/
$(LIBDIR) obj dep:
@mkdir -p $@
-inc $(DEPS)
clean:
@rm -rf obj dep
distclean: clean
@rm -f $(LIB)
CC := gcc
CFLAGS := -Wall -Wextra -O2 -g -Iinc -MMD -MP
AR := ar
ARFLAGS := rcs
SRCS := $(wildcard src/*.c)
OBJS := $(patsubst src/%.c,obj/%.o,$(SRCS))
DEPS := $(patsubst src/%.c,dep/%.d,$(SRCS))
LIBDIR := ../bin
LIB := $(LIBDIR)/libcalc.a
CC:C 編譯器。
CFLAGS:
-Wall -Wextra 開所有常用警告
-O2 最佳化。
-g 附上除錯資訊
-I 會把 加到 include path 清單的最前面
當程式碼有 #include "calc.h" 或 #include <calc.h> 時,編譯器就會依照以下順序搜尋:
gcc -Iinc -c src/add.c
代表編譯器在處理 add.c 裡的 #include "calc.h" 時,會先到 inc/ 目錄找AR/ARFLAGS:建靜態函式庫用的工具與旗標:
r 插入 / 更新檔案、c 建立(抑制警告)、s 產生索引(ar 對 archive 內符號建立索引,連結較快)
SRCS:掃所有 src/xxx.c
OBJS:把 src/xxx.c 對應成 obj/xxx.o
DEPS:把 src/xxx.c 對應成 dep/xxx.d(等等會把編譯器輸出的 .d 搬移到這裡)
LIBDIR/LIB:輸出靜態庫的位置:../bin/libcalc.a
.PHONY: all clean distclean
all: $(LIB)
預設 make 就是 make all,而 all 依賴 $(LIB),也就是「把 libcalc.a 做出來」
下面有clean disclean
3. 產出 lib(彙整所有 .o)
$(LIB): $(OBJS) | $(LIBDIR)
$(AR) $(ARFLAGS) $@ $(OBJS)
一般相依:$(LIB) 依賴所有 $(OBJS)。任何一個 .o 更新都會重新打包。
order-only 相依:| $(LIBDIR) 表示要先確保 ../bin 存在,但資料夾時間戳變化不會觸發重建
這個 [Day 14] 有提到避免資料夾本身變動造成不必要的重編
4. 編譯規則(pattern rule)+ 產生 .d 檔
obj/%.o: src/%.c | obj dep
$(CC) $(CFLAGS) -c $< -o $@
@mv $(@:.o=.d) dep/
order-only 相依 : | obj dep
會先建立 obj/ 與 dep/ 兩個資料夾。
$@
= 目標(例如 obj/xxx.o)
$(@:.o=.d)
= 把副檔名 .o 換成 .d → obj/xxx.d
@mv
到 dep/
→ dep/xxx.d
這樣 .d 全部集中在 dep/,乾淨好管理
CC := gcc
CFLAGS := -Wall -Wextra -O2 -g -I../calc/inc -MMD -MP
SRCS := main.c
OBJS := $(patsubst %.c,obj/%.o,$(SRCS))
DEPS := $(patsubst %.c,dep/%.d,$(SRCS))
BINDIR := ../bin
TARGET := $(BINDIR)/app
LDFLAGS := -L$(BINDIR) -lcalc
.PHONY: all clean distclean
all: $(TARGET)
$(TARGET): $(OBJS) | $(BINDIR)
$(CC) $(CFLAGS) $(OBJS) -o $@ $(LDFLAGS)
obj/%.o: %.c | obj dep
$(CC) $(CFLAGS) -c $< -o $@
@mv $(@:.o=.d) dep/
$(BINDIR) obj dep:
@mkdir -p $@
-inc $(DEPS)
clean:
@rm -rf obj dep
distclean: clean
@rm -f $(TARGET)
前面重複的就不講了
$(TARGET): $(OBJS) | $(BINDIR)
$(CC) $(CFLAGS) $(OBJS) -o $@ $(LDFLAGS)
一般相依:$(TARGET) 依賴所有 $(OBJS)。
order-only 相依:| $(BINDIR)
表示只保證 ../bin 存在,不因目錄時間戳改變而強制重建
libcalc.a
的相依
這裡雖然用 -lcalc 連結,但 Make 看不到 libcalc.a 是一個「檔案相依」。
因為這邊的步驟是頂層 Makefile 會先遞迴進 calc/ 把 libcalc.a 建好,再進 app/ 連結,所以沒問題
但如果是單獨在 app/ 目錄下執行 make 也要自動建好 libcalc.a,可以這裡補上顯式相依:
$(TARGET): $(OBJS) ../bin/libcalc.a | $(BINDIR)
這樣可以統一由頂層make管理會比較方便。
2. 編譯規則(pattern rule)+ 產生與整理 .d
obj/%.o: %.c | obj dep
$(CC) $(CFLAGS) -c $< -o $@
@mv $(@:.o=.d) dep/
%.c
編成 obj/%.o
| obj dep
先確保兩個資料夾存在