【问题标题】:Makefile with generated C++ source files带有生成的 C++ 源文件的 Makefile
【发布时间】:2026-01-16 01:05:02
【问题描述】:

我正在使用一个生成 C++ 源文件的工具,我需要将这些文件包含在我的 C++ 项目编译中。

为了特别简化,我的一个非生成源文件main.cpp如下:

extern void bar();

int main(int, char**)
{
    bar();
    return 0;
}

下面是我的Makefile。整体结构是我们项目中已经存在的,我只是添加了生成所需源文件bar.cpp和makefilebar.mk的部分,这将bar.cpp添加到SRCS变量中,所以它(我希望)被包含在编译中:

SRCS := $(CURDIR)/main.cpp

#################### Adding everything between here... #####
BAR_MK := bar.mk

ifeq (,$(findstring clean,$(MAKECMDGOALS)))
include $(BAR_MK)
endif
#################### ...and here ###########################

OBJ_DIR := $(CURDIR)/obj
OBJS := $(patsubst %.cpp,%.o,$(subst $(CURDIR),$(OBJ_DIR),$(SRCS)))

a.out: $(OBJS)
        @echo OBJS == $(OBJS)
        @echo SRCS == $(SRCS)
        $(CXX) $(OBJS)

#################### Adding everything between here... #####
GEN_DIR := $(CURDIR)/gen

# "Generate" my source file(s), add them to SRCS via an external makefile
$(BAR_MK): $(GEN_DIR)/bar.cpp
$(GEN_DIR)/bar.cpp:
        mkdir -p $(dir $@)
        echo "void bar () {}" > $@
        echo SRCS += $@ > $(BAR_MK)
#################### ...and here ###########################

$(OBJ_DIR)/%.o: %.cpp
        mkdir -p $(dir $@)
        $(CXX) -c -o $@ $<

clean:
        rm -rf $(OBJ_DIR)
        rm -f a.out
        rm -rf $(GEN_DIR)
        rm -f $(BAR_MK)

问题是,由于“未定义对bar() 的引用”,我第一次运行make 时编译失败,但随后立即运行make 成功。这是因为在make 第一次运行时,当它评估a.out: 规则时,它看到SRCS 只包含main.cpp,因此bar.cpp 不会被编译。然后在第二次(和后续)运行中,SRCS 包含 main.cppbar.cpp,因此 bar.cpp 在那个时候被编译。

这对我来说似乎很奇怪,因为根据“如何重制 Makefile”中的 GNU Make manual

检查完所有 makefile 后,如果确实有任何更改,make 从一个干净的状态开始并重新读取所有 makefile。

因此,在 GNU Make 在我的第一次 make 调用中处理 include bar.mk 后,发现它已过时并构建它,我希望它会重新开始从头开始进行解析Makefile 从一开始就包含bar.mk,而SRCS 又包含main.cppbar.cpp

我的期望错了吗?或者我的(添加到)Makefile 有什么问题吗?我在 Linux 上使用 GNU Make 4.1。

【问题讨论】:

    标签: makefile gnu-make


    【解决方案1】:

    bar.mk 为目标的规则没有配方。所以make认为没有办法生成缺失的bar.mk。但它不会立即失败,它会继续解析主 Makefile,生成 bar.cpp... 和 bar.mk 但它无法提前知道这一点。结果是生成了bar.mk 但不包括在内......并且make 不会失败。不确定这是否应该被视为错误。我填写了一份maybe-a-bug报告。

    无论如何,如果你明确告诉 make 如何构建 bar.mk 它应该可以工作:

    $(BAR_MK): $(GEN_DIR)/bar.cpp
            echo 'SRCS += $<' > $@
    
    $(GEN_DIR)/bar.cpp:
            mkdir -p $(dir $@)
            echo "void bar () {}" > $@
    

    编辑:添加更多 cmets / 替代解决方案

    请注意,$(BAR_MK) 规则仍然不完美:它声明了一个并非真实的先决条件。但在删除它之前,我们必须修复其他问题:您的模式规则与生成的源文件不匹配,因为您使用了$(CURDIR),它预先设置了完整路径。让我们摆脱它:

    SRCS := main.cpp
    BAR_MK := bar.mk
    
    ifeq (,$(findstring clean,$(MAKECMDGOALS)))
    include $(BAR_MK)
    endif
    
    OBJ_DIR := obj
    OBJS := $(patsubst %.cpp,$(OBJ_DIR)/%.o,$(SRCS))
    
    a.out: $(OBJS)
        @echo OBJS == $(OBJS)
        @echo SRCS == $(SRCS)
        $(CXX) $(OBJS)
    
    GEN_DIR := gen
    
    $(BAR_MK):
        echo 'SRCS += $(GEN_DIR)/bar.cpp' > $@
    
    $(GEN_DIR)/bar.cpp:
        mkdir -p $(dir $@)
        echo "void bar () {}" > $@
    
    $(OBJ_DIR)/%.o: %.cpp
        mkdir -p $(dir $@)
        $(CXX) -c -o $@ $<
    
    clean:
        rm -rf $(OBJ_DIR) a.out $(GEN_DIR) $(BAR_MK)
    

    最后,如果你真的想用相同的规则同时生成bar.mkgen/bar.cpp,还有一种可能,还是告诉make:a pattern rule with multiple targets

    模式规则可能有多个目标。与一般规则不同的是,这 不会以相同的先决条件执行许多不同的规则,并且 食谱。如果一个模式规则有多个目标,make 知道 规则的配方负责制作所有的目标。食谱 只执行一次以生成所有目标。当搜索一个 模式规则来匹配一个目标,其他规则的目标模式 与需要规则的目标相匹配的是偶然的: 只担心为文件提供配方和先决条件 目前有问题。然而,当这个文件的配方运行时, 其他目标被标记为已自行更新。

    下面是bara 将匹配模式规则的通配符:

    PATTERN_TARGET := $(subst bar,b%r,$(BAR_MK) $(GEN_DIR)/bar.cpp)
    $(PATTERN_TARGET):
        mkdir -p $(GEN_DIR)
        echo "void bar () {}" > $(GEN_DIR)/bar.cpp
        echo 'SRCS += $(GEN_DIR)/bar.cpp' > bar.mk
    

    但这可能太奇怪了,会完全破坏可维护性。首选其他两个选项之一,它们更容易理解。

    【讨论】:

    • 我猜您还想添加 bar.cpp 作为主要目标的明确先决条件。
    • 如果bar.mk 文件被正确包含,则不需要这样做。这是因为bar.mkbar.cpp 添加到SRCS 变量中。这应该会反映在OBJS 中,一切都应该没问题。
    • 但是在 bar.mk 尚不存在的情况下仍然失败。
    • 不,因为如果 bar.mk 不存在并且有创建它的规则,make 会执行配方,再次读取所有 makefile(包括新创建的 bar.mk)并从头开始(见the manual)
    • 并非如此:在生成源文件的情况下,模式等效于/absolute/path/obj/gen/bar.o: gen/bar.cpp,并且您没有生成gen/bar.cpp 的规则;您拥有的是/absolute/path/gen/bar.cpp。试一试,你会看到...