代码编织梦想

  很多基于 C/C++ 开发的项目,在编译、安装时都使用到了 make,这里简单对 make 工具进行简单介绍,并介绍 Makefile 的语法规则以及使用方法。主要能够理解负责执行构建部分的结构即可,其余则是一些帮助 Makefile 开发的一些辅助性工作(代码优化、简化、提高可读性等。)

1 make 工具与 Makefile 简介

   make 是一个用来简化从多个程序模块编译构建( build )可执行文件的 Unix 工具,通常用来完成自动化编译工作,在编辑好 Makefile 文件之后,执行 make 即可一键进行项目的构建,对于处理大型项目来说尤为便捷,不用手动输入过多编译、链接的命令。
  整个项目的编译规则写在 Makefile 文件中(文件名为 Makefile 或 makefile 均可),在使用 make 工具进行项目构建时,会从用户创建的 Makefile 中读取构建规则,按照构建规则对程序进行编译、链接。对于单个程序,我们可能只需要在 shell 中敲一行命令,即可编译、链接出我们需要的可执行文件,但是对于大型项目,可能跟会有很多源文件、头文件,对应有需要很多条程序的编译、链接命令。使用 Makefile 来管理这些规则,使用 make 命令执行构建,能够提高程序的开发效率。
   make 只会重新构建需要重新构建的部分。首次进行项目构建(首次执行 make 命令)之后,如果修改了源文件,再次进行构建,则 make 工具只会对修改部分进行构建,而不是对整个项目进行构建。对于已经构建过,相关依赖文件没有改动的部分,不需要进行重新构建,较为灵活。如果想单独构建 Makefile 中的某个特定的目标 target ,可以直接使用 make target。
   如果 Makefile 文件路径下,有与构建目标同名的文件夹或者文件,如果这些文件夹或者文件并不是真正的构建目标,则可能会使 make 工具误以为已经构建完毕。这种问题可以通过使用特殊内置构件目标名称来解决,参见下边的 2.2.3。

2 Makefile 编写规则

2.1 负责执行构建的代码部分

  构建部分代码的编写规则的结构十分清晰,分别为:构建目标、构建依赖、构建命令。这部分应该是 Makefile 中最主要的部分。如下所示:

target:  dependency1 dependency2 ...
<tab> command

即

构建目标: 构建依赖1 构建依赖2 ...
	构建命令
2.1.1 构建目标

  这里的 target 即为构建目标,可以理解为构建目标的名字或标签,(有些地方说的 target_label,也指代构建目标),它可以直接是操作目标,也可以是一些操作的名字,此时称为伪目标(phony target)。

2.1.2 构建依赖

  dependency 则是生成目标文件所需要的依赖,也可以称为前置条件,这里的依赖,可以是文件,也可以是其他的构建目标,也可以是空的,即不需要依赖(如clean)。

2.1.3 构建命令

  command 为生成目标文件所需要执行的 shell 命令, shell 命令这里也可以是空的,即不需要执行命令。这种情况通常出现在一些构建目标为伪目标的时候。
  注意:这里的构建命令 command 一行的 shell 命令前边有一个制表符 Tab,这个是 Makefile 正常工作所必须的。制表符是 Makefile 文件中默认的 .RECIPEPREFIX,如果我们想用使用其他的命令前缀,需要我们自己指定,如:

# 使用 '>' 作为命令前缀
.RECIPEPREFIX = >

target:
> echo "This is a shell command"

  除此之外,在 shell 命令中,我们需要注意的一点是,shell 命令可以有多行,每一行代表一个单独的 shell 环境。在开发过程中可能会遇到这种情况,前边一行 shell 命令进入到一个目录中,在下一行的 shell 中获取当前目录路径,会发现,仍然会回到 Makefile 当前的文件路径。如以下 Makefile 文件:

test:
	cd test && pwd
	pwd

  它对应的输出为:

cd test && pwd
/home/debris/workspace/make_file/test
pwd
/home/debris/workspace/make_file

  还有一种情况就是,前边一条 shell 命令获取的结果或者输出,新一行中的 shell 命令是无法直接使用的。这些情况可能会给开发者带来一些不便。为解决这些问题,开发者可以将这些不同的 shell 写在同一行中,不同的 shell 命令之间用 ; 分隔开。这时候可能会出现一行 shell 命令太多而不美观的问题,使用 shell 常用的换行方式 \ 即可。

2.2 Makefile 文件语法

  来介绍一下 Makefile 中一些基础语法。

2.2.1 注释

   Makefile 文件中的注释方式与 shell 相同,使用 # 表示注释。
   注意:在对构建目标进行构建时,终端会先逐行输出 shell 命令区域的内容,之后再执行改行的 shell 命令,称之为 echoing 。即便是注释,也会在终端输出。如果不想输出这些内容,则需要将这些注释或者命令以 @ 开头

2.2.2 通配符、模式匹配

   Makefile 文件中的通配符与 Bash 中的一致,有 *?[...]等。
   Make 命令允许对文件名,进行类似正则运算的匹配,主要用到的匹配符是%。如需要将 .c 文件编译为 .0 文件,构建目标和构建依赖可以这样写:

%.o: %.c
2.2.3 变量和赋值符号、赋值运算符

  在 Makefile 中我们可以使用等号自定义变量。在使用变量时,需要使用 $() 将其包裹起来。如果需要调用 shell 中的变量,比如需要用到 shell 中的一些环境变量 HOME,需要使用 $$HOME, shell 中使用时用的是 $HOME, 这里需要在其前边再加一个 $,因为 make 工具会将 $ 转义。
   Makefile 提供了四个赋值运算符:=(Lazy Set)、:=(Immediate Set)、?=(Lazy Set If Absent)、+=(Append)。

VARIABLE = value
# 在执行时扩展,允许递归扩展。
# Normal setting of a variable, but any other variables mentioned with the value field are recursively expanded with their value at the point at which the variable is used, not the one it had when it was declared

VARIABLE := value
# 在定义时扩展。
# Setting of a variable with simple expansion of the values inside - values within it are expanded at declaration time.

VARIABLE ?= value
# 只有在该变量为空时才设置值。
# Setting of a variable only if it doesn't have a value. value is always evaluated when VARIABLE is accessed.

VARIABLE += value
# 将值追加到变量的尾端。
# Setting of a variable only if it doesn't have a value. value is always evaluated when VARIABLE is accessed.
2.2.3 内置变量、自动变量、特殊内置变量

  内置变量参考这里 。 如 RM 为 shell 命令的 rm -f
   Makefiel 中提供一些自动变量,其值与当前规则有关,如:

  • $@ 指代当前的构建目标
  • $< 规则中先决文件(依赖文件)的名称
  • $@ 规则中目标文件的名称

  具体参考这里
  特殊的内置变量(也叫特殊内置构建目标名称),如下 2.3 中将提到的 .PHONY ,作用为不管当前文件夹下是否有于构建目标名称相同的文件或者目录里,进行 make 时都会执行对应的 shell 命令。更多特殊内置变量参考这里

2.2.4 判断、循环、函数

   Makefile 中的判断和循环语法与 Bash 相同, Makefile 中也可以使用 shell 中的函数。 Makefile 中还有一些其他的函数,如:

  • wildcard 函数,替换 Bash 的通配符
  • subst 文本替换函数,替换格式为 (subst 被替换字符(串), 替换后的字符(串), 替换目标列表)
  • patsubst 模式匹配替换,替换格式(patsubst 想要替换的模式串,替换后的模式串,替换目标)
  • 替换后缀名 如将 SRCS 中文件名的 .c 替换为 .o: OBJS = $(SRCS:.c=.o)
2.3 多个构建目标(target)

  在执行 make 命令时,实际上 make 工具只会对 Makefile 文件中的第一个构建目标进行构建。如果我们设置了多个构建目标,即 Makefile 中 target 有多个。你会发现,执行 make 命令时,只会执行第一个构建目标对应的 shell 命令,其余的都不会执行。如何解决这个问题呢?
  前边说过,构建依赖也可以是构建目标,这样的话,我们可以再添加一个构建目标,用来控制自动化构建时执行其他哪些构建目标,并将其设置为 Makefile 文件中第一个构建目标。构建目标的名称通常是任意的,但是第一个构建目标,最常用的名字为 default 或 all。
  这自然而然也就解决了另一个问题:在执行 make 构建项目时,我们可能只想执行其中一部分,而不是所有。比如我们为清理工作设置的构建目标 clean 以及其他一些为特殊需要设置的构建目标,这些在项目自动化构建时,我们可能不需要执行。选择性的将自动化构建时需要的构建目标添加到第一项构建目标中,其余的通过单独执行 make target来实现特殊功能的构建。

2.4 使用 makedepend 和更高级的 make 语法

   make depend 用 makedepend 来自动产生依赖,将这些依赖添加到 Makefile 文件的末尾。这里是使用 makedepend 的 Makefile 示例。

# 以下这部分 makefile 是通用的,仅仅通过修改其之前的一些宏或其他定义和删除'make depend'中附加到文件中的依赖项后即可用来构建任何可执行文件

# 指定编译器
CC = gcc

# 定义编译时的一些标志
CFLAGS = -Wall -g

# 定义除了 /usr/include 外的其他包含头文件的路径
INCLUDES = -I/home/newhall/include  -I../include

# 定义 /usr/lib 外的动态库路径
LFLAGS = -L/home/newhall/lib  -L../lib

# 指定需要链接的动态库
LIBS = -lmylib -lm

# 指定 C 源文件
SRCS = emitter.c error.c init.c lexer.c main.c symbol.c parser.c

# 宏内使用后缀替换
#   $(name:string1=string2)
#         将 name 中的每个 string1 替换为 string2
# 下边将 SRCS 中文件名的 .c 替换为 .o
#
OBJS = $(SRCS:.c=.o)

# 指定可执行文件名称 
MAIN = mycc

.PHONY: depend clean

all:    $(MAIN)
        @echo  Simple compiler named mycc has been compiled

$(MAIN): $(OBJS) 
        $(CC) $(CFLAGS) $(INCLUDES) -o $(MAIN) $(OBJS) $(LFLAGS) $(LIBS)

# 这是一个从 .c 文件构建 .o 文件的后缀替换,它使用自动变量 $< 和 $@。
# $< 规则中先决文件(依赖文件)的名称,这里是指 .c 文件
# $@ 规则中目标文件的名称,这里是指 .o 文件
# (see the gnu make manual section about automatic variables)
.c.o:
	$(CC) $(CFLAGS) $(INCLUDES) -c $<  -o $@

# 删除所有的 .o 文件以及
clean:
        $(RM) *.o *~ $(MAIN)

depend: $(SRCS)
        makedepend $(INCLUDES) $^



# 这一行不可删除, makedepend 需要
# DO NOT DELETE THIS LINE -- make depend needs it

3 示例

  话不多…话已经说了很多,接下来看一个栗子。
  准备三个源文件以及两个头文件,分别如下:

  • add.h
/* add 函数的声明 */
int add(int x, int y);
  • sub.h
/* sub 函数的声明 */
int sub(int x, int y);
  • add.cpp
/* add 函数的实现 */
#include "add.h"

int add(int x, int y){
  return x + y;
}
  • sub.cpp
/* sub 函数的实现 */
#include "sub.h"

int sub(int x, int y){
  return x - y;
}
  • main.cpp
/* 主函数中调用 add 和 sub 函数 */
#include <stdio.h>
#include "add.h"
#include "sub.h"
int main(){
  int a = 10;
  int b = 5;
  int c = add(a, b);
  int d = sub(a, b);
  printf("a + b = %d\n", c);
  printf("a - b = %d\n", d);
  return 0;
}

   Makefile 的内容如下所示

# Makefile 中的注释,和shell的注释相同

# 指定编译器为 g++
CC = g++

# 第一个构建目标,其构建依赖为其他的部分构建目标,这里不包括 clean 和 test
first_target:compile_add compile_sub compile_main link

# 这里若不指定输出的中间文件名,则会默认使用 add.o
compile_add: add.cpp
	$(CC) -c add.cpp -o this_add.o

compile_sub: sub.cpp
	$(CC) -c sub.cpp -o this_sub.o

compile_main: main.cpp
	$(CC) -c main.cpp -o main.o

# 前边三个编译过程,以及这一条连接过程,实际可以用一条命令代替:$(CC) main.cpp add.cpp sub.cpp -o main
# 这里将其分开,是为了更好理解 makefile 的编写规则
link: this_add.o this_sub.o main.o
	$(CC) -o main main.o this_add.o this_sub.o

#  使用特殊内置构建目标名称
.PHONY: clean test

# 具备清理功能的构建目标,它没有构建依赖,有多个 shell 命令
clean:
	$(RM) this_add.o
	$(RM) this_sub.o
	$(RM) main.o
	$(RM) main

# 具备测试相关的构建目标,这里只是一个示例,开发者可以根据自己需要定义构建目标
test:
	# 这里是测试
	@# 这里也是测试,这一行在执行 make test 时不会打印到屏幕
	./main

  执行 make 命令,终端输出如下:

g++ -c add.cpp -o this_add.o
g++ -c sub.cpp -o this_sub.o
g++ -c main.cpp -o main.o
g++ -o main main.o this_add.o this_sub.o

  执行 make clean 命令,终端输出如下:

rm -f this_add.o
rm -f this_sub.o
rm -f main.o
rm -f main

  执行 make test 命令,终端输出如下:

# 这里是测试
./main
a + b = 15
a - b = 5

4 总结

  现在回顾一下, Makefile 似乎也没有之前想象的那么复杂,不过是定义一套规则。 Makefile 基础语法看懂之后,仍感觉一个 Makefile 文件复杂难懂,那多半是 shell 命令部分较为复杂的问题了。

参考文章
GUN make
Using make and writing Makefiles
MakefileHowto
Makefile教程
make 使用教程

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/StoryZX/article/details/123988696

【linux】 自动化构建工具-make/makefile_yytengjian的博客-爱代码爱编程

        一个工程中的源文件不计其数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为 makefile就像一个shell脚本一样,其中也可以执行操作系统的命令。Makefile 文件描述了整个工程的编译、连接等规则。其中

golang 构建工具之 makefile_hatlonely的博客-爱代码爱编程

可能是因为编译太简单了,golang 并没有一个官方的构建工具(类似于 java 的 maven 和 gradle之类的),但是除了编译,我们可能还需要下载依赖,运行测试,甚至像 easyjson,protobuf,thri

makefile工具_alatebloomer的博客-爱代码爱编程

简介 1)make:利用 make 工具可以自动完成编译工作。这些工作包括:如果仅修改了某几个源文件,则只重新编译这几个源文件[make通过比对相应的.c文件与.o文件的时间];如果某个头文件被修改了,则重新编译所有包含该头文件的源文件。利用这种自动编译可大大简化开发工作,避免不必要的重新编译。 2)Mackfile:make工具通过一个称为 Mack

VSC新东西:Makefile工具扩展-爱代码爱编程

官宣 今天,我们非常高兴地宣布Visual Studio Code中的一项全新扩展:Makefile工具(预览版),此工具主要用于在Visual Studio Code集成开发环境中构建和调试Makefile工程。目前,此扩展还处于测试阶段,但是,我们内部测试了70多个流行开源的Makefile工程,显示出此扩展工具可以很好地和它们一起工作。那大家有兴趣

入门系列:编译过程-第4篇-make工具与Makefile文件概念-爱代码爱编程

说明:   本文章旨在总结备份、方便以后查询,由于是个人总结,如有不对,欢迎指正;另外,内容大部分来自网络、书籍、和各类手册,如若侵权请告知,马上删帖致歉。   QQ 群 号:513683159 【相互学习】内容来源:   C语言中文网   GNU make中文手册 ver-3.8 文章内容:   make与Makefile相关介绍。   Makefi

Makefile工具使用-爱代码爱编程

人们通常利用 make 工具来自动完成编译工作。这些工作包括:如果仅修改了某几个源文件,则只重新编译这几个源文件;如果某个头文件被修改了,则重新编译所有包含该头文件的源文件。利用这种自动编译可大大简化开发工作,避免不必要的重新编译。 Makefile make 工具通过一个称为 makefile 的文件来完成并自动维护编译工作。makefil

make工具和Makefile的使用-爱代码爱编程

一、make工具和Makefile文件的引入 &emsp 当源码文件比较多的时候就不适合通过直接输入gcc命令来编译,这时候就需要一个自动化的编译工具。 make:一般说GNU Make,是一个软件,用于将源代码文件编译为可执行的二进制文件,make工具主要用于完成自动化编译。make工具编译的时候需要Makefile文件提供编译文件。 Make

Linux中make工具及makefile文件-爱代码爱编程

一、make工具简介 1、make 工具通过一个称为 makefile 的文件(类似脚本)来完成并自动维护编译工作,针对目标(可执行文件)进行依赖性检测(要生成该可执行文件之前要有哪些中间文件)并执行相关动作(编译等)的工具 。其中makefile中内容包含make所要进行的处理动作以及依赖关系。 2、使用make 的其他好处:如果仅修改了某几个源文件