Vấn Đề Tạo Thư Mục Trong GNU Make - nói dối e blog

Vấn Đề Tạo Thư Mục Trong GNU Make

Trong quá trình xây dựng hệ thống Makefile, việc quản lý các tệp trung gian thường yêu cầu chúng ta phải phân tách chúng vào các thư mục riêng biệt. Tuy nhiên, các thư mục này thường chưa tồn tại khi bắt đầu quá trình biên dịch. Điều này đòi hỏi Makefile phải tự động tạo ra các thư mục cần thiết một cách thông minh.

Việc xử lý vấn đề này không hề đơn giản. Cách tiếp cận trực quan nhất là thiết lập mối quan hệ phụ thuộc giữa tệp đích và thư mục chứa nó. Ví dụ:

1
2
3
4
foo.c : out/foo.o
out/foo.o : out
out :
  mkdir $@

Tuy nhiên, cách làm này tồn tại một nhược điểm nghiêm trọng: thư mục (một loại tệp đặc biệt) có dấu thời gian (timestamp) rất “khó kiểm soát”. Mỗi khi bạn thêm/xóa tệp trong thư mục out, dấu thời gian của thư mục sẽ thay đổi, dẫn đến việc rebuild toàn bộ các thành phần phụ thuộc vào nó - điều hoàn toàn không mong muốn.

Giải pháp cổ điển: Sử dụng tệp “probe”

Tôi từng áp dụng kỹ thuật tạo ra một tệp đặc biệt .probe bên trong thư mục mục tiêu:

1
2
3
out/foo.o : foo.c out/.probe
out/.probe :
  mkdir -p $(dir $@) && touch $@

Tệp .probe sẽ giữ nguyên dấu thời gian sau khi được tạo lần đầu, giúp tránh rebuild không cần thiết. Tuy nhiên, giải pháp này vẫn tồn tại hai vấn đề:

  1. Để lại một tệp “vô hình” nhưng gây khó chịu khi quản lý
  2. Trên Windows, các tệp bắt đầu bằng . không được ẩn tự động, gây rối mắt

Giải pháp hiện đại: Phụ thuộc thứ tự (Order-only dependencies)

Từ phiên bản GNU Make 3.80, chúng ta có công cụ mạnh mẽ hơn: phụ thuộc thứ tự. Ký hiệu | cho phép thiết lập quan hệ phụ thuộc chỉ kiểm tra sự tồn tại, không xét dấu thời gian:

1
2
3
out/foo.o : foo.c | out
# Hoặc viết rõ ràng hơn:
out/foo.o : | out

Với cấu hình này:

  • Nếu thư mục out chưa tồn tại, nó sẽ được tạo trước khi build out/foo.o
  • Nếu out đã tồn tại, dấu thời gian của nó sẽ không ảnh hưởng đến quá trình build

Tự động hóa cho nhiều thư mục

Khi dự án yêu cầu tạo hàng loạt thư mục, việc viết rule thủ công cho từng thư mục là không khả thi. Tôi sử dụng biến MKDIRS để thu thập toàn bộ thư mục cần tạo:

1
2
3
4
5
MKDIRS += out
MKDIRS += build/temp

$(sort $(MKDIRS)) :
  mkdir -p $@

Tuy nhiên, cách này vẫn gặp vấn đề với cấu trúc thư mục phân cấp. Ví dụ: Khi tạo đồng thời buildbuild/temp, lệnh mkdir sẽ báo lỗi do thư mục cha/mẹ đã tồn tại.

Giải pháp tối ưu: Xử lý quan hệ phân cấp

Tôi phát triển một macro thông minh để xử lý quan hệ thư mục cha-con:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
MKDIRS := $(sort $(MKDIRS))
define SAFE_MKDIR
 CHILD := $(firstword $(filter $(1)/%,$(MKDIRS)))
 ifeq ($$(strip $$(CHILD)),)
  $(1) :
  $(MKDIR) $$(call pathname,$$@)
 else
  $(1) : | $$(CHILD)
 endif
endef
$(foreach dir,$(MKDIRS),$(eval $(call SAFE_MKDIR,$(dir))))

Cơ chế hoạt động:

  1. Sắp xếp và chuẩn hóa danh sách thư mục
  2. Với mỗi thư mục, kiểm tra xem nó có thư mục con nào trong danh sách không
  3. Nếu có, thiết lập phụ thuộc thứ tự vào thư mục con
  4. Chỉ thực hiện mkdir ở cấp thư mục sâu nhất

Lưu ý khi sử dụng

  • Đảm bảo giữ nguyên cấu trúc thụt lề và ký tự tab trong code Makefile
  • Kiểm tra kỹ phiên bản GNU Make (>=3.80) để sử dụng tính năng phụ thuộc thứ tự
  • Có thể kết hợp với các công cụ quản lý phiên bản như SVN/Git mà không lo xung đột với các thư mục ẩn .svn/.git

Giải pháp này đã được kiểm chứng trong các dự án C/C++ quy mô lớn, giúp tối ưu hóa quá trình build với hàng trăm thư mục trung gian.

0%