blob: d5f36cbb4b443524a1b6af058876e2f9ebdb411e [file] [log] [blame]
#!/usr/bin/env fuchsia-vendored-python
# Copyright 2021 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 subprocess
import sys
from typing import Dict, List, Set, Tuple
import logging
from assembly.assembly_input_bundle import (
CompiledPackageAdditionalShards,
CompiledPackageMainDefinition,
)
from depfile import DepFile
from assembly import (
AIBCreator,
AssemblyInputBundle,
DriverDetails,
FileEntry,
FilePath,
PackageDetails,
PackageManifest,
)
from serialization.serialization import (
instance_from_dict,
json_dumps,
json_load,
)
logger = logging.getLogger()
BOOTFS_COMPILED_PACKAGE_ALLOWLIST = [
"bootstrap",
"fshost",
"for-test2",
] # test package
def create_bundle(args: argparse.Namespace) -> None:
"""Create an Assembly Input Bundle (AIB)."""
aib_creator = AIBCreator(args.outdir)
# Add the base and cache packages, if they exist.
if args.base_pkg_list:
add_pkg_list_from_file(aib_creator.packages, args.base_pkg_list, "base")
if args.cache_pkg_list:
add_pkg_list_from_file(
aib_creator.packages, args.cache_pkg_list, "cache"
)
if args.flexible_pkg_list:
add_pkg_list_from_file(
aib_creator.packages, args.flexible_pkg_list, "flexible"
)
if args.system_pkg_list:
add_pkg_list_from_file(
aib_creator.system, args.system_pkg_list, "system"
)
if args.bootfs_pkg_list:
add_pkg_list_from_file(
aib_creator.packages, args.bootfs_pkg_list, "bootfs"
)
if args.on_demand_pkg_list:
add_pkg_list_from_file(
aib_creator.packages, args.on_demand_pkg_list, "on_demand"
)
if args.shell_cmds_list:
add_shell_commands_from_file(aib_creator, args.shell_cmds_list)
if args.compiled_packages:
add_compiled_packages_from_file(aib_creator, args.compiled_packages)
if args.base_drivers_pkg_list:
add_driver_list_from_file(
aib_creator,
args.base_drivers_pkg_list,
aib_creator.provided_base_driver_details,
)
if args.boot_drivers_pkg_list:
add_driver_list_from_file(
aib_creator,
args.boot_drivers_pkg_list,
aib_creator.provided_boot_driver_details,
)
if args.config_data_list:
for config_data_entry_file in args.config_data_list:
with open(config_data_entry_file) as config_data_entry:
add_config_data_entries_from_file(
aib_creator, config_data_entry
)
if args.bootfs_files_package:
aib_creator.bootfs_files_package = args.bootfs_files_package
if args.bootfs_files_list:
for bootfs_files_entry_file in args.bootfs_files_list:
with open(bootfs_files_entry_file) as bootfs_files_entry:
add_bootfs_files_from_list(aib_creator, bootfs_files_entry)
if args.kernel_cmdline:
add_kernel_cmdline_from_file(aib_creator, args.kernel_cmdline)
# Add any bootloaders.
if args.qemu_kernel:
aib_creator.qemu_kernel = args.qemu_kernel
# Create the AIB itself.
(assembly_input_bundle, assembly_config, deps) = aib_creator.build()
# Write out a dep file if one is requested.
if args.depfile:
with open(args.depfile, "w") as depfile:
DepFile.from_deps(assembly_config, deps).write_to(depfile)
# Write out a fini manifest of the files that have been copied, to create a
# package or archive that contains all of the files in the bundle.
if args.export_manifest:
with open(args.export_manifest, "w") as export_manifest:
assembly_input_bundle.write_fini_manifest(
export_manifest, base_dir=args.outdir
)
def add_pkg_list_from_file(
pkg_set: List[PackageDetails], pkg_list_file, pkg_set_name: str
):
pkg_list = _read_json_file(pkg_list_file)
for package in [PackageDetails(m, pkg_set_name) for m in pkg_list]:
if package in pkg_set:
raise ValueError(f"duplicate pkg manifest found: {package.package}")
pkg_set.add(package)
def add_kernel_cmdline_from_file(aib_creator: AIBCreator, kernel_cmdline_file):
cmdline_list = _read_json_file(kernel_cmdline_file)
for cmd in cmdline_list:
if cmd in aib_creator.kernel.args:
raise ValueError(f"duplicate kernel cmdline arg found: {cmd}")
aib_creator.kernel.args.add(cmd)
def add_driver_list_from_file(
aib_creator: AIBCreator, driver_list_file, driver_list
):
# cross-check the base and bootfs_package sets for the driver before adding
# it to the target driver_list.
driver_details_list = _read_json_file(driver_list_file)
for driver_details in driver_details_list:
if driver_details["package_target"] in aib_creator.packages:
raise ValueError(
f"duplicate pkg manifest found: {driver_details['package_target']}"
)
driver_list.append(
DriverDetails(
driver_details["package_target"],
driver_details["driver_components"],
)
)
def add_shell_commands_from_file(
aib_creator: AIBCreator, shell_commands_list_file
):
"""
[
{
"components": [
"ls"
],
"package": "ls"
}
]
"""
loaded_file = _read_json_file(shell_commands_list_file)
for command in loaded_file:
package = command["package"]
components = command["components"]
aib_creator.shell_commands[package].extend(
["bin/" + component for component in components]
)
def add_config_data_entries_from_file(
aib_creator: AIBCreator, config_data_entries
):
"""
config_data_entries schema:
[
{
'package_name': 'example_package',
'destination': 'foo.txt',
'source': 'src/sys/example/configs/example.json'
}
]
"""
_config_data = _read_json_file(config_data_entries)
for definition in _config_data:
entry = FileEntry(
definition["source"],
f"meta/data/{definition['package_name']}/{definition['destination']}",
)
aib_creator.config_data.append(entry)
def add_compiled_packages_from_file(aib_creator: AIBCreator, compiled_packages):
"""
compiled_packages should be
List[CompiledPackageMainDefinition|CompiledPackageAdditionalShards]
"""
_compiled_packages = _read_json_file(compiled_packages)
for package_def in _compiled_packages:
# Differentiate entries by field names
if "component_shards" in package_def:
# Transform from the GN format to the internal format
shards = {}
for definition in package_def["component_shards"]:
if definition["component_name"] in shards:
raise ValueError(
f"Unexpected repeated component name: {definition['component_name']}"
)
shards[definition["component_name"]] = definition["shards"]
package_def["component_shards"] = shards
aib_creator.compiled_package_shards.append(
instance_from_dict(CompiledPackageAdditionalShards, package_def)
)
else:
shards = {}
for definition in package_def["components"]:
if definition["component_name"] in shards:
raise ValueError(
f"Unexpected repeated component name: {definition['component_name']}"
)
shards[definition["component_name"]] = definition["shards"]
package_def["components"] = shards
main_def = instance_from_dict(
CompiledPackageMainDefinition, package_def
)
# Type unsafe; see assembly_input_bundle.py
if package_def.get("component_includes"):
main_def.includes = [
instance_from_dict(FileEntry, x)
for x in package_def["component_includes"]
]
if (
main_def.bootfs_package
and main_def.name not in BOOTFS_COMPILED_PACKAGE_ALLOWLIST
):
raise ValueError(
f"Compiled package {main_def.name} not in bootfs allowlist!"
)
aib_creator.compiled_packages.append(main_def)
def add_bootfs_files_from_list(aib_creator: AIBCreator, bootfs_files):
"""
bootfs_files schema:
[
{
'destination': 'bin/bar',
'source': 'src/sys/example/configs/example.json'
}
]
"""
_bootfs_files = _read_json_file(bootfs_files)
for entry in _bootfs_files:
# Not all distribution manifests have the source and destination pairs.
# For an example see: dart_kernel.gni
if "source" in entry and "destination" in entry:
aib_creator.bootfs_files.add(
FileEntry(entry["source"], entry["destination"])
)
def _read_json_file(pkg_list_file):
try:
return json.load(pkg_list_file)
except:
logger.exception(f"While parsing {pkg_list_file.name}")
raise
def generate_package_creation_manifest(args: argparse.Namespace) -> None:
"""Generate a package creation manifest for an Assembly Input Bundle (AIB)
Each AIB has a contents manifest that was created with it. This file lists
all of the files in the AIB, and their path within the build dir::
AIB/path/to/file_1=outdir/path/to/AIB/path/to/file_1
AIB/path/to/file_2=outdir/path/to/AIB/path/to/file_2
This format locates all the files in the AIB, relative to the
root_build_dir, and then gives their destination path within the AIB package
and archive.
To generate the package the AIB, a creation manifest is required (also in
FINI format). This is the same file, with the addition of the path to the
package metadata file::
meta/package=path/to/metadata/file
This fn generates the package metadata file, and then generates the creation
manifest file by appending the path to the metadata file to the entries in
the AIB contents manifest.
"""
meta_package_content = {"name": args.name, "version": "0"}
with open(args.meta_package, "w") as meta_package:
json.dump(meta_package_content, meta_package)
contents_manifest = args.contents_manifest.read()
with open(args.output, "w") as output:
output.write(contents_manifest)
output.write("meta/package={}".format(args.meta_package))
def generate_archive(args: argparse.Namespace) -> None:
"""Generate an archive of an Assembly Input Bundle (AIB)
Each AIB has a contents manifest that was created with it. This file lists
all of the files in the AIB, and their path within the build dir::
AIB/path/to/file_1=outdir/path/to/AIB/path/to/file_1
AIB/path/to/file_2=outdir/path/to/AIB/path/to/file_2
This format locates all the files in the AIB, relative to the
root_build_dir, and then gives their destination path within the AIB package
and archive.
To generate the archive of the AIB, a creation manifest is required (also in
FINI format). This is the same file, with the addition of the path to the
package meta.far.
meta.far=path/to/meta.far
This fn generates the creation manifest, appending the package meta.far to
the contents manifest, and then calling the tarmaker tool to build the
archive itself, using the generated creation manifest.
"""
deps: Set[str] = set()
# Read the AIB's contents manifest, all of these files will be added to the
# creation manifest for the archive.
contents_manifest = args.contents_manifest.readlines()
deps.add(args.contents_manifest.name)
with open(args.creation_manifest, "w") as creation_manifest:
if args.meta_far:
# Add the AIB's package meta.far to the creation manifest if one was
# provided.
creation_manifest.write("meta.far={}\n".format(args.meta_far))
# Add all files from the AIB's contents manifest.
for line in contents_manifest:
# Split out the lines so that a depfile for the action can be made
# from the contents_manifest's source paths.
src = line.split("=", 1)[1]
deps.add(src.strip())
creation_manifest.write(line)
# Build the archive itself.
cmd_args = [
args.tarmaker,
"-manifest",
args.creation_manifest,
"-output",
args.output,
]
subprocess.run(cmd_args, check=True)
if args.depfile:
with open(args.depfile, "w") as depfile:
DepFile.from_deps(args.output, deps).write_to(depfile)
def diff_bundles(args: argparse.Namespace) -> None:
first = AssemblyInputBundle.json_load(args.first)
second = AssemblyInputBundle.json_load(args.second)
result = first.difference(second)
if args.output:
with open(args.output, "w") as output:
result.json_dump(output)
else:
print(result)
def intersect_bundles(args: argparse.Namespace) -> None:
bundles = [AssemblyInputBundle.json_load(file) for file in args.bundles]
result = bundles[0]
for next_bundle in bundles[1:]:
result = result.intersection(next_bundle)
if args.output:
with open(args.output, "w") as output:
result.json_dump(output)
else:
print(result)
def find_blob(args: argparse.Namespace) -> None:
bundle = AssemblyInputBundle.json_load(args.bundle_config)
bundle_dir = os.path.dirname(args.bundle_config.name)
manifests: List[FilePath] = [*bundle.base, *bundle.cache, *bundle.system]
found_at = find_blob_in_manifests(args.blob, bundle_dir, manifests)
if found_at:
pkg_header = "Package"
path_header = "Path"
pkg_column_width = max(
len(pkg_header), *[len(entry[0]) for entry in found_at]
)
path_column_width = max(
len(path_header), *[len(entry[1]) for entry in found_at]
)
formatter = f"{{0: <{pkg_column_width}}} | {{1: <{path_column_width}}}"
header = formatter.format(pkg_header, path_header)
print(header)
print("=" * len(header))
for pkg, path in found_at:
print(formatter.format(pkg, path))
def find_blob_in_manifests(
blob_to_find: str, bundle_dir: str, manifests_to_search: List[FilePath]
) -> List[Tuple[FilePath, FilePath]]:
found_at: List[Tuple[FilePath, FilePath]] = []
known_manifests = set(manifests_to_search)
i = 0
while i < len(manifests_to_search):
pkg_manifest_path = manifests_to_search[i]
i += 1
with open(
os.path.join(bundle_dir, pkg_manifest_path), "r"
) as pkg_manifest_file:
manifest = json_load(PackageManifest, pkg_manifest_file)
if not manifest.blob_sources_relative:
raise ValueError(
f"Unexpected non-relative paths in AIB package manifest: {pkg_manifest_path}"
)
for blob in manifest.blobs:
if blob.merkle == blob_to_find:
found_at.append((pkg_manifest_path, blob.path))
for subpackage in manifest.subpackages:
subpackage_manifest_path = os.path.join(
os.path.dirname(pkg_manifest_path), subpackage.manifest_path
)
# remove `<dir>/../` sequences if present
subpackage_manifest_path = os.path.relpath(
subpackage_manifest_path
)
if subpackage_manifest_path not in known_manifests:
manifests_to_search.append(subpackage_manifest_path)
known_manifests.add(subpackage_manifest_path)
return found_at
def main():
parser = argparse.ArgumentParser(
description="Tool for creating Assembly Input Bundles in-tree, for use with out-of-tree assembly"
)
sub_parsers = parser.add_subparsers(
title="Commands",
description="Commands for working with Assembly Input Bundles",
)
###
#
# 'assembly_input_bundle_tool create' subcommand parser
#
bundle_creation_parser = sub_parsers.add_parser(
"create", help="Create an Assembly Input Bundle"
)
bundle_creation_parser.add_argument(
"--outdir",
required=True,
help="Path to the outdir that will contain the AIB",
)
bundle_creation_parser.add_argument(
"--base-pkg-list",
type=argparse.FileType("r"),
help="Path to a json list of package manifests for the 'base' package set",
)
bundle_creation_parser.add_argument(
"--bootfs-pkg-list",
type=argparse.FileType("r"),
help="Path to a json list of package manifests for the 'bootfs' package set",
)
bundle_creation_parser.add_argument(
"--on-demand-pkg-list",
type=argparse.FileType("r"),
help="Path to a json list of package manifests for the 'on-demand' package set",
)
bundle_creation_parser.add_argument(
"--boot-drivers-pkg-list",
type=argparse.FileType("r"),
help="Path to a json list of driver details for the 'bootfs' package set",
)
bundle_creation_parser.add_argument(
"--base-drivers-pkg-list",
type=argparse.FileType("r"),
help="Path to a json list of driver details for the 'base' package set",
)
bundle_creation_parser.add_argument(
"--shell-cmds-list",
type=argparse.FileType("r"),
help="Path to a json list of dictionaries with the manifest path as key and a list of shell_command components as the value",
)
bundle_creation_parser.add_argument(
"--cache-pkg-list",
type=argparse.FileType("r"),
help="Path to a json list of package manifests for the 'cache' package set",
)
bundle_creation_parser.add_argument(
"--flexible-pkg-list",
type=argparse.FileType("r"),
help="Path to a json list of package manifests for the 'flexible' package set",
)
bundle_creation_parser.add_argument(
"--system-pkg-list",
type=argparse.FileType("r"),
help="Path to a json list of package manifests for the 'system' package set",
)
bundle_creation_parser.add_argument(
"--kernel-cmdline",
type=argparse.FileType("r"),
help="Path to a json list of kernel cmdline arguments",
)
bundle_creation_parser.add_argument(
"--qemu-kernel", help="Path to the qemu kernel"
)
bundle_creation_parser.add_argument(
"--depfile",
help="Path to write a dependency file to",
)
bundle_creation_parser.add_argument(
"--export-manifest",
help="Path to write a FINI manifest of the contents of the AIB",
)
bundle_creation_parser.add_argument(
"--config-data-list",
action="append",
help="Path to a json file of config-data entries, may be specified multiple times",
)
bundle_creation_parser.add_argument(
"--bootfs-files-package",
help="Path to a package manifest that points to files to include in bootfs",
)
bundle_creation_parser.add_argument(
"--bootfs-files-list",
action="append",
help="Path to a json file of bootfs-file entries, may be specified multiple times",
)
bundle_creation_parser.add_argument(
"--compiled-packages",
type=argparse.FileType("r"),
help="Path to a json file of compiled package configuration",
)
bundle_creation_parser.set_defaults(handler=create_bundle)
###
#
# 'assembly_input_bundle_tool diff' subcommand parser
#
diff_bundles_parser = sub_parsers.add_parser(
"diff",
help="Calculate the difference between the first and second bundles (A-B).",
)
diff_bundles_parser.add_argument(
"first", help="The first bundle (A)", type=argparse.FileType("r")
)
diff_bundles_parser.add_argument(
"second", help="The second bundle (B)", type=argparse.FileType("r")
)
diff_bundles_parser.add_argument(
"--output",
help="A file to write the output to, instead of stdout.",
)
diff_bundles_parser.set_defaults(handler=diff_bundles)
###
#
# 'assembly_input_bundle_tool intersect' subcommand parser
#
intersect_bundles_parser = sub_parsers.add_parser(
"intersect", help="Calculate the intersection of the provided bundles."
)
intersect_bundles_parser.add_argument(
"bundles",
nargs="+",
action="extend",
help="Paths to the bundle configs.",
type=argparse.FileType("r"),
)
intersect_bundles_parser.add_argument(
"--output",
help="A file to write the output to, instead of stdout.",
)
intersect_bundles_parser.set_defaults(handler=intersect_bundles)
###
#
# 'assembly_input_bundle_tool generate-package-creation-manifest' subcommand
# parser
#
package_creation_manifest_parser = sub_parsers.add_parser(
"generate-package-creation-manifest",
help="(build tool) Generate the creation manifest for the package that contains an Assembly Input Bundle.",
)
package_creation_manifest_parser.add_argument(
"--contents-manifest", type=argparse.FileType("r"), required=True
)
package_creation_manifest_parser.add_argument("--name", required=True)
package_creation_manifest_parser.add_argument(
"--meta-package", required=True
)
package_creation_manifest_parser.add_argument("--output", required=True)
package_creation_manifest_parser.set_defaults(
handler=generate_package_creation_manifest
)
###
#
# 'assembly_input_bundle_tool generate-archive' subcommand parser
#
archive_creation_parser = sub_parsers.add_parser(
"generate-archive",
help="(build tool) Generate the tarmaker creation manifest for the tgz that contains an Assembly Input Bundle.",
)
archive_creation_parser.add_argument("--tarmaker", required=True)
archive_creation_parser.add_argument(
"--contents-manifest", type=argparse.FileType("r"), required=True
)
archive_creation_parser.add_argument("--meta-far")
archive_creation_parser.add_argument("--creation-manifest", required=True)
archive_creation_parser.add_argument("--output", required=True)
archive_creation_parser.add_argument("--depfile")
archive_creation_parser.set_defaults(handler=generate_archive)
###
#
# 'assembly_input_bundle_tool find-blob' subcommand parser
#
find_blob_parser = sub_parsers.add_parser(
"find-blob",
help="Find what causes a blob to be included in the Assembly Input Bundle.",
)
find_blob_parser.add_argument(
"--bundle-config",
required=True,
type=argparse.FileType("r"),
help="Path to the assembly_config.json for the bundle",
)
find_blob_parser.add_argument(
"--blob", required=True, help="Merkle of the blob to search for."
)
find_blob_parser.set_defaults(handler=find_blob)
args = parser.parse_args()
if "handler" in args:
# Dispatch to the handler fn.
args.handler(args)
else:
# argparse doesn't seem to automatically catch that not subparser was
# called, and so if there isn't a handler function (which is set by
# having specified a subcommand), then just display usage instead of
# a cryptic KeyError.
parser.print_help()
if __name__ == "__main__":
sys.exit(main())