在 [Day 12] make 專案目錄規劃實作解析的時候我們學會用makefile控制不同系統的編譯 gcc/clang
....etc。這篇文章我們會針對多目錄專案:子 Makefile 與遞迴 make
這個方向去做更多的延伸,主要的原因是因為當專案變大、功能變多的時候難免會需要用到不同資料夾或是路徑去做區分,在這樣的背景條件下,makefile的使用必定更加趨於複雜,所以針對這個部分,今天會特別設定一個主題專門給子 Makefile 與遞迴 make
會學到含有兩個makefile以上專案的makefile撰寫還有運作方式
當專案規模擴大時,設計佳的專案結構能讓開發更高效:
專案結構清晰化
把程式庫(lib)、應用程式(app)、測試(tests)分開管理。
快速理解專案
透過頂層 Makefile,我們可以馬上知道:
團隊協作
每個模組有自己的 Makefile,互相之間不干擾。
今天主要會介紹頂層的makefile連結的作用,明天會深入再介紹子資料夾中makefile的作用方式還有寫法
在 [Day 12] make 專案目錄規劃實作解析
的時候,我們設計一個包含 inc/ 還有 src/ 的專案結構:
Day12_c_proj/
├── Makefile
├── inc/
│ └── calc.h
└── src/
├── main.c
└── calc.c
今天我們要進一步做一個多目錄專案:
我們來實作一個簡單的專案:
calc/
:數學函式庫(會被編成 libcalc.a
)app/
:主程式,會連結 libcalc.a
生成 bin/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
請幫我先準備下面的程式碼
#ifndef CALC_H
#define CALC_H
int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);
#endif
#include "calc.h"
int add(int a, int b) { return a + b; }
#include "calc.h"
int sub(int a, int b) { return a - b; }
#include "calc.h"
int mul(int a, int b) { return a * b; }
#include <stdio.h>
#include "calc.h"
int main(void) {
printf("add(10,5)=%d\n", add(10,5));
printf("sub(10,5)=%d\n", sub(10,5));
printf("mul(10,5)=%d\n", mul(10,5));
return 0;
}
最頂層 Makefile 的任務就是呼叫子目錄的 Makefile。
好的~ 現在可以請你先自己嘗試理解看看這段code在做什麼,下面會公布解答~
.PHONY: all clean distclean calc app
BUILD ?= Release
SUBDIRS := calc app
all: calc app
calc:
@$(MAKE) -C calc BUILD=$(BUILD)
app: calc
@$(MAKE) -C app BUILD=$(BUILD)
clean:
@for d in $(SUBDIRS); do \
$(MAKE) -C $$d BUILD=$(BUILD) clean || exit 1; \
done
distclean:
@for d in $(SUBDIRS); do \
$(MAKE) -C $$d BUILD=$(BUILD) distclean || exit 1; \
done
@rm -rf bin
這邊用.PHONY
定義了幾個呼叫式all
clean
distclean
calc
app
~
.PHONY: all clean distclean calc app
BUILD ?= Release # BUILD 沒有被定義的話就被定義成Release
SUBDIRS := calc app # SUBDIRS 固定被assign成calc app
all 是頂層 Makefile 的預設目標(因為是第一個 rule)
依賴 calc 和 app
代表執行 make all 的時候,必須先完成 calc ,再完成 app。
它本身沒有其他作用,只是把兩個目標綁在一起的用途~
就是代表要同時建 calc 和 app,是最上層的打包指令
all: calc app # all 的意思是:同時編譯 calc 與 app
目的:呼叫兩個子目錄裡面的make
下面這段code的意思是: 進入 calc 子目錄,呼叫裡面的 Makefile
calc:
@$(MAKE) -C calc BUILD=$(BUILD) # make -C calc Release=$(Release)
$(MAKE)
是 GNU make 的內建變數(指向 make 本身)-C calc
→ 切換到 calc 目錄,執行裡面的 MakefileBUILD=$(BUILD)
→ 把變數傳下去,確保子 Makefile 收到 Release 或 Debug
前面的 @
→ 隱藏命令本身,只顯示輸出,不會把 make -C calc ...
印出來
同理,下面這段code的意思是: 進入 calc 子目錄,呼叫裡面的 Makefile
只是她是app: calc
這樣寫的,代表要建 app 之前,先確保 calc 已經完成
app: calc
@$(MAKE) -C app BUILD=$(BUILD) # make -C app Release=$(Release)
另外下面的內容可以回去參考[Day 12] make 專案目錄規劃實作解析會有詳細的解釋
clean:
@for d in $(SUBDIRS); do \
$(MAKE) -C $$d BUILD=$(BUILD) clean || exit 1; \
done
distclean:
@for d in $(SUBDIRS); do \
$(MAKE) -C $$d BUILD=$(BUILD) distclean || exit 1; \
done
@rm -rf bin
make all
│
├── target: calc
│ └── (進入 ./calc → build libcalc.a)
│
└── target: app (依賴 calc)
└── (進入 ./app → build bin/app,連結 libcalc.a)
$(MAKE) -C <dir>
會進入子目錄執行 Makefile。BUILD=$(BUILD)
把參數傳下去,確保 Debug/Release 一致。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 -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)
cd Day14_recursive_make
# 編譯
make
# => 先進 calc/ 產生 libcalc.a,再進 app/ 連結成 bin/app
# 執行
./bin/app
# 輸出:
# add(10,5)=15
# sub(10,5)=5
# mul(10,5)=50
# 清理中繼檔
make clean
# 完全清理(含執行檔與函式庫)
make distclean
calc/src/*.c
│
▼
[ 編譯成 .o 檔案 ]
│
▼
libcalc.a (由 calc/Makefile 打包)
│
└───────┐
▼
app/main.c
│
▼
[ 編譯成 .o 檔案 ]
│
▼
bin/app (最終執行檔)
make all
│
├──▶ make -C calc (進入 calc 子目錄,建 libcalc.a)
│
└──▶ make -C app (進入 app 子目錄,連結 libcalc.a → 生成 bin/app)
make -j
) 有時效果有限。