无垠之码

深度剖析代码之道


现代编译构建工具-Ninja语言

在传统的C/C++项目中,通常采用make系统,使用Makefile约束进行整个项目的编译构建。Makefile指定的编译依赖规则会使编译流程简单,但是make的依赖大而且复杂,在大型项目编译时,使用的模块越来越多,Makefile组织的项目编译时间越来越长,这个对于编译效率来说是一个极大的浪费。在执行增量或无操作时Make相较于Ninja构建时很慢,特别诸如Google Chrome这种将40,000个输入文件编译为单个可执行文件的工程,这可能会大大降低开发人员在大型项目上的工作速度。

根据Chromium的实际测试:在超过30,000个源文件的情况下,Ninja也能够在1秒钟内开始进行真正的构建。与之相比,通过资深工程师进行编写的Makefiles文件也需要10-20秒才能开始构建。

Ninja是由Google员工Evan Martin开发的小型构建系统。Ninja注重速度,Ninja被设计为使其输入文件由更高级别的构建系统生成,并且其被设计为尽可能快地运行构建。同时与Make相比,Ninja缺少诸如字符串操作比较、隐式规则、函数和遍历搜索等的功能,因为Ninja生成文件不需要手工编写。相反,应使用"生成器"生成Ninja生成文件。CMake,Meson,Gyp(Google早期用来维护chromium项目的构建系统,GN则是用来替代GYP的工具)和Gn是流行的构建管理软件工具,这些更高层次的工具,支持生成Ninja文件构建工程项目。

Ninja特有功能:

  1. Ninja特别支持在构建时发现额外的依赖项,从而可以轻松地为C/C++代码获取正确的头文件
  2. 更改编译标志将导致输出重新生成,因为输出隐式依赖于用于生成它们的命令行(等等,啥意思?)
  3. 规则可以提供正在运行的命令的简短描述,因此您可以在构建时打印例如CC foo.o而不是长命令行
  4. 构建工作始终并行运行。默认情况下,根据系统具有的CPU核数决定并行执行的线程数
  5. 命令输出始终是缓冲的。这意味着并行运行的命令不会交错其输出,当命令失败时可以将其失败输出打印在产生故障的完整命令行旁边

疑问: 特有功能1中,gcc -M 输出用于make系统的规则,该规则描述了源文件的依赖关系,makefile也可以轻松地为C/C++代码获取正确的头文件(见文章Gcc部分参数说明)

接下来将通过具体示例,探索GN、Meson和CMake这些现代构建工具如何生成Ninja文件,并分析它们在实际项目中的应用优势。

0.GN


GN是专为生成Ninja文件而设计的,运行速度极快,尤其适合大型项目,已在Chromium项目验证。其语法简单直接,避免了复杂的逻辑处理,专注于配置构建规则并且内置对依赖项的高效处理和验证机制,减少了构建错误,能够很好地应对超大规模代码库。但是其由Google内部主导开发,社区生态和扩展能力并没有Cmake和Meson好,并且缺少某些高级特性,如内置的包管理和复杂的自定义逻辑支持。

gn把.gn文件转换成.ninja文件,然后Ninja根据.ninja文件将源码生成目标程序

GN工具安装

apt-get install -y ninja-build
git clone https://gn.googlesource.com/gn
cd gn && python3 build/gen.py --allow-warning
out/gn_unittests

GN构建样例

gn-sample.tar.gz是关于gn构建的入门样例

tree -a
.
├── build
│   ├── BUILDCONFIG.gn
│   └── config
│       └── toolchains
│           └── BUILD.gn
├── BUILD.gn
├── .gn
└── src
    ├── BUILD.gn
    ├── main.c
    ├── tools.c
    └── tools.h
  1. .gn文件: gn命令执行时最先加载的配置文件,.gn置于根目录,buildconfig参数指定构建配置文件位置. 例子中指定的是//build/BUILDCONFIG.gn
  2. build/BUILDCONFIG.gn: 针对项目的一些设置包括不同操作系统的设置,默认编译工具的设置,例子中通过set_default_toolchain,指定//build/config/toolchains:gcc
  3. build/config/toolchains/BUILD.gn: 这个是针对工具链的一些设置
  4. BUILD.gn: 一般进行配置的文件,主要是配置需要编译的文件,库,最终的可执行文件,本例中依赖子目录src中的a.out目标
buildconfig = "//build/BUILDCONFIG.gn"
group("default") {
  deps = [ "//src:a.out" ]
}
set_default_toolchain("//build/config/toolchains:gcc")
import("//build/config/component.gni")  // 允许使用component模板
declare_args() {
  is_debug = false
  is_component_build = false   // 配置[静态编译,动态编译];如果为true的话,就全部是动态库,否则就是静态库
  is_mac = false
}
executable("a.out") {
  sources = [
    "main.c",
  ]
  include_dirs = [
    ".",
  ]
  deps = [
    ":tools",
  ]
  libs = [ "json-c" ]
  if (is_debug) {
    cflags = [ "-g", "-O0" ]
  }

shared_library("tools") {
  sources = [
    "tools.c",
  ]
  if (is_debug) {
    cflags = [ "-g", "-O0" ]
  }

component("test") {
  sources = [ "tools.c" ]
  if (is_debug) {
    cflags = [ "-g", "-O0" ]
  }
}
toolchain("gcc") {
  tool("cc") {
    depfile = "{{output}}.d"
    command = "gcc -MMD -MF $depfile {{defines}} {{include_dirs}} {{cflags}} {{cflags_c}} -c {{source}} -{{output}}"
    depsformat = "gcc"
    description = "CC {{output}}"
    outputs =
        [ "{{source_out_dir}}/{{target_output_name}}.{{source_name_part}}.o" ]
  }
  tool("cxx") {
    depfile = "{{output}}.d"
    command = "g++ -MMD -MF $depfile {{defines}} {{include_dirs}} {{cflags}} {{cflags_cc}} -c {{source}} -{{output}}"
    depsformat = "gcc"
    description = "CXX {{output}}"
    outputs =
        [ "{{source_out_dir}}/{{target_output_name}}.{{source_name_part}}.o" ]
  }
  tool("alink") {
    command = "ar rcs {{output}} {{inputs}}"
    description = "AR {{target_output_name}}{{output_extension}}"
    outputs =
        [ "{{target_out_dir}}/{{target_output_name}}{{output_extension}}" ]
    default_output_extension = ".a"
    output_prefix = "lib"
  }
  tool("solink") {
    soname = "{{target_output_name}}{{output_extension}}"
    sofile = "{{output_dir}}/$soname"
    rspfile = soname + ".rsp"
    if (is_mac) {
      os_specific_option = "-install_name @executable_path/$sofile"
      rspfile_content = "{{inputs}} {{solibs}} {{libs}}"
    } else {
      os_specific_option = "-Wl,-soname=$soname"
      rspfile_content = "-Wl,--whole-archive {{inputs}} {{solibs}} -Wl,--no-whole-archive {{libs}}"
    }
    command = "g++ -shared {{ldflags}} -o $sofile $os_specific_option @$rspfile"
    description = "SOLINK $soname"
    default_output_extension = ".so"
    default_output_dir = "{{root_out_dir}}"
    outputs = [ sofile ]
    link_output = sofile
    depend_output = sofile
    output_prefix = "lib"
  }
  tool("link") {
    outfile = "{{target_output_name}}{{output_extension}}"
    rspfile = "$outfile.rsp"
    if (is_mac) {
      command = "g++ {{ldflags}} -o $outfile @$rspfile {{solibs}} {{libs}}"
    } else {
      command = "g++ {{ldflags}} -o $outfile -Wl,--start-group @$rspfile {{solibs}} -Wl,--end-group {{libs}}"
    }
    description = "LINK $outfile"
    default_output_dir = "{{root_out_dir}}"
    rspfile_content = "{{inputs}}"
    outputs = [ outfile ]
  }
  tool("stamp") {
    command = "touch {{output}}"
    description = "STAMP {{output}}"
  }
  tool("copy") {
    command = "cp -af {{source}} {{output}}"
    description = "COPY {{source}} {{output}}"
  }
}
template("component") {
  if (is_component_build) {
    _component_mode = "shared_library
    if (!defined(invoker.output_name)) {
      _output_name = get_label_info(":$target_name", "label_no_toolchain")
      _output_name = string_replace(_output_name, "$target_name:$target_name", target_name)
      _output_name = string_replace(_output_name, "//", "")
      _output_name = string_replace(_output_name, "/", "_")
      _output_name = string_replace(_output_name, ":", "_")
    }
  } else if (defined(invoker.static_component_type)) {
    assert(invoker.static_component_type == "static_library" || invoker.static_component_type == "source_set")
    _component_mode = invoker.static_component_type
  } else if (!defined(invoker.sources) || invoker.sources == []) {
    _component_mode = "source_set"
  } else {
    _component_mode = "static_library"
  }
  target(_component_mode, target_name) {
    if (defined(_output_name)) {
      output_name = _output_name
    }
    if (is_component_build) {
      output_extension = "so"
    }
    forward_variables_from(invoker, ["testonly", "visibility",])
    forward_variables_from(invoker, "*", ["testonly", "visibility",])
  }
}

GN相关概念

1.GN命令

gn支持的命令
    analyze: Analyze which targets are affected by a list of files.
    args: Display or configure arguments declared by the build.
    check: Check header dependencies.
    clean: Cleans the output directory.
    clean_stale: Cleans the stale output files from the output directory.
    desc: Show lots of insightful information about a target or config.
    format: Format .gn files.
    gen: Generate ninja files.
    help: Does what you think.
    ls: List matching targets.
    meta: List target metadata collection results.
    outputs: Which files a source/target make.
    path: Find paths between two targets.
    refs: Find stuff referencing a target or file.

2.GN目标

申明的构建目标
    action: Declare a target that runs a script a single time.
    action_foreach: Declare a target that runs a script over a set of files.
    bundle_data: [iOS/macOS] Declare a target without output.
    copy: Declare a target that copies files.
    create_bundle: [iOS/macOS] Build an iOS or macOS bundle.
    executable: Declare an executable target.
    generated_file: Declare a generated_file target.
    group: Declare a named group of targets.
    loadable_module: Declare a loadable module target.
    rust_library: Declare a Rust library target.
    rust_proc_macro: Declare a Rust procedural macro target.
    shared_library: Declare a shared library target.
    source_set: Declare a source set target.
    static_library: Declare a static library target.
    target: Declare a target with the given programmatic type.

3.函数

可以在.gn文件中使用这些函数
    assert: Assert an expression is true at generation time.
    config: Defines a configuration object.
    declare_args: Declare build arguments.
    defined: Returns whether an identifier is defined.
    exec_script: Synchronously run a script and return the output.
    filter_exclude: Remove values that match a set of patterns.
    filter_include: Remove values that do not match a set of patterns.
    filter_labels_exclude: Remove labels that match a set of patterns.
    filter_labels_include: Remove labels that do not match a set of patterns.
    foreach: Iterate over a list.
    forward_variables_from: Copies variables from a different scope.
    get_label_info: Get an attribute from a target's label.
    get_path_info: Extract parts of a file or directory name.
    get_target_outputs: [file list] Get the list of outputs from a target.
    getenv: Get an environment variable.
    import: Import a file into the current scope.
    label_matches: Returns whether a label matches any of a list of patterns.
    not_needed: Mark variables from scope as not needed.
    pool: Defines a pool object.
    print: Prints to the console.
    print_stack_trace: Prints a stack trace.
    process_file_template: Do template expansion over a list of files.
    read_file: Read a file into a variable.
    rebase_path: Rebase a file or directory to another location.
    set_default_toolchain: Sets the default toolchain name.
    set_defaults: Set default values for a target type.
    split_list: Splits a list into N different sub-lists.
    string_join: Concatenates a list of strings with a separator.
    string_replace: Replaces substring in the given string.
    string_split: Split string into a list of strings.
    template: Define a template rule.
    tool: Specify arguments to a toolchain tool.
    toolchain: Defines a toolchain.
    write_file: Write a file to disk.

4.预定义变量

.gn文件中预定义变量
    current_cpu: [string] The processor architecture of the current toolchain.
    current_os: [string] The operating system of the current toolchain.
    current_toolchain: [string] Label of the current toolchain.
    default_toolchain: [string] Label of the default toolchain.
    gn_version: [number] The version of gn.
    host_cpu: [string] The processor architecture that GN is running on.
    host_os: [string] The operating system that GN is running on.
    invoker: [string] The invoking scope inside a template.
    python_path: [string] Absolute path of Python.
    root_build_dir: [string] Directory where build commands are run.
    root_gen_dir: [string] Directory for the toolchain's generated files.
    root_out_dir: [string] Root directory for toolchain output files.
    target_cpu: [string] The desired cpu architecture for the build.
    target_gen_dir: [string] Directory for a target's generated files.
    target_name: [string] The name of the current target.
    target_os: [string] The desired operating system for the build.
    target_out_dir: [string] Directory for target output files.

5.目标变量

定义在目标中的相关变量
    aliased_deps: [scope] Set of crate-dependency pairs.
    all_dependent_configs: [label list] Configs to be forced on dependents.
    allow_circular_includes_from: [label list] Permit includes from deps.
    arflags: [string list] Arguments passed to static_library archiver.
    args: [string list] Arguments passed to an action.
    asmflags: [string list] Flags passed to the assembler.
    assert_no_deps: [label pattern list] Ensure no deps on these targets.
    bridge_header: [string] Path to C/Objective-C compatibility header.
    bundle_contents_dir: Expansion of {{bundle_contents_dir}} in create_bundle.
    bundle_deps_filter: [label list] A list of labels that are filtered out.
    bundle_executable_dir: Expansion of {{bundle_executable_dir}} in create_bundle
    bundle_resources_dir: Expansion of {{bundle_resources_dir}} in create_bundle.
    bundle_root_dir: Expansion of {{bundle_root_dir}} in create_bundle.
    cflags: [string list] Flags passed to all C compiler variants.
    cflags_c: [string list] Flags passed to the C compiler.
    cflags_cc: [string list] Flags passed to the C++ compiler.
    cflags_objc: [string list] Flags passed to the Objective C compiler.
    cflags_objcc: [string list] Flags passed to the Objective C++ compiler.
    check_includes: [boolean] Controls whether a target's files are checked.
    code_signing_args: [string list] [deprecated] Args for the post-processing script.
    code_signing_outputs: [file list] [deprecated] Outputs of the post-processing step.
    code_signing_script: [file name] [deprecated] Script for the post-processing step.
    code_signing_sources: [file list] [deprecated] Sources for the post-processing step.
    complete_static_lib: [boolean] Links all deps into a static library.
    configs: [label list] Configs applying to this target or config.
    contents: Contents to write to file.
    crate_name: [string] The name for the compiled crate.
    crate_root: [string] The root source file for a binary or library.
    crate_type: [string] The type of linkage to use on a shared_library.
    data: [file list] Runtime data file dependencies.
    data_deps: [label list] Non-linked dependencies.
    data_keys: [string list] Keys from which to collect metadata.
    defines: [string list] C preprocessor defines.
    depfile: [string] File name for input dependencies for actions.
    deps: [label list] Private linked dependencies.
    externs: [scope] Set of Rust crate-dependency pairs.
    framework_dirs: [directory list] Additional framework search directories.
    frameworks: [name list] Name of frameworks that must be linked.
    friend: [label pattern list] Allow targets to include private headers.
    gen_deps: [label list] Declares targets that should generate when this one does.
    include_dirs: [directory list] Additional include directories.
    inputs: [file list] Additional compile-time dependencies.
    ldflags: [string list] Flags passed to the linker.
    lib_dirs: [directory list] Additional library directories.
    libs: [string list] Additional libraries to link.
    metadata: [scope] Metadata of this target.
    mnemonic: [string] Prefix displayed when ninja runs this action.
    module_name: [string] The name for the compiled module.
    output_conversion: Data format for generated_file targets.
    output_dir: [directory] Directory to put output file in.
    output_extension: [string] Value to use for the output's file extension.
    output_name: [string] Name for the output file other than the default.
    output_prefix_override: [boolean] Don't use prefix for output name.
    outputs: [file list] Output files for actions and copy targets.
    partial_info_plist: [filename] Path plist from asset catalog compiler.
    pool: [string] Label of the pool used by binary targets and actions.
    post_processing_args: [string list] Args for the post-processing script.
    post_processing_outputs: [file list] Outputs of the post-processing step.
    post_processing_script: [file name] Script for the post-processing step.
    post_processing_sources: [file list] Sources for the post-processing step.
    precompiled_header: [string] Header file to precompile.
    precompiled_header_type: [string] “gcc” or “msvc”.
    precompiled_source: [file name] Source file to precompile.
    product_type: [string] Product type for the bundle.
    public: [file list] Declare public header files for a target.
    public_configs: [label list] Configs applied to dependents.
    public_deps: [label list] Declare public dependencies.
    rebase: [boolean] Rebase collected metadata as files.
    response_file_contents: [string list] Contents of .rsp file for actions.
    rustflags: [string list] Flags passed to the Rust compiler.
    script: [file name] Script file for actions.
    sources: [file list] Source files for a target.
    swiftflags: [string list] Flags passed to the swift compiler.
    testonly: [boolean] Declares a target must only be used for testing.
    transparent: [bool] True if the bundle is transparent.
    visibility: [label list] A list of labels that can depend on a target.
    walk_keys: [string list] Key(s) for managing the metadata collection walk.
    weak_frameworks: [name list] Name of frameworks that must be weak linked.
    write_runtime_deps: Writes the target's runtime_deps to the given path.
    xcasset_compiler_flags: [string list] Flags passed to xcassets compiler
    xcode_extra_attributes: [scope] Extra attributes for Xcode projects.
    xcode_test_application_name: [string] Name for Xcode test target.   

6.其他

其他相关主题
    all: Print all the help at once
    buildargs: How build arguments work.
    dotfile: Info about the toplevel .gn file.
    execution: Build graph and execution overview.
    grammar: Language and grammar for GN build files.
    input_conversion: Processing input from exec_script and read_file.
    file_pattern: Matching more than one file.
    label_pattern: Matching more than one label.
    labels: About labels.
    metadata_collection: About metadata and its collection.
    ninja_rules: How Ninja build rules are named.
    nogncheck: Annotating includes for checking.
    output_conversion: Specifies how to transform a value to output.
    runtime_deps: How runtime dependency computation works.
    source_expansion: Map sources to outputs for scripts.
    switches: Show available command-line switches.

1.Meson


Meson是一个开源, 用户友好, 编译速度快的编译系统,很多开源软件都在拥抱Meson,比如Linux系统和服务管理器Systemd就是Meson系统构建,再比如QEMU,GNOME,PostgreSQL部分组件,都使用Meson构建系统。

  • multiplatform support for Linux, macOS, Windows, GCC, Clang, Visual Studio and others
  • supported languages include C, C++, D, Fortran, Java, Rust
  • build definitions in a very readable and user friendly non-Turing complete DSL
  • cross compilation for many operating systems as well as bare metal
  • optimized for extremely fast full and incremental builds without sacrificing correctness
  • built-in multiplatform dependency provider that works together with distro packages
  • fun!

虽然Meson具备以上有优点,但是相较于Cmake其生态还是较弱,不如CMake那样灵活,某些场景下需要额外工具辅助处理特殊需求。

Meson安装

sudo apt install ninja-build
sudo pip install meson

Meson构建样例

meson-sample.tar.gz是关于meson构建的入门样例. meson项目通过meson.build定义构建流程,meson_options.txt添加构建选项.

project('meson-sample', 'c', version: '0.0.1')
subdir('tools')

if get_option('debug')
    add_project_arguments('-DDEBUG', language: ['c'])
endif

if get_option('code_format')
    clang_format = find_program('clang-format', '/usr/sbin/clang-format', required : true)
    files = run_command(get_clang_format_files, 'src/**/*.c,src/**/*.h', check: false).stdout().split()
    foreach item:files
        run_command(clang_format, '--style=@0@'.format(get_option('code_style')) , '-i', item, check: false)
    endforeach
endif

tools = shared_library('libtools.so', './src/tools.c')
executable('a.out', './src/main.c', dependencies: dependency('json-c'), link_with: tools)
option('code_format', type : 'boolean', value : false, description : 'format code')
option('code_style', type : 'combo',  choices : ['llvm', 'google', 'chromium', 'mozilla', 'webkit'], value 'google', description : 'code style')
python3 = ['meson', 'runpython']
get_clang_format_files = python3 + files('get-clang-format-files.py')
import sys
import os
from glob import iglob

if len(sys.argv) != 2:
    print("Usage: {0} <path-glob>[,<path-glob>[,...]]".format(sys.argv[0]))
    sys.exit(1)

for path in sys.argv[1].split(','):
    for p in iglob(path, recursive=True):
        print(p)

2.CMake


CMake自然无须多言,其成为业界事实标准,几乎所有主流项目都支持。其丰富的文档、教程和插件,生态成熟的同时,对复杂的项目配置的支持、跨平台构建、第三方包集成以及自定义脚本都很优秀。但是与GN和Meson相比,生成Ninja文件的速度稍逊一筹,并且对新手不友好,需要较长时间熟悉其语法和功能,在大规模项目中,易导致配置膨胀,CMakeLists文件难以维护。

CMake安装

sudo apt install cmake
cmake .. -G Ninja

CMake构建样例

cmake-sample.tar.gz是关于cmake构建的入门样例.

cmake_minimum_required(VERSION 3.16.0)
project(cmake-sample)  

set(CMAKE_VERBOSE_MAKEFILE on)
set(CMAKE_EXPORT_COMPILE_COMMANDS on)
set(deplib json-c tools)
set(srcs ./src/main.c)  

add_library(tools SHARED ./src/tools.c)
target_compile_options(tools PRIVATE -g -O0)  

foreach(item ${srcs})
    get_filename_component(exec ${item} NAME_WE)
    add_executable(${exec} ${item})
    target_compile_options(${exec} PRIVATE -g -O0)
    target_link_libraries(${exec} PRIVATE ${deplib})
endforeach()

3.Ninja常用命令


依赖关系图

在Ninja源代码树中,为gn目标生成构建图表。如果没有给出目标,则为所有根目标生成一个图表

ninja -C out/Debug -t graph default | dot -Tpng -o default.png
#在Web浏览器中浏览依赖关系图。单击文件会将视图集中在该文件上,显示输入和输出
#该功能在某些版本存在bug
ninja -C out/Debug -t browse --port=8000 --no-browser gn 

gn工具构建依赖

清除构建产物

ninja -C out/Debug -t clean

显示构建命令

ninja -C out/Debug -t commands

gcc -MMD -MF obj/src/a.out.main.o.d  -I../../src   -c ../../src/main.c -o obj/src/a.out.main.o
gcc -MMD -MF obj/src/libtools.tools.o.d     -c ../../src/tools.c -o obj/src/libtools.tools.o
g++ -shared  -o ./libtools.so -Wl,-soname=libtools.so @libtools.so.rsp
g++  -o a.out -Wl,--start-group @a.out.rsp  -Wl,--end-group -ljson-c
touch obj/default.stamp

编译数据库

ninja -C out/Debug -t compdb

调试编译过程

ninja -C out/Debug -v -d stats -d explain -d keepdepfile -d keeprsp 

4.参考文献

  1. GN:quick-start
  2. 为速度而生的构建系统-Ninja
  3. ninja的使用及介绍
  4. GN:帮助手册
  5. Meson:Meson简介
comments powered by Disqus