我是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.o
中makefile
的依赖项。在运行make
和重新运行二进制文件./main
的明显测试期间,不会发现此错误,因为整个cow.cpp
库直接include
存放在main.cpp
中。因此一切似乎都很好,并且Moo!
可以按预期打印出来。
但是当我将cow.cpp
改为打印Bark!
而不是Moo!
时,运行make
并没有任何作用,现在我的./main
二进制文件已经用完了日期,并且Moo!
仍从./main
打印。
我很好奇,有经验的C ++开发人员如何通过更加复杂的代码库来避免此问题。也许,如果您强迫自己将每个文件拆分为一个头文件和一个实现文件,那么您至少能够迅速纠正所有此类错误?这似乎也不是防弹的。因为头文件有时包含一些内联实现。
我的示例使用make
而不是CMake
,但是看起来CMake
在target_link_libraries
中有相同的依赖项列表问题(尽管传递性有所帮助)。
作为一个相关问题:似乎显而易见的解决方案是构建系统仅查看源文件并推断依赖项(它可以进入一个级别并依靠CMake来处理传递性)。有没有理由不起作用?是否有一个实际上可以执行此操作的构建系统,或者我应该编写自己的构建系统?
谢谢!
答案 0 :(得分:3)
首先,您需要引用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 $@
编辑以回答评论
另一个问题是,将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是一个例子。