OpenJDK8 编译构建基础设施详解(2) - Make流程解析 置顶!

  |   0 评论   |   1,206 浏览

摘要

您也可以在知乎阅读此文: 知乎专栏-跟我一起阅读OpenJDK源

在上一篇文章中, 较详细的分析了 OpenJDK 编译过程中的两步的前一步:./configure , 这一篇主要来分析下make流程. make流程主要涉及的是OpenJDK 的makefile的结构, 以及make命令的工作流程.

注: 这些分析相对比较偏一些技术本身的框架. 对make目标以及相关的参数的使用与相关的说明. 还是建议多参考官方的编译说明.

本篇文章的关注重点主要是梳理清楚 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行;

image.png

这个其实根本在于makefile的语法怎么处理这个:

trim rule

注:
由于ifeq的第二个参数是由逗号分隔后往右trim的.导致有没有前导空格都会被trim干净.因此最后的效果的一样的.

那有没有空格是敏感的地方呢? 有,请看如下的一个解释图,这个是一般的函数调用的文本

image.png

从这里也可以看出, 一个是makefile的语法是真的很灵活.

OpenJDK中的makefile代码规范

以下内容引用自OpenJDK的官网的英文翻译:OpenJDK 代码规范

Makefile 中的空格和缩进比其他语言更成问题。尽管大多数时候空白(译者注: 空格或者tab) 并不重要,但有时候却很重要。这可能被认为是make中的一个设计缺陷,但我们无能为力。特别是,行首TAB缩进表示make 规则的命令(原文为: recipes,一般记为食谱,配方.实际解释为make执行的动作.),在make的字符串操作中,有时候空格很重要(有时候不重要)。

可能部分是由于这个原因,部分是由于工具的遗留性质,很少有编辑器/IDE/工具支持对 makefile 进行自动格式化。这使得保持空格使用和缩进成为一项“手工”任务。下面的规则适用于这个现实。为了简单起见,我们也对 shell 脚本和 autoconf 源代码使用了相同的规则集(对于那些适用的规则)。

下面是机器翻译: 我已经润色到足以让你读懂明白的程度.

空格和缩进规则

缩进,必要使用

  1. 缩进的基本级别是两个空格。使用它来进行“逻辑”缩进( if块函数定义等)。
  2. 如果一行必须断开,则使用四个空格进行缩进。
  3. Makefile 规则中的配方(Recipes)必须以 TAB (每个定义)开始。
  4. 如果单个制表符(解释为8个空格宽)不足以将配方(Recipes) 与周围的代码明确隔开(意味着至少4个空格差) ,则使用额外的制表符 tab
  5. recipe中的非shell 命令(例如 注释 和类似 ifdef 的指令)不能以 tab 开头,而是应该使用空格缩进到与周围 shell命令 相同的级别(此处将 tabs 解释为8个空格)。
  6. recipes中,额外的缩进应该在TAB之后使用空格来完成,就像在普通的 makefile 行中一样。
  7. 不完全的 recipes片段 (用于内联到 recipe 中的宏定义)应该像 recipe 一样处理,且行首一样以tab开始。

空格,需要使用的场景

  1. 永远不允许尾随空格 ( Trailing whitespaces )
  2. 除了以上的3-7规则(针对于recipe),不要使用tab, 请使用空格

空格: 推荐使用的场景

  1. There should be no whitespace between the list of targets and the : at the start of a rule.
  2. There should be an empty line before and after each rule.
  3. Avoid empty lines inside the recipe.
  4. Broken lines should end with a backslash, and a single space before the backslash (" \").
  5. A single space should separate a comma from the next argument in a function call.
  6. A single space should be used before and after assignment operators (e.g. :=, =, +=).

这些建议并不总是可行的,因为空格可能具有语义意义。如果make需要这些建议中的一个例外,理想情况下应该通过注释指出特别是逗号周围的空格可能是敏感的,所以要小心。

编码风格建议 (翻译有可能导致理解不正确,直接给原文)

  1. Use := (direct assignment) instead of = (recursive macro definition), unless you really need the recursive definition.
  2. 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.
  3. Avoid padding internally in a line with spaces to try to align some feature into columns with surrounding lines.
  4. 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):

为了避免文中造成的格式问题,我附上一张截图:

image.png

下面是我自己运行的结果, 与原文有出入. 具体我会解释

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

运行结果释义:

  1. 解析阶段解析第一行.直接打印.
  2. 立即扩展变量赋值, 操作. 直接打印.
  3. 第5行: 解析阶段 , 解析目标. 打印
  4. 第5行: 解析阶段, 解析依赖 ,打印
  5. 9: 解析变量引用: $(BAZ): 解析出来的结果作为目标名称. 这里是boo. 同时打印日志
  6. 解析完成. 开始执行默认目标. target
  7. 解析命令: $(BAR) , 此时打印出6行. (第二个6行)
  8. 解析命令行中的变量引用. 输出: In a command script , 这个行号是6,不是7有些诡异.
  9. 最后运行命令.
  10. boo , 首先是命令回显.
  11. 然后boo 是一个未定义的命令.因此出错了.直接退出 了.
  12. 然后打印了失败的shell输出.
  13. 最后打印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的执行会输出什么:

image.png

的确会打印出执行的每一个命名.对我们分析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的定义与执行过程,我相信你已经有这样的能力自己去完成了.

image.png

从上面的处理过程看,单只看make的过程,从主流程看并不是特别的复杂.读懂了之后反而还是蛮清晰的. 但是虽然功能主流程非常清晰.但是一细节的处理那就要了老命了. 比如上面提到的: MakeBase.gmk ,里面有非常多的取巧与hack的处理. 这些都是由于GNU make的一些缺陷造成的.

从这些看 ,GNU MAKE不是没有缺点, 只是我们没得选. 大家在这些有缺陷的软件技术大厦上构建起了各种各样的开源软件. 面对如此复杂与庞杂的技术体系. 后面有在屎山上封装更便捷工具的各种autotools系统. 各种流程可以把你直接绕晕为止. 也有这里如OpenJDK里面的各种取巧和独辟蹊径的解决缺陷的workaroud. 而OpenJDK还好不是AutoTools系列的重度用户(原因最开始JDK就不是标准的GNU规范的软件), 如果是那样的话,你可能就看不到此文章了.

如果有时间,后面会出一篇分析java命令是如何编译出来的文章.

参考文献

  1. Building the JDK
  2. OpenJDK Build README - OpenJDK8
  3. OpenJDK - Readme.md - github - 最新
  4. OpenJDK - 代码规范
  5. 跟我一起写 Makefile
  6. GNU Make 使用手册(中译版)- 于凤昌
  7. GNU make 中文手册 - 徐海兵
  8. GNU Make Manual - Rule-Introduction

评论

发表评论


取消