【问题标题】:C++ Makefile, is it possible to factorize it even more?C ++ Makefile,是否可以进一步分解它?
【发布时间】:2021-07-26 06:23:25
【问题描述】:

我有一个学校项目,我想编写一个 Makefile,我看到了一些使用 Makefile 和多个源目录和多个可执行文件的示例,但仍然无法将它正确地实现到我的 Makefile。

PS:我正在使用 doctest 进行单元测试(我无法更改它)。

这是项目结构(我无法更改):

.
├── bin
├── build
├── extern
│   └── doctest.h
├── include
│   ├── file1.hpp
│   └── file2.hpp
├── src
│   ├── file1.cpp
│   └── file2.cpp
├── tests
│   ├── file1-test.cpp
│   └── file2-test.cpp
└──  Makefile

我有以下目录:

  • bin: 对于所有可执行文件。

  • build:对于所有对象 (.o)。

  • extern: 用于 doctest 标头(这是我存储任何其他库的地方)

  • include:所有标头 (.hpp)。

  • src:适用于所有课程 (.cpp)。

  • tests:用于所有单元测试(还有.cpp

您可以看到file1.cpp 是一个类,file1.hpp 是类头,file1-test.cpp 是类的单元测试。

这是我的 Makefile:

BIN_DIR := bin/
BUILD_DIR := build/
EXTERN_DIR := extern/
INCLUDE_DIR := include/
SOURCE_DIR := src/
TESTS_DIR := tests/
DEP_DIR := .dep/

DEPENDS := $(patsubst %.o, $(BUILD_DIR)$(DEP_DIR)%.d, $(notdir $(wildcard $(BUILD_DIR)*.o)))

EXE := $(addprefix $(BIN_DIR), file1-test file2-test)

OBJS_1 := $(addprefix $(BUILD_DIR), file1.o)
OBJS_2 := $(addprefix $(BUILD_DIR), file1.o file2.o)

CXX := clang++
CXXFLAGS := -Wall -std=c++11 -g -O3 -I$(INCLUDE_DIR) -I$(EXTERN_DIR)

vpath %.cpp $(SOURCE_DIR) $(TESTS_DIR)

.PHONY: all clean

all: $(EXE)

$(BUILD_DIR) $(BIN_DIR) $(BUILD_DIR)$(DEP_DIR):
    @mkdir -p $@

$(BUILD_DIR)%.o: %.cpp | $(BUILD_DIR) $(BUILD_DIR)$(DEP_DIR)
    @$(CXX) $(CXXFLAGS) -MMD -MP -MF $(BUILD_DIR)$(DEP_DIR)$(notdir $(basename $@).d) -c $< -o $@

$(BIN_DIR)%: $(BUILD_DIR)%.o | $(BIN_DIR)
    @$(CXX) -o $@ $^

$(BIN_DIR)file1-test: $(OBJS_1)
$(BIN_DIR)file2-test: $(OBJS_2)

.PRECIOUS: $(BUILD_DIR)%.o

-include $(DEPENDS)

clean:
    -rm -rf $(BIN_DIR) $(BUILD_DIR)

所以我的问题是:

  • 我的 Makefile 是否遵循良好做法?

  • 优化了吗?如果没有,我怎样才能让它变得更好?

  • 对于每个新的可执行文件,我必须添加一个OBJS_X 变量和一个目标$(BIN_DIR)fileX-test: $(OBJS_X),我可以去掉它吗?如果是的话,有人可以给我写一些通用规则,这样我就不必每次想要一个新的可执行文件时都指定一个变量和一个目标。

  • 如果我只想编译一个可执行文件,我必须使用make bin/fileX-test。是否可以只运行make fileX-test 而不是make bin/fileX-test(但仍将其构建在bin 目录中)?我试图实现这样的规则:fileX-test: $(BIN_DIR)fileX-test 但它没有按我的意愿工作,在编译的最后它开始执行内置规则,我不知道为什么。谁能解释一下?

最终答案:

这是我认为一个很好的答案,如果它可以帮助以后的人:

BIN_DIR := bin/
BUILD_DIR := build/
EXTERN_DIR := extern/
INCLUDE_DIR := include/
SOURCE_DIR := src/
TESTS_DIR := tests/
DEP_DIR := $(BUILD_DIR).dep/

CXX := g++
CXXFLAGS := -Wall -std=c++11 -g -O3 -I$(INCLUDE_DIR) -I$(EXTERN_DIR)
DEPFLAGS := -MMD -MP -MF $(DEP_DIR)

vpath %.cpp $(SOURCE_DIR) $(TESTS_DIR)

file1-test_OBJECTS := $(addprefix $(BUILD_DIR), file1.o)
file2-test_OBJECTS := $(addprefix $(BUILD_DIR), file1.o file2.o)

EXE := $(patsubst %_OBJECTS, %, $(filter %_OBJECTS, $(.VARIABLES)))

.PHONY: all keep help check clean $(EXE)

all: $(EXE:%=$(BIN_DIR)%)

$(foreach E, $(EXE), $(eval $(BIN_DIR)$E: $($E_OBJECTS)))
$(foreach E, $(EXE), $(eval $E: $(BIN_DIR)$E ;))

$(BUILD_DIR) $(BIN_DIR) $(DEP_DIR):
    @mkdir -p $@

$(BUILD_DIR)%.o: %.cpp | $(BUILD_DIR) $(DEP_DIR) $(BIN_DIR)
    @$(CXX) $(CXXFLAGS) $(DEPFLAGS)$(@F:.o=.d) -c $< -o $@

$(BIN_DIR)%: $(BUILD_DIR)%.o
    @$(CXX) -o $@ $^

-include $(wildcard $(DEP_DIR)*.d)

keep: $(EXE:%=$(BUILD_DIR)%.o)

clean:
    -@rm -rf $(BIN_DIR)* $(BUILD_DIR)* $(DEP_DIR)*

【问题讨论】:

  • 广告 3。是的,您必须添加目标所需的对象列表,否则您会得到未定义的引用或多个定义(通常是 main)
  • @tansy 是的,这就是我的怀疑,但我们设法让它“体面”(我认为),请参阅编辑。

标签: c++ makefile project-structure


【解决方案1】:

大多数情况下,您的 makefile 非常好。您可以进行一些简化,但它们只是语法而不是真正的性能等:

DEP_DIR := .dep/

您永远不会单独使用它,因此如果您将其定义更改为:

DEP_DIR := $(BUILD_DIR).dep/

您可以简化对它的引用。

DEPENDS := $(patsubst %.o, $(BUILD_DIR)$(DEP_DIR)%.d, $(notdir $(wildcard $(BUILD_DIR)*.o)))

-include $(DEPENDS)

这似乎很复杂。为什么不摆脱 DEPENDS 直接写:

include $(wildcard $(DEP_DIR)*.d)

这个:

@$(CXX) $(CXXFLAGS) -MMD -MP -MF $(BUILD_DIR)$(DEP_DIR)$(notdir $(basename $@).d) -c $< -o $@

也很复杂。你可以把它(如果你只是DEP_DIR)写成:

@$(CXX) $(CXXFLAGS) -MMD -MP -MF $(DEP_DIR)$(@F:.o=.d) -c $< -o $@

为:

.PRECIOUS: $(BUILD_DIR)%.o

我绝对不会使用这个。 .PRECIOUS 应该很少使用,如果有的话。如果您想避免将目标文件视为中间文件,最好将它们直接列为先决条件,例如:

keep : $(EXE:$(BIN_DIR)%=$(BUILD_DIR)%.o)

但除非您有特殊需要查看这些目标文件,否则让 make 删除它们并没有什么坏处。

关于你关于快捷方式的问题:你看到你所做的行为的原因是你的目标定义:

fileX-test: $(BIN_DIR)fileX-test

没有附加配方,因此 make 将尝试使用隐式规则查找配方。它找到% : %.c 的内置配方,并且因为您设置了vpath,它可以找到匹配的%.c 文件,因此它使用它。为避免这种情况,您可以只提供一个空食谱;将以上内容替换为:

fileX-test: $(BIN_DIR)fileX-test ;

(注意添加分号)。

您的主要问题是如何简化:

EXE := $(addprefix $(BIN_DIR), file1-test file2-test)

OBJS_1 := $(addprefix $(BUILD_DIR), file1.o)
OBJS_2 := $(addprefix $(BUILD_DIR), file1.o file2.o)

all: $(EXE)

$(BIN_DIR)file1-test: $(OBJS_1)
$(BIN_DIR)file2-test: $(OBJS_2)

您可以自动执行此操作,但这样做需要了解 GNU make 的更深层部分。您可能会发现这组博文很有趣:http://make.mad-scientist.net/category/metaprogramming/(从底部/最旧的开始,然后逐步向上)。

将以上内容替换为:

# Write one of these for each program you need:

file1-test_OBJECTS = file1.o
file2-test_OBJECTS = file1.o file2.o

# Now everything below here is boilerplate

EXE = $(patsubst %_OBJECTS,%,$(filter %_OBJECTS,$(.VARIABLES)))

all: $(EXE:%=$(BIN_DIR)%)

$(foreach E,$(EXE),$(eval $(BIN_DIR)$E: $($E_OBJECTS)))
$(foreach E,$(EXE),$(eval $E: $(BIN_DIR)$E ;))
.PHONY: $(EXE)

【讨论】:

  • 感谢您的完整回答。我设法进行了所有更改,现在它工作正常,但是关于.PRECIOUS 的部分仍然有一些问题,我有保留规则(也在.PHONY 中声明)但我不知道我应该在哪里打电话它 ?我试图在这里调用它:$(BIN_DIR)%: $(BUILD_DIR)%.o keep | $(BIN_DIR),但它最终以make: Circular dependency dropped 结尾。看着:gnu.org/software/make/manual/make.html,使用.PRECIOUS.SECONDARY 看起来不错,甚至受到鼓励?
  • 您不必在任何地方构建keep 目标。它就在那里,以便先决条件在 makefile 中的某处明确列出。 .PRECIOUS 表示如果 make 在运行构建 .PRECIOUS 目标之一的规则时被中断(例如,您使用 ^C 停止它),则 make 不会清理该部分构建的目标。所以你可能有一个损坏的目标文件,有时很难调试。它仅适用于非常具体、不寻常的情况。
  • 我应该说,如果它们被打断,表现良好的工具会自己删除目标文件,因此对于这些类型的工具,make 这样做并不重要。或者,您可以防御性地编写规则,以便它们将输出生成到临时文件,然后使用mv 以原子方式重命名它们。但总的来说,将目标声明为 .PRECIOUS 并不是一个好主意,除非您真的想要这种行为。
  • 我终于设法让一切正常,我用最终答案编辑了我的原始帖子。感谢您的帮助和清晰的解释。
【解决方案2】:

我将我的评论变成一个答案,以允许其他人不赞成这种观点:我认为 CMake 更适合你。查看 this SO 了解 Make 和 CMake 之间的一些差异以及 CMake 的参数。

与您的问题相关的优势:

  • 它将让您更轻松地遵循良好做法
  • 它的扩展性更好
  • 您不必为添加到代码中的新可执行文件编写如此多的样板
  • 可以构建单个可执行文件,请参阅 this SO 作为提示。

【讨论】:

  • 感谢您的回答,如果我没有主题限制,这可能就是我会使用的(我应该在我的原始帖子中说)。
猜你喜欢
  • 2021-12-21
  • 1970-01-01
  • 1970-01-01
  • 2011-07-15
  • 2011-05-29
  • 1970-01-01
  • 2012-06-24
  • 2012-04-03
  • 2011-06-18
相关资源
最近更新 更多