Linux基础——Makefile编写优化(一)

简介: 本文主要是为了讲述如何能够编写更专业的Makefile,而不是仅仅通过一项简单的依赖规则生成几个固定目标。(重点参考李云老师编写的《专业嵌入式软件开发——全名走向高质量高效编程》一书,该书是我在图书馆偶然看到,发现原来长期一直使用的Makefile中居然还有这么多需要注意的细节,所以特地整理记录下来。)

本文主要是为了讲述如何能够编写更专业的Makefile,而不是仅仅通过一项简单的依赖规则生成几个固定目标。(重点参考李云老师编写的《专业嵌入式软件开发——全名走向高质量高效编程》一书,该书是我在图书馆偶然看到,发现原来长期一直使用的Makefile中居然还有这么多需要注意的细节,所以特地整理记录下来。)

文章目录

Makefile,开发环境全能管家

基本规则

假目标的用处

运用“变量”提高Makefile可维护性

自动变量

特殊变量

变量的类型与赋值

变量及其值的来源

避免变量被覆盖的方法

借助“模式”精简规则

通过“函数”增强功能

abspath函数

addprefix函数

addsuffix函数

eval函数

filter函数

filter-out函数

notdir函数

patsubst函数

strip函数

wildcard函数

提高编译环境的实用性

让编译环境更加有序

目录的自动创建与删除

通过目录管理文件

提升依赖关系管理

自动生成文件依赖关系

使用依赖关系文件

为依赖关系文件建立依赖关系

打造更专业的编译环境

规划项目目录结构

增进复用性

支持头文件目录的指定

实现库链接

增加一个bar模块

增强可使用性

管理对库的依赖关系

总结


Makefile,开发环境全能管家

Linux 环境下的程序员如果不会使用GNU make来构建和管理自己的工程,应该不能算是一个合格的专业程序员,至少不能称得上是 Unix程序员。在 Linux(unix )环境下使用GNU 的make工具能够比较容易的构建一个属于你自己的工程,整个工程的编译只需要一个命令就可以完成编译、连接以至于最后的执行。不过这需要我们投入一些时间去完成一个或者多个称之为Makefile 文件的编写。

所要完成的Makefile 文件描述了整个工程的编译、连接等规则。其中包括:工程中的哪些源文件需要编译以及如何编译、需要创建哪些库文件以及如何创建这些库文件、如何最后产生我们想要的可执行文件。尽管看起来可能是很复杂的事情,但是为工程编写Makefile 的好处是能够使用一行命令来完成“自动化编译”,一旦提供一个(通常对于一个工程来说会是多个)正确的 Makefile。编译整个工程你所要做的事就是在shell 提示符下输入make命令。整个工程完全自动编译,极大提高了效率。

make是一个命令工具,它解释Makefile 中的指令。在Makefile文件中描述了整个工程所有文件的编译顺序、编译规则。Makefile 有自己的书写格式、关键字、函数。像C 语言有自己的格式、关键字和函数一样。而且在Makefile 中可以使用系统shell所提供的任何命令来完成想要的工作。Makefile在绝大多数的IDE 开发环境中都在使用,已经成为一种工程的编译方法。


基本规则

基本规则

让我们先来粗略地看一看Makefile的规则。


target ... : prerequisites ...
  command
  ...
  ...


以上是Makefile的基本规则,解释如下:

目标:依赖

执行指令 …

target也就是一个目标文件,可以是Object File,也可以是执行文件。还可以是一个标签(Label)。

① prerequisites就是,要生成那个target所需要的文件或是目标。

② command也就是make需要执行的命令。(任意的Shell命令)

这里关于最基本的Makefile就不详细解释了,执行make命令后Makefile会首先生成描述的第一个目标,查看依赖是否成立,如果依赖不存在再去依次生成依赖。。。。最终成功生成第一个目标。


假目标的用处

假目标采用.PHONY关键字来定义,注意它必须是大写字母,通常最基本会将clean修改为假目标,这样避免了当前文件下存在“clean”同名的文件导致make执行出现偏差。如下:


.PHONY: clean
simple: main.o foo.o
  gcc -o simple main.o foo.o
main.o : main.c
  gcc -o main.o -c main.c
foo.o : foo.c
  gcc -o foo.o -c foo.c
clean:
  rm -rf simple main.o foo.o


  • 由于假目标并不会与文件相关联,所以每次构建假目标时它所在的规则中的命令都一定会被执行,也就是上面每次执行“make clean”都会执行文件清除操作。

运用“变量”提高Makefile可维护性

编写专业的Makefile同样离不开运用变量,通过使用变量可以使得Makefile更具有可维护性。如下:


.PHONY:clean
CC = gcc
RM = rm
EXE = simple
OBJS = main.o foo.o
$(EXE) : $(OBJS)
  $(CC) -o $(EXE) $(OBJS)
main.o : main.c
  $(CC) -o main.o -c main.c
foo.o : foo.c
  $(CC) -o foo.o -c foo.c
clean:
  $(RM) -rf $(EXE) $(OBJS)

20200102080545715.png

  • 这里,我们定义了CC,RM,EXE,OBJS四个变量,定义变量时其值可以为空(即无右值),引用变量需要采用“( 变 量 名 ) ” 或 者 “ (变量名)”或者“(变量名)”或者“{变量名}”的形式。
  • 引用变量的好处很明显,比如进入CC变量以后,如果需要修改编译器,只需要修改赋值变量这一处即可。Makefile中变量的数据类型可以理解为C语言中的字符串。

自动变量

在上面的Makefile中,存在目标名和先决条件在规则的命令中重复出现。如果目标名或者先决条件名发生改变,那得在相应的命令中都去修改,这很麻烦,为了省去这种麻烦,我们可以借助以下自动变量。


  • $@:用于表示一个规则中的目标,当一个规则中有多个目标时,指其中任何造成规则命令被运行的目标。
  • $^:表示的是规则中的所有先决条件。
  • $<:表示的是规则中的第一个先决条件。
.PHONY : all
all: first second third
  @echo "\$$@ = $@"
  @echo "$$^ = $^"
  @echo "$$< = $<"
first second third:

2020010208041738.png

除了这三个自动变量外,在Makefile中还可以使用其他的自动变量,后面我们会说到,以上三个自动变量是最常用的。使用上述自动变量来简化之前的Makefile:


.PHONY:clean
CC = gcc
RM = rm
EXE = simple
OBJS = main.o foo.o
$(EXE) : $(OBJS)
  $(CC) -o $@ $^
main.o : main.c
  $(CC) -o $@ -c $^
foo.o : foo.c
  $(CC) -o $@ -c $^
clean:
  $(RM) -rf $(EXE) $(OBJS)


特殊变量

在Makefile中,有两个特殊变量会经常用到:MAKE和MAKECMDGOALS。

MAKE:表示当前处理Makefile的命令名是什么。

MAKECMDGOALS:表示是是当前构建的目标名。

如下:


.PHONY = all clean
all clean:
  echo "MAKE = $(MAKE)"
  echo "MAKECMDGOALS = $(MAKECMDGOALS)"

20200104074240263.png

  • 从结果上看,MAKECMDGOALS变量指的是用户输入的目标,当只允许make命令不带参数时,根据Makefile的语法规则会将Makefile中的第一个目标作为默认目标,即上面的all目标,但是MAKECMDGOALS却仍是空而不是“all”。
  • 另外,从结果中能看到,运行make时可以同时指定多个目标,make在获得了多个目标后,将从左往右依次地构建目标。

变量的类型与赋值

变量的类型有递归扩展变量和简单扩展变量。

最简单的方式是使用“=”进行变量的定义和赋值,这种只用一个“=”符号定义的变量被称为递归扩展变量。如下:


  • 递归扩展变量
.PHONY = all
foo = $(bar)
bar = $(ugh)
ugh = HUH?
all:
  @echo $(foo)

20200104074919719.png

这种递归扩展变量最需要注意的一点就是要防止对变量进行循环扩展,容易造成一个死循环。


  • 简单扩展变量

简单扩展变量是用 “ := ”操作符来定义的。这种变量,make只会对其进行一次展开,如下示例:

.PHONY = all
x = foo
y = $(x) b
x = later
xx := foo
yy := $(xx) b
xx := later
all:
  @echo "x = $(y), xx = $(yy)"

20200104075439247.png

  • 另外,在Makefile中还可以实现条件赋值,“?=”操作符实现,当变量没有被定义时就定义它,并且将右边的值赋值给它;如果变量已经定义了,则不改变其原值。条件赋值操作可以用于为变量赋值默认值。
.PHONY = all
foo = x
foo ?= y
bar ?=y
all:
  @echo "foo=$(foo), bar=$(bar)"

2020010408003151.png

  • 还有一个非常有用的赋值方法是通过“+=”实现追加赋值。
.PHONY:all
objects = main.o foo.o bar.o utils.o
objects += another.o
all:
  @echo $(objects)

20200104080303344.png

变量及其值的来源

从前面的示例可以看出,在Makefile中可以对变量进行定义。此外,还有其他方式使得make获得变量。比如:


  • 对于自动变量,其值是在每一个规则中根据规则的上下文自动获得的。
  • 在运行make时,通过命令参数定义变量。例如下面的Makefile,如果使用“make bar=x”来运行它,得到结果就不一样了。
.PHONY : all
foo = x
foo ?= y
bar ?= y
all:
  @echo "foo = $(foo), bar = $(bar)"


  • 变量还可以来自shell环境,如下:20200119104657226.png

### 高级变量引用功能

如下的Makefile说明了变量引用的一种高级功能,即在赋值的同时完成文件名后缀替换操作。

.PHONY = all
foo = a.c b.c c.c
bar := $(foo:.c=.o)
all:
  @echo "bar = $(bar)"

2020011910515457.png

从截图中的运行结果来看,bar变量中的文件名由.c后缀变成了.o。与使用函数相比,这种方式根据简洁。当然,这种功能也可以采用后面将要介绍的patsubst函数来实现。


避免变量被覆盖的方法


上面介绍了make命令行上定义变量的方式能够使得Makefile文件中定义的变量值被覆盖。但是如果在设计Makefile时不希望发生这种覆盖现象,则需要使用override指令进行预防。具体如下:


.PHONY: all
override foo = x
all:
  @echo "foo = $(foo)"


借助“模式”精简规则


对于之前使用到的Makefile,其中存在多个规则用于构建目标文件。比如,main.o和foo.o,都是采用不同的规则进行描述,如果对于每个目标文件,都得写一个不同的规则来描述,那就变成了很麻烦的事了,虽然也能实现想要的效果。Makefile中的模式就是用来解决这种烦恼的。如下的Makefile就运用到了模式。


.PHONY: clean
CC=gcc
RM=rm
EXE=simple
OBJS=main.o foo.o
$(EXE) : $(OBJS)
  $(CC) -o $@ $^
%.o : %.c
  $(CC) -o $@ -c $^
clean:
  $(RM) -rf $(EXE) $(OBJS)

2020011911052752.png

通过“函数”增强功能

函数是Makefile中的另一个离奇,通过使用函数能显著增强Makefile的功能。对于之前的项目的Makefile,尽管使用了模式规则,但还有一件比较麻烦的事——在Makefile中要指明每一个项目源文件。

如下是采用了wildcard和patsubst函数修改后的Makefile:


.PHONY: clean
CC = gcc
RM = rm
EXE = simple
SRCS = $(wildcard *.c)
OBJS = $(patsubst %.c, %.o, $(SRCS))
$(EXE) : $(OBJS)
  $(CC) -o $@ $^
%.o : %.c
  $(CC) -o $@ -c $^
clean:
  $(RM) -rf $(EXE) $(OBJS)


执行结果如下:

20200119141409236.png

当使用函数编辑Makefile后,在增加需要编译的源文件后不需要再修改Makefile,直接就能对新增的文件进行编译。如下:

20200119141626797.png

下面就来详细介绍一下Makefile中经常用到的几个函数。更多函数的使用方法可以参考官方手册《GNU Make》。


abspath函数

abspath函数被用于将_names中的各路径名转换成绝对路径,并将转换后的结果返回。其形式:


$(abspath _names)

如下示例:


.PHONY: all
ROOT := $(abspath /usr/../lib)
all:
  @echo $(ROOT)

执行结果如下:

20200119142100668.png



相关文章
|
10月前
|
JavaScript Linux 网络安全
Termux安卓终端美化与开发实战:从下载到插件优化,小白也能玩转Linux
Termux是一款安卓平台上的开源终端模拟器,支持apt包管理、SSH连接及Python/Node.js/C++开发环境搭建,被誉为“手机上的Linux系统”。其特点包括零ROOT权限、跨平台开发和强大扩展性。本文详细介绍其安装准备、基础与高级环境配置、必备插件推荐、常见问题解决方法以及延伸学习资源,帮助用户充分利用Termux进行开发与学习。适用于Android 7+设备,原创内容转载请注明来源。
2642 77
|
7月前
|
NoSQL Linux 开发工具
Linux环境基础开发工具的使用(yum、vim、gcc、g++、gdb、make/Makefile)
本文介绍了yum 包管理工具、Vim 编辑器、gcc/g++ 编译器、gdb 调试器、编译原理及 Makefile 的使用,同时还配备了如何使用,以及图解。旨在帮助读者更好地理解和应用这些工具与技术。
379 0
|
12月前
|
Rust Ubuntu Java
[Linux工具] Makefile
Makefile是Linux环境下用于自动化编译和链接程序的配置文件,常用于简化大型项目的编译流程。通过定义目标文件、依赖文件及生成命令,Makefile能高效管理编译任务。它不仅适用于C语言项目,还可扩展到其他编程语言和非编程任务中。
[Linux工具] Makefile
|
11月前
|
Unix Linux C语言
【Linux】 Linux makefile 教程
本文详细介绍了 Linux 环境下 Makefile 的基本结构、语法和使用方法,并通过一个简单的 C++ 项目示例演示了 Makefile 的实际应用。Makefile 是一个强大而灵活的工具,通过合理配置,可以极大地简化项目的编译和管理过程,提高开发效率。希望本文能帮助您更好地理解和应用 Makefile,在实际项目中高效管理代码的编译和构建。
760 20
|
11月前
|
Unix Linux C语言
【Linux】 Linux makefile 教程
本文详细介绍了 Linux 环境下 Makefile 的基本结构、语法和使用方法,并通过一个简单的 C++ 项目示例演示了 Makefile 的实际应用。Makefile 是一个强大而灵活的工具,通过合理配置,可以极大地简化项目的编译和管理过程,提高开发效率。希望本文能帮助您更好地理解和应用 Makefile,在实际项目中高效管理代码的编译和构建。
255 16
|
11月前
|
缓存 NoSQL Linux
Linux系统内存使用优化技巧
交换空间(Swap)的优化 禁用 Swap sudo swapoff -a 作用:这个命令会禁用系统中所有的 Swap 空间。swapoff 命令用于关闭 Swap 空间,-a 参数表示关闭 /etc/fstab 文件中配置的所有 Swap 空间。 使用场景:在高性能应用场景下,比如数据库服务器或高性能计算服务器,禁用 Swap 可以减少磁盘 I/O,提高系统性能。
438 3
|
缓存 并行计算 Linux
深入解析Linux操作系统的内核优化策略
本文旨在探讨Linux操作系统内核的优化策略,包括内核参数调整、内存管理、CPU调度以及文件系统性能提升等方面。通过对这些关键领域的分析,我们可以理解如何有效地提高Linux系统的性能和稳定性,从而为用户提供更加流畅和高效的计算体验。
542 24
|
缓存 监控 网络协议
Linux操作系统的内核优化与实践####
本文旨在探讨Linux操作系统内核的优化策略与实际应用案例,深入分析内核参数调优、编译选项配置及实时性能监控的方法。通过具体实例讲解如何根据不同应用场景调整内核设置,以提升系统性能和稳定性,为系统管理员和技术爱好者提供实用的优化指南。 ####
|
存储 缓存 网络协议
Linux操作系统的内核优化与性能调优####
本文深入探讨了Linux操作系统内核的优化策略与性能调优方法,旨在为系统管理员和高级用户提供一套实用的指南。通过分析内核参数调整、文件系统选择、内存管理及网络配置等关键方面,本文揭示了如何有效提升Linux系统的稳定性和运行效率。不同于常规摘要仅概述内容的做法,本摘要直接指出文章的核心价值——提供具体可行的优化措施,助力读者实现系统性能的飞跃。 ####
|
监控 算法 Linux
Linux内核锁机制深度剖析与实践优化####
本文作为一篇技术性文章,深入探讨了Linux操作系统内核中锁机制的工作原理、类型及其在并发控制中的应用,旨在为开发者提供关于如何有效利用这些工具来提升系统性能和稳定性的见解。不同于常规摘要的概述性质,本文将直接通过具体案例分析,展示在不同场景下选择合适的锁策略对于解决竞争条件、死锁问题的重要性,以及如何根据实际需求调整锁的粒度以达到最佳效果,为读者呈现一份实用性强的实践指南。 ####