首页 > 解决方案 > 如何避免忘记 make/CMake 中的依赖关系?

问题描述

我是 C++ 新手,正在尝试掌握诸如 make/CMake 之类的构建系统的窍门。来自 Go,似乎存在一个持续的风险,即如果你忘记做一件小事,你的二进制文件就会变得陈旧。特别是,我找不到记住在 make/CMake 中更新依赖项/先决条件的最佳实践。我希望我遗漏了一些明显的东西。

例如,假设我有一个可以编译的基本 makefile main.cpp

CFLAGS = -stdlib=libc++ -std=c++17

main: main.o
    clang++ $(CFLAGS) main.o -o main

main.o: main.cpp
    clang++ $(CFLAGS) -c main.cpp -o main.o

主.cpp:

#include <iostream>

int main() {
    std::cout << "Hello, world\n";
}

到目前为止,一切都很好; make按预期工作。但是假设我有一些其他的仅标题库,称为cow.cpp

#include <iostream>

namespace cow {
    void moo() {
        std::cout << "Moo!\n";
    }
}

我决定通过`include“cow.cpp”moo()从内部调用:main.cpp

#include <iostream>
#include "cow.cpp"

int main() {
    std::cout << "Hello, world\n";
    cow::moo();
}

但是,我忘记更新 in 的依赖main.omakefilemake这个错误在运行和重新运行二进制的明显测试期间没有发现./main,因为整个cow.cpp库直接includemain.cpp. 所以一切看起来都很好,并按Moo!预期打印出来。

但是当我更改cow.cpp为 printBark!而不是 时Moo!,运行make不会做任何事情,现在我的./main二进制文件已经过时了,并且Moo!仍然从./main.

我很想知道有经验的 C++ 开发人员如何使用更复杂的代码库来避免这个问题。也许如果你强迫自己将每个文件拆分为一个头文件和一个实现文件,你至少能够快速纠正所有这些错误?这似乎也不是万无一失的。因为头文件有时包含一些内联实现。

我的示例使用make而不是CMake,但它看起来CMake有相同的依赖关系列表问题target_link_libraries(尽管传递性有点帮助)。

作为一个相关问题:似乎显而易见的解决方案是构建系统只查看源文件并推断依赖关系(它可以只进入一级并依赖 CMake 来处理传递性)。有没有理由这不起作用?是否有实际执行此操作的构建系统,或者我应该自己编写?

谢谢!

标签: c++makefilecmake

解决方案


首先,您需要在Makefile.

这可以通过函数来​​完成

SOURCES := $(wildcard *.cpp)
DEPENDS := $(patsubst %.cpp,%.d,$(SOURCES))

wich 将获取所有*.cpp文件的名称并替换并附加扩展名*.d来命名您的依赖项。

然后在你的代码中

-include $(DEPENDS)

-Makefile如果文件不存在,告诉不要抱怨。如果它们存在,它们将被包含并根据依赖关系正确重新编译您的源代码。

最后,可以使用以下选项自动创建依赖项:-MMD -MP用于创建对象文件的规则。在这里您可以找到完整的解释。生成依赖项的是MMDMP是为了避免一些错误。如果要在更新系统库时重新编译,请MD使用MMD.

在您的情况下,您可以尝试:

main.o: main.cpp
    clang++ $(CFLAGS) -MMD -MP -c main.cpp -o main.o

如果您有更多文件,最好使用单一规则来创建目标文件。就像是:

%.o: %.cpp Makefile 
    clang++ $(CFLAGS) -MMD -MP -c $< -o $@

你也可以看看这两个很好的答案:

在您的情况下,更合适的Makefile应该如下所示(可能有一些错误,但请告诉我):

CXX = clang++
CXXFLAGS = -stdlib=libc++ -std=c++17
WARNING := -Wall -Wextra

PROJDIR   := .
SOURCEDIR := $(PROJDIR)/
SOURCES   := $(wildcard $(SOURCEDIR)/*.cpp)
OBJDIR    := $(PROJDIR)/

OBJECTS := $(patsubst $(SOURCEDIR)/%.cpp,$(OBJDIR)/%.o,$(SOURCES))
DEPENDS := $(patsubst $(SOURCEDIR)/%.cpp,$(OBJDIR)/%.d,$(SOURCES))

# .PHONY means these rules get executed even if
# files of those names exist.
.PHONY: all clean

all: main

clean:
    $(RM) $(OBJECTS) $(DEPENDS) main

# Linking the executable from the object files
main: $(OBJECTS)
    $(CXX) $(WARNING) $(CXXFLAGS) $^ -o $@

#include your dependencies
-include $(DEPENDS)

#create OBJDIR if not existin (you should not need this)
$(OBJDIR):
    mkdir -p $(OBJDIR)

$(OBJDIR)/%.o: $(SOURCEDIR)/%.cpp Makefile | $(OBJDIR)
    $(CXX) $(WARNING) $(CXXFLAGS) -MMD -MP -c $< -o $@

编辑以回答评论 作为另一个问题,将 DEPENDS 定义重写为 just 有什么问题DEPENDS := $(wildcard $(OBJDIR)/*.d)吗?

好问题,我花了一段时间才明白你的意思

这里

$(wildcard pattern…)此字符串在生成文件中的任何位置使用,由与给定文件名模式之一匹配的现有文件名的空格分隔列表替换。如果没有现有文件名与模式匹配,则从通配符函数的输出中省略该模式。

因此,wildcard返回与模式匹配的文件名列表。patsubst作用于字符串,它不关心那些字符串是什么:它被用作创建依赖项的文件名的一种方式,而不是文件本身。在Makefile我发布的示例中,DEPENDS实际上在两种情况下使用:在使用make clean和使用includeso 进行清理时,在这种情况下它们都可以工作,因为您没有DEPENDS在任何规则中使用。存在一些差异(我尝试运行,您也应该确认)。DEPENDS := $(patsubst $(SOURCEDIR)/%.cpp,$(OBJDIR)/%.d,$(SOURCES))如果您运行没有对应文件的make clean依赖项,则不会被删除,而它们会随着您的更改而被删除。相反,您可能包含与您的文件无关的依赖项。*.d*.cpp*.cpp

我问了这个问题:让我们看看答案。

如果.d文件被粗心删除但.o文件仍然存在,那么我们就有麻烦了。在原始示例中,如果main.d被删除然后cow.cpp随后被更改,make则不会意识到它需要重新编译main.o,因此它永远不会重新创建依赖文件。有没有办法在.d不重新编译目标文件的情况下廉价地创建文件?如果是这样,那么我们可能会/.d在每个 make 命令上重新创建所有文件?

又是一个好问题。

是的你是对的。其实这是我的一个错误。发生这种情况是因为规则

main: main.o
    $(CXX) $(WARNING) $(CFLAGS) main.o -o main 

实际上应该是:

main: $(OBJECTS)
    $(CXX) $(WARNING) $(CXXFLAGS) $^ -o $@

因此,只要其中一个对象更改,它就会重新链接(更新可执行文件),并且只要他们的cpp文件更改,它们就会更改。

一个问题仍然存在:如果您删除了依赖项而不是对象,并且只更改了一个或多个头文件(而不是源文件),那么您的程序不会更新。

我还更正了答案的前一部分。

编辑 2 要创建依赖项,您还可以向您的 : 添加新规则Makefile是一个示例。


推荐阅读