GN in Zircon

This discussion assumes basic familiarity with GN syntax and concepts. This introduction to GN can provide that background.

GN uses a templating structure to abstract many of the build details away from the end user. Below are a subset of the templates the Zircon GN defines, focusing on the ones with which Zircon hackers are most likely to interact.

$zx/ prefix

TODO(BLD-353): This is a hold-over from the "layer cake" design and will probably change soon:

As discussed in the introduction, GN uses "source-absolute" paths that look like //a/b/c. In the Zircon GN files, we never use //. Instead, use $zx/foo to refer to //zircon/foo, e.g. "$zx/system/ulib/zircon".

executable() and test()

The primary target type in producing a binary is executable(). This produces an executable binary from the listed sources. The Zircon build also provides a means to indicate the location in the image wherein that binary should be installed via the install_path variable in the target scope. install_path can be:

  • a string: the path relative to the root of the BOOTFS (with no leading /)
  • omitted: use the default path of bin/<binary_name>
  • false: do not install this file at all

The build also provides a test() target, which is identical to executable() except that it sets testonly = true and that its default install_path is test/<binary_name> instead of bin/<binary_name>.

test() can be used for a test program that runs on Zircon or for a test program that runs on the host side. In fact, the same test() target can serve to build the same test program for both situations with no extra work required. (It's just what dependency paths reach that target that will determine whether it's built for host or for Zircon or for both.)

library()

The library() template is for any kind of "library" in the Zircon tradition, whether for the kernel, Zircon user code, or host-side code. The basic thing it means to be a "library" is that there is an include/ subdirectory of public header files. Dependents that list this library() target in their deps will automatically get -I switches for that include/ directory.

The default case with the most concise syntax is a static-only userland library. Making a library available as a shared library just requires adding the line shared = true. Likewise, making a library available for host-side use just requires adding the line host = true. These are in addition to the default static = true that makes the library available for userland static linking. For a library that should never be statically linked (aside from host-side or kernel uses), you can override the default with static = false.

For a library in the kernel, set kernel = true. This is the same whether it's a kernel-only library, or is code shared between kernel and user (and/or host). Setting kernel = true changes the default to static = false, so if a library can be used either in the kernel or in userland, then you must set static = true explicitly alongside kernel = true (unless you set shared = true and want to prohibit static linking of that library in userland).

Here’s an exemplar showing all the essential options. Most actual targets will be little more than a sources list and a deps list.

library("foo") {
  # Builds "libfoo.a" when static, "libfoo.so" when shared.

  static = true  # default, omitted unless kernel = true: build userland libfoo.a
  shared = true  # false if omitted: build userland libfoo.so
  kernel = true  # false if omitted: can be used from kernel
  host = true  # false if omitted: can be used in host tools

  sources = [
    "foo.c",
    "bar.cpp",
  ]

  deps = [
    # Can refer here to `source_set()` or other `library()` targets defined
    # locally.
    ":foo_minimal",  # Defined in this same BUILD.gn file.
    "foobar_subsystem",  # Defined in foobar_subsystem/BUILD.gn relative to here.

    # Explicitly link in static libbar.a even if libbar.so is available.
    "$zx/system/ulib/bar:static",

    # Be explicit about getting libbaz.so as a shared library.
    "$zx/system/ulib/baz:shared",

    # Compile with -Isystem/ulib/bozo/include, but don't link anything in.
    # This should usually not be used in `deps`, but only in `public_deps`.
    # See below.
    "$zx/system/ulib/bozo:headers",

    # Let system/ulib/quux/BUILD.gn decide whether static or shared is the
    # norm for that library.  (So far the defining `library()` will always
    # prefer the shared library if it's enabled; it would be easy to add the
    # option to build shared but default to static if that's ever useful.)
    "$zx/system/ulib/quux",

    # `library("quextras")` appears in system/ulib/quux/BUILD.gn because quux
    # and quextras want to share some private source code or for whatever
    # reason we've decided putting them in a single directory is right.
    # Because we're not using the target with the name of its directory,
    # the `:name` syntax selects the specific target within that BUILD.gn file.
    # For the derived target names, we use `.` before the suffix.
    # In fact, "quux:headers" is just an alias for "quux:quux.headers", etc.
    "$zx/system/ulib/quux:quextras",
    "$zx/system/ulib/quux:quextras_more.static",
    "$zx/system/ulib/quux:quextras_way_more.shared",

    # This is a `library()` that will set `static=false shared=true`
    # so `zircon:static` here wouldn't work but `zircon:shared` would work.
    "$zx/system/ulib/zircon",
  ]

  # Per-module compilation flags are always optional.
  # *Note*: For cases where the flag order matters, it may be necessary
  # to use a config() instead.
  cflags = [ "-Wfoo", "-fbar" ]
  cflags_cc = [ "-fonly-for-c++" ]
  cflags_c = [ "-fonly-for-c" ]
  asmflags = [ "-Wa,--some-as-switch" ]
  ldflags = [ "-Wl,--only-affects-shlib-link" ]
}

A heavily abridged real-world example of a kernel module:

# deps = [ "$zx/kernel/object" ] gets -Ikernel/object/include
library("object") {
  kernel = true
  sources = [
    "buffer_chain.cpp",
    "process_dispatcher.cpp",
  ]
  deps = [
    "$zx/kernel/dev/interrupt",
    "$zx/system/ulib/fbl",
  ]
}

Note system/ulib/fbl is not kernel/lib/fbl: the one fbl serves all. Here's a heavily abridged example for that case:

library("fbl") {
  kernel = true
  static = true
  sources = [
    "alloc_checker.cpp",
  ]
  if (is_kernel) {
    sources += [
      "arena.cpp",
      "arena_tests.cpp",
    ]
  } else {
    sources += [ "string.cpp" ]
  }
}

The actual fbl is a bad example because it has other complications, but this demonstrates how a library of shared code can be maintained in one place with one BUILD.gn file using one library target to describe both the kernel and userland incarnations. They share everything, but can differ as needed based on is_kernel conditionals.

Libraries define a standard set of targets (if relevant):

  • $target_name.headers is always provided, for just getting the headers and not linking it in
  • $target_name.static is provided if static = true (the default)
  • $target_name.shared is provided if shared = true

If the library is the main target in the file (e.g. $zx/foo:foo)--the common case--the static, shared, and headers sub-targets are aliased into $zx/foo:static, $zx/foo:shared, and $zx/foo:headers.

public_deps for header dependencies

In addition to deps and data_deps, GN also has public_deps. This is used when a target exposes a dependency in its public header files and needs to forward that dependency's settings up the dependency chain. Every use of public_deps should have a comment explaining why it's needed:

For example, library("async-loop") contains this:

  public_deps = [
    # <lib/async-loop/loop.h> has #include <lib/async/dispatcher.h>.
    "$zx/system/ulib/async:headers",
  ]

source_set() and static_library()

Some code that doesn't have an include directory can just use the native GN source_set() or static_library() targets.

A source set (see gn help source_set) is a way to create a logical grouping of files or to scope compilation switches narrowly. The object files will be linked directly into final binaries without going through any intermediate libraries. In contrast, the files in a static library are only pulled in as-needed to resolve symbols.

  • Code in the kernel itself should always use source_set. Static libraries currently interact poorly with inline assembly (Googlers see bug 124318741).

  • A source_set must be used when creating groups of tests since the test harness depends on static initializers while the static library linking rules will strip the tests. All kernel code.

  • A static_library should be used for higher-level things that looks like libraries or a part of one. The dead code stripping is more efficient which can produce faster links and smaller binaries in cases where some code isn't needed.

source_set("some_code") {
  sources = [
    "this.c",
    "that.cpp",
  ]
}

loadable_module()

This is not really used in the Zircon build so far, but could be. A loadable module is a shared object that's not linked directly but rather loaded dynamically via dlopen() or the like.

loadable_module() takes the install_path parameter like executable() does. But it has no default path, so it's like install_path = false unless you supply a path explicitly.

Zircon device drivers are loadable modules, but they have their own special templates that should be used instead of loadable_module().

driver() and test_driver()

Drivers are loadable modules with some special support and constraints.

  • They get a default install_path appropriate for drivers, so they will be found by devmgr.
  • They implicitly depend on libdriver so it shouldn't be listed in deps.
  • They implicitly use the static C++ standard library.

test_driver() is to driver() as test() is to executable().

driver("fvm") {
  sources = [
    "fvm.cpp",
  ]
  deps = [
    "$zx/system/ulib/ddktl",
    "$zx/system/ulib/fs",
    "$zx/system/ulib/zircon",
  ]
}

resources() and firmware()

A resource() target declares some file that might be needed in the BOOTFS image, but doesn’t directly cause anything to happen in the build. The style of the rule is as if it’s a copy from a source file to an output file in the build; it’s modelled on GN’s native copy() rule, and gn help copy explains why its syntax is exactly the way it is. outputs is single-element list containing a path in the BOOTFS.

import("$zx/public/gn/resource.gni")

resource("tables") {
  sources = [
    "data.tbl",
  ]
  outputs = [
    "data/some_lib/data_v1.tbl",
  ]
}

The purpose of resource() is to be listed in the data_deps of the target that uses the data:

library("uses_tables") {
  sources = [
    "read_table.cc",
  ]
  data_deps = [
    ":tables",
  ]
}

This can be a library(), an executable(), a source_set(), etc. Good practice is to put the data_deps in the finest-grained target that holds the code that uses the file at runtime. Doing so ensures that the relevant resource will be available at runtime.

If the resource is generated by the build, then the path in the sources list identifies its location in the build directory, usually using $target_out_dir or $target_gen_dir. In that case, the resource() must also have a deps list that includes the target that generates that file.

The build also allows for a special type of resource that is generated from the dependency graph. Using generated_resource() creates a resource file that is intended for use in data_deps, as in a normal resource(), but instead of using an existing source file it will generate a file at gn gen time with fixed contents or based on a metadata collection (see gn help generated_file for details).

firmware() is a special-case variant of resource(), intended for drivers. It places the resource in /lib/firmware/$path, where $path is a relative path to the resource in the /lib/firmware root. This mimics the calling convention in devhost, where a driver calls load_firmware(...) on a relative path.

fidl_library()

This template allows the definition of a FIDL library and its associated bindings. Declaring a fidl_library() target will cause the build to generate bindings for all supported languages.

import("$zx/public/gn/fidl.gni")

# Defined in $zx/system/fidl/fuchsia-io/BUILD.gn
fidl_library("fuchsia-io") {
  sources = [
    "io.fidl",
  ]
  public_deps = [
    "$zx/system/fidl/fuchsia-mem",
  ]
}

Note the use of public_deps. When a FIDL library's source files have using other_library; that's equivalent to a C/C++ library using #include <other_library/header> in its public headers. Since this is very common for FIDL (and Banjo) libraries, we don't require comments on every case when it follows this simple pattern.

Depending on which bindings are defined, the above example will generate a set of targets of the form $zx/system/fidl/fuchsia-io:fuchsia-io.<language>, or, in the case where the target name is the same as the directory name as above, $zx/system/fidl/fuchsia-io:<language>.

The common case today is "$zx/system/fidl/fuchsia-io:c".

banjo_library()

The definition of Banjo libraries is similar to that of FIDL libraries. A banjo_libary() target will generate bindings for all supported languages, though the set of supported languages will be different from that of FIDL.

import("$zx/public/gn/banjo.gni")

banjo_library("ddk-driver") {
  sources = [
    "driver.banjo",
  ]
}

Currently, listing the plain target with no :<language> suffix in deps gets both the C and C++ bindings. This will probably change in the near future to more closely follow the FIDL model: specify exactly which bindings you depend on.

See above about public_deps. Its use in banjo_library() is exactly like its use in fidl_library().