文章目录
1、说明
1.1 Makefile是什么
Makefile是Linux(UNIX)环境下管理工程文件的一种文件,它用来描述项目中哪些文件需要编译,哪些不需要编译,哪些文件需要先编译,哪些后编译,哪些文件怎样编译等等。如果不使用Makefile文件来管理,在项目中就需要逐个文件进行编译或其他操作,这样工作量就会非常庞大,而使用了Makefile来管理,只需要一个make或make xxx命令,就可以对整个项目工程进行相应的操作,使得平时的工程管理变得简单化、自动化。
1.2 书写规则
目标: 依赖1 依赖2 ...
<Tab键>命令
说明:当“目标”不存在或某个“依赖”比“目标”的时间要新,则执行“命令”。
- 目标:可以是文件,也可以是标签;
- 依赖:为生成“目标”的文件或其他“目标”,可以多个,也可以没有;
- 命令:需要执行的命令等,可以多个,但不能没有,每条命令前都是<Tab键>,命令间可以空行。
(另,Makefile中的注释符是“#”)
1.3 文件命名
Makefile一般的命名方式有GNUmakefile、makefile、Makefile三种,但是一般都会选择命名为Makefile。当执行make命令的时候,就会在当前路径下查找这几个Makefile文件,它们的查找顺序是GNUmakefile->makefile->Makefile。当然,也可以在make的时候使用-f参数来指定其他名称的"Makefile"文件。
1.4 工作流程
默认情况下,make会执行Makefile文件中出现的第一个依赖关系,也称为“最终目标”或“终极目标”。当发现依赖关系中的“依赖”比“目标”新的时候,它又会先去处理这些以这些“依赖”为“目标”的规则,处理完成之后就回过头来处理之前的“目标”。
2、逐步进入Makefile世界
2.1 示例1:最简单的Makefile
linrm@linrm-VirtualBox:~/MakefileTest/01$ ls
main.c Makefile
最简单的Makefile搭配最简单的代码。代码内容如下:
/* main.c */
#include <stdio.h>
int main(int argc, char **argv)
{
printf("hello, world!\n");
return 0;
}
最简单的Makefile如下:
main: main.c
gcc main.c -o main
使用:
linrm@linrm-VirtualBox:~/MakefileTest/01$ ls
main.c Makefile
linrm@linrm-VirtualBox:~/MakefileTest/01$ make
gcc main.c -o main
linrm@linrm-VirtualBox:~/MakefileTest/01$ ls
main main.c Makefile
linrm@linrm-VirtualBox:~/MakefileTest/01$ ./main
hello, world!
linrm@linrm-VirtualBox:~/MakefileTest/01$
2.2 示例2:同一个目录多个源文件
linrm@linrm-VirtualBox:~/MakefileTest/02$ ls
main.c Makefile test.c test.h
test.c/test.h内容如下:
/* test.c */
#include <stdio.h>
#include "test.h"
int test(void)
{
printf("%s\n", STRING);
}
/* test.h */
#ifndef _TEST_H_
#define _TEST_H_
#define STRING "hello, world!"
int test(void);
#endif
main.c内容如下:
/* main.c */
#include "test.h"
int main(int argc, char **argv)
{
test();
return 0;
}
Makefile内容如下:
main: main.o test.o
gcc main.o test.o -o main
main.o: main.c
gcc -c main.c -o main.o
test.o: test.c test.h
gcc -c test.c -o test.o
使用:
linrm@linrm-VirtualBox:~/MakefileTest/02$ ls
main.c Makefile test.c test.h
linrm@linrm-VirtualBox:~/MakefileTest/02$ make
gcc -c main.c -o main.o
gcc -c test.c -o test.o
gcc main.o test.o -o main
linrm@linrm-VirtualBox:~/MakefileTest/02$ ls
main main.c main.o Makefile test.c test.h test.o
linrm@linrm-VirtualBox:~/MakefileTest/02$
linrm@linrm-VirtualBox:~/MakefileTest/02$ ./main
hello, world!
linrm@linrm-VirtualBox:~/MakefileTest/02$
可以看到这次的目标文件中就有.o汇编文件,这种生成中间文件的方式有利于减少编译花费的时间,因为Makefile的核心规则是当“依赖”比“目标”新的时候才会执行“命令”。这样下来当只修改test.c时就不会连main.c也重复编译,例如对比下面和上面的gcc编译命令次数即可知道:
linrm@linrm-VirtualBox:~/MakefileTest/02$ touch test.c # 更新文件的时间戳
linrm@linrm-VirtualBox:~/MakefileTest/02$ make
gcc -c test.c -o test.o
gcc -o main main.o test.o
linrm@linrm-VirtualBox:~/MakefileTest/02$
2.3 伪目标介绍,并在示例2的基础上继续改良
示例2中make编译之后得到一些.o格式的中间汇编文件,有时候只是想看下源文件就不是很清晰了。这时可以自己添加一个“清理”的命令,修改后的Makefile如下:
main: main.o test.o
gcc main.o test.o -o main
main.o: main.c
gcc -c main.c -o main.o
test.o: test.c test.h
gcc -c test.c -o test.o
clean:
rm *.o main -rf
.PHONY: clean
这个clean是一个“目标”,但是发现它没有“依赖”,那怎样判断“目标”和“依赖”谁比较新呢?其实,clean并不是我们想要生成的一个文件,我们只是想让clean这个“目标”来执行它后面的“命令”,所以使用“.PHONY”关键字定义clean为假想目标,也就是“伪目标”。
经过改良之后,功能已经稍微增强了那么一丢丢。如果接着再新建一个c文件并且也要一起编译,那么还是要继续在Makefile里添加一条编译规则,但是如果需要添加多个文件呢,那显得有点麻烦了。没关系,Makefile里还提供了简便的解决方法,那就继续改良,修改后的Makefile内容:
main: main.o test.o
gcc -o main $^
%.o: %.c
gcc -c $< -o $@
clean:
rm *.o main -rf
.PHONY: clean
这样,再添加c文件也只需要在“目标”main中添加一个“依赖”即可。其中,以上使用到的符号含义如下:
符号 | 含义 |
---|---|
$^ | 所有依赖文件 |
$< | 第一个依赖文件 |
$@ | 目标文件 |
%.o | 匹配到所有的.o文件 |
2.4 变量介绍,并在示例2的基础上继续改良
变量类型 | 说明 |
---|---|
:= | 即时变量 |
= | 延时变量 |
?= | 延时变量,第一次定义才起效,如果前面已经定义后面就不起作用 |
+= | 附加,即时/延时取决于该变量的定义 |
先看变量的使用示例:
A := $(C)
B = $(C)
C = abc
D = arm
D ?= linux
all:
@echo A = $(A)
@echo B = $(B)
@echo D = $(D)
C += 123
测试结果:
A =
B = abc 123
D = arm
了解了基本的变量类型之后,就可以在示例2的基础上继续改良,修改后Makefile内容如下:
objs = main.o test.o
CC = gcc
CFLAGS = -Werror
main: $(objs)
$(CC) $(CFLAGS) -o main $^
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm *.o main -rf
.PHONY: clean
使用示例:
linrm@linrm-VirtualBox:~/MakefileTest/02$ make
gcc -Werror -c main.c -o main.o
gcc -Werror -c test.c -o test.o
gcc -Werror -o main main.o test.o
linrm@linrm-VirtualBox:~/MakefileTest/02$ file main
main: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=4bf30f4b9232d5f5eea573ba2d05bbfea4ec35d9, not stripped
linrm@linrm-VirtualBox:~/MakefileTest/02$
linrm@linrm-VirtualBox:~/MakefileTest/02$ make clean
rm *.o main -rf
linrm@linrm-VirtualBox:~/MakefileTest/02$ make CC=arm-linux-gnueabihf-gcc
arm-linux-gnueabihf-gcc -Werror -c main.c -o main.o
arm-linux-gnueabihf-gcc -Werror -c test.c -o test.o
arm-linux-gnueabihf-gcc -Werror -o main main.o test.o
linrm@linrm-VirtualBox:~/MakefileTest/02$
linrm@linrm-VirtualBox:~/MakefileTest/02$ file main
main: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 2.6.32, BuildID[sha1]=a1fc72638a44da6e0302f2b20d2218b6b4dcdc27, with debug_info, not stripped
linrm@linrm-VirtualBox:~/MakefileTest/02$
可以看到,通过变量的方法使得编译变得更加智能了,并且在make的时候也可以手动指定变量来覆盖Makefile文件中的变量,以上示例就指定了CC变量来编译得到不同平台的可执行程序。
2.5 函数介绍,并在示例2的基础上继续改良
函数介绍
格式:
$(函数名 参数) # 函数名与参数用空格或Tab键隔开,参数之间用逗号隔开,空格和逗号不作为参数的一部分
常见的函数(更多函数需自行探索):
$(foreach var, list, text) # 对于list的每个变量var执行text
$(filter pattern..., text) # 在text中取出符合patten格式的值
$(filter-out pattern..., text) # 在text中取出不符合patten格式的值
$(wildcard pattern) # pattern定义了文件名的格式
# wildcard函数就是取出真实存在的文件
$(patsubst pattern, replacement, $(var))
# 从$(var)中取出每一个值
# 如果符合pattern则替换为replacement
$(subst from, to, text) # 将text中使用to替换每一处from
$(findstring find, in) # 在字符串in中搜索find并返回find
在示例2的基础上继续改良
在前面的2.4中,如果修改了头文件,重新编译时就会发现并没有对包含该头文件的源文件进行重新编译,修改之前的具体情况如下:
linrm@linrm-VirtualBox:~/MakefileTest/02$ touch test.h
linrm@linrm-VirtualBox:~/MakefileTest/02$ make
make: 'main' is up to date.
linrm@linrm-VirtualBox:~/MakefileTest/02$
修改之后的Makefile内容如下(后面再分析为什么这么改):
objs = main.o test.o
CC = gcc
CFLAGS = -Werror
depend_files := $(patsubst %, .%.d, $(objs))
depend_files := $(wildcard $(depend_files))
main: $(objs)
$(CC) $(CFLAGS) -o main $^
ifneq ($(depend_files), )
include $(depend_files)
endif
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@ -MD -MF .$@.d
clean:
rm *.o .*.d main -rf
.PHONY: clean
使用:
linrm@linrm-VirtualBox:~/MakefileTest/02$ make
gcc -Werror -c main.c -o main.o -MD -MF .main.o.d
gcc -Werror -c test.c -o test.o -MD -MF .test.o.d
gcc -Werror -o main main.o test.o
linrm@linrm-VirtualBox:~/MakefileTest/02$
linrm@linrm-VirtualBox:~/MakefileTest/02$ touch test.h
linrm@linrm-VirtualBox:~/MakefileTest/02$ make
gcc -Werror -c main.c -o main.o -MD -MF .main.o.d
gcc -Werror -c test.c -o test.o -MD -MF .test.o.d
gcc -Werror -o main main.o test.o
linrm@linrm-VirtualBox:~/MakefileTest/02$
可以看到,只要修改了某个头文件,其他包含该头文件的源文件就会被重新编译了。这里gcc涉及了2个参数:“-MD”选项的目的是生成目标文件依赖的所有文件详细信息;“-MF filename”就是将前面的“-MD”选项生的内容写入到文件filename中(参考文章:[gcc的参数解释1] [gcc的参数解释2])。查看一下.main.o.d文件的内容:
main.o: main.c /usr/include/stdc-predef.h test.h
这个文件的内容看起来就是一个Makefile的“目标”和“依赖”,而文件的本身是被include关键字将它包含进来。需要注意的是,include前面可以是一个或多个空格,反正读取时都会被忽略掉,但是就不能是Tab键,因为这样就会被认为是一个命令而不是普通的语句。另外,在一个Makefile文件中使用include关键字时,它会暂停读取本文件的内容,等到读取include包含的文件时才返回继续往下读。所以,include编译生成的依赖文件就相当于为我们编写了一个Makefile的规则,所以只要它依赖的头文件被修改了,也就比“目标”新,就会“触发”该文件被重新编译。
2.6 示例3:VPATH/vpath的使用(可用于多目录查找文件)
linrm@linrm-VirtualBox:~/MakefileTest/03$ tree
.
├── include
│ └── test.h
├── Makefile
└── src
├── main.c
└── test.c
其中源文件都是直接从前面示例2中拷贝过来的,这种目录架构有利于代码的管理。项目的结构变了,相应的Makefile当然也要跟着变:
objs = main.o test.o
CC = gcc
CFLAGS = -Werror -Iinclude
VPATH = src include
#vpath %.c src
#vpath %.h include
depend_files := $(patsubst %, .%.d, $(objs))
depend_files := $(wildcard $(depend_files))
main: $(objs)
$(CC) $(CFLAGS) -o main $^
ifneq ($(depend_files), )
include $(depend_files)
endif
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@ -MD -MF .$@.d
clean:
rm *.o .*.d main -rf
.PHONY: clean
搜索文件比较重要的两个变量:
- VPATH:它是一个环境变量,使用时指定文件的路径,多路径可以使用空格或者冒号隔开,并且是按照前后顺序进行搜索;
- vpath:它是一个关键字,使用方法和VPATH差不多,但它是按照条件搜索,除了指定文件的路径,还要加上条件,格式为“vpath 条件 目录1:目录2…”;除此之外,如果不加目录则代表清楚符合条件的目录,如果条件与目录都不加则代表清除所有已被设置的文件搜索路径。
这种编译方式是将所有的规则都写到一个Makefile里了,这种方式对于文件较多的工程来讲还是不够方便的,还可以参考后面的编写方式。
2.7 示例4:参考完整项目的Makefile
(整理自韦东山老师教学视频)
a. 项目顶层Makefile
CROSS_COMPILE =
AS = $(CROSS_COMPILE)as
LD = $(CROSS_COMPILE)ld
CC = $(CROSS_COMPILE)gcc
CPP = $(CC) -E
AR = $(CROSS_COMPILE)ar
NM = $(CROSS_COMPILE)nm
STRIP = $(CROSS_COMPILE)strip
OBJCOPY = $(CROSS_COMPILE)objcopy
OBJDUMP = $(CROSS_COMPILE)objdump
export AS LD CC CPP AR NM
export STRIP OBJCOPY OBJDUMP
CFLAGS := -Wall -O2 -g
CFLAGS += -I $(shell pwd)/include
LDFLAGS :=
export CFLAGS LDFLAGS
TOPDIR := $(shell pwd)
export TOPDIR
TARGET := test
obj-y += main.o
obj-y += sub.o
obj-y += a/
all : start_recursive_build $(TARGET)
@echo $(TARGET) has been built!
start_recursive_build:
make -C ./ -f $(TOPDIR)/Makefile.build
$(TARGET) : built-in.o
$(CC) -o $(TARGET) built-in.o $(LDFLAGS)
clean:
rm -f $(shell find -name "*.o")
rm -f $(TARGET)
distclean:
rm -f $(shell find -name "*.o")
rm -f $(shell find -name "*.d")
rm -f $(TARGET)
b. 项目顶层Makefile.build
PHONY := __build
__build:
obj-y :=
subdir-y :=
EXTRA_CFLAGS :=
include Makefile
# obj-y := a.o b.o c/ d/
# $(filter %/, $(obj-y)) : c/ d/
# __subdir-y : c d
# subdir-y : c d
__subdir-y := $(patsubst %/,%,$(filter %/, $(obj-y)))
subdir-y += $(__subdir-y)
# c/built-in.o d/built-in.o
subdir_objs := $(foreach f,$(subdir-y),$(f)/built-in.o)
# a.o b.o
cur_objs := $(filter-out %/, $(obj-y))
dep_files := $(foreach f,$(cur_objs),.$(f).d)
dep_files := $(wildcard $(dep_files))
ifneq ($(dep_files),)
include $(dep_files)
endif
PHONY += $(subdir-y)
__build : $(subdir-y) built-in.o
$(subdir-y):
make -C $@ -f $(TOPDIR)/Makefile.build
built-in.o : $(cur_objs) $(subdir_objs)
$(LD) -r -o $@ $^
dep_file = .$@.d
%.o : %.c
$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<
.PHONY : $(PHONY)
c. 子目录Makefile
obj-y += xxx.o
obj-y += yyy.o
3、总结
其实,在实际使用中,很好会从0开始写一个Makefile,一般有现成的搬过来修修改改基本上就可以使用了,但前提也还是需要看得懂。另外,目前也有一些自动生成Makefile文件的开源软件,就比如autotools和cmake,两者的使用可以参考专栏:【Linux:Makefile】