Skip to content
Draft
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,4 @@ requirements.txt
node_modules/
uv.lock
**.vscode/**
archinstall**
65 changes: 49 additions & 16 deletions ISOMOD
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ CLEANUP="${CLEANUP:-1}"
LOG_FILE="${LOG_FILE:-1}"
# Requires 'pacman-contrib' if this is enabled
CACHING="${CACHING:-0}"
# Filesystem configs to include (space-separated, from configs/ directory)
# e.g. "bcachefs" or "zfs" or "bcachefs zfs"
ISO_CONFIGS="${ISO_CONFIGS:-}"

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

Expand Down Expand Up @@ -96,32 +99,62 @@ find "$PROFILE_DIR/syslinux" -name "*.cfg" -exec sed -i 's|\(APPEND.*archiso.*\)
echo "Mod pkg list and MOTD..."
{
echo "git"
# bcachefs support (removed from mainline kernel 6.18+, needs DKMS)
echo "linux-headers"
echo "bcachefs-dkms"
echo "bcachefs-tools"
echo "dkms"
} >> "$PROFILE_DIR/packages.x86_64"

# Ensure bcachefs DKMS module is built and loaded at boot
echo "Adding bcachefs DKMS boot service..."
mkdir -p "$PROFILE_DIR/airootfs/etc/systemd/system/multi-user.target.wants"
cat > "$PROFILE_DIR/airootfs/etc/systemd/system/bcachefs-dkms.service" << 'SVCEOF'
# Load filesystem configs (e.g. zfs, bcachefs) from configs/ directory
CONFIGS_DIR="$SCRIPT_DIR/configs"
DKMS_MODULES=""

if [ -n "$ISO_CONFIGS" ]; then
# DKMS base packages
{
echo "linux-headers"
echo "dkms"
} >> "$PROFILE_DIR/packages.x86_64"
fi

for name in $ISO_CONFIGS; do
conf_dir="$CONFIGS_DIR/$name"
if [ ! -d "$conf_dir" ]; then
echo "WARNING: Config '$name' not found in configs/, skipping"
continue
fi
conf="$conf_dir/config"
if [ -f "$conf" ]; then
echo "Loading config: $name"
# shellcheck disable=SC1090
source "$conf"
fi
done

# DKMS autoinstall service: builds modules and modprobes them at boot
if [ -n "$DKMS_MODULES" ]; then
echo "Adding DKMS autoinstall service..."
mkdir -p "$PROFILE_DIR/airootfs/etc/systemd/system/multi-user.target.wants"

# one ExecStartPost=modprobe per module
MODPROBE_LINES=""
for mod in $DKMS_MODULES; do
MODPROBE_LINES="${MODPROBE_LINES}ExecStartPost=/usr/sbin/modprobe ${mod}
"
done

cat > "$PROFILE_DIR/airootfs/etc/systemd/system/dkms-autoinstall.service" << DKMSEOF
[Unit]
Description=Build and load bcachefs DKMS module
After=local-fs.target
ConditionPathExists=!/lib/modules/%v/updates/dkms/bcachefs.ko
Description=DKMS autoinstall
After=multi-user.target
DefaultDependencies=no

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/bin/dkms autoinstall
ExecStart=/usr/sbin/modprobe bcachefs
${MODPROBE_LINES}RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
SVCEOF
ln -sf /etc/systemd/system/bcachefs-dkms.service "$PROFILE_DIR/airootfs/etc/systemd/system/multi-user.target.wants/bcachefs-dkms.service"
DKMSEOF
ln -sf /etc/systemd/system/dkms-autoinstall.service "$PROFILE_DIR/airootfs/etc/systemd/system/multi-user.target.wants/dkms-autoinstall.service"
fi

# Append custom MOTD
cat >> "$PROFILE_DIR/airootfs/etc/motd" << EOF
Expand Down
90 changes: 89 additions & 1 deletion archinstoo/archinstoo/lib/disk/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from archinstoo.lib.menu.menu_helper import MenuHelper
from archinstoo.lib.models.bootloader import Bootloader
from archinstoo.lib.models.device import (
DEFAULT_ZFS_DATASETS,
BDevice,
BtrfsMountOption,
DeviceModification,
Expand All @@ -24,6 +25,9 @@
Size,
SubvolumeModification,
Unit,
ZfsConfiguration,
ZfsLayoutType,
ZfsPool,
_DeviceInfo,
)
from archinstoo.lib.output import FormattedOutput
Expand Down Expand Up @@ -136,16 +140,24 @@ def select_disk_config(
default_layout = DiskLayoutType.Default.display_msg()
manual_mode = DiskLayoutType.Manual.display_msg()
pre_mount_mode = DiskLayoutType.Pre_mount.display_msg()
zfs_mode = 'ZFS layout'

items = [
MenuItem(manual_mode, value=manual_mode),
MenuItem(default_layout, value=default_layout),
MenuItem(pre_mount_mode, value=pre_mount_mode),
]

if advanced:
items.append(MenuItem(zfs_mode, value=zfs_mode))

group = MenuItemGroup(items, sort_items=False)

if preset:
group.set_selected_by_value(preset.config_type.display_msg())
if preset.zfs_config:
group.set_selected_by_value(zfs_mode)
else:
group.set_selected_by_value(preset.config_type.display_msg())

result = SelectMenu[str](
group,
Expand Down Expand Up @@ -180,6 +192,9 @@ def select_disk_config(
mountpoint=path,
)

if selection == zfs_mode:
return suggest_zfs_layout(handler)

preset_device = preset.device_modifications[0].device if preset and preset.device_modifications else None
device = select_device(preset_device, device_handler=handler)

Expand Down Expand Up @@ -605,3 +620,76 @@ def suggest_lvm_layout(
lvm_vol_group.volumes.append(home_vol)

return LvmConfiguration(LvmLayoutType.Default, [lvm_vol_group])


def suggest_zfs_layout(
device_handler: DeviceHandler | None = None,
pool_name: str = 'zpool',
dataset_prefix: str = 'archinstoo',
) -> DiskLayoutConfiguration | None:
"""Create a default ZFS layout: ESP + ZFS partition using remaining space."""
handler = device_handler or DeviceHandler()

device = select_device(device_handler=handler)
if not device:
return None

sector_size = device.device_info.sector_size
total_size = device.device_info.total_size
available_space = total_size

uefi = SysInfo.has_uefi()
if not uefi:
raise ValueError('ZFS requires UEFI boot (for ZFSBootMenu)')

partition_table = PartitionTable.GPT
device_modification = DeviceModification(device, wipe=True, partition_table=partition_table)
available_space = available_space.gpt_end().align()

# ESP at /boot/efi; kernels live in the ZFS root dataset under /boot
# so ZFSBootMenu can find them (it scans datasets, not the ESP)
start = Size(1, Unit.MiB, sector_size)
esp_partition = PartitionModification(
status=ModificationStatus.Create,
type=PartitionType.Primary,
start=start,
length=Size(500, Unit.MiB, sector_size),
mountpoint=Path('/boot/efi'),
fs_type=FilesystemType.Fat32,
flags=[PartitionFlag.ESP],
)
device_modification.add_partition(esp_partition)

# no filesystem; ZFS manages this partition directly
zfs_start = esp_partition.start + esp_partition.length
zfs_length = available_space - zfs_start

zfs_partition = PartitionModification(
status=ModificationStatus.Create,
type=PartitionType.Primary,
start=zfs_start,
length=zfs_length,
mountpoint=None,
fs_type=None,
)
device_modification.add_partition(zfs_partition)

zfs_pool = ZfsPool(
name=pool_name,
pvs=[zfs_partition],
dataset_prefix=dataset_prefix,
datasets=list(DEFAULT_ZFS_DATASETS),
compression='lz4',
mountpoint=Path('/mnt'),
)

zfs_config = ZfsConfiguration(
config_type=ZfsLayoutType.Default,
pool=zfs_pool,
)

return DiskLayoutConfiguration(
config_type=DiskLayoutType.Default,
device_modifications=[device_modification],
zfs_config=zfs_config,
)
4 changes: 4 additions & 0 deletions archinstoo/archinstoo/lib/disk/disk_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ def run(self, additional_title: str | None = None) -> DiskLayoutConfiguration |
def _check_dep_lvm(self) -> bool:
disk_layout_conf: DiskLayoutConfiguration | None = self._menu_item_group.find_by_key('disk_config').value

# LVM and ZFS are mutually exclusive
if disk_layout_conf and disk_layout_conf.zfs_config:
return False

return bool(disk_layout_conf and disk_layout_conf.config_type == DiskLayoutType.Default)

def _check_dep_btrfs(self) -> bool:
Expand Down
45 changes: 45 additions & 0 deletions archinstoo/archinstoo/lib/disk/filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from .device_handler import DeviceHandler
from .luks import Luks2
from .lvm import lvm_group_info, lvm_vol_info
from .zfs import zfs_create_dataset, zfs_create_datasets, zgenhostid, zpool_create, zpool_export, zpool_set_cachefile_none


class FilesystemHandler:
Expand Down Expand Up @@ -78,6 +79,14 @@ def perform_filesystem_operations(self, show_countdown: bool = True) -> None:
self._format_partitions([boot_part])

self.perform_lvm_operations()
elif self._disk_config.zfs_config:
# only format boot/ESP; ZFS partition has fs_type=None
for mod in device_mods:
boot_parts = [p for p in mod.partitions if p.fs_type is not None]
if boot_parts:
self._format_partitions(boot_parts)

self.perform_zfs_operations()
else:
for mod in device_mods:
self._format_partitions(mod.partitions)
Expand Down Expand Up @@ -144,6 +153,42 @@ def _validate_partitions(self, partitions: list[PartitionModification]) -> None:
if next(filter(check, partitions), None) is not None:
raise exc

def perform_zfs_operations(self) -> None:
info('Setting up ZFS config...')

zfs_config = self._disk_config.zfs_config
if not zfs_config:
return

pool = zfs_config.pool

# hostid must be set before pool creation so the pool records it;
# must match the target system value (0x00bab10c)
zgenhostid()

# the ZFS partition has fs_type=None (no filesystem, ZFS manages it)
zfs_device: Path | None = None
for mod in self._disk_config.device_modifications:
for part in mod.partitions:
if part.fs_type is None and part.dev_path and not any(f.name == 'BIOS_GRUB' for f in part.flags):
zfs_device = part.dev_path
break

if not zfs_device:
raise ValueError('No ZFS partition found in device modifications')

zpool_create(pool.name, zfs_device, pool.compression, pool.mountpoint)
zpool_set_cachefile_none(pool.name)

# base dataset (pool/prefix) acts as a namespace container
base_dataset = pool.full_dataset_prefix
zfs_create_dataset(base_dataset, {'mountpoint': 'none', 'compression': pool.compression})

zfs_create_datasets(base_dataset, pool.datasets)

# export now; re-imported during mount phase
zpool_export(pool.name)

def perform_lvm_operations(self) -> None:
info('Setting up LVM config...')

Expand Down
Loading