如何创建 Makefile
什么是 Make?
示例一 - 下载文件
什么是 Make?
这篇文章将通过两个小例子来探索GNU Make的基础知识。它是一款功能极其丰富的构建工具,虽然略显老旧,但由于它如此普及,因此至少值得了解一下它的工作原理。
警告:据我所知,这主要与 Mac 和 Linux 用户相关。除了启动 IDE 并让它处理事情,或者在可用的情况下使用 WSL 作为辅助之外,我对 Windows 上的构建工具或开发了解不多。我知道你可以make
通过GnuWin32获取。我不知道它的效果如何,也不知道是否有人在使用它。
简而言之,它是一个读取Makefilemake
并将源文件转换为可执行文件的工具。它不关心使用什么编译器,只关心构建的编排。
如果您之前从源代码编译过软件包,您可能熟悉以下命令集:
$ ./configure
$ make
$ sudo make install
大量 *nix 软件包以 C 或 C++ 源代码形式分发,其构建方式类似如下。第一行运行一个单独的程序来配置 Makefile,这在依赖系统库的大型项目中必不可少。最后一行通常会假定管理员权限,以便将刚刚构建的可执行文件复制到系统路径。make
不过,我们不需要这些来开始使用 。只需中间一行即可。名字很贴切,不是吗?
在这篇文章中,我将介绍两个具有不同目标的示例。如果你不知道自己在看什么,语法可能会看起来很晦涩(至少对我来说是这样),但一旦你了解了最基本的规则,它们就非常简单了。
示例一 - 下载文件
我们先来做个简单的。这个 Makefile 的作用仅仅是将boot
Clojure 的构建工具下载到用户当前目录。这个工具以 shim 的形式存在,它会下载一个 jar 文件来处理剩下的工作。这个 shim 非常小,所以有时把它放在项目目录本身而不是系统路径下会更方便。
.PHONY: deps help
SHELL = /bin/bash
export PATH := bin:$(PATH)
deps: bin/boot
bin/boot:
(mkdir -p bin/ && \
curl -fsSLo bin/boot https://github.com/boot-clj/boot-bin/releases/download/latest/boot.sh && \
chmod 755 bin/boot)
help:
@echo "Usage: make {deps|help}" 1>&2 && false
我们将从顶部开始。
.PHONY deps help
SHELL = /bin/bash
首先,我们声明伪目标。为了解释这一点,我们需要讨论它的核心make
:规则。
Make 的作用是将源代码编译成目标文件。为此,我们为其制定了规则,使其能够理解哪些源代码以及如何将它们输入编译器以获得正确的目标文件。最终,我们应该生成所有需要的目标文件——编译后的源代码。
记住这一点,规则就很容易理解了。每条规则都以要创建的目标名称开头,后跟一个冒号。冒号后面是该目标所依赖的任何目标,冒号下方缩进的是一系列命令(或称配方),用于根据目标的依赖项构建目标。当你make
使用目标调用时,它会明确地创建该目标;但当你单独调用它时,它只会开始执行它看到的第一个不以.
(like .PHONY
) 开头的规则。
接下来,我们定义shell可执行文件的位置,
语法$()
是 Make 变量。Make 的简洁之处在于它会自动将环境中找到的每个变量公开为 Make 变量,因此我们只需使用$PATH
from bash
with即可$(PATH)
。要定义自己的变量,只需将其赋值给变量名称,省略括号,就像我们在第一行中所做的那样——这就是对$(SHELL)
变量的赋值。
值得注意的是,我们:=
为其使用了赋值语法。这明确定义了一个简单扩展的赋值。该变量将被读取一次,仅此而已 - 其中的任何其他变量在赋值时都会立即扩展一次。
=
递归扩展的变量在被替换时会扩展其内部的所有内容。这很强大,但也可能导致诸如无限循环和执行缓慢之类的问题,因此务必注意两者的区别。
需要注意的是,这仅适用于本进程及其所有子进程——它并非永久性的,无法改变父进程。make
不过,如果你在 内部构建,这仍然很有用,并且不会扰乱你的全局环境!
然后我们进入第一条规则。在本例中,默认规则名为deps
,它是我们的伪目标之一。不会创建名为“deps”的文件。
deps: bin/boot
目标名称后面是一个冒号,后面跟着一个依赖项列表。这些目标必须在执行此规则之前完成。在执行此目标的命令块之前,Make 会确保每个目标都存在,如果找到,则会执行它们的规则。在本例中,依赖项是目标“bin/boot”。此规则没有关联任何命令,它所做的只是调用另一个规则。
bin/boot:
(mkdir -p bin/ && \
curl -fsSLo bin/boot https://github.com/boot-clj/boot-bin/releases/download/latest/boot.sh && \
chmod 755 bin/boot)
这不是一个伪目标,它包含一个斜杠,表示目录名。这个目标,或者说执行这条规则的结果,最终会到达我们添加到 PATH 的那个目录。
此规则没有任何依赖项 - 它们都与目标名称出现在同一行。但它确实包含命令 - 此规则将创建一个目录,执行该命令curl
以从 GitHub 下载文件,并执行该chmod
命令以使下载的文件可执行。
因此,运行make
会找到一条make deps
规则,该规则本身为空,但具有bin/boot
依赖关系。Make 会意识到该bin/boot
规则尚不存在,并执行该规则,从而创建相应的文件。
尝试运行它,然后再次运行它:
$ make
(mkdir -p bin/ && \
curl -fsSLo bin/boot https://github.com/boot-clj/boot-bin/releases/download/latest/boot.sh && \
chmod 755 bin/boot)
$ make
make: Nothing to be done for 'deps'.
第一次执行此规则后,发现名为 的文件boot
已存在于名为 的目录中./bin
。目标文件已找到,因此make
无需额外操作。这种便捷的特性被称为幂等性。重复调用与一次调用具有相同的效果:f(x);
和f(x); f(x);
是等效的。
太棒了!我们来看一些更典型的例子。
示例二:构建一些 C++
这个比较复杂。这个 makefile 是我在考虑之前就把它放到了一个全新的 C++ 项目目录中的。它更能体现出常见的 makefile 的样子,但其适用范围仍然很小。
它需要一个src
包含一堆.cpp
(和.h
) 文件的目录,并会创建一个名为 的目录,build
其中包含所有.o
目标文件和可执行文件,其名称由您指定。然后,您可以运行该可执行文件。
.PHONY: all clean help
CXX=clang++ -std=c++11
FLAGS=-Wall -Wextra -Werror -pedantic -c -g
BUILDDIR=build
SOURCEDIR=src
EXEC=YOUR_EXECUTABLE_NAME_HERE
SOURCES:=$(wildcard $(SOURCEDIR)/*.cpp)
OBJ:=$(patsubst $(SOURCEDIR)/%.cpp,$(BUILDDIR)/%.o,$(SOURCES))
all: dir $(BUILDDIR)/$(EXEC)
dir:
mkdir -p $(BUILDDIR)
$(BUILDDIR)/$(EXEC): $(OBJ)
$(CXX) $^ -o $@
$(OBJ): $(BUILDDIR)/%.o : $(SOURCEDIR)/%.cpp
$(CXX) $(FLAGS) $< -o $@
clean:
rm -rf $(BUILDDIR)/*.o $(BUILDDIR)/$(EXEC)
help:
@echo "Usage: make {all|clean|help}" 1>&2 && false
在最顶部我们再次有了虚假目标 - 这些目标不会创建真正的文件,它们只是为了作为参数来调用。
接下来我们通过分配变量$(CXX)
和将其指向我们的 C++ 编译器$(FLAGS)
:
CXX=clang++ -std=c++11
FLAGS=-Wall -Wextra -Werror -pedantic -c -g
这些并非特殊名称——您可以随意称呼它们。我们会在规则中直接引用它们。
C++ 编译分为两个阶段。首先,我们将所有独立的*.cpp/*.h
代码对编译成各自的.o
目标文件,然后在一个单独的步骤中将它们全部链接成一个可执行文件。我们传递给编译器的标志仅在从源代码构建对象时有效——链接已编译的对象则无需使用它们!这样,无论在规则评估中是否使用这组标志,我们都可以调用编译器。我喜欢让我的编译器尽可能严格——这些标志会将所有警告转换为阻止编译成功的错误,并启用全套检查。-c
标志指示编译器不要进入链接阶段,以.o
文件结束,并且-g
标志会生成源代码级调试信息。
更高级的 makefile 会包含多个构建配置。这又是一个入门套件。
接下来的三个任务只是配置所有内容的名称:
BUILDDIR=build
SOURCEDIR=src
EXEC=YOUR_EXECUTABLE_NAME_HERE
我认为build
输出和src
源文件是有意义的,但您可以在那里调整它们,并将$(EXEC)
成为最终编译的二进制文件。
下面我们定义源的位置以及对象的名称:
SOURCES:=$(wildcard $(SOURCEDIR)/*.cpp)
OBJ:=$(patsubst $(SOURCEDIR)/%.cpp,$(BUILDDIR)/%.o,$(SOURCES))
该$(SOURCES)
变量由函数构建wildcard
。此变量收集.cpp
内部带有扩展名的任何内容src/
。
接下来我们使用patsubst
。其语法为模式,替换,文本。%
模式和替换中的字符相同,其余部分互换。例如,此替换将“game.cpp”替换为“game.o”。对于文本,我们传入$(SOURCES)
刚刚定义的变量 - 因此该变量将包含找到的每个文件名的$(OBJ)
对应文件名。build/*.o
src/*.cpp
make
请查看快速参考来了解可用内容的完整概述。
我使用了简单扩展的变量赋值来实现这些功能。当你知道这样做能得到你需要的结果时,这样做是个好主意,尤其是在使用类似函数的时候wildcard
——递归扩展这些函数可能会(但并不总是)导致速度显著下降。
配置完所有变量后,我们就可以开始定义规则了。第一条规则是我们的默认行为,它被称为all
:
all: dir $(BUILDDIR)/$(EXEC)
这是我们的伪目标之一,因此没有对应的名为“all”的输出文件。另外,deps
与第一个示例一样,此规则没有命令,只有依赖项。这个规则有两个依赖项,dir
和$(BUILDDIR)/$(EXEC)
。它会按照找到的顺序执行它们,所以让我们跳到dir
第一个:
dir:
mkdir -p $(BUILDDIR)
这个命令没有依赖项,所以它会立即执行。这个命令很简单——它只是确保build
目录存在。完成后,我们可以执行$(BUILDDIR)/$(EXEC)
:
$(BUILDDIR)/$(EXEC): $(OBJ)
$(CXX) $^ -o $@
这条规则开始看起来有点奇怪了。目标本身bin/boot
与第一个例子没什么不同,只是使用 make 变量来构建它。如果你设置$(EXEC)
为my_cool_program
,则此目标名为build/my_cool_program
。它依赖于另一个 make 变量 ,$(OBJ)
我们刚刚将其定义为与每个源文件对应的目标文件。它会先解析,所以在查看命令之前,我们先看看这条规则:
$(OBJ): $(BUILDDIR)/%.o : $(SOURCEDIR)/%.cpp
$(CXX) $(FLAGS) $< -o $@
哇哦,这里有两组依赖关系!搞什么鬼,本。
这被称为静态模式规则。当我们有一系列目标时,我们会使用它。总体目标,$(OBJ)
由我们将要创建的每个目标文件组成。在第一个冒号之后,我们需要具体定义每个单独的对象如何依赖于特定的源。我们再次看到%
用于模式匹配的,与调用中的 up 类似patsubst
。每个目标文件的名称都与相应的“.cpp”文件相同,但扩展名会转换为“.o”。
此规则的命令块将针对每个匹配的源/目标对执行。我们使用在顶部定义的 make 变量来调用编译器并传入所有标志,其中包括-c
在链接阶段之前停止的标志,仅输出目标文件。
然后我们使用一些自动变量来填充正确的命令。$<
对应于我们正在处理的依赖项的名称,$@
对应于目标的名称。完全展开后,此$(CXX) $(FLAGS) $< -o $@
命令将如下所示clang++ -std=c++11 -Wall -Wextra -Werror -pedantic -c -g src/someClass.cpp -o build/someClass.o
。
太棒了!一旦此规则完成,每个“.cpp”文件在目录中都会有一个对应的“.o”文件build/
,这正是我们定义的$(OBJ)
。完成后make
,将跳回到调用规则,并使用将我们的对象链接在一起的命令完成$(CXX) $^ -o $@
。
这很类似,但我们省略了标志。我们还使用了另一个自动变量。$^
对应于所代表的整个列表$(OBJ)
。您也可以使用$+
,它完全包含每个列表成员 -$^
省略任何重复项。$@
部分与之前相同 - 它代表目标。这可能会运行类似 的命令clang++ --std=c++11 build/someClassOne.o build/someClassTwo.o build/someClassThree.o build/main.o -o build/my_cool_project
。
完成后,您已编译好可执行文件,可以运行了build/my_cool_project
。谢谢make
!
该 makefile 还提供clean
:
clean:
rm -rf $(BUILDDIR)/*.o $(BUILDDIR)/$(EXEC)
这是另一个没有任何依赖项的伪目标,它只会运行rm
以清除所有目标文件和可执行文件。这样,当您make
再次运行时,它将不得不重新构建所有内容。否则,它只会构建自上次构建项目以来发生过更改的所有文件。
我们只是触及了表面,但希望这能帮助您在遇到这些文件时稍微揭开它们的神秘面纱。
挑战:编写自己的make install
规则,将新创建的目标复制build
到更凉爽的地方!
照片由 Jason Briscoe 在 Unsplash 上拍摄
文章来源:https://dev.to/decidously/how-to-make-a-makefile-1dep