stages/squashfs: Add exclude_paths support

Add the ability to exclude files and directories from the squashfs
image. This uses the mksquashfs -regex -e FILES... feature, so simple
matches or regexes can be used.

This also adds a new test for squashfs, based on the existing test for
erofs.
This commit is contained in:
Brian C. Lane 2025-06-05 14:28:49 -07:00 committed by Achilleas Koutsou
parent 387806a8c3
commit 86c89a2421
3 changed files with 116 additions and 0 deletions

View file

@ -10,6 +10,7 @@ def main(inputs, output_dir, options):
source = inputs["tree"]["path"]
filename = options["filename"].lstrip("/")
compression = options.get("compression")
exclude_paths = options.get("exclude_paths")
target = os.path.join(output_dir, filename)
@ -22,6 +23,10 @@ def main(inputs, output_dir, options):
for opt, val in opts.items():
cmd += [f"-X{opt}", val]
if exclude_paths:
cmd += ["-regex", "-e"]
cmd.extend(exclude_paths)
subprocess.run(cmd, check=True)
return 0

View file

@ -14,6 +14,13 @@
"description": "Filename for squashfs image",
"type": "string"
},
"exclude_paths": {
"type": "array",
"description": "Regex of paths to exclude, can be files or directories",
"items": {
"type": "string"
}
},
"compression": {
"type": "object",
"additionalProperties": false,

View file

@ -0,0 +1,104 @@
#!/usr/bin/python3
import os.path
import subprocess
from unittest import mock
import pytest
from osbuild import testutil
from osbuild.testutil import has_executable, make_fake_input_tree
TEST_INPUT = [
({}, []),
({"compression": {"method": "lz4"}}, ["-comp", "lz4"]),
({"compression": {"method": "xz", "options": {"bcj": "x86"}}}, ["-comp", "xz", "-Xbcj", "x86"]),
({"exclude_paths": ["boot/.*", "root/.*"]}, ["-regex", "-e", "boot/.*", "root/.*"])
]
STAGE_NAME = "org.osbuild.squashfs"
@pytest.mark.skipif(not has_executable("mksquashfs"), reason="no mksquashfs")
@pytest.mark.parametrize("test_options,expected", TEST_INPUT)
def test_squashfs_integration(tmp_path, stage_module, test_options, expected): # pylint: disable=unused-argument
fake_input_tree = make_fake_input_tree(tmp_path, {
"/file-in-root.txt": "other content",
"/subdir/file-in-subdir.txt": "subdir content",
})
inputs = {
"tree": {
"path": fake_input_tree,
}
}
filename = "some-file.img"
options = {
"filename": filename,
}
options.update(test_options)
stage_module.main(inputs, tmp_path, options)
img_path = os.path.join(tmp_path, "some-file.img")
assert os.path.exists(img_path)
# validate the content
output = subprocess.check_output([
"unsquashfs", "-ls", img_path], encoding="utf-8")
assert "subdir/file-in-subdir.txt\n" in output
assert "file-in-root.txt\n" in output
@mock.patch("subprocess.run")
@pytest.mark.parametrize("test_options,expected", TEST_INPUT)
def test_squashfs(mock_run, tmp_path, stage_module, test_options, expected):
fake_input_tree = make_fake_input_tree(tmp_path, {
"/some-dir/some-file.txt": "content",
})
inputs = {
"tree": {
"path": fake_input_tree,
}
}
filename = "some-file.img"
options = {
"filename": filename,
}
options.update(test_options)
stage_module.main(inputs, tmp_path, options)
expected = [
"mksquashfs",
f"{fake_input_tree}",
f"{os.path.join(tmp_path, filename)}",
] + expected
mock_run.assert_called_with(expected, check=True)
@pytest.mark.parametrize("test_data,expected_err", [
# bad
({"extra": "option"}, "'extra' was unexpected"),
({"compression": {"method": "invalid"}}, "'invalid' is not one of ["),
({"compression": {}}, "'method' is a required property"),
({"compression": {"method": "xz", "options": {"bcj": "invalid"}}}, "'invalid' is not one of ["),
({"compression": {"method": "xz", "options": {"level": 9}}}, "Additional properties are not allowed"),
# good
({"compression": {"method": "lz4"}}, ""),
({"exclude_paths": ["boot/", "root/"]}, "")
])
def test_schema_validation_squashfs(stage_schema, test_data, expected_err):
test_input = {
"type": STAGE_NAME,
"options": {
"filename": "some-filename.img",
}
}
test_input["options"].update(test_data)
res = stage_schema.validate(test_input)
if expected_err == "":
assert res.valid is True, f"err: {[e.as_dict() for e in res.errors]}"
else:
assert res.valid is False
testutil.assert_jsonschema_error_contains(res, expected_err, expected_num_errs=1)