【linux】Makefile简要知识+一个通用Makefile


Makefile

 meke命令一般用于编译程序,而make命令都依赖于 Makefile 文件。
 最简单的Makefile如下:

hello:  hello.c
   gcc -o hello hello.c
clean:
   rm -f hello

注:缩进使用 tab 键 , 因为Makefile 会为每个以 tab 键开头的命令创建一个 shell 去执行。
 具体规则参考 《GUN Make 使用手册》

以下为粗略笔记

Makefile规则与示例

为什么需要Makefile

 提高编译效率。

Makefile样式

 一个简单的Makefile文件包含一系列"规则":

目标...:依赖...
<tab>命令
  • 命令被执行的两个简单条件:
    • 目标文件还没有生成
    • 依赖文件比目标文件新

先介绍Makefile的两个函数

  1. $(foreach var,list,text)
     for each var in list,change it to text。
     对list中的每一个元素,取出赋值给var,然后把var改为text所描述的形式。
    例子:
objs := a.o b.o
dep_files := $(foreach f, $(objs), .$(f).d)  // 最终 dep_files := .a.o.d .b.o.d
  1. $(wildcard pattern)
      把存在的 pattern 文件列出来
    例子:
src_files := $(wildcard *.c)

完善Makefile

  1. 先来一个简单粗暴、效率低的:
test: main.c hello.c hello.h
        gcc -o test main.c hello.c
  1. 再来一个Makefile,效率高、精炼,支持自动检测头文件
objs := main.o hello.o

test : $(objs)
    gcc -o test $^

# 需要判断是否存在依赖文件
# .main.o.d   .hello.o.d
dep_files := $(foreach f, $(objs), .$(f).d)
dep_files := $(wildcard $(dep_files))

# 把依赖文件包含进来
ifneq ($(dep_files),)
include $(dep_files)
endif

%.o : %.c
    gcc -Wp,-MD,.$@.d -c -o $@ $<

clean:
    rm *.o test -f

distclean:
    rm $(dep_files) *.o test -f

了解自动化变量。

通用Makefile的使用

参考linux内核的Makefile来编写一个通用的Makefile,特点:

  1. 支持多个目录、多层目录、多个文件;
  2. 支持给所有文件设置编译选项;
  3. 支持给目录设置编译选项;
  4. 支持给某个文件单独设置编译选项;
  5. 简单、好用。

通用的Makefile解释

零星知识点
make命令的使用

 执行make命令时,它会去当前目录下查找名为Makefile的文件,并根据它的只是去执行操作,生成第一个目标。
 也可以用 -f 选项指定文件,如:

make -f Makefile.build

指定文件的名字随意定义


 可以使用 -C 指定目录,切换到其他目录去,如:

make   -C   a/   abc.def

 可以指定目标,不再默认生成第一个目录,如:

make   -C   a/   abc.def   other_target
即时变量与临时变量

 变量定义语法:

形式 说明
A = xxx 延时变量
B ?= xxx 延时变量,只有第一次定义时赋值才成功,若曾被定义过,则此赋值无效。
C := xxx 立即变量
D += yyy 如果D在前面是延时变量,那么现在它还是延时变量
如果D在前面是立即变量,那么它现在还是立即变量

延时变量
 使用时才确定该值。
如:

A = $@
tets:
    @echo $A

输出 A 的值为 test
即时变量
 定义时立即确定该值。
如:

A := $@
tets:
    @echo $A

输出 A 的值为

变量的导出(export)

export 是供给子目录的 Makefile 使用的(即 sub-make),同一级的makefile是访问不到的。
 可以通过makefile中的内置变量MAKELEVEL可以查看当前的makefile的level。

Makefile中的shell

 Makefile中可以使用shell命令,如:

TOPDIR := $(shell_pwd)
Makefile中放置第一个命令

 执行目标时,如果不指定目标,则默认执行第一个目标。
所以,第一个目标的位置很重要。有时候不太方便把第一个目标完整地放在文件的前面,这时可以在文件的前面直接放置目标,在后面再完善它的依赖和命令。比如:

First_target:      // 这句话放在前面
........               // 其他代码,比如include其它文件得到后的 xxx 变量
First_target : $(xxx) $(xxx)
    command
假想目标

Makefile中可能会有这样的目标:

clean:
    rm     -f     $(shell find -name "*.o")
    rm    -f      $(TARGET)

:如果当前目录下更好有个名为“clean”的文件,那么执行“make clean”时就不会执行makefile中目标clean的内容,所以要把clean设置为假想目标即可:

.PHONY    :   clean
小知识

PHONY 是 phoney,即伪造的意思。PHONY后面的target是伪造的target,不是真实存在的文件。同时,注意make命令中后面的target默认是文件。

常用函数
  • $(foreach var, list, text)
    for each var in list,change it to text.
    直接上例子:
objs := a.o b.o
dep_files := $(foreach f, $(objs), .$(f).d)   // dep_files 的最终结果为“**.a.o.d .b.o.d**”

  • $(wildcard pattern)
    列出 pattern 存在的文件。
    例子:
src_files = $(wildcard *.c) // src_files的值为当前目录下所有 .c 文件。

  • $(filter pattern..., text)
    text中符合pattern格式的内容留下来
obj-y := a.o b.o c/ d/
DIR := $(filter %/, $(obj-y))   //结果为:c/ d/

  • $(filter-out pattern..., text)
    text中符合pattern格式的内容删除掉
obj-y := a.o b.o c/ d/
DIR := $(filter-out %/, $(obj-y))   // 结果为:a.o b.o

  • $(patsubst pattern, replacement, text)
    寻找text中符合pattern格式的内容,用replacement代替他们。
subdir-y := a.o b.o c/ d/
subdir := $(patsubst %/, %, $(obj-y))   // 结果为:c d

*通用Makefile设计思想
  1. 在Makefile文件中确定要编译的文件、目录,比如:
obj-y += main.o
obj-y += a/

Makefile 文件总是被 Makefile.build 包含的。

  1. 在 Makefile.build 中设置编译规则,有 3 条编译规则
    1. 怎么编译子目录?
      1. 进入子目录编译即可:
$(subdir-y):
    make -C $@ -f $(TOPDIR)/Makefile.build
2. 如何编译当前目录中的目标文件?
%.o : %.c
    $(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<
3. 当前目录下的 **.o** 和子目录下的 **built-in.o** 要打包起来:
$(TARGET) : built-in.o
    $(CC) $(LDFLAGS) -o $(TARGET) built-in.o

思想总结

  1. Makefile文件确定要编译的文件和目录
  2. Makefile.build文件包含规则
  3. 每个需要编译的子目录都放个Makefile文件,把需要编译的文件编译成built-in.o
  4. 再把所有子目录的built-in.o链接到顶层的built-in.o
  5. 最后把顶层built-in.o链接到APP

最好重新分析一下通用Makefile

一个通用Makefile

本程序的Makefile分为3类:

  1. 顶层目录的Makefile
  2. 顶层目录的Makefile.build
  3. 各级子目录的Makefile

源码(注释版)

Makefile

CROSS_COMPILE = # 交叉编译工具头,如:arm-linux-gnueabihf-
AS      = $(CROSS_COMPILE)as # 把汇编文件生成目标文件
LD      = $(CROSS_COMPILE)ld # 链接器,为前面生成的目标代码分配地址空间,将多个目标文件链接成一个库或者一个可执行文件
CC      = $(CROSS_COMPILE)gcc # 编译器,对 C 源文件进行编译处理,生成汇编文件
CPP    = $(CC) -E
AR      = $(CROSS_COMPILE)ar # 打包器,用于库操作,可以通过该工具从一个库中删除或则增加目标代码模块
NM     = $(CROSS_COMPILE)nm # 查看静态库文件中的符号表

STRIP       = $(CROSS_COMPILE)strip # 以最终生成的可执行文件或者库文件作为输入,然后消除掉其中的源码
OBJCOPY  = $(CROSS_COMPILE)objcopy # 复制一个目标文件的内容到另一个文件中,可用于不同源文件之间的格式转换
OBJDUMP = $(CROSS_COMPILE)objdump # 查看静态库或则动态库的签名方法

# 共享到sub-Makefile
export AS LD CC CPP AR NM
export STRIP OBJCOPY OBJDUMP

# -Wall : 允许发出 GCC 提供的所有有用的报警信息
# -O2 : “-On”优化等级
# -g : 在可执行程序中包含标准调试信息
# -I : 指定头文件路径(可多个)
CFLAGS := -Wall -O2 -g 
CFLAGS += -I $(shell pwd)/include

# LDFLAGS是告诉链接器从哪里寻找库文件,这在本Makefile是链接最后应用程序时的链接选项。
LDFLAGS := 

# 共享到sub-Makefile
export CFLAGS LDFLAGS

# 顶层路径
TOPDIR := $(shell pwd)
export TOPDIR

# 最终目标
TARGET := test

# 本次整个编译需要源 文件 和 目录
# 这里的“obj-y”是自己定义的一个格式,和“STRIP”这些一样,*但是 一般内核会搜集 ”obj-”的变量*
obj-y += main.o # 需要把当前目录下的 main.c 编进程序里
obj-y += sub.o # 需要把当前目录下的 sub.c 编进程序里
obj-y += subdir/ # 需要进入 subdir 这个子目录去寻找文件来编进程序里,具体是哪些文件,由 subdir 目录下的 Makefile 决定。
#obj-y += $(patsubst %.c,%.o,$(shell ls *.c))

# 第一个目标
all : start_recursive_build $(TARGET)
    @echo $(TARGET) has been built !
    
# 处理第一个依赖,**转到 Makefile.build 执行**
start_recursive_build:
    make -C ./ -f $(TOPDIR)/Makefile.build
    
# 处理最终目标,把前期处理得出的 built-in.o 用上
$(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)

Makefile.build

注意,include 命令,相对路径为 执行本 Makefile.build 的路径

# 伪目标
PHONY := __build
__build:

# 清空需要的变量
obj-y :=
subdir-y :=
EXTRA_CFLAGS :=

# 包含同级目录Makefile
# 这里要注意,相对路径为 执行本 Makefile.build 的路径
include Makefile

# 获取当前 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)

# 把子目录的目标定为以下注释
# 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))
# 使修改头文件 .h 后,重新make后可以重新编译(重要)
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

# 生成 cur_objs 目标
%.o : %.c
	$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<
	
.PHONY : $(PHONY)

使用说明

  • 本程序的Makefile分为3类:
    1. 顶层目录的 Makefile
    2. 顶层目录的 Makefile.build
    3. 各级子目录的 Makefile

1. 各级子目录的Makefile

  • 参考:
# 可以不添加下面两个变量
EXTRA_CFLAGS  := 
CFLAGS_test.o := 

obj-y += test.o
obj-y += subdir/
  • obj-y += file.o 表示把当前目录下的file.c编进程序里,
  • obj-y += subdir/ 表示要进入subdir这个子目录下去寻找文件来编进程序里,是哪些文件由subdir目录下的Makefile决定。
  • EXTRA_CFLAGS, 它给当前目录下的所有文件(不含其下的子目录)设置额外的编译选项, 可以不设置
  • CFLAGS_xxx.o, 它给当前目录下的xxx.c设置它自己的编译选项, 可以不设置
注意
  1. "subdir/"中的斜杠"/"不可省略
  2. 顶层Makefile中的CFLAGS在编译任意一个.c文件时都会使用
  3. CFLAGS EXTRA_CFLAGS CFLAGS_xxx.o 三者组成xxx.c的编译选项

2. 顶层目录的Makefile

主要作用:

  • 整个工程的参数初期定义
    • 架构
    • 工具链
    • 编译工具
    • 编译参数
    • 需要导出的变量
    • 等等
  • 定义最终目标
  • 跳转到 Makefile.build

3. 顶层目录的Makefile.build **

注意:该文件虽然放在顶层,但是也是提供给各级子目录使用的。
主要功能:

  • 把某个目录及它的所有子目录中、需要编进程序去的文件都编译出来,把各个subdir/built-in.o和当前目录的目标 .o 合并打包为当前目录的built-in.o

使用提示

  • 执行"make"来编译,执行 make clean 来清除,执行 make distclean 来彻底清除。
Makefile附件
一些符号
符号 说明
$@ 表示规则中的目标文件集
$% 当目标为函数库的时候,则表示规则中的目标成员名。反之为空。如一个目标为"foo.a(bar.o)",那么,"$%"就是"bar.o",以空格分隔开。
$< 依赖文件集合中的第一个文件,如果依赖文件以"%"形式出现,则表示符合模式的一系列文件集合
$? 所有比目标新的依赖集合,以空格分隔开。
$^ 所有依赖文件集合,以空格分隔开。如果依赖有相同,则取其一。
$+ 和 "$^"类同,但是不会把相同的删除掉。
$* 这个变量表示目标模式中 "%"及其之前的部分,如果目标是 test/a.test.c,目标模式为 a.%.c, 那么 "$* " 就是 test/a.test。

参考

posted @ 2020-11-10 21:02  李柱明  阅读(6596)  评论(3编辑  收藏  举报