OpenJDK8 编译构建基础设施详解(2) - Make流程解析 置顶!
摘要
您也可以在知乎阅读此文: 知乎专栏-跟我一起阅读OpenJDK源
在上一篇文章中, 较详细的分析了 OpenJDK 编译过程中的两步的前一步:./configure
, 这一篇主要来分析下make流程. make流程主要涉及的是OpenJDK 的makefile的结构, 以及make命令的工作流程.
注: 这些分析相对比较偏一些技术本身的框架. 对make目标以及相关的参数的使用与相关的说明. 还是建议多参考官方的编译说明.
- OpenJDK8编译构建说明:OpenJDK8U - OpenJDK Build README
- 最新OpenJDKReadme.md
- 官网Building - Doc
本篇文章的关注重点主要是梳理清楚 OpenJDK8 的makefile的组织结构以及整体的编译流程的执行过程. 并不会花太多的笔墨在产品层面的介绍如何编译出想要的目标产物, 以及不同的编译参数之间有怎样的差异. 相信如果你有了基本的分析能力的. 自己理清楚这些应该不是难事.
注: 您也可以在我的知乎专栏阅读此文: 知乎专栏 - OpenJDK8 编译构建基础设施详解 - Make流程解析
前置知识与必备技能
makefile是一项非常古老的技术.下面引用自wiki百科:
https://zh.wikipedia.org/wiki/Make目前虽有众多依赖关系检查工具,但是make是应用最广泛的一个。这要归功于它被包含在Unix系统中。^[1]^ 斯图亚特·费尔德曼(Stuart Feldman)在1977年在贝尔实验室(Bell Labs)里制作了这个软件。^[2]^ ^[3]^ ^[1]^ 2003年,斯图亚特·费尔德曼因发明了这样一个重要的工具而接受了美国计算机协会(ACM)颁发的软件系统奖。^[4]^
这样算起来, make的历史已经有45年的历史了. 估计比我们大多数的现役程序员年纪还大. 所以你没有听过.或者很排斥这门技术,也情有可缘. 巧的是世界上各地程序员对他的评价也是褒贬不一.
就像其他和make有着悠久历史的软件一样,make有着很多的拥护者和反对者。它的很多问题因现代大型的软件项目的出现而暴露出来。但是很多人争论说它在常见的情况下可以很好的工作,而且使用非常的简单,功能强大,表达清楚。无论如何,make仍然被用来编译很多完整的操作系统,而且现在替代品们在基本的操作上与它没有太大差别。
对于makefile的基本语法,在这里就不做回顾了. 如果你对makefile
的基本用法还不熟悉的话,可以参考如下一些资料进行了解:
以上都是非常经典的中文Makefile学习资料. 也是各位开源界前辈的辛苦付出和无私奉献才沉淀下来的资料. 在此表示由衷的感谢.
古怪的makefile语法
makefile 的语法特别古老与古怪. 稍有不甚,就有可能掉入未知的坑里. 因此掌握一些基本的调试技巧非常有助于排除makefile编写过程中的错误.
我们先看一个例子,下面这从而代码是摘抄于 OpenJDK的源码的根目录下的makefile
,链接地址:
https://hg.openjdk.java.net/jdk8u/jdk8u/file/a323800a7172/Makefile
行号: 39行 / 44行;
这个其实根本在于makefile的语法怎么处理这个:
注:
由于ifeq的第二个参数是由逗号分隔后往右trim的.导致有没有前导空格都会被trim干净.因此最后的效果的一样的.
那有没有空格是敏感的地方呢? 有,请看如下的一个解释图,这个是一般的函数调用的文本
从这里也可以看出, 一个是makefile的语法是真的很灵活.
OpenJDK中的makefile代码规范
以下内容引用自
OpenJDK
的官网的英文翻译:OpenJDK 代码规范
Makefile 中的空格和缩进比其他语言更成问题。尽管大多数时候空白(译者注: 空格或者tab) 并不重要,但有时候却很重要。这可能被认为是make
中的一个设计缺陷,但我们无能为力。特别是,行首TAB缩进表示make
规则的命令(原文为: recipes,一般记为食谱,配方.实际解释为make执行的动作.),在make
的字符串操作中,有时候空格很重要(有时候不重要)。
可能部分是由于这个原因,部分是由于工具的遗留性质,很少有编辑器/IDE/工具支持对 makefile 进行自动格式化。这使得保持空格使用和缩进成为一项“手工”任务。下面的规则适用于这个现实。为了简单起见,我们也对 shell 脚本和 autoconf 源代码使用了相同的规则集(对于那些适用的规则)。
下面是机器翻译: 我已经润色到足以让你读懂明白的程度.
空格和缩进规则
缩进,必要使用
- 缩进的基本级别是两个空格。使用它来进行“逻辑”缩进(
if块
,函数定义
等)。 - 如果一行必须断开,则使用四个空格进行缩进。
- Makefile 规则中的配方(Recipes)必须以 TAB (每个定义)开始。
- 如果单个制表符(解释为8个空格宽)不足以将配方(
Recipes
) 与周围的代码明确隔开(意味着至少4个空格差) ,则使用额外的制表符 tab。 recipe
中的非shell 命令
(例如注释
和类似ifdef
的指令)不能以 tab 开头,而是应该使用空格缩进到与周围shell命令
相同的级别(此处将 tabs 解释为8个空格)。- 在
recipes
中,额外的缩进应该在TAB之后使用空格来完成,就像在普通的 makefile 行中一样。 - 不完全的
recipes
片段 (用于内联到recipe
中的宏定义)应该像recipe
一样处理,且行首一样以tab开始。
空格,需要使用的场景
- 永远不允许尾随空格 ( Trailing whitespaces )
- 除了以上的3-7规则(针对于recipe),不要使用tab, 请使用空格
空格: 推荐使用的场景
- There should be no whitespace between the list of targets and the
:
at the start of a rule. - There should be an empty line before and after each rule.
- Avoid empty lines inside the recipe.
- Broken lines should end with a backslash, and a single space before the backslash ("
\
"). - A single space should separate a comma from the next argument in a function call.
- A single space should be used before and after assignment operators (e.g.
:=
,=
,+=
).
这些建议并不总是可行的,因为空格可能具有语义意义。如果make需要这些建议中的一个例外,理想情况下应该通过注释指出。特别是逗号周围的空格可能是敏感的,所以要小心。
编码风格建议 (翻译有可能导致理解不正确,直接给原文)
- Use
:=
(direct assignment) instead of=
(recursive macro definition), unless you really need the recursive definition. - In long lists, do not let the first and last element have different form. For instance, start the first element on a new line rather than after a
:=
, and end the list with an empty comment (#
) to be able to have a trailing backslash on the last line. - Avoid padding internally in a line with spaces to try to align some feature into columns with surrounding lines.
- For multiple commands run by the shell, separated by "
&& \
" or similar, all commands should start at the same indention level.
译者注:
- 尽量使用
:=
此类立即扩展变量,除非你真的需要递归扩展变量.- 对于很长的内容定义. 第一个元素风格以及最后一个元素风格要与其它保持一致. 实现方式是新起一行在赋值符号后面.同时在结尾处,用一个空的注释符号#结束.
- 尽量避免使用空格在行内进行补齐对齐操作, 因为是空格敏感的.你尽量不要这么做.
- 对于shell运行的多行命令,使用:"&& |" 或相似的东西进行分隔. 所有的命令应该使用相同的缩进
规则原因说明:
规则1-2:
这与 Java 不同,Java 有两倍长的缩进(4个空格逻辑缩进,8个空格换行)。但是,由于所有的缩进基本上都需要通过重复按空格来完成,而且换行是相当常见的,因此我们认为缩进级别应该尽量短。否则,一个折行(Wraped line)两次将需要16按空格键。
规则9-12
在 recipes
中需要tab
,但是为了保持理智,这是tab
的唯一可接受用途。makefile
语法使得在复杂的 makefile
中发现规则(rules
)变得不容易,需要一切有助于区分规则和非规则的东西。
规则16
递归宏定义( =
而不是 :=
) 会减慢make
的速度,并可能产生意想不到的效果。通常这不是你的意思。
规则17-18
Makefile 更改内容的一个非常典型的用例(场景)是向列表中添加内容(文件、编译器指令等)。这些规则有助于使这些改变变得容易和无上下文。否则,开发人员必须修改与实际更改无关的几行代码。很有可能一个填充得很好的网格(grid)不会被更新,并且从第一次更改开始就开始恶化。
规则19
分隔多个 shell 命令(例如,在 shell“ if”命令之后)不应被视为需要缩进的折行,而应被视为在相同缩进级别上指定命令列表的一种方法。这就是所有的意图; &&
仅仅是一个迂回的设备,用来使它在 makefile 中正确工作。
调试 makefile
可以使用的调试手段非常有限. makefile的书写与make的工作流程都特别简陋. 以至于我们可以调试和观察它的方法特别少. 不过, 我们还是有些比较实用的技巧用于makefile的编写与调试工作.
打印日志与错误退出.
具体的语法细节可以参考官方文档: Make-Control-Functions
INFO级别日志:
$(info log)
看如下命令, 下面的代码是makefile的所有内容. 只有一个默认的目标: hello
hello:
ec$(info hello world)ho "this is msg from echo command"
我们运行它:make
, 输出内容如下:
hello world
echo "this is msg from echo command"
this is msg from echo command
命令输出解释:
第1行: 这个是info函数的日志输出. info函数解析规则的reciple
的时候执行的. 这个规则要被执行才会被解析其command. 否则不会执行.
第2行: 规则中的命令回显. 可以使用 @开头的标志关闭它
第3行: echo命令的输出内容.
对于2行的执行为什么不影响第三行这个echo命令 ,原因就在于 其返回值为空
对于info函数,只会打印内容.并不会输出行号. 如果你想输出行号,可以使用warning函数
WARN级别日志:
$(warn log)
同样的, warn函数与info函数一样,你可以将它放置到make文件内容的任意地方.唯一不同的地方就是这个函数的日志打印出来是带有行号的. 这里直接引用一个比较复杂经典的例子:
下面的例子来自于陈皓的: 跟我一起写makefile
$(warning A top-level warning)
FOO := $(warning Right-hand side of a simple variable)bar
BAZ = $(warning Right-hand side of a recursive variable)boo
$(warning A target)target: $(warning In a prerequisite list)makefile
$(BAZ)
$(warning In a command script)
ls
$(BAZ):
为了避免文中造成的格式问题,我附上一张截图:
下面是我自己运行的结果, 与原文有出入. 具体我会解释
makefile:1: A top-level warning
makefile:2: Right-hand side of a simple variable
makefile:5: A target
makefile:5: In a prerequisite list
makefile:9: Right-hand side of a recursive variable
makefile:6: Right-hand side of a recursive variable
makefile:6: In a command script
boo
make: boo: Command not found
make: *** [target] Error 127
运行结果释义:
- 解析阶段解析第一行.直接打印.
- 立即扩展变量赋值, 操作. 直接打印.
- 第5行: 解析阶段 , 解析目标. 打印
- 第5行: 解析阶段, 解析依赖 ,打印
- 9: 解析变量引用:
$(BAZ)
: 解析出来的结果作为目标名称. 这里是boo. 同时打印日志- 解析完成. 开始执行默认目标. target
- 解析命令: $(BAR) , 此时打印出6行. (第二个6行)
- 解析命令行中的变量引用. 输出:
In a command script
, 这个行号是6,不是7有些诡异.- 最后运行命令.
- boo , 首先是命令回显.
- 然后
boo
是一个未定义的命令.因此出错了.直接退出 了.- 然后打印了失败的shell输出.
- 最后打印target失败.
为了让target不失败我改了一下,把 boo
修改为: pwd
$(warning A top-level warning)
FOO := $(warning Right-hand side of a simple variable)bar
BAZ = $(warning Right-hand side of a recursive variable)pwd
$(warning A target)target: $(warning In a prerequisite list)makefile
$(BAZ)
$(warning In a command script)
ls
$(BAZ):
输出结果为:
makefile:1: A top-level warning
makefile:2: Right-hand side of a simple variable
makefile:5: A target
makefile:5: In a prerequisite list
makefile:9: Right-hand side of a recursive variable
makefile:6: Right-hand side of a recursive variable
makefile:6: In a command script
pwd
/root/ccode/warn
ls
makefile
有一些细节可能无从考证, 就像上面的 脚本命令行那一行的warning一样,这个按理应该是第七行. 这个显示是第6行.有知道的大佬可以在留言区告知.
--just-print
在一个新的makefile 工作目标上,我所做的第一个测试就是以--just-print(-n)选项来调用make。这会使得make读进makefile并且输出它更新工作目标时将会执行的命令,但是不会真的执行它们。
注: 也可以使用短命令模式.
make -n
我们看下openJDK的执行会输出什么:
的确会打印出执行的每一个命名.对我们分析makefile的执行的确有一定的帮助.但是由于上面提到的openjdk的makeifle的结构相对比较复杂.有较多的makefile. 就像你知道了这个命令也无法肯定其输出的位置是什么. anyway,这是一个比较有用的命令.
--debug
当你需要知道make 如何分析你的目标依赖图时,可以使用
--debug
选项。除了运行调试器,这个选项是让你获得最详细信息的另一个方法. 可以看下我用此命令在上面的测试makefile的运行结果. 它会打印出比较详细的依赖关系分析过程.
[root@iZ25a8x4jw7Z ~/ccode/warn]#make --debug
GNU Make 3.81
Copyright (C) 2006 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
This program built for x86_64-redhat-linux-gnu
Reading makefiles...
makefile:1: A top-level warning
makefile:2: Right-hand side of a simple variable
makefile:5: A target
makefile:5: In a prerequisite list
makefile:9: Right-hand side of a recursive variable
Updating goal targets....
File `target' does not exist.
Must remake target `target'.
makefile:6: Right-hand side of a recursive variable
makefile:6: In a command script
pwd
/root/ccode/warn
ls
makefile vars.mk
Successfully remade target file `target'.
OpenJDK构建系统makefile结构.
对OpenJDK的makefile的数量进行统计.
*.gmk
的数量是70+ ;
对于根Makefile的文件名是:Makefile
, 一般的子makefile的后缀是: "gmk"
根makefile
OpenJDK 的根makefile有两个.一个是位于项目根目录的makefile, 另外一个是位于build目录中的:build/confName/Makefile
我们分别看一下这两个文件的内容:
源码目录makefile
文件位于: Makefile
文件链接: https://hg.openjdk.java.net/jdk8u/jdk8u/file/a323800a7172/Makefile
#
# 一些不依赖于make入口的全局的辅助性函数. ,这里从分析主流程的原因看.
# 这里包含这个文件,主要有两个目的.
# 1. 下面的call调用: ParseConfAndSpec ,主要是解析我查找 SPEC变量的值.
# 2. 为后面的各种脚本调用提供全局的一些辅助函数. 比如异常退出.
# ... and then we can include our helper functions
include $(root_dir)/make/MakeHelpers.gmk
$(eval $(call ParseLogLevel))
$(eval $(call ParseConfAndSpec)) # 主流程会解析出SPEC变量的值.也就是SPEC文件的路径
# Now determine if we have zero, one or several configurations to build.
ifeq ($(SPEC),)
# Since we got past ParseConfAndSpec, we must be building a global target. Do nothing.
else
ifeq ($(words $(SPEC)),1)
# 主流程会走这里 , 因为一般只会加载一个spec说明文件.
# /path/to/jdk8u/build/macosx-x86_64-normal-server-slowdebug/spec.gmk
$(warning we are building a single configuration. ready to Include Main.gmk)
# We are building a single configuration. This is the normal case. Execute the Main.gmk file.
include $(root_dir)/make/Main.gmk
else
# We are building multiple configurations.
# First, find out the valid targets
# Run the makefile with an arbitrary SPEC using -p -q (quiet dry-run and dump rules) to find
# available PHONY targets. Use this list as valid targets to pass on to the repeated calls.
all_phony_targets=$(filter-out $(global_targets), $(strip $(shell \
cd $(root_dir) && $(MAKE) -p -q FRC SPEC=$(firstword $(SPEC)) | \
grep ^.PHONY: | head -n 1 | cut -d " " -f 2-)))
$(all_phony_targets):
@$(foreach spec,$(SPEC),(cd $(root_dir) && $(MAKE) SPEC=$(spec) \
$(VERBOSE) VERBOSE=$(VERBOSE) LOG_LEVEL=$(LOG_LEVEL) $@) &&) true
.PHONY: $(all_phony_targets)
endif
endif
我们看一下, 上面这段代码主要是引入helper函数. 然后调用:ParseConfAndSpec
$(eval $(call ParseConfAndSpec))
ParseConfAndSpec
的核心目标是找出:spec.gmk文件. 此文件是./configure
命令从:common/autoconf/spec.gmk.in
模板填充变量后得到的. 位于目录:build/macosx-x86_64-normal-server-slowdebug/spec.gmk
,里面存储了各种各位的变量与配置. 以供下面所有的makefile编译时使用.
在
MakeHelpers.gmk
文件中定义了如下变量: ParseConfAndSpec
源文件226行: https://hg.openjdk.java.net/jdk8u/jdk8u/file/a323800a7172/make/MakeHelpers.gmk
define ParseConfAndSpec
# global_targets = help
# GetRealTarget = 传入的编译目标,默认是default (没有给参数时,会默认初始化为 default)
# 这个if的意思是: 如果在真实目标中存在除 help 这样的默认目标外还有其它目标.则走这里 (一般都满足.会传入真实的编译标)
ifneq ($$(filter-out $(global_targets),$$(call GetRealTarget)),)
# 对上面的 ifneq 的解释. 如果没有其它目标,就不解析了.
# If we only have global targets, no need to bother with SPEC or CONF
# 判断SPEC 变量是否定义
ifneq ($$(origin SPEC),undefined)
# SPEC 有定义. 检查是否设置正常
# We have been given a SPEC, check that it works out properly
ifeq ($$(wildcard $$(SPEC)),)
$$(info Cannot locate spec.gmk, given by SPEC=$$(SPEC))
$$(eval $$(call FatalError))
endif
ifneq ($$(origin CONF),undefined)
# We also have a CONF argument. This is OK only if this is a repeated call by ourselves,
# but complain if this is the top-level make call.
ifeq ($$(MAKELEVEL),0)
$$(info Cannot use CONF=$$(CONF) and SPEC=$$(SPEC) at the same time. Choose one.)
$$(eval $$(call FatalError))
endif
endif
# ... OK, we're satisfied, we'll use this SPEC later on
else
# 主流程是走这里. 一般我们不会指定SPEC参数.而使用build目录下的默认的spec文件
# Find all spec.gmk files in the build output directory
output_dir=$$(root_dir)/build
all_spec_files=$$(wildcard $$(output_dir)/*/spec.gmk)
# 检查我们需要的build目录是否有需要的spec.gmk文件. 如果没有: 报错退出 .同时提醒用户重新运行 ./configure 命令
ifeq ($$(all_spec_files),)
$$(info No configurations found for $$(root_dir)! Please run configure to create a configuration.)
$$(eval $$(call FatalError))
endif
# Extract the configuration names from the path
# 解析出所有的配置目录, 一般我们只有一个. 比如这里的: macosx-x86_64-normal-server-slowdebug
all_confs=$$(patsubst %/spec.gmk,%,$$(patsubst $$(output_dir)/%,%,$$(all_spec_files)))
# 这里是在编译的时候指定了CONF名称的场景, 一般我们不指定. 如果我们有多个编译目标的时候可能会指定此变量.
ifneq ($$(origin CONF),undefined)
# User have given a CONF= argument.
ifeq ($$(CONF),)
# If given CONF=, match all configurations
matching_confs=$$(strip $$(all_confs))
else
# Otherwise select those that contain the given CONF string
matching_confs=$$(strip $$(foreach var,$$(all_confs),$$(if $$(findstring $$(CONF),$$(var)),$$(var))))
endif
ifeq ($$(matching_confs),)
$$(info No configurations found matching CONF=$$(CONF))
$$(info Available configurations:)
$$(foreach var,$$(all_confs),$$(info * $$(var)))
$$(eval $$(call FatalError))
else
ifeq ($$(words $$(matching_confs)),1)
$$(info Building '$$(matching_confs)' (matching CONF=$$(CONF)))
else
$$(info Building target '$(call GetRealTarget)' in the following configurations (matching CONF=$$(CONF)):)
$$(foreach var,$$(matching_confs),$$(info * $$(var)))
endif
endif
# Create a SPEC definition. This will contain the path to one or more spec.gmk files.
SPEC=$$(addsuffix /spec.gmk,$$(addprefix $$(output_dir)/,$$(matching_confs)))
else
# 没有传入 CONF 变量, 使用默认的配置进行解析. 如果只有一个配置目录. 则直接解析这个目录,否则有多个目录的时候必须指定一下CONF名
# No CONF or SPEC given, check the available configurations
ifneq ($$(words $$(all_spec_files)),1)
$$(info No CONF given, but more than one configuration found in $$(output_dir).)
$$(info Available configurations:)
$$(foreach var,$$(all_confs),$$(info * $$(var)))
$$(info Please retry building with CONF=<config pattern> (or SPEC=<specfile>))
$$(eval $$(call FatalError))
endif
# 默认情况下,我们找到了唯一的一个spec.gmk文件.
# We found exactly one configuration, use it
SPEC=$$(strip $$(all_spec_files))
endif
endif
endif
endef
通过对函数ParseConfAndSpec
的分析,我们可以得知此函数主要是为了解决SPEC
变量值的问题. 如果有值就直接使用.如果没有值,就会去build目录遍历查询此文件,即:spec.gmk
,如果查询不到此文件.或者有多个都会有相应的处理.
- 如果没有查找到, 提示要先运行
./configure
命令生成build目录.再运行make命令 - 如果找到有多个. 那会让用户用过参数:
CONF=xxx
来加载具体的build目录,这个一般在有多个编译目标的时候会出现.比如交叉编译和本平台编译. 或者要同时编译32位和64位的目标. 此时可以生成多份build目录. - 如果只有一个,且是合法的目录. 那就直接加载这个目录的SPEC文件.
build目录的makefile
此目录下有一个wrapper的makefile. 因为从根目录运行make
命令时, 会有可能有多个buld目录可供选择. 要么只有一个时默认选择一个.要么有多个时, 通过选择.(注: 通过参数名CONF, make CONF=<substring>
指定)
我们看一下此文件的内容:
# This Makefile was generated by configure Sun May 22 20:08:01 CST 2022
# GENERATED FILE, DO NOT EDIT
SPEC:=/PATH/TO/OPENJDK/jdk8u/build/macosx-x86_64-normal-server-slowdebug/spec.gmk
include /PATH/TO/OPENJDK/jdk8u/Makefile
可以看到, 在build目录运行的时候,直接指定了一个有效的SPEC值. 且只有一个值, 这是因为在特定的目录下编译,我们是知道 我们的编译目标是什么的. 是明确的. 因此我们可以直接给变量SPEC
赋值, 然后include源代码目录的makefile进行编译. 就像直接调用make 在源代码目录一样,唯一的不一样就是指定了SPEC.
注: 通过以上分析. 如果有多个build配置时,我们可以有两种方法来指定一个特定的编译目录.
- 通过在sourcecode目录运行make,同时指定CONF参数. 值为目录名
- 通过进入特定的build目录,在此build目录下直接运行make命令
Main.gmk
上面我们已经分析到,不管是build目录的Makefile
还是源代码根目录的:Makefile
, 两者最终都是加载了Main.gmk
文件进行运行编译的. 我们看此文件的内容也可以发现里面我们找到了makefile中比较熟悉的各种目标的定义,这里摘抄一部分的定义如下:
代码位置: make/Main.gmk
仓库代码链接: https://hg.openjdk.java.net/jdk8u/jdk8u/file/a323800a7172/make/Main.gmk
# Now load the spec
include $(SPEC)
# Load the vital tools for all the makefiles.
include $(SRC_ROOT)/make/common/MakeBase.gmk
# Include the corresponding custom file, if present.
-include $(CUSTOM_MAKE_DIR)/Main.gmk
### Main targets
default: jdk
@$(call CheckIfMakeAtEnd)
all: images docs
@$(call CheckIfMakeAtEnd)
# Setup a rule for SPEC file that fails if executed. This check makes sure the configuration
# is up to date after changes to configure
$(SPEC): $(wildcard $(SRC_ROOT)/common/autoconf/*)
@$(ECHO) "ERROR: $(SPEC) is not up to date."
@$(ECHO) "Please rerun configure! Easiest way to do this is by running"
@$(ECHO) "'make reconfigure'."
@if test "x$(IGNORE_OLD_CONFIG)" != "xtrue"; then exit 1; fi
start-make: $(SPEC)
@$(call AtMakeStart)
langtools: langtools-only
langtools-only: start-make
@$(call TargetEnter)
@($(CD) $(LANGTOOLS_TOPDIR)/make && $(BUILD_LOG_WRAPPER) $(MAKE) $(MAKE_ARGS) -f BuildLangtools.gmk)
@$(call TargetExit)
corba: langtools corba-only
corba-only: start-make
@$(call TargetEnter)
@($(CD) $(CORBA_TOPDIR)/make && $(BUILD_LOG_WRAPPER) $(MAKE) $(MAKE_ARGS) -f BuildCorba.gmk)
@$(call TargetExit)
jaxp: langtools jaxp-only
jaxp-only: start-make
@$(call TargetEnter)
@($(CD) $(JAXP_TOPDIR)/make && $(BUILD_LOG_WRAPPER) $(MAKE) $(MAKE_ARGS) -f BuildJaxp.gmk)
@$(call TargetExit)
分析到这里. 大家应该能够自己分析后面的编译过程了. 因为这里是大家熟悉的makefile的目标的定义. 以及各种目标的相互依赖. 当然有很多比较高级的技巧. 一般我们在写makefile的时候可能不会,也用不到. 这里有很多的技巧可以阅读学习.
子makefile
所有的编译目标基本都是通过子maekfile文件独立运行完成的, 因为openJDK里面有很多的组件. 大部分都是相互独立的. 而每一个目标的编译, OpenJDK的编译构建系统是单独启一个子make过程进行编译的. 比如我们看一下langtool的编译过程.
知识点: langtool 是一系列的语言工具的源代码集体. 比如我们常见的javac , javap命令的核心功能.
jaxws: langtools jaxp jaxws-only
jaxws-only: start-make
@$(call TargetEnter)
@($(CD) $(JAXWS_TOPDIR)/make && $(BUILD_LOG_WRAPPER) $(MAKE) $(MAKE_ARGS) -f BuildJaxws.gmk)
@$(call TargetExit)
以上命令就是通过再调用make命令. 调用子make进程进行了编译. 同时每一个单独的组件又会有一个单独的主makfile入口.比如这里指定的. BuildJaxws.gmk
构建系统本身是不允许并发多线程编译 .但是他可以在拆分多个编译任务的时候进行.
MakeBase.gmk
这个文件主要是一个琐碎的细节处理相关的定义函数或者宏. 比如分割日志, 分割参数. 参数拼接等.比如下面摘抄的定义,Make是一个非常古老且功能羸弱的编译工具. 面对如此大的编译工程的时候,比如有的时候参数会特别长. 直接传可能会超过限制,所以需要做一些变通的处理.
# 文件位置: make/MakeBase.gmk
define ListPathsSafely_If
$(if $(word $3,$($1)),$(eval $1_LPS$3:=$(call compress_paths,$(wordlist $3,$4,$($1)))))
endef
define ListPathsSafely_Printf
$(if $(strip $($1_LPS$4)),$(if $(findstring $(LOG_LEVEL),trace),,@)printf \
-- "$(strip $($1_LPS$4))\n" | $(decompress_paths) $3)
endef
总结
先简单总结一下本文写了什么,然后再来吐槽下makefile的精髓与糟粕.
- 我们先简单说了一下必须的前置知识和技能,也就是需要能够看懂makefile的语法与基本的流程. 同时给出了相应的学习路径
- 然后简单聊了一下其古怪的语法 , 以及为什么会变成这个样子.
- 因此引出了makefile的语法规范 . 这里简单说了下OpenJDK官方的代码规范.以及为什么会这样.
- 最后,在分析make流程前, 简单展开说了一下怎样调试makefile,用以应对你在自己调试和跟踪makefile的时候不至于无从下手.
- 最后非常浅显的分析了各个makefile的执行流程. 我们知道了可以分别从源代码根目录进行make,也可以在相应的build目录进行. 同时我们也知道了,在make前需要进行./configure, 如果不进行此步. make命令也会比较贴心的提醒你去先运行
./configure
- 最后分析了下Main.gmk文件的基本逻辑.
把前面的流程简单总结一下, 我们可以大概以这个图来结束上面的分析. 记住这个图,基本就理清了 OpenJDK
的编译流程大框架. 当然更细节的编译需要看每一个子模块的makefile的定义与执行过程,我相信你已经有这样的能力自己去完成了.
从上面的处理过程看,单只看make的过程,从主流程看并不是特别的复杂.读懂了之后反而还是蛮清晰的. 但是虽然功能主流程非常清晰.但是一细节的处理那就要了老命了. 比如上面提到的: MakeBase.gmk ,里面有非常多的取巧与hack的处理. 这些都是由于GNU make
的一些缺陷造成的.
从这些看 ,GNU MAKE不是没有缺点, 只是我们没得选. 大家在这些有缺陷的软件技术大厦上构建起了各种各样的开源软件. 面对如此复杂与庞杂的技术体系. 后面有在屎山上封装更便捷工具的各种autotools系统. 各种流程可以把你直接绕晕为止. 也有这里如OpenJDK里面的各种取巧和独辟蹊径的解决缺陷的workaroud. 而OpenJDK还好不是AutoTools系列的重度用户(原因最开始JDK就不是标准的GNU规范的软件), 如果是那样的话,你可能就看不到此文章了.
如果有时间,后面会出一篇分析java
命令是如何编译出来的文章.