[Day 11] MAKE 指令和魔法變數 - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天
在Day11 的時候我們解釋了什麼叫做 make clean
還有 .PHONY
今天會用簡單的專案實作看看
makefile
偵測所在環境的編譯器 + 選用編譯器makefile
加入其他自訂的指令makefile
的解析 → 這篇文章最關鍵的部分 可以直接跳到最後章節 解析makefile
去看今天我們就用這個檔案結構來解釋
Day10_clean_phony_pro/
├── Makefile
├── inc/
│ └── calc.h
└── src/
├── main.c
└── calc.c
inc
→ include
→ 通常只放include 檔案(.h檔
)的地方src
→ source
→ 通常只放include 檔案(.c檔
)的地方
首先請按照上面的階層準備檔案
calc.h
#ifndef CALC_H
#define CALC_H
int add(int a, int b);
int sub(int a, int b);
#endif
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; }
# ========== 編譯器偵測 ==========
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)
以上的檔案應該都可以順利跑起來
上面的code 來看可以主要分成幾個部分,下面會以上面的檔案為例子逐步解析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
有時候編譯方式可以用makefile 的方式指定編譯的平台
上面makefile
主要就是指定了 clang
跟 gcc
兩個不同的編譯器,針對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
# ========== 基本編譯設定 ==========
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.c
、src/calc.c
(整個字串符合)%
部分分別是 src/main
和 src/calc
$(OBJDIR)/%.o
→ obj/src/main.o
、obj/src/calc.o
%
在make裡面代表萬用字元 有點類似 python裡面的 *
一樣
簡單來說 OBJS := $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
總結這個例子
%
是 pattern rule 的通配符,代表任意字串。patsubst
裡用 %.c → %.o
,就是把所有 .c
來源換成 .o
目標。
# ========== 刪除策略 ==========
RM := rm -rf
CLEAN_DIRS := $(OBJDIR) $(DEPDIR)
DIST_DIRS := $(BINDIR)
rm -rf
在 linux裡面的意思就是
rm
→ remove,刪除檔案/資料夾-r
→ recursive,遞迴刪除,意思是目錄裡的檔案、子目錄都會被刪掉-f
→ force,強制刪除,不會跳出確認,也不會因為「檔案不存在」就報錯rm -rf something
的效果就是:不問、不報錯,整個目錄樹直接砍掉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 # 最後連結出來的執行檔放這裡
之後的章節會在解釋需要分資料夾的原因
主要是用來宣告假目標的用法,可以用它來定義你想要執行的項目名稱跟實際的內容
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)
(編譯器,通常是 gcc
或 clang
)有沒有在 PATH 裡。
>/dev/null 2>&1
:把標準輸出/錯誤都丟掉。
|| { ... }
:如果找不到,就輸出錯誤訊息並 exit 127
(127 是常見的「command not found」錯誤碼)。
前面的 @
:隱藏命令本身的輸出,只顯示回應。