Hãy Để GNU Make Đặt Các Tệp Trung Gian Vào Thư Mục Riêng - nói dối e blog

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 (/\) 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:

1
2
$(ODIR)/%.o: %.c
    $(CC) -c $< -o $@

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.h, kết quả trả về có dạng:

1
xxx.o: xxx.c xxx.h

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:

1
vpath %.o $(ODIR)

Sau đó, quy tắc biên dịch có thể viết đơn giản:

1
2
%.o: %.c
    $(CC) -c $< -o $(ODIR)/$*.o

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:

  1. 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.
  2. 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).
  3. 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:

1
2
3
4
5
6
7
all: $(OBJS) a.exe

a.exe: $(OBJS)
    $(MAKE) exe

exe: $(OBJS)
    $(CC) $^ -o a.exe

Bằng cách này, make sẽ:

  1. Xây dựng toàn bộ $(OBJS) trước.
  2. 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:

  1. 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 đủ.

  2. 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.

  3. Tự động tạo file phụ thuộc có đường dẫn chính xác: Sử dụng

0%