Linux编程 | 使用 make
目录
简单的 makefile 文件
常规的 makefile 文件
常用参数
make 内置规则
后缀和模式规则
make 管理函数库
在Linux 环境中,make 是一个非常重要的编译命令。不管是自己进行项目还是安装应用软件,用户经常会用到 make 或 make install 命令。
make 与 makefile 文件
make 工具,可以将大型的开发项目分解成多个更易于管理的模块。对于含有多个源文件的应用程序,使用 make 和 makefile 工具就可以简洁明快地理顺各个源文件之间纷繁复杂的相互关系。另外,make工具简化了编译程序的步骤,以及拥有根据源文件的更新情况进行部分编译的功能,使make 工具可以大大提高项目开发的效率,也可以减少程序中出现的错误。
用法:make [选项] [目标] ...
虽然make命令内置了很多智能机制,但光凭其自身是无法了解应该如何建立应用程序的。你必须为其提供一个文件,告诉它应用程序应该如何构造,这个文件称为 makefile 文件。 make 工具最主要最基本的功能就是通过 makefile 文件来描述源程序之间的互相关系并自动维护编译工作。而 makefile 文件需要按照某种语法进行编写,文件中需要说明如何编译各个源文件并链接生成可执行文件,并要求定义源文件之间的依赖关系。
makefile文件一般都会和项目的其他源文件放在同一目录下。你的机器上可以同时存在许多不同的 makefile文件。事实上,如果管理的是一个大项目,你可以用多个不同的 makefile文件来分别管理项目的不同部分。
make命令和 makefile 文件的结合提供了一个在项目管理领域十分强大的工具。它不仅常被用于控制源代码的编译,而且还用于手册页的编写以及将应用程序安装到目标目录。
工作原理
———图片来自网络,侵权删除
make 运用——多文件管理
对 make 的简单应用,对多文件的管理。在拥有多个源文件时,就不能使用 gcc -o a.out main.c
命令一步编译完成,通常需要先将所有源文件编译,然后再将所有的编译后的文件链接在一起形成可执行文件。通常这个过程都需要两行及其以上的命令才可以完成,这会使得编译步骤变得很繁琐,通过 make 工具可以简化编译步骤。
比如,在程序中创建多个文件,在 a.h 中包含 Sum() 函数的申明,在 b.h 中包好 Max() 函数的申明。
/* a.h *
int Sum(int a, int b);
/* b.h */
int Max(int a, int b);
在 a.c 与 b.c 中编写如下代码
/* a.c */
int Sum(int a, int b)
{
return a + b;
}
/* b.h */
int Max(int a, int b)
{
return a > b ? a : b;
}
在 main.c 中编写如下程序
/* main.c */
#include <stdio.h>
#include <stdlib.h>
#include "a.h"
#include "b.h"
int main()
{
int a = 2;
int b = 3;
printf("max: %d",Max(a, b) );
printf("Sum: %d",Sum(a, b) );
return 0;
}
以上是一个多文件的程序,对该程序进行编译主要有以下两个步骤
- 对多个源文件进行编译(*.c => *.o)
- 命令
gcc -c 1.c 2.c 3.c ……
- 命令
- 对编译后的文件进行链接(*.o => 可执行程序)
- 命令
gcc [-I 头文件路径] main.o 1.o 2.o 3.o …… [-L 库文件路径] [-l+库名]
- 命令
在此程序中,只需要用到以下两个命令:
gcc -c a.c b.c
gcc -o main mian.o a.o b.o
如图:
创建文件(tips:使用 cat 命令编辑文件时,使用Ctrl+D结束输入)
编译文件
程序运行正常,但是我们可以看到仅仅只有三个文件就需要这么多命令,而且,如果程序需要修改,我们又需要重复一遍上述命令,稍稍显得繁琐了一些,下面让我们编写一个简单 makefile 文件,简化编译步骤吧。
make 运用——编写简单的 makefile 文件
makefile 文件的逻辑很简单,一般包含三个部分
- 宏定义
- 源文件之间的相互依赖关系
- 可执行的命令
make 工具依赖 makefile 中的关系进行编译,这种依赖关系在多源文件的程序编译中尤为重要,通过这种依赖关系的定义, make 工具可以避免许多不必要的编译工作。
对此文件编写 makefile 文件
# 用‘#’注释掉内容
# main 为该程序需要生成的目标文件,‘:’后面的文件为组成目标文件的依赖文件
# 第二行的首行使用Tab键缩进(首行对齐)
main: main.o a.o b.o
gcc -o main main.o a.o b.o
# 接下来分别说明 a.o 与 b.o 的开源,即可
# 在使用make 命令时,会在指定的目录下搜索文件,如果搜索不到文件就会报错
# 这里可以不写 a.h 和 b.h 头文件,make 会自动检索(默认为当前工作目录)
main.o: main.c
gcc -c main.c
a.o: a.c
gcc -c a.c
b.o: b.c
gcc -c b.c
# 这里可以使用 make clean 清理不需要的文件,比如生成的中间文件 *.o文件
# 另外,这里在 rf 命令前加了 ‘-’ ,忽略 rm 的执行结果,如在指定被删除文件不存在的情况下忽略报错信息
clean:
-rm -rf *.o
在Linux 系统中,习惯使用 makefile 作为文件名,如果要使用其他文件作为 makefile,可通过 -f
参数指定。
make -f makefile_name
该命令会执行 makefile 中指定的命令,并且在屏幕上输出命令内容
由于会对该程序编写多个 makefile 文件,这里把文件命令为 makefile1,执行 makefile1 显示“make: ‘main’ is up to date.”好像出了点小状况,没关系,只需要删除之前已经生成的 “main” 文件即可。
有些学过 shell 编程的小伙伴可能会问了,与其写这么麻烦还不如直接写成 shell 脚本,shell 脚本写起来可方便多了,而且也能达到相同的目的。
没有错,shell 脚本的确也可以做到。首先需要说明的是,make 工具一般主要被用来管理一个软件程序项目(用来完成大型软件的自动编译),而 shell 脚本可以说是一系列命令的集合,shell 脚本按照预先编译好的命令将会把全部的文件编译。对于一些小的程序来说,shell 脚本编写确实比 make 方便很多,但make 有一个绝对性的优势,make 在编译时,会根据目标上一次编译的时间和目标所依赖的源文件的更新时间而自动判断应当编译哪个源文件。对于一个大型项目,源文件动则成千上万文件,而开发过程中常常需要对其中的一部分文件进行改动,如果每次编译都把所有文件都编译一遍显然是不明智的。
这也解释了,为什么之前没有删除 “main” 文件,而提示 “make: ‘main’ is up to date.” 的原因了。因为组成 “main” 的所有文件都没有更新。
使用 make clean 命令
通过执行 make clean
命令执行预先设定在 makefile 文件中的命令
# clean 定义方式
clean:
-rm -rf *.o
可以看到,clean
的定义方式与我们之前学过的函数很相似,“clean” 就是函数名,冒号后面紧跟着的就是函数的具体实现。make 中还有一个命令 make install
,试想,模仿 “clean” 的写法编写 “install”
编写 make install 命令
目标 install 依赖于 main(最终生成的目标文件),所以 make 命令知道它必须首先创建 main,然后才能执行制作该目标所需的其他命令。用于制作 install 目标的规则由几个shell 脚本命令组成。由于make命令在执行规则时会调用一个shell,并且会针对每个规则使用一个新shell ,所以必须在上面每行代码的结尾加上一个反斜杠\,让所有she脚本命令在逻辑上处于同一行,并作为一个整体传递给一个shell 执行。这个命令以符号@开头,表示make在执行这些规则之前不会在标准输出上显示命令本身。
在Linux 系统下,可执行程序一般都安装在 /**/bin 目录下,常见的有
- /bin 是所有用户都可以访问并执行的可执行程序。包括超级用户及一般用户。
- /usr/bin 是系统安装时自带的一些可执行程序。即系统程序。
- /usr/local/bin 是用户自行编译安装时默认的可执行程序的安装位置。
这里我们只做测试,所以把该程序安装在当前目录下的 bin 目录中
install: main
@if [ -d ./bin ]; \
then \
cp main ./bin; \
chmod a+x ./bin/main; \
chmod og-w ./bin/main; \
echo "Installed in ./bin"; \
else \
echo "Sorry, './bin' does not found"; \
fi
删除所有无关文件,执行make
与make insstall
命令
目标 install 按顺序执行多个命令将应用程序安装到其最终位置。它并没有在执行下一个命令前检查前一个命令的执行是否成功。如果这点很重要,你可以将这些命令用符号&连接起来,如下所示:
install: main
@if [ -d ./bin ] ; \
then \
cp main ./bin && \
chmod a+x ./bin/main && \
chmod og-w ./bin/main && \
echo "Installed in ./bin" ; \
else \
echo "Sorry, './bin' does not found"; \
fi
make 运用——带宏定义的 makefile 文件
在编写 makefile 文件之前,先了解一下 makefile 文件中的参数,这些参数可以帮助我们更好的掌握 makefile 文件的编写。
makefile 文件常见的参数
在 makefile 中有一些预定义的宏变量
命 令 格 式 | 含义 |
---|---|
AR | 库文件维护程序的名称,默认值为ar 创建静态库.a |
AS | 汇编程序的名称,默认值为as |
CC | C编译器的名称,默认值为cc |
CPP | C预编译器的名称,默认值为$(CC) –E |
CXX | C++编译器的名称,默认值为g++ |
FC | FORTRAN编译器的名称,默认值为f77 |
RM | 文件删除程序的名称,默认值为rm –f |
ARFLAGS | 库文件维护程序的选项,无默认值 |
ASFLAGS | 汇编程序的选项,无默认值 |
CFLAGS | C编译器的选项,无默认值 |
CPPFLAGS | C预编译的选项,无默认值 |
CXXFLAGS | C++编译器的选项,无默认值 |
FFLAGS | FORTRAN编译器的选项,无默认值 |
另外,在 UNIX 系统中, $*
、$@
、$?
和 $$
<4 个特殊的宏值在执行命令的过程中会发生相应的变化。
宏 | 定义 |
---|---|
$? | 当前目标所依赖的文件列表中比当前目标文件还要新的文件 |
$@ | 当前目标的名字 |
$< | 当前依赖文件的名字 |
$* | 不包括后缀名的当前依赖文件的名字 |
在 makefile文件中,你可能还会看到下面两个有用的特殊字符,它们出现在命令之前
- -:告诉make命令忽略所有错误。例如,如果想创建一个目录,但又想忽略任何错误(比如目
录已存在),你就可以在 mkdir命令的前面加上一个减号。你将在本章后面的例子中看到符号
的应用。 - @:告诉make在执行某条命令前不要将该命令显示在标准输出上。如果想用echo命令给出一些
说明信息,这个字符将非常有用。
除此之外,make 工具中提供了部分关键字,在使用 make 工具时可以指定所有makefile中的目标,通常情况下 make 默认“all” 参数。
- “all”—— 这个伪目标是所有目标的目标,其功能一般是编译所有的目标。
- “clean” —— 这个伪目标功能是删除所有被make创建的文件。
- “install” —— 这个伪目标功能是安装已编译好的程序,其实就是把目标执行文件拷贝到指定的目标中去。
- “print” —— 这个伪目标的功能是例出改变过的源文件。
- “tar” —— 这个伪目标功能是把源程序打包备份。也就是一个tar文件。
- “dist” —— 这个伪目标功能是创建一个压缩文件,一般是把tar文件压成Z文件,或是gz文件。
- “TAGS” —— 这个伪目标功能是更新所有的目标,以备完整地重编译使用。
- “check”和“test” —— 这两个伪目标一般用来测试makefile的流程。
另外,make 实际工作原理还是通过命令的方式使用 gcc 工具来完成的,所以一些 gcc 的参数也要了解
后 缀 名 | 所对应的语言 |
---|---|
-S | 只是编译不汇编,生成汇编代码 |
-E | 只进行预编译,不做其他处理 |
-g | 在可执行程序中包含标准调试信息 |
-o file | 把输出文件输出到file里 |
-v | 打印出编译器内部编译各过程的命令行信息和编译器的版本 |
-I dir | 在头文件的搜索路径列表中添加dir目录 |
-L dir | 在库文件的搜索路径列表中添加dir目录 |
-static | 链接静态库 |
-library | 连接名为library的库文件 |
选 项 | 含 义 |
---|---|
-ansi | 支持符合ANSI标准的C程序 |
-pedantic | 允许发出ANSI C标准所列的全部警告信息 |
-pedantic-error | 允许发出ANSI C标准所列的全部错误信息 |
-w | 关闭所有告警 |
-Wall | 允许发出Gcc提供的所有有用的报警信息 |
-werror | 把所有的告警信息转化为错误信息,并在告警发生时终止编译过程 |
编写makefile
通常情况下makefile 会通过宏指定程序安装位置、指定头文件路径(用户自定义头文件)、以及gcc编译时的一些参数等
# which compiler 编译器
CC = gcc
# Where to install 程序安装位置
# INSTDIR = /user/local/bin
INSTDIR = /home/tr/Desktop/test/bin
# Where are include files kept 指定头文件路径
INCLUDE = .
# Options for development gcc编译器标志参数
CFLAGS = -g -Wall -ansi
# Options for release
CFLAGS = -O -Wall -ansi
# 如果在make时未指定目标,默认只创建all之后的
all: main clean
main: main.o a.o b.o
$(CC) -o main main.o a.o b.o
main.o: main.c
$(CC) -I$(INCLUDE) $(CFLAGS) -c main.c
a.o: a.c
$(CC) -I$(INCLUDE) $(CFLAGS) -c a.c
b.o: b.c
$(CC) -I$(INCLUDE) $(CFLAGS) -c b.c
# -作用或略执行结果,即使目标文件不存在也不会发生错误
clean:
-rm -rf *.o
install: main
@if [ -d $(INSTDIR) ]; \
then \
cp main $(INSTDIR); \
chmod a+x $(INSTDIR)/main; \
chmod og-w $(INSTDIR)/main; \
echo "Installed in $(INSTDIR)"; \
else \
echo "Sorry, $(INSTDIR) does not exit"; \
fi
一般来说,一个 makefile 文件中至少包含 all 、 install 、 clean 三个选项
-
make就是make all, 在 all 关键字后面加上要编译的文件,或者也可以带上某些选项,
比如,“ all :main clean ” 表示,最终生成 “mian” 程序,并且执行make clean
命令 -
make install就是把编译出来的二进制文件,库,配置文件等等放到相应目录下
-
make clean清除编译结果
执行 make
在 makefile 中, all 之后添加了 clean ,all : main clean
,在执行 make 时默认执行 all 之后所有内容,所以在编译完程序之后执行了 clean 中定义的命令。
通过宏定义的方式可以完成大多数的编译需求,另外。 make 命令本身带有大量的内置规则,他们可以极大的简化 makefile 文件内容。
make 内置规则
新建一个文件,命名为 foo.c,编写如下代码
#include <stdlib.h>
#include <stdio.h>
int main()
{
printf("Hello World!\n");
exit(EXIT_SUCCESS);
}
使用 make 工具编译,不指定任何 makefile 文件。我们发现也可以编译成功。
对于 foo.c 源文件没有编写 makefile文件,也没有指定编译器以及编译方式,可 make 知道如何去编译,因为 make 存在一种内置规则(又称为推导规则),它们都会使用宏定义,因此可以通过给宏赋予新值来改变其默认行为。
make CC=gcc CFLAGS="-Wall -g"
可以通过 make 的 -p
选项打印出 make 的所有内置规则,用于内容过多,这里只列出一部分
# makefile
SHELL = /bin/sh
# 默认
RM = rm -f
... ...
OUTPUT_OPTION = -o $@
COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c
%.o: %.c
# recipe to execute (内置):
$(COMPILE.c) $(OUTPUT_OPTION) $<
考虑到存在内置规则,可以将文件 makefile 只用于制作目标的规则去掉,而只需指定依赖关系,从而达到简化 makefile 文件的目的。因此,之前 main 程序的 makefile 文件改写为
main: main.o a.o b.o
main.o: a.h b.h main.c
a.o: a.c
b.o: b.c
可以看到通过简化的 makefile3 文件依然可以使用 make 工具编译。
虽然上述 makefile 文件可以执行,当然还是不建议这么写,毕竟内置规则是通用性规则,不足以应对所有情况,因此,可以取折中的方法,将其中部分简化,如:
上部分省略... ...
# Options for release
CFLAGS = -O -Wall -ansi
all: main
main: main.o a.o b.o
$(CC) -o main main.o a.o b.o
# ///////////////////////////////////////
main.o: main.c
a.o: a.c
b.o: b.c
# ///////////////////////////////////////
clean:
-rm -rf *.o
下部分省略... ...
后缀和模式规则
可以看到内置规则在使用时都利用了文件的后缀名(这类似 Windows和MS-DOS的文件扩展名)所以当给出带有某个特定后缀名的文件时,make命令知道应该用哪个规则来创建带有另一个不同后缀名的文件。最常见的一条规则是用于从一个以 .c
为后缀名的文件创建出一个以 .o
为后缀名的文件。该规则使用编译器进行编译,但并不对源文件进行链接。
有时,我们需要自己创建新规则。假设我们的系统中没有针对 C++ 的 .cpp
文件进行编译的规则。为解决这个问题,或者为每个单独的源文件指定一条规则,或者为make制定一条新的规则,专门用于从后缀名为cpp的源文件创建目标文件。假设这个项目中的源文件数量非常大,那么制定一条新规则将节省大量的键入时间,也使得为该项目增加新的源文件变得更加容易。
要想增加一条新的后缀规则,首先需要在 makefile文件中增加一行语句,告诉make命令这个新
的后缀名。然后即可用这个新的后缀名来定义规则。make使用特殊语法:
.<old_suffix>. <new_suffix>:
来定义一条通用规则,利用该规则可以从带有旧后缀名的文件创建带有新后缀名的文件,并保留原文件的前半部分。
下面是 makefile文件的一个片段,它用一个新的通用规则将 .cpp 文件编译为 .o 文件;
.SUFFIXES: .cpp
.cpp.o:
$(CC) -xc++ $(CFLAGS) -I$(INCLUDE) -c $<
特殊依赖关系 .cpp.o
:告诉 make,紧随其后的规则是用于将后缀名为 .cpp 的文件转换为后缀名为 .o 的文件。在定义这个依赖关系时,使用了特殊的宏名称,这是因为此时还不知道将要被转换的文件的名字。要想理解这条规则,只需要记住宏 s<
将被扩展为起始文件的名字(包含旧的后缀名).
注意,只需告诉make如何从 .cpp 文件得到 .o 文件,make已经知道如何从一个目标文件得到一个二进制可执行文件了。
当调用make命令时,它将使用这条新规则从 bar.cpp 文件得到 bar.o 文件,然后再使用它的内置规则从 .o 文件得到二进制可执行文件。-xc++ 标志的作用是告诉 gcc 编译器这是一个C++源文件。
如今的make版本已知道如何处理后缀名为 .cpp 的C++源文件了,但当需要将一种类型的文件转换为另一种类型的文件时,这个技术仍然很有用。
最新的make版本还包含一个新的语法以实现同样的效果,而且功能更强大。例如,模式规则可以用 ‘%’ 通配符语法来匹配文件名,而不是仅依赖于文件的后缀名。
%.cpp: %o
$(CC) -xc++ $(CFLAGS) -I$(INCLUDE) -c $<
用 make 管理函数库
对于大型项目,一种比较方便的做法是用函数库来管理多个编译产品。函数库实际上就是文件,它们通常以 .a(a是英文 archive的首字母)为后缀名,在该文件中包含了一组目标文件。make命令用一个特殊的语法来处理函数库,这使得函数库的管理工作变得非常容易。
静态库: lib库名.a
创建: ar crv lib库名.a 1.o 2.o ……
使用: gcc -o main main.o lib库名.a
gcc -o main main.o -L路径 -l库名动态库: lib库名.so
创建: gcc -fPIC -c 1.c 2.c 3.c ……
gcc -shared -o libfoo.so 1.o 2.o 3.o ……
或gcc -shared -fPIC -o lib库名.so 1.c 2.c 3.c ……
使用: gcc -o main main.o lib库名.so
gcc -o main main.o -L路径 -l库名
用于管理函数库的语法是 lib(file.o),它的含义是目标文件 file.o 是存储在函数库 lib.a 中的。make命令用一个内置规则来管理函数库,该规则的常见形式如下所示:
.c.a:
$(CC) -c $(CFLAGS) $<
$(AR) $(ARFLAGS) $@ $*.o
宏$(AR)
和$(ARFLAGS)
的默认取值通常分别是命令 ar 和选项 rv .这个相当简洁的语法告诉make要想从 .c 文件得到 .a 库文件,它必须应用上面两条规则。
- 第一条规则告诉它必须编译源文件以生成目标文件
- 第二条规则告诉它用 ar 命令将新的目标文件添加到函数库中
因此,如果有一个名为 fud 的函数库,其中包含目标文件 bas.o,则第一条规则中的 $<
将被替换为bas.c,而第二条规则中的 $@
和 $*
将被分别替换为库文件 fua.a 和名字 bas。
在实际应用中,管理函数库规则的使用非常简单。下面将文件 a.o 和 b.o 放入函数库mylib.a 中。只需对 makefile文件做很少的修改,命名为 makefile4 :
# which compiler 编译器
CC = gcc
# Where to install 程序安装位置
# INSTDIR = /user/local/bin
INSTDIR = /home/tr/Desktop/test/bin
# Where are include files kept 指定头文件路径
INCLUDE = .
# Options for development gcc编译器标志参数
CFLAGS = -g -Wall -ansi
# Options for release
CFLAGS = -O -Wall -ansi
all: main
# ///////////////////////////////////////
# Local Libraries
MYLIB = mylib.a
main: main.o $(MYLIB)
$(CC) -o main main.o $(MYLIB)
# 创建函数库
$(MYLIB): $(MYLIB)(a.o) $(MYLIB)(b.o)
main.o: main.c a.h b.h
a.o: a.c
b.o: b.c
clean:
-rm -rf *.o $(MYLIB)
# ///////////////////////////////////////
install: main
@if [ -d $(INSTDIR) ]; \
then \
cp main $(INSTDIR); \
chmod a+x $(INSTDIR)/main; \
chmod og-w $(INSTDIR)/main; \
echo "Installed in $(INSTDIR)"; \
else \
echo "Sorry, $(INSTDIR) does not exit"; \
fi