Commit 2ceafb6b7617

Vincent Demeester <vincent@sbr.pm>
2026-02-12 12:44:42
fix(desktop): restored usrbinenv activation for envfs
envfs force-disables the usrbinenv activation script, but on fresh installs /usr/bin/env doesn't exist yet and systemd refuses to boot before envfs can mount. Used mkOverride 0 to restore the activation script as a fallback. Also added remote-install.sh for remote NixOS installation via disko + nix copy, and updated install.sh cross-reference. See: https://github.com/NixOS/nixpkgs/issues/462556
1 parent 318e1d2
Changed files (3)
systems/common/desktop/default.nix
@@ -109,6 +109,17 @@
     # };
   };
 
+  # Workaround: envfs force-disables the usrbinenv activation script because it
+  # mounts a FUSE filesystem over /usr/bin at boot. But on fresh installs (or
+  # with impermanence), /usr/bin/env doesn't exist yet, and systemd refuses to
+  # boot before envfs can mount. Create it via activation as a fallback.
+  # See: https://github.com/NixOS/nixpkgs/issues/462556
+  system.activationScripts.usrbinenv = lib.mkOverride 0 ''
+    mkdir -p /usr/bin
+    chmod 0755 /usr/bin
+    ln -sfn ${config.environment.usrbinenv} /usr/bin/env
+  '';
+
   services = {
     envfs.enable = true;
     power-profiles-daemon.enable = true;
install.sh
@@ -30,7 +30,7 @@
 # Tips:
 #   If you run out of space on the live USB, you can either:
 #   - Resize the tmpfs:  mount -o remount,size=28G /nix/.rw-store
-#   - Build remotely and copy the closure over SSH (see README)
+#   - Build remotely using ./remote-install.sh (builds on workstation, copies over SSH)
 #
 # All extra arguments are forwarded to disko-install. Run with --help to see
 # the full list of disko-install options:
remote-install.sh
@@ -0,0 +1,211 @@
+#!/usr/bin/env bash
+# Remotely install NixOS on a target machine from your workstation.
+#
+# This script automates the full remote installation process:
+#   1. Verifies SSH connectivity to the target
+#   2. Copies your SSH public key for passwordless access (optional)
+#   3. Builds the disko partitioning script locally
+#   4. Builds the full NixOS system closure locally
+#   5. Copies the disko script to the target and runs it (format + mount)
+#   6. Resizes the live USB's tmpfs to fit the incoming closure
+#   7. Copies the system closure to the target
+#   8. Runs nixos-install on the target
+#
+# This avoids building anything on the target, which is useful when the
+# live USB doesn't have enough space for a full build.
+#
+# Prerequisites:
+#   - Target machine booted from NixOS live USB/CD
+#   - Password set for nixos user: passwd nixos
+#   - Network connectivity between workstation and target
+#   - Host has a disko config in systems/<hostname>/disks.nix
+#
+# The disk device is defined in systems/<hostname>/disks.nix.
+# LUKS-encrypted hosts will prompt for a passphrase during partitioning.
+#
+# Examples:
+#   ./remote-install.sh okinawa 192.168.1.108
+#   ./remote-install.sh okinawa 192.168.1.108 --no-copy-keys
+#   ./remote-install.sh okinawa 192.168.1.108 --tmpfs-size 24G
+#
+# Options:
+#   --no-copy-keys         Skip copying SSH public key to target
+#   --tmpfs-size SIZE      Size for /nix/.rw-store tmpfs (default: 28G)
+#   --skip-format          Skip disko format, only mount (for re-running)
+#   -h, --help             Show this help message
+
+set -euo pipefail
+
+# --- Colors and logging ---------------------------------------------------
+
+bold='\033[1m'
+blue='\033[1;34m'
+yellow='\033[1;33m'
+red='\033[1;31m'
+green='\033[1;32m'
+reset='\033[0m'
+
+info()    { echo -e "${blue}==>${reset} ${bold}$*${reset}"; }
+warn()    { echo -e "${yellow}==>${reset} ${bold}$*${reset}"; }
+error()   { echo -e "${red}==>${reset} ${bold}$*${reset}" >&2; }
+success() { echo -e "${green}==>${reset} ${bold}$*${reset}"; }
+
+# --- Usage -----------------------------------------------------------------
+
+usage() {
+    sed -n '/^# Remotely install/,/^[^#]/{ /^#/s/^# \{0,1\}//p }' "$0"
+    echo ""
+    echo "Usage: $0 <hostname> <target-ip> [options...]"
+    echo ""
+    echo "Arguments:"
+    echo "  hostname     NixOS host configuration to install (from flake)"
+    echo "  target-ip    IP address of the target machine (NixOS live USB)"
+}
+
+# --- Argument parsing ------------------------------------------------------
+
+COPY_KEYS=true
+TMPFS_SIZE="28G"
+SKIP_FORMAT=false
+POSITIONAL=()
+
+while [[ $# -gt 0 ]]; do
+    case "$1" in
+        -h|--help)
+            usage
+            exit 0
+            ;;
+        --no-copy-keys)
+            COPY_KEYS=false
+            shift
+            ;;
+        --tmpfs-size)
+            TMPFS_SIZE="$2"
+            shift 2
+            ;;
+        --skip-format)
+            SKIP_FORMAT=true
+            shift
+            ;;
+        *)
+            POSITIONAL+=("$1")
+            shift
+            ;;
+    esac
+done
+
+if [[ ${#POSITIONAL[@]} -lt 2 ]]; then
+    usage
+    exit 1
+fi
+
+HOSTNAME="${POSITIONAL[0]}"
+TARGET_IP="${POSITIONAL[1]}"
+
+SSH_USER="nixos"
+TARGET="${SSH_USER}@${TARGET_IP}"
+SSH_OPTS=(-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null)
+
+# SSH commands: use -t for interactive (LUKS passphrase), plain for scripted
+# shellcheck disable=SC2029  # $@ is intentionally expanded client-side
+ssh_cmd()    { ssh "${SSH_OPTS[@]}" "${TARGET}" "$@"; }
+ssh_tty()    { ssh "${SSH_OPTS[@]}" -t "${TARGET}" "$@"; }
+
+# --- Preflight checks ------------------------------------------------------
+
+info "Remote NixOS installation: ${HOSTNAME} → ${TARGET_IP}"
+echo ""
+
+# Check the host config exists
+if ! nix eval ".#nixosConfigurations.${HOSTNAME}" --apply 'x: true' &>/dev/null; then
+    error "No NixOS configuration found for '${HOSTNAME}'"
+    error "Available hosts:"
+    nix eval '.#nixosConfigurations' --apply 'x: builtins.attrNames x' 2>/dev/null
+    exit 1
+fi
+
+# Verify SSH connectivity
+info "Verifying SSH connectivity to ${TARGET}..."
+if ! ssh_cmd "echo 'ok'" &>/dev/null; then
+    error "Cannot connect to ${TARGET}"
+    error "Make sure:"
+    error "  1. Target is booted from NixOS live USB"
+    error "  2. You've run 'passwd nixos' on the target"
+    error "  3. IP address ${TARGET_IP} is correct"
+    exit 1
+fi
+success "SSH connection verified"
+
+# --- Step 1: Copy SSH keys -------------------------------------------------
+
+if [[ "${COPY_KEYS}" == true ]]; then
+    info "Copying SSH public key to target..."
+    ssh-copy-id "${SSH_OPTS[@]}" "${TARGET}" 2>/dev/null || true
+fi
+
+# --- Step 2: Build disko script locally ------------------------------------
+
+DISKO_LINK="/tmp/disko-${HOSTNAME}"
+
+if [[ "${SKIP_FORMAT}" == false ]]; then
+    info "Building disko partitioning script..."
+    nix build ".#nixosConfigurations.${HOSTNAME}.config.system.build.diskoScript" \
+        -o "${DISKO_LINK}"
+    DISKO_SCRIPT=$(readlink -f "${DISKO_LINK}")
+    success "Disko script: ${DISKO_SCRIPT}"
+fi
+
+# --- Step 3: Build system closure locally -----------------------------------
+
+info "Building system closure (this may take a while)..."
+nix build ".#nixosConfigurations.${HOSTNAME}.config.system.build.toplevel"
+SYSTEM_PATH=$(readlink -f result)
+CLOSURE_SIZE=$(nix path-info -Sh "${SYSTEM_PATH}" 2>/dev/null | awk '{print $2}')
+success "System closure: ${SYSTEM_PATH} (${CLOSURE_SIZE})"
+
+# --- Step 4: Format and mount the target disk -------------------------------
+
+if [[ "${SKIP_FORMAT}" == false ]]; then
+    echo ""
+    warn "This will ERASE the target disk on ${TARGET_IP}!"
+    warn "LUKS-encrypted hosts will prompt for a passphrase."
+    echo ""
+    read -p "Continue with disk formatting? [y/N] " -n 1 -r
+    echo ""
+    if [[ ! $REPLY =~ ^[Yy]$ ]]; then
+        error "Aborted."
+        exit 1
+    fi
+
+    info "Copying disko script to target..."
+    NIX_SSHOPTS="${SSH_OPTS[*]}" nix copy --to "ssh://${TARGET}" "${DISKO_SCRIPT}"
+
+    info "Running disko on target..."
+    ssh_tty "sudo ${DISKO_SCRIPT}"
+    success "Disk formatted and mounted"
+fi
+
+# --- Step 5: Resize tmpfs ---------------------------------------------------
+
+info "Resizing tmpfs on target to ${TMPFS_SIZE}..."
+ssh_cmd "sudo mount -o remount,size=${TMPFS_SIZE} /nix/.rw-store"
+success "tmpfs resized to ${TMPFS_SIZE}"
+
+# --- Step 6: Copy system closure to target ----------------------------------
+
+info "Copying system closure to target (${CLOSURE_SIZE})..."
+NIX_SSHOPTS="${SSH_OPTS[*]}" nix copy -v --to "ssh://${TARGET}" "${SYSTEM_PATH}"
+success "System closure copied"
+
+# --- Step 7: Install --------------------------------------------------------
+
+info "Running nixos-install on target..."
+ssh_tty "sudo nixos-install --root /mnt --system '${SYSTEM_PATH}' --no-channel-copy"
+
+info "Running system activation..."
+ssh_tty "sudo nixos-enter --root /mnt -- /run/current-system/activate"
+success "Installation complete!"
+
+echo ""
+info "You can now reboot the target:"
+echo "  ssh ${SSH_OPTS[*]} ${TARGET} 'sudo reboot'"