Học Makefile cùng những ví dụ hay nhất
(makefiletutorial.com)- Makefile chủ yếu được dùng để tự động hóa biên dịch C/C++ trong các dự án lớn bằng cách chỉ chạy lại những bước build cần thiết dựa trên thay đổi của tệp
- Make so sánh dấu thời gian của hệ thống tệp giữa target và dependency, và chỉ chạy lệnh khi target chưa tồn tại hoặc dependency mới hơn
- Quy tắc cơ bản có dạng
targets: prerequisitescùng các lệnh được thụt đầu dòng bằng tab; dùng khoảng trắng để thụt đầu dòng có thể khiếnmakethất bại - Có thể mở rộng luồng build bằng biến, wildcard, biến tự động, pattern rule, điều kiện, hàm, recursive make,
include,.PHONY,.DELETE_ON_ERROR - Ví dụ Cookbook tạo output trong
build/từ mã nguồn C/C++ trongsrc/, và cấu hình quản lý dependency tự động bằng-MMD -MPcùng-include
Khi nào cần Makefile
- Makefile được dùng để xác định những tệp nào cần được biên dịch lại trong các chương trình lớn
- Trường hợp trung tâm của hướng dẫn là biên dịch tệp C/C++
- Ngoài biên dịch, Make cũng có thể được dùng cho các công việc cần chạy một chuỗi lệnh tùy theo việc tệp có thay đổi hay không
- Các công cụ build khác cho C/C++ được giới thiệu gồm SCons, CMake, Bazel, Ninja
- Với Java có Ant, Maven, Gradle, còn các ngôn ngữ như Go, Rust, TypeScript có công cụ build riêng
- Các ngôn ngữ thông dịch như Python, Ruby, JavaScript thuần không cần biên dịch lại khi tệp thay đổi, nên các công cụ như Makefile không phải là bắt buộc
Mô hình thực thi cơ bản của Make
- Ví dụ hoạt động bằng cách tạo tệp
Makefiletrong terminal đã càimake, rồi chạymaketrong thư mục đó - Quy tắc cơ bản có dạng sau
targets: prerequisitescommandbắt đầu bằng tab
- Target thường là tên tệp, và lệnh là các bước để tạo ra target đó
- Prerequisites là các tệp phải tồn tại trước khi chạy lệnh, còn được gọi là dependencies
- 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; nếu dùng khoảng trắng thì
makesẽ thất bại
So sánh thời gian giữa target và dependency
make hellosẽ chạy lệnh gắn với targethello, nhưng nếu tệphellođã tồn tại thì lệnh sẽ không chạy- Target và tên tệp được liên kết trực tiếp; thông thường lệnh của target sẽ tạo ra tệp có cùng tên với target
- Trong quy tắc
blah: blah.c, lệnh biên dịch chỉ chạy khiblahchưa tồn tại hoặcblah.cmới hơnblah - Make không trực tiếp theo dõi lịch sử thay đổi mà dùng dấu thời gian hệ thống tệp như một heuristic
- Nếu sửa tệp rồi đổi thời gian sửa về quá khứ, Make có thể phán đoán sai rằng tệp đó chưa thay đổi
Target mặc định và clean
- Nếu không chỉ định target khi chạy
make, target đầu tiên trong Makefile sẽ được chạy mặc định - Target
allđược dùng khi muốn tạo nhiều target cùng lúc; nếu đặt nó làm quy tắc đầu tiên thì chỉ cần chạymakelà toàn bộ target sẽ được build cleanthường được dùng làm target để xóa các output build, nhưng không phải là từ khóa đặc biệt được Make dành riêng- Nếu
cleankhông phải target đầu tiên và cũng không phải dependency của target khác, thì phải gọi rõ ràng nhưmake cleanmới chạy - Nếu thực sự có một tệp tên là
clean, targetcleancó thể không chạy; điều này sẽ được xử lý sau bằng.PHONY
Biến và xử lý chuỗi
- Biến trong Make chỉ có thể chứa chuỗi, và hướng dẫn nhìn chung khuyến nghị dùng
:= - Tham chiếu biến dùng dạng
$(name)hoặc${name} - Dấu nháy đơn và nháy kép không có ý nghĩa đặc biệt trong chính Make, chúng chỉ là ký tự nằm trong biến
- Có hai cách chính để định nghĩa biến
=: biến đệ quy, sẽ tra cứu và mở rộng khi được sử dụng:=: biến mở rộng đơn giản, chỉ mở rộng những giá trị đã được định nghĩa tại thời điểm khai báo
?=chỉ gán giá trị nếu biến chưa được thiết lập+=dùng để thêm giá trị vào biến- Biến chưa được định nghĩa được xem là chuỗi rỗng
- Khoảng trắng ở cuối dòng không bị loại bỏ; để tạo biến chứa đúng một dấu cách có thể dùng kỹ thuật như
$(nullstring)
Wildcard và biến tự động
- Trong Make,
*và%đều được gọi là wildcard nhưng ý nghĩa khác nhau *tìm tên tệp khớp trên hệ thống tệp; an toàn hơn nếu bọc trong hàm wildcard như$(wildcard *.o)thay vì dùng trực tiếp trong định nghĩa biến- Nếu
*không khớp với tệp nào, nó có thể được giữ nguyên nếu không nằm trong hàmwildcard %được dùng trong quy tắc và một số hàm nhất định, phần khớp được gọi là stem- Các biến tự động thường dùng gồm
$@: tên target$?: các dependency mới hơn target$^: tất cả dependency$<: dependency đầu tiên
Quy tắc ngầm định và pattern rule
- Make có quy tắc ngầm định cho biên dịch C, nên việc build vẫn có thể diễn ra ngay cả khi không viết rõ lệnh biên dịch
- Một số quy tắc ngầm định tiêu biểu có dạng
n.ođược tạo từn.ctheo dạng$(CC) -c $(CPPFLAGS) $(CFLAGS) $^ -o $@n.ođược tạo từn.cchoặcn.cpptheo dạng$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $^ -o $@nđược tạo từn.obằng lệnh linker
- Các biến quan trọng trong quy tắc ngầm định là
CC,CXX,CFLAGS,CXXFLAGS,CPPFLAGS,LDFLAGS - Static pattern rule có dạng
targets...: target-pattern: prereq-patterns ..., trong đó stem khớp từ target sẽ được thay vào pattern của dependency - Pattern rule chứa
%trong target như%.o: %.c, và có thể dùng như quy tắc ngầm định do bạn tự tạo - Quy tắc dấu hai chấm kép
::cho phép định nghĩa nhiều quy tắc cho cùng một target nhưng ít khi được dùng
Thực thi lệnh và xử lý lỗi
- Thêm
@trước lệnh sẽ khiến chính lệnh đó không được in ra - Chạy
make -ssẽ hoạt động như thể mỗi dòng đều có@ở đầu - Mỗi dòng lệnh có hiệu ứng như được chạy trong một shell mới, nên
cd ..ở một dòng sẽ không ảnh hưởng đến dòng tiếp theo - Muốn giữ cùng trạng thái shell thì phải viết trên một dòng như
cd ..; echo \pwd`` hoặc nối dòng bằng backslash - Shell mặc định là
/bin/sh, nhưng có thể đổi nhưSHELL=/bin/bash - Để dùng
$của biến shell trong Makefile, cần viết thành$$ - Cách xử lý lỗi thay đổi theo phương thức thực thi
make -k: dù có lỗi vẫn tiếp tục chạy các công việc khác nếu có thể-trước lệnh: bỏ qua lỗi của lệnh đómake -i: áp dụng bỏ qua lỗi cho mọi lệnh
- Nếu dừng Make bằng
ctrl+c, Make sẽ xóa target mới nhất mà nó vừa tạo ra
Recursive make và biến môi trường
- Khi gọi lại make từ bên trong Makefile, nên dùng biến đặc biệt
$(MAKE)thay vìmake $(MAKE)truyền tiếp các cờ của make và xử lý ảnh hưởng của chúng một cách phù hợp- Khi Make khởi động, các biến môi trường đã được thiết lập lúc chạy sẽ tự động trở thành biến của Make
- Chỉ thị
exportđặt biến vào môi trường shell của mọi recipe - Muốn dùng biến trong make con thì phải
exportbiến đó - Dùng
.EXPORT_ALL_VARIABLEScó thể export mọi biến - Nếu chỉ định nhiều target cùng lúc như
make clean run test, chúng sẽ được chạy theo thứ tựclean,run,test
Điều kiện và hàm
- Câu lệnh điều kiện gồm
ifeq,ifneq,ifdef,ifndef,else,endif ifdefkhông mở rộng tham chiếu biến mà chỉ kiểm tra biến có được định nghĩa hay không- Có thể dùng
$(MAKEFLAGS)vàfindstringđể kiểm tra xem các cờ nhưmake -icó được truyền vào hay không - Hàm trong Make chủ yếu dùng cho xử lý văn bản, được gọi theo dạng
$(fn, arguments)hoặc${fn, arguments} - Các hàm chính gồm
subst: thay thế chuỗipatsubst: thay thế các từ khớp với patternforeach: biến đổi danh sách từ ngăn cách bằng khoảng trắngif: nếu đối số thứ nhất không rỗng thì dùng đối số thứ hai, ngược lại dùng đối số thứ bacall: gọi biến như một hàm cơ bản và dùng các tham số như$(1),$(2)shell: gọi shell nhưng đổi ký tự xuống dòng thành khoảng trắngfilter: chọn phần tử trong danh sách khớp một pattern nhất địnhfilter-out: chọn phần tử không khớp pattern nhất định
include, vpath, .PHONY, .DELETE_ON_ERROR
include filenames...cho phép đọc một hoặc nhiều Makefile khácincludehữu ích khi đọc các Makefile được tạo bởi tùy chọn như-Mcủagccvpath <pattern> <directories>chỉ định các thư mục để tìm tệp dependency cụ thể- Biến
VPATHcó thể thiết lập hành vi đường dẫn tìm kiếm tương tự trên phạm vi toàn cục - Dấu backslash
\được dùng để chia một lệnh dài thành nhiều dòng - Thêm
.PHONYvào target giúp Make không nhầm target đó với tệp cùng tên .DELETE_ON_ERRORsẽ xóa target của quy tắc nếu trong lúc chạy, một lệnh trả về trạng thái thoát khác 0.DELETE_ON_ERRORkhông chỉ áp dụng cho target đứng ngay trước nó nhưPHONY, mà áp dụng cho mọi target
Ví dụ Makefile Cookbook
- Ví dụ Cookbook cung cấp mẫu Makefile phù hợp cho các dự án quy mô vừa
- Nếu đặt tệp C/C++ vào thư mục
src/, Makefile sẽ tự động xác định target biên dịch và dependency - Cấu hình biến chính gồm
TARGET_EXEC := final_programBUILD_DIR := ./buildSRC_DIRS := ./src
SRCSdùngfindvà$(shell ...)để tìm các tệp*.cpp,*.c,*.sOBJSthêm tiền tốBUILD_DIRvào từng tệp nguồn và thêm.ođể tạo đường dẫn output buildDEPSthay.othành.dđể tạo danh sách các Makefile dependencyINC_DIRStìm mọi thư mục bên dướisrc/, cònINC_FLAGSthêm tiền tố-Ivào từng thư mụcCPPFLAGS := $(INC_FLAGS) -MMD -MPcấu hình để GCC tạo các tệp dependency.d- Mã nguồn C và C++ dùng các pattern rule riêng để tạo tệp
.odướibuild/, và các thư mục cần thiết được tạo bằngmkdir -p $(dir $@) cleanđược khai báo là.PHONYvà xóa thư mục build bằngrm -r $(BUILD_DIR)-include $(DEPS)sẽ include các tệp.d, đồng thời chặn lỗi nếu tệp chưa tồn tại như ở lần build đầu tiên
1 bình luận
Các ý kiến trên Hacker News
Có một số flag
makeít được biết đến nhưng hữu ích. Đồng bộ hóa đầu ra giúp stdout/stderr chỉ được in ra sau khi target kết thúc, giảm tình trạng log bị trộn lẫn và khó đọc khi build song songmake --output-sync=recurse -j10Trên các hệ thống nhiều người dùng đang bận, chỉ đặt số job bằng
-jcó thể là chưa đủ, nên cũng có thể giới hạn mức song song dựa trên load average:make -j10 --load-average=10--shuffle, tùy chọn ngẫu nhiên hóa thứ tự lập lịch target, cũng hữu ích trong CI để bắt các dependency bị thiếu trong Makefile:make --shuffle # or --shuffle=seed/reverse-B, tùy chọn buộc build lại toàn bộ mà không cần điều kiệnmake -jvài lần khiến máy rơi vào trạng thái DoS, nên gần như xem đó là bugKhoảng năm 1985, ở Boston University Graphics lab, tôi từng thấy một người dùng Makefile để điều phối việc tạo trình render 3D cho hoạt hình. Anh ấy là người bên Lisp, đang làm với sinh tự động theo thủ tục thời kỳ đầu và hệ thống diễn viên 3D; Makefile chỉ khoảng 10 dòng và rất thanh lịch
Chỉ bằng dependency đơn giản dựa trên ngày tháng file, nó tạo ra hàng trăm animation; Lisp tạo hình dạng 3D cho từng frame, còn Make tạo các frame
Vì đó là năm 1985, thời điểm hầu như chưa có những thứ mà nay ta xem là hiển nhiên trong 3D và animation, nên việc này khiến mọi người kinh ngạc. Sau đó anh ấy đã viết trình render 3D cho Iron Giant và tôi nhớ là cũng đóng vai trò cốt lõi trong Coraline. Đó là Brian Gardner
Make là một trong những công cụ mà tôi thật sự thấy may mắn vì đã học từ sớm trong sự nghiệp. Giờ tôi không dùng nhiều nữa, nhưng nó đã cho thấy sức mạnh của hệ thống khai báo so với hệ thống mệnh lệnh
Tôi nhận ra ý tưởng này mở rộng tự nhiên sang các công việc khác. Nhìn hình ở đầu trang web này, có vẻ tác giả cũng cảm thấy tương tự: nếu nhìn công thức nấu ăn theo cách khai báo như Makefile, thay vì như chuỗi lệnh kiểu script giống công thức truyền thống, ta có thể hiểu công thức tốt hơn
Bài liên quan: https://blog.gpkb.org/posts/cooking-with-make/
Tôi luôn ghi công thức theo cách có thể đọc như Makefile rồi mang vào bếp, và tò mò liệu có ai từng dàn trang hay hiển thị chúng theo kiểu này chưa. Có lẽ sẽ tiết kiệm được nhiều thời gian khi đọc công thức mới, vì không phải tự chuyển script thành Makefile trong đầu
Tất nhiên cái giá phải trả là bạn phải tự giải đồ thị dependency. Khi chỉ muốn làm theo các bước tuần tự đã được tuần tự hóa sẵn, bạn sẽ phải gánh thêm nhiều việc hơn
Bài viết nói rằng hầu hết recipe không được đánh dấu
.PHONY, và dùng điều đó như lý do để tutorial không nhất thiết phải đề cập, nhưng đó là một cái cớ yếu. Công cụ nên dạy cách dùng đúngTrong team, chúng tôi dùng make như một task runner, nên tôi từng bị nhắc nhở vì thêm và duy trì
.PHONYcho mọi recipeStyle guide Makefile của Clark Grubb khá tốt: https://clarkgrubb.com/makefile-style-guide
Tôi tò mò liệu có ai dùng style guide này không. Cũng muốn biết mọi người đánh dấu recipe phony ngay tại nơi khai báo, hay để một danh sách khổng lồ ở đầu file. Giá mà có một linter để ép quy tắc này
Dùng
-o pipefailtheo quán tính thì không hay. Pipefail có ích, nhưng nó phá vỡ thao tác phổ biến nhất mà mọi người làm trong pipeline: lọc đầu ra bằnggrep. Tốt hơn là chỉ thêm riêng vào những recipe cần nóĐánh dấu các target không phải file bằng
.PHONYlà đúng về mặt nghiêm ngặt, nhưng thường không cần thiết. Nếu có nhiều target, nó chỉ làm Makefile dài dòng không cần thiết, nên theo tôi chỉ nên thêm khi cầnVới các recipe tạo ra nhiều file đầu ra, trước đây khi pattern rule không phù hợp thì dummy file/flag file là cách chuẩn, nhưng từ GNU Make 4.3 đã hỗ trợ native group target. Nó cũng có trong Ubuntu 22.04 LTS: https://www.gnu.org/software/make/manual/html_node/Multiple-...
Điểm thú vị gần đây là CMake đã xác định rằng Makefile không phù hợp với các dự án dùng module C++20, còn Ninja mới là lựa chọn đúng. https://cmake.org/cmake/help/latest/manual/cmake-cxxmodules....
Về cơ bản, có vẻ họ cho rằng việc định nghĩa tĩnh các phụ thuộc của target là quá khó hoặc bất khả thi. Giờ việc này được xử lý động bằng các công cụ như
clang-scan-deps: https://llvm.org/devmtg/2019-04/slides/TechTalk-Lorenz-clang...Thực ra Ninja yêu cầu mọi phụ thuộc phải được định nghĩa tĩnh, nên xét ở điểm này nó còn ít tính năng hơn Make thông thường
Make có chỗ đứng của nó với tư cách công cụ build cho các codebase C lớn. Nhưng nó thường bị đối xử như một “trình chạy tác vụ theo dự án” đa dụng, và nó không thật sự hợp với việc đó. Vì ngay cả điều kiện đơn giản cũng khó viết
Ví dụ tôi đã thấy nhiều nỗ lực thiện chí nhằm bọc Terraform bằng Make, nhưng kết cục đều không tốt
Một công cụ hiện đại tuyệt vời để thay thế phần khiến Makefile trở nên lộn xộn là just: https://github.com/casey/just
Theo một ý tưởng hoàn toàn khác còn có Makedown từng được bàn trên HN 8 tháng trước: https://news.ycombinator.com/item?id=41825344
Makefile rất tuyệt, nhưng tốt nhất đừng sa đà quá. Vài năm trước tôi từng cố tạo một framework GNU Make thuần, rồi nhận ra về cơ bản mình đang phát minh lại autoconf; khi đó tôi mới hiểu vì sao GNU autotools ra đời
Makefile là một Turing tarpit kỳ lạ hơi giống Lisp. GNU Make thậm chí còn có cả tính năng metaprogramming, nên rất khó cưỡng lại thôi thúc metaprogram một hệ thống tà đạo ngay trong Makefile. Việc GNU Make được cài đặt cực kỳ rộng rãi cũng làm sức cám dỗ lớn hơn
Tôi là người tạo và là một trong các maintainer của Task, một lựa chọn thay thế cho Make. Nó đã tồn tại hơn 8 năm và vẫn tiếp tục phát triển, nên nếu bạn đang tìm thứ gì mới thì có thể thử. Cũng cứ thoải mái đặt câu hỏi
https://taskfile.dev/
https://github.com/go-task/task
Không biết có ai đã dùng tup chưa: https://gittup.org/tup/ex_dependencies.html
Đây là hệ thống build tự động xác định phụ thuộc dựa trên truy cập hệ thống tệp, nên có thể dùng với bất kỳ compiler hay công cụ nào