Skip to content
Open
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
44 changes: 40 additions & 4 deletions archinstall/lib/disk/device_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@
from archinstall.lib.models.device import (
DEFAULT_ITER_TIME,
BDevice,
BtrfsCompression,
BtrfsMountOption,
DeviceModification,
DiskEncryption,
FilesystemType,
LsblkInfo,
ModificationStatus,
PartitionFlag,
PartitionGUID,
PartitionModification,
PartitionTable,
SubvolumeModification,
Expand Down Expand Up @@ -422,6 +424,9 @@ def fetch_part_info(self, path: Path) -> LsblkInfo:

return lsblk_info

# ============================================================
# MODIFIED: create_lvm_btrfs_subvolumes - Now uses per-subvolume compression
# ============================================================
def create_lvm_btrfs_subvolumes(
self,
path: Path,
Expand All @@ -430,7 +435,17 @@ def create_lvm_btrfs_subvolumes(
) -> None:
info(f'Creating subvolumes: {path}')

mount(path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True)
# MODIFIED: Build mount options including compression from subvolumes
all_mount_options = mount_options.copy()
for subvol in btrfs_subvols:
if subvol.compression != BtrfsCompression.NONE:
comp_opt = subvol.compression.mount_option
if comp_opt and comp_opt not in all_mount_options:
# Remove any existing compress= options to avoid conflicts
all_mount_options = [o for o in all_mount_options if not o.startswith("compress=")]
all_mount_options.append(comp_opt)

mount(path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True, options=all_mount_options)

for sub_vol in sorted(btrfs_subvols, key=lambda x: x.name):
debug(f'Creating subvolume: {sub_vol.name}')
Expand All @@ -445,14 +460,18 @@ def create_lvm_btrfs_subvolumes(
except SysCallError as err:
raise DiskError(f'Could not set nodatacow attribute at {subvol_path}: {err}')

if BtrfsMountOption.compress.value in mount_options:
# MODIFIED: Use compression from subvolume, not mount_options
if sub_vol.compression != BtrfsCompression.NONE:
try:
SysCommand(f'chattr +c {subvol_path}')
except SysCallError as err:
raise DiskError(f'Could not set compress attribute at {subvol_path}: {err}')

umount(path)

# ============================================================
# MODIFIED: create_btrfs_volumes - Now uses per-subvolume compression
# ============================================================
def create_btrfs_volumes(
self,
part_mod: PartitionModification,
Expand All @@ -479,11 +498,21 @@ def create_btrfs_volumes(
luks_handler = None
dev_path = part_mod.safe_dev_path

# MODIFIED: Build mount options including compression from subvolumes
mount_options = part_mod.mount_options.copy()
for subvol in part_mod.btrfs_subvols:
if subvol.compression != BtrfsCompression.NONE:
comp_opt = subvol.compression.mount_option
if comp_opt and comp_opt not in mount_options:
# Remove any existing compress= options to avoid conflicts
mount_options = [o for o in mount_options if not o.startswith("compress=")]
mount_options.append(comp_opt)

mount(
dev_path,
self._TMP_BTRFS_MOUNT,
create_target_mountpoint=True,
options=part_mod.mount_options,
options=mount_options,
)

for sub_vol in sorted(part_mod.btrfs_subvols, key=lambda x: x.name):
Expand All @@ -493,6 +522,13 @@ def create_btrfs_volumes(

SysCommand(f'btrfs subvolume create -p {subvol_path}')

# MODIFIED: Apply compression attribute per subvolume
if sub_vol.compression != BtrfsCompression.NONE:
try:
SysCommand(f'chattr +c {subvol_path}')
except SysCallError as err:
raise DiskError(f'Could not set compress attribute at {subvol_path}: {err}')

umount(dev_path)

if luks_handler is not None and luks_handler.mapper_dev is not None:
Expand Down Expand Up @@ -633,4 +669,4 @@ def wipe_dev(self, block_device: BDevice) -> None:
self._wipe(block_device.device_info.path)


device_handler = DeviceHandler()
device_handler = DeviceHandler()
234 changes: 141 additions & 93 deletions archinstall/lib/disk/subvolume_menu.py
Original file line number Diff line number Diff line change
@@ -1,102 +1,150 @@
from pathlib import Path
from typing import assert_never, override

from archinstall.lib.menu.helpers import Input
from archinstall.lib.models.device import BtrfsCompression, SubvolumeModification
from archinstall.lib.menu.helpers import Input, Selection
from archinstall.lib.menu.list_manager import ListManager
from archinstall.lib.menu.util import prompt_dir
from archinstall.lib.models.device import SubvolumeModification
from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType


class SubvolumeMenu(ListManager[SubvolumeModification]):
def __init__(
self,
btrfs_subvols: list[SubvolumeModification],
prompt: str | None = None,
):
self._actions = [
tr('Add subvolume'),
tr('Edit subvolume'),
tr('Delete subvolume'),
]

super().__init__(
btrfs_subvols,
[self._actions[0]],
self._actions[1:],
prompt,
)

async def show(self) -> list[SubvolumeModification] | None:
return await super()._run()

@override
def selected_action_display(self, selection: SubvolumeModification) -> str:
return str(selection.name)

async def _add_subvolume(self, preset: SubvolumeModification | None = None) -> SubvolumeModification | None:
def validate(value: str | None) -> str | None:
if value:
return None
return tr('Value cannot be empty')

result = await Input(
header=tr('Enter subvolume name'),
allow_skip=True,
default_value=str(preset.name) if preset else None,
validator_callback=validate,
).show()

match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
name = result.get_value()
case ResultType.Reset:
raise ValueError('Unhandled result type')
case _:
assert_never(result.type_)

header = f'{tr("Subvolume name")}: {name}\n\n'
header += tr('Enter subvolume mountpoint')

path = await prompt_dir(
header=header,
allow_skip=True,
validate=True,
must_exist=False,
)

if not path:
return preset

return SubvolumeModification(Path(name), path)

@override
async def handle_action(
self,
action: str,
entry: SubvolumeModification | None,
data: list[SubvolumeModification],
) -> list[SubvolumeModification]:
if action == self._actions[0]:
new_subvolume = await self._add_subvolume()

if new_subvolume is not None:
# in case a user with the same username as an existing user
# was created we'll replace the existing one
data = [d for d in data if d.name != new_subvolume.name]
data += [new_subvolume]
elif entry is not None:
if action == self._actions[1]:
new_subvolume = await self._add_subvolume(entry)

if new_subvolume is not None:
# we'll remove the original subvolume and add the modified version
data = [d for d in data if d.name != entry.name and d.name != new_subvolume.name]
data += [new_subvolume]
elif action == self._actions[2]:
data = [d for d in data if d != entry]

return data
def __init__(
self,
btrfs_subvols: list[SubvolumeModification],
prompt: str | None = None,
):
self._actions = [
tr('Add subvolume'),
tr('Edit subvolume'),
tr('Delete subvolume'),
]

super().__init__(
btrfs_subvols,
[self._actions[0]],
self._actions[1:],
prompt,
)

async def show(self) -> list[SubvolumeModification] | None:
return await super()._run()

# MODIFIED: Show compression info in display
@override
def selected_action_display(self, selection: SubvolumeModification) -> str:
base = str(selection.name)
# NEW: Show compression if not default
if selection.compression != BtrfsCompression.ZSTD_3:
base += f" [{selection.compression.value}]"
return base

# MODIFIED: Added compression selection
async def _add_subvolume(self, preset: SubvolumeModification | None = None) -> SubvolumeModification | None:
def validate(value: str | None) -> str | None:
if value:
return None
return tr('Value cannot be empty')

result = await Input(
header=tr('Enter subvolume name'),
allow_skip=True,
default_value=str(preset.name) if preset else None,
validator_callback=validate,
).show()

match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
name = result.get_value()
case ResultType.Reset:
raise ValueError('Unhandled result type')
case _:
assert_never(result.type_)

header = f'{tr("Subvolume name")}: {name}\n\n'
header += tr('Enter subvolume mountpoint')

path = await prompt_dir(
header=header,
allow_skip=True,
validate=True,
must_exist=False,
)

if not path:
return preset

# NEW: Add compression selection
default_compression = preset.compression if preset else BtrfsCompression.ZSTD_3
compression = await self._select_compression(default_compression)

if compression is None:
if preset:
return preset
compression = BtrfsCompression.ZSTD_3

# MODIFIED: Include compression when creating subvolume
return SubvolumeModification(
Path(name),
path,
compression
)

# NEW: Compression selection UI method
async def _select_compression(self, default: BtrfsCompression) -> BtrfsCompression | None:
"""Display compression selection menu and return selected option"""
header = tr('Select compression algorithm for this subvolume') + '\n\n'
header += tr('Higher compression levels save more space but are slower') + '\n'
header += tr('ZSTD is generally recommended for most use cases') + '\n\n'
header += tr('Selection') + ':'

items = []
for display_name, comp_value in BtrfsCompression.get_ui_options():
label = display_name
if comp_value == default:
label = f"* {label} (default)"
items.append(MenuItem(label, value=comp_value))

group = MenuItemGroup(items, sort_items=False)

result = await Selection[BtrfsCompression](
group,
header=header,
allow_skip=True,
).show()

match result.type_:
case ResultType.Selection:
return result.get_value()
case ResultType.Skip:
return default
case _:
return None

@override
async def handle_action(
self,
action: str,
entry: SubvolumeModification | None,
data: list[SubvolumeModification],
) -> list[SubvolumeModification]:
if action == self._actions[0]:
new_subvolume = await self._add_subvolume()

if new_subvolume is not None:
data = [d for d in data if d.name != new_subvolume.name]
data += [new_subvolume]
elif entry is not None:
if action == self._actions[1]:
new_subvolume = await self._add_subvolume(entry)

if new_subvolume is not None:
data = [d for d in data if d.name != entry.name and d.name != new_subvolume.name]
data += [new_subvolume]
elif action == self._actions[2]:
data = [d for d in data if d != entry]

return data
Loading