Tạo Makefile cho dự án Go, Node.js, Python và C: target, biến, .PHONY và help tự liệt kê
Công cụ sinh sẵn Makefile với chín target thường dùng gồm build, run, test, lint, fmt, clean, install, docker-build và deploy, kèm khai báo biến, dòng .PHONY, target help tự quét chú thích trong chính file, và quan hệ phụ thuộc giữa các target. Toàn bộ dòng lệnh luôn được xuất bằng ký tự Tab thật, đúng yêu cầu bắt buộc của make.
Tính năng nổi bật
- Bốn bộ mẫu theo loại dự án: Go, Node.js, Python và C hoặc C++, mỗi bộ có lệnh và biến riêng
- Bật tắt từng target trong chín target thường dùng, danh sách .PHONY tự cập nhật theo lựa chọn
- Target help quét chính Makefile bằng grep và awk, in ra tên target kèm mô tả viết sau hai dấu thăng
- Đặt .DEFAULT_GOAL để gõ make không tham số sẽ hiện danh sách target thay vì chạy nhầm việc gì đó
- Bật tắt quan hệ phụ thuộc, ví dụ build phụ thuộc install và deploy phụ thuộc build
- Mẫu C sinh sẵn quy tắc mẫu biên dịch từng tệp nguồn sang tệp đối tượng, dùng biến tự động
- Tùy chọn đặt SHELL và .SHELLFLAGS chặt chẽ để job dừng ngay khi một lệnh trong chuỗi ống dẫn thất bại
- Bảng đếm số dòng bắt đầu bằng Tab và số dòng bắt đầu bằng dấu cách để bạn tự kiểm chứng trước khi lưu
Vì sao dự án nào cũng nên có một Makefile dù đã có script trong package hoặc trong CI
Mỗi dự án lâu ngày lại tích thêm một mớ lệnh dài mà chỉ người viết ra nhớ được: chuỗi cờ biên dịch, biến môi trường phải đặt trước khi chạy test, thứ tự bắt buộc giữa các bước. Người mới vào phải hỏi hoặc lục lại lịch sử trò chuyện. Makefile giải quyết đúng chuyện đó bằng một giao diện chung, gõ make help là thấy hết việc có thể làm, gõ make test là chạy được mà không cần biết bên dưới có bao nhiêu cờ. Điểm mạnh thứ hai là nó không phụ thuộc ngôn ngữ, nên một tổ chức có cả dịch vụ Go lẫn dịch vụ Node vẫn dùng chung đúng bốn lệnh quen thuộc cho mọi repository. Điểm mạnh thứ ba là chính pipeline cũng dùng lại được: phần script trong job chỉ còn một dòng gọi make, nghĩa là lệnh chạy trên máy lập trình viên và lệnh chạy trên máy chủ luôn giống nhau, không còn cảnh chạy được ở máy tôi mà hỏng trên CI vì hai nơi gõ hai câu lệnh khác nhau.
Lợi ích khi sử dụng
- Không còn lỗi missing separator vì công cụ luôn xuất ký tự Tab thật
- Người mới vào dự án chỉ cần gõ make help là biết có thể làm gì
- Cùng một câu lệnh chạy được ở máy cá nhân lẫn trong pipeline
- Bốn bộ mẫu bám sát công cụ thật của từng hệ sinh thái chứ không phải lệnh chung chung
- Có sẵn phần đặt SHELL chặt chẽ, thứ mà Makefile viết vội thường bỏ qua
Cách tạo Makefile bằng công cụ này
- 1Chọn loại dự án để nạp bộ biến và bộ lệnh phù hợp, rồi sửa giá trị APP_NAME cho đúng tên chương trình của bạn.
- 2Bật tắt từng target theo nhu cầu, tắt những target dự án chưa có script tương ứng để Makefile không chứa lệnh chết.
- 3Quyết định có sinh dòng .PHONY, có sinh target help tự liệt kê và có đặt help làm mục tiêu mặc định hay không.
- 4Bật quan hệ phụ thuộc nếu muốn make build tự chạy install trước, hoặc tắt đi nếu bạn muốn kiểm soát thứ tự bằng tay.
- 5Đối chiếu bảng đếm ở dưới khung kết quả, số dòng bắt đầu bằng dấu cách phải bằng không, rồi tải file về hoặc chép vào file tên Makefile ở gốc dự án.
Tab chứ không phải dấu cách: quy tắc duy nhất khiến Makefile khác mọi tệp cấu hình khác
Trong Makefile, mỗi dòng lệnh nằm dưới một target bắt buộc phải bắt đầu bằng đúng một ký tự Tab. Đây không phải quy ước thẩm mỹ mà là cú pháp: make phân biệt dòng lệnh với dòng khai báo hoàn toàn dựa vào ký tự đầu tiên. Thay Tab bằng bốn hay tám dấu cách, make sẽ dừng lại và in ra thông báo missing separator kèm số dòng, một thông báo không hề gợi ý gì về nguyên nhân thật nên người mới thường mất khá lâu mới hiểu. Rắc rối nằm ở chỗ hầu hết trình soạn thảo hiện đại mặc định đổi Tab thành dấu cách, và nhiều bộ định dạng tự động cũng làm thế khi lưu tệp. Cách phòng là thêm một tệp cấu hình trình soạn thảo khai báo riêng cho Makefile giữ nguyên Tab, hoặc thêm quy tắc tương ứng trong cấu hình dự án. Cách kiểm tra nhanh nhất là chạy lệnh xem tệp có hiện ký tự điều khiển, khi đó mỗi Tab hiện thành một chuỗi hai ký tự bắt đầu bằng dấu mũ, còn dấu cách vẫn là dấu cách. Bản GNU Make từ phiên bản 3.82 có cho phép đổi ký tự này bằng biến .RECIPEPREFIX, nhưng đừng dùng nếu không có lý do đặc biệt vì mọi người đọc file sau bạn đều mặc định là Tab. Công cụ trên trang này luôn xuất Tab thật ở cả nút chép lẫn nút tải, và hiển thị số dòng thụt bằng dấu cách để bạn tự kiểm chứng.
Biến trong Makefile: bốn toán tử gán và cách ghi đè từ dòng lệnh
Makefile có bốn cách gán biến và chúng khác nhau ở thời điểm tính giá trị. Dấu bằng đơn tạo biến gán trễ, biểu thức bên phải chỉ được tính vào lúc biến được dùng, nên nếu bên phải có lời gọi chương trình ngoài thì nó chạy lại mỗi lần tham chiếu. Dấu hai chấm kèm dấu bằng tạo biến gán ngay, giá trị được tính đúng một lần lúc make đọc tới dòng đó, đây là lựa chọn nên dùng cho hầu hết trường hợp vì dễ đoán và nhanh hơn. Dấu hỏi kèm dấu bằng chỉ gán khi biến chưa có giá trị, rất hợp cho biến muốn cho phép người dùng ghi đè, chẳng hạn viết APP_NAME với dấu hỏi bằng myapp rồi ai đó gọi make build APP_NAME=api là ghi đè được ngay trên dòng lệnh, hoặc đặt sẵn biến môi trường cùng tên. Dấu cộng kèm dấu bằng nối thêm vào giá trị đang có, thường dùng để bổ sung cờ biên dịch. Bên cạnh biến do bạn đặt còn có nhóm biến tự động chỉ dùng được bên trong phần lệnh: ký hiệu đô la a còng là tên target đang dựng, đô la dấu bé là điều kiện tiên quyết đầu tiên, đô la dấu mũ là toàn bộ danh sách điều kiện tiên quyết đã loại trùng. Nhóm này chính là thứ làm quy tắc mẫu của dự án C trở nên ngắn gọn, thay vì phải viết tay từng tệp nguồn.
.PHONY và bẫy target trùng tên với thư mục có thật
Bản chất của make là công cụ dựng tệp: nó coi tên target là tên một tệp cần tạo ra, và nó chỉ chạy phần lệnh khi tệp đó chưa tồn tại hoặc cũ hơn các điều kiện tiên quyết. Cơ chế này rất hợp cho biên dịch nhưng gây bất ngờ với các target chỉ là tên hành động. Nếu dự án của bạn có sẵn một thư mục tên build và Makefile lại có target tên build, make sẽ thấy tệp build đã tồn tại, so sánh thời gian sửa đổi rồi kết luận không có gì phải làm và in ra dòng thông báo target đã cập nhật. Bạn gõ make build mà không thấy gì xảy ra, dù lệnh bên trong hoàn toàn đúng. Tình huống tương tự xảy ra với test khi có thư mục tests, hoặc install, docs, clean. Cách xử lý chuẩn là khai báo những target đó trong danh sách .PHONY, nghĩa là nói với make rằng đây là hành động chứ không phải tệp, hãy luôn chạy phần lệnh và đừng so sánh thời gian gì cả. Có thể viết một dòng .PHONY liệt kê tất cả ở đầu file, hoặc viết nhiều dòng .PHONY rải rác ngay trước từng target. Lợi ích phụ là make cũng bỏ qua bước tìm quy tắc ngầm cho những target này nên chạy nhanh hơn một chút.
Target help tự liệt kê: một dòng grep và awk đọc lại chính Makefile
Một Makefile dùng lâu sẽ có mười lăm tới hai mươi target, và không ai nhớ hết. Mẹo phổ biến trong cộng đồng là để mỗi target tự mang mô tả của nó ngay trên cùng dòng khai báo, viết sau hai dấu thăng, rồi có một target tên help đọc lại chính tệp Makefile để in danh sách. Cơ chế gồm ba phần. Phần thứ nhất là biểu thức chính quy lọc ra những dòng vừa có dạng tên target theo sau bởi dấu hai chấm, vừa chứa hai dấu thăng. Phần thứ hai là biến MAKEFILE_LIST do make tự đặt, chứa đường dẫn của các tệp đang được đọc, nhờ vậy lệnh không cần biết tên tệp là gì và vẫn chạy đúng khi bạn có nhiều tệp include lẫn nhau. Phần thứ ba là awk tách mỗi dòng tại chuỗi hai dấu thăng rồi in tên target canh cột kèm mô tả, thường thêm mã màu cho dễ đọc. Lưu ý về cú pháp: trong Makefile, ký tự đô la muốn giữ nguyên để đưa xuống shell thì phải viết hai lần, nên biểu thức của awk có nhiều cặp đô la liền nhau, và đó là điều bình thường chứ không phải lỗi gõ nhầm. Kết hợp với dòng .DEFAULT_GOAL đặt thành help, người mới vào dự án chỉ cần gõ make là thấy toàn bộ việc có thể làm.
Phụ thuộc giữa target, thứ tự thực thi và một hiểu nhầm về mỗi dòng lệnh
Phần sau dấu hai chấm của một target là danh sách điều kiện tiên quyết, và make bảo đảm mọi điều kiện được xử lý xong trước khi chạy phần lệnh của target. Nhờ vậy bạn viết được chuỗi tự nhiên: deploy phụ thuộc build, build phụ thuộc install. Có một điểm cần cẩn thận khi chạy make song song bằng cờ chỉ số luồng, vì lúc đó các điều kiện tiên quyết độc lập được chạy đồng thời và những target dùng chung một thư mục tạm có thể giẫm lên nhau. Với trường hợp chỉ cần bảo đảm thứ tự mà không muốn kích hoạt dựng lại, hãy dùng điều kiện tiên quyết chỉ định thứ tự, đặt sau dấu gạch đứng trong danh sách. Hiểu nhầm phổ biến nhất lại nằm ở chỗ khác: mỗi dòng lệnh trong phần thân của một target được chạy trong một tiến trình shell riêng biệt. Nghĩa là dòng lệnh chuyển thư mục ở dòng trên không có hiệu lực với dòng dưới, và biến shell đặt ở dòng trên cũng biến mất. Muốn nhiều lệnh chạy chung một shell, hãy nối chúng bằng dấu chấm phẩy hoặc hai dấu và, kèm dấu gạch chéo ngược để xuống dòng cho dễ đọc. Đặt thêm SHELL cùng .SHELLFLAGS với các cờ dừng khi lỗi cũng đáng làm, vì mặc định một lệnh giữa chuỗi ống dẫn thất bại vẫn có thể bị bỏ qua.
Ranh giới với công cụ CI/CD: Makefile chạy ở máy bạn, workflow chạy trên máy chủ
Ba công cụ này bổ sung cho nhau chứ không thay thế nhau, và phân vai rõ ràng giúp cả hệ thống gọn hơn. Makefile trả lời câu hỏi làm gì: nó gói từng chuỗi lệnh dài thành một tên ngắn, chạy giống nhau trên máy lập trình viên và trên máy chủ, không cần dịch vụ nào bên ngoài. File cấu hình pipeline trả lời câu hỏi khi nào và ở đâu: chạy trên sự kiện nào, trên máy có hệ điều hành gì, với biến bí mật nào, kết quả lưu ở đâu. Cách phối hợp thường thấy là phần lệnh trong pipeline chỉ gọi lại các target đã có, ví dụ một dòng gọi make lint, một dòng gọi make test, một dòng gọi make build. Khi đổi cờ biên dịch bạn chỉ sửa Makefile, không phải sửa cả hai tệp cấu hình cho hai nền tảng khác nhau. Nếu bạn đang dùng GitHub, công cụ sinh workflow nằm tại /vi/tools/github-actions-generator với đầy đủ trigger, matrix và cache. Nếu bạn dùng GitLab, công cụ sinh file .gitlab-ci.yml nằm tại /vi/tools/gitlab-ci-generator với stages, services và artifacts. Phần đóng gói ứng dụng thành image thì thuộc về Dockerfile, có công cụ riêng tại /vi/tools/dockerfile-generator, còn target docker-build trong Makefile chỉ là lời gọi tới lệnh dựng image đó.
Câu hỏi thường gặp (FAQ)
Vì sao make báo missing separator?
Vì dòng lệnh của bạn bắt đầu bằng dấu cách thay vì ký tự Tab. Đây là lỗi phổ biến nhất khi viết Makefile và thường do trình soạn thảo tự đổi Tab thành dấu cách khi lưu. Hãy tắt tính năng đó riêng cho Makefile, hoặc chép lại từ khung kết quả của công cụ này vì nó luôn xuất Tab thật.
File phải đặt tên là gì, có bắt buộc viết hoa chữ M không?
make tự tìm theo thứ tự GNUmakefile rồi makefile rồi Makefile. Trong thực tế mọi người dùng Makefile viết hoa chữ M vì nó xếp lên đầu khi liệt kê thư mục và dễ nhận ra. File không có phần mở rộng và đặt ở gốc dự án. Muốn dùng tên khác thì phải gọi make kèm cờ chỉ định tệp.
Gõ make build mà không có gì xảy ra, chỉ báo target đã cập nhật?
Vì dự án của bạn có sẵn thư mục hoặc tệp trùng tên build, và make tưởng đó là sản phẩm cần dựng nên bỏ qua. Cách sửa là thêm build vào dòng .PHONY để make hiểu đây là một hành động chứ không phải tệp và luôn chạy phần lệnh.
Lệnh chuyển thư mục ở dòng trên không có hiệu lực ở dòng dưới?
Đúng, vì mỗi dòng lệnh chạy trong một tiến trình shell riêng, xong dòng nào là shell đó kết thúc. Muốn nhiều lệnh chạy chung một shell, hãy nối chúng bằng dấu chấm phẩy hoặc hai dấu và trên cùng một dòng logic, dùng dấu gạch chéo ngược cuối dòng để xuống hàng cho dễ đọc.
Nên dùng dấu bằng đơn hay dấu hai chấm kèm dấu bằng khi khai báo biến?
Dùng dấu hai chấm kèm dấu bằng cho phần lớn trường hợp vì giá trị được tính đúng một lần, dễ đoán và không chạy lại chương trình ngoài mỗi lần tham chiếu. Dùng dấu hỏi kèm dấu bằng cho biến muốn cho phép ghi đè từ dòng lệnh hoặc từ biến môi trường, ví dụ tên ứng dụng và số phiên bản.
Truyền tham số cho một target như thế nào?
Cách gọn nhất là ghi đè biến ngay trên dòng lệnh, ví dụ make build APP_NAME=api VERSION=1.2.0, với điều kiện biến đó được khai báo bằng dấu hỏi kèm dấu bằng. Đừng cố truyền tham số theo kiểu vị trí như chương trình dòng lệnh vì make sẽ hiểu chúng là tên target và báo không có quy tắc.
Target help hoạt động bằng cách nào?
Nó dùng grep lọc những dòng khai báo target có kèm chú thích viết sau hai dấu thăng, đọc chính tệp Makefile thông qua biến MAKEFILE_LIST, rồi đưa qua awk để tách phần tên và phần mô tả rồi in ra canh cột. Nhờ vậy chỉ cần thêm chú thích khi viết target mới là danh sách tự cập nhật.
Vì sao trong dòng help lại có nhiều ký tự đô la liền nhau?
Vì make coi ký tự đô la là mở đầu của một tham chiếu biến, nên muốn giữ nguyên ký tự đó để đưa xuống shell hoặc awk thì phải viết hai lần. Đó là lý do biểu thức trong dòng help nhìn có vẻ lạ mắt. Nếu bạn viết một lần, make sẽ cố thay bằng giá trị biến và kết quả in ra sẽ rỗng.
Chạy make song song có an toàn không?
An toàn khi các điều kiện tiên quyết thật sự độc lập. Nếu hai target cùng ghi vào một thư mục tạm hoặc cùng sửa một tệp thì chạy song song sẽ gây kết quả thất thường. Khi cần bảo đảm thứ tự mà không muốn kích hoạt dựng lại, hãy dùng điều kiện tiên quyết chỉ định thứ tự đặt sau dấu gạch đứng.
Makefile có chạy được trên Windows không?
Cần một môi trường có make và shell kiểu Unix, thường là hệ thống con Linux cho Windows, hoặc bộ công cụ đi kèm Git, hoặc MSYS2. Ngoài ra hãy tránh các lệnh phụ thuộc hệ điều hành trong phần thân target, ví dụ đường dẫn dùng dấu gạch chéo ngược, để cùng một Makefile chạy được ở nhiều nơi.
Dự án Node đã có scripts trong package thì còn cần Makefile không?
Cần khi dự án có việc nằm ngoài phạm vi của trình quản lý gói, ví dụ dựng image, chạy migration, gọi công cụ hệ thống, hoặc khi tổ chức của bạn muốn mọi repository dù viết bằng ngôn ngữ gì cũng dùng chung bốn lệnh quen thuộc. Makefile khi đó thường chỉ gọi lại các script sẵn có chứ không thay thế chúng.
Makefile sinh ra dùng ngay được chưa?
Nên coi là khung để chỉnh. Hãy kiểm tra lại tên thư mục mã nguồn, đường dẫn tệp thực thi, tên công cụ lint đã cài chưa, nội dung script deploy và các cờ biên dịch cho đúng dự án. Chạy thử từng target một lần trước khi đưa vào pipeline để không phát hiện lỗi giữa lúc phát hành.
Từ khóa liên quan
- makefile generator
- tạo makefile online
- makefile mẫu cho dự án go
- makefile cho node.js
- makefile cho python venv
- makefile cho c c++
- lỗi missing separator makefile
- makefile tab hay space
- phony target là gì
- makefile help target tự liệt kê
- MAKEFILE_LIST awk help
- biến makefile dấu bằng hai chấm
- biến tự động makefile
- makefile phụ thuộc giữa target
- DEFAULT_GOAL makefile
- makefile chạy song song
- makefile docker build target
- gọi make trong ci cd
- makefile clean target
- cách viết makefile chuẩn
