blob: 1e7b913525b1fa2aaacaf052f8c2d39db98d6dd2 [file] [log] [blame]
#!/usr/bin/env python3.8
# 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.
"""Make the legacy configuration set
Create an ImageAssembly configuration based on the GN-generated config, after
removing any other configuration sets from it.
"""
import argparse
import json
import os
import pathlib
import shutil
import sys
from typing import Any, Dict, List, Set, Tuple
from assembly import fast_copy, AssemblyInputBundle, BlobEntry, ConfigDataEntries
from assembly import FileEntry, FilePath, ImageAssemblyConfig, PackageManifest
from depfile import DepFile
from serialization import json_load, json_dump
# Some type annotations for clarity
PackageManifestList = List[FilePath]
Merkle = str
BlobList = List[Tuple[Merkle, FilePath]]
FileEntryList = List[FileEntry]
FileEntrySet = Set[FileEntry]
DepSet = Set[FilePath]
def copy_to_assembly_input_bundle(
config: ImageAssemblyConfig, config_data_entries: FileEntryList,
outdir: FilePath) -> Tuple[AssemblyInputBundle, DepSet]:
"""
Copy all the artifacts from the ImageAssemblyConfig into an AssemblyInputBundle that is in
outdir, tracking all copy operations in a DepFile that is returned with the resultant bundle.
Some notes on operation:
- <outdir> is removed and recreated anew when called.
- hardlinks are used for performance
- the return value contains a list of all files read/written by the
copying operation (ie. depfile contents)
"""
# Track all files we read
deps: DepSet = set()
# Init an empty resultant AssemblyInputBundle
result = AssemblyInputBundle()
# Copy over the boot args and zbi kernel args, unchanged, into the resultant
# assembly bundle
result.boot_args = config.boot_args
kernel_args = config.kernel.args
if kernel_args:
result.kernel.args = kernel_args
kernel_backstop = config.kernel.clock_backstop
if kernel_backstop:
result.kernel.clock_backstop = kernel_backstop
# Copy the manifests for the base package set into the assembly bundle
(base_pkgs, base_blobs, base_deps) = copy_packages(config, outdir, "base")
deps.update(base_deps)
result.base.update(base_pkgs)
# Strip any base pkgs from the cache set
config.cache = config.cache.difference(config.base)
# Copy the manifests for the cache package set into the assembly bundle
(cache_pkgs, cache_blobs,
cache_deps) = copy_packages(config, outdir, "cache")
deps.update(cache_deps)
result.cache.update(cache_pkgs)
# Copy the manifests for the system package set into the assembly bundle
(system_pkgs, system_blobs,
system_deps) = copy_packages(config, outdir, "system")
deps.update(system_deps)
result.system.update(system_pkgs)
# Copy the manifests for the bootfs package set into the assembly bundle
(bootfs_pkgs, bootfs_pkg_blobs,
bootfs_pkg_deps) = copy_packages(config, outdir, "bootfs_packages")
deps.update(bootfs_pkg_deps)
result.bootfs_packages.update(bootfs_pkgs)
# Deduplicate all blobs by merkle, but don't validate unique sources for
# each merkle, last one wins (we trust that in the in-tree build isn't going
# to make invalid merkles).
all_blobs = {}
for (merkle, source) in [*base_blobs, *cache_blobs, *system_blobs,
*bootfs_pkg_blobs]:
all_blobs[merkle] = source
# Copy all the blobs to their dir in the out-of-tree layout
(all_blobs, blob_deps) = copy_blobs(all_blobs, outdir)
deps.update(blob_deps)
result.blobs = set(
[os.path.relpath(blob_path, outdir) for blob_path in all_blobs])
# Copy the bootfs entries
(bootfs,
bootfs_deps) = copy_file_entries(config.bootfs_files, outdir, "bootfs")
deps.update(bootfs_deps)
result.bootfs_files.update(bootfs)
# Rebase the path to the kernel into the out-of-tree layout
kernel_src_path: Any = config.kernel.path
kernel_filename = os.path.basename(kernel_src_path)
kernel_dst_path = os.path.join("kernel", kernel_filename)
result.kernel.path = kernel_dst_path
# Copy the kernel itself into the out-of-tree layout
local_kernel_dst_path = os.path.join(outdir, kernel_dst_path)
os.makedirs(os.path.dirname(local_kernel_dst_path), exist_ok=True)
fast_copy(kernel_src_path, local_kernel_dst_path)
deps.add(kernel_src_path)
# Copy the config_data entries into the out-of-tree layout
(config_data,
config_data_deps) = copy_config_data_entries(config_data_entries, outdir)
deps.update(config_data_deps)
result.config_data = config_data
return (result, deps)
def copy_packages(
config: ImageAssemblyConfig, outdir: FilePath,
set_name: str) -> Tuple[PackageManifestList, BlobList, DepSet]:
"""Copy package manifests to the assembly bundle outdir, returning the set of blobs
that need to be copied as well (so that they blob copying can be done in a
single, deduplicated step.)
"""
package_manifests = getattr(config, set_name)
# Resultant paths to package manifests
packages = []
# All of the blobs to copy, deduplicated by merkle, and validated for
# conflicting sources.
blobs: BlobList = []
# The deps touched by this function.
deps: DepSet = set()
# Bail early if empty
if len(package_manifests) == 0:
return (packages, blobs, deps)
# Create the directory for the packages, now that we know it will exist
packages_dir = os.path.join("packages", set_name)
os.makedirs(os.path.join(outdir, packages_dir), exist_ok=True)
# Open each manifest, record the blobs, and then copy it to its destination,
# sorted by path to the package manifest.
for package_manifest_path in sorted(package_manifests):
with open(package_manifest_path, 'r') as file:
manifest = json_load(PackageManifest, file)
package_name = manifest.package.name
# Track in deps, since it was opened.
deps.add(package_manifest_path)
# But skip config-data, if we find it.
if "config-data" == package_name:
continue
# Instead of copying the package manifest itself, the contents of the
# manifest needs to be rewritten to reflect the new location of the
# blobs within it.
new_manifest = PackageManifest(manifest.package, [])
# For each blob in the manifest:
# 1) add it to set of all blobs
# 2) add it to the PackageManifest that will be written to the Assembly
# Input Bundle, using the correct source path for within the
# Assembly Input Bundle.
for blob in manifest.blobs:
source = blob.source_path
if source is None:
raise ValueError(
f"Found a blob with no source path: {package_name}::{blob.path} in {package_manifest_path}"
)
merkle = blob.merkle
blobs.append((merkle, source))
blob_destination = os.path.relpath(
make_internal_blob_path(blob.merkle), packages_dir)
new_manifest.blobs.append(
BlobEntry(
blob.path,
blob.merkle,
blob.size,
source_path=blob_destination))
new_manifest.set_blob_sources_relative(True)
# Write the new manifest to its destination within the input bundle.
rebased_destination = os.path.join(packages_dir, package_name)
package_manifest_destination = os.path.join(outdir, rebased_destination)
with open(package_manifest_destination, 'w') as new_manifest_file:
json_dump(new_manifest, new_manifest_file)
# Track the package manifest in our set of packages
packages.append(rebased_destination)
return (packages, blobs, deps)
def make_internal_blob_path(merkle: str) -> FilePath:
"""Common function to compute the destination path to a blob within the
AssemblyInputBundle's folder hierarchy.
"""
return os.path.join("blobs", merkle)
def copy_blobs(blobs: Dict[Merkle, FilePath],
outdir: FilePath) -> Tuple[List[FilePath], DepSet]:
blob_paths: List[FilePath] = []
deps: DepSet = set()
# Bail early if empty
if len(blobs) == 0:
return (blob_paths, deps)
# Create the directory for the blobs, now that we know it will exist.
blobs_dir = os.path.join(outdir, "blobs")
os.makedirs(blobs_dir)
# Copy all blobs
for (merkle, source) in blobs.items():
blob_destination = os.path.join(outdir, make_internal_blob_path(merkle))
blob_paths.append(blob_destination)
fast_copy(source, blob_destination)
deps.add(source)
return (blob_paths, deps)
def copy_file_entries(entries: FileEntrySet, outdir: FilePath,
set_name: str) -> Tuple[FileEntryList, DepSet]:
results: FileEntryList = []
deps: DepSet = set()
# Bail early if nothing to do
if len(entries) == 0:
return (results, deps)
for entry in entries:
rebased_destination = os.path.join(set_name, entry.destination)
copy_destination = os.path.join(outdir, rebased_destination)
# Create parents if they don't exist
os.makedirs(os.path.dirname(copy_destination), exist_ok=True)
# Hardlink the file from source to the destination, relative to the
# directory for all entries.
fast_copy(entry.source, copy_destination)
# Make a new FileEntry, which has a source of the path within the
# out-of-tree layout, and the same destination.
results.append(
FileEntry(
source_path=rebased_destination, dest_path=entry.destination))
# Add the copied file's source/destination to the set of touched files.
deps.add(entry.source)
return (results, deps)
def copy_config_data_entries(
entries: FileEntryList,
outdir: FilePath) -> Tuple[ConfigDataEntries, DepSet]:
"""
Take a list of entries for the config_data package, copy them into the
appropriate layout for the assembly input bundle, and then return the
config data entries and the set of DepEntries from the copying
This expects the entries to be destined for:
`meta/data/<package>/<path/to/file>`
and creates a ConfigDataEntries dict of PackageName:FileEntryList.
"""
results: ConfigDataEntries = {}
deps: DepSet = set()
if len(entries) == 0:
return (results, deps)
# Make a sorted list of a deduplicated set of the entries.
for entry in sorted(set(entries)):
# Crack the in-package path apart
#
# "meta" / "data" / package_name / path/to/file
parts = pathlib.Path(entry.destination).parts
if parts[:2] != ("meta", "data"):
raise ValueError(
"Found an unexpected destination path: {}".format(parts))
package_name = parts[2]
file_path = os.path.join(*parts[3:])
rebased_source_path = os.path.join(
"config_data", package_name, file_path)
copy_destination = os.path.join(outdir, rebased_source_path)
# Make any needed parents directories
os.makedirs(os.path.dirname(copy_destination), exist_ok=True)
# Hardlink the file from source to the destination
fast_copy(entry.source, copy_destination)
# Append the entry to the set of entries for the package
results.setdefault(package_name, set()).add(
FileEntry(source_path=rebased_source_path, dest_path=file_path))
# Add the copy operation to the depfile
deps.add(entry.source)
return (results, deps)
def main():
parser = argparse.ArgumentParser(
description=
"Create an image assembly configuration that is what remains after removing the configs to 'subtract'"
)
parser.add_argument(
"--image-assembly-config", type=argparse.FileType('r'), required=True)
parser.add_argument("--config-data-entries", type=argparse.FileType('r'))
parser.add_argument(
"--subtract", default=[], nargs="*", type=argparse.FileType('r'))
parser.add_argument("--outdir", required=True)
parser.add_argument("--depfile", type=argparse.FileType('w'))
parser.add_argument("--export-manifest", type=argparse.FileType('w'))
args = parser.parse_args()
# Remove the existing <outdir>, and recreate it
if os.path.exists(args.outdir):
shutil.rmtree(args.outdir)
os.makedirs(args.outdir)
# Read in the legacy config and the others to subtract from it
legacy: ImageAssemblyConfig = ImageAssemblyConfig.load(
args.image_assembly_config)
subtract = [ImageAssemblyConfig.load(other) for other in args.subtract]
# Subtract each from the legacy config, in the order given in args.
for other in subtract:
legacy = legacy.difference(other)
# Read in the config_data entries if available.
if args.config_data_entries:
config_data_entries = [
FileEntry.from_dict(entry)
for entry in json.load(args.config_data_entries)
]
else:
config_data_entries = []
assembly_config_manifest_path = os.path.join(
args.outdir, "assembly_config.json")
dep_file = DepFile(assembly_config_manifest_path)
# Copy the remaining contents to the out-of-tree Assembly Input Bundle layout
(assembly_input_bundle, deps) = copy_to_assembly_input_bundle(
legacy, config_data_entries, args.outdir)
dep_file.update(deps)
# Write out the resultant config into the outdir of the out-of-tree layout.
# the copy_set paths are all relative to cwd, so use the full cwd to the
# file as the dest_path, which will be rebased when generating the fini
# manifest later.
with open(assembly_config_manifest_path, 'w') as outfile:
assembly_input_bundle.write_to(outfile)
# 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:
assembly_input_bundle.write_fini_manifest(
args.export_manifest, base_dir=args.outdir)
# Write out a depfile.
if args.depfile:
dep_file.write_to(args.depfile)
if __name__ == "__main__":
sys.exit(main())