makefile是否可以在C++中强制执行依赖项限制

Can a makefile enforce dependency restrictions in C++

本文关键字:依赖 强制执行 C++ 是否 makefile      更新时间:2023-10-16

我们正在重构我们的代码库,并试图限制不同组件之间的直接依赖关系。我们的源代码树有几个顶级目录:src/a、src/b和src/c。

我们想实施一系列限制:

  • a中的文件不能依赖于b或c中的文件
  • b中的文件可以依赖于文件a,但不能依赖于文件c
  • c中的文件可以直接依赖于b中的文件,但不能依赖于a

强制执行第一个很简单。我有一个隐含的规则:

build/a/%.o : src/a/%.cpp
$(CXX) -I src/a $(OTHER_FLAGS) -o $@ $<

如果a下的文件试图包含b或c中的头文件,则生成将失败,因为找不到头。

第二个规则有一个类似的规则,将src/a和src/b指定为include目录。c号楼出现了问题。以下情况是允许的。

src/c/C.cpp
#include "b.h"
void C() { ... }
src/b/b.h
#include "a.h"
class B { ... };
src/a/a.h
class A { ... };

这里,来自c的文件包括来自b的文件(允许),而b又包括来自a的文件(也允许)。我们想防止这样的代码:

src/c/C_bad.cpp
// Direct inclusion of a
#include "a.h"
src/c/c_bad.h
// Direct inclusion of a
#include "a.h"

对于允许编译的情况,用于在src/c中构建文件的编译命令必须包括一个-Isrc/a,但这允许第二种情况也进行编译。

我怀疑我的问题的答案是编写一个脚本,该脚本查看编译器生成的依赖项,发现潜在的非法依赖项,然后查看源文件以确定这是否是直接依赖项。有没有一种合理的方法来结合编译器和/或makefile构造来实现这一点?

如果重要的话,我们使用GNUMake3.81和g++4.5.3,但如果可能的话,希望是可移植的。

更新

我们正在寻找一种需要努力才能违反规则的东西,而不是一种需要付出努力才能遵守规则的东西。(过去的经验表明,后者不太可能奏效。)虽然另一个答案中有一些好主意,但我接受了写剧本的说法,因为这是最需要努力解决的问题。

感谢大家的回答。

考虑到您在现有的代码库上应用这一点,我会选择"验证脚本"方法。

因此,当构建失败时,您不会修改构建过程并一次一个地切断依赖关系,而是会看到一个无投诉的文件列表。然后,您可以在考虑"大局"的情况下重构代码库,您所做的任何更改都将使用与以前相同的Makefile构建,从而简化测试和调试。

一旦重构,分析脚本就可以继续用作合规性检查器来验证未来的更新。

这种分析的一个可能的起点是使用makedependcpp -MM。例如,使用问题中列出的cpp/h文件:

[me@home]$find。。./b./b/b.h./a./a/a.h./c./c/c_bad.cpp./c/c.cpp./c/c_bad.h[me@home]$cpp-MM-Ia-Ib-Ic*/*.cppC_bad.o:C_bad.cpp a/a.hC.o:C.cpp b/b.h a/a.h[me@home]$#这也适用于头文件[me@home]$cpp-Ia-Ib-Ic-MM c/c_bad.hc_bad.o:c/c_bad.h a/a.h

解析这些输出以确定每个cpp文件的依赖关系并标记那些不符合要求的文件应该是相当直接的。

这种方法的缺点是无法区分直接依赖项和间接依赖项,因此,如果这很重要,您可能需要包括一个额外的步骤来检查源并找出直接依赖项。

您可以使-I选项特定于目标:

build/b/%.o: CPPFLAGS += -Isrc/a
build/c/%.o: CPPFLAGS += -Isrc/b

不过,这是gnu-make特有的,所以它不是可移植的。

是。但这需要一些体力劳动和纪律。

当构建C时,您可以依赖src/b/*.h中的头文件。

在项目B中,主目录中的任何头文件都应该是自包含的,并且不依赖于其他项目。您还需要B src/B/detail/*.h中的一个子目录。在这里,头文件可以包括src/a/*.h和src/B/*.h,但这是一个私有的实现细节,仅可用于B项目的源文件。

最简单的方法是将所有内容的包含路径更改为-Isrc。Include语句然后具有完整的相对路径

#include <a/a.h>

例如。这使得自动检查代码变得更加容易(也许是在提交挂钩中,而不是在makefile中)。


或者,可以对A和B标头中的宏做一些讨厌的事情:

// src/a/a.h
#ifndef SRC_A_H
#define SRC_A_H
#ifndef ALLOW_A
#error "you're not allowed to include A headers here"
#endif
//...

// src/b/b.h
#ifndef SRC_B_H
#define SRC_B_H
#ifdef ALLOW_A_INDIRECT
#define ALLOW_A
#endif
#include <a/a.h>
//...
#ifdef ALLOW_A_INDIRECT
#undef ALLOW_A
#endif
#endif // include guard

现在,这些制定规则将允许A和B构建好:

build/a/%.o: CPPFLAGS += -DALLOW_A
build/b/%.o: CPPFLAGS += -DALLOW_A

这将允许C仅通过B(以及B报头中的宏)进行访问

build/c/%.o: CPPFLAGS += -DALLOW_A_INDIRECT

注意,这需要一些纪律,尤其是在B的头球中,但我想如果它与现有的包括后卫放在一起,它。。。好吧,其实还是挺恶心的。