blob: fa38276b384a430dcd5f42c864934e1f78d2bae5 [file] [log] [blame] [edit]
#!/usr/bin/env fuchsia-vendored-python
# 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 argparse
import json
import os
import shutil
import sys
from dataclasses import dataclass, field
from typing import Optional
from assembly import FilePath, PackageCopier, PackageDetails, fast_copy_makedirs
from assembly.assembly_input_bundle import (
CompiledComponentDefinition,
CompiledPackageDefinition,
DepSet,
)
from depfile import DepFile
from serialization import instance_from_dict, json_dump, json_load
# See //src/lib/assembly/config_schema/src/developer_overrides.rs for documentation.
# These must be kept in sync with that file.
@dataclass
class ShellCommandEntryFromGN:
package: str
components: list[str]
@dataclass
class DeveloperOverridesFromGN:
"""This is the schema used to parse the developer overrides that are written by the GN template."""
target_name: Optional[str]
# The following are opaque dictionaries to this script, and don't need to be specified in any
# further detail, because they are written out just as they are read in.
developer_only_options: dict = field(default_factory=dict) # type: ignore
kernel: dict = field(default_factory=dict) # type: ignore
platform: dict = field(default_factory=dict) # type: ignore
product: dict = field(default_factory=dict) # type: ignore
board: dict = field(default_factory=dict) # type: ignore
bootfs_files_package: Optional[str] = field(default=None)
# Packages we need to copy, so we'll need real types for those
packages: list[PackageDetails] = field(default_factory=list)
packages_to_compile: list[CompiledPackageDefinition] = field(
default_factory=list
)
# The type that's deserialized from what GN writes is different from that which will be written
# out for Assembly to use.
shell_commands: list[ShellCommandEntryFromGN] = field(default_factory=list)
ShellCommandsForAssembly = dict[str, list[str]]
@dataclass
class DeveloperProvidedFileEntryFromGN:
field: str
path: str
@dataclass
class DeveloperProvidedFilesNodeFromGN:
node_path: str
fields: list[DeveloperProvidedFileEntryFromGN]
@dataclass
class DeveloperProvidedFilesNodeForAssembly:
node_path: str
fields: dict[str, str]
@dataclass
class DeveloperOverridesForAssembly:
"""This is the schema used to write out the overrides file for Assembly to use."""
target_name: Optional[str]
# The following are opaque dictionaries to this script, and don't need to be specified in any
# further detail, because they are written out just as they are read in.
developer_only_options: dict = field(default_factory=dict) # type: ignore
kernel: dict = field(default_factory=dict) # type: ignore
platform: dict = field(default_factory=dict) # type: ignore
product: dict = field(default_factory=dict) # type: ignore
board: dict = field(default_factory=dict) # type: ignore
bootfs_files_package: Optional[str] = field(default=None)
# Packages we need to copy, so we'll need real types for those
packages: list[PackageDetails] = field(default_factory=list)
packages_to_compile: list[CompiledPackageDefinition] = field(
default_factory=list
)
# The type that's written out for Assembly to use is different from that which is read from GN.
shell_commands: ShellCommandsForAssembly = field(default_factory=dict)
# A mapping of all files found in the platform and product types that are being copied and need
# to be tracked as relative to this file.
developer_provided_files: list[
DeveloperProvidedFilesNodeForAssembly
] = field(default_factory=list)
def main() -> int:
parser = argparse.ArgumentParser(
description="Tool for creating the file for Assembly developer overrides in-tree"
)
parser.add_argument(
"--input",
required=True,
type=argparse.FileType("r"),
help="Path to a json file containing the intermediate assembly developer overrides",
)
parser.add_argument(
"--input-file-paths",
type=argparse.FileType("r"),
help="Path to a json file containing a list of input files listed in the intermediate file",
)
parser.add_argument(
"--outdir",
required=True,
help="Path to the output dir that will contain the developer overrides",
)
parser.add_argument(
"--depfile",
help="Path to an optional depfile to write of all files used to construct the developer overrides",
)
args = parser.parse_args()
deps: DepSet = set()
# Remove the existing <outdir>, and recreate it and the "subpackages"
# subdirectory.
if os.path.exists(args.outdir):
shutil.rmtree(args.outdir)
os.makedirs(args.outdir)
overrides_from_gn = json_load(DeveloperOverridesFromGN, args.input)
deps.add(args.input.name)
# Prep the result.
overrides_for_assembly = DeveloperOverridesForAssembly(
overrides_from_gn.target_name,
developer_only_options=overrides_from_gn.developer_only_options,
kernel=overrides_from_gn.kernel,
platform=overrides_from_gn.platform,
product=overrides_from_gn.product,
board=overrides_from_gn.board,
bootfs_files_package=overrides_from_gn.bootfs_files_package,
)
overrides_for_assembly.shell_commands = {}
for shell_entry in overrides_from_gn.shell_commands:
overrides_for_assembly.shell_commands[shell_entry.package] = [
f"bin/{name}" for name in shell_entry.components
]
if overrides_from_gn.packages:
# Set up the package copier to copy all the packages
package_copier = PackageCopier(args.outdir)
# For each package details entry from GN, add the package to the set of packages to copy
# and then create a new package details entry for assembly that uses the new path of the
# copied package.
for package_entry in overrides_from_gn.packages:
destination_path, _ = package_copier.add_package(
package_entry.package
)
overrides_for_assembly.packages.append(
PackageDetails(destination_path, package_entry.set)
)
_, copy_deps = package_copier.perform_copy()
deps.update(copy_deps)
# TODO(https://fxbug.dev/406838880) - Refactor this to use the same mechanisms in
# assembly_input_bundle.py.
if overrides_from_gn.packages_to_compile:
packages_to_compile: list[CompiledPackageDefinition] = []
for package in overrides_from_gn.packages_to_compile:
if package.contents:
raise ValueError(
"\nExtra package contents for compiled_packages are not supported at this time.\n"
)
if package.includes:
raise ValueError(
"\nExtra component includes for compiled_packages are not supported at this time.\n"
)
components: list[CompiledComponentDefinition] = []
for component in package.components:
shards: set[FilePath] = set()
for shard in component.shards:
dest = os.path.join(
args.outdir,
"compiled_packages",
package.name,
component.component_name,
os.path.basename(shard),
)
deps.add(shard)
fast_copy_makedirs(shard, dest)
shards.add(os.path.relpath(dest, args.outdir))
components.append(
CompiledComponentDefinition(
component.component_name, shards
)
)
packages_to_compile.append(
CompiledPackageDefinition(
package.name,
components,
bootfs_package=package.bootfs_package,
)
)
overrides_for_assembly.packages_to_compile = packages_to_compile
outfile_path = os.path.join(args.outdir, "product_assembly_overrides.json")
# There are potentially a few file paths listed in the overrides. They need to be copied into
# the outdir, and then the references to them in the json override values removed as they are
# separately tracked so that they can be resolved based on the path to the developer overrides
# file (and directory of associated resources).
input_file_path_entries = []
if args.input_file_paths:
input_file_path_entries = json.load(args.input_file_paths)
# Copy the files to a pair of resources dirs:
for raw_entry in input_file_path_entries:
# The input_file_path_entries is a list of dicts, as the serialization library doesn't
# want to deserialize a 'list[Foo]'.
#
# So here the list of dicts parsed from json above is individually deserialized into
# the appropriate class.
entry = instance_from_dict(DeveloperProvidedFilesNodeFromGN, raw_entry)
# This is the 'fields' map that contains the new (copied-to) paths for the files in the
# developer overrides.
fields_for_assembly = {}
for field_entry in entry.fields:
input_file = field_entry.path
# This structures the files under 'resources' in the same structure that they have
# in the build dir, except that source-files and outputs from actions will have
# different root dirs.
if input_file.startswith("../../"):
# It's a source file, so strip the leading "../../" and put it in:
# resources/sources/path/to/file
new_relative_path = os.path.join(
"resources", "sources", input_file[6:]
)
else:
# It's the output of another action, or a generated file, so put it in
# resources/path/to/file
new_relative_path = os.path.join("resources", input_file)
# Copy the file
fast_copy_makedirs(
input_file, os.path.join(args.outdir, new_relative_path)
)
# Adding the source path to the depfile
deps.add(input_file)
# And then add it to the map of fields with files
fields_for_assembly[field_entry.field] = new_relative_path
# Add the fields for this node to the overrides for assembly struct.
overrides_for_assembly.developer_provided_files.append(
DeveloperProvidedFilesNodeForAssembly(
entry.node_path, fields_for_assembly
)
)
# Strip the file from the main part of the developer-overrides (as GN will have written them
# to the platform and product overrides config values.
# Start by getting the 'platform', 'product', etc. field from the struct. As this isn't a
# dict, but a struct, getaddr is used.
path_elements = entry.node_path.split(".")
starting_node_name = path_elements[0]
starting_node: dict[str, Any] = getattr(overrides_for_assembly, starting_node_name, None) # type: ignore
if not starting_node:
raise ValueError(f"Unknown field: {starting_node_name}")
# Get the dict for this node, iterating through the path to get the child dicts.
current_node = starting_node
for node_name in path_elements[1:]:
if not node_name in current_node:
raise ValueError(
f"Unable to locate node {node_name} from {path_elements} in {starting_node_name}={starting_node}"
)
current_node: dict[str, Any] = current_node[node_name] # type: ignore
# Remove all the fields that have fields from the node.
for field_entry in entry.fields:
if field_entry.field not in current_node:
raise ValueError(
f"Unable to locate field {field_entry.field} at {path_elements} in {starting_node_name}={starting_node}"
)
current_node.pop(field_entry.field)
# Write out the depfile.
if args.depfile:
with open(args.depfile, "w") as depfile:
DepFile.from_deps(outfile_path, deps).write_to(depfile)
# And write out the output for assembly.
with open(outfile_path, "w") as output:
json_dump(overrides_for_assembly, output, indent=2)
return 0
if __name__ == "__main__":
sys.exit(main())