Makefile là một cơ chế kiểm soát quy trình xây dựng phần mềm được tích hợp sẵn trong hệ thống Unix-like, cho phép tự động hóa việc biên dịch, liên kết và quản lý các tệp phụ thuộc. Thay vì chạy từng lệnh gcc thủ công, bạn định nghĩa logic xây dựng một lần — sau đó make sẽ tự động xác định tệp nào cần tái tạo dựa trên thời gian sửa đổi.
Cấu trúc cốt lõi của một Makefile
Mỗi rule trong Makefile gồm ba thành phần bắt buộc: mục tiêu (target), danh sách phụ thuộc (prerequisites), và các lệnh thực thi (recipe). Các lệnh phải bắt đầu bằng ký tự tab (không phải dấu cách).
Dưới đây là một Makefile mẫu tối giản nhưng đầy đủ chức năng:
# Biến toàn cục
COMPILER := clang
WARNINGS := -Wextra -Wpedantic
OUTPUT_BIN := app
# Mục tiêu mặc định
.PHONY: all
all: $(OUTPUT_BIN)
# Quy tắc liên kết chương trình
$(OUTPUT_BIN): main.o logger.o
$(COMPILER) $(WARNINGS) -o $@ $^
# Quy tắc biên dịch từng tệp nguồn
main.o: main.c common.h
$(COMPILER) $(WARNINGS) -c $< -o $@
logger.o: logger.c logger.h
$(COMPILER) $(WARNINGS) -c $< -o $@
# Dọn dẹp môi trường build
.PHONY: clean
clean:
rm -f $(OUTPUT_BIN) *.o
# Hỗ trợ tái sử dụng: rebuild khi header thay đổi
common.h logger.h: ;
Lưu ý:
.PHONYđánh dấu các mục tiêu không phải tệp thật (nhưall,clean) để tránh xung đột tên với tệp trong thư mục.$@đại diện cho tên mục tiêu hiện tại (ví dụ:app),$<là phụ thuộc đầu tiên (main.c), còn$^là danh sách tất cả phụ thuộc (main.o logger.o).- Dòng
common.h logger.h: ;khai báo các header như những mục tiêu "trống", đảm bảo bất kỳ thay đổi nào trong chúng đều kích hoạt lại biên dịch các tệp phụ thuộc.
Tối ưu hóa cho dự án đa thư mục
Với cấu trúc dự án phân cấp, ta áp dụng các hàm dựng sẵn của GNU Make để tự động khám phá và xử lý tệp:
# Cấu hình đường dẫn
SOURCE_ROOT := .
SRC_PATHS := $(SOURCE_ROOT)/src $(SOURCE_ROOT)/core $(SOURCE_ROOT)/drivers
BUILD_DIR := .build
# Tự động tìm tất cả tệp .c trong các thư mục nguồn
ALL_SOURCES := $(foreach dir,$(SRC_PATHS),$(wildcard $(dir)/*.c))
OBJECT_FILES := $(patsubst %.c,$(BUILD_DIR)/%.o,$(ALL_SOURCES))
# Đảm bảo thư mục build tồn tại trước khi biên dịch
$(BUILD_DIR):
mkdir -p $@
# Quy tắc tổng quát cho mọi tệp .c → .o
$(BUILD_DIR)/%.o: %.c | $(BUILD_DIR)
$(COMPILER) $(WARNINGS) -I$(SOURCE_ROOT)/include -c $< -o $@
# Mục tiêu chính
app: $(OBJECT_FILES)
$(COMPILER) $(WARNINGS) -o $@ $^ -lm
.PHONY: clean
clean:
rm -rf $(BUILD_DIR) app
Giải thích các kỹ thuật được dùng:
$(foreach ...): Lặp qua danh sách thư mục để thu thập tệp nguồn.| $(BUILD_DIR): Sử dụng order-only prerequisite — chỉ đảm bảo thư mục tồn tại, không so sánh thời gian sửa đổi.-I$(SOURCE_ROOT)/include: Truyền đường dẫn include để trình biên dịch tìm thấy header.-lm: Liên kết thư viện toán học — minh họa cách thêm thư viện ngoài vào quy trình liên kết.
Các ký tự đặc biệt và biến nội suy phổ biến
| Biến | Ý nghĩa | Ví dụ sử dụng |
|---|---|---|
$@ |
Tên mục tiêu hiện hành | gcc -o $@ main.o → gcc -o app main.o |
$< |
Phụ thuộc đầu tiên | gcc -c $< -o $@ → gcc -c main.c -o main.o |
$^ |
Tất cả phụ thuộc, không trùng lặp | gcc -o $@ $^ → gcc -o app main.o utils.o |
$? |
Các phụ thuộc mới hơn mục tiêu | Chỉ tái biên dịch nếu có tệp nguồn vừa được chỉnh sửa |
$(@D) |
Thư mục chứa mục tiêu | Nếu $@ = bin/app, thì $(@D) = bin |
$(@F) |
Tên tệp (không bao gồm đường dẫn) | Nếu $@ = bin/app, thì $(@F) = app |
Khi kết hợp với các hàm như $(shell ...), $(filter ...), hay $(sort ...), bạn có thể xây dựng các quy trình build linh hoạt, hỗ trợ điều kiện, phân nhánh nền tảng hoặc cấu hình theo môi trường — tất cả đều nằm trong một tệp văn bản thuần.