blob: 3c82fdc4f6b5bd7c94030148b5561f3c050fb0e2 [file] [log] [blame]
# Copyright 2024 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import("//build/group_with_inputs.gni")
# Generate the product assembly config file, after rebasing all input files in
# the 'platform' and 'product' configuration scopes, tracking them as 'inputs'
# to GN.
#
# NOTE: This creates a separate target, "$target_name.inputs", which MUST be
# depended upon (separately) by users of this template to ensure that the
# input files found in 'config_file_potential_inputs' are either source files or
# are created by the 'deps' that are passed to this template.
#
# This is a build optimization so that the json file generated by this template
# can be used with validation code that compares it with Bazel-generated files,
# without those validation targets taking compile-time dependencies on all the
# inputs / deps.
#
# Params:
#
# product_assembly_config
# [scope] The product assembly config (platform, product, packages,
# base_drivers, etc.) to include in the generated file.
#
# deps (optional)
# [list, gn labels] These are the deps for the inputs named by path in the
# platform and product scopes, only.
#
# GN Usual:
# testonly
# visibility
#
template("product_assembly_config_file") {
# Platform configuration schema fields that are paths to files. These need to
# be tracked as GN action `inputs`.
#
# Each is a path to a 'node' in the schema, and then a list of field names on
# that node:
#
# platform: {
# foo: {
# bar: {
# field: "../../path/to/source/filename.json"
# }
# }
# }
#
# is the following:
#
# {
# node_path = "foo.bar"
# items = [ "field" ]
# }
config_file_potential_inputs = [
{
node_path = "platform.ui"
fields = [ "sensor_config" ]
},
{
node_path = "platform.development_support"
fields = [
"authorized_ssh_keys_path",
"authorized_ssh_ca_certs_path",
]
},
{
node_path = "product.component_policy"
fields = [ "product_policies" ]
is_list = true
},
{
node_path = "platform.diagnostics.sampler"
fields = [
"fire_configs",
"metrics_configs",
]
is_list = true
},
{
node_path = "platform.forensics.cobalt"
fields = [ "registry" ]
},
{
node_path = "platform.storage.component_id_index"
fields = [ "product_index" ]
},
{
node_path = "platform.connectivity.network"
fields = [ "netcfg_config_path" ]
},
{
node_path = "product.build_info"
fields = [
"version",
"jiri_snapshot",
"latest_commit_date",
"minimum_utc_stamp",
]
},
]
# These paths need to be present in the generated assembly configuration json
# file as _rebased_ paths. In the example above, the GN label in GN for that
# file is: `//path/to/source/filename.json`
#
# Assembly runs in a root_build_dir such as `//out/default`, and so the
# rebased path from the root_build_dir to the file is
# `../../path/to/source/filename.json`
#
# The GN label for the file needs to be passed to GN in the `inputs` list for
# the `target_name` action defined by this template (below).
#
# However, because GN doesn't allow us to set the value of a variable in a
# scope using the `scope_name[var_name]` syntax, only read it, we cannot do
# any sort of programmatic field replacement, at least not without hard-coded
# logic that's repeated for each field (which prior versions of this file did
# when there was only field to deal with), e.g.:
#
# _p = invoker.platform
#
# # platform configuration with rebased contents:
# _new_platform_config = {
# forward_variables_from(_p, "*", [ "foo" ])
# if (defined(_p.foo)) {
# _foo = _p.foo
#
# # 'foo' with rebased contents:
# foo = {
# forward_variables_from(_p.foo, "*", ["bar"])
#
# if (defined(_foo.bar)) {
# _bar = _foo.bar
#
# # 'bar' with rebased contents:
# bar = {
# forward_variables_from(_bar, "*", ["field"])
# if (defined(_bar.field)) {
# field = rebase_path(_bar.field, root_build_dir)
# }
# }
# }
# }
# }
# }
#
# The original invoker.platform scope would need to be carefully reconstructed
# at every level using this pattern of forwarding variables that weren't part
# of the newly-constructed ones, and then assigning the newly-constructed ones
# to the appropriate variables in each of the nested scopes.
# The assembly configuration
config_inputs = []
config_contents = invoker.product_assembly_config
# Iterate over each item in the list above.
foreach(potential_input, config_file_potential_inputs) {
_node = {
}
_node = config_contents
# Since we can't use recursion, iteratively change `_node` to the next
# child in the path, e.g. 'foo', then 'bar'. The `continue` var is used
# to signal that some node along the path has been omitted, and this will
# be a no-op for the rest of the iteration (since it can't exit early)
_continue = true
foreach(_node_name, string_split(potential_input.node_path, ".")) {
if (_continue && defined(_node[_node_name])) {
# because GN will not let you replace a non-empty scope, this does a
# little workaround to what would be the following, if it was allowed:
#
# _node = _node[_node_name]
#
# Instead, we need to create a _next_node temporary to hold the next
# node, but even that temporary lives across iterations of the loop,
# and must be reset to an empty scope before re-assigning to it.
#
# Clear the previous _next_node, and then cache the next node to look
# at in _next_node.
_next_node = {
}
_next_node = _node[_node_name]
# Clear the _node var, so that we can assign _next_node to it.
_node = {
}
_node = _next_node
} else {
# The node wasn't found, so do nothing for the rest of the iteration.
_continue = false
}
}
# 'continue' will still be true if the all the nodes in the path were
# found.
if (_continue) {
foreach(_field_name, potential_input.fields) {
if (defined(_node[_field_name])) {
_file_paths = []
if (defined(potential_input.is_list) && potential_input.is_list) {
_file_paths += _node[_field_name]
} else {
_file_paths += [ _node[_field_name] ]
}
# Here the rebased path is converted back in to an absolute GN
# file label, from the GN root dir `//`:
#
# ../../path/to/filename.json
#
# becomes:
#
# //path/to/filename.json
#
# and if a file in root_build_dir:
#
# path/to/filename.json
#
# it becomes (for `root_build_dir` == "//out/default"):
#
# //out/default/path/to/filename.json
#
# And the resultant (non-rebased) path can be used in the GN
# action's `inputs` list
foreach(_file_path, _file_paths) {
_gn_path = "//" + rebase_path(_file_path, "//", root_build_dir)
config_inputs += [ _gn_path ]
}
}
}
}
}
# Generate the Product Assembly Overrides file itself.
#
# This does _not_ have deps on any of the passed in targets, which is why the
# separate target for the inputs is created.
#
generated_file(target_name) {
forward_variables_from(invoker,
[
"outputs",
"testonly",
"visibility",
])
output_conversion = "json"
contents = config_contents
}
group_with_inputs(target_name + ".inputs") {
forward_variables_from(invoker,
[
"deps",
"public_deps",
"testonly",
"visibility",
])
# in the event that neither deps nor public_deps are passed in, then pass
# an empty deps list so the template doesn't throw an error.
if (!defined(deps)) {
deps = []
}
# Add any inputs found by looking at the known config schema items
# that contain paths to files, so that they are guaranteed to be added to
# the ninja deps for the product assembly configuration.
inputs = config_inputs
}
}