Skip to content

Commit

Permalink
Define our own config for PE addons and UKI profiles
Browse files Browse the repository at this point in the history
ukify's config parser uses python's configparser module and as such
suffers from all its issues just like we used to in mkosi. Having ukify
parse the config file also means that we have to make sure any paths
configured in the profile are available in the sandbox.

Instead, let's define our own configs for the PE addons and UKI profiles
so we get to take advantage of our own config file parser and have full
knowledge of all the configured settings so we can mount extra stuff into
the sandbox if needed.

It also gets rid of the hack where we parse ukify's config file to figure
out the command line.
  • Loading branch information
DaanDeMeyer committed Oct 6, 2024
1 parent b11b0f2 commit a0efc1b
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 41 deletions.
50 changes: 29 additions & 21 deletions mkosi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@
format_bytes,
parse_boolean,
parse_config,
parse_ini,
summary,
systemd_tool_version,
want_selinux_relabel,
Expand Down Expand Up @@ -1487,7 +1486,7 @@ def run_ukify(
stub: Path,
output: Path,
*,
cmdline: str = "",
cmdline: Sequence[str] = (),
arguments: Sequence[PathString] = (),
options: Sequence[PathString] = (),
sign: bool = True,
Expand All @@ -1499,11 +1498,9 @@ def run_ukify(
if not (arch := context.config.architecture.to_efi()):
die(f"Architecture {context.config.architecture} does not support UEFI")

cmdline = cmdline.strip()

# Older versions of systemd-stub expect the cmdline section to be null terminated. We can't
# embed NUL terminators in argv so let's communicate the cmdline via a file instead.
(context.workspace / "cmdline").write_text(f"{cmdline}\x00")
(context.workspace / "cmdline").write_text(f"{' '.join(cmdline)}\x00")

cmd = [
python_binary(context.config, binary=ukify),
Expand Down Expand Up @@ -1645,7 +1642,7 @@ def build_uki(
options += ["--ro-bind", initrd, workdir(initrd)]

with complete_step(f"Generating unified kernel image for kernel version {kver}"):
run_ukify(context, stub, output, cmdline=" ".join(cmdline), arguments=arguments, options=options)
run_ukify(context, stub, output, cmdline=cmdline, arguments=arguments, options=options)


def systemd_stub_binary(context: Context) -> Path:
Expand Down Expand Up @@ -1977,15 +1974,14 @@ def install_pe_addons(context: Context) -> None:
addon_dir.mkdir(parents=True, exist_ok=True)

for addon in context.config.pe_addons:
output = addon_dir / addon.with_suffix(".addon.efi").name
output = addon_dir / f"{addon.output}.addon.efi"

with complete_step(f"Generating PE addon /{output.relative_to(context.root)}"):
run_ukify(
context,
stub,
output,
arguments=["--config", workdir(addon)],
options=["--ro-bind", addon, workdir(addon)],
cmdline=addon.cmdline,
)


Expand All @@ -2008,25 +2004,23 @@ def build_uki_profiles(context: Context, cmdline: Sequence[str]) -> list[Path]:
profiles = []

for profile in context.config.unified_kernel_image_profiles:
output = context.workspace / "uki-profiles" / profile.with_suffix(".efi").name

# We want to append the cmdline from the ukify config file to the base kernel command line so parse
# it from the ukify config file and append it to our own kernel command line.
id = profile.profile["ID"]
output = context.workspace / f"uki-profiles/{id}.efi"

profile_cmdline = ""
profile_section = context.workspace / f"uki-profiles/{id}.profile"

for section, k, v in parse_ini(profile):
if section == "UKI" and k == "Cmdline":
profile_cmdline = v.replace("\n", " ")
with profile_section.open("w") as f:
for k, v in profile.profile.items():
f.write(f"{k}={v}\n")

with complete_step(f"Generating UKI profile '{profile.stem}'"):
with complete_step(f"Generating UKI profile '{id}'"):
run_ukify(
context,
stub,
output,
cmdline=f"{' '.join(cmdline)} {profile_cmdline}",
arguments=["--config", workdir(profile)],
options=["--ro-bind", profile, workdir(profile)],
cmdline=[*cmdline, *profile.cmdline],
arguments=["--profile", f"@{profile_section}"],
options=["--ro-bind", profile_section, profile_section],
sign=False,
)

Expand Down Expand Up @@ -2410,6 +2404,20 @@ def check_inputs(config: Config) -> None:
if config.secure_boot_key_source != config.sign_expected_pcr_key_source:
die("Secure boot key source and expected PCR signatures key source have to be the same")

for addon in config.pe_addons:
if not addon.output:
die(
"PE addon configured without output filename",
hint="Use Output= to configure the output filename",
)

for profile in config.unified_kernel_image_profiles:
if "ID" not in profile.profile:
die(
"UKI Profile is missing ID key in its .profile section",
hint="Use Profile= to configure the profile ID",
)


def check_tool(config: Config, *tools: PathString, reason: str, hint: Optional[str] = None) -> Path:
tool = config.find_binary(*tools)
Expand Down
115 changes: 101 additions & 14 deletions mkosi/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -887,8 +887,8 @@ def config_match_enum(match: str, value: StrEnum) -> bool:


def config_make_list_parser(
delimiter: str,
*,
delimiter: Optional[str] = None,
parse: Callable[[str], Any] = str,
unescape: bool = False,
reset: bool = True,
Expand All @@ -904,13 +904,15 @@ def config_parse_list(value: Optional[str], old: Optional[list[Any]]) -> Optiona
if unescape:
lex = shlex.shlex(value, posix=True)
lex.whitespace_split = True
lex.whitespace = f"\n{delimiter}"
lex.whitespace = f"\n{delimiter or ''}"
lex.commenters = ""
values = list(lex)
if reset and not values:
return None
else:
values = value.replace(delimiter, "\n").split("\n")
if delimiter:
value = value.replace(delimiter, "\n")
values = value.split("\n")
if reset and len(values) == 1 and values[0] == "":
return None

Expand Down Expand Up @@ -947,8 +949,8 @@ def config_match_version(match: str, value: str) -> bool:


def config_make_dict_parser(
delimiter: str,
*,
delimiter: Optional[str] = None,
parse: Callable[[str], tuple[str, Any]],
unescape: bool = False,
allow_paths: bool = False,
Expand Down Expand Up @@ -985,13 +987,15 @@ def config_parse_dict(value: Optional[str], old: Optional[dict[str, Any]]) -> Op
if unescape:
lex = shlex.shlex(value, posix=True)
lex.whitespace_split = True
lex.whitespace = f"\n{delimiter}"
lex.whitespace = f"\n{delimiter or ''}"
lex.commenters = ""
values = list(lex)
if reset and not values:
return None
else:
values = value.replace(delimiter, "\n").split("\n")
if delimiter:
value = value.replace(delimiter, "\n")
values = value.split("\n")
if reset and len(values) == 1 and values[0] == "":
return None

Expand All @@ -1007,7 +1011,7 @@ def parse_environment(value: str) -> tuple[str, str]:
return (key, value)


def parse_credential(value: str) -> tuple[str, str]:
def parse_key_value(value: str) -> tuple[str, str]:
key, _, value = value.partition("=")
key, value = key.strip(), value.strip()
return (key, value)
Expand Down Expand Up @@ -1512,6 +1516,45 @@ def key_transformer(k: str) -> str:
)


@dataclasses.dataclass(frozen=True)
class PEAddon:
output: str
cmdline: list[str]


@dataclasses.dataclass(frozen=True)
class UKIProfile:
profile: dict[str, str]
cmdline: list[str]


def make_simple_config_parser(settings: Sequence[ConfigSetting], type: type[Any]) -> Callable[[str], Any]:
lookup = {s.name: s for s in settings}

def parse_simple_config(value: str) -> Any:
path = parse_path(value)
config = argparse.Namespace()

for section, name, value in parse_ini(path, only_sections=[s.section for s in settings]):
if not name and not value:
continue

if not (s := lookup.get(name)):
die(f"Unknown setting {name}")

if section != s.section:
logging.warning(f"Setting {name} should be configured in [{s.section}], not [{section}].")

if name != s.name:
logging.warning(f"Setting {name} is deprecated, please use {s.name} instead.")

setattr(config, s.dest, s.parse(value, getattr(config, s.dest, None)))

return type(**{k: v for k, v in vars(config).items() if k in inspect.signature(type).parameters})

return parse_simple_config


@dataclasses.dataclass(frozen=True)
class Config:
"""Type-hinted storage for command line arguments.
Expand Down Expand Up @@ -1584,7 +1627,7 @@ class Config:
shim_bootloader: ShimBootloader
unified_kernel_images: ConfigFeature
unified_kernel_image_format: str
unified_kernel_image_profiles: list[Path]
unified_kernel_image_profiles: list[UKIProfile]
initrds: list[Path]
initrd_packages: list[str]
initrd_volatile_packages: list[str]
Expand All @@ -1593,7 +1636,7 @@ class Config:
kernel_modules_include: list[str]
kernel_modules_exclude: list[str]
kernel_modules_include_host: bool
pe_addons: list[Path]
pe_addons: list[PEAddon]

kernel_modules_initrd: bool
kernel_modules_initrd_include: list[str]
Expand Down Expand Up @@ -1981,6 +2024,35 @@ def parse_ini(path: Path, only_sections: Collection[str] = ()) -> Iterator[tuple
yield section, "", ""


PE_ADDON_SETTINGS = (
ConfigSetting(
dest="output",
section="Addon",
parse=config_make_filename_parser("Output= requires a filename with no path components."),
default="",
),
ConfigSetting(
dest="cmdline",
section="Addon",
parse=config_make_list_parser(delimiter=" "),
),
)


UKI_PROFILE_SETTINGS = (
ConfigSetting(
dest="profile",
section="Profile",
parse=config_make_dict_parser(parse=parse_key_value),
),
ConfigSetting(
dest="cmdline",
section="Profile",
parse=config_make_list_parser(delimiter=" "),
),
)


SETTINGS = (
# Include section
ConfigSetting(
Expand Down Expand Up @@ -2501,7 +2573,10 @@ def parse_ini(path: Path, only_sections: Collection[str] = ()) -> Iterator[tuple
long="--uki-profile",
metavar="PATH",
section="Content",
parse=config_make_list_parser(delimiter=",", parse=make_path_parser()),
parse=config_make_list_parser(
delimiter=",",
parse=make_simple_config_parser(UKI_PROFILE_SETTINGS, UKIProfile),
),
recursive_paths=("mkosi.uki-profiles/",),
help="Configuration files to generate UKI profiles",
),
Expand Down Expand Up @@ -2571,7 +2646,10 @@ def parse_ini(path: Path, only_sections: Collection[str] = ()) -> Iterator[tuple
long="--pe-addon",
metavar="PATH",
section="Content",
parse=config_make_list_parser(delimiter=",", parse=make_path_parser()),
parse=config_make_list_parser(
delimiter=",",
parse=make_simple_config_parser(PE_ADDON_SETTINGS, PEAddon),
),
recursive_paths=("mkosi.pe-addons/",),
help="Configuration files to generate PE addons",
),
Expand Down Expand Up @@ -3153,9 +3231,7 @@ def parse_ini(path: Path, only_sections: Collection[str] = ()) -> Iterator[tuple
long="--credential",
metavar="NAME=VALUE",
section="Host",
parse=config_make_dict_parser(
delimiter=" ", parse=parse_credential, allow_paths=True, unescape=True
),
parse=config_make_dict_parser(delimiter=" ", parse=parse_key_value, allow_paths=True, unescape=True),
help="Pass a systemd credential to systemd-nspawn or qemu",
paths=("mkosi.credentials",),
),
Expand Down Expand Up @@ -4657,6 +4733,15 @@ def key_source_transformer(keysource: dict[str, Any], fieldtype: type[KeySource]
assert "Type" in keysource
return KeySource(type=KeySourceType(keysource["Type"]), source=keysource.get("Source", ""))

def pe_addon_transformer(addons: list[dict[str, Any]], fieldtype: type[PEAddon]) -> list[PEAddon]:
return [PEAddon(output=addon["Output"], cmdline=addon["Cmdline"]) for addon in addons]

def uki_profile_transformer(
profiles: list[dict[str, Any]],
fieldtype: type[UKIProfile],
) -> list[UKIProfile]:
return [UKIProfile(profile=profile["Profile"], cmdline=profile["Cmdline"]) for profile in profiles]

# The type of this should be
# dict[
# type,
Expand Down Expand Up @@ -4696,6 +4781,8 @@ def key_source_transformer(keysource: dict[str, Any], fieldtype: type[KeySource]
Network: enum_transformer,
KeySource: key_source_transformer,
Vmm: enum_transformer,
list[PEAddon]: pe_addon_transformer,
list[UKIProfile]: uki_profile_transformer,
}

def json_transformer(key: str, val: Any) -> Any:
Expand Down
Loading

0 comments on commit a0efc1b

Please sign in to comment.