【makefile入门】makefile选项和makefile.buil

上一篇文章:makefile入门教程

一、选项

make命令和gcc一样,也是存在多种选项,一些选项在编译调试时会带来很大的帮助,下面我将介绍一些常用的make选项

(1)-f选项

-f选项用于指定要使用哪个makefile,用法如下:

make -f <Makefile文件>

这一般用于要执行的makefile文件不叫makefile或者makefile位于其他的路径下。但注意如果使用了其他目录下的makefile,编译的其实还是自己目录下的文件,不要搞混。例如给定如下的目录

project/
│── makefile
│── src/
│   ├── main.c
│   ├── func.c
│   ├── func.h
│   ├── other_makefile  

目录共有两个makefile文件,一个在src目录中(但不是默认的名称),一个位于src目录外。这两个makefile文件,每一个都可以单独完成编译,使用方法如下:

# 假设当前终端路径在src目录下,直接使用make指令会提升找不到makefile
~/project/src$ make -f other_makefile    #使用方法1
~/project/src$ make -f ../makefile       #使用方法2

不过-f选项更常用于在一个makefile文件中调用其他的makefile文件,例如在makefile文件中调用makefile.build的文件,这往往用于多个子目录的情况。

all : 
	make -f $(dir)/makefile.build

(2)-C选项

-C用于进入指定目录并执行make,然后返回原目录,用法如下:

make -C <目录> <选项>

例如给定如下目录:

project/
│── src/
│   ├── main.c
│   ├── func.c
│   ├── func.h
│   ├── makefile  

假如现在在project这一目录,要在src中执行make命令来编译src中的文件,那么就可以使用-C选项来实现

~/project$ make -C ./src         #相当于在src目录下执行make
~/project$ make -C ./src clean   #相当于在src目录下执行make clean

(3)指定变量

用于临时修改makefile中的变量值,用法为:

make VARIABLE=value    #例如make CC=gcc

例如在目前makefile中有如下内容

CC = aarch64-linux-gun-gcc

all : 
	echo $(CC)

如果我们执行make CC=gcc,则会输出gcc,而makefile文件中这里仍然是aarch64-linux-gun-gcc这一长串,即临时修改。

(4)-n选项

-n选项常用于调试,即打印make计划执行的命令,但实际上并不执行,这在调试时很有用,可以帮助我们快速查看执行的顺序是否符合预期。

在这里插入图片描述

此外,对于别人的makefile文件,我们也可以使用该指令来快速查看该文件的执行顺序,从而更快的掌握该makefile。例如给定如下makefile内容,这里的依赖规则略微复杂,那么我们就可以使用make -n来查看实际的执行顺序

TARGET := test

all : start_recursive_build $(TARGET)
	@echo $(TARGET) has been built!

start_recursive_build:
	make -C ./ -f ./Makefile.build

$(TARGET) : start_recursive_build
	gcc -o $(TARGET) built-in.o

# 执行顺序输出为:
# make -C ./ -f ./Makefile.build
# gcc -o test built-in.o 
# echo test has been built!

(5)-j选项

-j选项用于指定同时执行的任务数,加快编译的速度。用法为:

make -j<n>    #例如make -j4

如果希望make自动使用所有的CPU核心,则可以使用如下命令:

make -j$(nproc)    #nproc指令用于返回CPU的核心数

该加速一般用于编译linux内核或者大型的项目,对于小型工程的加速效果并不是很明显。此外使用-j可能会导致编译过程报错,如果报错,可以尝试去掉-j重试!

二、 makefile.build

之前我们完成了makefile文件的优化,该文件实现自动编译并检测头文件,相当于是一个模板了。假如现在我们有一个工程,这个工程有多个子目录,我们要把一些子目录下的文件按照上述makefile构建的规则编译。那么能不能把这个makefile给单独拿出来,然后在每一个需要编译的文件目录下调用该makefile呢?这就是本节要讲的makefile.build文件了。

makefile.build其实也是makefile文件,只不过为了和makefile区分,虽然也可以叫其他的名字,但为了强调其功能就约定俗成就叫makefile.build了。

makefile.build在功能上是和makefile有所区分的:一般来说makefile 是主构建文件,一般用于管理整个编译过程;makefile.build 是辅助构建文件,通常用于管理子目录或模块的具体构建规则。

makefile.build的使用和前面所讲的选项内容是分不开的,下面我将通过一个实际案例来介绍一下makefile和makefile.build的调用关系。同时我认为该案例也是非常好的自动化模版,只要大家的文件目录符合我下面所给的目录格式,都可以拿过来不做任何更改直接使用!

project/
│── src/        #.c目录
│   ├── head.c
│   ├── func.c
│   ├── makefile
│── .../        #支持多个.c目录
│   ├── xxx.c
│── include/     #头文件目录
│   ├── func.h
│   ├── xxx.h
│── makefile
│── makefile.build

首先是makefile的内容,可以看到makefile只负责编译最终的可执行文件,中间的.o过程文件则不处理

CC := gcc
CFLAGS := -Wall -g -MMD
CFLAGS += -I $(shell pwd)/include    #设置头文件的目录,否则会提示找不到头文件
CFLAGS += -c -o
TARGET := test

PWD := $(shell pwd)

sub_dir := $(shell find . -maxdepth 1 -type d)   #maxdepth为1表示只搜索本级目录,type表示只搜索文件夹
sub_dir := $(filter ./%,$(sub_dir))
sub_dir := $(filter-out %include,$(sub_dir))     #剔除include文件夹,因为该文件夹放的都是头文件,不需要编译
subdir_objs := $(foreach f,$(sub_dir),$(f)/build.o)

export CC CFLAGS PWD    #这些要全局使用的值就export出去,这样其他的makefile文件就可以直接使用相应的值

#@用于抑制命令回显,即在终端只会输出build finish结果,而不是前面先输出一个echo build finish
all : start_recursive_build $(TARGET)
	@echo build finish   
#先执行如下规则,该指令会执行当前目录的makefile.build文件
start_recursive_build :
	make -C ./ -f $(PWD)/makefile.build
#编译最终的可执行文件
$(TARGET) : start_recursive_build
	$(CC) -o $(TARGET) build.o

# 清理生成的.o和目标文件
clean:
	rm -f $(TARGET) $(shell find -name "*.o")
# 清理生成的.o、.d和目标文件
distclean:
	rm -f $(TARGET) $(shell find -name "*.o")
	rm -f $(shell find -name "*.d")

其次是makefile.build的内容,在这里完成对过程.o文件的编译链接和头文件检测

__build:        #这个build非常关键!相当于是第一个目标,从而避免include makefile导致makefile的规则成为第一个目标

objs := $(wildcard *.c)
objs := $(patsubst %.c,%.o,$(objs))    #将所有的.c改成.o
make_exist := $(wildcard makefile)     #查看当前目录下是否存在makefile,如果有则包含
# 利用该ifneq语句来判断include makefile是否执行
ifneq ($(make_exist),)
include makefile   #主要是将makefile中定义的变量值拿过来用
endif

__build : build.o    #构建依赖关系
#将当前目录和子目录下的.o文件链接成一个build.o文件
build.o : $(objs) build_sub_dir          #先将本级目录.c编译,再去编译子级目录.c文件
	ld -r -o $@ $(objs) $(subdir_objs)   
#编译.o文件
%.o : %.c
	$(CC) $(CFLAGS) $@ $<      

# 对每一个子目录进行处理,注意PWD不是当前目录,而是第一级目录,即project目录
build_sub_dir : 
	@for dir in $(sub_dir); do make -C $$dir -f $(PWD)/makefile.build; done

#判断是否存在依赖文件
dep_files := $(wildcard *.d)

ifneq ($(dep_files),)
include $(dep_files)     #将依赖的关系添加到makefile中,实现自动的头文件检测
endif

下面我简单介绍一下下面的执行过程:

  • (project)执行make命令后,首先进入makefile文件开始执行,发现第一个目标为all,依赖于start_recursive_build和$(TARGET)两个目标,于是就是先去处理start_recursive_build目标。
  • (project)start_recursive_build目标为处理当前目录下的makefile.build文件,于是开始执行makefile.build的文件,相当于跳转执行,执行完后会再回来
  • (project)下面进入了makefile.build文件,其第一个目标为__build,依赖于build.o。而build.o又依赖于当前目录下的.o文件以及build_sub_dir,即编译子目录
  • (project)于是就先将当前目录下的.c文件编译成了.o文件,然后进入build_sub_dir目标,这里使用了for循环来处理每一个子目录
  • (project/src、…)对于每一个子目录,进入后同样执行makefile.build文件,由于子目录下没有子目录,于是编译完子目录生成build.o文件后返回
  • (project)此时build_sub_dir完成了子目录的编译,返回build.o规则,利用子目录和当前目录的.o开始编译build.o文件,并在编译完成后返回
  • (project)现在回到了一开始的makefile文件,将build.o链接成为可执行文件,从而完成整个编译的流程,打印build finish

这里我通过一个实际的案例给大家讲了一下makefile和makefile.build之间的关系,希望能给大家的学习带来帮助!