长生栈 长生栈
首页
  • 编程语言

    • C语言
    • C++
    • Java
    • Python
  • 数据结构和算法

    • 全排列算法实现
    • 动态规划算法
  • CMake
  • gitlab 安装和配置
  • docker快速搭建wordpress
  • electron+react开发和部署
  • Electron-创建你的应用程序
  • ImgUI编译环境
  • 搭建图集网站
  • 使用PlantUml画时序图
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Living Team

编程技术分享
首页
  • 编程语言

    • C语言
    • C++
    • Java
    • Python
  • 数据结构和算法

    • 全排列算法实现
    • 动态规划算法
  • CMake
  • gitlab 安装和配置
  • docker快速搭建wordpress
  • electron+react开发和部署
  • Electron-创建你的应用程序
  • ImgUI编译环境
  • 搭建图集网站
  • 使用PlantUml画时序图
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Cmake 基本使用
  • Linux和Windows常用命令对比
  • Nginx的使用方法
  • Bash常用语法
  • 使用PlantUml画时序图
  • 使用PlantUML画类图
  • 编译工具链
  • gcc工具的概述和使用
  • 自动化构建工具——Make/Makefile
    • make和Makefile简介
    • Makefile的工作原理分析
    • Makefile操作规则
    • Makefile的规则格式
    • 一个简单的Makefile
    • 伪目标(PHONY Target)
      • 使用伪目标all构建多个程序
    • 变量
      • 一个例子
      • 自定义变量
      • 自动化变量
      • 预定义变量
    • 函数
    • 一个复杂一点的Makefile
    • 参考文献
  • 使用开源AI模型Whisper为视频生成字幕
  • MySQL基础操作
  • 工具
DC Wang
2023-09-03
目录

自动化构建工具——Make/Makefile

# 自动化构建工具——Make/Makefile

Makefile 是一种强大且灵活的构建工具,具有广泛的适用性,使得软件构建过程更加高效和可管理。

# make和Makefile简介

Makefile是一个配置文件,可以对程序编译进行管理,其优点是:

  • 避免复杂命令行编译语句
  • 减少编译所需时间
  • 让编译自动运行

make是解释Makefile文件中指令的命令工具。

# Makefile的工作原理分析

                +--------------+
                | myprogram    |
                +------+-------+
                       |
           +-----------+-----------+
           |                       |
           v                       v
    +-------------+           +------------+
    | main.o      |           | util.o     |
    +------+------+           +------------+
           |                        |
           |                        |
           v                        v
    +-------------+           +------------+
    | main.c      |           | util.c     |
    +-------------+           +------------+

    +-------------+
    | main.h      |
    +-------------+

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

这个示意图展示了myprogram作为最终的目标,它依赖于main.o和util.o。main.o和util.o各自依赖于相应的.c文件和main.h头文件。

当make命令运行时,它会检查每个文件的时间戳以确定哪些文件需要重新编译。根据文件的时间戳,它会选择性地重新编译main.c和util.c,然后重新链接它们以生成最终的myprogram可执行文件,而不必重新编译所有文件。

  • 结论:Makefile确保了只更新发生变化的部分,提高了构建效率。

# Makefile操作规则

Makefile的操作规则

  • 如果工程没有编译过,所有的C文件都要编译并被连接。
  • 如果工程的某几个文件被修改,只需要编译被修改的这几个文件,并重新链接目标程序
  • 如果工程的头文件被修改了,那么所有包含此头文件的源文件都要重新编译,并重新连接目标程序

# Makefile的规则格式

target … : prerequisites …
        recipe
        …
        …
1
2
3
4

target是目标文件,可以是.o中间目标文件、可执行文件或者标签。

prerequisites(前提条件)是要生成目标文件所依赖的文件。

recipe是(配方)生成规则,recipe前面要有一个Tab符号(不能用空格代替)。

# 一个简单的Makefile

这是一个简单的 makefile,描述了名为 edit 的可执行文件如何依赖于八个目标文件,而这些目标文件又依赖于八个 C 源文件和三个头文件。

在此示例中,所有 C 文件都包括 defs.h,但只有那些定义编辑命令的文件包括 command.h,并且只有更改编辑器buffer的底层文件包括 buffer.h。

edit : main.o kbd.o command.o display.o \
       insert.o search.o files.o utils.o
        cc -o edit main.o kbd.o command.o display.o \
                   insert.o search.o files.o utils.o

main.o : main.c defs.h
        cc -c main.c
kbd.o : kbd.c defs.h command.h
        cc -c kbd.c
command.o : command.c defs.h command.h
        cc -c command.c
display.o : display.c defs.h buffer.h
        cc -c display.c
insert.o : insert.c defs.h buffer.h
        cc -c insert.c
search.o : search.c defs.h buffer.h
        cc -c search.c
files.o : files.c defs.h buffer.h command.h
        cc -c files.c
utils.o : utils.c defs.h
        cc -c utils.c
clean :
        rm edit main.o kbd.o command.o display.o \
           insert.o search.o files.o utils.o
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  • 使用此 makefile 创建名为 edit 的可执行文件

    make
    
    1

    Makefile中只有第一个目标是默认目标。上述例子中为'edit',当我们执行"make"命令的时候,默认最终构建'edit'可执行文件,我们可以使用.DEFAULT_GOAL来覆盖这种行为。

  • 使用此 makefile 从目录中删除可执行文件和所有目标文件

    make clean
    
    1

    目标'clean'不是一个文件,只是一个动作的名称。由于通常不希望执行此规则中的操作,'clean'不是任何目标的前提条件。因此,除非明确告诉它,否则make不会执行任何与它有关的操作。

  • 当一个目标是一个文件时,如果它任意一个前提条件发生变化,它就需要重新编译或重新链接。此外,任何自动生成的前提条件应该首先更新。在这个示例中,'edit'依赖于八个目标文件;目标文件'main.o'依赖于源文件'main.c'和头文件'defs.h'。如果'main.c'更新,那么先更新'main.o',然后更新'edit'。

# 伪目标(PHONY Target)

伪目标是一个实际上不是文件名称的目标;它只是一个名称,用于在明确请求时执行一个配方。使用伪目标有两个原因:避免与同名文件冲突和提高性能。

如果编写了一个规则,其配方不会创建目标文件,那么每当需要重新生成目标时,该配方都将被执行。以下是一个示例:

clean:
        rm *.o temp
1
2

因为 rm 命令不会创建名为 clean 的文件,所以可能永远不会存在这样的文件。因此,每次输入“make clean”时都会执行 rm 命令。

在上面这个示例中,如果在此目录中创建了一个名为clean的文件,clean目标将无法正常工作。由于它没有前提条件(prerequisites),clean将始终被视为最新,并且其配方不会被执行。为了避免这个问题,可以使用.PHONY将clean声明为伪目标,如下所示:

.PHONY: clean
clean:
        rm *.o temp
1
2
3

现在,无论是否存在名为 clean 的文件,“make clean”都将运行rm *.o temp。

一个伪目标不应该是一个真实目标文件的前提条件。

# 使用伪目标all构建多个程序

all : prog1 prog2 prog3
.PHONY : all

prog1 : prog1.o utils.o
        cc -o prog1 prog1.o utils.o

prog2 : prog2.o
        cc -o prog2 prog2.o

prog3 : prog3.o sort.o utils.o
        cc -o prog3 prog3.o sort.o utils.o
1
2
3
4
5
6
7
8
9
10
11

当一个目录包含多个程序时,最方便的做法是在一个Makefile中描述所有程序。由于Makefile中只有第一个目标是默认目标,因此通常将其作为名为'all'的伪目标,并将所有单独的程序作为前提条件。

# 变量

Makefile中可以使用变量,且有以下特点:

  • 变量类似于C语言的宏,但值可修改
  • 变量名大小写敏感
  • 变量名不应该包含:#=或空格
  • 变量使用时用$(var)形式

# 一个例子

在makefile中定义变量的最简单方式是使用等号(=)运算符。例如,将命令gcc分配给变量CC:

CC = gcc
1

这也被称为递归扩展变量,它在规则中的使用如下所示:

hello: hello.c
    ${CC} hello.c -o hello
1
2

正如您可能已经猜到的那样,当这个规则传递给终端时,配方会展开如下:

gcc hello.c -o hello
1

${CC}和$(CC)都是用于调用gcc的有效引用方式。但是,如果尝试将变量重新分配给自身,这将导致无限循环。让我们验证一下:

CC = gcc
CC = ${CC}

all:
    @echo ${CC}
1
2
3
4
5

运行make会产生以下结果:

$ make
Makefile:8: *** Recursive variable 'CC' references itself (eventually).  Stop.
1
2

为了避免这种情况,我们可以使用:=运算符(这也被称为简单扩展变量)。我们可以正常运行以下makefile:

CC := gcc
CC := ${CC}

all:
    @echo ${CC}
1
2
3
4
5

# 自定义变量

赋值方式 示例 说明
基本赋值 VARIABLE_NAME = some_value 基本赋值使用等号(=)分配变量值。
VARIABLE_NAME := new_value 冒号等号(:=)分配变量值,覆盖之前的值。
递归赋值 VARIABLE_NAME = $(OTHER_VAR) 递归赋值使用等号(=),支持延迟求值。
VARIABLE_NAME := $(OTHER_VAR) 冒号等号(:=)分配变量值,立即求值。
条件赋值 VARIABLE_NAME ?= default_value 条件赋值只在变量未定义时才分配值。
追加赋值 VARIABLE_NAME += appended_value 追加值到现有变量。

# 自动化变量

变量符号 描述 示例
$@ 当前目标的名称,例如:target: dependencies 中的target。 all: myprogram
$@在这里代表myprogram
$< 第一个依赖项的名称,例如:target: dependencies 中的dependencies 中的第一个文件。 myprogram: main.c util.c
$<在这里代表main.c
$^ 所有依赖项的名称,以空格分隔,例如:target: dependencies 中的所有依赖项。 myprogram: main.c util.c other.c
$^在这里代表main.c util.c other.c
$? 所有比目标文件新的依赖项的名称,以空格分隔。 myprogram: main.c util.c other.c
$?在这里代表比myprogram更新的依赖项

# 预定义变量

预定义变量 描述
CC C编译器的名称,默认为cc。
CFLAGS C编译器的标志,通常包括编译选项和警告标志。
LDFLAGS 链接器的标志,通常包括库路径和库文件。
LDLIBS 链接器的库文件列表。
RM 删除文件的命令,默认为rm -f。
AR 归档工具的名称,默认为ar。
ARFLAGS 归档工具的标志。
MAKE 调用make的命令,默认为make。
MAKEFLAGS 包含make命令行选项的变量。
SHELL Shell的名称,默认为/bin/sh。

这些预定义变量在Makefile中提供了有关编译器、链接器、命令和其他构建相关信息的访问。可以在Makefile中使用它们来自定义构建规则和行为。

# 函数

以下是一些常见的Makefile函数,包括示例,以表格形式列出:

函数 描述 示例
$(subst from,to,text) 在文本中将字符串from替换为字符串to。 $(subst old,new,hello world) 返回 hello new world
$(patsubst pattern,replacement,text) 使用模式匹配替换文本中的部分字符串。 $(patsubst %.c,%.o,main.c util.c) 返回 main.o util.o
$(strip string) 去掉字符串中的前导和尾随空格。 $(strip leading and trailing spaces ) 返回 leading and trailing spaces
$(findstring find,in) 检查字符串find是否存在于字符串in中。 $(findstring world,hello world) 返回 world
$(filter pattern...,text) 从文本中选择符合给定模式的部分。 $(filter %.c %.cpp,main.c util.cpp other.o) 返回 main.c util.cpp
$(wildcard pattern) 返回匹配指定模式的文件列表。 $(wildcard *.c) 返回所有以.c为扩展名的文件列表
$(shell command) 执行shell命令并返回其输出。 $(shell date) 返回当前日期和时间
$(foreach var,list,text) 对列表中的每个元素执行指定的文本操作。 $(foreach file,a.c b.c,$(file:%.c=%.o)) 返回 a.o b.o
$(if condition,then-part[,else-part]) 根据条件执行不同的操作。 $(if $(DEBUG),-g,-O2) 返回 -g 如果 DEBUG 变量已定义,否则返回 -O2
$(call function,args...) 调用自定义函数并传递参数。 自定义函数示例:define myfunc
echo $(1) $(2)
endef
$(call myfunc,arg1,arg2) 返回 arg1 arg2

上面演示了如何在Makefile中使用这些常见函数来处理文本、执行条件判断、执行外部命令以及定义和调用自定义函数。这些函数是Makefile中的强大工具,可用于自动化构建任务。

# 一个复杂一点的Makefile

# Usage:
# make        # 编译所有二进制文件
# make clean  # 删除所有二进制文件和对象文件

.PHONY = all clean

CC = gcc			# 使用的编译器

LINKERFLAG = -lm

SRCS := $(wildcard *.c)
BINS := $(SRCS:%.c=%)

all: ${BINS}

%: %.o
	@echo "Checking.."
	${CC} ${LINKERFLAG} $< -o $@

%.o: %.c
	@echo "Creating object.."
	${CC} -c $<

clean:
	@echo "Cleaning up..."
	rm -rvf *.o ${BINS}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  • 以#开头的行是注释。

  • .PHONY = all clean 定义了伪目标 all 和 clean。

  • 变量LINKERFLAG定义了在一个规则中与gcc一起使用的标志。

  • SRCS :=$(wildcard *.c):$(wildcard pattern)是用于文件名的一个函数。在这种情况下,所有具有 .c 扩展名的文件将被存储在变量 SRCS 中。

  • BINS := $(SRCS:%.c=%):这称为替代引用。在这种情况下,如果 SRCS 具有值'foo.c bar.c',BINS将具有 'foo bar'。

  • all: ${BINS}:伪目标 all 调用${BINS}中的值作为单独的目标。

  • 规则:

    %: %.o
      @echo "Checking.."
      ${CC} ${LINKERFLAG} $<; -o $@
    
    1
    2
    3

    让我们看一个示例来理解这个规则。假设 foo 是${BINS} 中的一个值。那么 %将与 foo 匹配(% 可以匹配任何目标名称)。以下是该规则的展开形式:

    foo: foo.o
      @echo "Checking.."
      gcc -lm foo.o -o foo
    
    1
    2
    3

    %被替换为 foo。$<被替换为 foo.o。$< 被模式匹配以匹配前提条件,$@ 匹配目标。此规则将针对${BINS} 中的每个值调用。

  • 规则:

    %.o: %.c
      @echo "Creating object.."
      ${CC} -c $<;
    
    1
    2
    3

    上一条规则中的每个前提条件(prerequisites)都被视为此规则的目标(target)。以下是该规则的扩展形式:

    foo.o: foo.c
      @echo "Creating object.."
      gcc -c foo.c
    
    1
    2
    3
  • Makefile的最后,我们移除了目标 clean 中的所有二进制文件和对象文件。

以下是上述 Makefile 的重写,假设它位于只有一个文件 foo.c 的目录中:

# Usage:
# make        # 编译所有二进制文件
# make clean  # 删除所有二进制文件和对象文件

.PHONY: all clean

CC = gcc			# 使用的编译器

LINKERFLAG = -lm

SRCS := foo.c
BINS := foo

all: foo

foo: foo.o
	@echo "Checking.."
	gcc -lm foo.o -o foo

foo.o: foo.c
	@echo "Creating object.."
	gcc -c foo.c

clean:
	@echo "Cleaning up..."
	rm -rvf foo.o foo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 参考文献

有关makefile的更多信息,请参阅 GNU Make 手册PDF版 (opens new window) 或 GNU Make 手册在线 (opens new window),其中提供了完整的参考和示例。

编辑 (opens new window)
#编译#Linux
上次更新: 2023/09/03, 14:05:06
gcc工具的概述和使用
使用开源AI模型Whisper为视频生成字幕

← gcc工具的概述和使用 使用开源AI模型Whisper为视频生成字幕→

最近更新
01
ESP32-网络摄像头方案
06-14
02
ESP32-PWM驱动SG90舵机
06-14
03
ESP32-实时操作系统freertos
06-14
更多文章>
Theme by Vdoing | Copyright © 2019-2025 DC Wang All right reserved | 辽公网安备 21021102001125号 | 吉ICP备20001966号-2
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式