`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
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:
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.
**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")
**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(
**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")
**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")
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,
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,
)
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]
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]:
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",
access the value from context.
"""
- profile: Optional[str]
+ profiles: list[str]
files: list[Path]
dependencies: list[str]
minimum_version: Optional[GenericVersion]
),
# 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",
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,
),
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()
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()
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()):
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
# 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)
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.
{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)}
### [Match] Section.
`Profile=`
-: Matches against the configured profile.
+: Matches against the configured profiles.
`Distribution=`
: Matches against the configured distribution.
### [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
* `$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.
| `DISTRIBUTION` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| `DISTRIBUTION_ARCHITECTURE` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| `RELEASE` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
-| `PROFILE` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | ✓ |
+| `PROFILES` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | ✓ |
| `CACHED` | | ✓ | | | | | | |
| `CHROOT_SCRIPT` | | | ✓ | ✓ | ✓ | ✓ | | |
| `SRCDIR` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
(d / "mkosi.conf").write_text(
"""\
[Config]
- Profile=profile
+ Profiles=profile
"""
)
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
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
"PrepareScripts": [
"/run/foo"
],
- "Profile": "profile",
+ "Profiles": [
+ "profile"
+ ],
"ProxyClientCertificate": "/my/client/cert",
"ProxyClientKey": "/my/client/key",
"ProxyExclude": [
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"],