Makefile

makefile是一个包含一组指令的文件,make build自动化工具使用这些指令來生成目标。

优点:自动化编译,可以极大的提高了软件开发的效率。

规则

1
2
3
4
target ... : prerequisites ...
command
...
...

target

可以是一个object file(目标文件),也可以是一个执行文件,还可以是一个标签(label)。

prerequisites

生成该target所依赖的文件和/或target。反斜杠( \ )是换行符的意思。

command

该target要执行的命令(任意的shell命令)

1
2
3
4
hello:hello.c
gcc -c hello.c
gcc -o hello hello.o
rm -f hello.o

工作流程浅析

  1. make会在当前目录下找Makefile / makefile
  2. 找到后,会寻找第一个目标文件(target) hello,并作为最终的目标文件。
  3. 如果 hello不存在活或其依赖 hello.c的修改时间比 hello新,那么就会执行command来重新生成 target

PS:一定要以一个 Tab 键作为开头

make 会一层一层的寻找文件依赖关系,直至编译出第一个目标文件。如果在寻找的过程中出现错误,那么 make 就会退出并报错。

make 所着重的是寻找依赖关系,command的命令的正确与否是不关心的。

makefile

书写规则

通配符

make支持三个通配符: *?~

*:匹配 0 或多个字符

?:匹配任意一个字符

~:Linux,~ = $HOME Mac,则需要自己设置 HOME目录

文件搜索

1
VPATH = src:../headers

设置搜索路径,make执行时,如果当前目录找不到,则到指定路径取搜寻。

另一种则是:vpath关键字,注意全小写

1
2
3
4
5
6
7
8
vpath <pattern> <directories> #为符合模式<pattern>的文件指定搜索目录<directories>。
vpath <pattern> #清除符合模式<pattern>的文件的搜索目录。
vpath #清除所有已被设置好了的文件搜索目录。
eg1:
vpath %.h ../headers #现在当前目录搜索,若没找到则在“../headers”目录下搜索所有以 .h 结尾的文件。
eg2:
vpath %.c foo:bar #make按照vpath语句的先后顺序来执行搜索
vpath % blish #其表示 .c 结尾的文件,先在“foo”目录,然后是“blish”,最后是“bar”目录。

伪目标

“伪目标”并不是一个文件,只是一个标签,由于“伪目标”不是文件,所以make无法生成它的依赖关系和决定它是否要执行。

PS:伪目标不能和文件重名。

1
.PHONY : clean  #使用“.PHONY”来显式地指明一个目标是“伪目标”

例如清空规则:

1
2
3
.PHONY : clean
clean :
rm *.o temp

书写命令

显示命令

1
@echo 正在编译XXX模块......

@是必须的,如果make执行时,带入make参数 -n--just-print ,那么其只是显示命令,但不会执行命令。

命令执行

1
2
3
4
5
exec:
cd /home/focu5
pwd #打印当前目录
exec:
cd /home/focu5;pwd #打印/home/focu5下的目录

命令出错

有些命令执行结果的正确与否我们是不关心的,那么有两种办法可以解决:

  1. 在命令前加 -
  2. 执行 make的时候,加上 -i / --ignore-errors参数(这种做法会忽略掉所有命令的错误

另外:参数的是 -k 或是 --keep-going ,指如果某规则中的命令出错了,那么就终止该规则的执行,但继续执行其它规则。

定义命令包

定义这种命令序列的语法以 define 开始,以 endef 结束,骨架如下:

1
2
3
4
5
6
7
define 包名
...
command
...
endef

# 通过 $(包名) 来调用包

包名不能和变量名重名。

使用变量

变量命名

1
2
3
4
5
6
7
8
9
10
11
12
13
objects = hello.c
tag = hello
hello: $(objects)
gcc -o hello $(objects)

.PHONA: clean
.PHONA: clean1

clean:
rm -f *.o *.out

clean1:
rm -f $(tag)

变量命名可以包含:数字、字符、下划线(可以是数字开头),但不能含有 :#= 或是空字符(空格、回车等)。变量名对大小写是敏感的

变量在声明的时候要进行初始化,使用时最好用 ()或{}将变量包裹起来,eg:$(obj) ${obj} 另外$$则表示使用$

变量会在使用它的时候精确展开。

变量高级用法

变量值替换

格式: $(var:a=b) 或是 ${var:a=b}a均替换成 b

变量嵌套

也就是用变量的值去做新的变量。个人觉得追加更简单一些。

1
2
3
4
5
6
x = y
y = z
a := $($(x)) #a = $(y), $(a) = z
a := $(x) $(y) # $(a) = y z

y += $(x) #$(y) = z y

隐含规则

示例:

1
2
3
4
5
6
7
8
#①
foo : foo.o bar.o
cc –o foo foo.o bar.o $(CFLAGS) $(LDFLAGS)
#②
foo.o : foo.c
cc –c foo.c $(CFLAGS)
bar.o : bar.c
cc –c bar.c $(CFLAGS)

makefile会有自动推导的过程,所以我们写成①的形式会更简洁一些。

部分隐含规则:

  • CC : C语言编译程序。默认命令是 cc

  • CXX : C++语言编译程序。默认命令是 g++

  • RM : 删除文件命令。默认命令是 rm –f

  • CFLAGS : C语言编译器参数。

  • CXXFLAGS : C++语言编译器参数。

  • LDFLAGS : 链接器参数。(如: ld

  • 使用的时候请用:$(CC) 、$(RM) 等

    更详细参考这里

示例:

把所有的 .c 文件都编译成 .o 文件.

1
2
%.o : %.c
$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@

其中, $@ 表示所有的目标的挨个值, $< 表示了所有依赖目标的挨个值。

自动变量

  • $@ : 表示规则中的目标文件集。在模式规则中,如果有多个目标,那么, $@ 就是匹配于目标中模式定义的集合。
  • $% : 仅当目标是函数库文件中,表示规则中的目标成员名。
  • $< : 依赖目标中的第一个目标名字。如果依赖目标是以模式(即 % )定义的,那么 $< 将是符合模式的一系列的文件集。注意,其是一个一个取出来的。
  • $? : 所有比目标新的依赖目标的集合。以空格分隔。
  • $^ : 所有的依赖目标的集合。以空格分隔。如果在依赖目标中有多个重复的,那么这个变量会去除重复的依赖目标,只保留一份。
  • $+ : 这个变量很像 $^ ,也是所有依赖目标的集合。只是它不去除重复的依赖目标。

Makefile中一些GNU约定俗成的伪目标

伪目标 含义
all 所有目标的目标,其功能一般是编译所有的目标
clean 删除所有被make创建的文件
install 安装已编译好的程序,其实就是把目标可执行文件拷贝到指定的目录中去
print 列出改变过的源文件
tar 把源程序打包备份. 也就是一个tar文件
dist 创建一个压缩文件, 一般是把tar文件压成Z文件. 或是gz文件
TAGS 更新所有的目标, 以备完整地重编译使用
check 或 test 一般用来测试makefile的流程

参考:

跟我一起写Makefile

https://cloud.tencent.com/developer/article/1343894

GN

Overview

GN是一种元构建系统,生成Ninja构建文件(Ninja build files),相较GYP而言,具有如下优点:

  1. 可读性更好,更容易编写和维护。
  2. 速度更快,谷歌官方给的数据是20倍的速度提升。
  3. 修改GN文件后,执行ninja构建时会自动更新Ninja构建文件。
  4. 更简单的模块依赖,提供了public_deps, data_deps等,在GYP中,只有一种目标依赖,导致依赖关系错综复杂,容易引入不必要的模块依赖。
  5. 提供了更好的工具查询模块依赖图谱。这在GYP构建系统中是一个噩梦,要查一个目标依赖哪些模块或者一个模块被哪些目标依赖几乎是不可能的。
  6. 更好的调试支持。在GN中,只需要一条print语句就可以解决。

demo

在 chromium 的 src 目录下创建一个新的 test文件夹:

1
2
air@ubuntu:~/Desktop/chromium/src/test$ ls
BUILD.gn demo.cc
1
2
3
4
5
6
7
8
//demo.c
#include <iostream>

using namespace std;

int main(){
cout<<"Hello, world.\n";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
//BUILD.gn
group("test") {
testonly = true
deps = [
":hello_world",
]
}

executable("hello_world") {
sources = [
"demo.cc",
]
}

然后在根目录的 BUILD.gn添加如下

1
2
3
4
5
6
7
8
9
group("gn_all") {
testonly = true

deps = [
...
"//url:url_unittests",
"//test", ——————————————> 添加目录
...
]

然后编译就行了

1
2
3
4
5
6
7
air@ubuntu:~/Desktop/chromium/src/test$ gn gen out/test
Done. Made 17759 targets from 2922 files in 5566ms
air@ubuntu:~/Desktop/chromium/src/test$ ninja -C out/test hello_world
ninja: Entering directory `out/test'
[60/60] LINK ./hello_world
air@ubuntu:~/Desktop/chromium/src/test$ ./out/test/hello_world
Hello, world.

构建流程

  1. 在当前目录中查找.gn文件并向上遍历目录树,直到找到.gn。将此目录设置为根目录,并解释此文件以查找生成配置文件的名称。
  2. 执行构建配置文件(这是默认工具链)。
  3. BUILD.gn文件加载到根目录中。
  4. 递归加载其他目录中的BUILD.gn以解析所有当前依赖项。如果在指定位置找不到生成文件,GN将在tools/GN/secondary内的相应位置查找
  5. 解析目标的依赖项后,将.ninja文件写出到磁盘。
  6. 当所有目标都解决后,写出root build.ninja文件。

配置文件

执行的第一个文件是构建配置文件。此文件的名称在标记存储库根目录的.gn文件中指定。在Chrome中是src/build/config/BUILDCONFIG.gn只有一个构建配置文件。

目标

目标是构建图中的一个节点。它通常代表将要生成的某种类型的可执行文件或库文件。目标取决于其他目标。内置的目标类型(请参阅gn help <targettype>以获取更多帮助)是:

  • action:运行一个脚本来生成一个文件。
  • action_foreach:为每个源文件运行一次脚本。
  • bundle_data:声明数据加入到Mac / iOS包。
  • create_bundle:创建一个Mac / iOS包。
  • executable:生成一个可执行文件。
  • group:引用一个或多个其他目标的虚拟依赖关系节点。
  • shared_library:.dll或.so。
  • loadable_module:.dll或.so只能在运行时加载。
  • source_set:一个轻量级的虚拟静态库(通常比真正的静态库更可取,因为它的构建速度会更快)。
  • static_library:.lib或.a文件(通常你会想要一个source_set)。

更多资料可以看官方文档。

参考

[1] https://chromium.googlesource.com/chromium/src/tools/gn/+/48062805e19b4697c5fbd926dc649c78b6aaa138/docs/language.md