Hãy Để GNU Make Đặt Các Tệp Trung Gian Vào Thư Mục Riêng
Dưới đây là phiên bản viết lại bằng tiếng Việt phong phú và chi tiết, giữ nguyên ý nghĩa nhưng diễn đạt theo cách khác và bổ sung thêm nội dung:
Hướng dẫn GNU Make lưu trữ các tệp trung gian vào thư mục riêng
Hôm nay mình muốn chia sẻ cách tối ưu hóa file Makefile trong dự án bằng cách di chuyển các tệp trung gian như .o
(object file) và .a
(static library) vào một thư mục độc lập. Trong quá trình thực hiện, mình đã gặp phải một số vấn đề thú vị liên quan đến cách GNU Make xử lý đường dẫn và quy tắc phụ thuộc.
Vấn đề đầu tiên: Xử lý đường dẫn phức tạp
Điều khiến mình đau đầu nhất chính là việc chuyển đổi giữa các ký tự gạch chéo (/
và \
) trong đường dẫn, đặc biệt khi làm việc trên nhiều hệ điều hành. Tuy nhiên, để đơn giản hóa, mình sẽ tập trung vào môi trường Unix/Linux trong bài viết này.
Giải pháp ban đầu: Chỉ định thư mục đầu ra cho .o
Mình bắt đầu bằng cách định nghĩa một biến $(ODIR)
để lưu trữ đường dẫn thư mục chứa tệp trung gian. Quy tắc biên dịch cơ bản được viết như sau:
|
|
Với cách này, GCC sẽ biên dịch trực tiếp các tệp .c
thành .o
trong thư mục $(ODIR)
.
Vấn đề phát sinh: Tự động tạo file phụ thuộc
Khi sử dụng lệnh gcc -MM
để tạo file phụ thuộc giữa .c
và .h
, kết quả trả về có dạng:
|
|
Tuy nhiên, đường dẫn $(ODIR)
không xuất hiện trước xxx.o
, dẫn đến lỗi khi Makefile tìm kiếm tệp.
Giải pháp 1: Dùng sed
để sửa file phụ thuộc
Mình có thể dùng công cụ sed
để thêm đường dẫn $(ODIR)/
vào tên tệp .o
. Tuy nhiên, cách này đòi hỏi xử lý thêm và không tối ưu.
Giải pháp 2: Sử dụng lệnh vpath
của GNU Make
Một cách hiệu quả hơn là dùng lệnh vpath
để chỉ định nơi tìm kiếm các tệp .o
:
|
|
Sau đó, quy tắc biên dịch có thể viết đơn giản:
|
|
Lúc này, Makefile sẽ tự động tìm kiếm .o
trong thư mục $(ODIR)
.
Vấn đề mới: Lỗi thiếu tệp .o
khi chạy lần đầu
Mặc dù cấu hình trên hoạt động tốt khi các tệp .o
đã tồn tại, nhưng lần đầu tiên chạy make
, hệ thống báo lỗi không tìm thấy .o
. Tuy nhiên, chạy lại lần thứ hai thì mọi thứ lại bình thường.
Nguyên nhân:
Khi phân tích kỹ, mình phát hiện ra rằng:
- Trong lần chạy đầu tiên,
a.exe
phụ thuộc vào các tệp.o
chưa tồn tại. - Makefile cố gắng xây dựng
.o
theo quy tắc%.o: %.c
, nhưng kết quả được lưu vào$(ODIR)
. - Tuy nhiên, danh sách phụ thuộc của
a.exe
vẫn giữ nguyên đường dẫn gốc (không có$(ODIR)
), dẫn đến lỗi.
Giải pháp: Tách quá trình liên kết thành mục tiêu giả
Để khắc phục, mình đã chia nhỏ quá trình xây dựng thành các bước rõ ràng:
|
|
Bằng cách này, make
sẽ:
- Xây dựng toàn bộ
$(OBJS)
trước. - Gọi lại
make
để thực hiện liên kết, đảm bảo tất cả.o
đã tồn tại trong$(ODIR)
.
Hạn chế của giải pháp hiện tại
Phương pháp này phụ thuộc vào thứ tự liệt kê các mục tiêu trong all
. Nếu sử dụng chế độ biên dịch đa luồng (make -j
), có thể xảy ra lỗi do thứ tự thực thi không đảm bảo. Tuy nhiên, trong thử nghiệm của mình, cách này vẫn hoạt động ổn định.
Gợi ý cải tiến
Để hoàn thiện hơn, có thể áp dụng các cách sau:
-
Sử dụng biến
addprefix
để thêm đường dẫn vào$(OBJS)
:1
OBJS = $(addprefix $(ODIR)/,$(notdir $(SRCS:.c=.o)))
Điều này đảm bảo mọi tham chiếu đến
.o
đều chứa đường dẫn đầy đủ. -
Tận dụng
secondary expansion
để trì hoãn xử lý phụ thuộc:1 2 3
.SECONDEXPANSION: a.exe: $$(OBJS) $(CC) $^ -o $@
Tính năng này giúp Makefile đánh giá lại phụ thuộc sau khi các biến đã được xác định.
-
Tự động tạo file phụ thuộc có đường dẫn chính xác: Sử dụng