【Android构建篇】MakeFile语法< 2 >

简介: 对于一个看不懂Makefile构建文件规则的人来说,这个Makefile语法和shell语法是真不一样,但是又引用了部分shell语法,可以说是shell语法的子类,Makefile语法继承了它。和shell语法不一样,这个更难一点,而且不太容易懂,所以后续还会持续更新这篇文章。

三、Makefile文件的语法


3.1 注释


井号(#)在Makefile中表示注释。


# 这是注释
result.txt: source.txt
    # 这是注释
    cp source.txt result.txt # 这也是注释


3.2 回声(echoing)


正常情况下,make会打印每条命令,然后再执行,这就叫做回声(echoing)。


test:
    # 这是测试


执行上面的规则,会得到下面的结果。


$ make test
# 这是测试


在命令的前面加上@,就可以关闭回声。


test:
    @# 这是测试


现在再执行make test,就不会有任何输出。

由于在构建过程中,需要了解当前在执行哪条命令,所以通常只在注释和纯显示的echo命令前面加上@。


test:
    @# 这是测试
    @echo TODO


3.3 通配符


通配符(wildcard)用来指定一组符合条件的文件名。Makefile 的通配符与 Bash 一致,主要有星号(*)、问号(?)和 […] 等。


比如:

*.o :表示所有后缀名为o的文件。

?:表示任意单个字符。

例如,abc?.txt匹配到的文件名是abca.txt、abcb.txt等。

[...]:表示括号中任意一个字符,可以使用短划线(-)表示范围。

例如,abc[de].txt匹配到的文件名可以是abc.txt或者abcde.txt,但不包括abcce.txt。

abc[a-d].txt则匹配到abc.txt、abcb.txt、abcc.txt和abcd.txt。


clean:
        rm -f *.o


3.4 模式匹配


Make命令允许对文件名,进行类似正则运算的匹配,主要用到的匹配符是%。比如,假定当前目录下有 f1.cf2.c两个源码文件,需要将它们编译为对应的对象文件。


%.o: %.c


这是一个Makefile文件中的一个典型规则,它表示:

对于所有依赖目标名以“.o”结尾的目标,在当前目录或者其它指定搜索路径下,查找与之同名但后缀名为“.c”的源文件,并执行相应的命令,生成这个目标。

换句话说,这个规则用于描述如何将一个C语言源文件编译成一个目标文件。在这个规则中,“%.o”和“%.c”是通配符形式,它们将会自动匹配当前目录下相同前缀名称的.o和.c文件。例如,foo.o 依赖于 foo.c,那么这条规则就会被用来编译foo.c生成foo.o。这个规则可以被多个目标文件依赖并使用到,方便批量编译源代码。

等同于下面的写法。


f1.o: f1.c
f2.o: f2.c


使用匹配符%,可以将大量同类型的文件,只用一条规则就完成构建。

在Makefile中,%是一个通配符,表示任意匹配0个或多个字符的模式。它用于将规则(依赖关系)应用于一组相关的目标文件(例如,所有以“.o”结尾的目标文件),而不需要一个一个地列举它们。

具体来说,%通配符常常和特定的后缀名一起使用,如:

%.o: %.c


表示所有以.c结尾的源文件都可以被编译成对应的.o目标文件。在实际使用时,%会被自动替换成被匹配的目标名称的通配符部分,从而实现批量处理和编译。

这种技巧可以使 Makefile 更灵活和可维护,减少重复工作,提高代码重用率。


3.5 变量和赋值符


Makefile 允许使用等号自定义变量。

txt = Hello World
test:
    @echo $(txt)

上面代码中,变量 txt 等于 Hello World。调用时,变量需要放在 $( ) 之中。

当需要在 Makefile 中引用变量的值时,通常使用美元符号和括号或者花括号。

1.美元符号和括号:适用于大部分情况。可以用于引用任何已定义的变量,以及未定义的变量(在这种情况下将返回空值)。

  test:
    @echo "Hello, $(NAME)!"

在这个规则中,$(NAME) 可以引用已经定义的变量 NAME 的值,也可以引用未定义的变量(在这种情况下将会输出空字符串)。

2.花括号:主要用于区分变量名和其他字符。花括号内的变量名称必须已经定义,否则会引发语法错误。

test:
  @echo "My name is ${FIRST_NAME}${LAST_NAME}"

在这个规则中,${FIRST_NAME} ${LAST_NAME} 都必须是已经定义的变量名称。花括号可以使这两个变量的名称与紧随其后的}符号区分开来,从而避免了歧义和错误。

总之,美元符号和括号或者花括号都是用于引用 Makefile 中的变量,但在具体使用时,可能需要根据场景的不同做出选择。

如果需要区分变量和其他字符,建议使用花括号;如果不需要区分,则使用美元符号和括号即可。



调用Shell变量,需要在美元符号前,再加一个美元符号,这是因为Make命令会对美元符号转义,等于$是一个普通字符。


test:
    @echo $$HOME


有时,变量的值可能指向另一个变量。


v1 = $(v2)


上面代码中,变量 v1 的值是另一个变量 v2。这时会产生一个问题,v1 的值到底在定义时扩展(静态扩展),还是在运行时扩展(动态扩展)?如果 v2 的值是动态的,这两种扩展方式的结果可能会差异很大。


为了解决类似问题,Makefile一共提供了四个赋值运算符 (=、:=、?=、+=),它们的区别请看StackOverflow。


VARIABLE = value
# 在执行时扩展,允许递归扩展。
VARIABLE := value
# 在定义时扩展。
VARIABLE ?= value
# 只有在该变量为空时才设置值。
VARIABLE += value
# 将值追加到变量的尾端。


3.6 内置变量(Implicit Variables)


Make命令提供一系列内置变量,比如,$(CC) 指向当前使用的编译器,$(MAKE) 指向当前使用的Make工具。这主要是为了跨平台的兼容性,详细的内置变量清单见手册。


output:
    $(CC) -o output input.c


3.7 自动变量(Automatic Variables)


Make命令还提供一些自动变量,它们的值与当前规则有关。主要有以下几个。


1)$@

$@:指当前目标,就是Make命令当前构建的那个目标。

比如,make foo的 $@ 就指代foo。


a.txt b.txt: 
    touch $@


等同于下面的写法。


a.txt:
    touch a.txt
b.txt:
    touch b.txt


2)$<

$<:指代第一个前置条件。比如,规则为 t: p1 p2,那么$< 就指代p1。


a.txt: b.txt c.txt
    cp $< $@ 


等同于下面的写法。


a.txt: b.txt c.txt
    cp b.txt a.txt 


3)$?

$? 指代比目标更新的所有前置条件,之间以空格分隔。比如,规则为 t: p1 p2,其中 p2 的时间戳比 t 新,$?就指代p2。


4)$^

$^ 指代所有前置条件,之间以空格分隔。比如,规则为 t: p1 p2,那么 $^ 就指代 p1 p2 。


5)$*

$* 指代匹配符 % 匹配的部分, 比如% 匹配 f1.txt 中的f1 ,$* 就表示 f1。


6)$(@D) 和 $(@F)

$(@D)$(@F) 分别指向 $@ 的目录名和文件名。比如,$@是 src/input.c,那么$(@D) 的值为 src ,$(@F) 的值为 input.c。

7)$(<D) 和 $(<F)

$(<D) $(<F) 分别指向$< 的目录名和文件名。

所有的自动变量清单,请看手册。下面是自动变量的一个例子。


dest/%.txt: src/%.txt
    @[ -d dest ] || mkdir dest
    cp $< $@

dest/%.txt表示目标文件的路径格式,它由两部分组成:


dest/表示目标文件所在的目录,%.txt表示匹配任意.txt文件。因此,对于src/foo.txt文件,它的目标文件就是dest/foo.txt。

src/%.txt表示依赖文件的路径格式,它匹配与目标文件同名的.txt文件,并且它们都在src/目录下。

规则的主体是命令行,它以一个Tab键开头。

-d dest表示要测试的路径,其中-d选项用于测试一个路径是否是一个目录。如果dest/目录存在,则这个命令返回0;否则,返回非0值。

||一个逻辑或运算符,当左侧命令返回非0值时,就会执行右侧的命令。

mkdir dest表示创建一个dest/目录。

● 然后,它使用cp命令将依赖文件$<(第一个前置条件)复制到$@(目标文件)中。$< 指代前置文件(src/%.txt),$@指代目标文件(dest/%.txt)。


举例二

$(<D)表示路径部分,表示$<所在的目录名。比如,如果$<src/foo.txt,那么$(<D)就表src/

$(<F)表示文件名部分,表示$<中最后一个斜杠后面的字符串,也就是文件名部分。比如,如果$<src/foo.txt,那么$(<F)就表示foo.txt

这些变量通常用于构建过程中的命令行中,以方便地引用目标文件和依赖项的路径和名称。例如,下面是一个示例规则:


obj/%.o: src/%.c
    $(CC) -c $< -o $@ -I$(<D)

这是一个Makefile规则,用于将src/目录下的.c文件编译成.o目标文件,并将它们放到obj/目录中。具体来说,它的语法如下:


obj/%.o表示目标文件的路径格式,它由两部分组成:obj/表示目标文件所在的目录,%.o表示匹配任意以.o结尾的文件。因此,对于src/foo.c文件,它的目标文件就是obj/foo.o

src/%.c表示依赖文件的路径格式,它匹配与目标文件同名的.c文件,并且它们都在src/目录下。

规则的主体是命令行,它以一个Tab键开头。在这个例子中,它首先使用$(CC)变量指定了gcc编译器,并使用-c选项告诉编译器只编译不链接。

然后,它使用$<变量引用第一个依赖文件(也就是源文件)的路径和名称。

-o表示指定输出文件名字(不然就是默认a.out) 。

$@表示目标文件的路径和名称。

-I$(<D)选项表示包含源代码所在的目录。

-I是一个编译器选项,用于指定头文件搜索路径。


具体来说,当我们编写一个C或C++程序时,通常需要包含一些标准库或自定义库的头文件。这些头文件通常位于某个特定的目录下,例如/usr/include/usr/local/include等。为了让编译器正确地查找这些头文件,我们需要使用-I选项将它们添加到搜索路径中。

例如,假设我们有一个foo.c文件,它需要包含位于include/目录下的头文件bar.h,那么可以使用如下的编译命令:

gcc -c foo.c -o foo.o -Iinclude

在这个示例中,-Iinclude选项会将include/目录添加到头文件搜索路径中,使得编译器能够正确地查找并包含bar.h

所以!!!!

当我们运行make命令时,它会检查所有的规则,并尝试构建那些需要更新的目标文件。在这个示例中,如果src/foo.c文件被修改,那么它的目标文件obj/foo.o就会被重新生成。

反之,如果目标文件已经是最新的,那么这个规则就不会被执行。通过使用Makefile,我们可以自动化地管理这些依赖关系,并且只需要更新必要的文件,从而提高效率并减少错误。


3.8 特殊变量


1).PHONY变量


有一个在 Makefile 文件中常见的特殊变量,那就是 .PHONY

.PHONY 是一个特殊的目标(target),用于声明一组“伪目标”,这些伪目标不代表真正的文件或者动作,而是代表需要运行的操作或任务名称。

例如:

.PHONY: all clean
all:
    gcc -o main main.c
clean:
    rm -f main

在这个例子中,我们使用 .PHONY 关键字来定义 all 和 clean 两个目标。在执行 make 命令时,可以通过指定这些目标来运行对应的操作,例如 make all 或 make clean。

需要注意的是,定义了伪目标后,并不会检查对应的文件是否存在或是否需要更新。相反,Makefile 将直接执行对应的命令,而不管是否已经有对应的文件存在。因此,定义伪目标需要通过其他方式确保其正确执行,例如在规则中加入必要的依赖项或者命令验证。

总之,.PHONY 关键字是用于定义伪目标的常用技巧,可以让 Makefile 文件更加清晰和易于维护。



2)VPATH变量

VPATH 是 Makefile 中的一个变量,用于指定寻找文件的路径。它是为了通过简单地设置查找路径来实现模块化设计。

当 Makefile 通过 include 包含其他 Makefile 或者要求在不同目录中搜索依赖项时,就需要使用 VPATH 变量来定义一个“搜索路径列表”。

如果一个依赖文件位于搜索路径内,则Make 将跳过当前目录,并进入下一搜索路径。如果依赖文件在所有搜索路径中都找不到,则会产生一条错误信息。

下面是一个示例:

VPATH = src:../headers
a.out: main.o sum.o
  gcc -o a.out main.o sum.o
main.o: main.c 
  gcc -c main.c
sum.o: sum.c
  gcc -c sum.c

这里,VPATH 定义了两个搜索路径,src ../headers

并使用冒号(: )来分隔搜索路径,例如 VPATH = dir1:dir2:dir3。冒号后面紧跟一个空格是为了避免与 Windows 路径中的驱动器符号(如 C:)发生歧义。

但在一些 Make 实现中,例如 GNU Make,也可以不加空格,像这样: VPATH=src:…/headers。

所以,如果你没在文件名前面指定它应该在哪个目录中被找到的话,当 Make 需要查找 main.csum.c 文件时,它将首先在 src 目录中查找,如果没有找到,在 ../headers 目录中查找。如果依然没有找到,将出现错误。

注意!!

你可以在每个文件名前面指定它应该在哪个目录中被找到。

怎么指定?

那么我来解释一下。

VPATH 中定义搜索路径时,Makefile 会从指定的目录中查找依赖文件。如果在 Makefile 中使用相对路径来引用文件,就需要在文件名前面指定它应该在哪个路径中被找到。

在依赖项列表(例如下面的目标:main.o sum.o)中,需要在文件名前面添加路径,以指示它们位于 VPATH 定义的哪个路径下:

main.o: src/main.c 
  gcc -c src/main.c
sum.o: ../headers/sum.c
  gcc -c ../headers/sum.c

在这个示例 Makefile 中,src/main.c 是相对路径,指相对于当前 Makefile 所在的目录中的 src 目录。同样地,…/headers/sum.c 也是相对路径,指相对于当前 Makefile 所在的目录中的 …/headers 目录。

相对路径根据指定的文件或目录与当前工作目录之间的相对位置来计算路径,而不考虑(或“忽略”)任何特定的根目录或绝对路径信息。因此,如果当前 Makefile 所在的目录为 /path/to/Makefile,则src/main.c../headers/sum.c 最终解析为路径为/path/to/src/main.c/path/headers/sum.c(“…/” 表示返回上级目录)。

● 两个 “…” 表示返回上一级目录。

在这段代码中,`sum.o 是一个目标文件,用于存储编译后的 sum.c 文件生成的二进制代码。而 -c 选项则表示只编译但不链接,也就是只生成目标文件而不生成可执行文件。

在 gcc 命令行中指定 “…/headers/sum.c” 作为源文件路径时,“…/” 表示返回到上一级目录,然后再进入 “headers/” 目录,最终找到名为 sum.c 的源文件。

因此,这段代码通过 “…/” 表示返回上一级目录,来定位 sum.c 文件所在的 headers 目录,并对其进行编译操作。该目录结构通常用于将源代码和头文件分开存放,便于代码管理和维护。



3)include变量


首先给出几个定义:

由Makefile文件中的所有规则组成的集合称为U1;

由file.dep文件中的所有规则组成的集合称为U2;

由集合U1和集合U2的并集称为集合U。

第一步:Makefile中的include命令先将文件file.dep包含进当前Makefile文件(第一次包含),这样Makefile文件中就有了file.dep文件的内容。

第二步:然后在集合U(特别注意,这里是集合U)中检查是否有以file.dep为目标的规则。如果U中没有以file.dep为目标的规则,或者虽然有以file.dep为目标的规则,但根据依赖关系(即便在规则中的命令执行后也)不能使file.dep文件发生更新,则Makefile文件最终包含的就是file.dep文件的当前内容,include命令执行结束。

第三步:如果集合U中有以file.dep为目标的规则,并且该规则使得file.dep文件发生了更新,则include命令会将更新后的file.dep文件再次包含进当前Makefile文件(再次包含),跳转到第一步往下继续执行。


3.9 判断和循环


这些是Makefile中的条件语句:

ifdef 检查变量名是否被定义过(即确定它是否存在)。如果是,则执行指定的命令。使用 ifndef 可以检查变量不存在的情况。

ifdef VAR
    <commands>
endif

ifeq ifneq 检查两个字符串是否相等或者不相等,如果相等则运行指定的命令。可以检查变量的值。

ifeq (string1, string2)
    <commands>
endif
ifneq (string1, string2)
    <commands>
endif

其中字符串可以是变量或者常量。

注意,在 Makefile 中使用上述条件语句时,应该使用 Tab 而不是四个空格来缩进任何指定的命令。

举个栗子!!

Makefile使用 Bash 语法,完成判断和循环。

ifeq ($(CC),gcc)
  libs=$(libs_for_gcc)
else
  libs=$(normal_libs)
endif

上面代码判断当前编译器是否 gcc ,然后指定不同的库文件。

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

上面代码的运行结果。

one

two

three



3.10 函数


Makefile 还可以使用函数,格式如下。


$(function arguments)
# 或者
${function arguments}


Makefile提供了许多内置函数,可供调用。下面是几个常用的内置函数。

(1)shell 函数


shell 函数用来执行 shell 命令


srcfiles := $(shell echo src/{00..99}.txt)


(2)wildcard 函数


wildcard 函数用来在 Makefile 中,替换 Bash 的通配符。


srcfiles := $(wildcard src/*.txt)


(3)subst 函数


subst 函数用来文本替换,格式如下。


$(subst from,to,text)


下面的例子将字符串"feet on the street"替换成"fEEt on the strEEt"。


$(subst ee,EE,feet on the street)


下面是一个稍微复杂的例子。


comma:= ,
empty:=
# space变量用两个空变量作为标识符,当中是一个空格
space:= $(empty) $(empty)
foo:= a b c
bar:= $(subst $(space),$(comma),$(foo))
# bar is now `a,b,c'.


(4)patsubst函数


patsubst 函数用于模式匹配的替换,格式如下。


$(patsubst pattern,replacement,text)


下面的例子将文件名"x.c.c bar.c",替换成"x.c.o bar.o"。


$(patsubst %.c,%.o,x.c.c bar.c)


(5)替换后缀名


替换后缀名函数的写法是:变量名 + 冒号 + 后缀名替换规则。它实际上patsubst函数的一种简写形式。


min: $(OUTPUT:.js=.min.js)


上面代码的意思是,将变量OUTPUT中的后缀名 .js 全部替换成 .min.js


目录
相关文章
|
16天前
|
安全 Android开发 iOS开发
Android vs. iOS:构建生态差异与技术较量的深度剖析###
本文深入探讨了Android与iOS两大移动操作系统在构建生态系统上的差异,揭示了它们各自的技术优势及面临的挑战。通过对比分析两者的开放性、用户体验、安全性及市场策略,本文旨在揭示这些差异如何塑造了当今智能手机市场的竞争格局,为开发者和用户提供决策参考。 ###
|
10天前
|
Java API Android开发
安卓应用程序开发的新手指南:从零开始构建你的第一个应用
【10月更文挑战第20天】在这个数字技术不断进步的时代,掌握移动应用开发技能无疑打开了一扇通往创新世界的大门。对于初学者来说,了解并学习如何从无到有构建一个安卓应用是至关重要的第一步。本文将为你提供一份详尽的入门指南,帮助你理解安卓开发的基础知识,并通过实际示例引导你完成第一个简单的应用项目。无论你是编程新手还是希望扩展你的技能集,这份指南都将是你宝贵的资源。
38 5
|
10天前
|
前端开发 JavaScript 测试技术
Android适合构建中大型项目的架构模式全面对比
Android适合构建中大型项目的架构模式全面对比
24 2
|
15天前
|
开发工具 Android开发 iOS开发
Android vs iOS:构建移动应用时的关键考量####
本文深入探讨了Android与iOS两大移动平台在开发环境、性能优化、用户体验设计及市场策略方面的差异性,旨在为开发者提供决策依据。通过对比分析,揭示两个平台各自的优势与挑战,帮助开发者根据项目需求做出更明智的选择。 ####
|
16天前
|
人工智能 Android开发
1024 云上见 构建AI总结助手,实现智能文档摘要 领罗马仕安卓充电器
1024 云上见 构建AI总结助手,实现智能文档摘要 领罗马仕安卓充电器
44 1
|
19天前
|
调度 Android开发 开发者
构建高效Android应用:探究Kotlin多线程优化策略
【10月更文挑战第11天】本文探讨了如何在Kotlin中实现高效的多线程方案,特别是在Android应用开发中。通过介绍Kotlin协程的基础知识、异步数据加载的实际案例,以及合理使用不同调度器的方法,帮助开发者提升应用性能和用户体验。
36 4
|
19天前
|
编解码 Android开发 UED
构建高效Android应用:从内存优化到用户体验
【10月更文挑战第11天】本文探讨了如何通过内存优化和用户体验改进来构建高效的Android应用。介绍了使用弱引用来减少内存占用、懒加载资源以降低启动时内存消耗、利用Kotlin协程进行异步处理以保持UI流畅,以及采用响应式设计适配不同屏幕尺寸等具体技术手段。
42 2
|
3天前
|
编解码 Java Android开发
通义灵码:在安卓开发中提升工作效率的真实应用案例
本文介绍了通义灵码在安卓开发中的应用。作为一名97年的聋人开发者,我在2024年Google Gemma竞赛中获得了冠军,拿下了很多项目竞赛奖励,通义灵码成为我的得力助手。文章详细展示了如何安装通义灵码插件,并通过多个实例说明其在适配国际语言、多种分辨率、业务逻辑开发和编程语言转换等方面的应用,显著提高了开发效率和准确性。
|
2天前
|
Android开发 开发者 UED
安卓开发中自定义View的实现与性能优化
【10月更文挑战第28天】在安卓开发领域,自定义View是提升应用界面独特性和用户体验的重要手段。本文将深入探讨如何高效地创建和管理自定义View,以及如何通过代码和性能调优来确保流畅的交互体验。我们将一起学习自定义View的生命周期、绘图基础和事件处理,进而探索内存和布局优化技巧,最终实现既美观又高效的安卓界面。
12 5
|
1天前
|
缓存 数据库 Android开发
安卓开发中的性能优化技巧
【10月更文挑战第29天】在移动应用的海洋中,性能是船只能否破浪前行的关键。本文将深入探讨安卓开发中的性能优化策略,从代码层面到系统层面,揭示如何让应用运行得更快、更流畅。我们将以实际案例和最佳实践为灯塔,引领开发者避开性能瓶颈的暗礁。
9 3