• Makefile+Make基础知识


    Makefile(make)基础

    Author:Onceday date:2022年7月27日

    本文收集整理于陈皓大佬的文章,以便贴合自己的思维逻辑,以下是原文文档:

    1.引言

    当源代码很多,代码架构复杂时,需要靠自动化编译工具来完成这些功能。

    Makefile可用作gcc+make工具的配置文件。

    其遵循以下原则:

    • 如果这个工程没有编译过,那么我们的所有C文件都要编译并被链接。

    • 如果这个工程的某几个C文件被修改,那么我们只编译被修改的C文件,并链接目标程序。

    • 如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的C文件,并链接目标程序。

    实际上,当代码有结构上更改时,务必全部编译一遍。只有小更改,如一些逻辑上改动,才可编译部分文件。

    Makefile文件的组成逻辑如下:

    target(目标文件):prerequisites(要求列表) ...
    command(shell gcc 命令)
    
    • 1
    • 2
    • target是一个目标文件,可以是Object File,也可以是执行文件。还可以是一个标签(Label)。

    • prerequisites就是生成target所需要的文件或是目标。

    • command是make需要执行的命令。

    核心规则:,prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。这就是Makefile的规则。也就是Makefile中最核心的内容。

    一个简单的例子

    onceday : once.o day.o
        gcc -o onceday once.o day.o
    once.o : once.c once.h
        gcc -o once.o -c once.c
    day.o  : day.c  day.h
        gcc -o day.o  -c day.c
    clean:
        rm -rf *.o
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    目标文件后面的就是依赖关系, 依赖关系说明了目标文件由哪些文件生成,因此在这些文件改变后需要重新编译或生成。

    接下来一行定义了如何生成目标文件的系统命令,以Tab键作为开头。

    clean是一个Target,但后面什么都没有,因此就像一个标签,需要显式通过make clean去调用它。

    make工作逻辑:

    1. make会在当前目录下找名字叫“Makefile”或“makefile”的文件。

    2. 如果找到,它会找文件中的第一个目标文件(target),如onceday。

    3. 如果onceday文件不存在,或是onceday所依赖的后面的 .o 文件的文件修改时间要比onceday这个文件新,那么,他就会执行后面所定义的命令来生成onceday这个文件。

    4. 如果onceday所依赖的.o文件也存在,那么make会在当前文件中找目标为.o文件的依赖性,如果找到则再根据规则(3)生成.o文件。

    5. 存在所需的C文件和H文件,于是make会生成 .o 文件,然后再用 .o 文件去完成make的第一个任务。

    make只处理makefile文件中的依赖关系,命令错误和文件缺失它不会处理。

    2.makefile的变量使用

    OBJ = once.o day.o
    
    onceday : $(OBJ)
        gcc -o onceday once.o day.o
    once.o : once.c once.h
        gcc -o once.o -c once.c
    day.o  : day.c  day.h
        gcc -o day.o  -c day.c
    clean:
        rm -rf *.o
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    类似于C语言的宏定义。直接文本替换。

    3.make自动推导makefile(隐晦规则)

    OBJ = once.o day.o
    
    onceday : $(OBJ)
        gcc -o onceday $(OBJ)
    once.o : once.h
    day.o  : day.h
    
    .PHONY :clean
    clean:
        rm -rf *.o
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    GNU的make很强大,它可以自动推导文件以及文件依赖关系后面的命令:

    • make看到一个[.o]文件,会自动的把[.c]文件加在依赖关系中

    • 并且 cc -c xxx.c -o xxx 也会被推导出来,

    .PHONY表示clean是个伪目标文件。

    注意:隐晦规则是由make支持,为了兼容性,最好编写纯粹的makefile文件。

    4.makefile包含和引用

    默认的makefile文件名是Makefilemakefile

    可用make自定义使用makefile文件

    make -f Make.Linux 
    make --file Make.AIX
    
    • 1
    • 2

    可以使用include关键字包含别的Makefile。如下形式:

    include foo.make *.mk $(bar)
    
    • 1
    • 会在当前的目录下寻找文件

    • 会在make的-I或者--include-dir的参数目录下寻找。

    • 会在/include下寻找, 如/usr/local/bin

    以下命令可以避免include报错退出运行:

    -include     #gcc make
    sinlcude     #其他版本
    
    • 1
    • 2

    环境变量MAKEFILES

    • 相当于include文件,文件来自MAKEFILES的值。

    • 全局作用,会影响所有的makefile文件。

    4.1 makefile文件搜寻

    VPATH用于在当前目录下找不到时,去指定目录查找的情况。

    VPATH = src:../headers
    
    • 1
    • 当前目录优先级最高

    • 目录由冒号分割

    • 按照从左到右的顺序依次查找

    5. 伪目标

    伪目标不是一个文件,而是一个标签,因此make无法生成它的依赖关系和决定它是否要执行,只有通过显示地指明这个“目标”才能让其生效。

    为了避免和文件重名的这种情况,可以使用一个特殊的标记“.PHONY”来显示地指明一个目标是“伪目标”,向make说明,不管是否有这个文件,这个目标就是“伪目标”。

    伪目标一般没有依赖的文件。可以为伪目标指定所依赖的文件。伪目标同样可以作为“默认目标”,只要将其放在第一个。

    all : once day
    .PHONY : all
    
    once: once.o
    gcc -o once once.o
    
    
    day: day.o
    gcc -o day day.o
    
    .......
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    由于伪目标的特性是,总是被执行的,所以其依赖项就会被执行,这样可以同时编译多个可执行文件了。

    6.多目标规则

    多个目标同时依赖于一个文件,并且其生成的命令大体类似,可以把其合并起来。

    bigoutput littleoutput : text.g
    generate text.g -$(subst output,,$@) > $@
    #等价于下面语句
    bigoutput : text.g
    generate text.g -big > bigoutput
    littleoutput : text.g
    generate text.g -little > littleoutput
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • $@表示目标的集合 ,会依次取出目标,并执行subst函数。
    6.1 静态模式
     :  : 
        
    ......
    
    • 1
    • 2
    • 3
    • target 定义了一系列的文件,即文件集合。

    • target-parrtern 指明了targets的模式,即文件集合中的目标文件。

    • prereq-parrterns 是目标的依赖模式,即文件集合中的目标文件的依赖文件。

    objects = foo.o bar.o
    all: $(objects)
    $(objects): %.o: %.c
    $(CC) -c $(CFLAGS) $< -o $@
    #展开后如下
    foo.o : foo.c
    $(CC) -c $(CFLAGS) foo.c -o foo.o
    bar.o : bar.c
    $(CC) -c $(CFLAGS) bar.c -o bar.o
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    %表示后缀匹配,即%.c表示所有以.c结尾的文件。

    $<表示遍历依赖文件。$@表示遍历目标文件

    6.2 自动生成依赖项

    gcc的-M命令可以自动寻找头文件包含关系。 如下:

    gcc -M     xxx.c  #会把标准库的头文件也包含在其中。
    gcc -MM    xxx.c  #会把标准库的头文件剔除掉
    
    • 1
    • 2

    生成一个“name.d"的makefile文件,专门用来存放依赖关系。且使make自动更新[.d]文件。

    %.d: %.c
    @set -e; rm -f $@; \
    $(CC) -M $(CPPFLAGS) $< > $@.$$$$; \
    sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
    rm -f $@.$$$$
    
    • 1
    • 2
    • 3
    • 4
    • 5

    上面就是一个能自动生成[.d]文件并更新其的模式规则。

    按一下方式使用即可:

    sources = foo.c bar.c
    include $(sources:.c=.d)
    
    • 1
    • 2

    7. makefile命令规则

    7.1 显示命令

    正常情况下,make会把要执行的命令在命令执行前输出到屏幕上。

    可使用@字符来取消显示!

    @echo 正在编译xxx模块  
    
    • 1

    会输出正在编译xxx模块

    可用make -n或者--just-print来只显示命令,而不会执行它。

    7.2 命令执行

    让上一个命令的效果能保持到第二个命令执行:

    xxx : xxx.x
        cd /home/xxx;pwd       #使用分号连接,只能位于一行。
    
    • 1
    • 2
    7.3 命令出错

    每当命令运行完后,make会检测每个命令的返回码,如果命令返回成功,那么make会执行下一条命令,当规则中所有的命令成功返回后,这个规则就算是成功完成了。如果一个规则中的某个命令出错了(命令退出码非零),那么make就会终止执行当前规则。

    可以通过忽略命令出错来避免这种情况:

    clean:
        -rm -f *.o
    
    • 1
    • 2
    7.4 嵌套执行make
    subsystem:
    $(MAKE) -C subdir
    
    • 1
    • 2

    其含义是先进入“subdir”目录,然后执行make命令。

    可以使用export向下面传递参数:

    export      #传递列表中的参数
    unexport    #不想传递的参数
    export     #传递所有的变量
    
    • 1
    • 2
    • 3

    使用以下make命令可显示进入文件夹的信息:

    make -w
    
    • 1
    7.5 定义命令包

    如果Makefile中出现一些相同命令序列,那么我们可以为这些相同的命令序列定义一个变量。

    define run-yacc
    yacc $(firstword $^)
    mv y.tab.c $@
    endef
    
    
    $(run-yacc) #调用命令
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    make再执行命令时,命令包中的每个命令会被依次独立执行。

    8.变量

    在Makefile中,变量可以使用在“目标”,“依赖目标”,“命令”或是Makefile的其它部分中。

    变量的命名字可以包含字符、数字,下划线(可以是数字开头),但不应该含有“:”、“#”、“=”或是空字符(空格、回车等)。

    变量是大小写敏感的,“foo”、“Foo”和“FOO”是三个不同的变量名。

    可按以下方式定义变量的值:

    foo = $(bar)
    bar = xxxx
    
    x := foo  #拒绝使用后定义的变量
    
    • 1
    • 2
    • 3
    • 4

    如何定义空格变量:

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

    注意: 这里先用一个Empty变量来标明变量的值开始了,而后面采用“#”注释符来表示变量定义的终止。

    因此像上面x := foo #xxx的定义会额外包含几个空白字符

    为了避免重复定义某个变量:

    FOO ?= bar
    
    • 1

    其含义是,如果FOO没有被定义过,那么变量FOO的值就是“bar”,如果FOO先前被定义过,那么这条语将什么也不做。

    8.1 变量替换

    变量值的替换:

    foo := a.o b.o c.o
    bar := $(foo:.o=.c)
    
    • 1
    • 2

    其意思是把变量"foo"中所有以“.o”结尾的字符替换成“.c"。

    或者利用静态模式:

    foo := a.o b.o c.o
    bar := $(foo:%.o=%.c)
    
    • 1
    • 2

    把变量的值再当前变量:

    x = y
    y = z
    z = u
    a := $($($(x)))
    
    • 1
    • 2
    • 3
    • 4

    用多个变量的值来组成一个变量的名字:

    first_second = Hello
    a = first
    b = second
    all = $($a_$b)
    
    • 1
    • 2
    • 3
    • 4

    变量的值可以是函数,也可以用到左边。

    8.2 追加变量值
    objects = main.o foo.o bar.o utils.o
    objects += another.o
    
    • 1
    • 2

    make会处理递归定义的问题,使用时无须担心。

    8.3 override指示符

    如果有变量是通常make的命令行参数设置的,那么Makefile中对这个变量的赋值会被忽略。

    override  = 
    override  := 
    override  += 
    
    • 1
    • 2
    • 3

    多行定义使用define指示符:

    override define foo
    bar
    endef
    
    • 1
    • 2
    • 3

    define指示符后面跟的是变量的名字,而重起一行定义变量的值,定义是以endef关键字结束。

    因为命令需要以[Tab]键开头,所以如果你用define定义的命令变量中没有以[Tab]键开头,那么make就不会把其认为是命令。

    8.4 目标变量

    可以为某个局部目标设置局部变量,而不会影响规则链外的全局变量的值。

     : 
     : override 
    
    • 1
    • 2

    示例如下:

    prog : CFLAGs = -g
    prog : prog.c foo.c bar.c
    $(CC) $(CFLAGS) prog.o foo.o bar.o
    
    • 1
    • 2
    • 3
    8.5 模式变量

    GNU的make中,支持模式变量(Pattern-specific Variable),,其把变量定义在某个目标上。可以给定一种“模式”,可以把变量定义在符合这种模式的所有目标上

    如下:

    %.o : CFLAGS = -o
    
     : 
     : override 
    
    • 1
    • 2
    • 3
    • 4

    9.使用条件判断

    libs_for_gcc = -lgnu
    normal_libs = 
    
    foo: $(objects)
    ifeq ($(CC),gcc)
    $(CC) -o foo $(objects) $(libs_for_gcc)
    else
    $(CC) -o foo $(objects) $(normal_libs)
    endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    条件表达式的语法为:

    
    
    endif 
    
    
    
    
    else
    
    endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    其中表示条件关键字,有四个,如下:

    xxxx (,)
    xxxx "" ""
    xxxx '' ''
    
    ifeq (,)
    ifneq "arg1" "arg2"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    ifdef用于测量变量是否有值:

    ifdef 
    xxxx
    else
    xxxx
    endif
    
    • 1
    • 2
    • 3
    • 4
    • 5

    最后一个就是ifndef,含义和ifdef相反。

    条件表达式的值在读取makefile的时候就开始计算了,并根据条件表达式的值来选择语句。

    10. 函数的使用

    其使用格式如下:

    $( )  #也可以使用大括号
    $(subst a,b,$(x)) 
    
    • 1
    • 2
    10.1 字符串处理函数
    10.1.1. 字符替换函数 subst
    $(subst ,,)
    
    • 1

    把字串中的字符串替换成

    10.1.2. patsubst 模式字符串替换函数
    $(patsubst ,,)
    
    • 1

    查找中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式,如果匹配的话,则以替换。这里,可以包括通配符“%”,表示任意长度的字串。如果中也包含“%”,那么,中的这个“%”将是中的那个“%”所代表的字串。(可以用“\”来转义,以“%”来表示真实含义的“%”字符)

    10.1.3. strip 去空格函数
    $(strip )
    
    • 1

    去掉字串中开头和结尾的空字符。

    10.1.4. findstring 查找字符串函数
    $(findstring ,)
    
    • 1

    在字串中查找字串。

    找到返回,如果找不到,返回空字符串。

    10.1.5. filter 过滤函数
    $(filter ,)
    
    • 1

    模式过滤字符串中的单词,保留符合模式的单词。可以有多个模式。

    返回符合模式的字串。

    10.1.6. filter-out 反过滤函数
    $(filter-out ,)
    
    • 1

    模式过滤字符串中的单词,去除符合模式的单词。可以有多个模式。返回不符合模式的字串。

    10.1.7.sort 排序函数
    $(sort )
    
    • 1

    给字符串中的单词排序(升序)。返回排序后的字符串。

    会去掉中相同的函数。

    10.1.8. word 取单词函数
    $(word ,)
    
    • 1

    取字符串中第个单词。(从一开始),返回字符串中第个单词。如果中的单词数要大,那么返回空字符串。

    10.1.9. wordlist 取单词串函数
    $(wordlist ,,)
    
    • 1

    从字符串中取从开始到的单词串。是一个数字。返回字符串中从的单词字串。如果中的单词数要大,那么返回空字符串。如果大于的单词数,那么返回从开始,到结束的单词串。

    10.1.10. words 单词个数统计函数
    $(words )
    
    • 1

    统计 中字符串中的单词个数。返回 中的单词数。如果要取 中最后的一个单词,可以这样: $(word $(words ),)

    10.1.11. firstword 首单词函数
    $(firstword )
    
    • 1

    取字符串中的第一个单词,返回字符串的第一个单词。

    10.1.12. 字符串函数实例—指定编译器对头文件的搜索路径参数CFLAGS
    override CFLAGS += $(patsubst %,-I%,$(subst :, ,$(VPATH))
    
    • 1
    10.2 文件名操作函数

    每个函数的参数字符串都会被当做一个或是一系列的文件名来对待。

    10.2.1 dir 去目录函数
    $(dir )
    
    • 1

    从文件名序列中取出目录部分。目录部分是指最后一个反斜杠(“/”)之前的部分。如果没有反斜杠,那么返回“./”。返回文件名序列的目录部分。

    示例:

    $(dir src/foo.c hacks)  #返回值 “src/ ./"
    
    • 1
    10.2.2 notdir 取出非目录部分
    $(notdir ) 
    
    • 1

    从文件名序列中取出非目录部分。非目录部分是指最后一个反斜杠(“ /”)之后的部分。返回文件名序列的非目录部分。

    示例:

    $(notdir src/foo.c hacks) #返回值是 “foo.c hacks"
    
    • 1
    10.2.3 suffix 取后缀函数
    $(suffix ) 
    
    • 1

    从文件名序列中取出各个文件名的后缀。返回文件名序列的后缀序列,如果文件没有后缀,则返回空字串。

    10.2.4 basename 取前缀函数
    $(basename )
    
    • 1

    从文件名序列中取出各个文件名的前缀部分。返回文件名序列的前缀序列,如果文件没有前缀,则返回空字串。

    10.2.5 addsuffix 加后缀函数
    $(addsuffix )
    
    • 1

    把后缀加到中的每个单词后面。返回加过后缀的文件名序列。

    10.2.6 addprefix 加前缀函数
    $(addprefix ,)
    
    • 1

    把前缀加到中的每个单词后面。返回加过前缀的文件名序列。

    10.2.7 join 连接函数
    $(join ,)
    
    • 1

    中的单词对应地加到的单词后面。如果的单词个数要比的多,那么,中的多出来的单词将保持原样。如果的单词个数要比多,那么,多出来的单词将被复制到中。

    示例:

    $(join aaa bbb,111 222 333) #返回值是“aaa111 bbb222 333"
    
    • 1
    10.3 foreach 函数

    循环函数,与shell里面的几乎一致:

    $(foreach ,,)
    
    • 1

    把参数中的单词逐一取出放到参数所指定的变量中,然后再执行所包含的表达式。每一次会返回一个字符串,循环过程中,的所返回的每个字符串会以空格分隔,最后当整个循环结束时,所返回的每个字符串所组成的整个字符串(以空格分隔)将会是foreach函数的返回值。

    相当于一个临时的局部变量,作用域只在foreach函数当中。

    10.4 if函数

    if函数很像GNU的make所支持的条件语句—ifeq,其可以包含“else”部分,或是不含。

    $(if ,)
    $(if ,,)
    
    • 1
    • 2

    参数是if的表达式,如果其返回的为非空字符串,那么这个表达式就相当于返回真,于是,会被计算,否则会被计算。

    if函数的返回值是,如果为真(非空字符串),那个会是整个函数的返回值,如果为假(空字符串),那么会是整个函数的返回值,此时如果没有被定义,那么,整个函数返回空字串。

    10.5 call函数

    call函数是唯一一个可以用来创建新的参数化的函数。可以写一个非常复杂的表达式,这个表达式中,可以定义许多参数,然后可以用call函数来向这个表达式传递参数。

    $(call ,,,,.....)
    
    • 1

    当make执行这个函数时,参数中的变量,如$(1)$(2)$(3)等,会被参数依次取代

    示例:

    reverse = $(1) $(2)
    foo = $(call reverse,a,b)
    
    • 1
    • 2

    参数的次序是可以自定义的,不一定是顺序的,序号对上即可!

    10.6 origin函数

    该函数不操作变量的值,只是告诉这个变量是哪里来的。

    $(origin ) #直接写变量的名字,不需要添加$符号
    
    • 1
    • “undefined”,如果从来没有定义过。

    • “default”,如果是一个默认的定义。

    • “environment”,如果是一个环境变量。

    • “file”,如果这个变量被定义在Makefile中。

    • “command line”,如果这个变量是被命令行定义的。

    • “override”,如果是被override指示符重新定义的。

    • “automatic”,如果是一个命令运行中的自动化变量。

    示例,用于判断某些变量的定义环境:

    ifdef bletch
    ifeq "$(origin bletch)" "environment"
    bletch = barf, gag, etc.
    endif
    endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    10.7 shell函数

    它的参数应该就是操作系统Shell的命令,并把执行操作系统命令后的输出作为函数返回。

    files :=$(shell echo *.c)
    
    • 1

    注意该函数的可能执行次数,因为这会对性能产生影响。

    10.8 控制make的函数
    10.8.1 error 产生一个致命错误
    $(error )
    
    • 1

    产生一个致命的错误,是错误信息。注意,error函数不会在一被使用就会产生错误信息,所以如果你把其定义在某个变量中,并在后续的脚本中使用这个变量,那么也是可以的。

    10.8.2 warning 产生一段警告信息
    $(warning )
    
    • 1

    它并不会让make退出。

    (基础的makefile+make知识就限于此,后续将总结常见规则)

  • 相关阅读:
    R语言检验样本是否符合正态性(检验样本是否来自一个正态分布总体):shapiro.test函数检验样本是否符合正态分布(normality test)
    同态加密:正则嵌入、理想格和RLWE问题
    华润燃气牵手腾讯云 数字技术助力燃气行业高质量发展
    Qt安装教程
    程序员新手村常见问题
    TP5 模型更新的返回值、返回值的判断以及所使用的SQL
    【Unity】零基础实现塔防游戏中敌人沿固定路径移动的功能
    DataX二次开发——(5)基于CopyIn原理新增greenplumwriter
    垃圾分代收集的过程是怎样的?
    ubuntu20.04挂载拓展盘保姆级流程
  • 原文地址:https://blog.csdn.net/Once_day/article/details/126032553