diff --git a/image/compose.bzl b/image/compose.bzl index 1a31398c..4713ed92 100644 --- a/image/compose.bzl +++ b/image/compose.bzl @@ -422,6 +422,7 @@ def _disk_impl(ctx: AnalysisContext) -> list[Provider]: mkfs_options = ctx.attrs.mkfs_options, seed = ctx.attrs.disk_seed, split = True, + strip_paths = ctx.attrs.strip_paths, strip_pkgdb = ctx.attrs.strip_pkgdb, verity_key = resolve_signing_key(ctx.attrs.verity_key), ) @@ -508,6 +509,7 @@ def _disk_impl(ctx: AnalysisContext) -> list[Provider]: mkfs_options = ctx.attrs.mkfs_options, seed = ctx.attrs.disk_seed, split = True, + strip_paths = ctx.attrs.strip_paths, strip_pkgdb = ctx.attrs.strip_pkgdb, verity_key = resolve_signing_key(ctx.attrs.verity_key), virtual_size = ctx.attrs.virtual_size, diff --git a/image/ddi.bzl b/image/ddi.bzl index f9bcbd41..e56bbb49 100644 --- a/image/ddi.bzl +++ b/image/ddi.bzl @@ -282,6 +282,7 @@ def declare_ddi( seed: str | None = None, verity_key: SigningKeyInfo | None = None, strip_pkgdb: bool = False, + strip_paths: list[str] = [], mkfs_options: dict[str, list[str]] = {}, basename: str = "image", identifier: str | None = None, @@ -294,10 +295,11 @@ def declare_ddi( With strip_pkgdb, the partitions omit the package database wherever the image's package system keeps it, for a system nothing ever resolves packages in. The image's `[pkgdb]` and `[sbom]` - subtargets still capture it from the tree itself. `extension` makes the driver package the tree - as an extension (see extension.bzl). `tmpfiles` replaces the tmpfiles lines of `image`. The - partitions leave out the contents of the directories in `exclude`. `virtual_size` goes into the - provider, see DdiInfo. + subtargets still capture it from the tree itself. `strip_paths` drops further files by glob, for + what the partitions do not need but a sibling target reading the same layer still does. + `extension` makes the driver package the tree as an extension (see extension.bzl). `tmpfiles` + replaces the tmpfiles lines of `image`. The partitions leave out the contents of the directories + in `exclude`. `virtual_size` goes into the provider, see DdiInfo. """ _check_definitions( definitions, @@ -400,6 +402,7 @@ def declare_ddi( "root_hash_out": None, "seed": seed, "signing": signing_key_spec(verity_key), + "strip_paths": strip_paths, } if tmpfiles != None: @@ -481,6 +484,11 @@ DDI_ATTRS = { default = {}, doc = "filesystem -> the options mkfs is given when formatting a partition of that type", ), + "strip_paths": attrs.list( + attrs.string(), + default = [], + doc = "globs, relative to the image root, to leave out of the partitions; the image still carries them", + ), "strip_pkgdb": attrs.bool( default = False, doc = "leave the package database out of the partitions; the image still carries it", diff --git a/image/ddi.py b/image/ddi.py index feedc9ab..244ff3ba 100755 --- a/image/ddi.py +++ b/image/ddi.py @@ -122,6 +122,8 @@ class Spec(finalize.ImageSpec): root_hash_out: str | None # Image paths holding the package database, stripped from the partitions. pkgdb_paths: list[str] + # Globs, relative to the image root, of further files to strip from the partitions. + strip_paths: list[str] # Image directories whose contents the partitions leave out. The directories stay, empty. exclude: list[str] # Where to list what the composed disk carries, its partitions taken together. @@ -318,6 +320,14 @@ def empty_directories(tree: Path, directories: list[str]) -> None: util.remove_path(child) +def strip_files(tree: Path, patterns: list[str]) -> None: + """Remove everything below `tree` that one of the globs in `patterns` matches.""" + for pattern in patterns: + # Sorted so a failure reports the same path every run, whatever order the tree is read in. + for path in sorted(tree.glob(pattern.lstrip("/"))): + util.remove_path(path) + + def _write_definitions( directory: Path, written: list[WrittenPartition], @@ -479,6 +489,7 @@ def main(argv: list[str] | None = None) -> None: # subtargets capture from the tree, so partitions nothing resolves packages in can drop it. for relative in spec["pkgdb_paths"]: util.remove_path(tree / relative, with_parents=True) + strip_files(tree, spec["strip_paths"]) empty_directories(tree, spec["exclude"]) if extension: diff --git a/image/ddi_test.py b/image/ddi_test.py index 9bd049c3..ebfec7a9 100644 --- a/image/ddi_test.py +++ b/image/ddi_test.py @@ -185,5 +185,44 @@ def test_removes_the_contents_and_keeps_the_directory(self) -> None: self.assertEqual((tree / "usr/os-release").read_text(), "ID=test") +class TestStripFiles(unittest.TestCase): + def _tree(self, scratch_dir: str) -> Path: + tree = Path(scratch_dir) + for version in ("6.17.0", "6.18.0"): + modules = tree / "usr/lib/modules" / version + (modules / "kernel/fs").mkdir(parents=True) + (modules / "vmlinuz").write_text("kernel") + (modules / "modules.dep").write_text("deps") + (modules / "kernel/fs/xfs.ko").write_text("module") + return tree + + def test_removes_every_match_and_nothing_else(self) -> None: + with tempfile.TemporaryDirectory(prefix="ddi_test.") as scratch_dir: + tree = self._tree(scratch_dir) + ddi.strip_files(tree, ["usr/lib/modules/*/vmlinuz"]) + self.assertEqual(sorted(tree.glob("usr/lib/modules/*/vmlinuz")), []) + self.assertEqual(len(sorted(tree.glob("usr/lib/modules/*/modules.dep"))), 2) + + def test_takes_a_directory_with_its_contents(self) -> None: + with tempfile.TemporaryDirectory(prefix="ddi_test.") as scratch_dir: + tree = self._tree(scratch_dir) + ddi.strip_files(tree, ["usr/lib/modules/*/kernel"]) + self.assertEqual(sorted(tree.glob("usr/lib/modules/*/kernel")), []) + self.assertTrue((tree / "usr/lib/modules/6.17.0").is_dir()) + + def test_reads_a_leading_slash_as_the_image_root(self) -> None: + with tempfile.TemporaryDirectory(prefix="ddi_test.") as scratch_dir: + tree = self._tree(scratch_dir) + ddi.strip_files(tree, ["/usr/lib/modules/*/vmlinuz"]) + self.assertEqual(sorted(tree.glob("usr/lib/modules/*/vmlinuz")), []) + + def test_ignores_a_pattern_that_matches_nothing(self) -> None: + # A glob holds for every image the rule builds, so one without the file is not an error. + with tempfile.TemporaryDirectory(prefix="ddi_test.") as scratch_dir: + tree = self._tree(scratch_dir) + ddi.strip_files(tree, ["usr/lib/modules/*/.vmlinuz.hmac"]) + self.assertEqual(len(sorted(tree.glob("usr/lib/modules/*/vmlinuz"))), 2) + + if __name__ == "__main__": unittest.main()