iT邦幫忙

2025 iThome 鐵人賽

DAY 14
0
Software Development

30 天精通 C 語言建置與除錯:從 Makefile 到 CMake 跨平台實戰系列 第 14

[Day 14] 看過有一個makefile的專案了? 那我們來看看兩個以上makefile專案怎麼寫

  • 分享至 

  • xImage
  •  

前情提要

[Day 12] make 專案目錄規劃實作解析的時候我們學會用makefile控制不同系統的編譯 gcc/clang....etc。這篇文章我們會針對多目錄專案:子 Makefile 與遞迴 make 這個方向去做更多的延伸,主要的原因是因為當專案變大、功能變多的時候難免會需要用到不同資料夾或是路徑去做區分,在這樣的背景條件下,makefile的使用必定更加趨於複雜,所以針對這個部分,今天會特別設定一個主題專門給子 Makefile 與遞迴 make

這篇文章的重點:

會學到含有兩個makefile以上專案的makefile撰寫還有運作方式

為什麼要用子 Makefile 與遞迴 make?

當專案規模擴大時,設計佳的專案結構能讓開發更高效:

  • 專案結構清晰化
    把程式庫(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

程式碼

請幫我先準備下面的程式碼

calc/inc/calc.h

#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

calc/src/add.c


#include "calc.h"
int add(int a, int b) { return a + b; }

calc/src/sub.c

#include "calc.h"
int sub(int a, int b) { return a - b; }

calc/src/mul.c


#include "calc.h"
int mul(int a, int b) { return a * b; }

app/main.c


#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 的任務就是呼叫子目錄的 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 目錄,執行裡面的 Makefile
BUILD=$(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 一致。

calc/Makefile (靜態函式庫)

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)


app/Makefile (應用程式)

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 (最終執行檔)

頂層 Makefile 的遞迴控制流程

make all
   │
   ├──▶ make -C calc   (進入 calc 子目錄,建 libcalc.a)
   │
   └──▶ make -C app    (進入 app 子目錄,連結 libcalc.a → 生成 bin/app)

結論

  • 子 Makefile:每個模組有自己的建置規則。
  • 遞迴 make:頂層負責調度,子目錄負責細節。
  • 好處:專案結構清楚、維護容易、協作方便。
  • 缺點:跨模組相依的偵測不如單一 Makefile 精準,平行化 (make -j) 有時效果有限。

上一篇
[Day 13] 解決 VSCode + WSL 權限問題 Makefile 符號 Cheat Sheet
系列文
30 天精通 C 語言建置與除錯:從 Makefile 到 CMake 跨平台實戰14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言