Repository: osbuild/osbuild
Pull request #2098 (2025-05-22 06:58:24)

stages/tar: allow chosen compression (HMS-8573, HMS-6116)
The `org.osbuild.tar` stage only supports auto compression which is based on the filename. For several (newer) artifacts such as Vagrant (`.box`) and WSL (`.wsl`) we want to explicitly give the compression algorithm.

I've chosen a (few) commonly used compression algorithms. If others are needed they are one-line followups away.

---

The one "directly" needed is `gzip`, I'd be fine with dropping the others under YAGNI?



Patch:
diff --git a/stages/org.osbuild.tar b/stages/org.osbuild.tar
index beb97a99..258eabbb 100755
--- a/stages/org.osbuild.tar
+++ b/stages/org.osbuild.tar
@@ -37,10 +37,19 @@ def main(inputs, output_dir, options):
     if transform:
         extra_args += ["--transform", transform]
 
+    compression = options.get("compression", "auto")
+
+    if compression == "auto":
+        extra_args += ["--auto-compress"]
+    else:
+        # Note that the enums in the schema have been chosen exactly
+        # as the long forms for the compression flag(s) that GNU tar
+        # accepts
+        extra_args += [f"--{compression}"]
+
     # Set up the tar command.
     tar_cmd = [
         "tar",
-        "--auto-compress",
         f"--format={tarfmt}",
         *extra_args,
         "-cf", os.path.join(output_dir, filename),
diff --git a/stages/org.osbuild.tar.meta.json b/stages/org.osbuild.tar.meta.json
index bec34f47..c0611821 100644
--- a/stages/org.osbuild.tar.meta.json
+++ b/stages/org.osbuild.tar.meta.json
@@ -65,6 +65,17 @@
           ],
           "default": "gnu"
         },
+        "compression": {
+          "description": "Compression to use",
+          "type": "string",
+          "enum": [
+            "auto",
+            "xz",
+            "gzip",
+            "zstd"
+          ],
+          "default": "auto"
+        },
         "acls": {
           "description": "Enable support for POSIX ACLs",
           "type": "boolean",


Test patch:
diff --git a/stages/test/test_tar.py b/stages/test/test_tar.py
index 064301a9..1ad26dcb 100644
--- a/stages/test/test_tar.py
+++ b/stages/test/test_tar.py
@@ -24,12 +24,18 @@ STAGE_NAME = "org.osbuild.tar"
         "filename": "foo",
         "transform": ["transform-cannot-be-passed-multiple-times"],
     }, " is not of type 'string'"),
+    ({
+        "filename": "foo",
+        "compression": "EXTREME",
+    }, "'EXTREME' is not one of"),
     # good
     ({"filename": "out.tar", "root-node": "include"}, ""),
     ({"filename": "out.tar", "paths": ["file1"]}, ""),
     ({"filename": "out.tar", "sparse": True}, ""),
     ({"filename": "out.tar"}, ""),
     ({"filename": "out.tar", "transform": "s/foo/bar"}, ""),
+    ({"filename": "out.tar", "compression": "auto"}, ""),
+    ({"filename": "out.tar", "compression": "gzip"}, ""),
 ])
 def test_schema_validation_tar(stage_schema, test_data, expected_err):
     test_input = {
@@ -160,3 +166,27 @@ def test_tar_disk_full(stage_module, fake_inputs, tmp_path_disk_full, capfd):
 
     assert ex.value.returncode == 2
     assert re.search(r"Wrote only \d+ of \d+ bytes", str(capfd.readouterr().err))
+
+
+@pytest.mark.skipif(not has_executable("tar"), reason="no tar executable")
+@pytest.mark.parametrize("filename,compression,expected", [
+    ("out.tar.gz", "auto", "gzip"),  # Auto uses the filename, which ends in .gz and should lead to gzip
+    ("out.unknown", "xz", "xz"),
+    ("out.unknown", "gzip", "gzip"),
+    ("out.unknown", "zstd", "zstandard"),
+])
+def test_tar_compress(tmp_path, stage_module, fake_inputs, filename, compression, expected):
+    options = {
+        "filename": filename,
+        "paths": [
+            "file1",
+            "file2",
+        ],
+        "compression": compression,
+    }
+    stage_module.main(fake_inputs, tmp_path, options)
+
+    tar_path = os.path.join(tmp_path, filename)
+    assert os.path.exists(tar_path)
+    output = subprocess.check_output(["file", tar_path], encoding="utf-8")
+    assert expected in output.lower()