Học Makefile với những ví dụ hay nhất
(makefiletutorial.com)- Makefile là công cụ giúp đơn giản hóa tự động hóa build C/C++ và quản lý dependency
- Hoạt động theo cơ chế phát hiện tệp thay đổi bằng timestamp, chỉ chạy bước biên dịch khi cần
- Giải thích cấu trúc cốt lõi như rule, command, prerequisite kèm ví dụ
- Cũng đề cập thực tế đến các tính năng nâng cao như automatic variables, pattern rules, variable expansion
- Giới thiệu tầm quan trọng của khả năng mở rộng và bảo trì thông qua mẫu Makefile thực chiến cho dự án quy mô trung bình
Giới thiệu hướng dẫn tutorial về Makefile
- Makefile là công cụ cốt lõi phụ trách tự động hóa build dự án và quản lý dependency
- Do có nhiều quy tắc ngầm và ký hiệu khác nhau, lúc mới tiếp cận có thể thấy phức tạp, nhưng hướng dẫn này tổng hợp các nội dung chính bằng những ví dụ ngắn gọn và có thể chạy trực tiếp
- Có thể hiểu từng phần thông qua các ví dụ thực hành theo từng mục
Bắt đầu
Mục đích tồn tại của Makefile
- Makefile được dùng để chỉ biên dịch lại những phần đã thay đổi trong các chương trình lớn
- Ngoài C/C++, nhiều ngôn ngữ cũng có công cụ build chuyên dụng riêng, nhưng Make vẫn được dùng rộng rãi trong các kịch bản build tổng quát
- Cốt lõi là logic phát hiện tệp thay đổi và chỉ chạy những tác vụ cần thiết
Các hệ thống build thay thế cho Make
- Nhóm C/C++: có nhiều lựa chọn như SCons, CMake, Bazel, Ninja
- Nhóm Java: Ant, Maven, Gradle
- Go, Rust, TypeScript cũng cung cấp công cụ build riêng
- Các ngôn ngữ thông dịch như Python, Ruby, JavaScript không cần biên dịch nên nhu cầu quản lý riêng kiểu Makefile thường thấp hơn
Phiên bản và các loại Make
- Có nhiều implementation của Make, nhưng hướng dẫn này được tối ưu cho GNU Make (chủ yếu dùng trên Linux, MacOS)
- Các ví dụ tương thích với cả GNU Make 3 và 4
Cách chạy ví dụ
- Sau khi cài
maketrong terminal, lưu từng ví dụ vào tệpMakefilerồi chạy lệnhmake - Các dòng lệnh trong Makefile bắt buộc phải được thụt đầu dòng bằng ký tự tab
Cú pháp cơ bản của Makefile
Cấu trúc của rule
-
target: prerequisite(s)- command
- command
-
Target: tên tệp kết quả build (thường là một tệp)
-
Command: shell script thực thi thực tế (bắt đầu bằng tab)
-
Prerequisite: danh sách tệp phải sẵn sàng trước khi target được build
Bản chất của Make
Ví dụ Hello World
hello:
echo "Hello, World"
echo "This line will print if the file hello does not exist."
- Target
hellokhông có dependency và chạy 2 command - Khi chạy
make hello, nếu tệphellokhông tồn tại thì các lệnh sẽ được thực thi. Nếu tệp đã tồn tại thì sẽ không chạy - Thông thường target được viết trùng với tên tệp
Ví dụ cơ bản biên dịch tệp C
- Tạo tệp
blah.c(nội dungint main() { return 0; }) - Viết Makefile sau
blah:
cc blah.c -o blah
- Khi chạy
make, nếu targetblahchưa có thì quá trình biên dịch sẽ chạy và tạo ra tệpblah - Ngay cả khi
blah.cthay đổi thì cũng không tự động biên dịch lại → cần thêm dependency
Cách thêm dependency
blah: blah.c
cc blah.c -o blah
- Giờ đây nếu
blah.cvừa được thay đổi, targetblahsẽ được build lại - Việc phát hiện thay đổi dựa trên timestamp của tệp
- Nếu can thiệp timestamp một cách tùy ý thì có thể dẫn tới hành vi ngoài ý muốn
Thêm ví dụ
Ví dụ target liên kết và dependency
blah: blah.o
cc blah.o -o blah
blah.o: blah.c
cc -c blah.c -o blah.o
blah.c:
echo "int main() { return 0; }" > blah.c
- Dependency được lần theo dạng cây và quá trình tạo ở từng bước được tự động hóa
Ví dụ target luôn được chạy
some_file: other_file
echo "This will always run, and runs second"
touch some_file
other_file:
echo "This will always run, and runs first"
- Vì
other_filekhông thực sự được tạo thành tệp nên command củasome_filesẽ chạy mỗi lần
Make clean
- Target
cleanthường được dùng để xóa các output build - Đây không phải từ khóa dành riêng trong Make, nên cần tự định nghĩa bằng command
- Nếu có tệp tên là
cleanthì có thể gây nhầm lẫn, vì vậy nên dùng.PHONY
Ví dụ:
some_file:
touch some_file
clean:
rm -f some_file
Xử lý biến
- Biến luôn là chuỗi.
- Thường nên dùng
:=, ngoài ra còn có nhiều kiểu gán như=,?=,+= - Ví dụ sử dụng:
files := file1 file2
some_file: $(files)
echo "Look at this variable: " $(files)
touch some_file
file1:
touch file1
file2:
touch file2
clean:
rm -f file1 file2 some_file
- Cách tham chiếu biến:
$(variable)hoặc${variable} - Dấu ngoặc kép trong Makefile không có ý nghĩa với chính Make (nhưng vẫn cần trong shell command)
Quản lý target
Target all
- Nếu muốn chạy nhiều target cùng lúc, hãy gán vai trò đó cho target đầu tiên (mặc định)
all: one two three
one:
touch one
two:
touch two
three:
touch three
clean:
rm -f one two three
Nhiều target và automatic variable
- Có thể chạy command riêng cho từng target trong nhiều target.
$@chứa tên target hiện tại
all: f1.o f2.o
f1.o f2.o:
echo $@
Automatic variable và wildcard
Wildcard *
*sẽ dò trực tiếp tên trên file system- Nên luôn bọc nó trong hàm
wildcardkhi sử dụng
print: $(wildcard *.c)
ls -la $?
- Không nên dùng trực tiếp
*trong định nghĩa biến
thing_wrong := *.o
thing_right := $(wildcard *.o)
Wildcard %
- Chủ yếu dùng trong pattern rules, có thể trích xuất và mở rộng theo mẫu đã chỉ định
Fancy Rules
Implicit rules
- Make tích hợp sẵn nhiều quy tắc mặc định ẩn liên quan đến build C/C++
- Các biến tiêu biểu:
CC,CXX,CFLAGS,CPPFLAGS,LDFLAGS - Ví dụ C:
CC = gcc
CFLAGS = -g
blah: blah.o
blah.c:
echo "int main() { return 0; }" > blah.c
clean:
rm -f blah*
Static Pattern Rules
- Có thể viết gọn nhiều rule cùng theo một pattern
objects = foo.o bar.o all.o
all: $(objects)
$(CC) $^ -o all
$(objects): %.o: %.c
$(CC) -c $^ -o $@
all.c:
echo "int main() { return 0; }" > all.c
%.c:
touch $@
clean:
rm -f *.c *.o all
Static Pattern Rules + hàm filter
- Dùng filter để chỉ chọn những đối tượng khớp với pattern phần mở rộng cụ thể
obj_files = foo.result bar.o lose.o
src_files = foo.raw bar.c lose.c
all: $(obj_files)
.PHONY: all
$(filter %.o,$(obj_files)): %.o: %.c
echo "target: $@ prereq: $
1 bình luận
Ý kiến trên Hacker News
Có người kể rằng vào năm 1985 đã tận mắt thấy một người ở phòng thí nghiệm Graphics của Boston University dùng Makefile để tạo một trình kết xuất 3D cho hoạt hình. Người đó là một lập trình viên Lisp, đang làm hệ thống sinh thủ tục ban đầu và hệ thống diễn viên 3D, và đã viết một Makefile thực sự thanh lịch chỉ khoảng 10 dòng. Cấu trúc này tự động tạo ra hàng trăm hoạt ảnh chỉ bằng phụ thuộc vào mốc thời gian tệp rất đơn giản. Hình dạng 3D của từng khung hình được tạo bằng Lisp, còn Make thì sinh ra các khung hình. Vào năm 1985, khác với ngày nay khi 3D và hoạt hình đã là chuyện hiển nhiên, lúc đó mọi người đều kinh ngạc; và người đó sau này chính là Brian Gardner, người phụ trách trình kết xuất 3D cho Iron Giant và Coraline
Bày tỏ thắc mắc không biết có phải là người trong 3d-consultant.com/bio.html hay không
Xác nhận xem có đúng là đang nói về bộ phim Coraline không
Giới thiệu một vài cờ hữu ích nhưng ít được biết đến khi dùng Make
--output-sync=recurse -j10: cờ này gom stdout/stderr lại và chỉ in ra khi công việc của từng target kết thúc; nếu không thì log sẽ bị trộn lẫn và khó phân tích--load-averagethay cho-jđể điều tiết tải hệ thống khi chạy song song (make -j10 --load-average=10)--shuffle, vốn xáo trộn ngẫu nhiên lịch chạy các target build, rất hữu ích trong môi trường CI để phát hiện vấn đề phụ thuộc trong MakefileCó nhắc đến ý tưởng nếu tổng hợp chính thức các tùy chọn khác nhau của make dưới dạng văn bản hoặc tài liệu rồi tích hợp vào chương trình thì sẽ dễ tiếp cận hơn
Tùy chọn người đó hay dùng là cờ
-Bđể ép build lại toàn bộVì đã thường xuyên thấy các vấn đề do
make -jgây ra trên máy DOS nên coi hiện tượng đó là bugHỏi liệu vấn đề song song hóa trên hệ thống bận rộn hay môi trường nhiều người dùng chẳng phải là việc hệ điều hành và bộ lập lịch của nó phải xử lý sao
Dù là các cờ hữu ích, nhưng vì những tùy chọn này không portable nên khuyên không nên dùng ngoài các dự án riêng tư chỉ phục vụ cho bản thân
Có ý kiến cho rằng việc bỏ qua
.PHONYtrong tutorial chỉ vì không dùng đến là một cái cớ yếu; đúng ra nên dạy cách dùng công cụ cho chuẩn.PHONYcho mọi recipe.PHONYcho từng recipe và gom lại một lần ở đầu tệp, đồng thời mong có linter để ép buộc-o pipefailmột cách máy móc là có vấn đề; khi dùnggreptrong pipeline chẳng hạn có thể bị hỏng, nên tùy tình huống mà áp dụng.PHONYcho target không phải tệp thì đúng về mặt chặt chẽ, nhưng hầu như không cần thiết và chỉ làm Makefile dài dòng hơn, nên chỉ dùng khi cầnCó ý kiến cho rằng Make là công cụ chuyên cho việc build các codebase C lớn
Có ý kiến rằng Make, hơn là job runner, là một công cụ shell đa dụng chuyển các shell script tuyến tính sang dạng phụ thuộc khai báo
Cũng có lập trường cho rằng cách nhìn Make như công cụ build chỉ dành cho codebase C không còn đúng nữa. Thực tế là trong 20 năm qua đã xuất hiện nhiều hệ thống build vững chắc và rõ ràng hơn. Cần cập nhật góc nhìn này
Hỏi công cụ nào là job runner tốt. (Sau đó có xin lỗi vì bản thân đã hiểu nhầm ý nghĩa của job runner)
Giới thiệu just như một công cụ thay thế hiện đại cho những phần khiến Makefile trở nên phức tạp
just tốt để thay thế một danh sách shell script, nhưng không thể thay thế chức năng cốt lõi của Make là “chỉ chạy lại những rule cần chạy lại”
Ngoài ra còn có các lựa chọn thay thế khác
Các công cụ thay thế tự nhận là thay thế Make, nhưng có người cho rằng chúng hoàn toàn khác nhau và khó mà so sánh trực tiếp. Cốt lõi của Make là tạo ra artifact và không build lại những gì đã build rồi. Trong khi đó just chỉ đóng vai trò công cụ chạy lệnh đơn giản
Ưu điểm của việc dùng Make làm công cụ thực thi lệnh là tính ổn định của một công cụ chuẩn gần như được cài ở khắp mọi nơi. Dù các lựa chọn thay thế có thể được thiết kế tốt hơn, vẫn không thấy cần thiết phải dùng vì còn vướng việc cài đặt riêng
Có người đang dùng Task khá tốt cho các dự án sở thích đơn giản viết bằng C, nhưng vẫn khó đánh giá nó có phù hợp với các dự án lớn hay không (trang chủ chính thức của Task)
Có người thấy thú vị khi gần đây CMake kết luận Makefile không phù hợp để hỗ trợ module C++20 và chọn ninja làm mặc định (hướng dẫn CMake)
clang-scan-deps(slide kỹ thuật)Có ý kiến cho rằng ràng buộc này thực ra là quyết định từ phía CMake hoặc do không có người hỗ trợ cho Makefile generator. Bản thân ninja cũng không hỗ trợ trực tiếp C++ modules (vấn đề liên quan), thậm chí ninja còn ít tính năng hơn Make và có vấn đề là mọi phụ thuộc đều phải được khai báo tĩnh
Có ý kiến cho rằng bản thân việc đưa module vào đã phức tạp và rối rắm
Hỏi có ai từng dùng tup chưa. (tài liệu chính thức)
Có người giới thiệu bản thân là người tạo ra và maintainer chính của Task, một công cụ thay thế Make. Đã phát triển hơn 8 năm và vẫn tiếp tục tiến hóa
just cũng được gợi ý như một công cụ thay thế Make khác (GitHub của just)
Một sự trùng hợp thú vị là có người dùng Task rất thường xuyên và sáng nay còn vừa mở issue
Có ý kiến cho rằng tutorial này có những vấn đề nguy hiểm và tinh vi
ifneq (,$(findstring t,$(firstword -$(MAKEFLAGS))))loadportable hơnguile, và trong môi trường cross-compilation thì cần chỉ định compiler cho chính xácCó thói quen luôn đặt Makefile trong mọi repo GitHub
makelà có thể ngay lập tức thực thi hành vi mong muốn cho từng dự án mà không cần nhớ riêng