{ pkgs, ... }: { environment.systemPackages = with pkgs; [ python3 git (pkgs.writeShellScriptBin "update-ref" '' set -euo pipefail RED='\033[31m'; YEL='\033[33m'; NC='\033[0m' die() { printf "''${RED}error:''${NC} %s\n" "$*" >&2; exit 2; } warn() { printf "''${YEL}warning:''${NC} %s\n" "$*" >&2; } usage() { cat >&2 <<'EOF' usage: update-ref [-R PATH|--athenix-repo=PATH] [-b BRANCH|--athenix-branch=BRANCH] [-m "msg"|--message "msg"] [-p[=false] [remote[=URL]]|--push[=false] [remote[=URL]]] [--make-local|-l] [--make-remote|-r] [--ssh] user= | system=: EOF exit 2 } # --- must be in a git repo (current dir) --- git rev-parse --is-inside-work-tree >/dev/null 2>&1 || die "This directory is not a git project" CUR_REPO_ROOT="$(git rev-parse --show-toplevel)" CUR_BRANCH="$(git rev-parse --abbrev-ref HEAD)" # --- athenix checkout (working tree) --- ATHENIX_DIR="$HOME/athenix" ATHENIX_BRANCH="" # --- current repo automation --- COMMIT_MSG="" PUSH_SPEC="" # --- push / url mode --- PUSH_SET=0 DO_PUSH=0 MODE_FORCE="" # "", local, remote TARGET="" is_remote_url() { # https://, http://, ssh://, or scp-style git@host:org/repo printf "%s" "$1" | grep -qE '^(https?|ssh)://|^[^/@:]+@[^/:]+:' } derive_full_hostname() { devtype="$1"; hostkey="$2" if printf "%s" "$hostkey" | grep -q '-' || printf "%s" "$hostkey" | grep -q "^$devtype"; then printf "%s" "$hostkey" elif printf "%s" "$hostkey" | grep -qE '^[0-9]+$'; then printf "%s" "$devtype$hostkey" else printf "%s" "$devtype-$hostkey" fi } extract_existing_fetch_url() { # args: mode file username key python3 - "$1" "$2" "$3" "$4" "$5"<<'PY' import sys, re, pathlib mode, file, username, key, use_ssh = sys.argv[1:5] t = pathlib.Path(file).read_text() def url_from_block(block: str) -> str: if not block: return "" m = re.search(r'url\s*=\s*"([^"]+)"\s*;', block) url = m.group(1) if m else "" if use_ssh = "true": return url # Already https if url.startswith("https://"): return url # ssh://git@host/org/repo.git m = re.match(r"ssh://(?:.+?)@([^/]+)/(.+)", url) if m: host, path = m.groups() return f"https://{host}/{path}" # git@host:org/repo.git m = re.match(r"(?:.+?)@([^:]+):(.+)", url) if m: host, path = m.groups() return f"https://{host}/{path}" # If you gave me something cursed raise ValueError(f"Unrecognized SSH git URL format: {url}") if mode == "user": m = re.search(r'(?s)\n\s*' + re.escape(username) + r'\.external\s*=\s*builtins\.fetchGit\s*\{(.*?)\n\s*\};', t) block = m.group(1) if m else "" print(url_from_block(block)) else: m = re.search(r'(?s)\n\s*"' + re.escape(key) + r'"\s*=\s*builtins\.fetchGit\s*\{(.*?)\n\s*\};', t) block = m.group(1) if m else "" print(url_from_block(block)) PY } # --- parse args --- while [ "$#" -gt 0 ]; do case "$1" in user=*|system=*) [ -z "$TARGET" ] || die "Only one subcommand allowed (user=... or system=...)" TARGET="$1"; shift ;; --athenix-repo=*) ATHENIX_DIR="''${1#*=}"; shift ;; -R) [ "$#" -ge 2 ] || usage ATHENIX_DIR="$2"; shift 2 ;; --athenix-branch=*) ATHENIX_BRANCH="''${1#*=}"; shift ;; -b) [ "$#" -ge 2 ] || usage ATHENIX_BRANCH="$2"; shift 2 ;; -m|--message) [ "$#" -ge 2 ] || usage COMMIT_MSG="$2"; shift 2 ;; -p|--push) PUSH_SET=1 DO_PUSH=1 PUSH_SPEC="" # If there is a next token, only consume it if it is a remote spec # and not another flag or the subcommand. if [ "$#" -ge 2 ]; then nxt="$2" if printf "%s" "$nxt" | grep -qE '^(user=|system=)'; then # next token is the subcommand; don't consume it shift elif printf "%s" "$nxt" | grep -qE '^-'; then # next token is another flag; don't consume it shift elif printf "%s" "$nxt" | grep -qE '^[A-Za-z0-9._-]+$'; then # remote name PUSH_SPEC="$nxt" shift 2 elif printf "%s" "$nxt" | grep -qE '^[A-Za-z0-9._-]+=.+$'; then # remote=URL PUSH_SPEC="$nxt" shift 2 else # unknown token; treat as not-a-push-spec and don't consume it shift fi else shift fi ;; -p=*|--push=*) PUSH_SET=1 val="''${1#*=}" case "$val" in false|0|no|off) DO_PUSH=0 ;; true|1|yes|on|"") DO_PUSH=1 ;; *) die "Invalid value for --push: $val (use true/false)" ;; esac shift ;; --make-local|-l) MODE_FORCE="local"; shift ;; --make-remote|-r) MODE_FORCE="remote"; shift ;; --ssh) USE_SSH="true"; shift ;; -h|--help) usage ;; *) die "Unknown argument: $1" ;; esac done [ -n "$TARGET" ] || die "Missing required subcommand: user= or system=:" # --- validate athenix working tree path --- [ -d "$ATHENIX_DIR" ] || die "$ATHENIX_DIR does not exist" git -C "$ATHENIX_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1 || die "$ATHENIX_DIR is not a git project (athenix checkout)" # --- -b behavior: fork/switch athenix working tree into branch --- if [ -n "$ATHENIX_BRANCH" ]; then ATH_CUR_BRANCH="$(git -C "$ATHENIX_DIR" rev-parse --abbrev-ref HEAD)" if [ "$ATH_CUR_BRANCH" != "$ATHENIX_BRANCH" ]; then if git -C "$ATHENIX_DIR" show-ref --verify --quiet "refs/heads/$ATHENIX_BRANCH"; then warn "Branch '$ATHENIX_BRANCH' already exists in $ATHENIX_DIR." warn "Delete and recreate it from current branch '$ATH_CUR_BRANCH' state? [y/N] " read -r ans || true case "''${ans:-N}" in y|Y|yes|YES) git -C "$ATHENIX_DIR" branch -D "$ATHENIX_BRANCH" git -C "$ATHENIX_DIR" switch -c "$ATHENIX_BRANCH" ;; *) git -C "$ATHENIX_DIR" switch "$ATHENIX_BRANCH" ;; esac else git -C "$ATHENIX_DIR" switch -c "$ATHENIX_BRANCH" fi fi fi # --- target file + identifiers --- MODE=""; FILE=""; USERNAME=""; DEVTYPE=""; HOSTKEY="" case "$TARGET" in user=*) MODE="user" USERNAME="''${TARGET#user=}" [ -n "$USERNAME" ] || die "user=: username missing" FILE="$ATHENIX_DIR/users.nix" ;; system=*) MODE="system" RHS="''${TARGET#system=}" printf "%s" "$RHS" | grep -q ':' || die "system=... must be system=:" DEVTYPE="''${RHS%%:*}" HOSTKEY="''${RHS#*:}" [ -n "$DEVTYPE" ] || die "system=:: device-type missing" [ -n "$HOSTKEY" ] || die "system=:: hostkey missing" FILE="$ATHENIX_DIR/inventory.nix" ;; esac [ -f "$FILE" ] || die "File not found: $FILE" # --- push default based on existing entry url in the target file --- EXISTING_URL="" ENTRY_EXISTS=0 if [ "$MODE" = "user" ]; then EXISTING_URL="$(extract_existing_fetch_url user "$FILE" "$USERNAME" "" "false")" [ -n "$EXISTING_URL" ] && ENTRY_EXISTS=1 || true else FULL="$(derive_full_hostname "$DEVTYPE" "$HOSTKEY")" EXISTING_URL="$(extract_existing_fetch_url system "$FILE" "" "$HOSTKEY")" if [ -n "$EXISTING_URL" ]; then ENTRY_EXISTS=1 elif [ "$FULL" != "$HOSTKEY" ]; then EXISTING_URL="$(extract_existing_fetch_url system "$FILE" "" "$FULL")" [ -n "$EXISTING_URL" ] && ENTRY_EXISTS=1 || true fi fi if [ "$PUSH_SET" -eq 0 ]; then if [ "$ENTRY_EXISTS" -eq 1 ] && is_remote_url "$EXISTING_URL"; then DO_PUSH=1 else DO_PUSH=0 [ "$MODE_FORCE" = "remote" ] && DO_PUSH=1 || true fi fi if [ "$MODE_FORCE" = "local" ] && [ "$PUSH_SET" -eq 0 ]; then DO_PUSH=0 fi # --- if current repo dirty, prompt --- if [ -n "$(git status --porcelain)" ]; then warn "This branch has untracked or uncommitted changes. Would you like to add, commit''${DO_PUSH:+, and push}? [y/N] " read -r ans || true case "''${ans:-N}" in y|Y|yes|YES) git add -A if ! git diff --cached --quiet; then if [ -n "$COMMIT_MSG" ]; then git commit -m "$COMMIT_MSG"; else git commit; fi else warn "No staged changes to commit." fi ;; *) warn "Proceeding without committing. (rev will be last committed HEAD.)" ;; esac fi # --- push current repo if requested --- PUSH_REMOTE_URL="" if [ "$DO_PUSH" -eq 1 ]; then if [ -n "$PUSH_SPEC" ]; then if printf "%s" "$PUSH_SPEC" | grep -q '='; then REM_NAME="''${PUSH_SPEC%%=*}" REM_URL="''${PUSH_SPEC#*=}" [ -n "$REM_NAME" ] || die "--push remote-name=URL: remote-name missing" [ -n "$REM_URL" ] || die "--push remote-name=URL: URL missing" if git remote get-url "$REM_NAME" >/dev/null 2>&1; then git remote set-url "$REM_NAME" "$REM_URL" else git remote add "$REM_NAME" "$REM_URL" fi git push -u "$REM_NAME" "$CUR_BRANCH" PUSH_REMOTE_URL="$REM_URL" else REM_NAME="$PUSH_SPEC" git push -u "$REM_NAME" "$CUR_BRANCH" PUSH_REMOTE_URL="$(git remote get-url "$REM_NAME")" fi else if ! git rev-parse --abbrev-ref --symbolic-full-name @{u} >/dev/null 2>&1; then die "No upstream is set. Set a default upstream with \"git branch -u /\"" fi git push UPSTREAM_REMOTE="$(git rev-parse --abbrev-ref --symbolic-full-name @{u} | cut -d/ -f1)" PUSH_REMOTE_URL="$(git remote get-url "$UPSTREAM_REMOTE")" fi fi CUR_REV="$(git -C "$CUR_REPO_ROOT" rev-parse HEAD)" # --- choose URL to write into fetchGit --- if [ "$MODE_FORCE" = "local" ]; then FETCH_URL="file://$CUR_REPO_ROOT" elif [ "$MODE_FORCE" = "remote" ]; then if [ "$DO_PUSH" -eq 1 ]; then FETCH_URL="$PUSH_REMOTE_URL" elif [ "$ENTRY_EXISTS" -eq 1 ] && [ -n "$EXISTING_URL" ] && is_remote_url "$EXISTING_URL"; then FETCH_URL="$EXISTING_URL" else CUR_ORIGIN="$(git remote get-url origin 2>/dev/null || true)" [ -n "$CUR_ORIGIN" ] && is_remote_url "$CUR_ORIGIN" || die "--make-remote requires a remote url (set origin or use -p remote=URL)" FETCH_URL="$CUR_ORIGIN" fi else if [ "$DO_PUSH" -eq 1 ]; then FETCH_URL="$PUSH_REMOTE_URL"; else FETCH_URL="file://$CUR_REPO_ROOT"; fi fi # --- rewrite users.nix or inventory.nix --- python3 - "$MODE" "$FILE" "$FETCH_URL" "$CUR_REV" "$USERNAME" "$DEVTYPE" "$HOSTKEY" <<'PY' import sys, re, pathlib mode = sys.argv[1] path = pathlib.Path(sys.argv[2]) fetch_url = sys.argv[3] rev = sys.argv[4] username = sys.argv[5] devtype = sys.argv[6] hostkey = sys.argv[7] text = path.read_text() def find_matching_brace(s: str, start: int) -> int: depth = 0 i = start in_str = False while i < len(s): ch = s[i] if in_str: if ch == '\\': i += 2 continue if ch == '"': in_str = False i += 1 continue if ch == '"': in_str = True i += 1 continue if ch == '{': depth += 1 elif ch == '}': depth -= 1 if depth == 0: return i i += 1 raise ValueError("Could not find matching '}'") def mk_fetch(entry_indent: str) -> str: # entry_indent is indentation for the whole `"key" = ;` line. # The attrset contents should be indented one level deeper. inner = entry_indent + " " return ( 'builtins.fetchGit {\n' f'{inner}url = "{fetch_url}";\n' f'{inner}rev = "{rev}";\n' f'{inner}submodules = true;\n' f'{entry_indent}}}' ) def full_hostname(devtype: str, hostkey: str) -> str: if hostkey.startswith(devtype) or "-" in hostkey: return hostkey if hostkey.isdigit(): return f"{devtype}{hostkey}" return f"{devtype}-{hostkey}" def update_user(t: str) -> str: mblock = re.search(r"(?s)athenix\.users\s*=\s*\{(.*?)\n\s*\};", t) if not mblock: raise SystemExit("error: could not locate `athenix.users = { ... };` block") # locate the full span of the users block to edit inside it # (re-find with groups for reconstruction) m2 = re.search(r"(?s)(athenix\.users\s*=\s*\{)(.*?)(\n\s*\};)", t) head, body, tail = m2.group(1), m2.group(2), m2.group(3) entry_re = re.search( r"(?s)(\n[ \t]*" + re.escape(username) + r"\.external\s*=\s*)builtins\.fetchGit\s*\{", body ) if entry_re: brace = body.rfind("{", 0, entry_re.end()) end = find_matching_brace(body, brace) semi = re.match(r"\s*;", body[end+1:]) if not semi: raise SystemExit("error: expected ';' after fetchGit attrset") semi_end = end + 1 + semi.end() line_start = body.rfind("\n", 0, entry_re.start()) + 1 indent = re.match(r"[ \t]*", body[line_start:entry_re.start()]).group(0) new_body = body[:entry_re.start()] + entry_re.group(1) + mk_fetch(indent) + ";" + body[semi_end:] else: indent = " " new_body = body + f"\n{indent}{username}.external = {mk_fetch(indent)};\n" return t[:m2.start()] + head + new_body + tail + t[m2.end():] def update_system(t: str) -> str: # Find devtype block robustly: start-of-file or newline. m = re.search(r"(?s)(^|\n)[ \t]*" + re.escape(devtype) + r"\s*=\s*\{", t) if not m: raise SystemExit(f"error: could not locate `{devtype} = {{ ... }};` block") dev_open = t.find("{", m.end() - 1) dev_close = find_matching_brace(t, dev_open) dev = t[dev_open:dev_close+1] # Find devices attrset inside dev dm = re.search(r"(?s)(^|\n)[ \t]*devices\s*=\s*\{", dev) if not dm: raise SystemExit(f"error: could not locate `devices = {{ ... }};` inside `{devtype}`") devices_open = dev.find("{", dm.end() - 1) devices_close = find_matching_brace(dev, devices_open) devices = dev[devices_open:devices_close+1] # indentation for entries in devices # find indent of the 'devices' line, then add 2 spaces candidates = [hostkey, full_hostname(devtype, hostkey)] seen = set() candidates = [c for c in candidates if not (c in seen or seen.add(c))] for key in candidates: entry = re.search( r'(?s)\n([ ]*)"' + re.escape(key) + r'"\s*=\s*builtins\.fetchGit\s*\{', devices ) if entry: entry_indent = entry.group(1) # find the '{' we matched brace = devices.find("{", entry.end() - 1) end = find_matching_brace(devices, brace) semi = re.match(r"\s*;", devices[end+1:]) if not semi: raise SystemExit("error: expected ';' after fetchGit attrset in devices") semi_end = end + 1 + semi.end() # Reconstruct the prefix: newline + indent + "key" = prefix = f'\n{entry_indent}"{key}" = ' new_devices = ( devices[:entry.start()] + prefix + mk_fetch(entry_indent) + ";" + devices[semi_end:] ) new_dev = dev[:devices_open] + new_devices + dev[devices_close+1:] return t[:dev_open] + new_dev + t[dev_close+1:] # Not found: append into devices (exact hostkey) # Indent for new entries: take indent of the closing '}' of devices, add 2 spaces. close_line_start = devices.rfind("\n", 0, len(devices)-1) + 1 close_indent = re.match(r"[ ]*", devices[close_line_start:]).group(0) entry_indent = close_indent + " " insertion = f'\n{entry_indent}"{hostkey}" = {mk_fetch(entry_indent)};\n' new_devices = devices[:-1].rstrip() + insertion + close_indent + "}" new_dev = dev[:devices_open] + new_devices + dev[devices_close+1:] return t[:dev_open] + new_dev + t[dev_close+1:] if mode == "user": out = update_user(text) elif mode == "system": out = update_system(text) else: raise SystemExit("error: unknown mode") path.write_text(out) PY cd $ATHENIX_DIR nix fmt **/*.nix cd $CUR_REPO_ROOT printf "updated %s\n" "$FILE" >&2 printf " url = %s\n" "$FETCH_URL" >&2 printf " rev = %s\n" "$CUR_REV" >&2 '') ]; }