Skip to content

Commit

Permalink
Make SplitArtifacts= take a list of values
Browse files Browse the repository at this point in the history
This allows more precision on which artifacts are actually split out of
the image and placed into the output directory. Defaults to splitting
the UKI, vmlinuz and the initrd out.
  • Loading branch information
NekkoDroid committed Oct 18, 2024
1 parent 1c6da5a commit b00ebbb
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 25 deletions.
23 changes: 18 additions & 5 deletions mkosi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
from mkosi.config import (
PACKAGE_GLOBS,
Args,
ArtifactOutput,
Bootloader,
Cacheonly,
Compression,
Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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"):
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
62 changes: 55 additions & 7 deletions mkosi/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,23 @@ def native(cls) -> "Architecture":
return cls.from_uname(platform.machine())


def parse_boolean(s: str) -> bool:
class ArtifactOutput(enum.IntFlag):
uki = 1
kernel = 2
initrd = 4
partitions = 8

# Set a high bit to prevent these from showing up when only the previous values are set
no = 0b10000000000000000000000000000000 | uki | kernel | initrd
yes = 0b10000000000000000000000000000000 | uki | kernel | initrd | partitions

# For some reason CI attempts to use the __contains__ from "int" when done on this type
# so just redefined the __contains__ to make it unambiguous
def __contains__(self, other: "ArtifactOutput") -> bool:
return (self.value & other.value) == other.value


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()
Expand All @@ -509,7 +525,16 @@ 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}")
return None


def parse_boolean(s: str) -> bool:
value = try_parse_boolean(s)

if value is None:
die(f"Invalid boolean literal: {s!r}")

return value


def parse_path(
Expand Down Expand Up @@ -1291,6 +1316,25 @@ 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

# Keep for backwards compatibility
boolean_value = try_parse_boolean(value)
if boolean_value is not 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()
Expand Down Expand Up @@ -1596,7 +1640,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]
Expand Down Expand Up @@ -2292,11 +2336,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",
Expand Down Expand Up @@ -4429,6 +4473,10 @@ def line_join_list(array: Iterable[object]) -> str:
return "\n ".join(str(item) for item in array) if array else "none"


def int_flag_names(flag: enum.IntFlag) -> str:
return none_to_none(flag.name).replace("|", "\n ")


def format_bytes(num_bytes: int) -> str:
if num_bytes >= 1024**3:
return f"{num_bytes/1024**3 :0.1f}G"
Expand Down Expand Up @@ -4508,7 +4556,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: {int_flag_names(config.split_artifacts)}
Repart Directories: {line_join_list(config.repart_dirs)}
Sector Size: {none_to_default(config.sector_size)}
Overlay: {yes_no(config.overlay)}
Expand Down
17 changes: 12 additions & 5 deletions mkosi/resources/man/mkosi.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions mkosi/sysupdate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
55 changes: 52 additions & 3 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from mkosi import expand_kernel_specifiers
from mkosi.config import (
Architecture,
ArtifactOutput,
Compression,
Config,
ConfigFeature,
Expand Down Expand Up @@ -235,19 +236,19 @@ def test_parse_config(tmp_path: Path) -> None:
with chdir(d):
_, [config] = parse_config()
assert config.bootable == ConfigFeature.auto
assert config.split_artifacts is False
assert config.split_artifacts == ArtifactOutput.no

# 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.yes
# 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.no

(d / "mkosi.images").mkdir()

Expand Down Expand Up @@ -1277,3 +1278,51 @@ def test_mkosi_version_executable(tmp_path: Path) -> None:
with chdir(d):
_, [config] = parse_config()
assert config.image_version == "1.2.3"


def test_split_artifacts(tmp_path: Path) -> None:
d = tmp_path

(d / "mkosi.conf").write_text(
"""
[Output]
SplitArtifacts=uki
"""
)

with chdir(d):
_, [config] = parse_config()
assert config.split_artifacts == ArtifactOutput.uki

(d / "mkosi.conf").write_text(
"""
[Output]
SplitArtifacts=uki
SplitArtifacts=kernel
SplitArtifacts=initrd
"""
)

with chdir(d):
_, [config] = parse_config()
assert config.split_artifacts == ArtifactOutput.uki | ArtifactOutput.kernel | ArtifactOutput.initrd


def test_split_artifacts_compat(tmp_path: Path) -> None:
d = tmp_path

with chdir(d):
_, [config] = parse_config()
assert config.split_artifacts == ArtifactOutput.no

(d / "mkosi.conf").write_text(
"""
[Output]
SplitArtifacts=yes
"""
)

with chdir(d):
"mkosi.conf"
_, [config] = parse_config()
assert config.split_artifacts == ArtifactOutput.yes
5 changes: 3 additions & 2 deletions tests/test_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from mkosi.config import (
Architecture,
Args,
ArtifactOutput,
BiosBootloader,
Bootloader,
Cacheonly,
Expand Down Expand Up @@ -333,7 +334,7 @@ def test_config() -> None:
}
],
"SourceDateEpoch": 12345,
"SplitArtifacts": true,
"SplitArtifacts": 3,
"Ssh": false,
"SshCertificate": "/path/to/cert",
"SshKey": null,
Expand Down Expand Up @@ -533,7 +534,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.uki | ArtifactOutput.kernel,
ssh=False,
ssh_certificate=Path("/path/to/cert"),
ssh_key=None,
Expand Down

0 comments on commit b00ebbb

Please sign in to comment.