`Bootable=`, `--bootable=`
-: Takes a boolean or `auto`. Enables or disable generating of a bootable
+: Takes a boolean or `auto`. Enables or disables generation of a bootable
image. If enabled, mkosi will install systemd-boot, run kernel-install,
generate unified kernel images for installed kernels and add an ESP
partition when the disk image output is used. If systemd-boot is not
executed, no unified kernel images will be generated and no ESP partition
will be added to the image if the disk output format is used.
+`UseSubvolumes=`, `--use-subvolumes=`
+
+: Takes a boolean or `auto`. Enables or disables use of btrfs subvolumes for
+ directory tree outputs. If enabled, mkosi will create the root directory as
+ a btrfs subvolume and use btrfs subvolume snapshots where possible to copy
+ base or cached trees which is much faster than doing a recursive copy. If
+ explicitly enabled and `btrfs` is not installed or subvolumes cannot be
+ created, an error is raised. If `auto`, missing `btrfs` or failures to
+ create subvolumes are ignored.
+
`KernelCommandLine=`, `--kernel-command-line=`
: Use the specified kernel command line when building images. By default
from textwrap import dedent
from typing import Callable, ContextManager, Optional, TextIO, TypeVar, Union, cast
+from mkosi.btrfs import btrfs_maybe_snapshot_subvolume
from mkosi.config import (
ConfigFeature,
GenericVersion,
@contextlib.contextmanager
def mount_cache_overlay(state: MkosiState) -> Iterator[None]:
- if not state.config.incremental:
+ if not state.config.incremental or not any(state.root.iterdir()):
yield
return
- with mount_overlay([state.root], state.cache_overlay, state.workdir, state.root, read_only=False):
+ d = state.workspace / "cache-overlay"
+ d.mkdir(mode=0o755, exist_ok=True)
+
+ with mount_overlay([state.root], d, state.workdir, state.root, read_only=False):
yield
def mount_build_overlay(state: MkosiState, read_only: bool = False) -> ContextManager[Path]:
- return mount_overlay([state.root], state.build_overlay, state.workdir, state.root, read_only)
+ d = state.workspace / "build-overlay"
+ if not d.is_symlink():
+ d.mkdir(mode=0o755, exist_ok=True)
+ return mount_overlay([state.root], state.workspace.joinpath("build-overlay"), state.workdir, state.root, read_only)
def run_prepare_script(state: MkosiState, build: bool) -> None:
if not state.config.base_trees or state.config.overlay:
return
+ # We only do this step once, even if we're doing cached images. If we're doing a cached image on top of
+ # base trees, the cached image is an overlay on top of the base trees, so the unpacked base trees are
+ # guaranteed to still be pristine when we run the second time to generate the final image.
+ if any(state.root.iterdir()):
+ return
+
with complete_step("Copying in base trees…"):
for path in state.config.base_trees:
-
if path.is_dir():
- copy_path(path, state.root)
+ btrfs_maybe_snapshot_subvolume(state.config, path, state.root)
elif path.suffix == ".tar":
shutil.unpack_archive(path, state.root)
elif path.suffix == ".raw":
with complete_step("Installing cache copies"):
unlink_try_hard(final)
- shutil.move(state.cache_overlay, final)
+
+ # We only use the cache-overlay directory for caching if we have a base tree, otherwise we just
+ # cache the root directory.
+ if state.workspace.joinpath("cache-overlay").exists():
+ shutil.move(state.workspace / "cache-overlay", final)
+ else:
+ shutil.move(state.root, final)
if state.config.build_script:
unlink_try_hard(build)
- shutil.move(state.build_overlay, build)
+ shutil.move(state.workspace / "build-overlay", build)
def dir_size(path: Union[Path, os.DirEntry[str]]) -> int:
return False
with complete_step("Copying cached trees"):
- copy_path(final, state.root)
+ btrfs_maybe_snapshot_subvolume(state.config, final, state.root)
if state.config.build_script:
- state.build_overlay.rmdir()
- state.build_overlay.symlink_to(build)
+ state.workspace.joinpath("build-overlay").symlink_to(build)
return True
--- /dev/null
+import shutil
+from pathlib import Path
+
+from mkosi.config import ConfigFeature, MkosiConfig
+from mkosi.install import copy_path
+from mkosi.log import die
+from mkosi.run import run
+
+
+def btrfs_maybe_make_subvolume(config: MkosiConfig, path: Path, mode: int) -> None:
+ if config.use_subvolumes == ConfigFeature.enabled and not shutil.which("btrfs"):
+ die("Subvolumes requested but the btrfs command was not found")
+
+ if config.use_subvolumes != ConfigFeature.disabled:
+ result = run(["btrfs", "subvolume", "create", path], check=config.use_subvolumes == ConfigFeature.enabled).returncode
+ else:
+ result = 1
+
+ if result == 0:
+ path.chmod(mode)
+ else:
+ path.mkdir(mode)
+
+
+def btrfs_maybe_snapshot_subvolume(config: MkosiConfig, src: Path, dst: Path) -> None:
+ subvolume = (config.use_subvolumes == ConfigFeature.enabled or
+ config.use_subvolumes == ConfigFeature.auto and shutil.which("btrfs") is not None)
+
+ if config.use_subvolumes == ConfigFeature.enabled and not shutil.which("btrfs"):
+ die("Subvolumes requested but the btrfs command was not found")
+
+ # Subvolumes always have inode 256 so we can use that to check if a directory is a subvolume.
+ if not subvolume or src.stat().st_ino != 256 or (dst.exists() and any(dst.iterdir())):
+ return copy_path(src, dst)
+
+ # btrfs can't snapshot to an existing directory so make sure the destination does not exist.
+ if dst.exists():
+ dst.rmdir()
+
+ run(["btrfs", "subvolume", "snapshot", src, dst],
+ check=config.use_subvolumes == ConfigFeature.enabled)
kernel_command_line_extra: list[str]
acl: bool
bootable: ConfigFeature
+ use_subvolumes: ConfigFeature
# QEMU-specific options
qemu_gui: bool
section="Output",
parse=config_parse_boolean,
),
+ MkosiConfigSetting(
+ dest="use_subvolumes",
+ section="Output",
+ parse=config_parse_feature,
+ ),
MkosiConfigSetting(
dest="packages",
section="Content",
nargs="?",
action=action,
)
+ group.add_argument(
+ "--use-subvolumes",
+ metavar="FEATURE",
+ help="Use btrfs subvolumes for faster directory operations where possible",
+ nargs="?",
+ action=action,
+ )
group = parser.add_argument_group("Content options")
group.add_argument(
import importlib
from pathlib import Path
+from mkosi.btrfs import btrfs_maybe_make_subvolume
from mkosi.config import MkosiConfig
from mkosi.distributions import DistributionInstaller
from mkosi.log import die
die("No installer for this distribution.")
self.installer = instance
- self.root.mkdir(exist_ok=True, mode=0o755)
- self.build_overlay.mkdir(exist_ok=True, mode=0o755)
- self.cache_overlay.mkdir(exist_ok=True, mode=0o755)
- self.workdir.mkdir(exist_ok=True)
- self.staging.mkdir(exist_ok=True)
+ btrfs_maybe_make_subvolume(self.config, self.root, mode=0o755)
+ self.workdir.mkdir()
+ self.staging.mkdir()
@property
def root(self) -> Path:
return self.workspace / "root"
- @property
- def cache_overlay(self) -> Path:
- return self.workspace / "cache-overlay"
-
- @property
- def build_overlay(self) -> Path:
- return self.workspace / "build-overlay"
-
@property
def workdir(self) -> Path:
return self.workspace / "workdir"