Skip to content

Add an optional swap file for hibernation - #4729

Open
VictorZaca wants to merge 11 commits into
archlinux:masterfrom
VictorZaca:swapfile-hibernation
Open

Add an optional swap file for hibernation#4729
VictorZaca wants to merge 11 commits into
archlinux:masterfrom
VictorZaca:swapfile-hibernation

Conversation

@VictorZaca

Copy link
Copy Markdown
Contributor

archinstall only sets up zram today, and zram cannot hold a hibernation image: the compressed pages live in the RAM you are trying to write out. So hibernation is a manual job after the install — create the file, find the offset, add two kernel parameters, add the resume hook, rebuild the initramfs. #994 has been asking for this since 2022, and #2371 and #2450 are the same request.

Installer.add_swapfile() has also been sitting in the tree unused since #1558 in March 2023. This wires it up and fixes what would have broken if anything had called it.

The menu

swap-menu

zram and the swap file can be on together, which is probably what most people want. zram stays at priority 100 and does the day-to-day swapping; the file sits at -1 so a hibernation image has somewhere to go.

swap-menu2

The parts worth reviewing

The file is sized at MemTotal and created before minimal_installation(), so the hook and the kernel parameters are in place before the initramfs is built.

On btrfs it gets a subvolume of its own at /swap. btrfs will not snapshot a subvolume that holds an active swap file, and turning on hibernation should not make / unsnapshottable as a side effect.

The offset comes from btrfs inspect-internal map-swapfile -r, not filefrag. On btrfs the two disagree (140544 vs 72960 on my test VM) and filefrag's number will not resume. It fails silently, the machine just cold-boots, so it is easy to get wrong.

swap used to hold the zram configuration directly. Both old shapes still parse (true and {"enabled": ..., "algorithm": ...}); the new one is {"zram": {...}, "swapfile": true}. tests/test_swap_config.py covers all of them plus a round trip through ArchConfigHandler.

add_swapfile()'s signature changed. Nothing in the tree calls it, but a plugin might.

Two things I deliberately left alone: only_hd.py enables the swap menu item without ever acting on it, which is pre-existing and equally true of zram, and schema.json has no swap key and is currently invalid JSON anyway.

Testing

49 tests pass, ruff and mypy --strict are clean on each commit individually, and locales_generator.sh check passes. Hibernation is verified end to end in QEMU on btrfs with systemd-boot and UKI — the full round trip is in the first comment below.

Closes #994
Closes #2371
Refs #2450, #4169

The swap option only described zram, so there was nowhere to record that
the user also wants a swap file. Wrap it in a SwapConfiguration that
carries both.

The option is widely used in existing configuration files, so parsing
still accepts the two shapes it used to take, a bare bool and a dict of
zram keys, and both round-trip through the new serialization.
add_swapfile() defaulted to a hardcoded 4G, which is too small to hibernate
most machines and too large for the smallest ones. Size it from MemTotal
instead: a hibernation image is a snapshot of the physical memory, so a swap
file that size is always big enough to hold one.

A MemTotal-sized dd would write tens of gigabytes of zeros before the base
install even starts, so hand the allocation and the formatting to mkswap.

Check the free space on the target first as well, leaving room for the base
install, so an oversized swap file fails with a readable error instead of
pacstrap running the root partition out of space midway through.
The physical offset filefrag reports is not the one btrfs resumes from, so the
offset the generic path collects is wrong there: the machine boots normally and
silently fails to restore. Read it back with btrfs inspect-internal
map-swapfile instead, and create the file with btrfs filesystem mkswapfile,
which also marks it NODATACOW as swapon requires on btrfs.

Put it in a subvolume of its own, because btrfs refuses to snapshot a subvolume
that holds an active swap file and the root one has to stay snapshottable for
snapper, timeshift and grub-btrfs to keep working.

Take the filesystem off the mount rather than off the disk layout, since a
pre-mounted configuration carries no partition modifications to look at.

Checked against btrfs-progs 7.1 on a root subvolume mounted compress=zstd:
map-swapfile reports 140544 where filefrag reports 72960, and snapshotting the
root subvolume with swap active only succeeds once the swap file is moved out
of it.
add_swapfile() appended resume to the end of HOOKS, which lands it after fsck,
and added it unconditionally. Insert it before fsck instead, mirroring how
_prepare_encrypt() places its own hook, so it runs once udev and whichever of
encrypt and lvm2 the layout pulled in have made the resume device available.

Skip it altogether when a HSM device is in use. The initramfs keeps the systemd
hook in that case, and that one already installs systemd-hibernate-resume along
with the generator that reads resume= and resume_offset= off the kernel command
line, so the busybox hook has nothing left to do.
The swap entry was a yes/no question about zram, so there was no way to ask
for a swap file. Turn it into a selection over the four combinations of zram
and a swap file, keeping zram as the default and the compression algorithm
prompt that follows it.
minimal_installation() is what runs mkinitcpio, so the swap file has to be in
place before it: the resume hook and the resume= and resume_offset= parameters
add_swapfile() collects would otherwise never reach the initramfs.

zram keeps its existing position after the base install, since setup_swap()
pacstraps zram-generator into the target and needs one to exist.
examples/config-sample.json still documented swap in the shape the option had
back when it carried nothing but zram, and the JSON options table still listed
it as a plain boolean; write the current one in both.

Add tests over SwapConfiguration.parse_arg for every shape the option accepts,
old and new, over the json() round trip and over the config file path, so
compatibility with config files already out there cannot regress unnoticed.
Cover the derived swap file size as well, since rounding it down would leave
part of the hibernation image with nowhere to go.
@VictorZaca
VictorZaca requested a review from Torxed as a code owner August 24, 2026 18:25
Copilot AI lite review requested due to automatic review settings August 24, 2026 18:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds first-class support for creating and configuring an on-disk swap file suitable for hibernation (including resume kernel params / initramfs hook), while keeping existing zram swap support and maintaining backward compatibility for prior swap config shapes.

Changes:

  • Introduces SwapConfiguration (zram + swapfile) and updates config parsing, menus, and previews accordingly.
  • Wires Installer.add_swapfile() into the guided installation flow before initramfs generation, and adds swapfile sizing logic based on MemTotal.
  • Adds/updates documentation, examples, locales, and tests to cover old/new config formats and size calculation.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/test_swap_config.py Adds coverage for swap config backward compatibility, round-tripping, config-file parsing, and swapfile sizing.
tests/test_args.py Updates config parsing expectations to use SwapConfiguration instead of ZramConfiguration.
examples/config-sample.json Updates sample config to new swap: { zram: {...}, swapfile: ... } shape.
docs/cli_parameters/config/config_options.csv Updates user-facing docs for the swap config option.
archinstall/scripts/guided.py Creates swapfile before minimal_installation() when enabled; updates zram setup accessors.
archinstall/locales/base.pot Adds/updates translatable strings for the new swap selection UI and summary.
archinstall/lib/models/application.py Adds SwapConfiguration model with parsing, JSON serialization, and summary output.
archinstall/lib/installer.py Implements swapfile creation, filesystem-specific offset detection, resume hook preparation, and swapfile sizing.
archinstall/lib/global_menu.py Switches menu default/value type to SwapConfiguration and updates swap preview formatting.
archinstall/lib/general/system_menu.py Reworks swap selection UI to support zram-only, swapfile-only, both, or none.
archinstall/lib/args.py Changes ArchConfig.swap type and config parsing to SwapConfiguration.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread docs/cli_parameters/config/config_options.csv Outdated
Comment thread archinstall/lib/installer.py Outdated
Comment thread archinstall/lib/installer.py
The row listed zram and swapfile as if they were the accepted values, in the
same shape audio_config lists pipewire and pulseaudio. Following that gets a
crash rather than a misconfiguration: parse_arg takes a bool or a dict, and
'zram' in arg matches a substring when arg is a string, so "swap": "zram" falls
into the dict branch and dies on arg.get with AttributeError.

Spell out the dict instead, the way the other dict-valued options in the table
already do.
statvfs was called on the target root, which is the wrong filesystem as soon as
a caller hands add_swapfile a path on a mount of its own. Nothing in the tree
does, the file is always /swap/swapfile or /swapfile, but this is a public
method and the signature changed here, so it should hold up for a plugin.

Walk up to the nearest existing ancestor rather than the parent: the btrfs
default puts the file in /swap, which does not exist yet at this point, and a
nested directory on a separate mount would fall back to the target root and
measure the wrong filesystem again. Path.parents starts at the parent and / is
always there, so the walk always terminates.
The message asserted that snapshots would be blocked, but the directory may
already be a dedicated subvolume, in which case nothing is wrong: the only
snapshots that fail are of the swap subvolume itself, which nobody takes. Name
the condition instead of claiming the outcome.
pylint flagged W0621: installer.py already imports mount from disk.utils, so
naming the local for the measured filesystem the same thing shadowed it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SWAP on LVM LUKS for hibernation Hibernate option

2 participants