【问题标题】:How to avoid forgetting dependencies in make/CMake?如何避免忘记 make/CMake 中的依赖关系?
【发布时间】:2019-01-12 18:54:49
【问题描述】:

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

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

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

main.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" 从main.cpp 内部调用moo():

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

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

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

但是当我将cow.cpp 更改为打印Bark! 而不是Moo! 时,运行make 不会执行任何操作,现在我的./main 二进制文件已过时,并且Moo! 仍在打印来自./main。

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

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

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

谢谢!

【问题讨论】:

  • 你能显示你的 CMakeLists.txt
  • 不相关的;您自己控制构建,但如果您使用 IDE,IDE 很可能会看到 cow.cpp 的 cpp 扩展名并决定像提供的任何其他 cpp 文件一样编译和链接它。出于这个原因,并且因为人们希望编译和链接 cpp 文件,最好不要将 cpp 扩展名用于您打算包含的任何内容。使用 hpp、impl 或非 cpp 的东西。
  • 比你想象的还要糟糕。 make 天真地使用文件时间戳来确定依赖关系是否已更改,因此更改文件内容而不更改时间戳比依赖代码更新将导致 make 什么都不做,而碰撞时间戳而不更改内容将导致 make 重建。因此,如果您仍在使用基于make 的构建系统,您唯一的选择是在任何构建之前执行make clean(或从先前构建中手动删除工件),即强制重建。
  • 同时构建和处理任意项目的完整依赖树并非易事,可能比简单构建它需要更长的时间。
  • Make 和 CMake 是完全不同的东西(尽管 CMake 能够生成 Make 项目)。请在问题中只留下这些工具之一。由于几乎整个帖子都是关于 Make,我建议删除 CMake(从标签和问题中)。否则,您的帖子中有几个不相关的问题,但在 Stack Overflow 上,我们希望看到每个问题帖子一个问题。

标签: c++ makefile cmake


【解决方案1】:

首先,您需要在 Makefile 中引用依赖文件。

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

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

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

然后在你的代码中

-include $(DEPENDS)

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

最后可以使用以下选项自动创建依赖项:-MMD -MP 用于创建对象文件的规则。 Here你可以找到完整的解释。生成依赖项的是MMD; MP 是为了避免一些错误。如果您想在更新系统库时重新编译,请使用 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 $@

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

在您的情况下,更合适的 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 $@

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

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

来自here

$(wildcard pattern…) 这个字符串,在 makefile 的任何地方使用,是 替换为以空格分隔的现有文件的名称列表 匹配给定的文件名模式之一。如果没有现有文件名 匹配一个模式,然后从输出中省略该模式 通配符函数。

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

我问this questions:让我们看看答案。

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

又是一个好问题。

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

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

其实应该是:

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

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

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

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

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

【讨论】:

  • 谢谢,这很有帮助!一个极端情况:如果.d 文件因粗心而被删除,但.o 文件仍然存在,那么我们就有麻烦了。在原始示例中,如果main.d 被删除,然后cow.cpp 被更改,make 将不会意识到它需要重新编译main.o,因此它永远不会重新创建依赖文件。有没有办法在不重新编译目标文件的情况下廉价地创建 .d 文件?如果是这样,那么我们可能会在每个make 命令上重新创建所有/.d 文件。但也许我太偏执了......
  • 作为另一个问题,将DEPENDS 定义重写为DEPENDS := $(wildcard $(OBJDIR)/*.d) 有什么问题吗?
猜你喜欢
  • 1970-01-01
  • 2012-01-15
  • 2012-05-11
  • 1970-01-01
  • 2014-12-08
  • 2020-04-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多