Repository: osbuild/osbuild
Pull request #2119 (2025-06-13 14:10:57)

Adjust tests to work better in downstream environments
- don't attempt to create system units during the RPM build
- skip x86_64-specific tests on different architectures
- mark tests needing a TOML-writing library for easier exclusion downstream (no such library is present in RHEL currently)

This is all based on the test results from #2116 


Patch:
diff --git a/.github/workflows/stale-cleanup.yml b/.github/workflows/stale-cleanup.yml
index 0384c0d0..346fb6c4 100644
--- a/.github/workflows/stale-cleanup.yml
+++ b/.github/workflows/stale-cleanup.yml
@@ -8,6 +8,7 @@ jobs:
   stale:
     runs-on: ubuntu-24.04
     permissions:
+      actions: write  # needed to clean up the saved action state
       issues: write
       pull-requests: write
     steps:
diff --git a/osbuild.spec b/osbuild.spec
index 1db22fca..7b491027 100644
--- a/osbuild.spec
+++ b/osbuild.spec
@@ -1,7 +1,7 @@
 %global         forgeurl https://github.com/osbuild/osbuild
 %global         selinuxtype targeted
 
-Version:        153
+Version:        154
 
 %forgemeta
 
diff --git a/osbuild/__init__.py b/osbuild/__init__.py
index f0e8095a..0012f217 100644
--- a/osbuild/__init__.py
+++ b/osbuild/__init__.py
@@ -10,7 +10,7 @@ independent of osbuild but used across the osbuild codebase.
 
 from .pipeline import Manifest, Pipeline, Stage
 
-__version__ = "153"
+__version__ = "154"
 
 __all__ = [
     "Manifest",
diff --git a/setup.py b/setup.py
index 707f7aa4..bfa9c507 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@ import setuptools
 
 setuptools.setup(
     name="osbuild",
-    version="153",
+    version="154",
     description="A build system for OS images",
     packages=[
         "osbuild",
diff --git a/stages/org.osbuild.wsl-distribution.conf b/stages/org.osbuild.wsl-distribution.conf
new file mode 100755
index 00000000..01466961
--- /dev/null
+++ b/stages/org.osbuild.wsl-distribution.conf
@@ -0,0 +1,60 @@
+#!/usr/bin/python3
+import sys
+
+import iniparse
+
+import osbuild.api
+
+
+def main(tree, options):
+    oobe = options.get("oobe", {})
+    shortcut = options.get("shortcut", {})
+
+    # Don't write an empty file
+    if not oobe and not shortcut:
+        return 0
+
+    wsl_conf_path = f"{tree}/etc/wsl-distribution.conf"
+    wsl_config = iniparse.SafeConfigParser()
+
+    try:
+        with open(wsl_conf_path, "r", encoding="utf8") as f:
+            wsl_config.readfp(f)
+    except FileNotFoundError:
+        print(
+            f"WSL distribution configuration file '{wsl_conf_path}' does not exist, will be created."
+        )
+
+    if oobe:
+        if not wsl_config.has_section("oobe"):
+            wsl_config.add_section("oobe")
+
+        if "default_uid" in oobe:
+            wsl_config.set("oobe", "defaultUid", str(oobe["default_uid"]))
+
+        if "default_name" in oobe:
+            wsl_config.set("oobe", "defaultName", oobe["default_name"])
+
+    if shortcut:
+        if not wsl_config.has_section("shortcut"):
+            wsl_config.add_section("shortcut")
+
+        if "enabled" in shortcut:
+            # this turns the boolean `True` into `true` (or `False`)
+            wsl_config.set(
+                "shortcut", "enabled", str(shortcut["enabled"]).lower()
+            )
+
+        if "icon" in shortcut:
+            wsl_config.set("shortcut", "icon", shortcut["icon"])
+
+    with open(wsl_conf_path, mode="w", encoding="utf8") as f:
+        wsl_config.write(f)
+
+    return 0
+
+
+if __name__ == "__main__":
+    args = osbuild.api.arguments()
+    r = main(args["tree"], args["options"])
+    sys.exit(r)
diff --git a/stages/org.osbuild.wsl-distribution.conf.meta.json b/stages/org.osbuild.wsl-distribution.conf.meta.json
new file mode 100644
index 00000000..0e146758
--- /dev/null
+++ b/stages/org.osbuild.wsl-distribution.conf.meta.json
@@ -0,0 +1,46 @@
+{
+  "summary": "Configure distribution settings in Windows Subsystem for Linux.",
+  "description": [
+    "The stage configures the WSL distribution settings on the system."
+  ],
+  "schema_2": {
+    "options": {
+      "additionalProperties": false,
+      "description": "WSL distribution configuration.",
+      "properties": {
+        "oobe": {
+          "type": "object",
+          "description": "Configures the [oobe] section.",
+          "additionalProperties": false,
+          "minProperties": 1,
+          "properties": {
+            "default_uid": {
+              "type": "integer",
+              "default": 1000,
+              "description": "Default login UID."
+            },
+            "default_name": {
+              "type": "string",
+              "description": "Name of the distribution."
+            }
+          }
+        },
+        "shortcut": {
+          "type": "object",
+          "description": "Configures the [shortcut] section.",
+          "additionalProperties": false,
+          "properties": {
+            "enabled": {
+              "type": "boolean",
+              "description": "Enable a shortcut icon."
+            },
+            "icon": {
+              "type": "string",
+              "description": "Path of the shortcut icon."
+            }
+          }
+        }
+      }
+    }
+  }
+}


Test patch:
diff --git a/stages/test/test_bootc_install_config.py b/stages/test/test_bootc_install_config.py
index c839f6f4..afedcf6e 100644
--- a/stages/test/test_bootc_install_config.py
+++ b/stages/test/test_bootc_install_config.py
@@ -73,6 +73,7 @@ STAGE_NAME = "org.osbuild.bootc.install.config"
         )
     ]
 )
+@pytest.mark.tomlwrite
 def test_bootc_install_config(tmp_path, stage_module, options, expected_config):
     stage_module.main(tmp_path, options)
     # different toml libraries write out arrays in different ways (one line vs many), so we need to parse the result
diff --git a/stages/test/test_containers_storage_conf.py b/stages/test/test_containers_storage_conf.py
index d98409e6..d8c315cc 100644
--- a/stages/test/test_containers_storage_conf.py
+++ b/stages/test/test_containers_storage_conf.py
@@ -18,6 +18,7 @@ TEST_INPUT = [
 STAGE_NAME = "org.osbuild.containers.storage.conf"
 
 
+@pytest.mark.tomlwrite
 @pytest.mark.parametrize("test_filename", ["/etc/containers/storage.conf", "/usr/share/containers/storage.conf"])
 @pytest.mark.parametrize("test_storage,test_options,expected", TEST_INPUT)
 def test_containers_storage_conf_integration(tmp_path, stage_module, test_filename, test_storage, test_options, expected):  # pylint: disable=unused-argument
diff --git a/stages/test/test_systemd_unit_create.py b/stages/test/test_systemd_unit_create.py
index 582b7450..eb2af02f 100644
--- a/stages/test/test_systemd_unit_create.py
+++ b/stages/test/test_systemd_unit_create.py
@@ -469,6 +469,8 @@ def test_systemd_unit_create(tmp_path, stage_module, unit_type, unit_path, expec
     stage_module.main(tmp_path, options)
     assert os.path.exists(expected_unit_path)
     if unit_type == "system":
+        if os.environ.get("RPM_BUILD_ROOT"):
+            pytest.skip("systemd-analyze can't be run in the RPM build root environment")
         # When verifying user units, systemd expects runtime directories and more generally a booted system. Setting
         # something up like that to verify it is more trouble than it's worth, especially since the system units are
         # identical.
@@ -562,6 +564,8 @@ def test_systemd_unit_create_mount(tmp_path, stage_module, unit_type, unit_path,
     stage_module.main(tmp_path, options)
     assert os.path.exists(expected_unit_path)
     if unit_type == "system":
+        if os.environ.get("RPM_BUILD_ROOT"):
+            pytest.skip("systemd-analyze can't be run in the RPM build root environment")
         # When verifying user units, systemd expects runtime directories and more generally a booted system. Setting
         # something up like that to verify it is more trouble than it's worth, especially since the system units are
         # identical.
diff --git a/stages/test/test_wsl_distribution_conf.py b/stages/test/test_wsl_distribution_conf.py
new file mode 100644
index 00000000..f15d3225
--- /dev/null
+++ b/stages/test/test_wsl_distribution_conf.py
@@ -0,0 +1,80 @@
+#!/usr/bin/python3
+
+import pytest
+
+from osbuild import testutil
+
+STAGE_NAME = "org.osbuild.wsl-distribution.conf"
+
+
+@pytest.mark.parametrize("test_data,expected_err", [
+    # bad
+    ({"oobe": "must-be-object"}, "'must-be-object' is not of type 'object'"),
+    ({"shortcut": "must-be-object"}, "'must-be-object' is not of type 'object'"),
+    ({"shortcut": {"enabled": 1}}, "1 is not of type 'boolean'"),
+    ({"oobe": {"default_uid": True}}, "True is not of type 'integer'"),
+    # good
+    ({
+        "oobe":
+        {
+            "default_uid": 1000,
+            "default_name": "RedHatEnterpriseLinux-10.0",
+        },
+        "shortcut":
+        {
+            "enabled": True,
+            "icon": "/usr/share/pixmaps/fedora-logo.ico",
+        }
+    }, "")
+])
+def test_wsl_distribution_conf_schema_validation(stage_schema, test_data, expected_err):
+    test_input = {
+        "type": STAGE_NAME,
+        "options": {
+        }
+    }
+    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)
+
+
+def test_wsl_distribution_conf_writes_file(tmp_path, stage_module):
+    treepath = tmp_path / "tree"
+
+    etcpath = treepath / "etc"
+    etcpath.mkdir(parents=True, exist_ok=True)
+
+    confpath = "wsl-distribution.conf"
+
+    fullpath = etcpath / confpath
+
+    options = {
+        "oobe": {
+            "default_name": "RedHatEnterpriseLinux-10.0",
+            "default_uid": 1000,
+        },
+        "shortcut": {
+            "enabled": True,
+            "icon": "/usr/share/pixmaps/fedora-logo.ico",
+        }
+    }
+
+    stage_module.main(treepath, options)
+
+    assert fullpath.exists()
+
+    confdata = fullpath.read_text()
+
+    assert confdata == """\
+[oobe]
+defaultUid = 1000
+defaultName = RedHatEnterpriseLinux-10.0
+
+[shortcut]
+enabled = true
+icon = /usr/share/pixmaps/fedora-logo.ico"""
diff --git a/test/conftest.py b/test/conftest.py
index f2ff9f3f..b748e847 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -17,3 +17,6 @@ def pytest_configure(config):
     # pylint: disable=global-statement
     global unsupported_filesystems
     unsupported_filesystems = config.getoption("--unsupported-fs")
+    config.addinivalue_line(
+        "markers", "tomlwrite: mark test to run only in an environment with a TOML-writing library available"
+    )
diff --git a/test/mod/test_util_toml.py b/test/mod/test_util_toml.py
index 9bcb022f..3cfe1f0d 100644
--- a/test/mod/test_util_toml.py
+++ b/test/mod/test_util_toml.py
@@ -4,6 +4,8 @@
 import os
 from tempfile import TemporaryDirectory
 
+import pytest
+
 from osbuild.util import toml
 
 data_obj = {
@@ -26,6 +28,7 @@ str = "test"
 """
 
 
+@pytest.mark.tomlwrite
 def test_write_read():
     with TemporaryDirectory() as tmpdir:
         path = os.path.join(tmpdir, "test.toml")