# Copyright 2023 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Library for specifying GN args for builders.
More details can be found in design doc: go/gn-args-in-starlark-dd
"""
load("@stdlib//internal/graph.star", "graph")
load("@stdlib//internal/luci/common.star", "keys")
load("//project.star", "settings")
load("./chrome_settings.star", "per_builder_outputs_config")
load("./nodes.star", "nodes")
_GN_CONFIG = nodes.create_unscoped_node_type("gn_config")
_PHASED_CONFIG = nodes.create_unscoped_node_type("phased_config")
_GN_ARGS_FILE_NAME = "gn-args.json"
def _get_gn_args_resolver():
resolved_gn_args_by_config_node = {}
def merge_gn_args_into_dict(dst, args_file = "", gn_args = {}):
"""Merge imported GN file and GN args into the specified dict.
The function modifies the dst dict and has no return values.
The entries from the input parameters will take priority and overwrite
those duplicated entries in the dst dict as the result of the merge.
Args:
dst: (dict) The dict into which the rest of the inputs will be
merged.
args_file: (string) The string path of an imported GN file.
gn_args: (dict) A dict of GN arg key-value pairs.
"""
if args_file:
if dst["args_file"]:
fail("Each GN config can only contain a single args_file")
dst["args_file"] = args_file
dst["gn_args"].update(gn_args)
def visitor(_, children):
return [c for c in children if c.key.kind == _GN_CONFIG.kind]
def resolve(gn_config_node):
"""Resolve the GN args of a given GN config node.
Essentially, this function traverses the graph by DFS with memoization.
Args:
gn_config_node: (node of "gn_config" kind) The node to resolve
GN args for; typically a node representing a builder.
Returns:
A dict representing the GN args of the input node, with entries:
* args_file: a string path of an imported GN file.
* gn_args: a dict of GN arg key-value pairs.
"""
for n in graph.descendants(
gn_config_node.key,
visitor = visitor,
order_by = graph.DEFINITION_ORDER,
topology = graph.DEPTH_FIRST,
):
if n in resolved_gn_args_by_config_node:
continue
gn_args = {
"args_file": "",
"gn_args": {},
}
# Merge GN args of child nodes in DEFINITION_ORDER of the edges.
for child in graph.children(
n.key,
kind = _GN_CONFIG.kind,
order_by = graph.DEFINITION_ORDER,
):
child_config = resolved_gn_args_by_config_node[child]
merge_gn_args_into_dict(gn_args, child_config["args_file"], child_config["gn_args"])
# Merge GN args of the current node.
merge_gn_args_into_dict(
gn_args,
n.props.args_file,
n.props.gn_args,
)
# Memoize.
resolved_gn_args_by_config_node[n] = gn_args
config = {k: v for k, v in resolved_gn_args_by_config_node[gn_config_node].items() if v}
gn_args = gn_config_node.props.builder_defaults | config.get("gn_args", {})
if gn_args:
config["gn_args"] = gn_args
if "target_os" not in gn_args:
fail("target_os is required for gn_args: {}".format(gn_config_node.key.id))
if "target_cpu" not in gn_args:
fail("target_cpu is required for gn_arg: {}".format(gn_config_node.key.id))
return config
return resolve
def _create_gn_config_node(*, name, gn_args = {}, configs = [], args_file = "", builder_group = "", builder_defaults = {}):
"""Create a node of "gn_config" kind and place it into the graph.
Args:
name: (string) The name of the GN config.
gn_args: (dict) A dict of GN arg key-value pairs.
configs: ([string]) A list of GN config name strings.
args_file: (string) The string path of an imported GN file.
builder_group: (string) The builder group of a builder node.
builder_defaults: (dict) A dict of GN arg key-value pairs. In
contrast to `gn_args`, arguments specified here are only to
the builder that they are specified and not included when
the config is referenced by another builder and will be
overridden by the expanded configs specified in `configs`.
This is used for implementing automatic GN args based on
non-gn-arg builder configuration values.
Returns:
The node key of the created node.
"""
gn_config_key = _GN_CONFIG.add(name, props = {
"gn_args": gn_args,
"args_file": args_file,
"builder_group": builder_group,
"builder_defaults": builder_defaults,
})
# Create edges in the graph from the new node to its included nodes.
# Edge creation follows the order of the configs in the list. This order
# also determines the final value of a GN arg when there are duplicated
# entries of GN args for a builder, i.e. an included config with a larger
# index in the list will overwrite duplicated GN args for any config with
# a smaller index.
for child_config_name in configs:
graph.add_edge(gn_config_key, _GN_CONFIG.key(child_config_name))
return gn_config_key
def _config(*, name = None, args = {}, configs = [], args_file = ""):
"""Define a GN config.
This method can be used to create commonly used GN config by setting
the "name" parameter, see //gn_args.star for more examples.
It can also be used without setting a "name" to specify the gn_args
property within a builder definition.
Args:
name: (string) The name of the GN config.
args: (dict) GN arg key-value pairs.
configs: (list) String names of included GN configs.
args_file: (string) The string path of an imported GN file.
Returns:
None if the "name" parameter is set; otherwise, the input for
the "gn_args" field within a builder definition.
"""
if name:
_create_gn_config_node(
name = name,
gn_args = args,
configs = configs,
args_file = args_file,
)
return None
else:
return struct(
gn_args = args,
configs = configs,
args_file = args_file,
)
gn_args = struct(
config = _config,
)
def register_gn_args(builder_group, bucket, builder, gn_args, use_siso):
"""Register GN args for a builder.
Internally creates a node of gn_config kind for a builder.
The builder ID ("bucket/builder") will be used as the name of the node.
Args:
builder_group: (string) The builder group name.
bucket: (string) The bucket name.
builder: (string) The builder name.
gn_args: The string name of a GN config, a dict of phased GN config,
or the return value of a gn_args.config method call without setting
the "name" parameter.
use_siso: (boolean) if True, configs will automatically set the use_siso
GN arg to true unless it is set by the config.
Returns:
A list of generated GN args file paths relative to the per-builder
output root dir if gn_args is set; None otherwise.
"""
defaults = {}
# TODO: jwata - Re-enable this logic for Chrome after the GN args config migration.
if use_siso and not settings.project.startswith("chrome"):
defaults["use_siso"] = True
# Function for formating GN config for GN config node creation.
def format_gn_config(config):
if type(config) == "string":
d = {"configs": [config]}
else:
d = {a: getattr(config, a) for a in dir(config)}
d["builder_defaults"] = defaults
return d
builder_node_name = "{}/{}".format(bucket, builder)
if gn_args:
if type(gn_args) == "dict":
# Phased GN config.
builder_node_key = _PHASED_CONFIG.add(builder_node_name, props = {
"builder_group": builder_group,
})
phase_prefix = "{}/{}".format(bucket, builder)
for phase_name, config in gn_args.items():
phase_node_name = "{}:{}".format(phase_prefix, phase_name)
formatted_config = format_gn_config(config)
phase_node_key = _create_gn_config_node(name = phase_node_name, **formatted_config)
graph.add_edge(builder_node_key, phase_node_key)
else:
# Non-phased GN config.
formatted_config = format_gn_config(gn_args)
formatted_config["builder_group"] = builder_group
builder_node_key = _create_gn_config_node(name = builder_node_name, **formatted_config)
graph.add_edge(keys.project(), builder_node_key)
return ["{}/{}/{}".format(bucket, builder, _GN_ARGS_FILE_NAME)]
else:
return None
def _generate_gn_args(ctx):
"""Generator callback for generating "gn-args.json" files.
Args:
ctx: the context object.
"""
args_gn_locations = {}
root_out_dir = per_builder_outputs_config().root_dir
resolve_gn_args = _get_gn_args_resolver()
# Function for validating special restrictions
def validate_gn_args(gn_args_dict):
# "chromium" group builders build public artifacts and should not
# include Chrome proprietary codec.
if node.props.builder_group == "chromium":
if (gn_args_dict["gn_args"].get("proprietary_codecs", False) or
gn_args_dict["gn_args"].get("ffmpeg_branding", "Chromium") != "Chromium"):
fail("\"chromium\" group builder " +
"{} should not include Chrome proprietary codec".format(node.key.id))
# Function for generating args-gn.json files
def gen_args_gn_json(gn_args_dict):
gn_args_file_path = "{}/{}/{}".format(root_out_dir, node.key.id, _GN_ARGS_FILE_NAME)
ctx.output[gn_args_file_path] = json.indent(json.encode(gn_args_dict), indent = " ")
builder_name = node.key.id.rsplit("/", 1)[-1]
builder_group = node.props.builder_group
args_gn_locations.setdefault(
builder_group,
{},
)[builder_name] = "{}/{}".format(node.key.id, _GN_ARGS_FILE_NAME)
# Builders with non-phased GN configs
for node in graph.children(keys.project(), _GN_CONFIG.kind):
gn_args_dict = resolve_gn_args(node)
validate_gn_args(gn_args_dict)
gen_args_gn_json(gn_args_dict)
# Builders with phased GN configs
for node in graph.children(keys.project(), _PHASED_CONFIG.kind):
phases_dict = {}
for phase_node in graph.children(node.key):
phase_args = resolve_gn_args(phase_node)
validate_gn_args(phase_args)
phases_dict[phase_node.key.id.split(":")[-1]] = phase_args
gen_args_gn_json({"phases": phases_dict})
locations_file_path = "{}/gn_args_locations.json".format(root_out_dir)
ctx.output[locations_file_path] = json.indent(
json.encode(args_gn_locations),
indent = " ",
)
lucicfg.generator(_generate_gn_args)