diff --git a/scripts/codex-maintenance.sh b/scripts/codex-maintenance.sh index 5518e78..8e5f845 100755 --- a/scripts/codex-maintenance.sh +++ b/scripts/codex-maintenance.sh @@ -1,22 +1,14 @@ #!/usr/bin/env bash set -euo pipefail -export NIX_CONFIG="${NIX_CONFIG:-} -experimental-features = nix-command flakes -accept-flake-config = false -warn-dirty = false -" +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/nix-bootstrap.sh +source "${script_dir}/lib/nix-bootstrap.sh" +# shellcheck source=lib/nix-eval.sh +source "${script_dir}/lib/nix-eval.sh" MODE="${1:-validate}" -ensure_nix_profile() { - if [ -f /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh ]; then - . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh - elif [ -f "$HOME/.nix-profile/etc/profile.d/nix.sh" ]; then - . "$HOME/.nix-profile/etc/profile.d/nix.sh" - fi -} - ensure_nix_profile if ! command -v nix >/dev/null 2>&1; then @@ -24,8 +16,7 @@ if ! command -v nix >/dev/null 2>&1; then exit 127 fi -hosts_json="$(nix eval --json --no-use-registries --no-accept-flake-config .#nixosConfigurations --apply builtins.attrNames)" -hosts="$(echo "$hosts_json" | jq -r '.[]')" +hosts="$(list_flake_targets .)" echo "Hosts:" echo "$hosts" @@ -44,17 +35,17 @@ fi echo echo "Checking Nix formatting with nixpkgs-fmt..." -nix run --no-use-registries --no-accept-flake-config github:NixOS/nixpkgs/nixos-25.11#nixpkgs-fmt -- --check . +nix run "${NIX_EVAL_FLAGS[@]}" github:NixOS/nixpkgs/nixos-25.11#nixpkgs-fmt -- --check . echo echo "Running statix lint..." -nix run --no-use-registries --no-accept-flake-config github:NixOS/nixpkgs/nixos-25.11#statix -- check . +nix run "${NIX_EVAL_FLAGS[@]}" github:NixOS/nixpkgs/nixos-25.11#statix -- check . echo echo "Evaluating host toplevel derivations..." for host in $hosts; do echo "==> $host" - nix eval --raw --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.toplevel.drvPath" + nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.toplevel.drvPath" # lxc-* hosts deploy via a directly pct-restore-able tarball instead of # nixos-install (see docs/auto-installer.md); proxmox-* hosts can @@ -64,22 +55,21 @@ for host in $hosts; do case "$host" in lxc-*) echo "==> $host (tarball)" - nix eval --raw --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.tarball.drvPath" + nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.tarball.drvPath" ;; proxmox-*) echo "==> $host (diskoImagesScript)" - nix eval --raw --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.diskoImagesScript.drvPath" + nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.diskoImagesScript.drvPath" ;; esac done echo echo "Evaluating buildable packages..." -packages_json="$(nix eval --json --no-use-registries --no-accept-flake-config .#packages.x86_64-linux --apply builtins.attrNames)" -packages="$(echo "$packages_json" | jq -r '.[]')" +packages="$(nix eval --json "${NIX_EVAL_FLAGS[@]}" .#packages.x86_64-linux --apply builtins.attrNames | jq -r '.[]')" for pkg in $packages; do echo "==> packages.x86_64-linux.${pkg}" - nix eval --raw --no-use-registries --no-accept-flake-config ".#packages.x86_64-linux.${pkg}" + nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#packages.x86_64-linux.${pkg}" done if [[ "$MODE" == "dry-run" ]]; then @@ -87,16 +77,16 @@ if [[ "$MODE" == "dry-run" ]]; then echo "Running dry-run builds for all hosts. This will not create result symlinks." for host in $hosts; do echo "==> Dry-run build: $host" - nix build --dry-run --no-link --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.toplevel" + nix build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.toplevel" case "$host" in lxc-*) echo "==> Dry-run build: $host (tarball)" - nix build --dry-run --no-link --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.tarball" + nix build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.tarball" ;; proxmox-*) echo "==> Dry-run build: $host (diskoImagesScript)" - nix build --dry-run --no-link --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.diskoImagesScript" + nix build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.diskoImagesScript" ;; esac done @@ -105,7 +95,7 @@ if [[ "$MODE" == "dry-run" ]]; then echo "Running dry-run builds for all packages." for pkg in $packages; do echo "==> Dry-run build: packages.x86_64-linux.${pkg}" - nix build --dry-run --no-link --no-use-registries --no-accept-flake-config ".#packages.x86_64-linux.${pkg}" + nix build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}" ".#packages.x86_64-linux.${pkg}" done fi diff --git a/scripts/codex-setup.sh b/scripts/codex-setup.sh index 99f6e43..e1e67c8 100755 --- a/scripts/codex-setup.sh +++ b/scripts/codex-setup.sh @@ -1,19 +1,11 @@ #!/usr/bin/env bash set -euo pipefail -export NIX_CONFIG="${NIX_CONFIG:-} -experimental-features = nix-command flakes -accept-flake-config = false -warn-dirty = false -" - -ensure_nix_profile() { - if [ -f /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh ]; then - . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh - elif [ -f "$HOME/.nix-profile/etc/profile.d/nix.sh" ]; then - . "$HOME/.nix-profile/etc/profile.d/nix.sh" - fi -} +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/nix-bootstrap.sh +source "${script_dir}/lib/nix-bootstrap.sh" +# shellcheck source=lib/nix-eval.sh +source "${script_dir}/lib/nix-eval.sh" install_nix_if_missing() { if command -v nix >/dev/null 2>&1; then @@ -79,13 +71,13 @@ if ! command -v jq >/dev/null 2>&1; then fi echo "Available NixOS hosts:" -hosts="$(nix eval --json --no-use-registries --no-accept-flake-config .#nixosConfigurations --apply builtins.attrNames | jq -r '.[]')" +hosts="$(list_flake_targets .)" echo "$hosts" echo "Evaluating all host toplevel derivations..." for host in $hosts; do echo "==> Evaluating $host" - nix eval --raw --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.toplevel.drvPath" + nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.toplevel.drvPath" done echo "Codex setup complete." diff --git a/scripts/create-proxmox-resource.sh b/scripts/create-proxmox-resource.sh index e10473e..c3434de 100755 --- a/scripts/create-proxmox-resource.sh +++ b/scripts/create-proxmox-resource.sh @@ -26,6 +26,8 @@ set -euo pipefail repo_root="$(cd "$(dirname "$0")/.." && pwd)" # shellcheck source=env.sh source "${repo_root}/scripts/env.sh" +# shellcheck source=lib/nix-eval.sh +source "${repo_root}/scripts/lib/nix-eval.sh" sync_keys="${repo_root}/scripts/sync-host-keys.sh" @@ -240,15 +242,21 @@ platform_prefix="lxc" [[ -z "$memory" ]] && memory="$PROXMOX_DEFAULT_MEMORY_MB" # --- discover / resolve the flake target from --host -------------------- +# Emits "\t" pairs for every ${platform_prefix}-* flake +# target -- the one source both --list and the --host lookup below read +# from, so they can never see a different set of targets from each other. +targets_for_platform() { + local target + for target in $(list_flake_targets "$repo_root" 2>/dev/null | grep -- "^${platform_prefix}-"); do + printf '%s\t%s\n' "$target" "$(flake_target_hostname "$repo_root" "$target")" + done +} + list_hosts() { local target hostname - for target in $(nix eval --json --no-use-registries --no-accept-flake-config \ - "${repo_root}#nixosConfigurations" --apply builtins.attrNames 2>/dev/null \ - | jq -r --arg p "${platform_prefix}-" '.[] | select(startswith($p))'); do - hostname="$(nix eval --raw --no-use-registries --no-accept-flake-config \ - "${repo_root}#nixosConfigurations.${target}.config.networking.hostName" 2>/dev/null)" + while IFS=$'\t' read -r target hostname; do printf ' %-12s -> %s\n' "$hostname" "$target" - done + done < <(targets_for_platform) } if [[ "$do_list" -eq 1 ]]; then @@ -263,16 +271,12 @@ if [[ -z "$host" ]]; then fi flake_target="" -for target in $(nix eval --json --no-use-registries --no-accept-flake-config \ - "${repo_root}#nixosConfigurations" --apply builtins.attrNames \ - | jq -r --arg p "${platform_prefix}-" '.[] | select(startswith($p))'); do - hn="$(nix eval --raw --no-use-registries --no-accept-flake-config \ - "${repo_root}#nixosConfigurations.${target}.config.networking.hostName")" - if [[ "$hn" == "$host" ]]; then +while IFS=$'\t' read -r target hostname; do + if [[ "$hostname" == "$host" ]]; then flake_target="$target" break fi -done +done < <(targets_for_platform) if [[ -z "$flake_target" ]]; then echo "ERROR: no ${platform_prefix}-* target has hostName '${host}'." >&2 diff --git a/scripts/lib/nix-bootstrap.sh b/scripts/lib/nix-bootstrap.sh new file mode 100644 index 0000000..f972703 --- /dev/null +++ b/scripts/lib/nix-bootstrap.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Shared Nix bootstrap for scripts/codex-setup.sh and +# scripts/codex-maintenance.sh: the nix.conf settings both need in effect +# before a single `nix` command runs (flakes enabled, never honor a flake +# input's own nixConfig, no "dirty tree" warning spam), plus a helper to +# pull an already-installed Nix's daemon/profile script onto PATH if it +# isn't there yet. Source this instead of copying it -- see CLAUDE.md. +export NIX_CONFIG="${NIX_CONFIG:-} +experimental-features = nix-command flakes +accept-flake-config = false +warn-dirty = false +" + +ensure_nix_profile() { + if [ -f /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh ]; then + . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh + elif [ -f "$HOME/.nix-profile/etc/profile.d/nix.sh" ]; then + . "$HOME/.nix-profile/etc/profile.d/nix.sh" + fi +} diff --git a/scripts/lib/nix-eval.sh b/scripts/lib/nix-eval.sh new file mode 100644 index 0000000..cca380e --- /dev/null +++ b/scripts/lib/nix-eval.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Shared flake-introspection helpers for scripts/*.sh. Source alongside +# env.sh: +# source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/nix-eval.sh" +# +# NIX_EVAL_FLAGS: --no-use-registries so a call here never resolves through +# the user's global flake registry (every call targets this repo's own +# flake, or an explicit github: ref, not a registry alias); --no-accept-flake-config +# so a flake input's own nixConfig (e.g. a dependency's substituters) is +# never honored -- matches accept-flake-config = false already set repo-wide +# (see lib/nix-bootstrap.sh / CLAUDE.md). Reuse this array rather than +# retyping the two flags at each call site. +declare -a NIX_EVAL_FLAGS=(--no-use-registries --no-accept-flake-config) + +# list_flake_targets +# Prints the attribute names under #nixosConfigurations, one per +# line, e.g.: +# list_flake_targets . # from inside the repo +# list_flake_targets "$repo_root" # from anywhere +list_flake_targets() { + local flake_ref="$1" + nix eval --json "${NIX_EVAL_FLAGS[@]}" \ + "${flake_ref}#nixosConfigurations" --apply builtins.attrNames \ + | jq -r '.[]' +} + +# flake_target_hostname +# Prints one nixosConfigurations target's config.networking.hostName. +# Empty (not an error under set -e) if the target doesn't exist or the +# eval otherwise fails -- callers that need to distinguish "empty" from +# "eval failed" should check $? themselves instead of relying on this. +flake_target_hostname() { + local flake_ref="$1" target="$2" + nix eval --raw "${NIX_EVAL_FLAGS[@]}" \ + "${flake_ref}#nixosConfigurations.${target}.config.networking.hostName" 2>/dev/null +} diff --git a/scripts/lib/ssh-host-keys.sh b/scripts/lib/ssh-host-keys.sh new file mode 100644 index 0000000..cd0b658 --- /dev/null +++ b/scripts/lib/ssh-host-keys.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Shared SSH-host-key / age-conversion helpers for scripts/sync-host-keys.sh +# and scripts/prepare-host-key.sh -- both generate the same kind of key +# (ed25519, no passphrase, the sops-nix age-derivation input) and convert it +# to an age recipient the same way; kept in one place so the two can't +# drift apart. +# +# Uses NIX_OPTS (an array of extra `nix-shell` options -- see env.sh's +# nix_extra_opts) if the caller has already set it, so a decision to avoid +# an unreachable nix-cache is reused here instead of probed again. Falls +# back to no extra options if the caller never sourced env.sh. +if ! declare -p NIX_OPTS >/dev/null 2>&1; then + declare -a NIX_OPTS=() +fi + +# generate_host_ed25519_key +# Writes and .pub. Caller is responsible for refusing to +# overwrite an existing keyfile -- this always runs ssh-keygen fresh. +generate_host_ed25519_key() { + local hostname="$1" keyfile="$2" + nix-shell "${NIX_OPTS[@]}" -p openssh --run \ + "ssh-keygen -t ed25519 -N '' -C '${hostname}' -f '${keyfile}'" >/dev/null +} + +# ssh_pubkey_to_age +# Prints the age public key derived from an ed25519 SSH public key file. +ssh_pubkey_to_age() { + local pubkeyfile="$1" + nix-shell "${NIX_OPTS[@]}" -p ssh-to-age --run "ssh-to-age -i '${pubkeyfile}'" +} diff --git a/scripts/prepare-host-key.sh b/scripts/prepare-host-key.sh index 94f0b3d..fb311ca 100755 --- a/scripts/prepare-host-key.sh +++ b/scripts/prepare-host-key.sh @@ -23,6 +23,10 @@ set -euo pipefail repo_root="$(cd "$(dirname "$0")/.." && pwd)" +# shellcheck source=env.sh +source "${repo_root}/scripts/env.sh" +# shellcheck source=lib/ssh-host-keys.sh +source "${repo_root}/scripts/lib/ssh-host-keys.sh" hostname="${1:?usage: scripts/prepare-host-key.sh }" sops_yaml="${repo_root}/.sops.yaml" @@ -41,9 +45,10 @@ if [[ -f "$keyfile" ]]; then exit 1 fi -nix-shell -p openssh --run "ssh-keygen -t ed25519 -N '' -C '${hostname}' -f '${keyfile}'" >/dev/null +nix_extra_opts +generate_host_ed25519_key "$hostname" "$keyfile" -age_pub="$(nix-shell -p ssh-to-age --run "ssh-to-age -i '${keyfile}.pub'")" +age_pub="$(ssh_pubkey_to_age "${keyfile}.pub")" cat < ${host}: generating host key" - nix-shell "${NIX_OPTS[@]}" -p openssh --run "ssh-keygen -t ed25519 -N '' -C '${host}' -f '${keyfile}'" >/dev/null + generate_host_ed25519_key "$host" "$keyfile" fi else echo "==> ${host}: host key already present" @@ -170,7 +172,7 @@ queue_host_sync() { if [[ "$dry_run" -eq 1 ]]; then age_pub="dry-run-placeholder-not-a-real-key" else - age_pub="$(nix-shell "${NIX_OPTS[@]}" -p ssh-to-age --run "ssh-to-age -i '${keyfile}.pub'")" + age_pub="$(ssh_pubkey_to_age "${keyfile}.pub")" fi add_keys_json="$(jq --arg host "$host" --arg key "$age_pub" \ '. + [{host: $host, age_key: $key}]' <<<"$add_keys_json")"