From: Daan De Meyer Date: Sat, 21 Sep 2024 11:42:08 +0000 (+0200) Subject: Allow configuring more than one profile X-Git-Tag: v25~273^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=refs%2Fpull%2F3057%2Fhead;p=thirdparty%2Fmkosi.git Allow configuring more than one profile 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. --- diff --git a/NEWS.md b/NEWS.md index ad08c83b7..2ba680888 100644 --- 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 diff --git a/mkosi/__init__.py b/mkosi/__init__.py index 327d2e469..2136cb885 100644 --- a/mkosi/__init__.py +++ b/mkosi/__init__.py @@ -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, diff --git a/mkosi/config.py b/mkosi/config.py index 34c1e0de8..bb76f01fd 100644 --- a/mkosi/config.py +++ b/mkosi/config.py @@ -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)} diff --git a/mkosi/resources/man/mkosi.md b/mkosi/resources/man/mkosi.md index 1602fae77..14b9e945f 100644 --- a/mkosi/resources/man/mkosi.md +++ b/mkosi/resources/man/mkosi.md @@ -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` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | diff --git a/tests/test_config.py b/tests/test_config.py index ed5f1e62e..b02dfdac8 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -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 diff --git a/tests/test_json.py b/tests/test_json.py index cb8673a41..4dad9a9f2 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -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"],