Post

ninja代码解析01

ninja代码不多,主要分成两个部分,libninja + libninja-re2c

有些unix平台支持browse的,还会带上brows.py的代码, 跟libninja-re2c一起到browse.o里去,最后跟libninja一起合成ninja的二进制

理论上unix下的ninja binary里是直接有一个python代码的字符串文件的,它是通过inlie.sh把一个browse.py脚本直接转成了二进制的字符串数组丢给了”build/browse_py.h”里的kBrowsePy

1
ninja -t browse

所以上面的命令其实跑的是一个python代码, 通过pipe + fork + dup2 + execvp + python -从stdin读输入的py脚本,然后让他跑起来,第一次看到觉得非常的骚

libninja-re2c主要是用来解析ninja文件,比如里面主要就两个部分,第一个是lexer,第二个是depfile_parse

主要的功能逻辑其实都在libninja里,这里先大体看下ninja的设计逻辑语法,对后面理解代码很有帮助

语法

ninja的语法专注在编译,所以其实非常的简单,一开始的设计就不是让人手写的,希望用工具生成。

  1. edge,一条边,就是一个编译语句,会有输入,输出,规则,变量等
  2. target,一般是编译的输出,build语句的输出端
  3. output,跟target差不多
  4. input,build output的依赖输入
  5. rule,规则,在edge的build中指定的规则,指定了这个edge是怎么从输入产生输出的
  6. scope,作用域,变量的作用范围

关键字

  1. build, 定义edge
  2. rule,定义规则
  3. pool,定义pool,这个主要是为了并行度,在rule下指定pool,这个使用这个rule的edge的并行度就依赖这个pool的规则
  4. default,指定默认的一个或多个target
  5. include,添加ninja file到当前scope
  6. subninja,添加一个ninja文件,其scope跟当前的不一样
  7. phony,比较特殊rule,指定非文件target

变量

全局或者scope的,都直接用var = value的形式,然后用$var的形式引用

当然有内置的变量,in/out/builddir/ninja_required_version等

rule的内置一些变量值得说一下

rule cc
  command = gcc -c $in -o $out -MMD -MF $out.d
  description = CC $out
  depfile = $out.d
  deps = gcc
  pool = console
  1. cc就是rule的name
  2. command就是编译的命令,$in是输入,$out是输出
  3. description是编译的描述, 代替command没-v的时候的输出
  4. generator, bool, 这个rule的生成文件不会背默认清理
  5. in_newline: 换行符分割的输入
  6. in,空格分隔的输入
  7. depfile,依赖文件,这个文件会记录这个输出的依赖,如果依赖改变了,输出就会重新编译
  8. deps,依赖类型,gcc/msvc等
  9. msvc_deps_prefile,在deps = msvc情况下,指定需要去除的msvc输出前缀。
  10. restat,在command执行结束后,如果output时间戳不变,则当作未执行
  11. rspfile_content,rspfile,同时指定,在执行command前,把rspfile_content写入rspfile文件,执行成功后删除。
  12. pool指定pool,console是预定义的,这玩意的并行度只有1,比较特殊的是可以stdin, stdout, stderr的重定向

增量编译

  1. 输入输出时间戳
  2. ninja_deps + ninja_log,维护一个构建数据库了
  3. depfile,配合编译器依赖文件

depfile主要是依赖文件,它解决了自动发现和跟踪依赖关系的问题,特别是在C/C++项目中的头文件依赖。这玩意要依赖编译器生成的

  • gcc = -MMD -MF $out.d
  • msvc = /showIncludes /F$out.d

depfile的主要作用是记录一个输出文件的完整依赖关系,尤其是那些在构建开始前可能无法确定的依赖。以C/C++为例,源文件中可能包含许多头文件,这些头文件又可能包含其他头文件,形成复杂的依赖网络。

这种在编译下就不用手动维护依赖关系了,增量构建的效率非常高(直接编译器发现,非常准)

简单的例子

cxx = g++

rule cc
  command = ${cxx} -c $in -o $out
  description = ${cxx} $out

build output0 output1 | output2 output3: rule_name $  
        input0 input1 $      # 显示依赖
        | input2 input3 $    # 隐式依赖
        || input4 input5     # order-only依赖 可有可无
    var0 = str0 
    var1 = str1
  • order-only依赖的意思就是编译顺序,先input4 input5编译完了,再编译output0 output1
  • 显示依赖就是显示的依赖,如果input0 input1改变了,output0 output1就会重新编译
  • 隐式依赖就是不显示的依赖,如果input2 input3改变了,output0 output1(隐式输出)也会重新编译
  • $是转移,没有换行

隐式依赖的特点, 暂时没感觉有什么用

  1. 重建触发:如果隐式依赖(如示例中的input2和input3)发生变化,相关的输出文件(output0和output1以及隐式输出output2和output3)将被重新构建。

  2. 不作为命令行参数:与显式依赖不同,隐式依赖通常不会直接传递给构建命令。

  3. 隐式输出:在示例中,output2和output3是隐式输出。它们会被构建规则生成,但可能不是主要目标。隐式输出同样会根据依赖变化而重新构建

This post is licensed under CC BY 4.0 by the author.

Trending Tags