Makefile,头文件依赖性

116

假设我有一个Makefile规则:

%.o: %.c
 gcc -Wall -Iinclude ...

每当一个头文件更改时,我希望 *.o 能够被重新构建。与其计算出所有依赖关系,不如只要 /include 目录下的任何头文件更改,那么该目录下的所有对象都必须重新构建。

我想不出一种好的方法来修改规则以适应这一点,欢迎提供建议。如果头文件列表不需要硬编码,则可获得额外加分。


在我写下我的答案之后,我看了一下相关列表,发现了这个链接:https://dev59.com/VXVC5IYBdhLWcg3wbghT,它似乎是一个重复的问题。克里斯·多德的答案与我的相同,只是使用了不同的命名规范。 - dmckee --- ex-moderator kitten
10个回答

136

如果您正在使用GNU编译器,编译器可以为您组装一个依赖项列表。Makefile片段:

depend: .depend

.depend: $(SRCS)
        rm -f "$@"
        $(CC) $(CFLAGS) -MM $^ -MF "$@"

include .depend
或者
depend: .depend

.depend: $(SRCS)
        rm -f "$@"
        $(CC) $(CFLAGS) -MM $^ > "$@"

include .depend

其中SRCS是一个指向你所有源文件列表的变量。

还有一个工具叫做makedepend,但我从来没有像gcc -MM一样喜欢它。


3
我喜欢这个技巧,但我该如何让 depend 只在源文件更改时运行?它似乎每次都在运行... - chase
2
@chase:嗯,我不小心把依赖关系放在了目标文件上,显然应该是源文件,并且两个目标的依赖顺序也错了。这就是我凭记忆打字的后果。现在再试试看。 - dmckee --- ex-moderator kitten
4
是否有办法在每个文件名前添加某个前缀以显示它位于另一个目录中,例如build/file.o - RiaD
我将SRCS更改为OBJECTS,其中OBJECTS是我的*.o文件列表。这似乎可以防止depend每次运行,并且还可以捕获仅对头文件进行的更改。这似乎与先前的评论相反..我有什么遗漏吗? - BigBrownBear00
@dmckee,以下问题在处理多个目标及其依赖关系时是否存在相同的问题?https://stackoverflow.com/questions/30043480/make-recipe-to-prevent-rebuilding-of-non-dependent-targets# - Sami Kenjat
显示剩余2条评论

110

大多数答案出人意料的复杂或错误。然而,已经在其他地方发布了简单而强大的示例[codereview]。诚然,gnu预处理器提供的选项有点令人困惑。然而,使用-MM从构建目标中删除所有目录是有文档记录并且不是bug[gpp]:

默认情况下,CPP采用主输入文件的名称,删除任何目录组件和任何文件后缀,例如'.c',并附上平台通常的对象后缀。

(较新的) -MMD选项可能是您想要的。为了完整起见,这里提供一个支持多个src目录和build目录的makefile示例,并带有一些注释。如果您需要一个没有build目录的简单版本,请参见[codereview]。

CXX = clang++
CXX_FLAGS = -Wfatal-errors -Wall -Wextra -Wpedantic -Wconversion -Wshadow

# Final binary
BIN = mybin
# Put all auto generated stuff to this build dir.
BUILD_DIR = ./build

# List of all .cpp source files.
CPP = main.cpp $(wildcard dir1/*.cpp) $(wildcard dir2/*.cpp)

# All .o files go to build dir.
OBJ = $(CPP:%.cpp=$(BUILD_DIR)/%.o)
# Gcc/Clang will create these .d files containing dependencies.
DEP = $(OBJ:%.o=%.d)

# Default target named after the binary.
$(BIN) : $(BUILD_DIR)/$(BIN)

# Actual target of the binary - depends on all .o files.
$(BUILD_DIR)/$(BIN) : $(OBJ)
    # Create build directories - same structure as sources.
    mkdir -p $(@D)
    # Just link all the object files.
    $(CXX) $(CXX_FLAGS) $^ -o $@

# Include all .d files
-include $(DEP)

# Build target for every single object file.
# The potential dependency on header files is covered
# by calling `-include $(DEP)`.
$(BUILD_DIR)/%.o : %.cpp
    mkdir -p $(@D)
    # The -MMD flags additionaly creates a .d file with
    # the same name as the .o file.
    $(CXX) $(CXX_FLAGS) -MMD -c $< -o $@

.PHONY : clean
clean :
    # This should remove all generated files.
    -rm $(BUILD_DIR)/$(BIN) $(OBJ) $(DEP)

这种方法有效的原因是如果有多个依赖关系线路指向同一目标,这些依赖关系将被简单地连接起来,例如:

该方法之所以可行,是因为如果一个目标有多条依赖关系链,则这些依赖会被简单地连接在一起,例如:

a.o: a.h
a.o: a.c
    ./cmd

等同于:

a.o: a.c a.h
    ./cmd

如提及的:Makefile multiple dependency lines for a single target?


2
OBJ变量值中有一个拼写错误:CPP应该改为CPPS - ctrucza
1
开箱即用,尽管hpp和cpp都在同一个目录下,但它无法为我定位到头文件。 - villasv
1
如果你的源文件(a.cppb.cpp)在 ./src/ 目录下,那么这个替换操作会使得 $(OBJ)=./build/src/a.o ./build/src/b.o 吗? - galois
1
很棒的回答。作为一个非常微小(并且略微离题!)的改进,我建议使用在这里列出的GNU make标准变量名:https://www.gnu.org/software/make/manual/html_node/Implicit-Variables.html。因此,使用CXXFLAGS而不是CXX_FLAGS,并且链接时您是不是指的是LDFLAGS而不是CXX_FLAGS? - Matt Wallis
我不明白 *.d 文件是如何自动更新的。我的意思是,假设我删除了 *.d 文件,然后更改了一些依赖项(比如一些 *.h 文件)。然后当我运行 make 时,没有重新编译任何东西。难道最好为 *.d makefiles 设定一个规则以便重新生成吗?或者我们只使用 clean 来删除某些内容? - LRDPRDX
显示剩余2条评论

29

正如我在这里所发的帖子中提到的,gcc可以同时创建依赖关系并进行编译:

DEPS := $(OBJS:.o=.d)

-include $(DEPS)

%.o: %.c
    $(CC) $(CFLAGS) -MM -MF $(patsubst %.o,%.d,$@) -o $@ $<

'-MF'参数指定了一个文件来存储所依赖的文件。

'-include' 开头的破折号告诉Make在.d文件不存在时继续编译(例如第一次编译)。

请注意,gcc中关于-o选项似乎存在一个错误。 如果你将对象文件名设置为比如obj/_file__c.o,则生成的 file .d仍将包含 file .o而不是obj/_file__c.o。


4
当我尝试这样做时,所有的.o文件都被创建为空文件。我的目标文件在一个build子文件夹中(因此$OBJECTS包含build/main.o build/smbus.o build/etc...),这肯定会像你描述的那样创建.d文件,但它根本没有构建.o文件,如果我删除-MM和-MF,它就会构建.o文件。 - bobpaul
1
使用-MT将解决您答案中最后几行的注释,该注释更新每个依赖项列表的目标。 - Godric Seer
4
因为 man gcc 中说 -MM 暗示了 -E,该选项会在预处理后停止。你需要使用 -MMD 代替 -MM:https://dev59.com/NnE95IYBdhLWcg3wPrgI#30142139 - Ciro Santilli OurBigBook.com

23

这样怎么样:

includes = $(wildcard include/*.h)

%.o: %.c ${includes}
    gcc -Wall -Iinclude ...

您也可以直接使用通配符,但我发现我需要在多个地方使用它们。

请注意,这仅适用于小型项目,因为它假定每个目标文件都依赖于每个头文件。


18
这个方法可行,但问题在于每次做出小改动时,每个目标文件都会重新编译。比如,如果你有100个源代码/头文件,只更改一个文件,那么所有100个文件都会重新编译。 - Nicholas Hamilton
2
这是一个非常糟糕的解决方案。当然,它可以在小型项目上工作,但对于任何生产规模的团队和构建来说,这将导致可怕的编译时间,并成为每次运行 make clean all 的等价物。 - Julien Guertault
在我的测试中,这根本不起作用。 gcc 行根本没有执行,而是执行了内置规则(%o:%.c 规则)。 - Penghe Geng

6

Martin的解决方案很好,但是不能处理位于子目录中的.o文件。Godric指出,-MT标志可以解决这个问题,但同时会阻止正确写入.o文件。下面的方法将解决这两个问题:

DEPS := $(OBJS:.o=.d)

-include $(DEPS)

%.o: %.c
    $(CC) $(CFLAGS) -MM -MT $@ -MF $(patsubst %.o,%.d,$@) $<
    $(CC) $(CFLAGS) -o $@ $<

3

这是两行代码:

CPPFLAGS = -MMD
-include $(OBJS:.c=.d)

只要你在OBJS中列出了所有的目标文件,这将与默认的构建配方一起使用。


3
这将完美地完成任务,甚至可以处理指定的子目录:
    $(CC) $(CFLAGS) -MD -o $@ $<

我用gcc 4.8.3进行了测试。


2
Sophie的答案稍作修改,允许将*.d文件输出到不同的文件夹中(我只会粘贴生成依赖文件的有趣部分):
$(OBJDIR)/%.o: %.cpp
# Generate dependency file
    mkdir -p $(@D:$(OBJDIR)%=$(DEPDIR)%)
    $(CXX) $(CXXFLAGS) $(CPPFLAGS) -MM -MT $@ $< -MF $(@:$(OBJDIR)/%.o=$(DEPDIR)/%.d)
# Generate object file
    mkdir -p $(@D)
    $(CXX) $(CXXFLAGS) $(CPPFLAGS) -c $< -o $@

请注意参数。
-MT $@

这个参数用于确保生成的 *.d 文件中的目标(即对象文件名)包含 *.o 文件的完整路径而不仅仅是文件名。
我不知道为什么在使用 -MMD 和 -c (如 Sophie 的 版本)组合时,不需要此参数。在这种组合中,它似乎将 *.o 文件的完整路径写入 *.d 文件中。如果没有这个组合,-MMD 也只会将纯文件名而没有任何目录组件写入 *.d 文件中。也许有人知道为什么 -MMD 与 -c 结合时会写入完整路径。我在 g++ 手册中没有找到任何提示。

1
我更喜欢这个解决方案,而不是Michael Williamson所接受的答案,它可以捕捉到源代码+内联文件、源代码+头文件以及仅源代码的更改。优势在于,如果只有少量更改,整个库不需要重新编译。对于几个文件的项目来说,这并不是一个很大的考虑因素,但如果你有10个或100个源文件,你会注意到差异。
COMMAND= gcc -Wall -Iinclude ...

%.o: %.cpp %.inl
    $(COMMAND)

%.o: %.cpp %.hpp
    $(COMMAND)

%.o: %.cpp
    $(COMMAND)

2
只有在头文件中没有任何需要重新编译与相应实现文件不同的cpp文件的情况下,此方法才有效。 - matec

1
以下对我有效:

DEPS := $(OBJS:.o=.d)

-include $(DEPS)

%.o: %.cpp
    $(CXX) $(CFLAGS) -MMD -c -o $@ $<

1
-MMD:将创建一个.d文件,其内容类似于“A.o: B.cpp C.hpp D.hpp”。这将存储目标文件以及它所依赖的.cpp和.hpp文件。这是生成二进制文件的技巧,当.hpp文件发生更改时,可以再次生成二进制文件。 -include:https://www.gnu.org/software/make/manual/html_node/Include.html,它将包含“A.o: B.cpp C.hpp D.hpp”字符串,创建一个多行规则。 - Caio V.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接