今天分享的是如何一步步深入地学习Makefile。在Linux中编译代码,不像是Windows中有很多集成的IDE,Linux中都是通过基本的编译工具如gcc来进行,比如要编译main.c这个文件,可以使用gcc main.c -o main.但是如果源文件很多,这种方法就不适用了,所以,必须要学会使用Makefile。
1、Makefile三要素:目标、依赖、命令
目标:依赖的文件或者是其他目标
<Tab>命令1
<Tab>命令2
例:
代码语言:javascript复制targeta:targetb targetc
echo "targeta"
targetb:
echo "targetb"
targetc:
echo "targetc"
如果当前文件夹有一个叫targetb的文件,那么当执行make targetb的时候就不会被执行,为了防止这种情况,可以将targetb定义为伪目标.
代码语言:javascript复制.PHONY : targetb
targeta:targetb targetc
echo "targeta"
targetb:
echo "targetb"
targetc:
echo "targetc"
只要我们不期待生成目标文件,就应该把它定义成伪目标
一个简单的例子:
main.c文件
代码语言:javascript复制#include <stdio.h>
int main ()
{
play();
stop();
}
mp3.c文件
代码语言:javascript复制void play(void)
{
printf("play.n");
}
void stop(void)
{
printf("stop.n");
}
Makefile文件
代码语言:javascript复制mp3:main.o mp3.o
gcc main.o mp3.o -o mp3
main.o:
gcc -c main.c -o main.o
mp3.o:
gcc -c mp3.c -o mp3.o
.PHONY:clean
clean:
rm mp3
2、Makefile中的变量:系统变量、自定义变量、自动化变量
(1)系统变量:
(2)自定义变量:
= 延迟赋值
:= 立即赋值
?= 空赋值
= 追加赋值
例:
代码语言:javascript复制cc=5
bb3=9
bb4=8
bb=$(cc) #延迟赋值
bb2:=$(cc) #立即赋值
bb3?=$(cc) #空赋值
bb4 =$(cc) #追加赋值
cc=6
.PHONY:all
all:
echo "$(bb)"
echo "$(bb2)"
echo "$(bb3)"
echo "$(bb4)"
来看一下执行结果:
第一个延迟赋值,所以bb的值是最后cc的值,为6;第二个是立即赋值,所以bb2的值是5;第三个是空赋值,只有当变量的值为空时才赋值,这里因为bb3有值了,所以不会被赋值;第四个是追加赋值,在原来的基础上继续添加。
(3)自动化变量
$< 第一个依赖文件
$^ 全部的依赖文件
$@ 目标
使用自动化变量可以使Makefile文件更好地修改,类似于C语言中使用宏定义来封装,比如将前面写的Makefile进行改造
代码语言:javascript复制cc=gcc
target=mp3
objs=main.o mp3.o
$(target):$(objs)
$(cc) $^ -o $@
main.o:main.c
$(cc) -c main.c -o main.o
mp3.o:mp3.c
$(cc) -c mp3.c -o mp3.o
.PHONY:clean
clean:
rm mp3
执行结果与之前完全一致
3、模式匹配
%:匹配任意多个非空字符(Shell:*通配符)
例:对上面的Makefile进一步改造
代码语言:javascript复制cc=gcc
target=mp3
objs=main.o mp3.o
$(target):$(objs)
$(cc) $^ -o $@
%.o:%.c
$(cc) -c %.c -o %.o
.PHONY:clean
clean:
rm mp3
4、默认规则
.o文件默认使用.c文件来进行编译,所以,可以把上面的.o与.c的依赖去掉,
这样,Makefile进一步改造成这样:
代码语言:javascript复制cc=gcc
target=mp3
objs=main.o mp3.o
$(target):$(objs)
$(cc) $^ -o $@
.PHONY:clean
clean:
rm mp3
5、条件分支
ifeq (var1,var2)
...
else
...
endif
ifneq (var1,var2)
...
else
...
endif
为了使上面的Makefile既适用于x86平台,也适用于arm平台,我们可以使用条件分支语句来对上面的Makefile改造
代码语言:javascript复制ARCH?=x86
ifeq ($(ARCH),x86)
cc=gcc
else
cc=arm-linux-gnueabihf-gcc
endif
target=mp3
objs=main.o mp3.o
$(target):$(objs)
$(cc) $^ -o $@
.PHONY:clean
clean:
rm mp3 *.o
6、Makefile的常用函数
(1)模式替换函数-patsubst
函数原型:$(patsubst PATTERN,REPLACMENT,TEXT)
代码语言:javascript复制.PHONY:all
all:
echo "$(patsubst %.c,%.o,x.c.c bar.c)"
当调用patsubst函数的时候,会去将后面的x.c.c、bar.c和前面的%.c匹配,匹配成功则变成.o文件
(2)取文件名函数-notdir
echo "$(notdir src/foo.c hacks)"
调用这个函数会去掉原本的路径,只保留文件名
(3)获取匹配模式文件名函数-wildcard
echo "$(wildcard *.c)"
调用这个函数会找出.c结尾的文件
(4)循环函数-foreach
例:
代码语言:javascript复制.PHONY:all
dirs:=a b c d
files:=$(foreach dir,$(dirs),$(wildcard $(dir)/*))
all:
echo "$(files)"
注意:逗号前不要乱打空格,否则出问题,比如下面这样写就是错的
代码语言:javascript复制files:=$(foreach dir ,$(dirs),$(wildcard $(dir)/*))
借用函数来改造前面写好的Makefile
代码语言:javascript复制ARCH?=x86
ifeq ($(ARCH),x86)
cc=gcc
else
cc=arm-linux-gnueabihf-gcc
endif
TARGET=mp3
BUILD_DIR=build
SRC_DIR=module1 module2
SOURCES:=$(foreach dir,$(SRC_DIR),$(wildcard $(dir)/*.c))
OBJS=$(patsubst %.c,$(BUILD_DIR)/%.o,$(notdir $(SOURCES)))
VPATH=$(SRC_DIR)
$(BUILD_DIR)/$(TARGET):$(OBJS)
$(cc) $^ -o $@
$(BUILD_DIR)/%.o:%.c | creat_build
$(cc) -c $< -o $@
.PHONY:clean creat_build
clean:
rm $(TARGET) *.o
creat_build:
mkdir -p $(BUILD_DIR)
来看一下结果:
从提示信息可以看出,它先创建了一个build文件夹,然后将main.c编译成了main.o,将mp3.c编译成了mp3.o,都放在build文件夹下面,最后将两个.o文件生成最终的可执行目标mp3。
7、头文件问题
最后一个问题就是头文件包含问题,在前面的例子当中,都没有使用到头文件,在这里,介绍一下头文件的使用。我们把main.c和mp3.c分别放在不同的文件夹module1和module2里,体现实际工程中的模块思想,然后建一个include文件夹用来存放头文件。
(1)写一个头文件,并把头文件添加到编译器的头文件路径中
gcc -I "头文件路径"
(2)实时检查头文件的更新情况,一旦头文件发生变化,应该要重新编译所有相关文件
创建一个mp3.h头文件
代码语言:javascript复制#ifndef _MP3_H
#define _MP3_H
void play(void);
void stop(void);
#define song "my love"
#endif
Makefile 主要修改的地方是:
代码语言:javascript复制INC_DIR=include
CFLAGS=$(patsubst %,-I%,$(INC_DIR))
INCLUDES=$(foreach dir, $(INC_DIR),$(wildcard $(dir)/*.h))
$(BUILD_DIR)/%.o:%.c $(INCLUDES) | creat_build
$(cc) -c $< -o $@ $(CFLAGS)
第一句是使用变量INC_DIR来保存头文件的路径,第二句是使用模式替换函数在路径前加上-I,这是为了后面gcc 选项指定依赖的头文件路径。第三句是头文件路径里的每一个头文件,并且通过第四句把每一个头文件作为.o的依赖文件,也就是当头文件发生变化时,编译器要重新编译生成.o文件。
完整Makefile代码
代码语言:javascript复制ARCH?=x86
ifeq ($(ARCH),x86)
cc=gcc
else
cc=arm-linux-gnueabihf-gcc
endif
TARGET=mp3
BUILD_DIR=build
SRC_DIR:=module1 module2
INC_DIR=include
CFLAGS=$(patsubst %,-I%,$(INC_DIR))
INCLUDES=$(foreach dir, $(INC_DIR),$(wildcard $(dir)/*.h))
SOURCES:=$(foreach dir, $(SRC_DIR),$(wildcard $(dir)/*.c))
OBJS=$(patsubst %.c,$(BUILD_DIR)/%.o,$(notdir $(SOURCES)))
VPATH=$(SRC_DIR)
$(BUILD_DIR)/$(TARGET):$(OBJS)
$(cc) $^ -o $@
$(BUILD_DIR)/%.o:%.c $(INCLUDES) | creat_build
$(cc) -c $< -o $@ $(CFLAGS)
.PHONY:clean creat_build
clean:
rm -r $(BUILD_DIR)
creat_build:
mkdir -p $(BUILD_DIR)
来看一下结果:
可以看到,加上头文件之后,之前的警告已经没有了,并且能够生成最终的可执行目标。