diff --git a/mkosi/__init__.py b/mkosi/__init__.py index fe8453898..768960a75 100644 --- a/mkosi/__init__.py +++ b/mkosi/__init__.py @@ -50,6 +50,7 @@ from mkosi.config import ( PACKAGE_GLOBS, Args, + ArtifactOutput, Bootloader, Cacheonly, Compression, @@ -2100,8 +2101,11 @@ def make_uki( output, ) - extract_pe_section(context, output, ".linux", context.staging / context.config.output_split_kernel) - extract_pe_section(context, output, ".initrd", context.staging / context.config.output_split_initrd) + if ArtifactOutput.kernel in context.config.split_artifacts: + extract_pe_section(context, output, ".linux", context.staging / context.config.output_split_kernel) + + if ArtifactOutput.initrd in context.config.split_artifacts: + extract_pe_section(context, output, ".initrd", context.staging / context.config.output_split_initrd) def compressor_command(context: Context, compression: Compression) -> list[PathString]: @@ -2178,6 +2182,9 @@ def get_uki(context: Context) -> Optional[Path]: def copy_uki(context: Context) -> None: + if ArtifactOutput.uki not in context.config.split_artifacts: + return + if (context.staging / context.config.output_split_uki).exists(): return @@ -2186,6 +2193,9 @@ def copy_uki(context: Context) -> None: def copy_vmlinuz(context: Context) -> None: + if ArtifactOutput.kernel not in context.config.split_artifacts: + return + if (context.staging / context.config.output_split_kernel).exists(): return @@ -2201,6 +2211,9 @@ def copy_vmlinuz(context: Context) -> None: def copy_initrd(context: Context) -> None: + if ArtifactOutput.initrd not in context.config.split_artifacts: + return + if not want_initrd(context): return @@ -3378,7 +3391,7 @@ def make_extension_image(context: Context, output: Path) -> None: ] # fmt: skip if context.config.sector_size: cmdline += ["--sector-size", str(context.config.sector_size)] - if context.config.split_artifacts: + if ArtifactOutput.partitions in context.config.split_artifacts: cmdline += ["--split=yes"] with complete_step(f"Building {context.config.output_format} extension image"): @@ -3400,7 +3413,7 @@ def make_extension_image(context: Context, output: Path) -> None: logging.debug(json.dumps(j, indent=4)) - if context.config.split_artifacts: + if ArtifactOutput.partitions in context.config.split_artifacts: for p in (Partition.from_dict(d) for d in j): if p.split_path: maybe_compress(context, context.config.compress_output, p.split_path) @@ -3645,7 +3658,7 @@ def build_image(context: Context) -> None: partitions = make_disk(context, msg="Formatting ESP/XBOOTLDR partitions") grub_bios_setup(context, partitions) - if context.config.split_artifacts: + if ArtifactOutput.partitions in context.config.split_artifacts: make_disk(context, split=True, msg="Extracting partitions") copy_nspawn_settings(context) diff --git a/mkosi/config.py b/mkosi/config.py index 0bb255685..35d2a986b 100644 --- a/mkosi/config.py +++ b/mkosi/config.py @@ -499,7 +499,16 @@ def native(cls) -> "Architecture": return cls.from_uname(platform.machine()) -def parse_boolean(s: str) -> bool: +class ArtifactOutput(enum.Flag): + uki = 1 + kernel = 2 + initrd = 4 + partitions = 8 + no = uki | kernel | initrd + yes = uki | kernel | initrd | partitions + + +def try_parse_boolean(s: str) -> Optional[bool]: "Parse 1/true/yes/y/t/on as true and 0/false/no/n/f/off/None as false" s_l = s.lower() @@ -509,7 +518,14 @@ def parse_boolean(s: str) -> bool: if s_l in {"0", "false", "no", "n", "f", "off", "never"}: return False - die(f"Invalid boolean literal: {s!r}") + +def parse_boolean(s: str) -> bool: + value = try_parse_boolean(s) + + if value == None: + die(f"Invalid boolean literal: {s!r}") + + return value def parse_path( @@ -1291,6 +1307,21 @@ def config_parse_key_source(value: Optional[str], old: Optional[KeySource]) -> O return KeySource(type=type, source=source) +def config_parse_artifact_output(value: Optional[str], old: Optional[ArtifactOutput]) -> Optional[ArtifactOutput]: + if not value: + return None + + boolean_value = try_parse_boolean(value) + if boolean_value != None: + return ArtifactOutput.yes if boolean_value else ArtifactOutput.no + + try: + new = ArtifactOutput[value] + except KeyError: + die(f"'{value}' is not a valid {ArtifactOutput.__name__}") + + return old | new if old else new + class SettingScope(StrEnum): # Not passed down to subimages local = enum.auto() @@ -1596,7 +1627,7 @@ class Config: output_mode: Optional[int] image_id: Optional[str] image_version: Optional[str] - split_artifacts: bool + split_artifacts: ArtifactOutput repart_dirs: list[Path] sysupdate_dir: Optional[Path] sector_size: Optional[int] @@ -2292,11 +2323,11 @@ def parse_ini(path: Path, only_sections: Collection[str] = ()) -> Iterator[tuple ), ConfigSetting( dest="split_artifacts", - metavar="BOOL", nargs="?", section="Output", - parse=config_parse_boolean, - help="Generate split partitions", + parse=config_parse_artifact_output, + default=ArtifactOutput.no, + help="Split artifacts out of the final image", ), ConfigSetting( dest="repart_dirs", @@ -4508,7 +4539,7 @@ def summary(config: Config) -> str: Output Mode: {format_octal_or_default(config.output_mode)} Image ID: {config.image_id} Image Version: {config.image_version} - Split Artifacts: {yes_no(config.split_artifacts)} + Split Artifacts: {config.split_artifacts} Repart Directories: {line_join_list(config.repart_dirs)} Sector Size: {none_to_default(config.sector_size)} Overlay: {yes_no(config.overlay)} @@ -4783,6 +4814,9 @@ def uki_profile_transformer( ) -> list[UKIProfile]: return [UKIProfile(profile=profile["Profile"], cmdline=profile["Cmdline"]) for profile in profiles] + def artifact_output_transformer(output: str, fieldtype: type[ArtifactOutput]) -> ArtifactOutput: + return ArtifactOutput[output] + # The type of this should be # dict[ # type, @@ -4824,6 +4858,7 @@ def uki_profile_transformer( Vmm: enum_transformer, list[PEAddon]: pe_addon_transformer, list[UKIProfile]: uki_profile_transformer, + ArtifactOutput: artifact_output_transformer, } def json_transformer(key: str, val: Any) -> Any: diff --git a/mkosi/resources/man/mkosi.1.md b/mkosi/resources/man/mkosi.1.md index 0d2c29f98..aeb27b9ba 100644 --- a/mkosi/resources/man/mkosi.1.md +++ b/mkosi/resources/man/mkosi.1.md @@ -596,13 +596,20 @@ boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`, invoked. The image ID is automatically added to `/usr/lib/os-release`. `SplitArtifacts=`, `--split-artifacts` -: If specified and building a disk image, pass `--split=yes` to systemd-repart - to have it write out split partition files for each configured partition. - Read the [man](https://www.freedesktop.org/software/systemd/man/systemd-repart.html#--split=BOOL) +: The artifact types to split out of the final image. A comma-delimited + list consisting of `uki`, `kernel`, `initrd` and `partitions`. When + building a bootable image `kernel` and `initrd` correspond to their + artifact found in the image (or in the UKI), while `uki` copies out the + entire UKI. + + When building a disk image and `partitions` is specified, + pass `--split=yes` to systemd-repart to have it write out split partition + files for each configured partition. Read the + [man](https://www.freedesktop.org/software/systemd/man/systemd-repart.html#--split=BOOL) page for more information. This is useful in A/B update scenarios where an existing disk image shall be augmented with a new version of a root or `/usr` partition along with its Verity partition and unified - kernel. + kernel. By default `uki`, `kernel` and `initrd` are split out. `RepartDirectories=`, `--repart-dir=` : Paths to directories containing systemd-repart partition definition @@ -2229,7 +2236,7 @@ current working directory. The following scripts are supported: * If **`mkosi.clean`** (`CleanScripts=`) exists, it is executed right after the outputs of a previous build have been cleaned up. A clean script can clean up any outputs that mkosi does not know about (e.g. - artifacts from `SplitArtifacts=yes` or RPMs built in a build script). + artifacts from `SplitArtifacts=partitions` or RPMs built in a build script). Note that this script does not use the tools tree even if one is configured. * If **`mkosi.version`** exists and is executable, it is run during diff --git a/mkosi/sysupdate.py b/mkosi/sysupdate.py index 5cb216817..ea18b149e 100644 --- a/mkosi/sysupdate.py +++ b/mkosi/sysupdate.py @@ -4,15 +4,15 @@ import sys from pathlib import Path -from mkosi.config import Args, Config +from mkosi.config import Args, ArtifactOutput, Config from mkosi.log import die from mkosi.run import run from mkosi.types import PathString def run_sysupdate(args: Args, config: Config) -> None: - if not config.split_artifacts: - die("SplitArtifacts= must be enabled to be able to use mkosi sysupdate") + if ArtifactOutput.partitions not in config.split_artifacts: + die("SplitArtifacts=partitions must be set to be able to use mkosi sysupdate") if not config.sysupdate_dir: die( diff --git a/tests/test_config.py b/tests/test_config.py index 4cca01157..4f4075c18 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -12,6 +12,7 @@ from mkosi import expand_kernel_specifiers from mkosi.config import ( Architecture, + ArtifactOutput, Compression, Config, ConfigFeature, @@ -228,26 +229,26 @@ def test_parse_config(tmp_path: Path) -> None: (d / "abc/mkosi.conf.d/abc.conf").write_text( """\ [Output] - SplitArtifacts=yes + SplitArtifacts=partitions """ ) with chdir(d): _, [config] = parse_config() assert config.bootable == ConfigFeature.auto - assert config.split_artifacts is False + assert config.split_artifacts == [ArtifactOutput.uki, ArtifactOutput.kernel, ArtifactOutput.initrd] # Passing the directory should include both the main config file and the dropin. _, [config] = parse_config(["--include", os.fspath(d / "abc")] * 2) assert config.bootable == ConfigFeature.enabled - assert config.split_artifacts is True + assert config.split_artifacts == [ArtifactOutput.partitions] # The same extra config should not be parsed more than once. assert config.build_packages == ["abc"] # Passing the main config file should not include the dropin. _, [config] = parse_config(["--include", os.fspath(d / "abc/mkosi.conf")]) assert config.bootable == ConfigFeature.enabled - assert config.split_artifacts is False + assert config.split_artifacts == [ArtifactOutput.uki, ArtifactOutput.kernel, ArtifactOutput.initrd] (d / "mkosi.images").mkdir() diff --git a/tests/test_json.py b/tests/test_json.py index 3b64356d0..4f240a5f3 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -11,6 +11,7 @@ from mkosi.config import ( Architecture, Args, + ArtifactOutput, BiosBootloader, Bootloader, Cacheonly, @@ -333,7 +334,11 @@ def test_config() -> None: } ], "SourceDateEpoch": 12345, - "SplitArtifacts": true, + "SplitArtifacts": [ + "kernel", + "initrd", + "partitions" + ], "Ssh": false, "SshCertificate": "/path/to/cert", "SshKey": null, @@ -533,7 +538,7 @@ def test_config() -> None: sign_expected_pcr_certificate=Path("/my/cert"), skeleton_trees=[ConfigTree(Path("/foo/bar"), Path("/")), ConfigTree(Path("/bar/baz"), Path("/qux"))], source_date_epoch=12345, - split_artifacts=True, + split_artifacts=ArtifactOutput.yes, ssh=False, ssh_certificate=Path("/path/to/cert"), ssh_key=None,