Bài 3 · Nâng cao · 22 phút· Cập nhật 11/06/2026
Make & đồ thị build
Biên soạn bởi Nguyễn Anh Tuấn
Học Make qua đồ thị phụ thuộc: viết Makefile với target, prerequisites, recipe; build tăng dần (incremental build) chỉ dịch lại thứ cần thiết.
Dự án thật có hàng trăm file. Chạy lại trọn hành trình biên dịch cho toàn bộ mỗi lần sửa một dòng là phí thời gian. Make mô hình hoá build thành một đồ thị phụ thuộc và chỉ dịch lại phần thực sự cần - dựa trên thời gian sửa file.
- ▸Build = đồ thị: target ← prerequisites (vd app ← main.o, util.o).
- ▸Quy tắc: target build lại khi một prerequisite MỚI HƠN nó (hoặc target chưa có).
- ▸Kết quả: build tăng dần - chỉ làm phần đổi và những gì phụ thuộc nó.
Một Makefile gồm các luật. Mỗi luật: target - danh sách prerequisites, rồi dòng recipe (lệnh tạo target) bắt đầu bằng một ký tự TAB:
Makefile
CC = gcc
CFLAGS = -Wall -Wextra -std=c17
app: main.o util.o
$(CC) $(CFLAGS) main.o util.o -o app
main.o: main.c util.h
$(CC) $(CFLAGS) -c main.c -o main.o
util.o: util.c util.h
$(CC) $(CFLAGS) -c util.c -o util.o
.PHONY: clean
clean:
rm -f *.o app Bẫy số một: TAB, không phải dấu cách
Đây chính là chỗ Make phát huy tác dụng. Bấm “sửa” một file, công cụ sẽ tô vàng đúng những mục cần build lại; bấm make để chỉ dịch lại đúng những mục đó:
Sửa util.c → chỉ util.o và app build lại. Sửa header util.h → cả hai .o build lại (vì cả hai phụ thuộc nó). Đó là build tăng dần: chỉ làm phần thực sự cần.
- ▸Sửa util.c → chỉ util.o và app build lại (main.o không liên quan).
- ▸Sửa util.h → cả main.o và util.o build lại (cả hai phụ thuộc nó), rồi app.
- ▸Không sửa gì → make in “'app' is up to date”, dịch lại 0 file.
Makefile thật tránh lặp bằng biến và pattern rule (một luật cho mọi .c → .o). Khi dự án lớn hơn nữa, người ta chuyển sang CMake & Meson để khỏi viết Makefile tay:
Makefile-gon
CC = gcc
CFLAGS = -Wall -Wextra -std=c17
OBJ = main.o util.o
app: $(OBJ)
$(CC) $(CFLAGS) $(OBJ) -o $@ # $@ = ten target (app)
%.o: %.c # luat chung: moi .c -> .o
$(CC) $(CFLAGS) -c $< -o $@ # $< = prerequisite dau (file .c)
.PHONY: all clean
all: app
clean:
rm -f $(OBJ) app - ▸Biến: CC, CFLAGS, OBJ - đổi một chỗ, áp dụng mọi nơi.
- ▸Tự động: $@ = target, $< = prerequisite đầu, $^ = mọi prerequisite.
- ▸.PHONY: all, clean - target “không phải file”, luôn chạy.
Make không đọc mã C, nên nếu bạn quên khai báo header trong prerequisites, sửa header sẽ không kích hoạt build lại - sinh ra bản dịch cũ/sai. Cách chuẩn: nhờ trình biên dịch tự sinh phụ thuộc:
auto-deps
# -MMD: sinh file .d ghi cac header ma moi .c phu thuoc
# -MP : them target rong cho header (tranh loi khi xoa header)
CFLAGS = -Wall -Wextra -std=c17 -MMD -MP
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
-include $(OBJ:.o=.d) # nap cac phu thuoc da sinh ra Viết một Makefile thật, chạy build tăng dần, dùng touch để thấy chỉ phần phụ thuộc được dịch lại, và tự sinh phụ thuộc header.
$ unzip 03-make-build-graph.zip && cd 03-make-build-graph # tai zip bang nut o tren $ make # build lan dau: dich tat ca $ make # lan hai: 'app' is up to date $ touch util.h # gia lap sua header $ make # chi build lai cac muc phu thuoc util.h $ make clean
Xem từng file (5) ↡
Câu hỏi thường gặp
Bằng THỜI GIAN sửa file (mtime). Một target được build lại nếu nó chưa tồn tại, hoặc có một prerequisite MỚI HƠN nó. Nhờ vậy chỉ phần thực sự thay đổi (và những gì phụ thuộc nó) mới phải dịch lại - gọi là build tăng dần.
Bài Hành trình biên dịch →target: prerequisites - rồi xuống dòng, một dòng recipe BẮT ĐẦU BẰNG KÝ TỰ TAB. Recipe là lệnh shell tạo ra target. Vd: app: main.o util.o (xuống dòng, TAB) gcc main.o util.o -o app.
Make phân biệt dòng recipe bằng ký tự TAB ở đầu (quy ước lịch sử). Dùng dấu cách sẽ báo lỗi “missing separator”. Đây là bẫy số một của người mới - chỉnh editor để dòng recipe trong Makefile dùng TAB thật.
Đánh dấu target KHÔNG phải là một file thật (như clean, all, test). Nếu tình cờ có file tên “clean”, Make sẽ tưởng clean đã “mới” và bỏ qua. .PHONY: clean bảo Make luôn chạy recipe của clean.
Biến (CC = gcc, CFLAGS = -Wall) tránh lặp và đổi một chỗ. Pattern rule %.o: %.c gom mọi luật “.c → .o” thành một, dùng $< (prerequisite đầu) và $@ (target). Giúp Makefile ngắn và không lặp.
Vì bạn QUÊN khai báo header trong prerequisites (main.o: main.c - thiếu util.h). Make không “đọc” mã C nên không tự biết. Cách chuẩn: để trình biên dịch tự sinh phụ thuộc bằng -MMD -MP rồi -include các file .d.
Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.
Trả lời vài câu để chắc rằng em đã nắm bài.
Make dựa vào đâu để quyết định một target có cần build lại không?
- 1
Sửa util.c
Trong công cụ ở Bước 3, bấm “sửa
util.c” rồi xem trước make sẽ build lại những gì. Vì saomain.ođược bỏ qua?Hoàn thành khi:
util.ovàappbuild lại;main.obỏ qua vì nó không phụ thuộcutil.c(chỉ phụ thuộcmain.cvàutil.h). - 2
Sửa header
Bấm “sửa
util.h”. Bao nhiêu mục build lại? Vì sao nhiều hơn?Hoàn thành khi: Ba:
main.o,util.o,app- vì CẢ HAI file.ođều phụ thuộcutil.h, nên đổi header lan ra cả hai rồi tớiapp. - 3
Viết một luật
Viết luật Make cho
util.o, biết nó cầnutil.cvàutil.h.Hoàn thành khi:
util.o: util.c util.h\n<TAB>gcc -c util.c -o util.o - 4
TAB hay space
Make báo “
missing separator”. Nguyên nhân thường gặp nhất là gì?Hoàn thành khi: Dòng recipe dùng dấu cách thay vì TAB. Sửa lại đầu dòng recipe bằng một ký tự TAB.
- 5
.PHONY clean
Vì sao target
cleannên khai báo.PHONY: clean?Hoàn thành khi: Để Make LUÔN chạy recipe của
clean, kể cả khi tình cờ có một file tên “clean” (nếu không, Make tưởng đã mới và bỏ qua). - 6
make hai lần
Bạn chạy
make, rồi chạymakelần nữa mà không sửa gì. Lần hai in ra gì? Vì sao?Hoàn thành khi: “
make: 'app' is up to date.” - vì mọi target đã mới hơn prerequisite, không có gì để build lại.