Linux 开发 | 学习 Makefile

@(linux 编程)[开发技能, 工具使用]

What is GNU Make

Make 是控制工程中通过源码生成可执行文件和其他相关文件的工具。Make 通过 Makefile 获取如何编译、链接和安装清理工程的信息。

本文记录如何为自己的工程编写一个Makefile,主要参考 GNU Make Manual。获取详细信息请直接阅读手册。

[TOC]


上部分

Makefile 基本语法介绍。


Makefile概述

基本格式

基本上每一个 Makefile 主体就是由若干个以下规则模块组成 : 表明输出的目标,输出目标的依赖对象和生成目标需要执行的命令。

target ..: prerequisites ...
    recipe 
    ....
  • target : 目标、标记。可以是文件,比如下文 基本例子 中的 edit 或者 main.o,也可以是标签,比如 clean。
  • prerequisites : 先决条件,前提依赖,指明想要 target,需要先有哪些依赖。如 基本例子 中,要输出 edit, 需要先编译 main.o...等文件。
  • recipe : 执行的命令。每行命令以tab开头(.RECIPEPREFIX 默认规定的)

举个例子, 我们写了 hello.c 打印可爱的 hello world,一般编写后,我们在命令行执行 gcc -o hello hello.c 生成 hello 可执行文件。这里, hello 就是我们的目标 target, 而 gcc -o hello hello.c 就是命令 recipe, 对应的依赖 prerequisites 就是 hello.c。 所以可以写第一个 Makefile:

hello: hello.c
    gcc -o hello hello.c

然后直接在目录下输入make,就可以得到 hello

Makefile 主要组成

  • 显式规则 明确写出来的依赖关系,如上述的 prerequisites....
  • 隐式规则 Make 自己推导出来的规则,比如目标为 main.o 就推出依赖条件中需要 main.c和对应的编译命令
  • 变量定义 类似程序中宏定义, 文本替换。
  • 文件指示 (有点像程序中预编译涉及到的.)
    • include 其他 Makefile : -include xx.md;
    • 选择执行(类似选择编译 #ifdef...);
    • 定义多行命令(define ... endef)
  • 注释 以 # 开头

Make 工作流程

  • Make 读取当前目录下命名为 GNUmakefile/Makefile/makefile 或者 -f 直接指定的所有规则文件。
  • 读入被 include 的其他 Makefile,在对应位置展开
  • 初始化变量
  • 推导隐式规则;分析所有规则,创建依赖关系链,决定哪些需要【重新】生成,执行命令。
    • 从第一个 target(排除以 . 开头的 target,比如 .PHONY)开始,这个就是默认目标,本次任务的终极目标(或者可以显式设定 .DEFAULT_GOAL)。 比如在下面的 基本例子 中,edit 就是终极目标。
    • 判断目标是否存在, 依赖的对象是否有更新
    • 根据依赖关系一步一步追溯查找,建立依赖关系链,执行需要执行的命令,最终输出终极目标。
    • 没有在依赖链上的目标是不会被直接执行到的,比如 clean。

基本例子

借用手册举的例子,有一个工程,由几个.c 和 .h 文件组成,最终输出可执行文件 edit,简单(简陋..?)地用以下 makefile 描述他们的依赖关系。

objects = main.o kbd.o command.o display.o \
        insert.o search.o files.o utils.o
        
# make读取到的第一个target, 默认设置为 终极目标
edit: $(objects)
    cc -o edit $(objects)
    
# 隐含规则, make 根据 xx.o 推导出依赖 xx.c, 以及相应的编译命令
# 如 -> main.o: defs.h
# 等同 :
#   -> main.o: main.c defs.h
#   ->    cc -o main.o main.c defs.h
main.o: defs.h
kbd.o: defs.h command.h
command.o: defs.h command.h
display.o: defs.h buffer.h
insert.o: defs.h buffer.h
search.o: defs.h buffer.h
files.o: defs.h buffer.h command.h
utils.o: defs.h

# .PHONY 定义为目标 clean 
# 和 edit 没有依赖关系 直接执行 make 是不会运行到的
# 通过 make clean 执行
.PHONY: clean
clean:
    -rm edit $(objects)**

编写规则

Make 会读取 Makefile 中 第一个规则的第一个目标, 设置为要完成的最终目标。其他目标都是被这个目标的依赖关系连带进来的。比如基本例子中最终目标是 edit,而 eidt 依赖 main.o...等文件链接而来,所以 make 就会自动去执行 main.o..等文件的生成规则,最后再合成 edit。 规则包含 : 依赖关系 和 生成目标的方法

上面的 Makefile 修改一下:

# Makefile learn by lcd

SRCS = main.c
SRCS += command.c
SRCS += display.c
SRCS += files.c
SRCS += insert.c
SRCS += kbd.c
SRCS += search.c
SRCS += utils.c

# 语法替换
OBJS = $(SRCS:.c=.o)
DEPS = $(SRCS:.c=.d)

# 第一个目标 all,终极目标
all : edit

edit : $(OBJS) 
    $(CC) -o edit $(OBJS)

-include $(DEPS)
# 包含触发下面的 DEPS 依赖
$(DEPS) : %.d : %.c
    @set -e; rm -f $@;\
        $(CC) -MM $(CPPFLAGS) $< > $@.$$$$;\
        sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@;\
        rm -f $@.$$$$

.PHONY : clean
clean :
    @-rm edit $(OBJS) $(DEPS)

说明下规则

取其中一段

edit : $(OBJS) 
    $(CC) -o edit $(OBJS)
# 在这里实际展开应该是
edit : main.o command.o ...
    cc -o edit main.o command.o ...
  • 第一行说明文件的依赖关系,edit 是由 main.o command.o... 这几个文件链接而成的,依赖于他们。如果其中某个或多个 xx.o... 文件日期比 edit 新或者 edit 不存在,那么依赖关系就发生了。
  • 发生依赖关系,Make 就会去执行下面的命令(tab缩进),其说明 edit 是如何通过依赖对象生成的。Make 会以 shell(/bin/sh)来执行命令。
  • 上面这段规则,目标targets 是 edit, Makefile 中,targets 是文件名也可以是标号(比如clean),多个用空格分开,可以使用通配符(shell)。

Make 搜寻文件

实际中,比较大的工程文件都会分类放在不同目录下,当 Make 需要寻找文件依赖关系的时候,需要告知去寻找的路径,否则 make 只会查找当前目录。 两种方法:

  • VPATH (变量)
VPATH = src:../inc

如上,指定了 ./src../inc 两个目录,冒号分隔,当前目录 搜索不到依赖文件的情况下,Make 就会依顺序进行搜索。

  • vpath (关键字) 注意:这不是一个变量,按照使用方式可以多次调用设定文件的搜索模式。 vpath 使用的三种方法 1、vpath <pattern> <directories> 为符合模式<pattern>的文件指定搜索目录<directories>。 2、vpath <pattern> 清除符合模式<pattern>的文件的搜索目录。 3、vpath 清除所有已被设置好了的文件搜索目录。

vapth 使用方法中, <pattern>需要包含“%”字符。“%”的意思是匹配零或若干字符。例如,“%.h”表示所有以“.h”结尾的文件。<pattern>指定了要搜索的文件集,而 <directories>则指定了<pattern>的文件集的搜索的目录。

vpath %.c   dir1 # 在 dir1 寻找 .c 文件
vpath %     dir2 # 在 dir2 寻找 任何需要的文件
vpaht %.c   dir3 # 同 1
# 当前目录找不到的情况下i, 按照 dir1.2.3的顺序查找 .c 文件

伪目标

上面例子中,clean 就是一个伪目标。伪目标是一个标签,执行一些动作,比如清除文件,安装程序等。因为没有依赖关系,所以 make 无法直接决定是否需要执行。我们显示地用 .PHONY来告诉 make 这是一个伪目标, 避免与实际目标命名冲突。 同运行程序的时候我们给个参数让程序执行特定动作一样,运行 make 时指定伪目标标签,指定执行对应的命令。就如上述例子,执行 make clean 时进行清理工作。

静态模式

对应多个目标对象,构建每个对象对应名称的依赖关系的规则。 举个例子怎么用

OBJS = aa.o bb.o cc.o
$(OBJS) : %.o : %.c
    cc -c $< -o $@
# 等同:
aa.o : aa.c
    cc -c aa.c -o aa.o
bb.o : bb.c
    cc -c bb.c -o bb.o
cc.o : cc.c
    cc -c cc.c -o cc.o

上述例子,Make 从 OBJS 集合中获取符合 目标模式 %.o 的文件作为目标,依赖模式 %.c 取前面获取的“%.o”的“%” 部分作为自己的前缀。在文件很多的情况下,可以大大提高了书写效率。

自动生成依赖关系

如果在 main.c 中包含了 defs.h 文件,那么依赖关系上我们需要写上 defs.h,这样,当 defs.h 文件修改了(比如新定义了一个宏..),Make 才会重新执行依赖关系。但是对于一个文件包含什么头文件,对应修改 Makefile,这样是很难维护的。

C/C++ 编译器 -MM 功能可以自动找寻文件的包含 ,生成依赖关系。 执行:

$ gcc -MM mian.c

输出:

main.o : main.c defs.h

因此,我们借助编译器帮我们自动生成依赖关系,并包含到 Makefile 中

-include $(DEPS)
$(DEPS) : %.d : %.c
    @set -e; rm -f $@;\
        $(CC) -MM $(CPPFLAGS) $< > $@.$$$$;\
        sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@;\
        rm -f $@.$$$$

上述的 -include 把每个源文件对应的依赖 [.d] 文件(gcc -MM生成的依赖关系)包含进来,把 [.d] 文件的更新也纳入 Makefile 中,修改了某个文件的依赖关系,对应命令执行生成新的依赖文件。

上面的命令,每个[.d] 文件依赖对应的[.c]文件,具体说明下执行命令的作用:

  • $@ $* 和 $< 是自动变量
  • rm -f $@ 删除旧的目标文件
  • 借助编译器(-MM)为每个源文件生成依赖关系并保存到对应的 name.xxxx (在Makefile中 $ 有特殊含义,如果要表示它的字面意思需要写两个 $,所以 Makefile 中的四个 $ 传给Shell变成两个 $,而两个 $ 在Shell中表示当前进程的id,一般用它给临时文件起名,以保证文件名唯一。)
  • sed 替换输出文本,达到的目的类似如下
# 编译器输出格式 
main.o : main.c defs.h
# 转换为如下格式
main.o main.d : main.c defs.h

并保存到[.d]文件。

  • 删除临时文件

最后展开就如同开头例子一样,列出每个[.o]文件的依赖, 相比前面似乎更加复杂了,但是想想,在很多源文件的情况下,就会变得很简洁。


编写执行的命令

target .. : prerequisites ...
    recipe 
    ....

上述 recipe 命令部分由若干条 shell 命令行组成,一般用于生成、更新对象。 默认使用 /bin/sh 执行命令。 默认每行命令必须以 Tab 开头。规则下对应的所有以 Tab 开头的指令,会被传递到对应的 shell 执行。 Makefile 执行指令必须在 recipe 这个位置。

在命令中使用变量注意

前面提到, $ 在 make 和 shell 中要注意的地方, 再举个例子

LIST = one two three
all:
    for i in $(LIST); do \
        echo $$i; \
    done

实际传递给 shell 的模式

    for i in one two three; do \
        echo $i; \
    done

结果输出 :

one
two
three

避免错误使用给自己带来的错误。

命令回响

在 Makefile 中执行如下命令,

echo 命令执行

终端会输出如下 :

echo 命令执行
命令执行

第一行是执行的命令完整打印(回响),第二行才是我们需要的输出的,关闭命令回响的方法是在该行命令前添加 @

@echo 命令执行

如果 Make 执行时,带参数“-n”或“--just-print”,那么其只是显示命令,不会执行命令,这个功能有利于我们调试我们的 Makefile,看看我们书写的命令执行起来是什么样子的或是什么顺序的。 而 Make 带参数“-s”或“--slient”则是全面禁止命令的显示。

命令的依赖

shell 按顺序一条条执行规则指定的命令。但是如果需要让上一条命令的结果应用到下一条,需要用分号分隔命令并保证命令处于同一行。

假设在目录 /home/lcd/mf/ 下执行 Makefile

exec1 :
    @cd /home/lcd/kk
    @pwd

# show : /home/lcd/mf

exec2 :
    @cd /home/lcd/kk; pwd

#  show : /home/lcd/kk

exec1 显示的依然是 Makefile 执行的所在目录,因为上一条命令的结果没有应用到下一条。

忽略出错命令

一般情况,Make 会一条一条执行命令,当某条命令执行后出错, Make 会终止当前规则,这可能导致整个任务终止。 有时候执行一些命令无需考虑出错,比如某文件存在删除,不存在就不管等。 这种情况下不希望出错终止,可以在任务前添加一个减号 -

clean :
    -rm -f *.o

一个全局方法是, Make 运行加上“-i”或是“--ignore-errors”参数,那么, Makefile 中所有命令都会忽略错误。 如果一个规则是以“.IGNORE”作为目标的,那么这个规则中的所有命令将会忽略错误。 Make 的参数的是“-k”或是“--keep-going”,这个参数的意思是,如果某规则中的命令出错了,那么就终目该规则的执行,但继续执行其它规则。

Makefile 嵌套

对于一个比较大的工程,不同模块分类在不同目录,分别用一个 Makefile 进行管理,模块化编译,方便工程维护和保证 Makefile 的简洁。

  • 例如,子目录 subdir 下有一个 Makefile 描述该目录模块的编译规则, 那么总控 Makefile 中调用子目录 Makefile 可以这么写:
subsystem :
    cd subdir && $(MAKE)
# 等价
subsystem :
    $(MAKE) -C subdir

使用 $(MAKE) 宏定义,在某些情况下调用嵌套我们可以直接修改添加参数。另外,当运行 Make 时候添加诸如 ‘-t’ (‘--touch’), ‘-n’ (‘--just-print’), or ‘-q’ (‘--question’) z这些特殊选线时,使用 $(MAKE) 可以保证语句相当于在前面添加了 + 号 的作用(特殊命令,继续执行)。很正常,希望测试的时候命令不是真的执行,但是包含其他 Makefile 这种命令是例外,必须执行,不然 Makefile 就不完整了, 我是这么理解的。

  • 上层 Makefile 中定义的变量是可以在被调用的下一层 Makefile 中使用的, 前提是该变量在上层中被显式暴露 export,同理,可以采用 unexport取消。
export OBJS # 传递 变量 OBJS
export # 不指定,全部传递

如此,在下面的 makefile 就可以直接使用了。但是如果下层目录已经定义了该变量,那么下层默认使用的是它自己定义的变量值,除非上层 makefile 在调用下层 makefile 时给参数 -e,则会强行覆盖。

  • 两个变量,一个是 SHELL,一个是 MAKEFLAGS,这两个变量不管你是否 export,其总是要传递到下层 Makefile 中。
  • Make 命令中的有几个参数并不往下传递,它们是“-C”,“-f”,“-h”“-o”和“-W”
  • 嵌套执行中, “-w”或是“--print-directory”会在 Make 的过程中让你看到目前的工作目录。 比如,如果我们的下级 Make 目录是/home/lcd/mf/subdir,如果我们使用“make -w”来执行,那么当进入该目录时,我们会看到: make: Entering directory '/home/lcd/mf/subdir' 而在完成下层 make 后离开目录时,我们会看到: make: Leaving directory `/home/lcd/mf/subdir' 当你使用“-C”参数来指定 Make 下层 Makefile 时,“-w”会被自动打开的。如果参数中有“-s”(“--slient”)或是“--no-print-directory”,那么,“-w”总是失效的。

命令组宏定义

和程序中的宏定义,展开一样。因为直接展开,注意缩进问题。

define xxx_name 
xxx
xxx
endef

targe : xx
    $(xxx_name)
#等同
targe : xx
    xxx
    xxx

使用空命令

target : ;

上述规则, 执行了一条空指令。这样写的一些理由是:

  • 避免 Make 自己推测命令(隐性规则)
  • Make 不会报错他不知道该对象如何生成,并假设已经是最新。

Makefile 中的变量

Makefile 中的变量,就如程序中的宏定义,代表一个字串,在使用的地方展开,通过 $(variable) 表示变量的内容,和 shell 类似。

变量赋值

foo = $(bar)
bar = $(ugh)
ugh = huh?
all :
    echo $(foo)
# 显示 : huh?

赋值 ?=, +=, = 和 := 的差别

  • ?= 如果没有被赋值过就赋予等号后面的值
  • += 添加等号后面的值
  • = 最基本的赋值(最后才展开) make会将整个makefile展开后,再决定变量的值。也就是说,变量的值展开是在最后, 使我们可以在最后才指定变量的值。
x = XXX
y = $(x)
x = YYY

在上例中,y的值将会是 YYY ,而不是 XXX。

  • := 是覆盖之前的值(类似C中的 = ) 变量的值决定于它在makefile中的位置,而不是整个makefile展开后的最终值。
x := XXX
y := $(x)
x := YYY

在上例中,y的值将会是 XXX ,而不是 YYY了。

变量值替换

foo := a.o b.o c.o  
bar := $(foo:.o=.c)
# 等同
foo := a.o b.o c.o  
bar := $(foo:%.o=%.c)

# bar : a.c b.c c.c

定义一个空格

nullstring :=  
space := $(nullstring) # end of the line  

操作符号对右边一开始的空格不做处理,所以很难描述一个空格,上面的方法实现了,sapce 保存一个空格。

override 指示符

如果在运行 Make 的时候在命令参数设置了变量,则 Makefile 对变量的设置默认被忽略,如果不想被忽略,可以使用override

override <variable> = <value>   
override <variable> := <value>  
override <variable> += <more text>  

模式变量

对应变量只应用到符合模式的对象上

%.o : CFLAGS = -O 
# <pattern ...> : override <variable-assignment>

自动变量

为了方便扩展, 经常会看到类似的一些奇怪符号。

  • $@ $@ 指代当前目标,Make 命令当前构建的那个目标。比如,make foo 的 $@ 就指代 foo。
a.txt b.txt: 
    touch $@
#等同于下面的写法。
a.txt:
    touch a.txt
b.txt:
    touch b.txt
  • $< $< 指代第一个依赖条件。 如果依赖对象是一个序列,依次代表每一个依赖条件
a.txt: b.txt c.txt
    cp $< $@ 
#等同于下面的写法。
    cp b.txt a.txt 
    cp c.txt a.txt 
  • $^ $^ 指代所有前置条件,之间以空格分隔。 比如,规则为 t: p1 p2,那么 $^ 就指代 p1 p2 。
  • $? $? 指代比目标更新的所有前置条件,之间以空格分隔。 比如,规则为 t: p1 p2,其中 p2 的时间戳比 t 新,$?就指代p2。
  • $* $* 指代匹配符 % 匹配的部分, 比如 %.txt 匹配 f1.txt 中的 f1 ,$* 就表示 f1。
  • $(@D)$(@F) $(@D)$(@F) 分别指向 $@ 的目录名和文件名。 比如,$@ 是 src/input.c,那么$(@D) 的值为 src ,$(@F) 的值为 input.c。
  • $(<D)$(<F) $(<D)$(<F) 分别指向 $< 的目录名和文件名。

手册中的详细描述


条件判断

简述

类比程序中的条件编译, Make 可以根据运行时不同情况选择执行不同分支。

libs_for_gcc = -lgnu
normal_libs =

foo : $(OBJS)
ifeq ($(CC), gcc)  # 不缩进
    $(CC) -o foo $(OBJ) $(libs_for_gcc) # 传递命令,缩进
else                
    $(CC) -o foo $(OBJ) $(normal_libs)
endif

简单地说,上面表达的是,如果使用的编译器是 gcc,则编译时添加参数libs_for_gcc,否则给另一个参数normal_libs。 其实和 C 中的条件编译差不多

注意

条件语句部分不需要缩进, 否则会被认为是传递给 shell 的命令

Make 条件判断语法

看起来和 shell 中的条件判断差不多,

分支组成

# if-endif
conditional-directive 
    text-if-true 
endif 

# if-else-endif
conditional-directive 
    text-if-true 
else 
    text-if-false 
endif 

# if-elsif0-elsif2..-elsifn-else-endif
conditional-directive-one 
    text-if-one-is-true 
else  conditional-directive-two 
    text-if-two-is-true 
else 
    text-if-one-and-two-are-false 
endif 

# 多分支例子
ifeq $(STRING), 'AA'
    echo AA
else ifeq $(STRING), 'BB'
    echo BB
else
    echo XX
endif

判断关键词

  • 判断相等 完全展开变量进行比较
ifeq (ARG1, ARG2)
ifeq 'ARG1' 'ARG2'
ifeq "ARG1" "ARG2"
ifeq "ARG1" 'ARG2'
ifeq 'ARG1' "ARG2"
# 对应
#ifneq

手册举的例子,用于判断变量是否为空

ifeq ($(strip $(foo)),) 
    text-if-empty 
endif 
  • 判断是否定义
ifdef variable_name
ifndef variable_name

注意例子, 只做第一次展开!!!!,体会下一下例子差别

bar =
foo = $(bar)
ifdef foo               
# 展开 foo -> $(bar)
# 所以认为定义了, 即使 foo 最终是空
    echo foo def
endif

ifdef bar
# 展开 bar, 空,认为未定义
    echo bar def
endif

对于嵌套 Makefile, 不允许一个完整的 if-endif 语句跨越两个 Makefile

例子,判断执行 flag

函数 findstring 用于判断 A 字符串是否在 B 字符串, 没有返回空,有返回 A 下面例子, 根据是否带有“-t” 参数执行不同命令。

archive.a: ...
ifneq (,$(findstring t,$(MAKEFLAGS)))
# 如果执行make -t, 则执行这里
# -t touch, 表示实际不执行命令,但是这里需要这个参数后命令
# 依旧执行,所以前缀 ``+``
    +touch archive.a
    +ranlib -t archive.a
else
    ranlib archive.a
endif

函数调用

调用语法

函数可以出现在任何变量可以出现的位置,对变量进行文本处理。

$(function     arguments)  # 风格 1
${function     arguments}  # 风格 2
# 选一种 别混用 统一好看避免不必要麻烦

如上, 对应下面例子中, function 对应“subst”, arguments 对应“$(space),$(comma),$(foo))”, “subst”这个函数提供替换字符功能

comma:= , 
empty:= 
space:= $(empty) $(empty) 
foo:= a b c 
bar:=   $(subst $(space),$(comma),$(foo))
# foo 中的空格替换为逗号
#  bar is now ‘a,b,c’. 

function 后面对应传递的参数,第一个参数与函数名通过空格或者 tab 划分,如果一个函数参数不止一个,不同参数通过逗号分隔。函数调用返回,通过 $ 获取,和变量使用一致。

字符串处理函数

文本替换函数

  • 简单替换 将“text”中的“from”部分替换为“to”
$(subst from ,to ,text)
# 例子
# 返回 : ‘fEEt on the strEEt’. 
$(subst ee, EE, feet on the street) 
  • 模式替换 读取函数名后面的模式, 匹配“text”中符合的部分替换为第二个参数指定的内容。
$(patsubst pattern ,replacement ,text) 
# 例子 1
# 模式替换,同时, 多个空格会被折叠为一个
# 返回 :x.c.o bar.o
$(patsubst %.c, %.o, x.c.c      bar.c) 

# 例子 2
objects = foo.o bar.o baz.o 
$(patsubst %.o, %.c, $(objects)) 
# equil to
$(objects:.o=.c) 

去空格函数

去除字符串开头和结尾的空格,同时对中间的多个空格替换为一个。

$(strip string) 
# 例子
# 返回: a b c 
$(strip a b   c )

在判断变量是否为空的情况下使用,可以避免多次赋值带来的空格影响,提高鲁棒性

字符查找函数

判断字符中是否包含指定字符串, 有返回查找的字符串,否则返回空。

$(findstring find ,in) 
# 例子
# 返回 : a
$(findstring a,a b c)
# 返回 : ""
$(findstring a,b c) 

字符串模式过滤

返回符合或者不符合的字符串, 输入字符单词空格区分

  • 返回符合的字符串
$(filter pattern ...,text) 
# 例子
# 返回 foo.c bar.c baz.s
sources := foo.c bar.c baz.s ugh.h 
$(filter %.c %.s, $(sources)) 
  • 返回不符合的字符串
$(filter-out pattern ...,text) 
# 例子
# 返回 foo.o bar.o
objects = main1.o foo.o main2.o bar.o 
mains = main1.o main2.o 
$(filter-out $(mains), $(objects)) 

排序、去重函数

按字母顺序对序列(空格划分)进行排序,同时去除重复的词组, 返回按单个空格进行划分。

$(sort list) 
# 例子
# 返回 : bar foo lose 
$(sort foo bar   lose lose) 

字符串切片函数

数组数组

$(word n ,text) 
# 返回 : bar
$(word   2,  foo bar baz) 

$(wordlist s ,e ,text) 
# 返回 : bar baz 
$(wordlist 2, 3, foo bar baz) 

$(words text)
# 字符串成员个数 空格划分
# 返回 : 4
$(words aa bb cc dd)

$(firstword names ...)
# 返回 : foo
$(firstword foo bar) 

$(lastword names ...) 
# 返回 : bar
$(lastword foo bar) 

举个实际例子

C 编译器编译参数 -I 后带路径,下面例子通过 VPATH 生成 CFLAGS 变量供编译器使用。

VPATH = src:../headers
# 空格 替换原来的分隔符号
# 返回 : src ../headers
$(subst :, ,$(VPATH))
# 加上-I 前缀
# 返回 : -Isrc -I../headers
override CFLAGS += $(patsubst %,-I%,$(subst :, ,$(VPATH)))

文件名处理函数

$(dir names ...)
# 返回除去最后文件名的路径部分, 没有路径直接返回“./”
# 返回 : lcd/src/ ./ 
$(dir lcd/src/foo.c hacks) 

$(notdir names ...) 
# 返回不包含目录的文件名
# 返回 : foo.c hacks 
$(notdir src/foo.c hacks lcd/)

$(suffix names ...)
# 返回文件后缀(逆序第一个 . 后面字符串)
# 返回 : .c .c .o
$(suffix src/foo.c src-1.0/bar.c hacks lcd.c.o) 

$(basename names ...) 
# 去除后缀, 文件目录内的后缀(.)不包括
# 返回 : src/foo src-1.0/bar hacks
$(basename src/foo.c src-1.0/bar hacks)

$(addsuffix suffix, names ...) 
# 加后缀
# 返回 : foo.c bar.c 
$(addsuffix .c, foo bar) 

$(addprefix prefix, names ...) 
# 加前缀
# 返回 : src/foo src/bar 
$(addprefix src/, foo bar) 

$(join list1, list2)
# 对应连接参数
# 返回 : a.c b.o 
$(join a b, .c .o)

$(wildcard pattern ) 
# 获取工作目录下所有符合模式的文件
# 返回所有 .C 文件
$(wildcard *.c)

$(realpath names ...) 
# 返回绝对路径, 不包含 . 或者 ..
# 如果文件不存在,返回空

$(abspath names ...) 
# 返回绝对路径, 不包含 . 或者 ..

条件函数

  • if 如果 condition 为真, 返回 then-part 代表的值, 否则放回 else-part 的值(没有的话默认返回空)。
$(if condition,then-part [,else-part])

# 例子
# 返回 : false
con =
$(if $(conn), "true", "false")
  • or 依次展开每个参数,遇到非空的就停止,并返回该值,否则最后返回空。
$(or  condition1 [,condition2 [,condition3 ...]])

# 例子
# 返回 : CONN2
conn1 =
conn2 = CONN2
conn3 = CONN3
$(or $(conn1), $(conn2), $(conn3))
  • and 展开所有参数,如果有一个为空,返回空,如果全部都不为空,返回最后一个参数。
$(and  condition1 [,condition2 [,condition3 ...]])

# 例子 1
# 返回 : 空, 因为有一个空
conn1 =
conn2 = CONN2
conn3 = CONN3
$(and $(conn1), $(conn2), $(conn3))

# 例子 2
# 返回 : CONN3
conn1 = CONN3
conn2 = CONN2
conn3 = CONN3
$(and $(conn1), $(conn2), $(conn3))

循环函数 foreach

这个函数执行过程, 按顺序依次取出 list 中的单词逐个取出放入到临时变量 var 中, 返回 text, 每次返回的 text 以空格分开,遍历所有单词后返回完整的组合字符串。

$(foreach var, list, text)

# 例子 1
# 返回 a.c b.c c.c d.c e.c f.c
list = a b c d e f
$(foreach var, list, $(var).c)

#例子 2
dirs := a b c d
files := $(foreach dir, $(dirs), $(wildcard $(dir)/*))
# 等同于
files := $(wildcard a/* b/* c/* d/*)

读写文件函数

file 支持读写,通过 op 确定操作, 后跟操作文件和写入文本(读取的时候不能包含),写操作,如果文件不存在,会自动创建。

  • “>” 覆盖写
  • “>>” 追加写
  • “< ” 读取
$(file op filename[,text])

没试过...

自定义函数

当make执行这个函数时,variable参数中的变量,如$(1),$(2),$(3)等,会被参 数 parm1, parm2,parm3 依次取代。而 variable 的返回值就是call函数的返 回值。例如:

$(call variable,param1,param2,...)

#  例子
reverse1 = $(1) $(2)
reverse2 = $(2) $(1)
foo1 = $(call reverse1, a, b) 
foo2 = $(call reverse2, a, b)
# foo1 == a b
# foo2 == b a

可以通过 call 函数定义复杂的函数组合

value 函数

value 函数返回变量未经展开的值, 如例子

a = aabb
b = $(a)
echo $(b)
# aabb
echo '$(value b)'
# $(a)

shell 函数

Makefile 中除了命令区域,是不能直接执行 shell 命令,但是可以通过 shell 函数执行,调用该函数,会生成一个新的程序,所以需要注意效率问题。 例子, 在 Makefile 中获取最后一个 git 提交的 SHA 赋值给变量。

FW_VER = $(shell git log -1 --pretty="%h")

Make 控制函数

用于在运行过程中提供信息, 打印 log

@echo $(info msg)
@echo $(warning msg)
@echo $(error msg)
# error 中断执行

origin 函数

不操作变量, 返回变量定义的地方

eval 函数

flavor 函数

guile 函数


下部分

运行参数返回值以及隐含规则等介绍。 具体手册



参考

GNU Make Manual 中文版-跟我一起写makefile

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏武军超python专栏

2018-7-16python中四种组合数据类型和pycharm的安装和使用

集合(set) discard删除数据时如果集合里面没有那个数据什么也不做,集合相减可以直接用-,+*/都不能用

21150
来自专栏Janti

Java多线程高并发学习笔记(一)——Thread&Runnable

 进程与线程 首先来看百度百科关于进程的介绍: 进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它可以申请和拥有系统资源,是一个动态的概念,是一个活...

764100
来自专栏芋道源码1024

Java初中级面试题(2)

63270
来自专栏Linux驱动

37.Linux驱动调试-根据oops的栈信息,确定函数调用过程

在上章里,我们分析了oops的PC值在哪个函数出错的 本章便通过栈信息来分析函数调用过程 1.上章的oops栈信息如下图所示: ? 9fe0: 代表最初的栈顶S...

29350
来自专栏Python私房菜

简析Python中的四种队列

在Python文档中搜索队列(queue)会发现,Python标准库中包含了四种队列,分别是queue.Queue / asyncio.Queue / mult...

16930
来自专栏黑白安全

C++如何调用class类中方法实现多线程编程

众所周知在使用C++创建多线程执行时只能传递一个方法到thread模块中去创建线程执行。但是有时候我们往往需要使用多线程去执行某个对象中的方法,而对象中的方法却...

13320
来自专栏JackeyGao的博客

Django小技巧08: Blank or Null

Django Model API 中提供了blank和null两个参数, 非常容易混淆。当我第一次使用 Django 的时候, 总是不能恰当的使用这两个参数。

9430
来自专栏技巅

GlusterFS之内存池(mem-pool)使用实例分析

21560
来自专栏我是攻城师

关于线程可见性一个“诡异”的问题

如果执行上面的代码,大多人可能觉得会死循环,因为这里没有任何的同步策略,比如synchronized,Lock,atomic,volatile等关键字,也就是说...

10930
来自专栏Python小屋

Python编程常见出错信息及原因分析(1)

1.被0除错误 演示代码: >>> 2 / 0 Traceback (most recent call last): File "<pyshell#0>",...

33260

扫码关注云+社区

领取腾讯云代金券