]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
Allow configuring more than one profile 3057/head
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Sat, 21 Sep 2024 11:42:08 +0000 (13:42 +0200)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Sat, 21 Sep 2024 19:10:22 +0000 (21:10 +0200)
For many use cases it's useful to be able to configure more than one
profile, an example is selecting a generic desktop profile and a more
specific kde profile as well.

NEWS.md
mkosi/__init__.py
mkosi/config.py
mkosi/resources/man/mkosi.md
tests/test_config.py
tests/test_json.py

diff --git a/NEWS.md b/NEWS.md
index ad08c83b7bcb85a4e4a4de03bc828062f8077cc7..2ba680888db2227df7dc183bcd84d1224c3ed3ab 100644 (file)
--- a/NEWS.md
+++ b/NEWS.md
@@ -59,6 +59,9 @@
   `mkosi.conf.d` instead of before it. To set defaults for use in `mkosi.conf.d`
   based on the configured profile, use an early dropin in `mkosi.conf.d` that
   matches on the configured profile instead.
+- `Profile=` is renamed to `Profiles=` and takes a comma separated list of
+  profiles now. Scripts now receive `$PROFILES` with a comma separated lists
+  of profiles instead of `$PROFILE`.
 
 ## v24
 
index 327d2e4690865f983e1f1b767e15a16f54d53208..2136cb8859fa4949e71d7eb7909014aac76dc585 100644 (file)
@@ -517,8 +517,8 @@ def run_configure_scripts(config: Config) -> Config:
         MKOSI_GID=str(os.getgid()),
     )
 
-    if config.profile:
-        env["PROFILE"] = config.profile
+    if config.profiles:
+        env["PROFILES"] = ",".join(config.profiles)
 
     with finalize_source_mounts(config, ephemeral=False) as sources:
         for script in config.configure_scripts:
@@ -560,8 +560,8 @@ def run_sync_scripts(config: Config) -> None:
         CACHED=one_zero(have_cache(config)),
     )
 
-    if config.profile:
-        env["PROFILE"] = config.profile
+    if config.profiles:
+        env["PROFILES"] = ",".join(config.profiles)
 
     # We make sure to mount everything in to make ssh work since syncing might involve git which
     # could invoke ssh.
@@ -690,8 +690,8 @@ def run_prepare_scripts(context: Context, build: bool) -> None:
         **GIT_ENV,
     )
 
-    if context.config.profile:
-        env["PROFILE"] = context.config.profile
+    if context.config.profiles:
+        env["PROFILES"] = ",".join(context.config.profiles)
 
     if context.config.build_dir is not None:
         env |= dict(BUILDDIR="/work/build")
@@ -767,8 +767,8 @@ def run_build_scripts(context: Context) -> None:
         **GIT_ENV,
     )
 
-    if context.config.profile:
-        env["PROFILE"] = context.config.profile
+    if context.config.profiles:
+        env["PROFILES"] = ",".join(context.config.profiles)
 
     if context.config.build_dir is not None:
         env |= dict(
@@ -840,8 +840,8 @@ def run_postinst_scripts(context: Context) -> None:
         **GIT_ENV,
     )
 
-    if context.config.profile:
-        env["PROFILE"] = context.config.profile
+    if context.config.profiles:
+        env["PROFILES"] = ",".join(context.config.profiles)
 
     if context.config.build_dir is not None:
         env |= dict(BUILDDIR="/work/build")
@@ -906,8 +906,8 @@ def run_finalize_scripts(context: Context) -> None:
         **GIT_ENV,
     )
 
-    if context.config.profile:
-        env["PROFILE"] = context.config.profile
+    if context.config.profiles:
+        env["PROFILES"] = ",".join(context.config.profiles)
 
     if context.config.build_dir is not None:
         env |= dict(BUILDDIR="/work/build")
@@ -963,8 +963,8 @@ def run_postoutput_scripts(context: Context) -> None:
         MKOSI_CONFIG="/work/config.json",
     )
 
-    if context.config.profile:
-        env["PROFILE"] = context.config.profile
+    if context.config.profiles:
+        env["PROFILES"] = ",".join(context.config.profiles)
 
     with (
         finalize_source_mounts(context.config, ephemeral=context.config.build_sources_ephemeral) as sources,
@@ -3858,8 +3858,8 @@ def run_clean_scripts(config: Config) -> None:
         MKOSI_CONFIG="/work/config.json",
     )
 
-    if config.profile:
-        env["PROFILE"] = config.profile
+    if config.profiles:
+        env["PROFILES"] = ",".join(config.profiles)
 
     with (
         finalize_source_mounts(config, ephemeral=False) as sources,
index 34c1e0de8f520e9a7a5351be61e990734f60ce49..bb76f01fd6725dff0b1a972d41051209036b1ca8 100644 (file)
@@ -48,6 +48,8 @@ from mkosi.util import (
 )
 from mkosi.versioncomp import GenericVersion
 
+T = TypeVar("T")
+
 ConfigParseCallback = Callable[[Optional[str], Optional[Any]], Any]
 ConfigMatchCallback = Callable[[str, Any], bool]
 ConfigDefaultCallback = Callable[[argparse.Namespace], Any]
@@ -577,8 +579,11 @@ def config_match_build_sources(match: str, value: list[ConfigTree]) -> bool:
     return Path(match.lstrip("/")) in [tree.target for tree in value if tree.target]
 
 
-def config_match_repositories(match: str, value: list[str]) -> bool:
-    return match in value
+def config_make_list_matcher(parse: Callable[[str], T]) -> ConfigMatchCallback:
+    def config_match_list(match: str, value: list[T]) -> bool:
+        return parse(match) in value
+
+    return config_match_list
 
 
 def config_parse_string(value: Optional[str], old: Optional[str]) -> Optional[str]:
@@ -1107,10 +1112,7 @@ def config_parse_number(value: Optional[str], old: Optional[int] = None) -> Opti
         die(f"{value!r} is not a valid number")
 
 
-def config_parse_profile(value: Optional[str], old: Optional[int] = None) -> Optional[str]:
-    if not value:
-        return None
-
+def parse_profile(value: str) -> str:
     if not is_valid_filename(value):
         die(
             f"{value!r} is not a valid profile",
@@ -1477,7 +1479,7 @@ class Config:
     access the value from context.
     """
 
-    profile: Optional[str]
+    profiles: list[str]
     files: list[Path]
     dependencies: list[str]
     minimum_version: Optional[GenericVersion]
@@ -1952,13 +1954,15 @@ SETTINGS = (
     ),
     # Config section
     ConfigSetting(
-        dest="profile",
+        dest="profiles",
+        long="--profile",
         section="Config",
         specifier="p",
-        help="Build the specified profile",
-        parse=config_parse_profile,
-        match=config_make_string_matcher(),
+        help="Build the specified profiles",
+        parse=config_make_list_parser(delimiter=",", parse=parse_profile),
+        match=config_make_list_matcher(parse=parse_profile),
         scope=SettingScope.universal,
+        compat_names=("Profile",),
     ),
     ConfigSetting(
         dest="dependencies",
@@ -2064,7 +2068,7 @@ SETTINGS = (
         metavar="REPOS",
         section="Distribution",
         parse=config_make_list_parser(delimiter=","),
-        match=config_match_repositories,
+        match=config_make_list_matcher(parse=str),
         help="Repositories to use",
         scope=SettingScope.universal,
     ),
@@ -3777,7 +3781,7 @@ class ParseContext:
 
         return match_triggered is not False
 
-    def parse_config_one(self, path: Path, profiles: bool = False, local: bool = False) -> bool:
+    def parse_config_one(self, path: Path, parse_profiles: bool = False, parse_local: bool = False) -> bool:
         s: Optional[ConfigSetting]  # Make mypy happy
         extras = path.is_dir()
 
@@ -3788,7 +3792,7 @@ class ParseContext:
             return False
 
         if extras:
-            if local:
+            if parse_local:
                 if (
                     (localpath := path.parent / "mkosi.local/mkosi.conf").exists()
                     or (localpath := path.parent / "mkosi.local.conf").exists()
@@ -3871,20 +3875,20 @@ class ParseContext:
                 setattr(self.config, s.dest, s.parse(v, getattr(self.config, s.dest, None)))
                 self.parse_new_includes()
 
-        profilepath = None
-        if profiles:
-            profile = self.finalize_value(SETTINGS_LOOKUP_BY_DEST["profile"])
-            self.immutable.add("Profile")
+        profilepaths = []
+        if parse_profiles:
+            profiles = self.finalize_value(SETTINGS_LOOKUP_BY_DEST["profiles"])
+            self.immutable.add("Profiles")
 
-            if profile:
+            for profile in profiles or []:
                 for p in (Path(profile), Path(f"{profile}.conf")):
-                    profilepath = Path("mkosi.profiles") / p
-                    if profilepath.exists():
+                    p = Path("mkosi.profiles") / p
+                    if p.exists():
                         break
                 else:
                     die(f"Profile '{profile}' not found in mkosi.profiles/")
 
-                setattr(self.config, "profile", profile)
+                profilepaths += [p]
 
         if extras and (path.parent / "mkosi.conf.d").exists():
             for p in sorted((path.parent / "mkosi.conf.d").iterdir()):
@@ -3892,9 +3896,9 @@ class ParseContext:
                     with chdir(p if p.is_dir() else Path.cwd()):
                         self.parse_config_one(p if p.is_file() else Path("."))
 
-        if profilepath:
-            with chdir(profilepath if profilepath.is_dir() else Path.cwd()):
-                self.parse_config_one(profilepath if profilepath.is_file() else Path("."))
+        for p in profilepaths:
+            with chdir(p if p.is_dir() else Path.cwd()):
+                self.parse_config_one(p if p.is_file() else Path("."))
 
         return True
 
@@ -3982,7 +3986,7 @@ def parse_config(
 
     # Parse the global configuration unless the user explicitly asked us not to.
     if args.directory is not None:
-        context.parse_config_one(Path("."), profiles=True, local=True)
+        context.parse_config_one(Path("."), parse_profiles=True, parse_local=True)
 
     config = copy.deepcopy(context.config)
 
@@ -4054,7 +4058,7 @@ def parse_config(
             context.defaults = argparse.Namespace()
 
             with chdir(p if p.is_dir() else Path.cwd()):
-                if not context.parse_config_one(p if p.is_file() else Path("."), local=True):
+                if not context.parse_config_one(p if p.is_file() else Path("."), parse_local=True):
                     continue
 
             # Consolidate all settings into one namespace again.
@@ -4335,7 +4339,7 @@ def summary(config: Config) -> str:
 {bold(f"IMAGE: {config.image or 'default'}")}
 
     {bold("CONFIG")}:
-                            Profile: {none_to_none(config.profile)}
+                           Profiles: {line_join_list(config.profiles)}
                        Dependencies: {line_join_list(config.dependencies)}
                     Minimum Version: {none_to_none(config.minimum_version)}
                   Configure Scripts: {line_join_list(config.configure_scripts)}
index 1602fae77133f79f6222d2c7c31e75461824312f..14b9e945f6be7ffc3e511de592225548167a984e 100644 (file)
@@ -1743,7 +1743,7 @@ boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`,
 ### [Match] Section.
 
 `Profile=`
-:   Matches against the configured profile.
+:   Matches against the configured profiles.
 
 `Distribution=`
 :   Matches against the configured distribution.
@@ -1864,11 +1864,11 @@ config file is read:
 
 ### [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.d/*.conf` drop in configuration files.
+`Profiles=`, `--profile=`
+:   Select the given profiles. A profile is a configuration file or
+    directory in the `mkosi.profiles/` directory. The configuration files
+    and directories of each profile are included after parsing the
+    `mkosi.conf.d/*.conf` drop in configuration.
 
 `Dependencies=`, `--dependency=`
 :   The images that this image depends on specified as a comma-separated
@@ -2169,7 +2169,8 @@ Scripts executed by mkosi receive the following environment variables:
 * `$DISTRIBUTION_ARCHITECTURE` contains the architecture from
   `$ARCHITECTURE` in the format used by the configured distribution.
 
-* `$PROFILE` contains the profile from the `Profile=` setting.
+* `$PROFILES` contains the profiles from the `Profiles=` setting as a
+  comma-delimited string.
 
 * `$CACHED=` is set to `1` if a cached image is available, `0` otherwise.
 
@@ -2268,7 +2269,7 @@ Consult this table for which script receives which environment variables:
 | `DISTRIBUTION`              | ✓           | ✓      | ✓         | ✓       | ✓          | ✓          | ✓            | ✓       |
 | `DISTRIBUTION_ARCHITECTURE` | ✓           | ✓      | ✓         | ✓       | ✓          | ✓          | ✓            | ✓       |
 | `RELEASE`                   | ✓           | ✓      | ✓         | ✓       | ✓          | ✓          | ✓            | ✓       |
-| `PROFILE                  | ✓           | ✓      | ✓         | ✓       | ✓          | ✓          |              | ✓       |
+| `PROFILES`                  | ✓           | ✓      | ✓         | ✓       | ✓          | ✓          |              | ✓       |
 | `CACHED`                    |             | ✓      |           |         |            |            |              |         |
 | `CHROOT_SCRIPT`             |             |        | ✓         | ✓       | ✓          | ✓          |              |         |
 | `SRCDIR`                    | ✓           | ✓      | ✓         | ✓       | ✓          | ✓          | ✓            | ✓       |
index ed5f1e62e3a9da0996a30e3ff57083dd95abd7d1..b02dfdac8bd09794fc1d4f7a5a686ce1bc404c40 100644 (file)
@@ -361,7 +361,7 @@ def test_profiles(tmp_path: Path) -> None:
     (d / "mkosi.conf").write_text(
         """\
         [Config]
-        Profile=profile
+        Profiles=profile
         """
     )
 
@@ -376,7 +376,7 @@ def test_profiles(tmp_path: Path) -> None:
     with chdir(d):
         _, [config] = parse_config()
 
-    assert config.profile == "profile"
+    assert config.profiles == ["profile"]
     # The profile should override mkosi.conf.d/
     assert config.distribution == Distribution.fedora
     assert config.qemu_kvm == ConfigFeature.enabled
@@ -386,11 +386,34 @@ def test_profiles(tmp_path: Path) -> None:
     with chdir(d):
         _, [config] = parse_config(["--profile", "profile"])
 
-    assert config.profile == "profile"
+    assert config.profiles == ["profile"]
     # The profile should override mkosi.conf.d/
     assert config.distribution == Distribution.fedora
     assert config.qemu_kvm == ConfigFeature.enabled
 
+    (d / "mkosi.conf").write_text(
+        """\
+        [Config]
+        Profiles=profile,abc
+        """
+    )
+
+    (d / "mkosi.profiles/abc.conf").write_text(
+        """\
+        [Match]
+        Profile=abc
+
+        [Distribution]
+        Distribution=opensuse
+        """
+    )
+
+    with chdir(d):
+        _, [config] = parse_config()
+
+    assert config.profiles == ["profile", "abc"]
+    assert config.distribution == Distribution.opensuse
+
 
 def test_override_default(tmp_path: Path) -> None:
     d = tmp_path
index cb8673a41a1cf3a13cb7871cee1bd1458d1d4689..4dad9a9f24c27ce8b1f458c3d0058fa5b15eb517 100644 (file)
@@ -215,7 +215,9 @@ def test_config() -> None:
             "PrepareScripts": [
                 "/run/foo"
             ],
-            "Profile": "profile",
+            "Profiles": [
+                "profile"
+            ],
             "ProxyClientCertificate": "/my/client/cert",
             "ProxyClientKey": "/my/client/key",
             "ProxyExclude": [
@@ -442,7 +444,7 @@ def test_config() -> None:
         postinst_scripts=[Path("/bar/qux")],
         postoutput_scripts=[Path("/foo/src")],
         prepare_scripts=[Path("/run/foo")],
-        profile="profile",
+        profiles=["profile"],
         proxy_client_certificate=Path("/my/client/cert"),
         proxy_client_key=Path("/my/client/key"),
         proxy_exclude=["www.example.com"],