GNU Makefile eval 函数用法 及 OpenJDK中MakeHelpers.gmk辅助函数定义解析

  |   0 评论   |   1,484 浏览

1 openjdk makefile定义摘抄

源文件(54行): https://hg.openjdk.java.net/jdk8u/jdk8u/file/a323800a7172/Makefile

# ... and then we can include our helper functions

include $(root_dir)/make/MakeHelpers.gmk

$(eval $(call ParseLogLevel))
$(eval $(call ParseConfAndSpec))

MakeHelpers.gmk文件中定义了如下变量:
源文件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

2 call 基本用法.

基本语法

$(call variable,param,param,…)

语法说明:

  • 这里的variable是定义的递归扩展变量的名字. 比如func1. 此类变量一般使用递归扩展的方式进行定义.
    • 比如:func_add = $(shell echo $$(( $(1) + $(2) )) ) , 这里定义了一个能够实现数字加法的函数.
  • 参数是在扩展 前面的variable的时候传入的值. 分别依次把param1 ,param2 ... 赋值给临时变量:$(1),$(2) .... 用于表达式的扩展.

示例, 这里大概定义了一个加法的函数.可以让 make实现加法的操作.

func_add = $(shell echo $$(( $(1) + $(2) )) )

$(warning $(call func_add,1,2))

输出:
makefile:3: 3
make: *** No targets. Stop.

3 eval 基本用法

定义一个模板文件:
文件名: hello.mk

define hello_target
hello:;
	@echo $(1) says hello world
	@echo current directory is:$$(shell pwd)
endef

定义一个主文件:

文件名: makefile

program := main
include hello.mk


$(eval $(call hello_target,$(program)))

运行:make输出:

main says hello world
current directory is:/root/ccode/template

解释说明:

  • define结合着eval使用的基本功能是把一段代码原样的插入到makefile的一部分. 就像是直接在这里进行书写的一样. 同时makefile会对插入的内容进行语法解析.可以生成相应的变量/ 目标等内容.就像是直接在这里写的一样.
  • eval本身会进行一次扩展. 但是扩展的时候传的变量是在主makefile中定义好的. 无法进行函数化的调用参数传入.
  • 为了能够函数化的进行参数传递, 先进行一次call操作. 然后再把结果传递给eval进行处理.
    • 注: call的处理实际与eval是一样的. 只是call本身是得到一个变量值. (扩展后的) eval有能力让值变为makefile文件的定义的一部分.

4 分解 evalcall调用的整体过程

首先我们再贴一下这个 target目标模板:

define hello_target
hello:;
	@echo $(1) says hello world
	@echo current directory is:$$(shell pwd)
endef

defilne的使用简单说明:
define相当于定义了一个递归扩展的变量,不同的是这个变量的内容(值)可以包含多行.
更明确的定义:可以写为:

define hello_target =
hello:;
	@echo $(1) says hello world
	@echo current directory is:$$(shell pwd)
endef

也就是说,如果不写赋值号,默认定义的是 递归扩展(延迟扩展)变量.

首先我们可以把这个模板inlude进来.然后最好是能够原样输出一下变量的内容. 这里要用到一个函数 value来进行输出. 它能够按照变量的字面值进行输出.这样我们就能检验每一步操作的输入和输出的内容是什么.

此时的 makefile文件内容如下:

program := main
include hello.mk

$(info $(value hello_target))

这样可以原样输出变量:hello_target的内容.

这里用到了info函数,按理用它可以直接输出一个变量的内容. 但是任何对于变量的引用.如果是扩展型变量(递归扩展性变量) , 都会触发一次解引用(或者扩展) .因此如果要原样的看一个变量的内容,最好的方法就是用 value函数来打印一个变量的字面值.

输出内容如下:

hello:;
	@echo $(1) says hello world
	@echo current directory is:$$(shell pwd)

内容的值与我们想象的一样.没有进行扩展. 重点是看两个有 $的地方. 一个是: $(1),另外一个是:$$(shell pwd)

接下来是看调用 call之后的值是什么. 我们先尝试使用call进行调用.然后尝试直接info输出:

program := main
include hello.mk

$(info ---------------------)
$(info $(value $(call hello_target,$(program))))
$(info ---------------------)

此时得到的输出为:

---------------------

---------------------
make: *** No targets.  Stop.

发现没有任何输出. 原因是value的语法使用上的问题.

value的参数是变量的名字,不是变量的引用. 因此,需要把变量的内容用一个变量承载起来.然后打印出来:

[root@iZ25a8x4jw7Z ~/ccode/template]#vim makefile
program := main
include hello.mk

$(info ---------------------)
$(info $(value $(call hello_target,$(program))))
$(info ---------------------)

call_rs := $(call hello_target,$(program))
$(info $(value call_rs))

输出如下:

---------------------

---------------------
hello:;
	@echo main says hello world
	@echo current directory is:$(shell pwd)

make: *** No targets.  Stop.

解释:

  • $(1) 已经被展开了.$$(shell pwd)的两个$ 也已经变为了一个$ .

这样call函数会把 $(1) 进行扩展. 其它的两个 $ 会扩展为一个 $ .后面这个双 $ 的扩展不是因为call. 实际就是一个变量引用而触发的扩展而已.

此时,输入到eval函数的参数值已经显而易见了.

对于下面这个

eval在进行计算表达式的值的时候, 会把上面已经得到的:$(shell pwd) 进一步的执行.

注意: 这个执行不是像call函数一样把 原来的值当成一个String varible 直接进行替换,然后把这个替换后的整个string作为一个整体让make作为makefile的一部分进行解析.

正确的理解是:

  • 直接把call函数的结果进行解析执行. 也就是说把string内容当作makefile的一部分.进行解析执行.

下面是对GNU的官方文档的引用:

The eval function is very special: it allows you to define new makefile constructs that are not constant; which are the result of evaluating other variables and functions. The argument to the eval function is expanded, then the results of that expansion are parsed as makefile syntax. The expanded results can define new make variables, targets, implicit or explicit rules, etc.

翻译:

eval 函数非常特别: 允许你定义一个新的 makefile counstructs(makefile 结构) , 且这个结构可以是非常量的: 也就是说, 它可以是其它变量和函数的计算结果. eval函数的参数会被扩展. 然后扩展的结果将会以 makefile 语法进行解析.

备注:

这里说的在被makefile语法解析之前会被扩展. 这个是说的参数的扩展.或者说是如果引用的函数.是对函数的扩展. 对于:

c := hello
define var_def
b :=$(1)
a = $$(b)# 这里没有使用立即扩展是因为,如果是立即扩展.则不在makefile语法parse前是否有替换,最后都是hello.无法区分.
endef

$(eval $(call var_def,$(c)))
$(info the define value of a is $(value a))
# 如果 eval对于call的结果会再做一次 replace的话. 那a的值就是 hello.
# 如果 不是的话, 那a的值还是 $(b)
# 注: 这里的取值用 value函数.这样可以取原值.

输出:
[root@iZ25a8x4jw7Z ~/ccode/tmp]#make
the define value of a is $(b)
make: *** No targets. Stop.
说明不会在 makefile语法解析前再做一次replace.

The result of the eval function is always the empty string; thus, it can be placed virtually anywhere in a makefile without causing syntax errors.

翻译: eval 函数的执行结果总是 empty string ; 这样, 它可以被虚拟放置在makefile的任何地方.且不会引起语法错误.

It’s important to realize that the eval argument is expanded twice ; first by the eval function, then the results of that expansion are expanded again when they are parsed as makefile syntax. This means you may need to provide extra levels of escaping for “$” characters when using eval. The value function (see Value Function) can sometimes be useful in these situations, to circumvent unwanted expansions.

翻译:

理解 eval函数的参数会被扩展两次是十分重要的; 第一次是被 eval 函数,然后是当其被解析为makefile语法的时候对其进行二次扩展. 这意味着你需要提供额外的逃逸字符为:"$". 另外的 value函数有时候会特别有用在这个场景下. 它可以用来避免不想要的扩展.

解释:

这个第一次的扩展,实际是对于 eval的参数引用的一个本身内建的扩展而已. 就像引用一般的变量一样: 引用变量和函数都会进行扩展得到最终的引用值.比如如下的代码,是一个直接引用了一个参数,而并没有使用 call进行嵌套调用.:

a := hello
c := a

define var_def
b = $$($(c))
endef

$(eval $(var_def))
$(info $(value b))

输出:

$(a)
make: *** No targets.  Stop.

这个效果与上面的使用call的嵌套调用是一样的.

结论:

  • 这里所说的二次扩展,实际应该算一次. 另外一次是把变量的值作为makefile文件内容一样进行解析时,本身会有的扩展.
  • eval函数的作用就是把string内容当作是内容的一部分进行解析执行就可以了. 这个与一般的shell函数的eval的执行是类似的.
  • 因此:
    • 它是: 把string的内容当作正文一样直接解析. 这样解析执行产生的影响就像这些内容是真正在makefile文件中存在一样.
    • 它不是: 把string的内容进行replace, 然后把replace后的内容当作 makefile的正文.让makefile进行使用.

5 解释openjdk的辅助函数

ParseConfAndSpec上面的注释已经较好的说明了这个流程, 主要就是去搜索和定位一个默认使用的spec.gmk文件的路径并且把其赋值给SPEC变量的过程.

image.png

6 参考

  1. GNU Make Eval 函数官方手册
  2. GNUMake手册中译版 - 于凤昌 - 繁体版本
  3. GNUMake 手册中译版- 于凤昌 - 简体版本
  4. https://makefiletutorial.com/#conditional-ifelse
  5. Makefile语法及通用模板
  6. Makefile 有什么奇技淫巧? -知乎

评论

发表评论


取消