harmony 鸿蒙Build System Coding Specifications and Best Practices

  • 2022-08-09
  • 浏览 (594)

Build System Coding Specifications and Best Practices

Overview

Generate Ninja (GN) is a meta-build system that generates build files for Ninja. It is the front end of Ninja. GN and Ninja together complete OpenHarmony build tasks.

GN

GN is used in large software systems such as Chromium, Fuchsia, and OpenHarmony. However, the GN syntax has limitations rooted in its design philosophy. For details, see https://gn.googlesource.com/gn/+/main/docs/language.md#Design-philosophy. For example, GN does not support wildcards and cannot get the length of a list. If you find it complex to implement something with GN, stop and consider whether it is necessary to do it. For more details about GN, see https://gn.googlesource.com/gn/+/main/docs/.

Intended Audience and Purpose

This document is intended for OpenHarmony developers. It describes the GN coding style and practices, but does not cover the GN syntax. For details about the GN basics, see https://gn.googlesource.com/gn/+/main/docs/reference.md.

General Principles

Scripts must be easy to read and maintain, and have good scalability and performance while functioning well.

Coding Style

Naming

Follow the Linux kernel naming style, that is, lowercase letters + underscores (_).

Local Variables

A local variable is a variable restricted to use in a certain scope and cannot be passed down.

Different from global variables, local variables start with an underscore (_).

# Example 1:
action("some_action") {
  ...
  # _output is a local variable.
  _output = "${target_out_dir}/${target_name}.out"
  outputs = [ _output ]
  args = [
    ...
      "--output",
      rebase_path(_output, root_build_dir),
      ...
  ]
  ...
}

Global Variables

A global variable starts with a lowercase letter.

Use declare_args to declare the variable value only if the variable value can be modified by gn args.

# Example 2
declare_args() {
  # The value of some_feature can be changed by gn args.
  some_feature = false
}

Targets

Name the targets in the lowercase letters + underscores (_) format.

Name the subtargets in templates in the ${target_name}+double underscores (__)+suffix format. This naming convention has the following advantages:

  • ${target_name} prevents duplicate subtarget names.

  • The double underscores (__) help locate the module to which a subtarget belongs.

  # Example 3
  template("ohos_shared_library") {
    # "{target_name}" (Target name) + "__" (double underscores) + "notice" (suffix)
    _notice_target = "${target_name}__notice"
    collect_notice(_notice_target) {
      ...
    }
    shared_library(target_name) {
      ...
    }
  }

Custom Templates

Name templates in the verb+object format.

# Example 4
# Good
template("compile_resources") {
  ...
}

Formatting

GN scripts must be formatted before being submitted. Formatting ensures consistency in style, such as code alignment and line feed. Use the format tool provided by GN to format your scripts. The command is as follows:

$ gn format path-to-BUILD.gn

gn format sorts the imported files in alphabetical order. To maintain the original sequence, add an empty comment line.

For example, the original import sequence is as follows:

# Example 5
import("//b.gni")
import("//a.gni")

gn format sorts the files as follows:

import("//a.gni")
import("//b.gni")

To maintain the original sequence, add an empty comment line.

import("//b.gni")
# Comment to keep the original sequence
import("//a.gni")

Coding Practices

Guidelines

The build script completes the following tasks:

  1. Describes the dependency (deps) between modules.

In practice, the most common problem is lack of dependency.

  1. Defines the module build rules (rule).

In practice, unclear input and output are common problems.

Lack of dependency leads to the following problems:

  • Unexpected compilation error
  # Example 6
  # Lack of dependency poses a possibility of compilation errors.
  shared_library("a") {
    ...
  }
  shared_library("b") {
    ...
    ldflags = [ "-la" ]
    deps = []
    ...
  }
  group("images") {
    deps = [ ":b" ]
  }

In this example, libb.so is linked to liba.so, which means that b depends on a. However, the dependency of b on a is not declared in the dependency list (deps) of b. Compilation is performed concurrently. An error occurs if liba.so is not available when libb.so attempts to create a link to it.

If liba.so is available, the compilation is successful. Therefore, lack of dependency poses a possibility of compilation errors.

  • Missing compilation of modules

In example 6, images are the target to build. Since images depend only on b, a will not be compiled. However, as b depends on a, an error occurs when b is linked.

Another problem is unnecessary dependencies. Unnecessary dependencies reduce concurrency and slow down compilation. The following is an example:

_compile_js_target does not necessarily depend on _compile_resource_target. If this dependency is added, _compile_js_target can be compiled only after _compile_resource_target is compiled.

# Example 7:
# Unnecessary dependencies slow down compilation.
template("too_much_deps") {
  ...
  _gen_resource_target = "${target_name}__res"
  action(_gen_resource_target) {
    ...
  }

  _compile_resource_target = "${target_name}__compile_res"
  action(_compile_resource_target) {
    deps = [":$_gen_resource_target"]
    ...
  }

  _compile_js_target = "${target_name}__js"
  action(_compile_js_target) {
    # This deps is not required.
    deps = [":$_compile_resource_target"]
  }
}

Unclear input leads to the following problems:

  • Modified code is not compiled during incremental compilation.
  • The cache being used is still hit after the code is changed.

In the following example, foo.py references the functions in bar.py. This means bar.py is the input of foo.py. You need to add bar.py to input or depfile of implict_input_action. Otherwise, if bar.py is modified, implict_input_action will not be recompiled.

# Example 8:
action("implict_input_action") {
  script = "//path-to-foo.py"
  ...
}
#!/usr/bin/env
# Content of foo.py
import bar
...
bar.some_function()
...

Unclear output leads to the following problems:

  • Implicit output
  • A failure to obtain the implicit output from the cache

In the following example, foo.py generates two files: a.out and b.out. However, the output of implict_output_action declares only a.out. In this case, b.out is an implicit output, and the cache stores only a.out. When the cache is hit, b.out cannot be compiled.

# Example 9
action("implict_output_action") {
  outputs = ["${target_out_dir}/a.out"]
  script = "//path-to-foo.py"
  ...
}
#!/usr/bin/env
# Content of foo.py
...
write_file("b.out")
write_file("a.out")
...

Templates

Do not use GN native templates. Use the templates provided by the build system.

The GN native templates include source_set, shared_library, static_library, action, executable and group.

The native templates are not recommended due to the following reasons:

  • The native templates provide only the minimal build configuration. They cannot provide functions, such as parsing external_deps, collecting notice, and generating installation information.

  • The native action template cannot automatically detect the changes in the dependencies of the input file, and cannot start recompilation. See Example 8.

The table below lists the mapping between the GN native templates and templates provided by OpenHarmony Compilation and Build subsystem.

OpenHarmony Template GN Native Template
ohos_shared_library shared_library
ohos_source_set source_set
ohos_executable executable
ohos_static_library static_library
action_with_pydeps action
ohos_group group

Using Python Scripts

You are advised to use Python scripts instead of shell scripts in action. Compared with shell scripts, Python scripts feature:

  • More user-friendly syntax, which eliminates errors caused by lack of a space
  • Easier to read
  • Easier to maintain and debug
  • Faster compilation due to caching of Python tasks

rebase_path

  • Call rebase_path only in args of action.
  # Example 10
  template("foo") {
    action(target_name) {
      ...
      args = [
        # Call rebase_path only in args.
        "--bar=" + rebase_path(invoker.bar, root_build_dir),
        ...
      ]
      ...
    }
  }
  
  foo("good") {
    bar = something
    ...
  }
  • If rebase_path is called twice for the same variable, unexpected results occur.
  # Example 11
  template("foo") {
    action(target_name) {
      ...
      args = [
        # After rebase_path is called twice for bar, the bar value passed is incorrect.
        "--bar=" + rebase_path(invoker.bar, root_build_dir),
        ...
      ]
      ...
    }
  }
  
  foo("bad") {
    # Do not call rebase_path here.
    bar = rebase_path(some_value, root_build_dir)
    ...
  }

Sharing Data Between Modules

It is common to share data between modules. For example, module A wants to know the output and deps of module B.

  • Data sharing within the same BUILD.gn

Data in the same BUILD.gn can be shared by defining global variables.

In the following example, the output of module a is the input of module b, and can be shared with module b via global variables.

  # Example 12
  _output_a = get_label_info(":a", "out_dir") + "/a.out"
  action("a") {
    outputs = _output_a
    ...
  }
  action("b") {
    inputs = [_output_a]
    ...
  }
  • Data sharing between different BUILD.gns

The best way to share data between different BUILD.gn is to save the data as files and transfer the files between modules. You can refer to write_meta_data in the OpenHarmony HAP build process.

forward_variable_from

  • To customize a template, pass (forward) testonly first because the testonly target may depend on the template target.
  # Example 13
  # For a customized template, pass testonly first.
  template("foo") {
    forward_variable_from(invoker, ["testonly"])
    ...
  }
  • Do not use asterisks (*) to forward variables. Required variables must be explicitly forwarded one by one.
  # Example 14
  # Bad. The asterisk (*) is used to forward the variable.
  template("foo") {
    forward_variable_from(invoker, "*")
    ...
  }
  
  # Good. Variables are explicitly forwarded one by one.
  template("bar") {
    # 
    forward_variable_from(invoker, [
                                       "testonly",
                                       "deps",
                                       ...
                                     ])
    ...
  }

target_name

The value of target_name varies with the scope.

# Example 15
# The value of target_name varies with the scope.
template("foo") {
  # The displayed target_name is "${target_name}".
  print(target_name)
  _code_gen_target = "${target_name}__gen"
  code_gen(_code_gen_target) {
    # The displayed target_name is "${target_name}__gen".
    print(target_name)
    ...
  }
  _compile_gen_target = "${target_name}__compile"
  compile(_compile_gen_target) {
    # The displayed target_name is "${target_name}__compile".
    print(target_name)
    ...
  }
  ...
}

public_configs

To export header files from a module, use public_configs.

# Example 16
# b depends on a and inherits from the headers of a.
config("headers") {
  include_dirs = ["//path-to-headers"]
  ...
}
shared_library("a") {
  public_configs = [":headers"]
  ...
}
executable("b") {
  deps = [":a"]
  ...
}

template

A custom template must contain a subtarget named target_name. This subtarget is used as the target of the template and depends on other subtargets. Otherwise, the subtargets will not be compiled.

# Example 17
# A custom template must have a subtarget named target_name.
template("foo") {
  _code_gen_target = "${target_name}__gen"
  code_gen(_code_gen_target) {
    ...
  }
  _compile_gen_target = "${target_name}__compile"
  compile(_compile_gen_target) {
    # The displayed target_name is "${target_name}__compile".
    print(target_name)
    ...
  }
  ...
  group(target_name) {
    deps = [
    # _compile_gen_target depends on _code_gen_target. Therefore, target_name only needs to depend on _compile_gen_target.
      ":$_compile_gen_target"
    ]
  }
}

set_source_assignment_filter

In addition to sources, set_source_assignment_filter can be used to filter other variables. After the filtering is complete, clear the filter and sources.

# Example 18
# Use set_source_assignment_filter to filter dependencies and add the dependencies with labels matching *:*_res to the dependency list.
_deps = []
foreach(_possible_dep, invoker.deps) {
  set_source_assignment_filter(["*:*_res"])
  _label = get_label_info(_possible_dep, "label_no_toolchain")
  sources = []
  sources = [ _label ]
  if (sources = []) {
    _deps += _sources
  }
}
sources = []
set_source_assignment_filter([])

In the latest version, set_source_assignment_filter is replaced by filter_include and filter_exclude.

Setting deps and external_deps

An OpenHarmony component is a group of modules that can provide a capability. When defining a module, set part_name to specify the component to which the module belongs.

You must also declare inner_kits of a component for other components to call. For details, see bundle.json in the source code. inner_kits applies only to dependent modules in different components.

If modules a and b has the same part_name, modules a and b belong to the same component. In this case, declare the dependency between them using deps.

If modules a and b have different part_name, modules a and b belong to different components. In this case, declare the dependency between them using external_deps in the Component name:Module name format. See Example 19.

# Example 19
shared_library("a") {
  ...
  external_deps = ["part_name_of_b:b"]
  ...
}

你可能感兴趣的鸿蒙文章

harmony 鸿蒙Subsystems

harmony 鸿蒙AI Framework Development Guide

harmony 鸿蒙NNRt Access Adaptation

harmony 鸿蒙Application Privilege Configuration

harmony 鸿蒙Development Example

harmony 鸿蒙Setting Up a Development Environment

harmony 鸿蒙Development Guidelines

harmony 鸿蒙Application Framework Overview

harmony 鸿蒙ArkCompiler Development

harmony 鸿蒙Custom Window Title Bar Development

0  赞