目录

linux 编译构建工具

前言

Linux 下可用的编译器有 GCC、LLVM-clang、EGCS 和 PGCC,其中 GCC 是绝对的老大!GCC(GNU Compiler Collection)是由 GNU 开发的编程语言译器套件。GNU 编译器套件包括 C、C++、Objective-C、Fortran、Ada、Go 及 D 语言前端,也包括了这些语言的库(如:libstdc++,libgcj 等)

1、关于工具链关系

CMake是一种跨平台编译构建工具(也就是项目构建生成工具),其将程序员编写 CMakeLists.txt 文件转化 makefile 文件,最后调用 make 命令执行相关工作。
make是一个脚本工具(也就是项目构建工具/编译批处理工具),用于解释执行 Makefile 脚本的命令工具。
makefile是脚本语言,用于描述了整个工程的编译、链接等规则,调用 GCC(也可以是其它编译器,如:clang)命令执行编译、链接动作。
gcc是编译器,用于编译源文件,链接生成执行文件。
gdb是调试器,用于调试 c/c++ 程序,也可以调试 Ada、Objective-C、Pascal 及其它语言。

2、关于交叉编译器

GNU 的 GCC 主要用于编译电脑软件,主要针对 x86 体系的处理器;对于 ARM 、MIPS 等体系的处理器,我们需要使用交叉编译器(如:arm-none-linux-gnueabi-gcc)来编译,其过程:先在电脑上使用交叉编译器编译生成执行文件,再将执行文件复制/烧录到控制板上运行。

3、工具或源码下载
工具 源码包
make 官网下载清华镜像
gcc 官网下载清华镜像

一、gcc 编译器

1、linux 系统下安装 ●

1.1、Ubuntu 系统下的在线安装

在 Ubuntu 下安装 GCC 和其他一些 Linux 系统有点不一样。默认的 Ubuntu 存储库包含一个名为build-essential的元包,它包含 GCC 编译器以及编译软件所需的许多库和其他实用程序。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#####方法一#####
sudo apt update                  #首先更新包列表,
sudo apt install build-essential #安装`build-essential`软件包,
gcc --version                    #查看 gcc 版本用于验证是否安装成功。

#####方法二#####
# sudo apt-get build-dep gcc     #安装`gcc`
# gcc -v                         #查看 gcc 版本用于验证是否安装成功。

#####方法三#####
# sudo apt-get install gcc       #安装`gcc`
# sudo apt-get install g++       #安装`g++`
# gcc -v                         #查看 gcc 版本用于验证是否安装成功。

1.2、Ubuntu 下在线安装最新版

因为个别工程需要多个 GCC 的编译器或者是库来支持,我们可能需要在同一个 Linux 系统当中安装多个 GCC 版本来实现支持的目的。从低版本到高版本,以及最新版本的 GCC,都可从 Ubuntu Toolchain PPA 获得。虽然 Ubuntu 官方软件仓库尽可能囊括所有的开源软件,但仍有很多软件包由于各种原因不能进入官方软件仓库,为了方便 Ubuntu 用户使用,Ubuntu 为软件开发者提供了 launchpad 平台来创建自己的 repositories(仓库)提供了个人软件包集,即 PPA(Personal Package Archives)个人软件包档案。从网上找到的 ppa:ubuntu-toolchain-r/test 支持很多 gcc 版本,列表【Package】项表示 gcc 软件包名称,【Version】项表示具体版本及支持 Ubuntu 版本,例如:版本10.3.0-1ubuntu1~20.04表示 gcc 10.3.0 以及支持 Ubuntu 20.04(备注:原以为支持 1~20.04 全部版本,结果测试 Ubuntu 16.04 提示找不到软件包,即是只支持 Ubuntu 20.04)。其实从 gcc 官网也找到此作者的仓库,其路径:https://gcc.gnu.org>>Download>>Binaries → GFortran Wiki → GNU/Linux>>Most Linux distributions offer gfortran packages → Ubuntu>>minor updates(小更新)或 new testing versions(新的测试版本)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#####安装仓库以及gcc方法#####
sudo apt install software-properties-common          #安装`PPA`仓库工具(只需安装一次)
sudo add-apt-repository ppa:ubuntu-toolchain-r/test  #加入`PPA`仓库
sudo apt update                                      #更新`PPA`仓库信息
sudo apt install gcc-9 g++-9                         #安装`GCC`和`G++`(需要安装什么版本就输入什么版本)
                                                     #如果没找到软件包,则提示“E: Unable to locate package XXX”
#####加gcc版本优先级方法#####                           #注意要把之前在环境变量加入的软件执行文件路径移除,否则可能会冲突!
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 90  --slave /usr/bin/g++ g++ /usr/bin/g++-9
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-5 999 --slave /usr/bin/g++ g++ /usr/bin/g++-5
                                                     #备注:①我的系统 Ubuntu 16.04 已安装:gcc 5.4.0;②上面的 --slave 的 g++ 表示的工作版本由 gcc 配置决定
#####选择gcc工作版本方法#####
sudo update-alternatives --config gcc                #选择gcc工作版本,根据界面提示输入对应数字+回车选择

#####移除专用PPA仓库方法#####
sudo add-apt-repository --remove ppa:ubuntu-toolchain-r/test
                                                     #移除上面加入的PPA仓库
#####移除gcc某优先级方法#####
sudo update-alternatives --remove gcc /usr/bin/gcc-9 #移除 gcc 9 的优先级(将从优先级列表中移除)

1.3、Ubuntu 下自己编译与安装

待续……

2、windows 下安装 ●

2.1、可选工具:

● GNU 并没提供 Windows 的 GCC 版本,但我们可以安装第三方制作的 GCC Windows 版本,其中集成 GCC 编译器的软件主要有:MinGW/MinGW-w64TDM-GCCCygwin

2.2、工具区别:

MinGW/MinGW-w64(Minimalist GNU for Windows)是为 windows 系统打造的编译器套件,其主要是把 GNU 的 gcc、make 等编译工具移植到 Windows 平台下,使得原是在 linux 写的 C 源代码可以在 windows 上编译及运行(exe 执行文件)。
TDM-GCC 衍生自 MinGW 和 MinGW-w64 的项目(功能是一样的),也是分为 32 位与 64 位两个版本,32 位版本的编译目标仅兼容 32 位应用程序,64 位版本的编译目标兼容 32 和 64 位应用程序。TDM-GCC 对比 MinGW-w64 会集成更新的 gcc 编译器,目前集成的版本为 GCC 10.3.0。
Cygwin 是一个在 windows 平台上运行的类 unix 模拟环境,其集成绝大部分的 linux 软件包(其中就包括 GCC 编译工具)。

2.3、工具特点:

Cygwin 大而全面,提供完整的类 Unix 环境,凡是在 Cygwin 环境中编译构建的软件在运行时必须依赖 cygwin1.dll 库文件。
MinGW / MinGW-w64 / TDM-GCC 小而高效,其主要是编译器,一般只支持编译使用 C/C++ 标准库的程序。

2.4、工具安装:

● 关于 Cygwin 的安装与使用,请移步《linux-工具-linux 模拟环境 cygwin》。
● 关于 MinGW-w64 的安装与使用,请移步《MinGW-w64 编译套件(GNU 工具集)》。
● 下载 TDM-GCC 编译工具,不需要勾选检测是否最新版本,直接点击Creat安装即可,最后在命令窗口输入gcc -v可查看 gcc 版本。特别说明:查看 make 版本的命令为mingw32-make -v

../img/20200915_10.jpg

3、GCC 的编译方法 ●

3.1、一步编译方法:

1
gcc -o hello hello.c              # 1步完成:预处理->编译->汇编->链接(可多个C文件编译)

3.2、两步编译方法:

1
2
gcc -c -o hello.o hello.c -I./inc # 编译  (生成二进制机器码) → Makefile 中一般都是使用两步编译方式([-I]为源文件的包含文件所在路径(如:头文件所在路径[./inc]))(绝大部分选项参数都在编译这里设置)
gcc    -o hello   hello.o         # 链接  (生成可执行文件)  → Makefile 中一般都是使用两步编译方式(链接时常用选项参数:-Txxx.lds  -Wl,-Map=xxx.map,--cref  -Wl,--gc-sections)

3.3、多步编译方法:

1
2
3
4
5
#      ┌→指定生成文件名         (可多个C文件编译)
gcc -E -o hello.i hello.c         # 预处理(经过预处理的C原始程序) ┐
gcc -S -o hello.s hello.i         # 编译  (编译生成汇编语言代码)  ├→ ★助记★: 选项->ESc, 生成文件->iso
gcc -c -o hello.o hello.s         # 汇编  (汇编生成二进制机器码)  ┘
gcc    -o hello   hello.o         # 链接  (链接生成可执行文件)
4、GCC 的编译参数 ●
  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
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
# 一些编译常用选项
gcc --version                     # 查看gcc版本
gcc -v                            # 查看gcc版本(包括配置参数信息)
gcc -x c xxxx.b -x none hello.c   # 忽略文件类型强制以C文件类型编译[xxxx.b],自动识别文件类型编译[hello.c]
gcc -std=c99 hello.c -o hello     # 使用[C99]标准编译(gcc 默认使用的是 C89 的标准,而 -std=gun99 是 GNU 的 C99,及 -std=gnu11 是 C11 与 GNU 的拓展))
gcc -static  hello.c -o hello     # 静态编译生成的可执行文件(优点:独立无依赖,缺点:文件很大)
gcc -g       hello.c -o hello     # 在可执行程序中包含标准调试信息
gcc -O2      hello.c -o hello     # 用[-O2]优化选项编译生成可执行文件(常用优化级别:-O0,-O1,-O2,-O3)
gcc -Og      hello.c -o hello     # 在[-O1]的基础上去掉那些影响调试的优化息
gcc -Os      hello.c -o hello     # 在[-O2]的基本上尽量编译出小体积程序
gcc -DUSE_DB hello.c -o hello     # [-DUSE_DB]表示加入全局宏[define USE_DB]

# GCC警告提示常用选项
gcc -w        wrong.c -o wrong    # [-w]        关闭所有警告   (w小写)
gcc -W        wrong.c -o wrong    # [-W]        开启认为错误的警告(W大写)
gcc -Werror   wrong.c -o wrong    # [-Werror]   开启将警告转换成错误(主要用于排除警告代码)
gcc -Wall     wrong.c -o wrong    # [-Wall]     开启所有警告(建议读者养成使用该选项的习惯)
gcc -W -Wall  wrong.c -o wrong    # [-W -Wall]  开启常规警告(建议读者养成使用该选项的习惯)
gcc -ansi     wrong.c -o wrong    # [-ansi]     该选项强制GCC生成标准语法所要求的告警信息
gcc -pedantic wrong.c -o wrong    # [-pedantic] 该选项允许发出 ANSI C 标准所列的全部警告信息
                                  # 更多参数:https://blog.csdn.net/laifengyuan1/article/details/108662522
# 生成依赖信息(非编译,主要用于 Makefile)
gcc -M     -I./inc main.c         # 生成完整的头文件依赖关系,如生成: main.o: main.c main.h /usr/include/stdc-predef.h /usr/include/stdio.h
gcc -MM    -I./inc main.c         # 生成不包含系统头文件的依赖关系,如生成: main.o: main.c main.h
gcc -E -M  -I./inc main.c         # 同上面对应命令,且 -E 表示强调只是预处理,不做编译动作
gcc -E -MM -I./inc main.c         # 同上面对应命令,且 -E 表示强调只是预处理,不做编译动作
gcc -MMD -MP -MF"out/x.d" -I./inc main.c # 直接生成依赖关系信息文件[x.d],可在编译过程中同时生成,从而提高效率(-MMD:生成默认依赖关系信息文件,-MP:信息中同时生成头文件伪目标,-MF:更改依赖关系信息文件路径及名称)

# --------------------------动/静态库编译方法:-----------------------
#【静态库】编译方法:
gcc -c -o zzz.o zzz.c -I.                # 两步编译方法:一、生成的目标文件, [r] 更新及替换,(replace)
ar rcs libzzz.a zzz.o xxx.o              # 两步编译方法:二、打包成为静态库。 [c] 创建静态库,(create)
nm libzzz.a                              # 查看库中包含的函数全局变量等信息。 [s] 及建立索引。

#【静态库】使用简介:
gcc -o test test.c -I. ./libzzz.a        # 方式一:直接指定静态库文件名称及路径与源文件一同编译。
gcc -o test test.c -I. -L. -l:libzzz.a   # 方式二:直接指定静态库文件名称及通过[-L]指出路径与源文件一同编译。
gcc -o test test.c -I. -L. -lzzz         # 方式三:通过[-l]指定静态库文件名称及通过[-L]指出路径与源文件一同编译。
                                         #【特别注意】:
                                         # >>>> 库文件必须书写到源文件及中间文件的后面,否则会编译出错,说找不到调用函数!!!
                                         # [-I] 为源文件的包含文件所在路径(如:[-I.]头文件所在当目录)(I:Include、L:Location、l:library)
                                         # [-L] 为告知链接器库文件所在路径(如:[-L.]库文件所在当目录)
                                         # [-l] 为指定库文件名称,两种方式(如:[-lzzz]库文件名称为"libzzz.so"或"libzzz.a",[-l:libzzz.a]库文件名称为"libzzz.a")
                                         #【扩展知识】:
                                         # [/lib]或[/usr/lib] 设置库路径方法(1):系统默认搜索库目录,库文件要放到此目录,否则程序编译时找不到库,最后在命令窗口输入命令生效配置(sudo ldconfig)
                                         # [LIBRARY_PATH]     设置库路径方法(2):修改静态库环境变量,在命令窗口输入命令临时追加库路径(如:export LIBRARY_PATH=$LIBRARY_PATH:~/lib)
                                         # [参考网文](https://blog.csdn.net/daidaihema/article/details/80902012)
                                         # [参考网文](https://blog.csdn.net/m0_38032942/article/details/111158489)
                                         # [参考网文](https://blog.csdn.net/hyl999/article/details/108642313)
# --------------------------------------
#【动态库】编译方法:
gcc -fPIC -shared zzz.c -o libzzz.so     # 一步编译方法:一步直接生成动态库。 [.so]     共享对象(共享库/动态链接库)(shared object)

gcc -fPIC -c      zzz.c -o zzz.o         # 两步编译方法:一、生成的目标文件, [-fPIC]   编译为位置无关码,[相对地址,为编译共享库(动态库)准备](Position Independent Code)
gcc       -shared zzz.o -o libzzz.so     # 两步编译方法:二、生成动态链接库。 [-share]  编译为动态链接库。[共享对象/共享库/动态链接库]

#【动态库】使用简介:
gcc -o test test.c -I. ./libzzz.so       # 方式一:直接指定静态库文件名称及路径与源文件一同编译。
gcc -o test test.c -I. -L. -l:libzzz.so  # 方式二:直接指定静态库文件名称及通过[-L]指出路径与源文件一同编译。
gcc -o test test.c -I. -L. -lzzz         # 方式三:通过[-l]指定静态库文件名称及通过[-L]指出路径与源文件一同编译。
                                         #【特别注意】:
                                         # >>>> 库文件必须书写到源文件及中间文件的后面,否则会编译出错,说找不到调用函数!!!
                                         # [-I] 为源文件的包含文件所在路径(如:[-I.]头文件所在当目录)(I:Include、L:Location、l:library)
                                         # [-L] 为告知链接器库文件所在路径(如:[-L.]库文件所在当目录)
                                         # [-l] 为指定库文件名称,两种方式(如:[-lzzz]库文件名称为"libzzz.so"或"libzzz.a",[-l:libzzz.so]库文件名称为"libzzz.so")
                                         #【扩展知识】:
                                         # [/lib]或[/usr/lib] 设置库路径方法(1):系统默认搜索库目录,库文件要放到此目录,否则程序运行时找不到库,最后在命令窗口输入命令生效配置(sudo ldconfig)
                                         # [LD_LIBRARY_PATH]  设置库路径方法(2):修改动态库环境变量,使用文本编辑器打开系统环境配置文件(如:/etc/profile),在其里面最后追加库路径(如:export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:~/lib),最后在命令窗口输入命令生效配置(如:source /etc/profile)
                                         # [/etc/ld.so.conf]  设置库路径方法(3):修改动态库配置文件,使用文本编辑器打开它,在其里面最后添加库路径(如:~/lib),最后在命令窗口输入命令生效配置(sudo ldconfig)
                                         # [参考网文](https://mp.weixin.qq.com/s/31UN152prQdMHPGrMiC53Q)
# --------------------------------------
#【标准库】中常用库:
-lm           # 数学库(math)
-lc           # 标准C库(C lib)

# --------------------------选项参数综合展示:-----------------------
# 链接、编译、汇编相关选项参数
CFLAG_L =    -mcpu=cortex-m3 -mthumb -specs=nano.specs -Wl,--gc-sections -TSTM32F103XB_FLASH.ld -Wl,-Map=stm32f103.map,--cref
CFLAG_O = -c -mcpu=cortex-m3 -mthumb -fdata-sections -ffunction-sections -DUSE_HAL_DRIVER -Wa,-a,-ad,-alms=$(@:.o=.lst) -Wall -Og -I./inc
AFLAG_O = -c -mcpu=cortex-m3 -mthumb -fdata-sections -ffunction-sections -DUSE_HAL_DRIVER
CFLAG_M = -MMD -MP -MF"$(@:%.o=%.d)"
# 目标文件 → 执行文件
$(TARGET).elf: $(OBJS) $(LIBS)
	@$(CC) $(CFLAG_L) -o $@ $(filter %.o, $^) $(LIBS) -lc -lm -lnosys
# 源码文件 → 目标文件
$(OBJS_D)%.o: %.c
	@$(CC) $(CFLAG_O) $(CFLAG_M) $< -o $@
# 汇编文件 → 目标文件
$(OBJS_D)%.o: %.s
	@$(AS) $(AFLAG_O) $(CFLAG_M) $< -o $@

# --------------------------相关文件后缀补充:-----------------------
.c            # C原始程序
.C/.cc/.cxx   # C++原始程序
.m            # Objective C原始程序
.i            # 已经过预处理的C原始程序       ┐
.ii           # 已经过预处理的C++原始程序     ├→ ★助记★: 生成文件->iso
.s/.S         # 汇编语言原始程序              │
.o            # 目标文件(二进制机器码)        ┘
.a/.so        # 编译后的库文件(共享库(动态库)的后缀名由“.so”和版本号组成,静态库的后缀名为“.a”)
              #               (如: 数学共享库的库名为“libm.so.5”,这里的标识字符为m,版本号为5,
              #                                      “libm.a” 则是静态数学库)
5、GCC 的编译图解 ●

../img/20200915_11.jpg

二、交叉编译器

1、交叉编译器命名规则 ●
1.1、arch(架构)-vendor(厂商)-os(系统)-eabi(接口)
分类 说明
arch(架构) 体系架构:ARM 、MIPS、x86 等
vendor(厂商) 工具链的供应商:苹果、none(无供应商)等
os(系统) 目标的操作系统:linux、none(裸机)等
eabi(接口) 嵌入式应用二进制接口:eabi、gnueabi 等

1
2
3
4
arch   # 'ɑːrtʃ
vendor # 'vendə(r)
eabi   # Embedded Application Binary Interface
       # ɪm'bedɪd            'baɪnəri
1.2、abi 与 eabi 的区别

ABI: 二进制应用程序接口。在计算机中,应用二进制接口描述了应用程序(或者其他类型)和操作系统之间或其他应用程序的低级接口。
EABI:嵌入式 ABI。嵌入式应用二进制接口指定了文件格式、数据类型、寄存器使用、堆积组织优化和在一个嵌入式软件中的参数的标准约定。开发者使用自己的汇编语言也可以使用 EABI 作为与兼容的编译器生成的汇编语言的接口。
两者区别总结:ABI 用于计算机上,EABI 用于嵌入式平台上(如 ARM,MIPS 等)。

2、arm 框架交叉编译器 ●
2.1、armcc

armcc 是 ARM 公司推出的商用收费编译工具,功能和 arm-none-eabi-gcc 类似,用于编译裸机程序(u-boot、kernel、单片机程序),但是不能编译 Linux 应用程序。armcc 一般和 ARM 开发工具一起,例如 Keil MDK、ADS、RVDS 和 DS-5 中的编译器都是 armcc。

2.2、arm-none-eabi-gcc

arm-none-eabi-gcc 为 ARM 框架、无供应商、无系统、嵌入式二进制接口的交叉编译器,一般适合 ARM7、Cortex-M、Cortex-R 内核的芯片使用,它使用的是 newlib 这个专用于嵌入式系统的 C 库,用于编译裸机程序(u-boot、kernel、单片机程序),但是不能编译 Linux 应用程序。注意:交叉编译器目前都是 32 位执行程序,依赖 32 位库,事前需要安装 ia32-libs(网文)或 lsb-core。进入网页选择你需要的版本下载:官网最新版官网 2021 版Ubuntu-launchpad 软件开发者平台 及依赖 lsb-core。补充:关于 Windows 版本,为直接安装文件,双击安装即可,还有安装后需要手工加入环境变量【如图】。

1
2
3
4
5
6
7
8
wget https://launchpad.net/gcc-arm-embedded/5.0/5-2016-q3-update/+download/gcc-arm-none-eabi-5_4-2016q3-20160926-linux.tar.bz2 #下载交叉编译器压缩包
sudo tar -xjvf gcc-arm-none-eabi-5_4-2016q3-20160926-linux.tar.bz2 -C /usr/lib/gcc #将交叉编译器压缩包解压到:/usr/lib/gcc
sudo gedit /etc/profile             #打开环境变量,在文件最后添加内容:export PATH=$PATH:/usr/lib/gcc/gcc-arm-none-eabi-5_4-2016q3/bin
source /etc/profile                 #使能环境变量。
sudo apt-get install lsb-core       #64位操作系统需要安装32位依赖库:lsb-core     (arm-none-eabi-gcc 用到)
sudo apt-get install lib32ncurses5  #64位操作系统需要安装32位依赖库:lib32ncurses5(arm-none-eabi-gdb 用到)
arm-none-eabi-gcc -v                #查看版本(验证是否安装成功)
#arm-none-eabi-gcc hello.c -o hello #编译一个程序例子
2.3、arm-none-linux-gnueabi-gcc

arm-none-linux-gnueabi-gcc 是 Codesourcery 公司(目前已经被 Mentor 收购)基于 GCC(使用 Glibc 库)推出的的 ARM 交叉编译器。主要用于基于 ARM 架构的 Linux 系统,可用于编译 ARM 架构的 u-boot、Linux 内核、linux 应用等。一般 ARM9、ARM11、Cortex-A 内核,带有 Linux 操作系统的会用到。注意:交叉编译器目前都是 32 位执行程序,依赖 32 位库,事前需要安装 ia32-libs(网文)或 lsb-core

1
2
3
4
5
6
7
8
wget http://www.codesourcery.com/sgpp/lite/arm/portal/package4571/public/arm-none-linux-gnueabi/arm-2014.05-29-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 #下载交叉编译器压缩包  
sudo tar -xjvf arm-2014.05-29-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 -C /usr/lib/gcc #将交叉编译器压缩包解压到:/usr/lib/gcc  
sudo gedit /etc/profile             #打开环境变量,在文件最后添加内容:export PATH=$PATH:/usr/lib/gcc/arm-2014.05/bin 
source /etc/profile                 #使能环境变量。
sudo apt-get install lsb-core       #64位操作系统需要安装32位依赖库:lsb-core     (arm-none-eabi-gcc 用到)
sudo apt-get install lib32ncurses5  #64位操作系统需要安装32位依赖库:lib32ncurses5(arm-none-eabi-gdb 用到)
arm-none-linux-gnueabi-gcc -v       #查看版本(验证是否安装成功)
#arm-none-linux-gnueabi-gcc hello.c -o hello #编译一个程序例子

不同系统/软件环境下的版本
● Linux 解压版:在 Linux 系统(如:Ubuntu、RedHat 等)直接解压即可使用。推荐方式!
● Linux 安装版:在 Linux 系统下执行后按照提示安装后使用。
● Windows 解压版:在 Windows 系统下解压后使用,但是需要 MinGW32。
● Windows 安装版:在 Windows 系统下安装后使用。
● 源码版:交叉编译器源代码,按需编译生成相应版本。
下面为多种版本下载,同时增加网友博文提供的网盘下载:

Linux 解压版 网友网盘
arm-2006q1-3-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 网盘
arm-2006q1-6-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 网盘
arm-2006q3-26-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 网盘
arm-2007q1-10-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 网盘
arm-2007q1-21-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 网盘
arm-2007q3-51-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 网盘
arm-2008q1-126-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 网盘
arm-2008q3-41-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 网盘
arm-2008q3-72-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 网盘
arm-2009q1-176-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 网盘
arm-2009q1-203-arm-none-linux-gnueabi-i686-pc-linux-gnu.bz2 网盘
arm-2009q3-67-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 网盘
arm-2010.09-50-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 网盘
arm-2010q1-202-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 网盘
arm-2011.03-41-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 网盘
arm-2012.09-64-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 网盘
arm-2013.05-24-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 网盘
arm-2013.11-33-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 网盘
arm-2014.05-29-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 网盘
Linux 安装版 网友网盘
arm-2006q3-26-arm-none-linux-gnueabi.bin 网盘
arm-2007q1-10-arm-none-linux-gnueabi.bin 网盘
arm-2007q1-21-arm-none-linux-gnueabi.bin 网盘
arm-2007q3-51-arm-none-linux-gnueabi.bin 网盘
arm-2008q1-126-arm-none-linux-gnueabi.bin 网盘
arm-2008q3-41-arm-none-linux-gnueabi.bin 网盘
arm-2008q3-72-arm-none-linux-gnueabi.bin 网盘
arm-2009q1-176-arm-none-linux-gnueabi.bin 网盘
arm-2009q1-203-arm-none-linux-gnueabi.bin 网盘
arm-2009q3-67-arm-none-linux-gnueabi.bin 网盘
arm-2010.09-50-arm-none-linux-gnueabi.bin 网盘
arm-2010q1-202-arm-none-linux-gnueabi.bin 网盘
arm-2011.03-41-arm-none-linux-gnueabi.bin 网盘
arm-2011.09-70-arm-none-linux-gnueabi.bin 网盘
arm-2012.03-57-arm-none-linux-gnueabi.bin 网盘
arm-2012.09-64-arm-none-linux-gnueabi.bin 网盘
arm-2013.05-24-arm-none-linux-gnueabi.bin 网盘
arm-2013.11-33-arm-none-linux-gnueabi.bin 网盘
arm-2014.05-29-arm-none-linux-gnueabi.bin 网盘
Windows Mingw32 版 网友网盘
gnu-csl-arm-2005Q1B-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 网盘
arm-2006q3-26-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 网盘
arm-2007q1-10-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 网盘
arm-2007q1-21-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 网盘
arm-2007q3-51-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 网盘
arm-2008q1-126-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 网盘
arm-2008q3-41-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 网盘
arm-2008q3-72-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 网盘
arm-2009q1-176-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 网盘
arm-2009q1-203-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 网盘
arm-2009q3-67-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 网盘
arm-2010.09-50-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 网盘
arm-2010q1-202-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 网盘
arm-2011.03-41-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 网盘
arm-2011.09-70-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 网盘
arm-2012.03-57-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 网盘
arm-2012.09-64-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 网盘
arm-2013.05-24-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 网盘
arm-2013.11-33-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 网盘
arm-2014.05-29-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 网盘
Windows 安装版 网友网盘
gnu-csl-arm-2005Q1B-arm-none-linux-gnueabi.exe 网盘
arm-2006q1-3-arm-none-linux-gnueabi.exe 网盘
arm-2006q1-6-arm-none-linux-gnueabi.exe 网盘
arm-2006q3-26-arm-none-linux-gnueabi.exe 网盘
arm-2007q1-10-arm-none-linux-gnueabi.exe 网盘
arm-2007q1-21-arm-none-linux-gnueabi.exe 网盘
arm-2007q3-51-arm-none-linux-gnueabi.exe 网盘
arm-2008q1-126-arm-none-linux-gnueabi.exe 网盘
arm-2008q3-41-arm-none-linux-gnueabi.exe 网盘
arm-2008q3-72-arm-none-linux-gnueabi.exe 网盘
arm-2009q1-176-arm-none-linux-gnueabi.exe 网盘
arm-2009q1-203-arm-none-linux-gnueabi.exe 网盘
arm-2009q3-67-arm-none-linux-gnueabi.exe 网盘
arm-2010.09-50-arm-none-linux-gnueabi.exe 网盘
arm-2010q1-202-arm-none-linux-gnueabi.exe 网盘
arm-2011.03-41-arm-none-linux-gnueabi.exe 网盘
arm-2011.09-70-arm-none-linux-gnueabi.exe 网盘
arm-2012.03-57-arm-none-linux-gnueabi.exe 网盘
arm-2012.09-64-arm-none-linux-gnueabi.exe 网盘
arm-2013.05-24-arm-none-linux-gnueabi.exe 网盘
arm-2013.11-33-arm-none-linux-gnueabi.exe 网盘
arm-2014.05-29-arm-none-linux-gnueabi.exe 网盘
源代码 网友网盘
gnu-csl-arm-2005Q1B-arm-none-linux-gnueabi.src.tar.bz2 网盘
arm-2006q1-3-arm-none-linux-gnueabi.src.tar.bz2 网盘
arm-2006q1-6-arm-none-linux-gnueabi.src.tar.bz2 网盘
arm-2006q3-26-arm-none-linux-gnueabi.src.tar.bz2 网盘
arm-2007q1-10-arm-none-linux-gnueabi.src.tar.bz2 网盘
arm-2007q1-21-arm-none-linux-gnueabi.src.tar.bz2 网盘
arm-2007q3-51-arm-none-linux-gnueabi.src.tar.bz2 网盘
arm-2008q1-126-arm-none-linux-gnueabi.src.tar.bz2 网盘
arm-2008q3-41-arm-none-linux-gnueabi.src.tar.bz2 网盘
arm-2008q3-72-arm-none-linux-gnueabi.src.tar.bz2 网盘
arm-2009q1-176-arm-none-linux-gnueabi.src.tar.bz2 网盘
arm-2009q1-203-arm-none-linux-gnueabi.src.tar.bz2.bz2 网盘
arm-2009q3-67-arm-none-linux-gnueabi.src.tar.bz2 网盘
arm-2010.09-50-arm-none-linux-gnueabi.src.tar.bz2 网盘
arm-2010q1-202-arm-none-linux-gnueabi.src.tar.bz2 网盘
arm-2011.03-41-arm-none-linux-gnueabi.src.tar.bz2 网盘
arm-2011.09-70-arm-none-linux-gnueabi.src.tar.bz2 网盘
arm-2012.03-57-arm-none-linux-gnueabi.src.tar.bz2 网盘
arm-2012.09-64-arm-none-linux-gnueabi.src.tar.bz2 网盘
arm-2013.05-24-arm-none-linux-gnueabi.src.tar.bz2 网盘
arm-2013.11-33-arm-none-linux-gnueabi.src.tar.bz2 网盘
arm-2014.05-29-arm-none-linux-gnueabi.src.tar.bz2 网盘

补充
:arm-linux-gcc 与 arm-none-linux-gnueabi 有区别吗?
答:eabi 标准的要好些,可能 arm-linux-gcc 就是 arm-none-linux-gnueabi 的一个链接而已。
:arm-linux-gnueabi-gcc 和 arm-linux-gnueabihf-gcc 的区别?
答:两个交叉编译器分别适用于 armel 和 armhf 两个不同的架构,armel 和 armhf 这两种架构在对待浮点运算采取了不同的策略(有 fpu 的 arm 才能支持这两种浮点运算策略)。其实这两个交叉编译器只不过是 gcc 的选项 -mfloat-abi 的默认值不同,其有三种值 soft、softfp、hard。

2.4、arm-none-uclinuxeabi-gcc

arm-none-uclinuxeabi 用于 uCLinux

2.5、arm-none-symbianelf-gcc

arm-none-symbianelf 用于 symbian

2.6、arm-eabi-gcc

arm-eabi-gcc 是 Android ARM 编译器

三、make+Makefile 脚本

1、make、Makefile 关系 ●

make是一个脚本工具,用于解释执行 Makefile 脚本的命令工具。
makefile是脚本语言,用于描述整个工程的编译、链接等规则,调用 GCC(也可以是其它编译器,如:clang)命令执行编译、链接动作。脚本文件默认名称为 Makefile 或 makefile。

2、make 命令的使用方法 ●

make 命令常用格式为:make [选项] [目标(操作)]
make 命令是用来解释和执行 Makefile 脚本命令,所以[目标(操作)]是由 Makefile 脚本提供的,如果不指出则 make 工具默认执行 Makefile 中的第一个目标。

2.1、【目标】目标的名称约定常常有以下惯例:

目标名称 作用
all 表示编译所有并生成执行文件
install 表示安装刚刚生成的执行文件
clean 表示清除全部目标及执行文件
distclean 表示清除全部目标及执行文件,包括 Makefile
dist 包装成一个压缩文件以供发布
distcheck 同上,并检查压缩包是否正常

2.2、【选项】选项参数较少用到,其主要包括:

选项 含义
-ffilename 指定执行名称为 filename 的 Makefile 文件
-Cdirname 指定执行目录为 dirname(Makefile 文件所在目录)
-e 不允许在 Makefile 中替换环境变量的赋值
-k 执行命令出错时,放弃当前目标,继续维护其他目标
-n 按实际运行时的执行顺序模拟执行命令(包括用 @ 开头的命令),没有实际执行效果,仅仅用于显示执行过程
-p 显示 Makefile 中所有的变量和内部规则
-r 忽略内部规则
-s 执行但不显示命令,常用来检查 Makefile 的正确性
-S 如果执行命令出错就退出
-t 修改每个目标文件的创建日期
-I 忽略运行 make 中执行命令的错误
-V 显示 make 的版本号

2.3、【返回】make 命令执行后的三个返回码:

选项 含义
0 表示成功执行,则返回【0】
1 如果 make 运行时出现任何错误,则返回【1】
2 如果你使用了make的“-q”选项,并且 make 使得一些目标不需要更新,则返回【2】

2.4、【步骤】配置、编译、安装/卸载:

步骤 作用
configure 配置操作。configure 是一个 shell 脚本,它可以自动设定源程序以符合各种不同平台上 Unix 系统的特性,并且根据系统参数及环境产生合适的 Makefile 文件或是 C 的头文件,让源程序可以很方便地在这些不同的平台上被编译连接。待续……
make 编译操作(操作第一个目标)
make install 安装操作

2.5、【使用】演示例子:

在命令窗口运行命令:

1
2
3
4
make            # 默认执行 Makefile 第一个目标
make all        # 指定执行 Makefile 目标为 all(常见目标:all、install、clean)
make -f mkf     # 同`make`    , 只是 Makefile 文件名称为 mkf
make -f mkf all # 同`make all`,只是 Makefile 文件名称为 mkf
3、Makefile 的语法+例子 ●
3.1、【语法:基本知识】

3.1.1、Makefile 包含五项内容:1、显性规则,2、隐性规则,3、变量定义,4、文件指示,5、注释。

分类
使用
显性规则 书写者明显指出依赖文件或命令操作等规则
隐性规则 make 自动推导依赖文件或命令操作等规则
变量定义 变量只是定义一串字符串,其相当于宏定义
文件指示 预处理(引用另一个 Makefile、哪些代码不处理、变量定义处理)
注释 “#”为行注释符(没块注释,强烈建议独占一行并且在行首加“#”注释),如果需要使用“#”字符,则书写格式为\#

3.1.2、Makefile 处理文件分为:目标、依赖。其中目标分为:实目标、伪目标。

分类
含义
实目标 真正要生成的以文件形式存放在磁盘上的目标(最终的目标只能是一个)。如果想生成多个实目标(文件),则在伪目标的依赖中加入若干个其它实目标(文件)。
伪目标 伪目标不要求生成实际的文件,它后面可以没有依赖文件(也可以加别的目标),它主要是用于完成一些辅助操作。其中典型案例:clean:清除操作。
多目标 一句依赖中有多个目标,它只是语法一种表达,实质多目标就是根据规则拆解为多个的单目标语句,目的只是简化书写。
关系
处理方式
目标与依赖的执行顺序关系 先执行最深层的依赖,再逐级往上执行目标!每对目标与依赖的执行总规则:尝试创建或更新目标文件( include 包含文件同理)
目标与依赖的搜索对象关系 只要目标有依赖,make 工具就会尝试在搜索目录中寻找依赖文件是否存在或者是否为别的目标。如果都为否,则 make 运行就会抛出错误!备注:Makefile 搜索目录为当前 Makefile 根目录及 VPATH、vpath 设置的目录。
目标与依赖的路径定义关系 目标文件在书写时没写明路径表示为当前 Makefile 根目录文件,有写明路径就是指定目录文件。依赖文件没写明路径时由 Makefile 搜索目录中找出文件(优先搜索当前 Makefile 根目录文件),有写明路径时就是指定目录中找出文件(没找出可能还会尝试从搜索目录找出);如果最后依赖文件没找出时,就会从另一个同路径同名称的目标中尝试生成!总结:对于实目标和实依赖,都必须现场得出确定的路径,之后才可以进一步的操作。备注:Makefile 搜索目录为当前 Makefile 根目录及 VPATH、vpath 设置的目录。
目标与依赖的规则执行关系 只有目标文件不存在或依赖文件更新(依赖新于目标),才会去运行规则(执行他们下面的操作命令)!对于编译工作来说,依赖就是代码的源文件与头文件,只有依赖文件发生改变,才会编译更新目标,否则不执行(会提示:无更新或已更新)!如果想重新全编译,则需要删除全部中间目标文件再运行编译!
目标与依赖的包含文件关系 当使用 include 包含文件时,同时还会运行固有规则:搜索与包含文件同路径同名称的目标并尝试运行规则!具体有如下:只有当【包含文件存在】并且【没找到对应目标对应目标无依赖对应实目标新于依赖】才放弃运行规则,否则必定运行规则(它优先在用户操作的所有目标前执行以下规则:尝试创建或更新包含文件,并执行同路径同名称目标命令)。除上面情形外,当出现包含文件不存在、或找不到对应目标都会抛出错误!

3.1.3、Makefile 整体执行顺序:

整体执行顺序
1、读入主 Makefile
2、读入被include包含进来的其他 Makefile
3、初始化文件中的变量
4、推导隐性规则,并分析所有规则
5、为所有的目标文件创建依赖关系链
6、根据依赖关系,决定哪些目标要重新生成
7、执行生成命令

3.1.4、Makefile 几个重要特殊字符:$ # ; \ % * ? ~ -

特殊字符
作用
$$$$ 随机号,自动产生的一个随机编号
$$ 变量符,是访问一个 shell 命令中定义的变量(如:$$var),而非 makefile 的变量。如果某规则在同一个 shell 运行多条命令,则这些命令之间使用“;”连接起来,这样命令相互之间共享变量;否则(多行命令或空格分隔命令)相互之间为没有关联的 shell 命令,不能共享变量
$ 变量符,定义一串字符串,其相当于宏定义。如:$(abc) 表示变量 abc
# 注释符,如果通过 \# 转义则为普通的 # 字符
; 分隔符,命令与命令之间加入“;”,表示同一 shell 执行命令,可继承前面命令结果(典型例子:cd ~/123/ ; pwd 则结果是“~/123/”,否则是原目录下!)
\ 转义符换行符注意:换行符后面不能有任何字符,包括空格!
% 通配符,表示匹配零个或若干字符
* 所有符,表示任意一个或多个字符。如:*.c 表示所有 C 文件
? 任一符,表示任意一个字符 或 对新的变量或文件执行操作
~ 家目录。也可以指定用户目录(~xiaomin/test 表示用户 xiaomin 的宿主目录下的 test 目录),但 windows 系统需要由环境变量“HOME”决定

3.1.5、Makefile 命令前缀

命令前缀
作用
不加前缀 输出(打印)命令本身,输出(打印)命令执行的结果
@ 在命令前面加@,表示只输出命令执行的结果,不输出命令本身
- 在命令前面加-,表示忽略 Makefile 因命令出错而终止运行,典型应用:-include包含源码文件依赖信息,在源码文件依赖信息文件没创建(不存在)时忽略错误

3.1.6、Makefile 常用文件类型:

文件类型
unix/linux windows 描述
源文件 .c .c 源代码文件
关系文件 .d .d 源文件依赖关系信息,GNU 组织建议用编译器为每一个源文件自动生成依赖关系的内容放到一个文件中
中间文件 .o .obj 中间目标文件,是二进制机器码,把它们链接起来就是执行文件
库文件 .a .so .lib .dll 由于源文件太多,编译生成的中间目标文件太多,或是某个相对独立功能模块,可以打包为一个库文件
执行文件 .exe 执行文件(在 Windows 系统中编译生成的目标(PC 软件)必须要加.exe后缀,否则每次编译都会重新生成目标)

3.1.7、Makefile 的基本语法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
target: prerequisites   # 目标: 依赖(可多个文件)。(注:只要声明目标,则表明新的语段开始)
	command             # 命令,前面必须加【tab键】
	command1 ; command2 # 命令,同一[shell]执行命令,可继承前面命令结果(典型例子:cd ~/123/ ; pwd 则结果是 ~/123/,否则是原目录下!)
	command3   command4 # 命令,前后命令各自运行(多行命令也是如此!)(实质就是各自启动一个新 shell 运行命令,所以互不关联!)
	                    # 注意(1):上面是一个整体语段,中间不能插入诸如“xxx=abc”变量定义等非命令语句!!!
	                    # 注意(2):不建议在语句后面加注释,因为会影响语句表达,强烈建议在独立行编写注释!!
	                    # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
备注:
(1) 代码行后面加入注释,是为了解释每行语句的作用,实际编写 Makefile 脚本时不应
    这样注释,因为可能会影响语句表达甚至出错,所以建议测试例子时把代码行注释删掉。
(2) 实际上 Makefile 脚本语法要求要尽量避免每行文本代码后面存在空格等,因为可能
    会影响语句表达甚至出错,特别变量行尾,因为判断语句判断变量时后面空格一同判断。

3.1.8、Makefile 的演示例子:

编写 Makefile 文件内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
####【生成多个中间文件】####
objects = a.o b.o       # 备注:UNIX 中间文件为`.o`文件,在 Windows 则是`.obj`文件。
.PHONY:oobj             # (1) 表明 oobj 是一个伪目标,不是目标文件!
oobj: $(objects)        # (2) 因为 oobj 是一个伪目标,所以最终得到是其依赖:a.o b.o 中间文件【多个文件】。
$(objects): %.o: %.c    # 静态的多目标语句,展开后目标与依赖为【一对一】关系!
	gcc -c $< -o $@     # 逐一将依赖(`.c`文件)编译为目标(`.o`文件)(目标为:a.o b.o,依赖为:a.c b.c)
	@echo "$@ $^"       # 打印出目标与依赖(因上面是静态模式,所以 $^ 全部依赖只有一个对象)

####【伪目标的典型应用】####
.PHONY:clean            # 为了避免出错,使用【.PHONY】说明【clean】是一个伪目标(PHONY 读音 ˈfəʊni)
clean:
	rm -f *.i *.s *.o   # 删除编译生成的文件

在命令窗口运行命令及结果:

1
2
make oobj               # 运行命令,当前文件夹下的 a.c b.c 源文件编译为中间目标文件!(备注:要事先创建 a.c b.c 这两个文件,它们可以是空白文件)
make clean              # 运行命令,当前文件夹下扩展名为 *.i *.s *.o 的文件被删除!

3.1.9、Makefile 的常规语法汇总:

 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
28
29
cd subdir && $(MAKE) tag     # 调用文件,父 makefile 调用子 makefile 文件。具体:进入 subdir 目录,运行`make tag`命令,完成后返回原目录!
-include out/a.d             # 包含文件,导入依赖信息。备注:包含文件还会执行规则!
VPATH = src:hard/board       # 增加 make 文件搜索目录。备注:可用冒号或者空格隔开!

objs = out/a.o out/b.o       # 变量定义(赋值),还有":="、"?="、"+=",以及修饰符"override"和"export"。

main.o: main.c main.h        # 单目标多依赖语句,最常规的目标依赖语句。

main.o main.d: main.c main.h # 多目标多依赖语句,表示几个目标具有相同的依赖(集),只要依赖有变动,则几个目标都要更新与执行命令。

$(objs:.o=.c)                # 替换变量所有字段,将变量所有字段尾部的".o"替换为".c"。
$(objs:%.o=%.c)              # 同上
$(@:%.o=%.c)                 # 同上,对象为自动变量(目标)
                                                                ┌→必定是【$(objs)】指定的文件,【out/%.o】与【$(objs)】的路径必须是一样!
$(objs): out/%.o: %.c        # 静态的多目标语句,展开后目标与依赖为【一对一】关系!目标为指定目录[out/]的指定".d",依赖为 Makefile [搜索目录]中与目标[同名称]的".c"文件。
$(objs): out/%.o: src/%.c    # 同上,只是依赖也是指定路径!
                                                                ┌→必定是 Makefile【依赖关系链】中的文件,从【上级依赖】中获得!
out/%.d: %.c                 # 动态的多目标语句,展开后目标与依赖为【一对一】关系!目标为指定目录[out/]的某个".d",依赖为 Makefile [搜索目录]中与目标[同名称]的".c"文件。
out/%.d: src/%.c             # 同上,只是依赖也是指定路径!
                                                                ┌→必定是 Makefile【依赖关系链】中的文件,从【上级依赖】中获得!
%.d: %.c                     # 动态的多目标语句,展开后目标与依赖为【一对一】关系!目标为[当前根目录]中的某个".d",依赖为 Makefile [搜索目录]中与目标[同名称]的".c"文件。
%.d: %.c | mkdir_target      # 同上,并且在其〖所有操作前执行一次〗mkdir_target 目标的操作。这个对于操作前创建文件夹非常有用,它一般定义为伪目标,它下面命令为创建文件夹。

$(objs): $(objs:.o=.c)       # 多目标多依赖语句,展开后目标与依赖为【一对多】关系!目标为".o"文件,依赖为".c"文件。备注:每个目标具有相同的依赖(集)!

$(func arg1,arg2...)         # 操作函数,"func"为函数,"arg1"第一个参数,"arg2"第二个参数……

$(MAKECMDGOALS)              # make 命令的输入参数(例如:`make clean`命令,则输入参数为`clean`)

3.1.10、Makefile 编译多个文件写法:

 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
28
29
30
31
32
33
####################################################
# 方法一:
# 【动态的多目标语句】编译出多个目标文件,特点如下:
# 1、[目标文件]须【指出路径】,否则就是当前目录下;
# 2、[目标文件]从【上级依赖】中获得;
# 3、[依赖文件]从【搜索目录】下寻找;
# 4、[目标文件]与[依赖文件]为【同名】的不同类型文件;
# 5、[目标文件]与[依赖文件]为【一对一】关系。
####################################################
all: out/main.o out/foo.o

out/%.o: %.c
	gcc -c $< -o $@

####################################################
# 方法二:(--前提:make 的版本 3.81 或更高版本--)
# 【动态代码】生成多段编译语句,每段编译一个文件:
# 1、使用 define      实现一个伪函数体;(变量用 $$)
# 2、使用 $(call )    实现调用一个伪函数并传递参数;
# 3、使用 $(eval )    将表达式(字符串)转为脚本代码;
# 4、使用 $(foreach ) 从一串字符串中依次取出一个字
#    段作为入口参数并运行伪函数体。
####################################################
define FUNC
$1: $2
	gcc -c -o $$@ $$<
endef

SRCS_F := app/main.c usr/foo.c
all: $(addprefix out/,$(notdir $(SRCS_F:%.c=%.o)))

$(foreach v,$(SRCS_F),$(eval $(call FUNC,$(addprefix out/,$(notdir $(v:%.c=%.o))),$(v))))

3.1.11、Makefile 的错误信息解释:

错误信息解释
1、make: *** No rule to make target 'build/main.o', needed by 'build/main'. Stop
● 错误:脚本工具(make) 无法找到build/main.o依赖文件。
◆ 原因:依赖文件不在 Makefile 的搜索目录中,可能要增加搜索目录或要生成依赖文件。
2、app/main.c:2:19: fatal error: hello.h: No such file or directory
● 错误:编译工具(gcc)编译app/main.c没找到包含文件hello.h
◆ 解决:在编译命令加入包含路径选项,如:-I usr/inc
3、fatal error: cannot specify -o with -c, -S or -E with multiple files
● 错误:该项目将要生成两个或更多相同名称的.o文件,产生了冲突。
◆ 解决:使用$(eval $(call ……))编译语句,依赖写法由$$^改为$$<

3.1.11、网上资料

● 陈皓-跟我一起写 Makefile:《网友整理版》、《原作者博文》、《网友总结》、《网友讲解》。
● 李云-驾驭 Makefile:《原作者博文》、《网友网文》、《网友仓库》。

3.2、【变量:单行定义】

3.2.1、Makefile 的自动变量:(更多:网文

变量
作用
$< 代表当前规则中首个依赖文件,如果依赖目标是以模式(即“%”)定义的,则$<是符合模式的一系列的文件集
$^ 代表当前规则中所有依赖文件,并去除重复
$+ 代表当前规则中所有依赖文件(不去除重复)
$@ 代表当前规则中单个目标文件(因为多个目标最终被拆解为单个目标语句表达格式)
$* 不包含扩展名的目标文件名称(这个是 GNU make 特有的,其它的 make 不一定支持)
$(@D) 截取目标文件的目录部分,相当于函数$(dir $@)
$(@F) 截取目标文件的文件名部分,相当于函数$(notdir $@)
$(^D) 截取所有依赖目标文件中目录部分,并去除重复,相当于函数$(dir $^)
$(^F) 截取所有依赖目标文件中文件名部分,并去除重复,相当于函数$(notdir $^)
$(<D) $(<F) $(+D) $(+F) $(?D) $(?F) 原理同上
$? 所有比目标新的依赖集合,以空格分隔
$n 传递给脚本或函数的参数。n 是一个数字,表示第几个参数。例如,第一个参数是 $1,第二个参数是 $2
$$ $$var是在访问一个 shell 命令内定义的变量,而非 makefile 的变量。如果某规则在同一个 shell 运行多条命令,则这些命令之间使用;连接起来,这样命令相互之间共享变量;否则(多行命令或空格分隔命令)相互之间为没有关联的 shell 命令,不能共享变量

3.2.2、Makefile 的默认变量:(更多:整理版原版

变量
作用
$(AS) 汇编程序(默认为as
$(ASFLAGS) 汇编选项(默认为空)
$(CC) 变量:C  编译器(默认为cc
$(CPP) 变量:C  程序的预处理器(默认为$(CC) -E
$(CFLAGS) 变量:C  编译器选项参数(默认为空)
$(CXX) 变量:C++ 编译器(默认为g++
$(CXXFLAGS) 变量:C++ 编译器选项参数(默认为空)
$(AR) 变量:库文件打包命令(默认命令是ar
$(ARFLAGS) 变量:库选项参数(默认为空)
$(RM) 变量:删除文件命令(默认命令是rm -f
$(MAKE) 变量:当前使用的 make 命令工具(默认命令是make
$(MAKECMDGOALS) 变量:当前使用的 make 命令的输入参数(例如:make clean命令,则输入参数为clean
$(MAKEFLAGS) 变量:主要用于调用子 Makefile 时传递make命令参数(例如:cd subdir && $(MAKE) MAKEFLAGS=则表示不想往下层传递make命令任何参数)
$(SHELL) 变量:当前使用的 shell(命令终端,包括所在路径)!注意:不管是否export都会传递给子 Makefile!

3.2.3、Makefile 变量的名称要求:

用户变量的命名字可以包含字母、数字,下划线(可以是数字开头),但不应该含有:#=或是空字符(空格、回车等),并且不建议使用全部大写字母。其实变量只是一个宏定义,它代表一串字符串,Makefile 运行前将变量展开为最终的一串字符串,所以并不能在 Makefile 运行命令过程修改其值!

3.2.4、Makefile 变量的运算符:

运算符
使用
= 动态变量,会随引用变化
:= 静态变量,不随引用变化
?= 只有变量为空时才会赋值
+= 将字串追加到变量的尾端(自动加空格隔开)
override 变量强制覆盖的关键字!可避免 shell 传递来的同名变量修改其值
export 在父 Makefile 文件使用 export 声明变量,则可传给子 Makefile 文件使用其对应克隆变量。注意:子 Makefile 修改克隆变量的值不会影响父 Makefile 源变量的值!即:子 Makefile 不能通过变量传值给父 Makefile !补充:unexport 表示取消分享变量。

3.2.5、Makefile 变量的基本语法:

1
2
3
AA = abc    # 定义变量 AA (如果改用 := 表示定义的当前目标变量就固定下来,之后不会跟随其引用的变量变化而变化)
$(AA)       # $(AA) 代表 abc。(注意:`AA = abc`定义后面不能有任何多余字符,包括空格、注释等,否则影响执行!)
${AA}       # 同上。            !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

3.2.6、Makefile 变量的常规用法:

编写 Makefile 文件内容:

 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 特别注意:!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# 下面的`a = first`、`b = second`、`xx1 = a`、`xx2 = 1`后面不能有空格等,否则影响结果!

####【直接代替】####
CC = gcc                      # 定义变量 CC,则 $(CC) 代表 gcc(注:$(CC) 可以写成 ${CC})
objs := a.o b.o               # 定义变量 objs,则 $(objs) 代表 a.o b.o 
all: $(objs)                  # 这句告诉 make 工具:最终得到 a.o b.o 中间文件
$(objs): %.o: %.c             # 从 $(objects) 变量中制作出【一对一】关系的目标(`.o`文件)和依赖(`.c`文件)
	$(CC) -O2 $< -o $@        # 逐一将依赖(`.c`文件)编译为目标(`.o`文件)(目标为:a.o b.o,依赖为:a.c b.c)

####【变量追加】####
objs += fff.o                 # 追加变量,则此时最新变量就是 a.o b.o fff.o


####【字段替换】####
obj3  := a.o b.o c.o d.s      # 下面替换的最终结果为:a.c b.c c.c d.s
src2 := $(obj3:.o=.c)         # 将变量 $(obj3)【全部】定义的文件名称尾部的".o"替换为".c" 并取出传递给 src2
src1 := $(obj3:%.o=%.c)       # 将变量 $(obj3)【全部】定义的文件名称尾部的".o"替换为".c" 并取出传递给 src1

####【字符替换】####
first_second = Hello
a = first
b = second
src3 = $($a_$b)               # 则“$a_$b”组成了“first_second”,于是 $(src3) 的值就是“Hello”
                              # 注意:变量定义后面不能有任何字符(包括空格、注释等),否则这些字符也被替换进去!!!
                              # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
a_objs := a.o b.o c.o
1_objs := 1.o 2.o 3.o
xx1 = a
xx2 = 1
src4 := $($(xx1)_objs:.o=.c)  # 表示将目标 a_objs 的源文件名称尾部的 .o 替换为 .c 并传给 src4
src5 := $($(xx2)_objs:.o=.c)  # 表示将目标 1_objs 的源文件名称尾部的 .o 替换为 .c 并传给 src5
                              # 注意:变量定义后面不能有任何字符(包括空格、注释等),否则这些字符也被替换进去!!!
                              # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
test:
	@echo "objs: $(objs)"     # 打印信息为 objs: a.o b.o fff.o (如果 @echo 改为 echo 则表示本行字符命令也打印出来)
	@echo "src1: $(src1)"     # 打印信息为 src1: a.c b.c c.c d.s
	@echo "src2: $(src2)"     # 打印信息为 src2: a.c b.c c.c d.s
	@echo "src3: $(src3)"     # 打印信息为 src3: Hello
	@echo "src4: $(src4)"     # 打印信息为 src4: a.c b.c c.c
	@echo "src5: $(src5)"     # 打印信息为 src5: 1.c 2.c 3.c

在命令窗口运行命令及结果:

1
2
3
4
5
6
7
make test                     # 运行命令,结果如下:
objs: a.o b.o fff.o
src1: a.c b.c c.c d.s
src2: a.c b.c c.c d.s
src3: Hello
src4: a.c b.c c.c
src5: 1.c 2.c 3.c

3.2.7、Makefile 变量的覆盖用法:

编写 Makefile 文件内容:

1
2
3
4
5
6
7
8
9
######普通变量######
src1 := a.c b.c c.c
all1:
	@echo "src1: $(src1)"

######变量覆盖######
override src2 := a.c b.c c.c
all2:                    # 上行`override`是变量强制覆盖关键字!!!!!!!!
	@echo "src2: $(src2)"

make 命令执行结果:

1
2
3
4
5
6
7
######普通变量######
make all1 src1=abc       # 运行命令,
src1: abc                # 运行结果(Makefile 变量无法覆盖 shell 命令行变量[src1])。

######变量覆盖######
make all2 src2=abc       # 运行命令,
src2: a.c b.c c.c        # 运行结果(Makefile 变量[src2]强制覆盖 shell 命令行变量)。
3.3、【码块:多行定义】

3.3.1、Makefile 多行定义的基本语法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
define AA             # 定义 AA 变量为代码块
command               # 命令,备注:前面无须加【tab键】
endef                 # 注意:多行定义里面不能有条件判断语句,否则make工具直接抛出错误!

$(AA)                 # $(AA) 代表 代码块

# 特别用途-------------
define LFTAB          # 定义 LFTAB 为【换行+Tab格】,辅助将一行字符串转换为多行字符串命令(替换或插入操作)!


endef

3.3.2、Makefile 多行定义的常规用法:

编写 Makefile 文件内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 定义删除命令
define RUN_RM
rm -f $(objects)
rm -f 1.o 2.o
endef

# make操作目标
objects = a.o b.o
.PHONY:clean
clean:
	$(RUN_RM)

在命令窗口运行命令及结果:

1
make clean              # 运行命令,当前文件夹下的 a.o b.o 1.o 2.o 文件被删除!
3.4、【条件:判断语句】

3.4.1、Makefile 条件的基本语法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
ifeq (arg1,arg2)      # 判断 arg1 与 arg2 是否相同
else
endif

ifneq (arg1,arg2)     # 判断 arg1 与 arg2 是否不相同
else
endif

ifdef arg             # 判断 如果定义了变量 arg
else
endif

ifndef arg            # 判断 如果没有定义变量 arg
else
endif

3.4.2、Makefile 条件的常规用法:

编写 Makefile 文件内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 特别注意:!!!!!!!!!!!!!!!!!!!!
# 下面的`CC = gcc`后面不能有空格等,否则影响结果!
CC = gcc
objects = a.o b.o
.PHONY:clean          # 为了避免出错,使用【.PHONY】说明【clean】是一个伪目标
clean:                # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
ifeq ($(CC),gcc)      # 判断 $(CC) == gcc(注意:`CC = gcc`定义后面不能有任何多余字符,包括空格、注释等!!)
ifdef objects         # 等同 ifneq ($(objects),)
	rm -f $(objects)  # 删除编译生成的中间文件
endif
else
ifdef objects 
	rm -f $(objects:.o=.obj)
endif
endif

在命令窗口运行命令及结果:

1
make clean              # 运行命令,当前文件夹下的 a.o b.o 文件被删除!
3.5、【函数:功能函数】

在 Makefile 中可以使用函数来处理变量,从而让我们的命令或是规则更为的灵活和智能。make 所支持的函数也不算很多,不过已经足够我们的操作了。函数调用后,函数的返回值可以当做变量来使用。对于 makefile 函数处理的字符串,不应有$回车 换行等控制字符,而且有些符号需要加\符进行转义,例如\\。对于 makefile 函数处理字符串的基本单元为字段,它以空格 Tab作为分割。备注:对于 makefile 函数的控制参数,%是通配符还是普通字符,由具体函数决定!

特殊字符 作用
% 通配符,匹配零或若干字符
\ 转义符换行符(注意:作为换行符后面不能有空格!)
$ 变量符

3.5.1、函数基本语法:

1
2
3
$(func arg1,arg2…)             # 函数名和参数之间以“空格”分隔,参数与参数之间以“逗号”分隔。 
${func arg1,arg2…}             # 同上

3.5.2、『字符串、字符段』操作函数:

 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# --------------------------------------------------------------------------------------------------------
# 一、字符串/字符段操作函数 ------------------------------------------------------------------------------
# --------------------------------------------------------------------------------------------------------

# 01.〖字符串:查找〗函数       (★注意:输入参数的“%”只作为“普通字符”★)
$(findstring find_str,str)      # 在“str”字串中查找“find_str”字符串,如果找到则返回“find_str”原字符串(表示“真”的意思),否则返回空字符(表示“假”的意思)
                                # $(findstring a b,a b c) 在“a b c”字串中查找“a b”字串,返回“a b”->“真”
                                # $(findstring a b,b c)   在“b c”  字串中查找“a b”字串,返回“”---->“假”(空字符)

# 02.〖字符串:替换〗函数       (★注意:输入参数的“%”只作为“普通字符”★)
$(subst from,to,str)            # 把“str”中的“from”字符串替换为“to”,返回替换后的字符串
                                # $(subst is i,IS I,this is a dog) “this is a dog”替换后为“thIS Is a dog”

# -------------------------------

# 03.【字符段:替换】函数       (★注意:同一控制参数的第一个`%`为通配符,后面的`%`都是普通字符★)
$(patsubst from,to,str)         # 把“str”中的“from”字段替换为“to”,返回替换后的字符串
                                # $(patsubst %.c,%.o,x.c.c aaa.c) “x.c.c aaa.c”替换后为“x.c.o aaa.o”(只能使用“%”通配符)

# 04.【字符段:筛选】函数       (★注意:同上★)
$(filter pat1 par2 ...,str)     # 从“str”中过滤掉【非“pat1”“par2”...】字段
                                # $(filter %.c %.s,aaa.c bbb.c ccc.s ddd.h) “aaa.c bbb.c ccc.s ddd.h”筛选后返回“aaa.c bbb.c ccc.s”

# 05.【字符段:过滤】函数       (★注意:同上★)
$(filter-out pat1 par2 ...,str) # 从“str”中过滤掉【“pat1”“par2”...】字段
                                # $(filter-out %.c %.s,aaa.c bbb.c ccc.s ddd.h) “aaa.c bbb.c ccc.s ddd.h”过滤后返回“ddd.h”

# -------------------------------

# 06.取出【首个字段】函数
$(firstword str)                # 取出“str”字符串中的首个字段
                                # $(firstword aaa bbb)返回值是“aaa”

# 07.取出【某个字段】函数
$(word n,str)                   # 取出“str”字符串中第【n】个字段
                                # $(word 2, aaa bbb ccc)返回值是“bbb”

# 08.取出【多个字段】函数
$(wordlist s,e,str)             # 取出“str”字符串中第【s~e】个字段
                                # $(wordlist 2, 3, aaa bbb ccc)返返回值是“bbb ccc”

# 09.统计【字段个数】函数
$(words str)                    # 统计“str”字符串中的字段个数
                                # $(words, aaa bbb ccc)返回值是“3”

# 10.排序【全部字段】函数
$(sort str)                     # 给“str”多个字段排序,返回排序后的字符串,同时去除相同字段!!!
                                # $(sort bbb bbb aaa ccc) 返回“aaa bbb ccc”

# 11.整理【全部字段】函数      (★备注:去除多余空字符,空字符包括空格、[Tab]等不可显示字符★)
$(strip str)                    # 把“str”字串中【多余空字符】格式化为各一个【空格】分隔
                                # $(strip a 	b % c ) “a 	b % c ”去除多余的空格,结果是“a b % c”

# 12.合并【纵向字段】函数
$(join str1,str2)               # 把“str1”和“str2”字符串的全部字段进行纵向合并
                                # $(join aaa	bbb , 111 222 333) 返回“aaa111 bbb222 333”

3.5.3、『文件路径、名称』操作函数:

 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
28
29
30
31
32
33
34
35
36
# --------------------------------------------------------------------------------------------------------
# 二、文件路径/名称操作函数 ------------------------------------------------------------------------------
# --------------------------------------------------------------------------------------------------------

# 13.取【路径】函数
$(dir str)                      # 取出“str”字符串各个字段的路径
                                # $(dir src/aaa.c bbb.c) 返回“src/ ./”

# 14.取【除路径】函数
$(notdir str)                   # 取出“str”字符串各个字段的除去路径内容
                                # $(notdir src/aaa.c bbb) 返回“aaa.c bbb”

# 15.取【扩展名】函数
$(suffix str)                   # 取出“str”字符串各个字段的扩展名(包括除“.”)
                                # $(suffix src/aaa.c bbb.c fff) 返回“.c .c”

# 16.取【除扩展名】函数
$(basename str)                 # 取出“str”字符串各个字段的除去扩展名内容(不取“.”)
                                # $(basename src/aaa.c bbb) 返回“src/aaa bbb”

# 17.加【前缀】函数
$(addprefix prefix,str)         # 加入“str”字符串各个字段的前缀(例如:加路径)
                                # $(addprefix src/,fff/aaa.c bbb) 返回“src/fff/aaa.c src/bbb”

# 19.加【后缀】函数
$(addsuffix suffix,str)         # 加入“str”字符串各个字段的后缀(例如:加扩展名)
                                # $(addsuffix .c,src/aaa bbb.) 返回“src/aaa.c bbb..c”

# 19.列出【路径+文件】函数
$(wildcard str1,str2,...)       # 列出“str1”“str2”… 路径下的对应文件(需要通配符`*`配合使用)
                                # $(wildcard *.c ./src/*.c) 返回“aaa.c bbb.c ./src/ccc.c”
# 20.得到【绝对路径】函数
$(realpath path)                # 将【相对路径】转换成【绝对路径】,会判断文件和目录是否存在,如果不存在,则返回空。
$(abspath path)                 # 将【相对路径】转换成【绝对路径】,不判断文件和目录是否存在。
                                # $(realpath .) 返回当前路径的绝对路径,例如“/home/o2ospring/Downloads/makefile_demo”
                                # $(abspath .)  返回当前路径的绝对路径,例如“/home/o2ospring/Downloads/makefile_demo”

3.5.4、『相关扩展、控制』操作函数:

 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# --------------------------------------------------------------------------------------------------------
# 三、相关扩展/控制操作函数 ------------------------------------------------------------------------------
# --------------------------------------------------------------------------------------------------------

# 21.【调用shell命令】函数
$(shell shell_cmd)              # 调用 shell 命令“shell_cmd”运行并将返回信息传递给 Makefile 脚本(因要启动另一个 shell,会影响系统性能!!!)
                                # mk_val = $(shell pwd)   结果“/home/xiaomin/test”(当前所在目录)信息传递给 Makefile 变量 mk_val
                                # $(shell gcc -MM main.c) 结果“main.o: main.c main.h”

# 22.【变量参数化】函数
var = $(1) $(2) ...             # $(1)表示取第一个参数“str1”,$(2)表示取第二个参数“str2”,……
$(call var,str1,str2,...)       # 将 var 视为函数(调用),将 str1 视为第一个参数,将 str2 视为第二个参数,则将上行表达式视为函数体!
                                # v3 = $(3) $(1) $(2)
                                # $(call v3,bb bb,cc,aa)  结果“aa bb bb cc”(注意:bb bb 是一个参数,因为没有用逗号分开!)
                                # $(call v3,$(shell pwd)) 结果“/home/xiaomin/test”(因其它两个参数为空,即只会得出一个参数)
# 23.【条件判断】函数
$(if true,true_str1)            # 如果“true”为非空字符(真),则返回“true_str1”表态式,否则返回“空”
$(if true,true_str1,false_str2) # 如果“true”为非空字符(真),则返回“true_str1”表态式,否则返回“false_str2”表态式
                                # v1 = 0
                                # $(if v1,$(shell pwd))   结果“/home/xiaomin/test”
                                # $(if v1,if ...,else..)  结果“if ...”
                                # $(if $(strip $(SRCS_F)),,$(error No source files)) 表示如果源文件没定义则提示出错信息并停止运行
# 24.【循环处理】函数
$(foreach v,str,text)           # 将“str”中的字段逐次取出缓存到“v”再传给“text”表达式处理,最后返回整串字符串(其中 v 为临时变量)
                                # $(foreach v,a b c,$(v).o) 结果“a.o b.o c.o”

# 25.【动态代码】函数
$(eval text)                    # 将“text”的内容将作为 makefile 的一部分而被 make 解析和执行。(注意:text 中应要使用 $$ 来表示 make 的变量符 $)
                                # 通过 $(eval ) $(call ) $(foreach ) 可实现更加灵活地生成 make 代码,并且可实现向生成代码传递参数!!!
                                # define FUNC
                                # OBJ_PATH := out
                                # obj = $$(OBJ_PATH)/$(strip $1)
                                # $$(obj): $2
                                # 	gcc $$< -o $$@
                                # endef
                                # 
                                # all : out/foo
                                # 
                                # $(eval $(call FUNC,foo,main.c))    # FUNC 宏就像是函数,foo 为第一个参数,main.c 第二个参数,在代码内分别用 $1 $2 来表示。
# -------------------------------

# 26.【错误控制】函数
$(error msg)                    # 显示“msg”错误信息及强行停止 Makefile 运行
                                # $(error error message!) 结果“error message!.  Stop.”错误信息显示及停止运行


# 27.【警告控制】函数
$(warning msg)                  # 显示“msg”警告信息但不会停止 Makefile 运行
                                # $(warning warning message!) 结果“warning message!”警告信息显示

备注:make 命令执行后有三个退出码:
0 -- 表示成功执行,则返回【0】。
1 -- 如果 make 运行时出现任何错误,则返回【1】。
2 -- 如果你使用了make的“-q”选项,并且 make 使得一些目标不需要更新,则返回【2】。

# -------------------------------  

# 28.【变量来源】函数
val_in_file := test_file
override val_override := test_override

$(origin not_define)    # 结果“undefined”    变量没有定义!!!!!
$(origin PATH)          # 结果“environment”  shell  的环境变量
$(origin val_in_cmd)    # 结果“command line” make 命令参数变量(补充:运行命令为 make val_in_cmd=val_cmd)
$(origin val_in_file)   # 结果“file”         Makefile 用户变量
$(origin CC)            # 结果“default”      Makefile 默认变量
$(origin val_override)  # 结果“override”     Makefile 中使用 override 重新定义的变量
$(origin @)             # 结果“automatic”    Makefile 自动变量(如:$(@))

3.5.5、相关函数例子:

──点击展开例子──

首先说明,本例子只是展示函数的功能,使用@echo命令打印出其输出结果。
──测试方法──
1)Makefile 文件创建:在一目录下创建一个文件名称为 Makefile 或 makefile,并编写内容。
2)Makefile 操作方法:在命令终端里,进入 makefile 所在目录,输入make tN运行(N 为数字)。
──再次强调──
1、对于 makefile 函数处理字符串的基本单元为字段,它以空格 Tab作为分割。
2、对于 makefile 函数的控制参数,%是通配符还是普通字符,由具体函数决定!

  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
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# --------------------------------------------------------------------------------------------------------
# 一、字符串/字符段操作函数 ------------------------------------------------------------------------------
# --------------------------------------------------------------------------------------------------------

# 01.〖字符串:查找〗函数 -------------------------
words := the other the% ath%
t1:
	@echo "%th%  findstring: $(findstring %th%,$(words))"
	@echo "he   findstring: $(findstring he,$(words))"
	@echo "he%   findstring: $(findstring he%,$(words))"
	@echo "other t findstring: $(findstring other t,$(words))"
# 运行命令:make t1
# 输出结果:
# %th%  findstring:          # 表示“假”(★注:“%”只作“普通字符”)
# he   findstring: he       # 表示“真”
# he%   findstring: he%      # 表示“真”(★注:“%”只作“普通字符”)
# other t findstring: other t  # 表示“真”

# 02.〖字符串:替换〗函数 -------------------------
sources := fun.c foo.c \
           main.c test.c.o
t2:
	@echo "subst:$(subst .c,.o,$(sources))"
	@echo "subst:$(subst %.c,.o,$(sources))"
	@echo "subst:$(subst .c f,.o f,$(sources))"
# 运行命令:make t2
# 输出结果:
# subst:fun.o foo.o main.o test.o.o
# subst:fun.c foo.c main.c test.c.o
# subst:fun.o foo.c main.c test.c.o

##################################################

# 03.【字符段:替换】函数 -------------------------
sources := fun.c foo.c \
           main.c test.c.o
t3:
	@echo "patsubst:$(patsubst .c,.o,$(sources))"
	@echo "patsubst:$(patsubst %.c,.o,$(sources))"
	@echo "patsubst:$(patsubst %.c,%.o,$(sources))"
# 运行命令:make t3
# 输出结果:
# patsubst:fun.c foo.c main.c test.c.o
# patsubst:.o .o .o test.c.o
# patsubst:fun.o foo.o main.o test.c.o

# 04.【字符段:筛选】函数 -------------------------
words := he the thee athee ffth hen other the% ggthxxaa ggth%aa
t4:
	@echo "he   filter: $(filter he,$(words))"
	@echo "th%  filter: $(filter th%,$(words))"
	@echo "%th  filter: $(filter %th,$(words))"
	@echo "%th%aa filter: $(filter %th%aa,$(words))"
# 运行命令:make t4
# 输出结果:
# he   filter: he
# th%  filter: the thee the%
# %th  filter: ffth
# %th%aa filter: ggth%aa

# 05.【字符段:过滤】函数 -------------------------
words := he the thee athee ffth hen other the% ggthxxaa ggth%aa
t5:
	@echo "he   filter-out: $(filter-out he,$(words))"
	@echo "th%  filter-out: $(filter-out th%,$(words))"
	@echo "%th  filter-out: $(filter-out %th,$(words))"
	@echo "%th%aa filter-out: $(filter-out %th%aa,$(words))"
# 运行命令:make t5
# 输出结果:
# he   filter: the thee athee ffth hen other the% ggthxxaa ggth%aa
# th%  filter: he athee ffth hen other ggthxxaa ggth%aa
# %th  filter: he the thee athee hen other the% ggthxxaa ggth%aa
# %th%aa filter: he the thee athee ffth hen other the% ggthxxaa

##################################################

# 06.取出【首个字段】函数 ------------------------
CURRENT_PATH := $(subst /, ,$(PWD))
t6:
	@echo "current path: $(PWD)"
	@echo "current path first word: $(firstword $(CURRENT_PATH))"
# 运行命令:make t6
# 输出结果:
# current path: /home/liao/makefile_test
# current path first word: home

# 07.取出【某个字段】函数 ------------------------
CURRENT_PATH := $(subst /, ,$(PWD))
t7:
	@echo "current path: $(PWD)"
	@echo "current path second word: $(word 2,$(CURRENT_PATH))"
# 运行命令:make t7
# 输出结果:
# current path: /home/liao/makefile_test
# current path second word: liao

# 08.取出【多个字段】函数 ------------------------
CURRENT_PATH := $(subst /, ,$(PWD))
t8:
	@echo "current path: $(PWD)"
	@echo "current path second word: $(wordlist 2,50,$(CURRENT_PATH))"
# 运行命令:make t8
# 输出结果:
# current path: /home/liao/makefile_test
# current path second word: liao makefile_test

# 09.统计【字段个数】函数 ------------------------
CURRENT_PATH := $(subst /, ,$(PWD))
t9:
	@echo "current path: $(PWD)"
	@echo "current path words: $(words $(CURRENT_PATH))"
# 运行命令:make t9
# 输出结果:
# current path: /home/liao/makefile_test
# current path words: 3

# 10.排序【全部字段】函数 -----------------------
source := b c 1 2 d   0 	c
t10:
	@echo "sort:$(sort $(source))"
# 运行命令:make t10
# 输出结果:
# sort:0 1 2 b c d

# 11.整理【全部字段】函数 -----------------------
t11:
	@echo "strip: $(strip a  	b		c x  f  )"  # 包含“Tab”“空格”
# 运行命令:make t11
# 输出结果:
# strip: a b c x f

# 12.连接【纵向字段】函数 -----------------------
t12:
	@echo "join: $(join aaa	bbb , 111 222 333)"
# 运行命令:make t12
# 输出结果:
# join: aaa111 bbb222 333

# --------------------------------------------------------------------------------------------------------
# 二、文件路径/名称操作函数 ------------------------------------------------------------------------------
# --------------------------------------------------------------------------------------------------------

待续……

# --------------------------------------------------------------------------------------------------------
# 三、相关扩展/控制操作函数 ------------------------------------------------------------------------------
# --------------------------------------------------------------------------------------------------------

# 22.【变量参数化】函数 -------------------------
define target
	@echo $0
	@echo $1
	@echo $2
endef

.PHONY: t22
t22:
	$(call target,hello,work)

# 运行命令:make t22
# 输出结果:
# target
# hello
# work

待续……

3.6、【文件:包含调用】

3.6.1、Makefile 文件〖包含〗的基本语法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 注意:include 前【绝不能】用[Tab键]开始
include file    # 把其它 Makefile 包含进来,file 是包含进来的文件(可多个文件,也可包括路径)
                # 注意:【包含】不但是把其它 Makefile 代码拷贝过来,还会执行固有的规则!

关于 include 有两项动作:
(1) 包含:把别的 Makefile 代码拷贝过来,相当于在自身编写这些代码一样。
(2) 规则:搜索与包含文件同路径同名称的目标并尝试运行规则!具体有如下:
    只有当【包含文件存在】并且【没找到对应目标  对应目标无依赖  
	对应实目标新于依赖】才放弃运行规则,否则必定尝试运行规则(它优先在
    用户操作的所有目标前执行:尝试创建或更新包含文件,并执行同路径同
    名称目标命令)。除上面情形外,当出现包含文件不存在、或找不到对应目
    标都会抛出错误!
──点击展开 include 执行固有规则说明──

一、使用-include包含文件,没有与包含文件同路径同名称的目标:

脚本内容:

1
2
3
4
5
6
.PHONY: all  
 
all:  
	@echo "1.run all..."  
 
-include a.txt  

脚本运行:

1
2
3
4
5
6
7
8
rm -f a.txt       # 运行命令,目的是为了保证 a.txt 不能存在  
make              # 运行命令,执行脚本  
#                 # 运行结果:■■不运行规则■■。  
# 1.run all...    # 看到脚本成功执行`all`目标,因`-`会忽略包含文件不存在影响!相当于没有 include 操作!  
touch a.txt       # 运行命令,创建 a.txt 文件。  
make              # 运行命令,执行脚本  
#                 # 运行结果:■■不运行规则■■。  
# 1.run all...    # 看到脚本成功执行`all`目标,即:【包含文件存在】并且【没找到对应目标】才放弃运行规则●●●●●  

二、使用include包含文件,没有与包含文件同路径同名称的目标:

脚本内容:

1
2
3
4
5
6
.PHONY: all  
 
all:  
	@echo "1.run all..."  
 
include a.txt  

脚本运行:

1
2
3
4
5
6
7
8
9
rm -f a.txt       # 运行命令,目的是为了保证 a.txt 不能存在  
make              # 运行命令,执行脚本  
#                 # 运行结果:■■■运行规则■■。出错!因没有找`a.txt`包含文件并且没有同名目标而中止运行!  
# Makefile:6: a.txt: No such file or directory  
# make: *** No rule to make target 'a.txt'.  Stop.  
touch a.txt       # 运行命令,创建 a.txt 文件。  
make              # 运行命令,执行脚本  
#                 # 运行结果:■■不运行规则■■。  
# 1.run all...    # 看到脚本成功执行`all`目标,即:【包含文件存在】并且【没找到对应目标】才放弃运行规则●●●●●  

三、使用include包含文件,存在与包含文件同路径同名称的实目标:

脚本内容:

1
2
3
4
5
6
7
8
9
.PHONY: all  
 
all:  
	@echo "1.run all..."  
 
include a.txt  
 
a.txt:  
	@echo "2.run a.txt..."  

脚本运行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
rm -f a.txt       # 运行命令,目的是为了保证 a.txt 不能存在  
make              # 运行命令,执行脚本  
#                 # 运行结果:■■■运行规则■■。虽找不出包含文件(a.txt),但仍然运行规则!!!!  
# Makefile:6: a.txt: No such file or directory  
# 2.run a.txt...  # 先运行与包含文件同路径同名称的目标(a.txt)◆◆◆◆◆  
# 1.run all...    # 再运行用户操作的目标(all)  
touch a.txt       # 运行命令,创建 a.txt 文件。  
make              # 运行命令,执行脚本  
#                 # 运行结果:■■不运行规则■■。  
# 1.run all...    # 看到脚本只执行`all`目标,即:【包含文件存在】并且【找到同路径同名称[目标无依赖]】才放弃运行规则●●●●●  

四、使用include包含文件,存在与包含文件同路径同名称的实目标,以及目标有依赖文件:

脚本内容:

1
2
3
4
5
6
7
8
9
.PHONY: all  
 
all:  
	@echo "1.run all..."  
 
include a.txt  
 
a.txt: b.txt  
	@echo "2.run a.txt..."  

脚本运行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
rm -f a.txt b.txt # 运行命令,目的是为了保证 a.txt 和 b.txt 不能存在  
make              # 运行命令,执行脚本  
#                 # 运行结果:■■■运行规则■■。虽找不出包含文件(a.txt),但仍然运行规则,不过因依赖文件(b.txt)不存在抛出错误并结束运行!!!  
# Makefile:6: a.txt: No such file or directory  
# make: *** No rule to make target 'b.txt', needed by 'a.txt'.  Stop.  
rm -f a.txt b.txt # 运行命令,删除文件  
touch a.txt       # 运行命令,先创建目标文件(a.txt)┐  
touch b.txt       # 运行命令,再创建依赖文件(b.txt)┴→【即:(a.txt)旧于(b.txt)】  
make              # 运行命令,执行脚本  
#                 # 运行结果:■■■运行规则■■。  
# 2.run a.txt...  # 先运行与包含文件同路径同名称的目标(a.txt)◆◆◆◆◆  
# 1.run all...    # 再运行用户操作的目标(all)  
rm -f a.txt b.txt # 运行命令,删除文件  
touch b.txt       # 运行命令,先创建目标文件(b.txt)┐  
touch a.txt       # 运行命令,再创建依赖文件(a.txt)┴→【即:(a.txt)新于(b.txt)】  
make              # 运行命令,执行脚本  
#                 # 运行结果:■■不运行规则■■。  
# 1.run all...    # 看到脚本只执行`all`目标,即:【包含文件存在】并且【找到同路径同名称[实目标新于依赖]】才放弃运行规则●●●●●  

总结:Makefile 包含文件时,不但是把其它 Makefile 代码拷贝过来,还会搜索与包含文件同路径同名称的目标并尝试运行规则!具体有如下:只有当【包含文件存在】并且【没找到对应目标对应目标无依赖对应实目标新于依赖】才放弃运行规则,否则必定运行规则(它优先在用户操作的所有目标前执行:尝试创建或更新包含文件,并执行同路径同名称目标命令)。除上面情形外,当出现包含文件不存在、或找不到对应目标都会抛出错误!

3.6.2、Makefile 文件〖包含〗的常规用法:

【父】Makefile 文件内容:

1
2
3
4
5
6
7
8
####包含子Makefile文件####
include ./subdir/Subfile

aaaa = 1.i 2.i
.PHONY:clean
clean: clean_1 clean_2
clean_1:
	rm -f x.c y.c              # 删除 x.c y.c 文件 

【子】Subfile 文件内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
############################
# 此文件在`subdir`子文件夹下
############################

all_1: a.c b.c
	gcc $^ -o $@
	                           # 编译 a.c b.c 为 all_1 执行文件
.PHONY:clean_2 
clean_2:  
	rm -f *.o $(aaaa)          # 删除编译的中间文件

在命令窗口运行命令及结果:

1
2
3
make clean                     # 运行命令,
                               # 结果:删除【当前文件夹】下的 x.c y.c 1.i 2.i 文件及扩展名为 *.o 的文件!
                               # 注意:命令`rm -f *.o $(aaaa)`并不是对`subdir`子文件夹操作!

3.6.3、Makefile 文件【调用】的基本语法:

1
2
3
# 注意:调用其实就是直接运行命令,所以行首记得加[Tab]键
	cd subdir && $(MAKE) tag  # 表示进入 subdir 目录,并且运行`$(MAKE) tag`命令(默认命令是`make`,即 `make tag`)
	$(MAKE) tag -C subdir     # 同上

3.6.4、Makefile 文件【调用】的常规用法:

【父】Makefile 文件内容:

1
2
3
4
5
6
7
8
9
all_1:
	cd subdir && $(MAKE) all_1
	                           # 表示调用下级目录 subdir 的 Makefile 文件,执行其目标 all_1
	                           # $(MAKE) 为当前使用的 make 命令工具(默认命令是`make`)(等同 cd subdir && make all_1)
all_2:
	$(MAKE) clean_2 -C subdir
	                           # 表示调用下级目录 subdir 的 Makefile 文件,执行其目标 clean_2
	                           # $(MAKE) 为当前使用的 make 命令工具(默认命令是`make`)(等同 make clean_2 -C subdir)
	rm -f x.c y.c              # 同时删除当前父级文件夹下的 x.c y.c 文件

【子】Makefile 文件内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
############################
# 此文件在`subdir`子文件夹下
############################

all_1: a.c b.c
	gcc $^ -o $@
	                           # 编译 a.c b.c 为 all_1 执行文件
.PHONY:clean_2 
clean_2:  
	rm -f *.i *.s *.o
	                           # 删除编译的中间文件

在命令窗口运行命令及结果:

1
2
3
4
make all_1                     # 运行命令,编译【subdir】文件夹下的 a.c b.c 源文件!
make all_2                     # 运行命令,结果【subdir】文件夹下扩展名为 *.i *.s *.o 的文件被删除,
                               #             以及当前父级文件夹下的 x.c y.c 文件被删除!!!!!!!
							   
3.7、【路径:搜索目录】

3.7.1、Makefile 文件【搜索目录】的基本语法:

1
2
3
4
5
6
# 方式一:
VPATH = directories            # `添加`搜索目录<directories>,多个目录用冒号或者空格隔开。备注:VPATH 是`变量`!
# 方式二:
vpath pattern directories      # `添加`指定<pattern>文件类型的搜索目录为<directories>,多个目录用冒号或者空格隔开。
vpath pattern                  # `清除`指定<pattern>文件类型的搜索目录。
vpath                          # `清除所有`已被设置好了的文件搜索目录。

3.7.2、Makefile 文件【搜索目录】的常规用法:

1
2
3
4
5
VPATH = src:hard/board   # 设置相关文件可能所在的目录"src"和"hard/board"
vpath %.c src:hard/board # 增加".c"源文件可能所在的目录"src"
vpath %.h include        # 增加".h"头文件可能所在的目录"include"
vpath % modules          # 增加"任意"文件可能所在的目录"modules"
vpath %.cpp              # 清除之前增加的".cpp"源文件全部目录

3.7.3、Makefile 文件【VPATH】与【vpath】要点说明:

1、在实际应用工程中,有大量的源文件和头文件存放在不同的目录中,如果不指出搜寻目录,很可能出现某些文件找不到文件的情况!
2、VPATH 是变量,vpath 只是关键字!vpath 的功能稍强以及更细化更为灵活一些。
3、VPATH、vpath 只对 make 工具起作用,但对其调用的其它工具(如:gcc)不起作用!
4、对于使用 vpath 为同一类型文件设置多个搜索目录时,强烈建议使用一个 vpath 设置,不建议使用多个 vpath 来设置。因为某环境下可能是“追加”搜索目录,但另一环境下则可能是“取代”之前设置的搜索目录!

3.8、【基本 makefile】

makefile 脚本基础写法:(展示例子:1_micro

 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
28
29
30
31
32
####################################################
# 1、本案例展示多个文件编译和链接。
# 2、无论我们写再复杂的 Makefile,最终展开所有变量和
#    脚本语句后,与本例 makefile 展示的形式都是一样,
#    不一样的只是展开的目标更多、每个目标的依赖更多、
#    每个目标的操作命令更多而已!
# 3、补充说明:脚本所有内容(包括变量、函数等)展开后
#    都是一组字符串,最终就不存在变量这类的内容了!
####################################################

.PHONY: all
all: main
main:    hello.o test.o main.o
	gcc  hello.o test.o main.o -o main  # 链接

hello.o: hello.c hello.h
hello.o: hello.c
	gcc -c hello.c -o hello.o           # 编译

test.o: test.c test.h
test.o: test.c
	gcc -c test.c -o test.o             # 编译

main.o: main.c main.h hello.h test.h
main.o: main.c
	gcc -c main.c -o main.o             # 编译
#	@echo $^                            # 如果开启打印,就会发现【$^】为【main.c main.h hello.h test.h】!

.PHONY: clean                           # 为了避免出错,使用 .PHONY 说明 clean 是一个伪目标
clean:
	rm -f main.o test.o hello.o main    # 删除编译生成的文件

../img/20200915_23.jpg
从此图可以理解 makefile 为什么与编译过程是倒过来写的。它与我们访问文件夹路径是一样的,先是根目录,再下一级目录,这样可以方便 make 工具快速分析出 makefile 里面要操作的文件的依赖关系!注意执行的次序则是反过来:先执行最深层的依赖,再逐级往上执行目标!

Makefile 编译多个文件写法:

 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
28
29
30
31
32
33
####################################################
# 方法一:
# 【动态的多目标语句】编译出多个目标文件,特点如下:
# 1、[目标文件]须【指出路径】,否则就是当前目录下;
# 2、[目标文件]从【上级依赖】中获得;
# 3、[依赖文件]从【搜索目录】下寻找;
# 4、[目标文件]与[依赖文件]为【同名】的不同类型文件;
# 5、[目标文件]与[依赖文件]为【一对一】关系。
####################################################
all: out/main.o out/foo.o

out/%.o: %.c
	gcc -c $< -o $@

####################################################
# 方法二:(--前提:make 的版本 3.81 或更高版本--)
# 【动态代码】生成多段编译语句,每段编译一个文件:
# 1、使用 define      实现一个伪函数体;(变量用 $$)
# 2、使用 $(call )    实现调用一个伪函数并传递参数;
# 3、使用 $(eval )    将表达式(字符串)转为脚本代码;
# 4、使用 $(foreach ) 从一串字符串中依次取出一个字
#    段作为入口参数并运行伪函数体。
####################################################
define FUNC
$1: $2
	gcc -c -o $$@ $$<
endef

SRCS_F := app/main.c usr/foo.c
all: $(addprefix out/,$(notdir $(SRCS_F:%.c=%.o)))

$(foreach v,$(SRCS_F),$(eval $(call FUNC,$(addprefix out/,$(notdir $(v:%.c=%.o))),$(v))))

3.9、【实例 makefile】
3.9.1、【写法一】

写法一:使用统一参数编译源码文件,不支持为一些模块定制编译参数,适合一些中小型的项目工程。(展示例子:2_medium

◆◆【脚本剧情主线】

1
2
3
4
5
exe: xxx.o yyy.o          # 生成执行文件:使用编译工具(如:gcc)来编译生成最终的执行文件
xxx.o xxx.d: xxx.c xxx.h  # 告知依赖关系:告知脚本工具(make)中间文件依赖的源码文件及其包含的文件
xxx.d: xxx.c              # 生成依赖信息:借助编译工具(如:gcc)来生成源码文件的包含文件的依赖关系
xxx.o: xxx.c              # 生成中间文件:使用编译工具(如:gcc)来编译生成中间文件

◆①【脚本主要内容】

  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
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
####################################################
# 1、配置编译相关参数、编译文件(夹)、目标及中间文件
####################################################

# 定义生成目标,包括其路径及文件名【只能是一个对象】
TARGET = ./build/app.exe

# 定义中间文件夹,用于存放中间文件【只能是一个对象】
OBJS_D = ./build/

# 添加编译源文件,包括路径及文件名【多个用空格分隔】
SRCS_F = ./app/main.c                             \
         ./usr/hello.c                            \
         ./usr/test.c

# 添加编译文件夹,要指定文件扩展名【多个用空格分隔】
SRCS_D = ./modules/led/*.c                        \
         ./modules/delay/*.c

# 添加编译库文件,包括路径及文件名【多个用空格分隔】
LIBS   = 

# 添加源码文件及包含文件所在的路径【多个用空格分隔】
VPATH  = ./app                                    \
         ./usr/                                   \
         ./modules/led/                           \
         ./modules/delay

# 定义编译器及其环境相关参数
CC = gcc
RM = rm
CFLAG_L = 
CFLAG_O = -c -W -Wall
CFLAG_M = -MMD -MP -MF"$(@:%.o=%.d)"
RMFLAGS = -fr
MKDIR   = mkdir -p
#ECHO_SET = @set -e
#ECHO_END = 

# 生成源文件包含路径选项参数
ifdef VPATH
SRCS_INC := $(addprefix -I,$(strip $(VPATH)))
endif

####################################################
# 2、根据上面的相关配置,生成包括路径的相关文件对象
####################################################

VPATH := $(addsuffix /,$(VPATH))
VPATH := $(subst //,/,$(VPATH))
SRCS_INC := $(addsuffix /,$(SRCS_INC))
SRCS_INC := $(subst //,/,$(SRCS_INC))
OBJS_D := $(addsuffix /,$(OBJS_D))
OBJS_D := $(subst //,/,$(OBJS_D))
DEPS_D := $(OBJS_D)
DIRS := $(sort $(OBJS_D) $(dir $(TARGET)))
SRCS := $(filter %.c, $(SRCS_F) $(wildcard $(SRCS_D)))
OBJS := $(SRCS:.c=.o)
OBJS := $(addprefix $(OBJS_D),$(notdir $(OBJS)))
DEPS := $(SRCS:.c=.d)
DEPS := $(addprefix $(OBJS_D),$(notdir $(DEPS)))
OBJX := $(sort $(OBJS) $(DEPS) $(addsuffix .tmp,$(DEPS)))
LIBS := $(sort $(LIBS))

ifdef VPATH
CFLAG_O += $(SRCS_INC)
#CFLAG_M += $(SRCS_INC)
endif

####################################################
# 3、执行创建文件夹、生成依赖、编译、删除等相关操作
####################################################

.PHONY: all clean echo

all: $(TARGET)

# 非执行"clean/echo"才包含
ifneq ($(MAKECMDGOALS),clean)
ifneq ($(MAKECMDGOALS),echo)
-include $(DEPS)
endif
endif

# 编译出最终执行文件
$(TARGET): $(OBJS) $(LIBS)
	@echo "[2]making $@..."
	@$(CC) $(CFLAG_L) -o $@ $(filter %.o, $^) $(LIBS)

# 编译出中间目标文件
$(OBJS_D)%.o: %.c
	@echo "[1]making $@..."
	@$(CC) $(CFLAG_O) $(CFLAG_M) $< -o $@

# 清除目标及执行文件
clean:
	$(MKDIR) $(DIRS)
	$(RM) $(RMFLAGS) $(OBJX) $(TARGET)

# 查看变量对象相关值
echo:
	$(MKDIR) $(DIRS)
	@echo "VPATH: $(VPATH)"      # VPATH 为[make]工具增加搜索目录
	@echo "DIRS: $(DIRS)"        # DIRS 为编译过程需要创建的文件夹(如:build/)
	@echo "SRCS: $(SRCS)"        # SRCS 为全部源码文件及其路径    (如:usr/hello.c)
	@echo "OBJS: $(OBJS)"        # OBJS 为全部中间目标文件及其路径(如:build/hello.o)
	@echo "DEPS: $(DEPS)"        # DEPS 为全部依赖信息文件及其路径(源码文件包含其它文件的信息)(如:build/hello.d)
	@echo "OBJX: $(OBJX)"        # OBJX 为所有中间文件及其路径(包括:OBJS 和 DEPS)
	@echo "LIBS: $(LIBS)"        # LIBS 为编译包含的库文件及其路径(如:build/abc.a)
	@echo "CFLAG_O: $(CFLAG_O)"  # CFLAG_O 编译中间目标文件的选项参数(如:-c -I./usr/)
	@echo "CFLAG_M: $(CFLAG_M)"  # CFLAG_O 生成依赖信息文件的选项参数(如:-MM -I./usr/)

◆◆【脚本命令操作】

1
2
3
4
5
6
7
####################################################
# 在[shell]中进入[Makefile]所在的目录,输入命令执行:
####################################################
make echo  # 执行查看变量(同时创建所需文件夹)
make all   # 执行编译操作
make clean # 执行清除操作(同时创建所需文件夹)

补充说明:
1、在 Windows 系统测试时出现了小插曲,每次编译都会重新生成执行文件,困扰了我一天!后来发现是编译器自动为执行文件增加“.exe”扩展名,而 Makefile 指出最终生成的执行文件是一个没有扩展名的文件,所以 make 认为没有生成最终执行文件要重新生成!解决:Makefile 指出最终生成的执行文件加上“.exe”扩展名即可。
2、在 Windows 系统运行 Makefile ,需要使用【TDM-GCC】提供的 make 和 gcc 工具(注:make 命令改名为mingw32-make),以及使用【git】提供的命令窗口运行(其集成 linux 基本命令工具)。
3.9.2、【写法二】

写法二:使用多个子 Makefile 编译工程,支持为一些模块定制编译参数,适合一些中大型的项目工程。(展示例子:3_large

◆◆【脚本剧情主线】

1
2
3
4
5
1、与【写法一】没有本质区别,只是把一个 Makefile 分为多个子 Makefile 操作;
1、通过父 Makefile 调用各个子 Makefile(注:生成执行文件的子 Makefile 须最后调用);
2、将多个 Makefile 脚本的公共代码分别移到[makecore.mk]和[makeenvi.mk]两个文件;
3、因多个 Makefile 文件都会生成一个最终文件,及一个项目工程只能生成一个执行文件,
  则要求:只能一个子 Makefile 生成执行文件,其它子 Makefile 生成库文件;

◆①【父 Makefile】

 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
export ROOT = $(realpath .)
.PHONY: all clean echo
include makeenvi.mk

####################################################
# 1、定义子 Makefile 文件所在路径。【多个用空格分隔】
####################################################

# 注意: 唯一生成执行文件的子 Makefile 必须放到最后面
MAKEFILE_D = modules/delay                         \
             app

####################################################
# 2、整理子 Makefile 文件路径信息。通过几个函数整理
#    为方便之后生成命令字符串准备,例如整理为:
#    cd modules/crc// cd app//
####################################################

MAKEFILE_CALL := $(addsuffix /,$(MAKEFILE_D))
MAKEFILE_CALL := $(subst //,/,$(MAKEFILE_CALL))
MAKEFILE_CALL := $(addsuffix /,$(MAKEFILE_CALL))
MAKEFILE_CALL := $(addprefix cd////,$(MAKEFILE_CALL))
MAKEFILE_CALL := $(subst cd////,@cd ,$(MAKEFILE_CALL))
# 定义【换行+Tab格】
define LFTAB


endef

####################################################
# 3、调用子 Makefile 文件执行命令。使用字符串函数的
#    替换功能生成对应的命令字符串,例如生成为:
#    @cd modules/crc && $(MAKE) all
#    @cd app         && $(MAKE) all
####################################################

all:
	$(subst //, && $(MAKE) all$(LFTAB),$(MAKEFILE_CALL))
	@echo "+-----o2ospring-------------+"
	@echo "| :-) completed.            |"
	@echo "+---------------------------+"

clean:
	$(subst //, && $(MAKE) clean$(LFTAB),$(MAKEFILE_CALL))

echo:
	$(subst //, && $(MAKE) echo$(LFTAB),$(MAKEFILE_CALL))

◆②【子 Makefile】生成执行文件

 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
28
29
30
31
32
33
34
####################################################
# 1、配置编译相关参数、编译文件(夹)、目标及中间文件
####################################################

# 定义生成目标,包括其路径及文件名【只能是一个对象】
TARGET = ../build/objs/app.exe

# 定义中间文件夹,用于存放中间文件【只能是一个对象】
OBJS_D = ../build/objs

# 添加编译源文件,包括路径及文件名【多个用空格分隔】
SRCS_F = ../app/main.c                            \
         ../usr/hello.c                           \
         ../usr/test.c

# 添加编译文件夹,要指定文件扩展名【多个用空格分隔】
SRCS_D = ../modules/led/*.c

# 添加编译库文件,包括路径及文件名【多个用空格分隔】
LIBS   = ../build/libs/delay.a

# 添加源码文件及包含文件所在的路径【多个用空格分隔】
VPATH  = ../app                                   \
         ../usr/                                  \
         ../modules/led/                          \
         ../modules/delay

# 定义编译器及其环境相关参数
include ../makeenvi.mk
CFLAG_O += -W -Wall

# 包含[Makefile]脚本公共代码
include ../makecore.mk

◆③【子 Makefile】生成库文件

 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
28
29
####################################################
# 1、配置编译相关参数、编译文件(夹)、目标及中间文件
####################################################

# 定义生成文件,包括其路径及文件名【只能是一个对象】
LIB    = ../../build/libs/delay.a

# 定义中间文件夹,用于存放中间文件【只能是一个对象】
OBJS_D = ../../build/objs

# 添加编译源文件,包括路径及文件名【多个用空格分隔】
SRCS_F = delay.c

# 添加编译文件夹,要指定文件扩展名【多个用空格分隔】
SRCS_D = 

# 添加编译库文件,包括路径及文件名【多个用空格分隔】
LIBS   = 

# 添加源码文件及包含文件所在的路径【多个用空格分隔】
VPATH  = 

# 定义编译器及其环境相关参数
include ../../makeenvi.mk
CFLAG_O += -W -Wall -O1

# 包含[Makefile]脚本公共代码
include ../../makecore.mk

◆④【Makefile 公共代码】环境相关参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
####################################################
# 1、定义编译器及环境相关参数,方便不同环境/平台移植
####################################################

CC = gcc
AR = ar
RM = rm
CFLAG_L = 
CFLAG_O = -c
CFLAG_M = -MMD -MP -MF"$(@:%.o=%.d)"
ARFLAGS = rcs
RMFLAGS = -fr
MKDIR   = mkdir -p
#ECHO_SET = @set -e
#ECHO_END = 
ifdef VPATH
SRCS_INC := $(addprefix -I,$(strip $(VPATH)))
endif

◆⑤【Makefile 公共代码】编译核心代码

 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
####################################################
# 2、根据上面的相关配置,生成包括路径的相关文件对象
####################################################

VPATH := $(addsuffix /,$(VPATH))
VPATH := $(subst //,/,$(VPATH))
SRCS_INC := $(addsuffix /,$(SRCS_INC))
SRCS_INC := $(subst //,/,$(SRCS_INC))
OBJS_D := $(addsuffix /,$(OBJS_D))
OBJS_D := $(subst //,/,$(OBJS_D))
DEPS_D := $(OBJS_D)
DIRS := $(sort $(OBJS_D) $(dir $(TARGET)) $(dir $(LIB)))
SRCS := $(filter %.c, $(SRCS_F) $(wildcard $(SRCS_D)))
OBJS := $(SRCS:.c=.o)
OBJS := $(addprefix $(OBJS_D),$(notdir $(OBJS)))
DEPS := $(SRCS:.c=.d)
DEPS := $(addprefix $(OBJS_D),$(notdir $(DEPS)))
OBJX := $(sort $(OBJS) $(DEPS) $(addsuffix .tmp,$(DEPS)))
LIBS := $(sort $(LIBS))

ifdef VPATH
CFLAG_O += $(SRCS_INC)
#CFLAG_M += $(SRCS_INC)
endif

####################################################
# 3、执行创建文件夹、生成依赖、编译、删除等相关操作
####################################################

.PHONY: all clean echo

ifdef TARGET
all: $(TARGET)
endif

ifdef LIB
all: $(LIB)
endif

# 非执行"clean/echo"才包含
ifneq ($(MAKECMDGOALS),clean)
ifneq ($(MAKECMDGOALS),echo)
-include $(DEPS)
endif
endif

# 编译出最终执行文件
ifdef TARGET
$(TARGET): $(OBJS) $(LIBS)
	@echo "[2]making $@..."
	@$(CC) $(CFLAG_L) -o $@ $(filter %.o, $^) $(LIBS)
endif

# 打包生成静态库文件
ifdef LIB
$(LIB): $(OBJS)
	@echo "[2]making $@..."
	@$(AR) $(ARFLAGS) $@ $(filter %.o, $^)
endif

# 编译出中间目标文件
$(OBJS_D)%.o: %.c
	@echo "[1]making $@..."
	@$(CC) $(CFLAG_O) $(CFLAG_M) $< -o $@

# 清除目标及执行文件
clean:
	$(MKDIR) $(DIRS)
	$(RM) $(RMFLAGS) $(OBJX) $(LIB) $(TARGET)

# 查看变量对象相关值
echo:
	$(MKDIR) $(DIRS)
	@echo "VPATH: $(VPATH)"         # VPATH 为[make]工具增加搜索目录
	@echo "DIRS: $(DIRS)"           # DIRS 为编译过程需要创建的文件夹(如:build/)
	@echo "SRCS: $(SRCS)"           # SRCS 为全部源码文件及其路径    (如:usr/hello.c)
	@echo "OBJS: $(OBJS)"           # OBJS 为全部中间目标文件及其路径(如:build/hello.o)
	@echo "DEPS: $(DEPS)"           # DEPS 为全部依赖信息文件及其路径(源码文件包含其它文件的信息)(如:build/hello.d)
	@echo "OBJX: $(OBJX)"           # OBJX 为所有中间文件及其路径(包括:OBJS 和 DEPS)
	@echo "LIBS: $(LIBS)"           # LIBS 为编译包含的库文件及其路径(如:build/abc.a)
	@echo "CFLAG_O: $(CFLAG_O)"     # CFLAG_O 编译中间目标文件的选项参数(如:-c -I./usr/)
	@echo "CFLAG_M: $(CFLAG_M)"     # CFLAG_O 生成依赖信息文件的选项参数(如:-MM -I./usr/)
	@echo "-------------------"

◆◆【脚本命令操作】

1
2
3
4
5
6
7
####################################################
# 在[shell]中进入[makecore.mk]所在的目录,输入命令执行:
####################################################
make echo         # 执行查看变量(同时创建所需文件夹)
make all          # 执行编译操作
make clean        # 执行清除操作(同时创建所需文件夹)

补充说明:
1、在 Windows 系统测试时出现了小插曲,每次编译都会重新生成执行文件,困扰了我一天!后来发现是编译器自动为执行文件增加“.exe”扩展名,而 Makefile 指出最终生成的执行文件是一个没有扩展名的文件,所以 make 认为没有生成最终执行文件要重新生成!解决:Makefile 指出最终生成的执行文件加上“.exe”扩展名即可。
2、在 Windows 系统运行 Makefile ,需要使用【TDM-GCC】提供的 make 和 gcc 工具(注:make 命令改名为mingw32-make),以及使用【git】提供的命令窗口运行(其集成 linux 基本命令工具)。
3.9.3、【案例三】

案例三:基于【写法二】应用于【STM32F103】单片机工程。(展示例子:4_stm32f1xx

说明:具体请阅读《linux-STM32F开发-makefile 构建与使用》,可以进一步了解一些编译参数、链接脚本、生成的几种执行文件,以及交叉编译器的一些工具!

3.9.4、【说明四】

说明四:关于依赖关系信息生成方法,原来设计使用-MM参数生成,现在改为-MMD参数生成!原因:因为使用-MM参数方式时,每编译一个文件会多运行一次 gcc 和调用一次 sed 处理文本,效率会低很多,而且一些平台可能没有 sed 工具!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 【旧方法】生成代码依赖信息
%.d: %.c
	@set -e; \
	rm -fr $@.tmp; \
	gcc -E -MM $< > $@.tmp; \
	sed 's,\(.*\)\.o[ :]*,$(dir $@)\1.o $@: ,g' < $@.tmp > $@; \
	rm -fr $@.tmp

# 【新方法】编译出中间目标文件,同时生成代码依赖信息(备注:Windows 下`-MF`使用绝对路径时会出错!)
%.o: %.c
	gcc -c -MMD -MP -MF"$(@:%.o=%.d)" $< -o $@

特别备注:
1、在脚本中使用sed工具进行相关操作:从临时文件【$@.tmp】(如: main.d.tmp) 读出内容,对某段文本进行替换后以写覆盖方式保存到文件【$@】(如: main.d)。替换文本内容案例: 【main.o: 】替换为【build/main.o build/main.d: 】。其替换语法为正则表达式,相关知识请移步《正则表达式 regex》了解。

四、相关的编译构建工具

CMake
Qmake
XMake
Meson
SCons
BusyBox
Autotools
待续……