Linux笔记(9)| 一步步深入Makefile

2020-07-10 10:20:22 浏览数 (1)

今天分享的是如何一步步深入地学习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)

来看一下结果:

可以看到,加上头文件之后,之前的警告已经没有了,并且能够生成最终的可执行目标。

0 人点赞