Skip to content

Commit

Permalink
Allow configuring more than one profile
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
DaanDeMeyer committed Sep 21, 2024
1 parent 457a115 commit c6d38f2
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 56 deletions.
32 changes: 16 additions & 16 deletions mkosi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -3852,8 +3852,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,
Expand Down
62 changes: 34 additions & 28 deletions mkosi/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
)
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]
Expand Down Expand Up @@ -580,8 +582,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] = str) -> 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]:
Expand Down Expand Up @@ -1110,10 +1115,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",
Expand Down Expand Up @@ -1480,7 +1482,7 @@ class Config:
access the value from context.
"""

profile: Optional[str]
profiles: list[str]
files: list[Path]
dependencies: list[str]
minimum_version: Optional[GenericVersion]
Expand Down Expand Up @@ -1955,13 +1957,15 @@ def parse_ini(path: Path, only_sections: Collection[str] = ()) -> Iterator[tuple
),
# 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",
Expand Down Expand Up @@ -2067,7 +2071,7 @@ def parse_ini(path: Path, only_sections: Collection[str] = ()) -> Iterator[tuple
metavar="REPOS",
section="Distribution",
parse=config_make_list_parser(delimiter=","),
match=config_match_repositories,
match=config_make_list_matcher(),
help="Repositories to use",
scope=SettingScope.universal,
),
Expand Down Expand Up @@ -3780,7 +3784,7 @@ def match_config(self, path: Path) -> bool:

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()

Expand All @@ -3791,7 +3795,7 @@ def parse_config_one(self, path: Path, profiles: bool = False, local: bool = Fal
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()
Expand Down Expand Up @@ -3874,30 +3878,32 @@ def parse_config_one(self, path: Path, profiles: bool = False, local: bool = Fal
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]

setattr(self.config, "profiles", profiles)

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":
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

Expand Down Expand Up @@ -3985,7 +3991,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)

Expand Down Expand Up @@ -4057,7 +4063,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.
Expand Down Expand Up @@ -4338,7 +4344,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)}
Expand Down
15 changes: 8 additions & 7 deletions mkosi/resources/man/mkosi.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down
29 changes: 26 additions & 3 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ def test_profiles(tmp_path: Path) -> None:
(d / "mkosi.conf").write_text(
"""\
[Config]
Profile=profile
Profiles=profile
"""
)

Expand All @@ -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
Expand All @@ -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.conf.d/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
Expand Down
6 changes: 4 additions & 2 deletions tests/test_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,9 @@ def test_config() -> None:
"PrepareScripts": [
"/run/foo"
],
"Profile": "profile",
"Profiles": [
"profile"
],
"ProxyClientCertificate": "/my/client/cert",
"ProxyClientKey": "/my/client/key",
"ProxyExclude": [
Expand Down Expand Up @@ -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"],
Expand Down

0 comments on commit c6d38f2

Please sign in to comment.