]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
Add support for profiles
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Sun, 22 Oct 2023 13:53:22 +0000 (15:53 +0200)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Mon, 23 Oct 2023 09:28:18 +0000 (11:28 +0200)
A profile is a set of configuration options that represents a known
variant of the given image. Its primary purpose is to allow grouping
known useful derivatives of the current image under a recognizable
identifier.

The difference with presets is that presets represent the individual
images that might need to be built to complete the final image, whereas
profiles change how the individual presets are built. Specifically, only
ever one profile can be selected, while it's perfectly valid to build
many presets.

We parse the specified profile after mkosi.conf but before
mkosi.conf.d to allow configuring the profile to build in mkosi.conf.

mkosi/config.py
mkosi/resources/mkosi.md
tests/test_config.py
tests/test_json.py

index 531ee72857a3b9ff40806a0bb408c9609f2560cc..2e0508b975b1aeb6c6dc796085d06ad75aa06a24 100644 (file)
@@ -507,6 +507,17 @@ def config_parse_bytes(value: Optional[str], old: Optional[int] = None) -> Optio
     return result
 
 
+def config_parse_profile(value: Optional[str], old: Optional[int] = None) -> Optional[str]:
+    if not value:
+        return None
+
+    if not is_valid_filename(value):
+        die(f"{value!r} is not a valid profile",
+            hint="Profile= or --profile= requires a name with no path components.")
+
+    return value
+
+
 @dataclasses.dataclass(frozen=True)
 class MkosiConfigSetting:
     dest: str
@@ -704,6 +715,7 @@ class MkosiConfig:
     access the value from state.
     """
 
+    profile: Optional[str]
     include: tuple[str, ...]
     presets: tuple[str, ...]
     dependencies: tuple[str, ...]
@@ -1038,6 +1050,13 @@ SETTINGS = (
         parse=config_make_list_parser(delimiter=",", reset=False, parse=make_path_parser()),
         help="Include configuration from the specified file or directory",
     ),
+    MkosiConfigSetting(
+        dest="profile",
+        section="Config",
+        help="Build the specified profile",
+        parse=config_parse_profile,
+        match=config_make_string_matcher(),
+    ),
     MkosiConfigSetting(
         dest="presets",
         long="--preset",
@@ -2256,7 +2275,12 @@ def parse_config(argv: Sequence[str] = ()) -> tuple[MkosiArgs, tuple[MkosiConfig
 
         return triggered is not False
 
-    def parse_config(path: Path, namespace: argparse.Namespace, defaults: argparse.Namespace) -> bool:
+    def parse_config(
+        path: Path,
+        namespace: argparse.Namespace,
+        defaults: argparse.Namespace,
+        profiles: bool = False,
+    ) -> bool:
         s: Optional[MkosiConfigSetting] # Make mypy happy
         extras = path.is_dir()
 
@@ -2306,6 +2330,24 @@ def parse_config(argv: Sequence[str] = ()) -> tuple[MkosiArgs, tuple[MkosiConfig
                 with parse_new_includes(namespace, defaults):
                     setattr(ns, s.dest, s.parse(v, getattr(ns, s.dest, None)))
 
+        if profiles:
+            finalize_default(SETTINGS_LOOKUP_BY_DEST["profile"], namespace, defaults)
+            profile = getattr(namespace, "profile")
+            immutable_settings.add("Profile")
+
+            if profile:
+                for p in (profile, f"{profile}.conf"):
+                    p = Path("mkosi.profiles") / p
+                    if p.exists():
+                        break
+                else:
+                    die(f"Profile '{profile}' not found in mkosi.profiles/")
+
+                setattr(namespace, "profile", profile)
+
+                with chdir(p if p.is_dir() else Path.cwd()):
+                    parse_config(p if p.is_file() else Path("."), namespace, defaults)
+
         if extras and (path.parent / "mkosi.conf.d").exists():
             for p in sorted((path.parent / "mkosi.conf.d").iterdir()):
                 if p.is_dir() or p.suffix == ".conf":
@@ -2354,7 +2396,7 @@ def parse_config(argv: Sequence[str] = ()) -> tuple[MkosiArgs, tuple[MkosiConfig
     include = ()
 
     if args.directory is not None:
-        parse_config(Path("."), namespace, defaults)
+        parse_config(Path("."), namespace, defaults, profiles=True)
 
         finalize_default(SETTINGS_LOOKUP_BY_DEST["presets"], namespace, defaults)
         include = getattr(namespace, "presets")
@@ -2644,6 +2686,7 @@ def summary(config: MkosiConfig) -> str:
 {bold(f"PRESET: {config.preset or 'default'}")}
 
     {bold("CONFIG")}:
+                       Profile: {none_to_none(config.profile)}
                        Include: {line_join_list(config.include)}
 
     {bold("PRESET")}:
index 94485125a5751fddbd94795477b5df4fbb0bd733..ce3a9f5c93f7d54e22c852c32626b2a3d834adf5 100644 (file)
@@ -299,6 +299,10 @@ boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`,
 
 ### [Match] Section.
 
+`Profile=`
+
+: Matches against the configured profile.
+
 `Distribution=`
 
 : Matches against the configured distribution.
@@ -350,6 +354,7 @@ boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`,
 
 | Matcher           | Globs | Rich Comparisons | Default                 |
 |-------------------|-------|------------------|-------------------------|
+| `Profile=`        | no    | no               | match fails             |
 | `Distribution=`   | no    | no               | match host distribution |
 | `Release=`        | no    | no               | match host release      |
 | `Architecture=`   | no    | no               | match host architecture |
@@ -362,6 +367,14 @@ boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`,
 
 ### [Config] Section
 
+`Profile=`, `--profile=`
+
+: Select the given profile. A profile is a configuration file or
+  directory in the `mkosi.profiles/` directory. When selected, this
+  configuration file or directory is included after parsing the
+  `mkosi.conf` file, but before any `mkosi.conf.d/*.conf` drop in
+  configuration.
+
 `Include=`, `--include=`
 
 : Include extra configuration from the given file or directory. The
index 5b84b9577a04b26cdbcd2ff6e9e399f872114d40..6c57a8d2669870b55dafdfe32c32a05dae90a087 100644 (file)
@@ -214,6 +214,44 @@ def test_parse_config(tmp_path: Path) -> None:
         assert config.split_artifacts is False
 
 
+def test_profiles(tmp_path: Path) -> None:
+    d = tmp_path
+
+    (d / "mkosi.profiles").mkdir()
+    (d / "mkosi.profiles/profile.conf").write_text(
+        """\
+        [Distribution]
+        Distribution=fedora
+
+        [Host]
+        QemuKvm=yes
+        """
+    )
+
+    (d / "mkosi.conf").write_text(
+        """\
+        [Config]
+        Profile=profile
+        """
+    )
+
+    (d / "mkosi.conf.d").mkdir()
+    (d / "mkosi.conf.d/abc.conf").write_text(
+        """\
+        [Distribution]
+        Distribution=debian
+        """
+    )
+
+    with chdir(d):
+        _, [config] = parse_config()
+
+    assert config.profile == "profile"
+    # mkosi.conf.d/ should override the profile
+    assert config.distribution == Distribution.debian
+    assert config.qemu_kvm == ConfigFeature.enabled
+
+
 def test_parse_load_verb(tmp_path: Path) -> None:
     with chdir(tmp_path):
         assert parse_config(["build"])[0].verb == Verb.build
index 461c14de631f958870da7e88c84d839167d24e0e..20e7a89c3d9e6dc4174632240568d394b23c6f26 100644 (file)
@@ -179,6 +179,7 @@ def test_config() -> None:
                 "default",
                 "initrd"
             ],
+            "Profile": "profile",
             "QemuArgs": [],
             "QemuCdrom": false,
             "QemuFirmware": "linux",
@@ -311,6 +312,7 @@ def test_config() -> None:
         prepare_scripts = [Path("/run/foo")],
         preset = "default",
         presets = ("default", "initrd"),
+        profile = "profile",
         qemu_args = [],
         qemu_cdrom = False,
         qemu_firmware = QemuFirmware.linux,