使用 Makefile 中的模式构建可执行文件

Build an executable using a pattern in Makefile

这里是一个简单的Makefile内容:

CXXCOMP=g++
CXXFLAGS=-std=c++11 -g

%.o: %.cpp
    $(CXXCOMP) $(CXXFLAGS) -c $<

main: %.o
    $(CXXCOMP) $(CXXFLAGS) -o $@ $^

我预计第一条规则将为每个 .cpp 生成一个 .o 文件,而第二条规则将从所有 .o 生成一个 main 文件一个。

实际上,当我尝试创建 main 时,以下语句会打印到终端:

target `main' doesn't match the target pattern

你能更深入地解释一下这是怎么回事以及如何解决这个小问题吗?

%.o 不是 shell 通配符,它​​仅在 模式规则 的上下文中有效。要执行您想要的操作,您必须使用 wildcard function,例如

 main: $(wildcard *.o)
           $(CXXCOMP) $(CXXFLAGS) -o $@ $^

但正如 Matt 正确指出的那样,存在一系列不同的问题。更好的方法是对源文件使用 make 变量,然后构造目标文件列表。

SRC = $(wildcard *.cpp)
OBJ = $(SRC:.cpp=.o)

main: $(OBJ)
      ...

正如 John Bollinger 在评论中指出的那样,您可能应该明确命名源文件或目标文件。不这样做会更不安全,更不可预测,而且您为 makefile 复杂性付出的代价甚至不值得。

为了规避复杂性,您必须将所有头文件、源文件、目标文件和输出文件放在同一个文件夹中,这不是很好。但是,这将允许您简单地使用 $(wildcard *.cpp) 函数。否则,您必须链接一系列调用以首先获取 src 目录中的所有源文件,使用 notdir 函数去除名称的目录部分,然后使用 patsubst 函数对文件扩展名进行切换。那看起来像这样:

OBJS = $(patsubst %.cpp, %.o, $(notdir $(wildcard src/*.cpp)))

我个人更喜欢明确列出所有目标文件,但这将满足您的要求。

顺便说一句,您需要 wildcard 函数的原因是因为虽然通配符通常会在规则和目标中按预期工作,但您在变量赋值中使用它,它正在被采用字面上地。如果将 OBJS 设置为 *.o 并回显该变量,您会得到:

OBJS = *.o

echo_objs:
    @echo $(OBS)

输出:

echo *.o

运行 整个 makefile 给出以下错误:

make: *** No rule to make target '*.o', needed by 'project'.  Stop.

当前目录中没有目标文件,因此在赋值中使用通配符函数导致不回显任何内容,这是通常的行为。我回显了目录中所有目标文件的名称:none.

我喜欢做的是像这样列出目标文件:

OBJS = main.o file.o string.o # or whatever

然后,之前像这样设置 vpath 变量:

vpath %.cpp src
vpath %.hpp include

我可以简单地写:

OBJS = main.o
ASMS = $(patsubst %.o, %.asm, $(OBJS))

TARGET = project_name

all: assembler_output $(TARGET) # etc

$(TARGET): $(OBJS)
    $(CXX) $(CXXFLAGS) $(CPPFLAGS) -I include    -o $@ $^ $(LDFLAGS)

%.o: %.cpp
    $(CXX) $(CXXFLAGS) $(CPPFLAGS) -I include -c -o $@ $^

assembler_output: $(ASMS)

%.asm: %.cpp
    $(CXX) $(CXXFLAGS) $(CPPFLAGS) -I include -masm=intel -S -o $@ $^

.PHONY: clean
clean:
    $(RM) $(OBJS) $(TARGET)

我包含了额外的目标,因为 a) 我喜欢在调用链接器之前查看编译器(偶尔还有预处理器)在做什么,并且 b) 希望它有助于模块化您的目标。


在回答了主要问题之后,我确实对您的 makefile 提出了一些建议。你真的应该坚持命名 C 编译器变量 CC 和 C++ 编译器 CXX 的约定,特别是如果你想发布你的代码供其他人使用。您的用户希望在构建项目时能够配置项目的编译设置(也许他们没有或使用 g++),并且当他们不断更改 CXX 时他们会感到惊讶变量无济于事。

虽然玩具项目的 makefile 很短,但它们很快就会变得非常笨重,尤其是测试代码、安装指令等,因此让您的用户不得不 cat -n ./makefile | grep -E 'CXX' 了解所有可能的编译指令寻找实际变量会很烦人。更不用说您在 makefile 中声明变量 in,因此它们无法覆盖设置。您可能希望对这些变量使用 ?=,这样只有在未通过控制台设置任何设置时才会定义它们。

其实如果你在项目目录下运行make --print-data-basegrep -E 'COMPILE',你可以看到确切的命令make有built-in编译C、C++、汇编程序、fortran 等。对于 C++,命令为:

COMPILE.cc = $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c

尽管您的用户可以配置这些标志,但请确保您的强制性 include 目录不在 CPPFLAGS 中。该变量用于可选库的包含目录,以防您的应用程序允许用户在安装了特定库的情况下使用其他功能进行编译。

链接器命令是 LINK.cc = $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH),其中 LDFLAGS 是您的用户也希望能够配置的链接器变量。


一旦您的项目开始变得越来越大、越来越复杂,所有这些都变得很重要。大型项目通常会有嵌套的子文件夹,top-level makefile 将递归调用每个嵌套的 makefile,这就是坚持明确的 well-defined 约定开始真正重要的时候。

编写好的 makefile 需要练习,但它可能会非常有益。不幸的是,我见过一些真正的大师级作品,其中 none 是我写的。不过,希望这能有所帮助;对我来说,在我真正理解和接受公认的做法之前,从技术的角度来看,了解事情为什么按照现在的方式完成总是有帮助的。