← C Thực Chiến · Toolchain & Kiểm soát chất lượng

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

Dòng recipe PHẢI bắt đầu bằng một ký tự TAB. Dùng dấu cách sẽ báo missing separator. Hãy chỉnh editor để Makefile dùng TAB thật.

Đâ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 một file nguồn (cập nhật thời gian), rồi chạy make:
main.o main.cutil.h ✓ mới
util.o util.cutil.h ✓ mới
app main.outil.o ✓ mới
✓ Tất cả đã mới - make in ra “Nothing to be done” và không dịch lại gì.

Sửa util.c → chỉ util.oapp 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ếnpattern 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
Chạy trên máy bạn - kỹ năng thật cần gõ lệnh thật

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
Tải mã nguồn (.zip) Xem trên GitHub bài lab: 03-make-build-graph
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.

Bài CMake & Meson →

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Make dựa vào đâu để quyết định một target có cần build lại không?

  1. 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ì sao main.o được bỏ qua?

    Hoàn thành khi: util.oapp build lại; main.o bỏ qua vì nó không phụ thuộc util.c (chỉ phụ thuộc main.cutil.h).

  2. 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ộc util.h, nên đổi header lan ra cả hai rồi tới app.

  3. 3

    Viết một luật

    Viết luật Make cho util.o, biết nó cần util.cutil.h.

    Hoàn thành khi: util.o: util.c util.h\n<TAB>gcc -c util.c -o util.o

  4. 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. 5

    .PHONY clean

    Vì sao target clean nê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. 6

    make hai lần

    Bạn chạy make, rồi chạy make lầ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.