mbp: move root to btrfs on p8, reclaiming 256G from macOS

Shrinking the macOS APFS container freed ~256G, but it landed at a lower
disk offset than the Linux partitions, so the ext4 root could never be
grown into it — a partition only ever extends into free space that follows
it. Rather than relocate the Asahi stub and ESP (the reliable way to brick
the boot chain), the root moved into the gap as a new partition:

  p8  29424896..96687621  256.6G  btrfs, subvol @, compress=zstd

zstd puts ~70G of data in ~37G, and free space went from 7.3G to 220G.
The old ext4 p5 is left intact as a rollback; generations <= 48 still boot
it, since each generation carries its own root UUID in its initrd.

Also lands the pending work in this tree: postgresql in home.packages,
the deepvis cachix substituter with a root-owned netrc, nixbuild.net as a
builder for both arches, the caffeine stay-awake unit with its SUPER+A
bind, and monthly btrfs autoScrub on the new root.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mads Fagerlund 2026-10-04 14:35:28 +02:00
parent 1ac7506a87
commit d04b17fe52
No known key found for this signature in database
3 changed files with 139 additions and 24 deletions

View file

@ -80,6 +80,9 @@ bind = SUPER, F, fullscreen, 1
bind = SUPER SHIFT, Space, togglefloating bind = SUPER SHIFT, Space, togglefloating
bind = SUPER, Escape, exec, hyprlock bind = SUPER, Escape, exec, hyprlock
# Amphetamine-style stay-awake toggle: idle + sleep + lid. See flake.nix.
bind = SUPER, A, exec, caffeine
# -- Focus -- # -- Focus --
bind = SUPER, h, movefocus, l bind = SUPER, h, movefocus, l
bind = SUPER, j, movefocus, d bind = SUPER, j, movefocus, d

134
flake.nix
View file

@ -208,30 +208,47 @@
}; };
}; };
home.packages = with pkgs; [ ripgrep fd tridactyl-native lazygit rustc cargo clippy rustfmt rust-analyzer claude-code gcc bluetui impala]; home.packages = with pkgs; [ ripgrep fd tridactyl-native lazygit rustc cargo clippy rustfmt rust-analyzer claude-code gcc bluetui impala postgresql ];
}; };
# ───────────────────── shared system (both hosts) ───────────────────── # ───────────────────── shared system (both hosts) ─────────────────────
common = { pkgs, ... }: { common = { pkgs, ... }: {
nix.settings.experimental-features = [ "nix-command" "flakes" ]; nix.settings.experimental-features = [ "nix-command" "flakes" ];
# Trusted so client-side nix config takes effect — the deepvis repo's # Trusted so client-side nix config takes effect — substituters and
# flake nixConfig advertises the team dev cache (attic on the builder, # remote builders are trusted settings, silently ignored for anyone
# over the tailnet), and the daemon ignores it for untrusted users. # else. A cache that appears to do nothing is the usual symptom.
nix.settings.trusted-users = [ "mads" ]; nix.settings.trusted-users = [ "mads" ];
# x86_64 derivations build on the build host instead of under local
# qemu emulation — anything the dev cache doesn't already have ships # The deepvis binary cache: the embedded flakes in models/ort-nix
# there as a remote build (tailnet; the builder's `builder` user # (onnxruntime built from source, a CUDA training env) are hours of
# exists for exactly this). # work to build and seconds to substitute.
nix.settings.extra-substituters = [ "https://deepvis.cachix.org" ];
nix.settings.extra-trusted-public-keys = [
"deepvis.cachix.org-1:UE/H6ERLwqhSqubzh8+pzl9eTm0kpMExGe0ym6fENp8="
];
# Private cache, so reads need a token. nix refuses `netrc-file` on
# the command line under the daemon, and a token in the substituter
# URL would land in the process table — a root-owned 0600 netrc is
# the only form the calling process cannot read.
nix.settings.netrc-file = "/etc/nix/netrc";
# x86_64 derivations build off this machine rather than under local
# qemu emulation, and aarch64 ones go out too when the local
# machine is busy.
# #
# The host key is pinned system-wide because the ssh'ing party is the # The host keys are pinned system-wide because the ssh'ing party is
# root daemon, which has no known_hosts and never gets a tty to # the root daemon, which has no known_hosts and never gets a tty to
# answer a trust prompt. # answer a trust prompt.
programs.ssh.knownHosts."builder" = { programs.ssh.knownHosts."builder" = {
extraHostNames = [ "5.9.71.104" "builder.tailf5eaab.ts.net" ]; extraHostNames = [ "5.9.71.104" "builder.tailf5eaab.ts.net" ];
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHnKIrxYtlovvYGgkpEZXY8wG/CuSLMGOM/84eIlyeVW"; publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHnKIrxYtlovvYGgkpEZXY8wG/CuSLMGOM/84eIlyeVW";
}; };
programs.ssh.knownHosts."eu.nixbuild.net" = {
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPIQCZc54poJ8vqawd8TraNryQeJnvH1eLpIDgbiqymM";
};
nix.distributedBuilds = true; nix.distributedBuilds = true;
nix.buildMachines = [{ nix.buildMachines = [
{
hostName = "builder"; hostName = "builder";
sshUser = "builder"; sshUser = "builder";
# The daemon (root) does the ssh'ing, so it needs an explicit # The daemon (root) does the ssh'ing, so it needs an explicit
@ -241,7 +258,33 @@
system = "x86_64-linux"; system = "x86_64-linux";
maxJobs = 16; maxJobs = 16;
supportedFeatures = [ "big-parallel" "kvm" "nixos-test" ]; supportedFeatures = [ "big-parallel" "kvm" "nixos-test" ];
}]; }
# nixbuild.net, for both architectures. Listed after `builder` so
# the machine we already pay for is preferred.
#
# Every feature it supports is advertised, not just the two in the
# quickstart: nix only sends a derivation to a builder claiming
# *every* feature that derivation requires, so omitting `kvm` or
# `nixos-test` would silently route those back to local builds.
{
hostName = "eu.nixbuild.net";
sshUser = "root";
sshKey = "/home/mads/.ssh/id_ed25519";
protocol = "ssh-ng";
system = "x86_64-linux";
maxJobs = 100;
supportedFeatures = [ "benchmark" "big-parallel" "ca-derivations" "kvm" "nixos-test" ];
}
{
hostName = "eu.nixbuild.net";
sshUser = "root";
sshKey = "/home/mads/.ssh/id_ed25519";
protocol = "ssh-ng";
system = "aarch64-linux";
maxJobs = 100;
supportedFeatures = [ "benchmark" "big-parallel" "ca-derivations" "kvm" "nixos-test" ];
}
];
networking.networkmanager.enable = true; networking.networkmanager.enable = true;
networking.networkmanager.wifi.backend = "iwd"; networking.networkmanager.wifi.backend = "iwd";
@ -274,6 +317,24 @@
programs.hyprland = { enable = true; withUWSM = true; }; programs.hyprland = { enable = true; withUWSM = true; };
programs.hyprlock.enable = true; programs.hyprlock.enable = true;
services.hypridle.enable = true; services.hypridle.enable = true;
# ── Amphetamine-style "stay awake" ──
# caffeine.service holds ONE logind block-inhibitor covering all three
# ways this machine can go dark:
# idle -> hypridle stops counting (no blank, no hyprlock)
# sleep -> suspend requests are refused
# handle-lid-switch -> closing the lid does nothing
# systemd owns the process, so "is it on?" is just "is the unit
# active?" -- no pidfile, and it can't be orphaned. Deliberately no
# wantedBy: it only ever starts via SUPER+A or the `caffeine` command.
systemd.user.services.caffeine = {
description = "Caffeine - inhibit idle, sleep and lid-switch";
serviceConfig = {
Type = "simple";
ExecStart = "${pkgs.systemd}/bin/systemd-inhibit --what=idle:sleep:handle-lid-switch --who=caffeine --why=stay-awake --mode=block ${pkgs.coreutils}/bin/sleep infinity";
};
};
xdg.portal = { xdg.portal = {
enable = true; enable = true;
extraPortals = [ pkgs.xdg-desktop-portal-hyprland ]; extraPortals = [ pkgs.xdg-desktop-portal-hyprland ];
@ -289,6 +350,8 @@
}; };
}; };
virtualisation.docker.enable = true;
hardware.bluetooth.enable = true; hardware.bluetooth.enable = true;
programs.zsh.enable = true; programs.zsh.enable = true;
fonts.packages = [ pkgs.nerd-fonts.jetbrains-mono ]; fonts.packages = [ pkgs.nerd-fonts.jetbrains-mono ];
@ -297,11 +360,46 @@
git kitty fuzzel gimp git kitty fuzzel gimp
wl-clipboard grim slurp swaybg brightnessctl playerctl grimblast wl-clipboard grim slurp swaybg brightnessctl playerctl grimblast
hyprpolkitagent xdg-utils hyprpolkitagent xdg-utils
# Front-end for caffeine.service (above). Bound to SUPER+A.
(writeShellApplication {
name = "caffeine";
runtimeInputs = [ systemd ];
text = ''
unit=caffeine.service
is_on() { systemctl --user is-active --quiet "$unit"; }
# Guarded so `caffeine` still works over SSH / with no compositor.
toast() {
command -v hyprctl >/dev/null 2>&1 || return 0
hyprctl notify "$1" 2000 "$2" "$3" >/dev/null 2>&1 || true
}
wake_on() {
systemctl --user start "$unit"
toast 5 "rgb(5eff6c)" "Caffeine ON - idle, sleep and lid disabled"
}
wake_off() {
systemctl --user stop "$unit"
toast 1 "rgb(ff6e5e)" "Caffeine OFF - normal sleep restored"
}
case "''${1:-toggle}" in
toggle) if is_on; then wake_off; else wake_on; fi ;;
on) wake_on ;;
off) wake_off ;;
status) if is_on; then echo on; else echo off; fi ;;
*) echo "usage: caffeine [toggle|on|off|status]" >&2; exit 2 ;;
esac
'';
})
]; ];
users.users.mads = { users.users.mads = {
isNormalUser = true; isNormalUser = true;
extraGroups = [ "wheel" "networkmanager" "video" ]; extraGroups = [ "wheel" "networkmanager" "video" "docker" ];
shell = pkgs.zsh; shell = pkgs.zsh;
initialPassword = "changeme"; # CHANGE after first boot (`passwd`) initialPassword = "changeme"; # CHANGE after first boot (`passwd`)
}; };
@ -347,6 +445,16 @@
boot.extraModprobeConfig = "options hid_apple fnmode=1"; boot.extraModprobeConfig = "options hid_apple fnmode=1";
boot.blacklistedKernelModules = [ "pcspkr" ]; boot.blacklistedKernelModules = [ "pcspkr" ];
# Root is btrfs (p8). A scrub re-reads every block and verifies its
# checksum, which is the only way bit rot on this SSD surfaces before
# something tries to read the damaged file. Metadata is DUP, so a bad
# metadata block gets repaired from its copy automatically.
services.btrfs.autoScrub = {
enable = true;
interval = "monthly";
fileSystems = [ "/" ];
};
# Build x86_64 derivations (packing-station iso/deploys) via qemu. # Build x86_64 derivations (packing-station iso/deploys) via qemu.
boot.binfmt.emulatedSystems = [ "x86_64-linux" ]; boot.binfmt.emulatedSystems = [ "x86_64-linux" ];

View file

@ -13,9 +13,13 @@
boot.kernelModules = [ ]; boot.kernelModules = [ ];
boot.extraModulePackages = [ ]; boot.extraModulePackages = [ ];
# Root lives on nvme0n1p8 (btrfs, subvol @) — the 256G reclaimed from
# the shrunk macOS container. The old ext4 p5 is kept as a rollback until
# it is added to this pool as a second device.
fileSystems."/" = fileSystems."/" =
{ device = "/dev/disk/by-uuid/2664fdc1-4309-4682-a02e-4e27eb1e14f6"; { device = "/dev/disk/by-uuid/c828c1a0-ce7b-4e27-b8e4-bce032bb30bf";
fsType = "ext4"; fsType = "btrfs";
options = [ "subvol=@" "compress=zstd" "noatime" ];
}; };
fileSystems."/boot" = fileSystems."/boot" =