iT邦幫忙

2025 iThome 鐵人賽

DAY 12
0

[Day 11] MAKE 指令和魔法變數 - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天
在Day11 的時候我們解釋了什麼叫做 make clean 還有 .PHONY 今天會用簡單的專案實作看看

在這篇文章中,我們會了解...

  • 透過Text Functions (GNU make)查找make相關指令跟功能
  • 使用makefile 偵測所在環境的編譯器 + 選用編譯器
  • makefile加入其他自訂的指令
  • makefile的解析 → 這篇文章最關鍵的部分 可以直接跳到最後章節 解析makefile 去看

1. 本日專案結構

今天我們就用這個檔案結構來解釋

Day10_clean_phony_pro/
├── Makefile
├── inc/
│   └── calc.h
└── src/
    ├── main.c
    └── calc.c

incinclude → 通常只放include 檔案(.h檔)的地方
srcsource → 通常只放include 檔案(.c檔)的地方

首先請按照上面的階層準備檔案

inc

  • calc.h
#ifndef CALC_H
#define CALC_H
int add(int a, int b);
int sub(int a, int b);
  

#endif

src

  • main.c
#include <stdio.h>
#include "calc.h"
int main(void) {
    int x = 42, y = 5;
    printf("[app] add=%d, sub=%d\n", add(x, y), sub(x, y));
    return 0;
}
  • calc.c
#include "calc.h"
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

makefile

# ========== 編譯器偵測 ==========
CC ?= $(shell if command -v gcc >/dev/null 2>&1; then echo gcc; \
            elif command -v clang >/dev/null 2>&1; then echo clang; fi)
ifeq ($(CC),)
$(error No C compiler found. Install gcc or clang.)
endif
# ========== 基本編譯設定 ==========

CSTD    := c11
WARN    := -Wall -Wextra -Wpedantic
OPT     := -O2
DBG     := -g
INCDIR  := inc
CFLAGS  := -std=$(CSTD) $(WARN) $(OPT) $(DBG) -I$(INCDIR)

# ========== 目錄與檔案 ==========
SRCDIR  := src
BINDIR  := bin
OBJDIR  := obj
DEPDIR  := dep
TARGET  := $(BINDIR)/app

# 只抓 src/ 內的 .c(包含 main.c)
SRCS    := $(wildcard $(SRCDIR)/*.c)
OBJS    := $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
# (Day13 會用到的自動相依;先預留)
DEPS    := $(patsubst %.c,$(DEPDIR)/%.d,$(SRCS))

  

# ========== 刪除策略 ==========

RM          := rm -rf
CLEAN_DIRS  := $(OBJDIR) $(DEPDIR)
DIST_DIRS   := $(BINDIR)

# ========== PHONY ==========

.PHONY: all clean distclean rebuild run help tree preflight

all: preflight $(TARGET)
preflight:
    @command -v "$(CC)" >/dev/null 2>&1 || { echo "Error: '$(CC)' not found in PATH."; exit 127; }

$(TARGET): $(OBJS) | $(BINDIR)

    $(CC) $(CFLAGS) $(OBJS) -o $@

# 編譯(建立必要子目錄;order-only 依賴)

$(OBJDIR)/%.o: %.c | $(OBJDIR) $(DEPDIR)

    @mkdir -p $(dir $@)

    $(CC) $(CFLAGS) -c $< -o $@

$(BINDIR) $(OBJDIR) $(DEPDIR):

    @mkdir -p $@

run: all

    @./$(TARGET)

rebuild: distclean all

tree:
    @echo "CC   : $(CC)"
    @echo "SRCS : $(SRCS)"
    @echo "OBJS : $(OBJS)"

help:
    @echo "make / make run / make clean / make distclean / make rebuild / make tree / make help"
clean:
    @echo "[CLEAN] $(CLEAN_DIRS)"
    @$(RM) $(CLEAN_DIRS)
distclean: clean
    @echo "[CLEAN] $(DIST_DIRS)"
    @$(RM) $(DIST_DIRS)

2. 建立makefile

以上的檔案應該都可以順利跑起來
上面的code 來看可以主要分成幾個部分,下面會以上面的檔案為例子逐步解析makefile的用法。

3. 解析makefile

1. 編譯平台的選擇

# ========== 編譯器偵測 ==========
CC ?= $(shell if command -v gcc >/dev/null 2>&1; then echo gcc; \
            elif command -v clang >/dev/null 2>&1; then echo clang; fi)
ifeq ($(CC),)
$(error No C compiler found. Install gcc or clang.)
endif

有時候編譯方式可以用makefile 的方式指定編譯的平台
上面makefile 主要就是指定了 clanggcc 兩個不同的編譯器,針對gcc編譯器含有其他名詞的說明可以看下面的文章。
[Day 05] 什麼是 gnu、gcc、gdb、Make、CMake? - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天
有些資工系的課程會教怎麼設計實作一個編譯器,不過這邊沒有那麼複雜,只是使用不同的編譯器來編譯。
首先會先下這個指令:
CC 主要是一個變數名稱,不過在makefile中通常指的是c的編譯器,有點像是大家隱藏的默契或是約定成俗,很多人都會這麼寫。
?=這個符號主要表示 conditional assignment 如果 CC 這個變數還沒有被定義的話才會被定義,如果之前已經被定義的話就不會覆蓋已經定義的變數值
$makefile 中 是 展開變數或函式 的特殊符號 ,比較常見的用法應該是:

CC = gcc
CFLAGS = -Wall -O2

all:
	$(CC) $(CFLAGS) main.c -o app

  • $(CC) → 會展開成 gcc
  • $(CFLAGS) → 會展開成 -Wall -O2
    所以
    $(CC) $(CFLAGS) main.c -o app 就等同於
    gcc -Wall -O2 main.c -o app

2.基本編譯設定

# ========== 基本編譯設定 ==========
SRCDIR  := src
BINDIR  := bin
OBJDIR  := obj
DEPDIR  := dep
TARGET  := $(BINDIR)/app

:= 表示設定了之後就被固定了,不能再被二次定義 = 則可以被二次定義
所以上面的變數被定義之後就不會再被改變

# 只抓 src/ 內的 .c(包含 main.c)
SRCS    := $(wildcard $(SRCDIR)/*.c)
OBJS    := $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))

$ 還有 := 剛剛有被提到了,有新加入的patsubst 還有 %o
patsubst 是 make的內建函式
Text Functions (GNU make)

$(patsubst <pattern>,<replacement>,<text>)

意思是:
<text> 裡的每個字串,符合 <pattern> 的就替換成 <replacement>

例如在這個專案的例子中

SRCS = src/main.c src/calc.c

OBJS := $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))

展開步驟:

  • %.c → 匹配 src/main.csrc/calc.c(整個字串符合)
  • % 部分分別是 src/mainsrc/calc
  • 代入 replacement:$(OBJDIR)/%.oobj/src/main.oobj/src/calc.o

% 在make裡面代表萬用字元 有點類似 python裡面的 * 一樣
簡單來說 OBJS    := $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))

總結這個例子

  • %pattern rule 的通配符,代表任意字串。
  • patsubst 裡用 %.c → %.o,就是把所有 .c 來源換成 .o 目標。

3. 重新編譯之前刪除之前編譯的檔案


# ========== 刪除策略 ==========

RM          := rm -rf
CLEAN_DIRS  := $(OBJDIR) $(DEPDIR)
DIST_DIRS   := $(BINDIR)

rm -rf 在 linux裡面的意思就是

  • rm → remove,刪除檔案/資料夾
  • -r → recursive,遞迴刪除,意思是目錄裡的檔案、子目錄都會被刪掉
  • -f → force,強制刪除,不會跳出確認,也不會因為「檔案不存在」就報錯
    所以 rm -rf something 的效果就是:不問、不報錯,整個目錄樹直接砍掉
    在makefile 中他這樣寫
clean:
	$(RM) $(CLEAN_DIRS)

distclean:
	$(RM) $(CLEAN_DIRS) $(DIST_DIRS)

其實是代表

rm -rf obj dep      # clean
rm -rf obj dep bin  # distclean

前面已經有定義

OBJDIR  := obj     # 編譯出來的 .o 物件檔放這裡
DEPDIR  := dep     # 依賴檔(.d 檔)放這裡
BINDIR  := bin     # 最後連結出來的執行檔放這裡

之後的章節會在解釋需要分資料夾的原因

4. .PHONY

主要是用來宣告假目標的用法,可以用它來定義你想要執行的項目名稱跟實際的內容
Phony Targets (GNU make)


# ========== PHONY ==========

.PHONY: all clean distclean rebuild run help tree preflight

all: preflight $(TARGET)
preflight:
    @command -v "$(CC)" >/dev/null 2>&1 || { echo "Error: '$(CC)' not found in PATH."; exit 127; }

$(TARGET): $(OBJS) | $(BINDIR)

    $(CC) $(CFLAGS) $(OBJS) -o $@

像是這邊定義了preflight是一個檢查動作。

  • command -v "$(CC)":檢查 $(CC)(編譯器,通常是 gccclang)有沒有在 PATH 裡。

  • >/dev/null 2>&1:把標準輸出/錯誤都丟掉。

  • || { ... }:如果找不到,就輸出錯誤訊息並 exit 127(127 是常見的「command not found」錯誤碼)。

  • 前面的 @:隱藏命令本身的輸出,只顯示回應。


上一篇
[Day 11] MAKE 指令和魔法變數
系列文
30 天精通 C 語言建置與除錯:從 Makefile 到 CMake 跨平台實戰12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言