如何避免忘记make / CMake中的依赖项?

时间:2019-01-12 18:54:50

标签: c++ makefile cmake

我是C ++的新手,正在尝试摆脱make / CMake之类的构建系统的困扰。来自围棋,似乎一直存在着这样的风险:如果您忘记做一点事情,您的二进制文件将变得过时。特别是,我找不到记住记住在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

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

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

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

但是,我忘了更新main.omakefile的依赖项。在运行make和重新运行二进制文件./main的明显测试期间,不会发现此错误,因为整个cow.cpp库直接include存放在main.cpp中。因此一切似乎都很好,并且Moo!可以按预期打印出来。

但是当我将cow.cpp改为打印Bark!而不是Moo!时,运行make并没有任何作用,现在我的./main二进制文件已经用完了日期,并且Moo!仍从./main打印。

我很好奇,有经验的C ++开发人员如何通过更加复杂的代码库来避免此问题。也许,如果您强迫自己将每个文件拆分为一个头文件和一个实现文件,那么您至少能够迅速纠正所有此类错误?这似乎也不是防弹的。因为头文件有时包含一些内联实现。

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

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

谢谢!

1 个答案:

答案 0 :(得分:3)

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

这可以通过功能

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

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

然后在您的代码中

-include $(DEPENDS)

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

最后,可以使用以下选项自动创建依赖关系:-MMD -MP用于创建对象文件的规则。 Here,您可以找到完整的说明。产生依赖关系的是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 $@

您还可以看看这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 $@

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

很好的问题,我花了一段时间了解您的观点

来自here

  

$(wildcard pattern…)此字符串用于makefile中的任何位置,   由以空格分隔的现有文件名称列表代替,   匹配给定的文件名模式之一。如果没有现有的文件名   匹配某个模式,则该模式将从输出中省略   通配符功能。

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

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

如果从粗心中删除了.d个文件,但保留了.o个文件,那么我们就有麻烦了。在原始示例中,如果删除main.d然后更改cow.cppmake不会意识到它需要重新编译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是一个例子。