refactor: cleanup scripts
This commit is contained in:
Generated
+3
-3
@@ -7,11 +7,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1778401693,
|
"lastModified": 1778606796,
|
||||||
"narHash": "sha256-OVHdCqXXUF5UdGkH+FF2ZL06OLZjj2kvP2dIUmzVWoo=",
|
"narHash": "sha256-P2krpSkFVYJ89bgsnAZ9RtQiGwiTW77sfSJp9SEDscM=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "home-manager",
|
"repo": "home-manager",
|
||||||
"rev": "389b83002efc26f1145e89a6a8e6edc5a6435948",
|
"rev": "e1fd7350f4410972bcb8c42a697d8c924ffe642a",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
set positional-arguments
|
set positional-arguments
|
||||||
|
|
||||||
host := `hostname`
|
host := `hostname`
|
||||||
key_services := "github gradescope"
|
|
||||||
|
|
||||||
# Default: list available recipes.
|
# Default: list available recipes.
|
||||||
default:
|
default:
|
||||||
@@ -15,125 +14,40 @@ rebuild:
|
|||||||
update:
|
update:
|
||||||
nix flake update
|
nix flake update
|
||||||
|
|
||||||
# Clean up old generations and optimize store
|
# Clean up old generations and optimize store, then bake the result for next boot
|
||||||
gc:
|
gc:
|
||||||
sudo nix-collect-garbage --delete-older-than 14d
|
sudo nix-collect-garbage --delete-older-than 14d
|
||||||
sudo nix-store --optimise
|
sudo nix-store --optimise
|
||||||
sudo nixos-rebuild boot --flake .#{{host}}
|
sudo nixos-rebuild boot --flake .#{{host}}
|
||||||
|
|
||||||
# Generate Ed25519 SSH keys for each configured service
|
# Generate Ed25519 SSH keys for each named service (default: github gradescope).
|
||||||
gen-keys:
|
# Override: just gen-keys gitlab bitbucket
|
||||||
#!/usr/bin/env bash
|
gen-keys *services='github gradescope':
|
||||||
set -euo pipefail
|
@./scripts/gen-keys.sh {{services}}
|
||||||
|
|
||||||
mkdir -p ~/.ssh
|
# Download the latest NixOS installer ISO into iso/.
|
||||||
chmod 700 ~/.ssh
|
# Override defaults: just fetch-iso --channel nixos-unstable --edition minimal
|
||||||
|
fetch-iso *args:
|
||||||
|
@./scripts/fetch-iso.sh "$@"
|
||||||
|
|
||||||
for service in {{key_services}}; do
|
# Write a bootable NixOS installer USB.
|
||||||
file="$HOME/.ssh/id_$service"
|
# Example: just make-usb --iso iso/latest-nixos-graphical-x86_64-linux.iso
|
||||||
if [ -f "$file" ]; then
|
# The script self-elevates via sudo if needed.
|
||||||
echo "skipping: $file already exists"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
echo "generating: $file"
|
|
||||||
ssh-keygen -t ed25519 -f "$file" -N "" -C "$service@$(hostname -s)"
|
|
||||||
done
|
|
||||||
|
|
||||||
# Build a bootable NixOS installer USB (writes ISO + optional extras partition).
|
|
||||||
# Pass arguments through to scripts/make-usb.sh, e.g.:
|
|
||||||
# just make-usb --iso ~/Downloads/nixos.iso
|
|
||||||
# Re-executes itself under sudo if not already root. Quoting is preserved
|
|
||||||
# via `set positional-arguments` + "$@".
|
|
||||||
make-usb *args:
|
make-usb *args:
|
||||||
#!/usr/bin/env bash
|
@./scripts/make-usb.sh "$@"
|
||||||
set -euo pipefail
|
|
||||||
if [[ ! -x scripts/make-usb.sh ]]; then
|
|
||||||
chmod +x scripts/make-usb.sh
|
|
||||||
fi
|
|
||||||
if (( EUID == 0 )); then
|
|
||||||
exec ./scripts/make-usb.sh "$@"
|
|
||||||
else
|
|
||||||
exec sudo --preserve-env=PATH ./scripts/make-usb.sh "$@"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Register a new host: scaffold hosts/<name>/ and add a flake.nix entry.
|
# Scaffold hosts/<name>/ and add a flake.nix entry.
|
||||||
# Example: just register-host blackbear
|
# Example: just register-host blackbear
|
||||||
register-host name:
|
register-host name:
|
||||||
@python3 scripts/register-host.py --repo-root . "{{name}}"
|
@./scripts/register-host.py --repo-root . "{{name}}"
|
||||||
|
|
||||||
# Capture the current machine's hardware config into hosts/<name>/hardware.nix.
|
# Capture the current machine's hardware into hosts/<name>/hardware.nix.
|
||||||
# Run this on the *new* machine. Two contexts work:
|
# Run this on the new machine.
|
||||||
# 1. Already-installed NixOS: just runs nixos-generate-config against /.
|
# Example: just capture-hardware blackbear
|
||||||
# 2. Booted from installer with target mounted at /mnt: detected
|
|
||||||
# automatically and --root /mnt is used.
|
|
||||||
# Example: just capture-hardware blackbear
|
|
||||||
capture-hardware name:
|
capture-hardware name:
|
||||||
#!/usr/bin/env bash
|
@./scripts/capture-hardware.sh "{{name}}"
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
name="{{name}}"
|
# Show which hosts are currently registered in flake.nix.
|
||||||
target="hosts/$name/hardware.nix"
|
|
||||||
|
|
||||||
if [[ ! -d "hosts/$name" ]]; then
|
|
||||||
echo "[-] hosts/$name does not exist. Run \`just register-host $name\` first." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v nixos-generate-config >/dev/null 2>&1; then
|
|
||||||
echo "[-] nixos-generate-config not found." >&2
|
|
||||||
echo " Run this command on a NixOS machine (or inside the installer)." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Detect whether we're in the installer with a target mounted at /mnt.
|
|
||||||
extra_args=()
|
|
||||||
if mountpoint -q /mnt 2>/dev/null && [[ -d /mnt/etc ]]; then
|
|
||||||
echo "[+] Detected mounted target at /mnt — using --root /mnt"
|
|
||||||
extra_args+=(--root /mnt)
|
|
||||||
elif mountpoint -q /mnt 2>/dev/null; then
|
|
||||||
echo "[!] /mnt is mounted but doesn't look like a NixOS target (no /mnt/etc)."
|
|
||||||
echo " Continuing as if on a running system. Pass --no-filesystems by editing"
|
|
||||||
echo " hardware.nix afterwards if disks aren't permanent."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Confirm before overwriting a non-placeholder hardware.nix.
|
|
||||||
if [[ -f "$target" ]] && ! grep -q 'PLACEHOLDER hardware.nix' "$target"; then
|
|
||||||
echo "[!] $target already exists and doesn't look like a placeholder." >&2
|
|
||||||
read -r -p "Overwrite? (yes/[no]) " confirm
|
|
||||||
[[ "$confirm" == "yes" ]] || { echo "aborted."; exit 1; }
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[+] Generating hardware config..."
|
|
||||||
# The redirect runs in the user's shell BEFORE sudo, so the file is
|
|
||||||
# owned by you, not root. sudo is needed because nixos-generate-config
|
|
||||||
# reads /proc, /sys, and may probe mounted filesystems.
|
|
||||||
sudo nixos-generate-config --show-hardware-config "${extra_args[@]}" > "$target"
|
|
||||||
echo "[+] Wrote $target"
|
|
||||||
if command -v git >/dev/null && git -C . rev-parse >/dev/null 2>&1; then
|
|
||||||
echo "[+] git diff:"
|
|
||||||
git --no-pager diff -- "$target" || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Show which hosts are currently registered in flake.nix (parses the file directly).
|
|
||||||
hosts:
|
hosts:
|
||||||
#!/usr/bin/env python3
|
@./scripts/list-hosts.py
|
||||||
import re, sys
|
|
||||||
src = open('flake.nix').read()
|
|
||||||
m = re.search(r'nixosConfigurations\s*=\s*\{', src)
|
|
||||||
if not m:
|
|
||||||
sys.exit(0)
|
|
||||||
# Brace-match to find the closing }.
|
|
||||||
depth, i, close = 0, m.end() - 1, None
|
|
||||||
while i < len(src):
|
|
||||||
c = src[i]
|
|
||||||
if c == '{': depth += 1
|
|
||||||
elif c == '}':
|
|
||||||
depth -= 1
|
|
||||||
if depth == 0:
|
|
||||||
close = i
|
|
||||||
break
|
|
||||||
i += 1
|
|
||||||
block = src[m.end():close] if close else ''
|
|
||||||
for name in re.findall(r'^[ \t]*([A-Za-z_][\w-]*)\s*=\s*nixpkgs\.lib\.nixosSystem', block, re.M):
|
|
||||||
print(name)
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,5 +11,10 @@
|
|||||||
curl
|
curl
|
||||||
git
|
git
|
||||||
tree
|
tree
|
||||||
|
util-linux
|
||||||
|
coreutils
|
||||||
|
gptfdisk
|
||||||
|
exfatprogs
|
||||||
|
parted
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
Executable
+82
@@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Generate hardware.nix for a registered host.
|
||||||
|
#
|
||||||
|
# Run this on the NEW machine (the one whose hardware you want captured),
|
||||||
|
# inside this repo. Two contexts are auto-detected:
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
NAME=""
|
||||||
|
REPO_ROOT="."
|
||||||
|
|
||||||
|
while (($#)); do
|
||||||
|
case "$1" in
|
||||||
|
--repo-root) REPO_ROOT="${2:?missing path}"; shift 2 ;;
|
||||||
|
-h|--help)
|
||||||
|
awk '/^#!/ {next} /^[^#]/ {exit} {sub(/^# ?/, ""); print}' "$0"
|
||||||
|
exit 0 ;;
|
||||||
|
-*) echo "[-] unknown flag: $1" >&2; exit 1 ;;
|
||||||
|
*)
|
||||||
|
if [[ -z "$NAME" ]]; then
|
||||||
|
NAME="$1"; shift
|
||||||
|
else
|
||||||
|
echo "[-] unexpected positional arg: $1" >&2; exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$NAME" ]]; then
|
||||||
|
echo "[-] usage: $0 <name>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
target="hosts/${NAME}/hardware.nix"
|
||||||
|
|
||||||
|
if [[ ! -d "hosts/${NAME}" ]]; then
|
||||||
|
echo "[-] hosts/${NAME} does not exist." >&2
|
||||||
|
echo " Run \`just register-host ${NAME}\` first." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v nixos-generate-config >/dev/null 2>&1; then
|
||||||
|
echo "[-] nixos-generate-config not found." >&2
|
||||||
|
echo " Run this on a NixOS machine (running system or installer ISO)." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Detect installer-mounted-at-/mnt vs running-system.
|
||||||
|
extra_args=()
|
||||||
|
if mountpoint -q /mnt 2>/dev/null && [[ -d /mnt/etc ]]; then
|
||||||
|
echo "[+] Detected mounted target at /mnt — using --root /mnt"
|
||||||
|
extra_args+=(--root /mnt)
|
||||||
|
elif mountpoint -q /mnt 2>/dev/null; then
|
||||||
|
echo "[!] /mnt is mounted but doesn't look like a NixOS target (no /mnt/etc)."
|
||||||
|
echo " Continuing as if on a running system. Edit hardware.nix afterwards"
|
||||||
|
echo " if disks aren't permanent."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Confirm before overwriting a non-placeholder hardware.nix.
|
||||||
|
if [[ -f "$target" ]] && ! grep -q 'PLACEHOLDER hardware.nix' "$target"; then
|
||||||
|
echo "[!] $target already exists and doesn't look like a placeholder." >&2
|
||||||
|
if [[ -t 0 ]]; then
|
||||||
|
read -r -p "Overwrite? (yes/[no]) " confirm
|
||||||
|
[[ "$confirm" == "yes" ]] || { echo "aborted."; exit 1; }
|
||||||
|
else
|
||||||
|
echo "[-] non-interactive; refusing to overwrite. Delete $target first." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[+] Generating hardware config..."
|
||||||
|
# The redirect runs in the user's shell BEFORE sudo, so the file is owned
|
||||||
|
# by the invoking user. sudo is needed because nixos-generate-config
|
||||||
|
# probes /proc, /sys, and may need to read mounted filesystems.
|
||||||
|
sudo nixos-generate-config --show-hardware-config "${extra_args[@]}" > "$target"
|
||||||
|
echo "[+] Wrote $target"
|
||||||
|
|
||||||
|
if command -v git >/dev/null && git rev-parse >/dev/null 2>&1; then
|
||||||
|
echo "[+] git diff:"
|
||||||
|
git --no-pager diff -- "$target" || true
|
||||||
|
fi
|
||||||
Executable
+81
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Download the latest NixOS installer ISO into iso/.
|
||||||
|
#
|
||||||
|
# Saves the ISO under its real upstream snapshot name (e.g.
|
||||||
|
# nixos-graphical-25.11.10684.8fd9daa3db09-x86_64-linux.iso) so different
|
||||||
|
# channel heads don't clobber each other. Maintains a stable
|
||||||
|
# `latest-nixos-<edition>-<arch>.iso` symlink in iso/ pointing at the newest.
|
||||||
|
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CHANNEL="nixos-25.11"
|
||||||
|
EDITION="graphical"
|
||||||
|
ARCH="x86_64-linux"
|
||||||
|
DEST_DIR="iso"
|
||||||
|
|
||||||
|
while (($#)); do
|
||||||
|
case "$1" in
|
||||||
|
--channel) CHANNEL="${2:?missing value}"; shift 2 ;;
|
||||||
|
--edition) EDITION="${2:?missing value}"; shift 2 ;;
|
||||||
|
--arch) ARCH="${2:?missing value}"; shift 2 ;;
|
||||||
|
--dest) DEST_DIR="${2:?missing value}"; shift 2 ;;
|
||||||
|
-h|--help)
|
||||||
|
awk '/^#!/ {next} /^[^#]/ {exit} {sub(/^# ?/, ""); print}' "$0"
|
||||||
|
exit 0 ;;
|
||||||
|
*) echo "[-] unknown arg: $1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
BASE="https://channels.nixos.org/${CHANNEL}"
|
||||||
|
LATEST_NAME="latest-nixos-${EDITION}-${ARCH}.iso"
|
||||||
|
|
||||||
|
mkdir -p "$DEST_DIR"
|
||||||
|
|
||||||
|
# Fetch the small sha256 file first. Its format is `<hash> <real-filename>`
|
||||||
|
# so we can derive the actual snapshot filename from it.
|
||||||
|
echo "[+] Fetching ${BASE}/${LATEST_NAME}.sha256"
|
||||||
|
tmp_sum="$(mktemp)"
|
||||||
|
trap 'rm -f "$tmp_sum"' EXIT
|
||||||
|
curl --fail --location --silent --show-error --output "$tmp_sum" \
|
||||||
|
"${BASE}/${LATEST_NAME}.sha256" \
|
||||||
|
|| { echo "[-] sha256 fetch failed" >&2; exit 2; }
|
||||||
|
|
||||||
|
real_name="$(awk '{print $2}' "$tmp_sum")"
|
||||||
|
if [[ -z "$real_name" || "$real_name" != *.iso ]]; then
|
||||||
|
echo "[-] couldn't parse filename from upstream sha256:" >&2
|
||||||
|
cat "$tmp_sum" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
iso="${DEST_DIR}/${real_name}"
|
||||||
|
sum="${DEST_DIR}/${real_name}.sha256"
|
||||||
|
|
||||||
|
if [[ -f "$iso" && -f "$sum" ]]; then
|
||||||
|
if ( cd "$DEST_DIR" && sha256sum --status -c "${real_name}.sha256" ); then
|
||||||
|
echo "[+] ${iso} already present and verified."
|
||||||
|
ln -sfn "${real_name}" "${DEST_DIR}/${LATEST_NAME}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "[!] ${iso} present but sha256 mismatch — re-downloading from scratch."
|
||||||
|
# curl --continue-at would resume from the corrupt bytes; nuke first.
|
||||||
|
rm -f "$iso"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mv "$tmp_sum" "$sum"
|
||||||
|
trap - EXIT
|
||||||
|
|
||||||
|
echo "[+] Downloading ${BASE}/${LATEST_NAME}"
|
||||||
|
echo " -> ${iso}"
|
||||||
|
curl --fail --location --progress-bar --continue-at - \
|
||||||
|
--output "$iso" "${BASE}/${LATEST_NAME}" \
|
||||||
|
|| { echo "[-] download failed" >&2; exit 2; }
|
||||||
|
|
||||||
|
echo "[+] Verifying sha256..."
|
||||||
|
( cd "$DEST_DIR" && sha256sum -c "${real_name}.sha256" ) \
|
||||||
|
|| { echo "[-] sha256 verification failed" >&2; exit 3; }
|
||||||
|
|
||||||
|
ln -sfn "${real_name}" "${DEST_DIR}/${LATEST_NAME}"
|
||||||
|
echo "[+] ${iso}"
|
||||||
|
echo "[+] symlink: ${DEST_DIR}/${LATEST_NAME} -> ${real_name}"
|
||||||
Executable
+30
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Generate Ed25519 SSH keys for the given service names.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||||
|
awk '/^#!/ {next} /^[^#]/ {exit} {sub(/^# ?/, ""); print}' "$0"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( $# == 0 )); then
|
||||||
|
echo "[+] No services specified; nothing to do."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
chmod 700 ~/.ssh
|
||||||
|
|
||||||
|
host_short="$(hostname -s)"
|
||||||
|
|
||||||
|
for service in "$@"; do
|
||||||
|
file="$HOME/.ssh/id_$service"
|
||||||
|
if [[ -f "$file" ]]; then
|
||||||
|
echo "[+] skipping: $file already exists"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo "[+] generating: $file"
|
||||||
|
ssh-keygen -t ed25519 -f "$file" -N "" -C "$service@$host_short"
|
||||||
|
done
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Print the nixosConfigurations defined in flake.nix.
|
||||||
|
|
||||||
|
Parses flake.nix directly (no `nix eval`, so it works without nix tooling
|
||||||
|
and is fast). Brace-matches to find the closing `}` of nixosConfigurations
|
||||||
|
so it handles nested attrsets correctly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def list_hosts(flake_path: Path) -> list[str]:
|
||||||
|
src = flake_path.read_text()
|
||||||
|
m = re.search(r"nixosConfigurations\s*=\s*\{", src)
|
||||||
|
if not m:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Brace-match to find the matching closing }.
|
||||||
|
depth, i, close = 0, m.end() - 1, None
|
||||||
|
while i < len(src):
|
||||||
|
c = src[i]
|
||||||
|
if c == "{":
|
||||||
|
depth += 1
|
||||||
|
elif c == "}":
|
||||||
|
depth -= 1
|
||||||
|
if depth == 0:
|
||||||
|
close = i
|
||||||
|
break
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
block = src[m.end():close] if close is not None else ""
|
||||||
|
return re.findall(
|
||||||
|
r"^[ \t]*([A-Za-z_][\w-]*)\s*=\s*nixpkgs\.lib\.nixosSystem",
|
||||||
|
block,
|
||||||
|
re.M,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
flake = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("flake.nix")
|
||||||
|
if not flake.is_file():
|
||||||
|
print(f"[-] not a file: {flake}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
for name in list_hosts(flake):
|
||||||
|
print(name)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Regular → Executable
+67
-177
@@ -1,16 +1,16 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# make-usb.sh
|
# Write a NixOS installer ISO to a USB stick (raw dd).
|
||||||
#
|
#
|
||||||
# Write a NixOS installer ISO to a USB stick (raw dd)
|
# The script self-elevates via sudo if not already running as root, so
|
||||||
# and optionally append a writable "extras" partition with files
|
# you can run it as your normal user.
|
||||||
# (this repo, SSH keys, etc.) that you want available during install.
|
#
|
||||||
|
# After booting the resulting USB on the target machine, fetch this repo
|
||||||
|
# over the network with `git clone <url>` from the installer shell.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Styling ###
|
|
||||||
|
|
||||||
if [[ -t 1 ]]; then
|
if [[ -t 1 ]]; then
|
||||||
GREEN=$'\e[32m'; YELLOW=$'\e[33m'; RED=$'\e[31m'; BLUE=$'\e[34m'
|
GREEN=$'\e[32m'; YELLOW=$'\e[33m'; RED=$'\e[31m'; BLUE=$'\e[34m'
|
||||||
BOLD=$'\e[1m'; RESET=$'\e[0m'
|
BOLD=$'\e[1m'; RESET=$'\e[0m'
|
||||||
@@ -23,23 +23,25 @@ err() { printf '%s[-]%s %s\n' "$RED" "$RESET" "$*" >&2; }
|
|||||||
info() { printf '%s[i]%s %s\n' "$BLUE" "$RESET" "$*"; }
|
info() { printf '%s[i]%s %s\n' "$BLUE" "$RESET" "$*"; }
|
||||||
die() { err "$1"; exit "${2:-1}"; }
|
die() { err "$1"; exit "${2:-1}"; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ISO_PATH=""
|
ISO_PATH=""
|
||||||
DEVICE=""
|
DEVICE=""
|
||||||
SKIP_EXTRAS=0
|
|
||||||
ALLOW_INTERNAL=0
|
ALLOW_INTERNAL=0
|
||||||
ASSUME_YES=0
|
ASSUME_YES=0
|
||||||
REPO_ROOT=""
|
|
||||||
|
# Save original args before the parser shifts them away; needed if we
|
||||||
|
# later re-exec under sudo.
|
||||||
|
ORIG_ARGS=("$@")
|
||||||
|
|
||||||
while (($#)); do
|
while (($#)); do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--iso) ISO_PATH="${2:?missing path}"; shift 2 ;;
|
--iso) ISO_PATH="${2:?missing path}"; shift 2 ;;
|
||||||
--device|--dev) DEVICE="${2:?missing path}"; shift 2 ;;
|
--device|--dev) DEVICE="${2:?missing path}"; shift 2 ;;
|
||||||
--no-extras) SKIP_EXTRAS=1; shift ;;
|
|
||||||
--allow-internal) ALLOW_INTERNAL=1; shift ;;
|
--allow-internal) ALLOW_INTERNAL=1; shift ;;
|
||||||
--yes|-y) ASSUME_YES=1; shift ;;
|
--yes|-y) ASSUME_YES=1; shift ;;
|
||||||
--repo-root) REPO_ROOT="${2:?missing path}"; shift 2 ;;
|
|
||||||
-h|--help)
|
-h|--help)
|
||||||
sed -n '2,16p' "$0" | sed 's/^# \{0,1\}//'
|
awk '/^#!/ {next} /^[^#]/ {exit} {sub(/^# ?/, ""); print}' "$0"
|
||||||
exit 0 ;;
|
exit 0 ;;
|
||||||
*) die "unknown arg: $1" 2 ;;
|
*) die "unknown arg: $1" 2 ;;
|
||||||
esac
|
esac
|
||||||
@@ -47,10 +49,19 @@ done
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Preflight checks ###
|
# Self-elevate via sudo if not already root. We re-exec rather than tell
|
||||||
|
# the user to prefix the command, so `just make-usb` and `./scripts/make-usb.sh`
|
||||||
(( EUID == 0 )) || die "run as root: sudo $0 $*" 1
|
# both Just Work.
|
||||||
|
if (( EUID != 0 )); then
|
||||||
|
if ! command -v sudo >/dev/null 2>&1; then
|
||||||
|
die "must run as root and sudo is not installed" 1
|
||||||
|
fi
|
||||||
|
# ORIG_ARGS may be empty; ${ORIG_ARGS[@]+"${ORIG_ARGS[@]}"} expands
|
||||||
|
# safely under set -u even when the array is unset.
|
||||||
|
exec sudo --preserve-env=PATH -- "$0" ${ORIG_ARGS[@]+"${ORIG_ARGS[@]}"}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Require an interactive tty for any prompt we still need.
|
||||||
need_tty=0
|
need_tty=0
|
||||||
[[ -z "$DEVICE" || -z "$ISO_PATH" ]] && need_tty=1
|
[[ -z "$DEVICE" || -z "$ISO_PATH" ]] && need_tty=1
|
||||||
(( ASSUME_YES )) || need_tty=1
|
(( ASSUME_YES )) || need_tty=1
|
||||||
@@ -58,8 +69,8 @@ if (( need_tty )) && ! [[ -t 0 ]]; then
|
|||||||
die "stdin is not a tty; pass --iso, --device, and --yes for non-interactive use" 2
|
die "stdin is not a tty; pass --iso, --device, and --yes for non-interactive use" 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Required commands. We try to be helpful about missing ones on NixOS.
|
# Required commands.
|
||||||
REQUIRED=(lsblk dd sync wipefs sgdisk partprobe blockdev mount umount mountpoint mkfs.exfat awk)
|
REQUIRED=(lsblk dd sync wipefs blockdev mount umount awk)
|
||||||
missing=()
|
missing=()
|
||||||
for cmd in "${REQUIRED[@]}"; do
|
for cmd in "${REQUIRED[@]}"; do
|
||||||
command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd")
|
command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd")
|
||||||
@@ -69,42 +80,16 @@ if ((${#missing[@]})); then
|
|||||||
cat >&2 <<'EOF'
|
cat >&2 <<'EOF'
|
||||||
|
|
||||||
On NixOS / with Nix installed, re-run inside a shell that has them:
|
On NixOS / with Nix installed, re-run inside a shell that has them:
|
||||||
nix shell nixpkgs#util-linux nixpkgs#coreutils nixpkgs#gptfdisk \
|
nix shell nixpkgs#util-linux nixpkgs#coreutils -c sudo ./scripts/make-usb.sh
|
||||||
nixpkgs#exfatprogs nixpkgs#parted -c sudo ./scripts/make-usb.sh
|
|
||||||
|
|
||||||
On Debian/Ubuntu:
|
On Debian/Ubuntu:
|
||||||
sudo apt install util-linux coreutils gdisk exfatprogs parted
|
sudo apt install util-linux coreutils
|
||||||
EOF
|
EOF
|
||||||
exit 3
|
exit 3
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Locate repo root if not provided (used as a copy candidate for extras).
|
|
||||||
if [[ -z "$REPO_ROOT" ]]; then
|
|
||||||
if REPO_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel 2>/dev/null)"; then
|
|
||||||
:
|
|
||||||
else
|
|
||||||
REPO_ROOT=""
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Cleanup trap ###
|
|
||||||
|
|
||||||
TMP_MNT=""
|
|
||||||
cleanup() {
|
|
||||||
set +e
|
|
||||||
if [[ -n "$TMP_MNT" && -d "$TMP_MNT" ]]; then
|
|
||||||
mountpoint -q "$TMP_MNT" && umount "$TMP_MNT"
|
|
||||||
rmdir "$TMP_MNT" 2>/dev/null
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
trap cleanup EXIT INT TERM
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Helpers ###
|
|
||||||
|
|
||||||
human() { numfmt --to=iec --suffix=B "$1" 2>/dev/null || echo "$1"; }
|
human() { numfmt --to=iec --suffix=B "$1" 2>/dev/null || echo "$1"; }
|
||||||
|
|
||||||
is_removable() {
|
is_removable() {
|
||||||
@@ -182,8 +167,6 @@ select_device() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Pick device ###
|
|
||||||
|
|
||||||
if [[ -z "$DEVICE" ]]; then
|
if [[ -z "$DEVICE" ]]; then
|
||||||
select_device
|
select_device
|
||||||
else
|
else
|
||||||
@@ -205,8 +188,6 @@ log "Target: $DEVICE ($(human "$DEV_BYTES"))"
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Pick ISO ###
|
|
||||||
|
|
||||||
if [[ -z "$ISO_PATH" ]]; then
|
if [[ -z "$ISO_PATH" ]]; then
|
||||||
while :; do
|
while :; do
|
||||||
read -r -e -p "Path to NixOS ISO: " ISO_PATH
|
read -r -e -p "Path to NixOS ISO: " ISO_PATH
|
||||||
@@ -228,18 +209,20 @@ if command -v file >/dev/null 2>&1; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Sanity: ISO must fit on disk with room for an extras partition.
|
# Sanity: ISO must fit on disk.
|
||||||
MIN_EXTRAS_MB=64
|
if (( ISO_BYTES > DEV_BYTES )); then
|
||||||
if (( ISO_BYTES + MIN_EXTRAS_MB*1024*1024 > DEV_BYTES )); then
|
die "ISO is larger than the disk" 4
|
||||||
if (( ISO_BYTES > DEV_BYTES )); then
|
|
||||||
die "ISO is larger than the disk" 4
|
|
||||||
fi
|
|
||||||
warn "Less than ${MIN_EXTRAS_MB}MiB free after ISO; extras partition will be skipped."
|
|
||||||
SKIP_EXTRAS=1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Optional sha256 sidecar verification.
|
# Optional sha256 sidecar verification.
|
||||||
if [[ -f "$ISO_PATH.sha256" ]]; then
|
# Resolve through symlinks so `make-usb --iso iso/latest-...iso` finds
|
||||||
|
# the sidecar that lives next to the real file.
|
||||||
|
ISO_REAL="$(readlink -f "$ISO_PATH")"
|
||||||
|
if [[ -f "$ISO_REAL.sha256" ]]; then
|
||||||
|
log "Verifying $ISO_REAL.sha256..."
|
||||||
|
( cd "$(dirname "$ISO_REAL")" && sha256sum -c "$(basename "$ISO_REAL").sha256" ) \
|
||||||
|
|| die "sha256 verification failed" 5
|
||||||
|
elif [[ -f "$ISO_PATH.sha256" ]]; then
|
||||||
log "Verifying $ISO_PATH.sha256..."
|
log "Verifying $ISO_PATH.sha256..."
|
||||||
( cd "$(dirname "$ISO_PATH")" && sha256sum -c "$(basename "$ISO_PATH").sha256" ) \
|
( cd "$(dirname "$ISO_PATH")" && sha256sum -c "$(basename "$ISO_PATH").sha256" ) \
|
||||||
|| die "sha256 verification failed" 5
|
|| die "sha256 verification failed" 5
|
||||||
@@ -247,14 +230,11 @@ fi
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Final confirmation ###
|
|
||||||
|
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
|
|
||||||
${BOLD}About to:${RESET}
|
${BOLD}About to:${RESET}
|
||||||
${RED}WIPE${RESET} $DEVICE ($(human "$DEV_BYTES"))
|
${RED}WIPE${RESET} $DEVICE ($(human "$DEV_BYTES"))
|
||||||
Write ISO $ISO_PATH
|
Write ISO $ISO_PATH
|
||||||
Add extras $( ((SKIP_EXTRAS)) && echo "no" || echo "yes (exFAT, in trailing free space)" )
|
|
||||||
|
|
||||||
${YELLOW}All data on $DEVICE will be destroyed.${RESET}
|
${YELLOW}All data on $DEVICE will be destroyed.${RESET}
|
||||||
EOF
|
EOF
|
||||||
@@ -266,8 +246,6 @@ fi
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Unmount anything on the target ###
|
|
||||||
|
|
||||||
log "Unmounting any partitions on $DEVICE..."
|
log "Unmounting any partitions on $DEVICE..."
|
||||||
while read -r part mnt; do
|
while read -r part mnt; do
|
||||||
[[ -n "$mnt" ]] || continue
|
[[ -n "$mnt" ]] || continue
|
||||||
@@ -275,131 +253,43 @@ while read -r part mnt; do
|
|||||||
umount "$part" 2>/dev/null || umount -l "$part" 2>/dev/null || warn "could not umount $part"
|
umount "$part" 2>/dev/null || umount -l "$part" 2>/dev/null || warn "could not umount $part"
|
||||||
done < <(lsblk -lnpo NAME,MOUNTPOINT "$DEVICE" | awk '$2!=""')
|
done < <(lsblk -lnpo NAME,MOUNTPOINT "$DEVICE" | awk '$2!=""')
|
||||||
|
|
||||||
### Wipe & write ###
|
|
||||||
|
|
||||||
log "Wiping signatures..."
|
log "Wiping signatures..."
|
||||||
wipefs -a "$DEVICE" >/dev/null
|
wipefs -a "$DEVICE" >/dev/null
|
||||||
|
|
||||||
log "Writing ISO with dd (this can take a few minutes)..."
|
log "Writing ISO with dd (this can take a few minutes)..."
|
||||||
dd if="$ISO_PATH" of="$DEVICE" bs=4M status=progress conv=fsync oflag=direct \
|
if ! dd if="$ISO_PATH" of="$DEVICE" bs=4M status=progress conv=fsync oflag=direct; then
|
||||||
|| die "dd failed" 5
|
err "dd failed."
|
||||||
sync
|
# Surface the most recent kernel complaints about this device so the
|
||||||
log "ISO written."
|
# user can distinguish bad-block / hardware failure ("Medium Error",
|
||||||
|
# "Hardware Error") from cable/port flakiness ("usb ... reset",
|
||||||
partprobe "$DEVICE" 2>/dev/null || true
|
# "Communication failure") without having to dig through dmesg.
|
||||||
sleep 1
|
dev_short="$(basename "$DEVICE")"
|
||||||
|
if dmesg_out="$(dmesg 2>/dev/null | grep -E "(${dev_short}|usb [0-9]+-[0-9]+)" | tail -10)" \
|
||||||
|
&& [[ -n "$dmesg_out" ]]; then
|
||||||
|
warn "Recent kernel messages mentioning ${dev_short} or USB events:"
|
||||||
### Extras partition ###
|
printf '%s\n' "$dmesg_out" | sed 's/^/ /' >&2
|
||||||
|
warn "Look for 'Medium Error' / 'Hardware Error' (bad flash → replace stick),"
|
||||||
if (( SKIP_EXTRAS )); then
|
warn "or 'reset' / 'Communication failure' (cable/port → try a different one)."
|
||||||
log "Skipping extras partition."
|
else
|
||||||
else
|
warn "Run \`sudo dmesg | tail -30\` to see why the kernel rejected the write."
|
||||||
log "Preparing extras partition in trailing free space..."
|
fi
|
||||||
|
exit 5
|
||||||
# The NixOS ISO uses an isohybrid GPT. The backup GPT header still sits at
|
|
||||||
# the *original ISO end*, not the actual disk end. `sgdisk -e` relocates it
|
|
||||||
# so we can add a partition in the freed trailing region.
|
|
||||||
# `sgdisk -n` with 0:0:0 creates a new partition starting at the first free
|
|
||||||
# sector and ending at the last free sector (i.e., filling the rest).
|
|
||||||
sgdisk -e "$DEVICE" >/dev/null
|
|
||||||
sgdisk -n 0:0:0 -t 0:0700 -c 0:"EXTRAS" "$DEVICE" >/dev/null
|
|
||||||
partprobe "$DEVICE" 2>/dev/null || true
|
|
||||||
sleep 1
|
|
||||||
|
|
||||||
# Find the new partition (highest-numbered one on the disk).
|
|
||||||
EXTRAS_PART="$(lsblk -lnpo NAME,TYPE "$DEVICE" \
|
|
||||||
| awk '$2=="part"{print $1}' | tail -n1)"
|
|
||||||
[[ -b "$EXTRAS_PART" ]] || die "extras partition didn't appear" 5
|
|
||||||
log "Extras partition: $EXTRAS_PART"
|
|
||||||
|
|
||||||
log "Formatting $EXTRAS_PART as exFAT..."
|
|
||||||
mkfs.exfat -L EXTRAS "$EXTRAS_PART" >/dev/null 2>&1 \
|
|
||||||
|| die "mkfs.exfat failed" 5
|
|
||||||
|
|
||||||
TMP_MNT="$(mktemp -d)"
|
|
||||||
mount "$EXTRAS_PART" "$TMP_MNT"
|
|
||||||
|
|
||||||
echo
|
|
||||||
log "What to copy onto the extras partition? (space-separated numbers; Enter for none)"
|
|
||||||
echo
|
|
||||||
echo " 1) This repo ${REPO_ROOT:-<not detected>}"
|
|
||||||
echo " 2) Your public SSH keys ~/.ssh/*.pub"
|
|
||||||
echo " 3) An arbitrary file/dir (you'll be prompted for the path)"
|
|
||||||
echo " 4) A custom message/README (you type it inline)"
|
|
||||||
echo
|
|
||||||
|
|
||||||
read -r -p "Selection: " sel || sel=""
|
|
||||||
for tok in $sel; do
|
|
||||||
case "$tok" in
|
|
||||||
1)
|
|
||||||
if [[ -z "$REPO_ROOT" ]]; then
|
|
||||||
warn "no repo detected; pass --repo-root or run from a git checkout"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
log "Copying repo -> /repo (git tracked files only)..."
|
|
||||||
# Use `git archive` if possible, it respects .gitignore and skips .git.
|
|
||||||
if git -C "$REPO_ROOT" rev-parse >/dev/null 2>&1; then
|
|
||||||
mkdir -p "$TMP_MNT/repo"
|
|
||||||
git -C "$REPO_ROOT" archive --format=tar HEAD \
|
|
||||||
| tar -x -C "$TMP_MNT/repo"
|
|
||||||
else
|
|
||||||
mkdir -p "$TMP_MNT/repo"
|
|
||||||
cp -aT "$REPO_ROOT" "$TMP_MNT/repo"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
2)
|
|
||||||
# Run as the invoking user, not root, so ~ resolves correctly.
|
|
||||||
user_home="$(getent passwd "${SUDO_USER:-$USER}" | cut -d: -f6)"
|
|
||||||
if compgen -G "$user_home/.ssh/*.pub" >/dev/null; then
|
|
||||||
mkdir -p "$TMP_MNT/ssh-keys"
|
|
||||||
cp "$user_home"/.ssh/*.pub "$TMP_MNT/ssh-keys/"
|
|
||||||
log "Copied $(ls "$user_home"/.ssh/*.pub | wc -l) public key(s)."
|
|
||||||
else
|
|
||||||
warn "no .pub keys found in $user_home/.ssh"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
3)
|
|
||||||
read -r -e -p "Path to copy: " extra_path
|
|
||||||
extra_path="${extra_path/#\~/$HOME}"
|
|
||||||
if [[ -e "$extra_path" ]]; then
|
|
||||||
cp -a "$extra_path" "$TMP_MNT/"
|
|
||||||
log "Copied $extra_path"
|
|
||||||
else
|
|
||||||
warn "not found: $extra_path"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
4)
|
|
||||||
info "Type your note. End with a line containing only EOF:"
|
|
||||||
: >"$TMP_MNT/NOTE.txt"
|
|
||||||
while IFS= read -r line; do
|
|
||||||
[[ "$line" == "EOF" ]] && break
|
|
||||||
printf '%s\n' "$line" >>"$TMP_MNT/NOTE.txt"
|
|
||||||
done
|
|
||||||
;;
|
|
||||||
*) warn "unknown option: $tok" ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
sync
|
|
||||||
umount "$TMP_MNT"
|
|
||||||
rmdir "$TMP_MNT"
|
|
||||||
TMP_MNT=""
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
log "Final sync (flushing kernel buffers; may take a moment)..."
|
||||||
|
|
||||||
### Done ###
|
|
||||||
|
|
||||||
log "Final sync..."
|
|
||||||
sync
|
sync
|
||||||
blockdev --flushbufs "$DEVICE" 2>/dev/null || true
|
blockdev --flushbufs "$DEVICE" 2>/dev/null || true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
echo
|
echo
|
||||||
log "${BOLD}Done.${RESET} You can unplug $DEVICE now."
|
log "${BOLD}Done.${RESET} You can unplug $DEVICE now."
|
||||||
echo
|
echo
|
||||||
info "Boot the new machine from this USB. Once at the installer shell:"
|
info "Boot the new machine from this USB. Once at the installer shell:"
|
||||||
info " - If you copied the repo, it's on the EXTRAS partition (mountable as exFAT)."
|
info " - Connect to the network (Ethernet, or 'sudo systemctl start wpa_supplicant' + 'wpa_cli', or 'nmtui' on the graphical ISO)."
|
||||||
info " - To install: partition the target disk, mount at /mnt, then"
|
info " - Clone your repo: git clone <your-repo-url> /tmp/notfiles"
|
||||||
info " nixos-install --flake /mnt/extras/repo#<hostname>"
|
info " - Partition the target disk, mount at /mnt, then:"
|
||||||
|
info " nixos-install --flake /tmp/notfiles#<hostname>"
|
||||||
echo
|
echo
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ from pathlib import Path
|
|||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
|
|
||||||
|
|
||||||
### Color helpers ###
|
|
||||||
|
|
||||||
def _supports_color() -> bool:
|
def _supports_color() -> bool:
|
||||||
return sys.stderr.isatty()
|
return sys.stderr.isatty()
|
||||||
@@ -42,8 +41,6 @@ def die(msg: str, code: int = 1) -> None:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Validation ###
|
|
||||||
|
|
||||||
# RFC 1123 hostname: 1-63 chars, [a-z0-9-], no leading/trailing dash.
|
# RFC 1123 hostname: 1-63 chars, [a-z0-9-], no leading/trailing dash.
|
||||||
HOST_RE = re.compile(r"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$")
|
HOST_RE = re.compile(r"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$")
|
||||||
|
|
||||||
@@ -53,8 +50,6 @@ def valid_hostname(name: str) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Templates ###
|
|
||||||
|
|
||||||
DEFAULT_NIX_TEMPLATE = dedent("""\
|
DEFAULT_NIX_TEMPLATE = dedent("""\
|
||||||
{{ pkgs, ... }}: {{
|
{{ pkgs, ... }}: {{
|
||||||
imports = [
|
imports = [
|
||||||
@@ -105,7 +100,6 @@ PLACEHOLDER_HARDWARE = dedent("""\
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Helpers ###
|
|
||||||
|
|
||||||
def add_to_flake(flake_path: Path, name: str, *, template_host: str | None = None) -> None:
|
def add_to_flake(flake_path: Path, name: str, *, template_host: str | None = None) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -119,13 +113,11 @@ def add_to_flake(flake_path: Path, name: str, *, template_host: str | None = Non
|
|||||||
"""
|
"""
|
||||||
src = flake_path.read_text()
|
src = flake_path.read_text()
|
||||||
|
|
||||||
# Find `nixosConfigurations = {`; be flexible about whitespace.
|
|
||||||
m = re.search(r"nixosConfigurations\s*=\s*\{", src)
|
m = re.search(r"nixosConfigurations\s*=\s*\{", src)
|
||||||
if not m:
|
if not m:
|
||||||
die(f"could not find `nixosConfigurations = {{` in {flake_path}")
|
die(f"could not find `nixosConfigurations = {{` in {flake_path}")
|
||||||
|
|
||||||
open_pos = m.end() - 1 # index of the '{'
|
open_pos = m.end() - 1 # index of the '{'
|
||||||
|
|
||||||
# Brace-match to find the closing '}' of this attrset.
|
# Brace-match to find the closing '}' of this attrset.
|
||||||
depth = 0
|
depth = 0
|
||||||
close_pos = None
|
close_pos = None
|
||||||
@@ -250,7 +242,7 @@ def add_to_flake(flake_path: Path, name: str, *, template_host: str | None = Non
|
|||||||
|
|
||||||
|
|
||||||
def host_in_flake(flake_path: Path, name: str) -> bool:
|
def host_in_flake(flake_path: Path, name: str) -> bool:
|
||||||
"""Cheap textual check; does flake.nix already define this host?"""
|
"""Cheap textual check, does flake.nix already define this host?"""
|
||||||
src = flake_path.read_text()
|
src = flake_path.read_text()
|
||||||
return bool(
|
return bool(
|
||||||
re.search(rf"(^|\W){re.escape(name)}\s*=\s*nixpkgs\.lib\.nixosSystem", src)
|
re.search(rf"(^|\W){re.escape(name)}\s*=\s*nixpkgs\.lib\.nixosSystem", src)
|
||||||
@@ -269,6 +261,7 @@ def find_template_host(flake_path: Path) -> str | None:
|
|||||||
return m2.group(1) if m2 else None
|
return m2.group(1) if m2 else None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
p.add_argument("name", help="hostname for the new host (lowercase, RFC 1123)")
|
p.add_argument("name", help="hostname for the new host (lowercase, RFC 1123)")
|
||||||
@@ -291,9 +284,9 @@ def main() -> int:
|
|||||||
die(f"host `{args.name}` already defined in {flake} — refusing to touch anything")
|
die(f"host `{args.name}` already defined in {flake} — refusing to touch anything")
|
||||||
|
|
||||||
template_host = find_template_host(flake)
|
template_host = find_template_host(flake)
|
||||||
log(f"repo: {repo}")
|
log(f"repo: {repo}")
|
||||||
log(f"new host: {args.name}")
|
log(f"new host: {args.name}")
|
||||||
log(f"template host: {template_host or '<none — using default block>'}")
|
log(f"template host: {template_host or '<none, using default block>'}")
|
||||||
|
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
warn("dry-run: nothing written")
|
warn("dry-run: nothing written")
|
||||||
|
|||||||
Reference in New Issue
Block a user