blob: 33b94d3bcd4515b66eb3d0b78b33cb66ee670c97 [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.
"""Python class for managing the copying of packages
Copies packages into a single directory structure, deduplicating the blobs into a single, combined
blobstore.
"""
import os
from serialization import json_dump, json_load
from .common import FileEntry, FilePath, fast_copy
from .package_manifest import BlobEntry, PackageManifest, SubpackageEntry
DepSet = set[FilePath]
Merkle = str
BlobSources = dict[Merkle, FilePath]
__all__ = [
"PackageCopier",
]
class DuplicatePackageException(Exception):
"""To be raised when an attempt is made to add multiple packages with the same name to the same
invocation of the PackageCopier"""
...
class PackageManifestParsingException(Exception):
"""To be raised when an attempt to parse a json file into a PackageManifest object fails"""
...
class PackageCopyingException(Exception):
"""To be raised when there is an exception copying a package."""
...
class PackageCopier:
"""Class for managing the copying of packages into a directory with a shared, combined, blobstore.
This creates the following directory structure under the destination outdir:
file layout:
./
packages/
<package name>
blobs/
<merkle>
subpackages/
<merkle>
Files matching the patterns `packages/*/<package name>` and
`subpackages/<merkle>` are JSON representations of the
`PackageManifest` type (see `package_manifest.py`). (The `<merkle>` in
`subpackages/` is the merkle of the subpackage's metafar.) A named package
that is also referenced as a subpackage will have a copy of its manifest in
both directories. (This is an intentional duplication to allow the
simplification of the creation and use of the AIBs.)
Each `PackageManifest` contains a `blobs` list of `BlobEntry` and an
optional `subpackages` list of `SubpackageEntry`. The `source_path` of a
`BlobEntry` references a file containing the blob bytes, in the `blobs/`
directory. The `manifest_path` of a `SubpackageEntry` references another
`PackageManifest` file in `subpackages/<merkle>`.
"""
def __init__(self, outdir: FilePath) -> None:
# The directory to create the AIB in. The manifest will be placed in
# the root of this dir.
self.outdir = outdir
# The internal paths, computed once for consistency
self.packages_dir = os.path.join(outdir, "packages")
self.subpackages_dir = os.path.join(outdir, "subpackages")
self.blobstore = os.path.join(outdir, "blobs")
# The package manifests:
self.manifests: dict[str, tuple[FileEntry, PackageManifest]] = {}
# The subpackages of each package
self.subpackages: dict[Merkle, tuple[FileEntry, PackageManifest]] = {}
# The files opened by the package copier
self.deps: DepSet = set()
def add_package(
self, package_manifest_path: FilePath
) -> tuple[FilePath, PackageManifest]:
"""Add a package manifest to the set of packages to copy.
Packages may not be fully copied until the 'perform_copy()' function is called.
"""
# Open and parse the package manifest.
self.deps.add(package_manifest_path)
if not isinstance(package_manifest_path, str):
raise ValueError(f"Found PackageDetails! {package_manifest_path}")
manifest = open_package(package_manifest_path)
package_name = manifest.package.name
if package_name in self.manifests:
raise DuplicatePackageException(
f"A package with the name '{package_name}' already exists."
)
# Cache the destination path, and the manifest itself to use when copying
paths = FileEntry(
package_manifest_path, os.path.join(self.packages_dir, package_name)
)
self.manifests[package_name] = (paths, manifest)
# Recursively find subpackages and add them to the set of packages to
self._add_subpackages(os.path.dirname(package_manifest_path), manifest)
# Return the future destination path, relative to the outdir, and the manifest itself, to
# the caller in case they need the path for their own manifests, or have further validation
# that they wish to perform on the data within the package manifest.
rebased_destination = os.path.relpath(paths.destination, self.outdir)
return (rebased_destination, manifest)
def _add_subpackages(
self, package_manifest_dir: FilePath, manifest: PackageManifest
) -> None:
for subpackage in manifest.subpackages:
if subpackage.merkle not in self.subpackages:
# It's a new subpackage, so process it
# Make the path relative to the package manifest if necessary.
subpackage_manifest_path = subpackage.manifest_path
if manifest.blob_sources_relative == "file":
subpackage_manifest_path = os.path.join(
package_manifest_dir, subpackage_manifest_path
)
# Parse the package manifest.
self.deps.add(subpackage_manifest_path)
subpackage_manifest = open_package(subpackage_manifest_path)
# Add it to the set of subpackages to copy.
subpackage_destination = os.path.join(
self.subpackages_dir, subpackage.merkle
)
self.subpackages[subpackage.merkle] = (
FileEntry(subpackage_manifest_path, subpackage_destination),
subpackage_manifest,
)
# And recurse into its own subpackages.
self._add_subpackages(
os.path.dirname(subpackage_manifest_path),
subpackage_manifest,
)
def perform_copy(self) -> tuple[list[FilePath], DepSet]:
"""Finalize the copy operation.
All actual copying may be deferred until this function is called.
"""
# The blobs to copy
blobs: BlobSources = {}
# The blob destination paths (relative to the outdir)
blob_paths: list[FilePath] = []
if self.manifests:
os.makedirs(self.packages_dir)
for paths, manifest in self.manifests.values():
(package_blobs, package_deps) = _copy_package(
paths, manifest, self.subpackages_dir, self.blobstore
)
blobs.update(package_blobs)
self.deps.update(package_deps)
# For historical reasons, the subpackages dir is always created, but the other two folders
# are only created when they have contents.
os.makedirs(self.subpackages_dir)
for paths, manifest in self.subpackages.values():
(package_blobs, package_deps) = _copy_package(
paths, manifest, self.subpackages_dir, self.blobstore
)
blobs.update(package_blobs)
self.deps.update(package_deps)
if blobs:
# Copy all blobs
os.makedirs(self.blobstore)
for merkle, source in blobs.items():
blob_destination = os.path.join(self.blobstore, merkle)
blob_paths.append(
os.path.relpath(blob_destination, self.outdir)
)
self.deps.add(fast_copy(source, blob_destination))
return (blob_paths, self.deps)
def open_package(package_manifest_path: FilePath) -> PackageManifest:
with open(package_manifest_path, "r") as file:
try:
manifest = json_load(PackageManifest, file)
except Exception as exc:
raise PackageManifestParsingException(
f"loading PackageManifest from {package_manifest_path}"
) from exc
return manifest
def _copy_package(
manifest_paths: FileEntry,
manifest: PackageManifest,
subpackages_dir: FilePath,
blobstore: FilePath,
) -> tuple[BlobSources, DepSet]:
"""Copy a package manifest to the assembly bundle outdir, adding its
blobs to the set of blobs that need to be copied as well. If the package
has subpackages, recursively copy those as well, skipping any
subpackages that have already been copied.
"""
blobs: BlobSources = {}
deps: DepSet = set()
# 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, [])
new_manifest.repository = manifest.repository
new_manifest.abi_revision = manifest.abi_revision
new_manifest.set_paths_relative(True)
# For each blob in the manifest:
# 1) add it to set of all blobs
# 2) add it to the PackageManifest that will be written, using the correct source path for
# internal blobstore.
for blob in manifest.blobs:
source = blob.source_path
if source is None:
raise ValueError(f"Found a blob with no source path: {blob.path}")
# Make the path relative to the package manifest if necessary.
if manifest.blob_sources_relative == "file":
source = os.path.join(
os.path.dirname(manifest_paths.source), source
)
blobs[blob.merkle] = source
blob_destination = os.path.join(blobstore, blob.merkle)
relative_blob_destination = os.path.relpath(
blob_destination, os.path.dirname(manifest_paths.destination)
)
new_manifest.blobs.append(
BlobEntry(
blob.path,
blob.merkle,
blob.size,
source_path=relative_blob_destination,
)
)
for subpackage in manifest.subpackages:
subpackage_destination = os.path.join(
subpackages_dir, subpackage.merkle
)
relative_subpackage_destination = os.path.relpath(
subpackage_destination, os.path.dirname(manifest_paths.destination)
)
new_manifest.subpackages.append(
SubpackageEntry(
subpackage.name,
subpackage.merkle,
manifest_path=relative_subpackage_destination,
)
)
with open(manifest_paths.destination, "w") as new_manifest_file:
json_dump(new_manifest, new_manifest_file)
return (blobs, deps)