Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Define our own config for PE addons and UKI profiles #3106

Merged
merged 1 commit into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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="PEAddon",
parse=config_make_filename_parser("Output= requires a filename with no path components."),
default="",
),
ConfigSetting(
dest="cmdline",
section="PEAddon",
parse=config_make_list_parser(delimiter=" "),
),
)


UKI_PROFILE_SETTINGS = (
ConfigSetting(
dest="profile",
section="UKIProfile",
parse=config_make_dict_parser(parse=parse_key_value),
),
ConfigSetting(
dest="cmdline",
section="UKIProfile",
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