合并极其相似的 Make 规则

Merge extremely similar Make rules

我有以下 makefile,我在其中从相同的文件(发布和调试)创建了两个不同的目标:

SRCDIR:=src/
CXXFILES:=$(wildcard $(SRCDIR)*.cpp)
INC:=-Iinc/
CXXFLAGS:=-std=gnu++20 -Wshadow=local -Wall -Wfatal-errors
CPPFLAGS:=$(INC) -MMD -MP
LDFLAGS:=
ODIR:=obj/
DEBUGODIR:=$(ODIR)debug/
RELEASEODIR:=$(ODIR)release/
DEBUG_OFILES := $(patsubst $(SRCDIR)%,$(DEBUGODIR)%,$(patsubst %.cpp,%.cpp.o,$(CXXFILES)))
RELEASE_OFILES := $(patsubst $(SRCDIR)%,$(RELEASEODIR)%,$(patsubst %.cpp,%.cpp.o,$(CXXFILES)))
ALL_OFILES := $(DEBUG_OFILES) $(RELEASE_OFILES)
RELEASE_TARGET := final
DEBUG_TARGET := final_debug
WERROR_CONFIG := -Werror -Wno-error=unused-variable

.DEFAULT_GOAL := all

.PHONY: all clean debug release docs

all: release debug

release: CXXFLAGS += -O2 $(WERROR_CONFIG)
release: $(RELEASE_TARGET)

debug: CXXFLAGS += -Og -ggdb
debug: CPPFLAGS += -DDEBUG
debug: $(DEBUG_TARGET)

-include $(DEBUG_OFILES:%.o=%.d)
-include $(RELEASE_OFILES:%.o=%.d)

$(ALL_OFILES) : Makefile

$(RELEASEODIR) $(DEBUGODIR) :
    mkdir -p $@

$(RELEASE_OFILES) : | $(RELEASEODIR)
$(DEBUG_OFILES) : | $(DEBUGODIR)

$(DEBUGODIR)%.cpp.o: $(SRCDIR)%.cpp
    $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@

$(RELEASEODIR)%.cpp.o: $(SRCDIR)%.cpp
    $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@

$(DEBUG_TARGET): $(DEBUG_OFILES)
    $(CXX) -o $@ $^ $(LDFLAGS)

$(RELEASE_TARGET): $(RELEASE_OFILES)
    $(CXX) -o $@ $^ $(LDFLAGS)

clean:
    rm -rf $(ODIR)
    rm -f $(RELEASE_TARGET)
    rm -f $(DEBUG_TARGET)

这很好用:

make
mkdir -p obj/release/
mkdir -p obj/debug/
g++ -Iinc/ -MMD -MP -std=gnu++20 -Wshadow=local -Wall -Wfatal-errors -O2 -Werror -Wno-error=unused-variable -c src/fraction.cpp -o obj/release/fraction.cpp.o
g++ -Iinc/ -MMD -MP -std=gnu++20 -Wshadow=local -Wall -Wfatal-errors -O2 -Werror -Wno-error=unused-variable -c src/main.cpp -o obj/release/main.cpp.o
g++ -Iinc/ -MMD -MP -DDEBUG -std=gnu++20 -Wshadow=local -Wall -Wfatal-errors -Og -ggdb -c src/fraction.cpp -o obj/debug/fraction.cpp.o
g++ -Iinc/ -MMD -MP -DDEBUG -std=gnu++20 -Wshadow=local -Wall -Wfatal-errors -Og -ggdb -c src/main.cpp -o obj/debug/main.cpp.o
g++ -o final obj/release/fraction.cpp.o obj/release/main.cpp.o 
g++ -o final_debug obj/debug/fraction.cpp.o obj/debug/main.cpp.o 

但是我们有一些非常相似的目标,所以我尝试合并它们

SRCDIR:=src/
CXXFILES:=$(wildcard $(SRCDIR)*.cpp)
INC:=-Iinc/
CXXFLAGS:=-std=gnu++20 -Wshadow=local -Wall -Wfatal-errors
CPPFLAGS:=$(INC) -MMD -MP
LDFLAGS:=
ODIR:=obj/
DEBUGODIR:=$(ODIR)debug/
RELEASEODIR:=$(ODIR)release/
DEBUG_OFILES := $(patsubst $(SRCDIR)%,$(DEBUGODIR)%,$(patsubst %.cpp,%.cpp.o,$(CXXFILES)))
RELEASE_OFILES := $(patsubst $(SRCDIR)%,$(RELEASEODIR)%,$(patsubst %.cpp,%.cpp.o,$(CXXFILES)))
ALL_OFILES := $(DEBUG_OFILES) $(RELEASE_OFILES)
RELEASE_TARGET := final
DEBUG_TARGET := final_debug
WERROR_CONFIG := -Werror -Wno-error=unused-variable

.DEFAULT_GOAL := all

.PHONY: all clean debug release docs

all: release debug

release: CXXFLAGS += -O2 $(WERROR_CONFIG)
release: $(RELEASE_TARGET)

debug: CXXFLAGS += -Og -ggdb
debug: CPPFLAGS += -DDEBUG
debug: $(DEBUG_TARGET)

-include $(DEBUG_OFILES:%.o=%.d)
-include $(RELEASE_OFILES:%.o=%.d)

$(ALL_OFILES) : Makefile

$(RELEASEODIR) $(DEBUGODIR) :
    mkdir -p $@

$(RELEASE_OFILES) : | $(RELEASEODIR)
$(DEBUG_OFILES) : | $(DEBUGODIR)

$(DEBUGODIR)%.cpp.o $(RELEASEODIR)%.cpp.o: $(SRCDIR)%.cpp
    $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@

$(DEBUG_TARGET): $(DEBUG_OFILES)
    $(CXX) -o $@ $^ $(LDFLAGS)

$(RELEASE_TARGET): $(RELEASE_OFILES)
    $(CXX) -o $@ $^ $(LDFLAGS)

clean:
    rm -rf $(ODIR)
    rm -f $(RELEASE_TARGET)
    rm -f $(DEBUG_TARGET)

现在 makefile 完全失败了:

mkdir -p obj/release/
mkdir -p obj/debug/
g++ -Iinc/ -MMD -MP -std=gnu++20 -Wshadow=local -Wall -Wfatal-errors -O2 -Werror -Wno-error=unused-variable -c src/fraction.cpp -o obj/release/fraction.cpp.o
g++ -Iinc/ -MMD -MP -std=gnu++20 -Wshadow=local -Wall -Wfatal-errors -O2 -Werror -Wno-error=unused-variable -c src/main.cpp -o obj/release/main.cpp.o
g++ -o final obj/release/fraction.cpp.o obj/release/main.cpp.o 
g++ -o final_debug obj/debug/fraction.cpp.o obj/debug/main.cpp.o 
/usr/sbin/ld: cannot find obj/debug/fraction.cpp.o: No such file or directory
/usr/sbin/ld: cannot find obj/debug/main.cpp.o: No such file or directory
collect2: error: ld returned 1 exit status
make: *** [Makefile:45: final_debug] Error 1
make: *** Waiting for unfinished jobs....

我在这里错过了什么?为什么 make 不执行调试规则? 运行 独立的目标也给出了一些意想不到的结果:

make debug
mkdir -p obj/debug/
mkdir -p obj/release/
g++ -Iinc/ -MMD -MP -DDEBUG -std=gnu++20 -Wshadow=local -Wall -Wfatal-errors -Og -ggdb -c src/fraction.cpp -o obj/debug/fraction.cpp.o
g++ -Iinc/ -MMD -MP -DDEBUG -std=gnu++20 -Wshadow=local -Wall -Wfatal-errors -Og -ggdb -c src/main.cpp -o obj/debug/main.cpp.o
g++ -o final_debug obj/debug/fraction.cpp.o obj/debug/main.cpp.o 

为什么 make debug 突然创建 obj/release/

$(DEBUGODIR)%.cpp.o $(RELEASEODIR)%.cpp.o: $(SRCDIR)%.cpp
    $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@

是一个有多个目标的模式规则。 GNU make 特别处理这个问题:它认为执行配方 once 会构建 all 目标。而你的食谱没有。

它曾经是指示一次构建多个目标的配方的唯一方法。自 GNU make 4.3 以来,有一个专用结构 (&:)。

基本上有两种方法可以为所欲为:eval和二次扩展。让我们从允许以编程方式实例化 make 构造的 eval 开始:

# $(1) is the target directory
define MY_RULE
$(1)%.cpp.o: $$(SRCDIR)%.cpp
    $$(CXX) $$(CPPFLAGS) $$(CXXFLAGS) -c $$< -o $$@
endef
$(foreach d,$(DEBUGODIR) $(RELEASEODIR),$(eval $(call MY_RULE,$(d))))

注意MY_RULE定义中的$$,它们是必需的,因为它们会被make展开两次:第一次展开eval的参数时,第二次当将结果解析为 make 构造。

二次扩张是另一种选择。 .SECONDEXPANSION: 特殊目标之后的每条规则都将其先决条件列表扩展了两次。在第二次扩展中,定义了一些自动变量。所以把它放在你的 Makefile 的末尾应该做你想做的:

.SECONDEXPANSION:
$(ALL_OFILES): $$(SRCDIR)/$$(patsubst %.o,%,$$(@F))
    $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@