From 35223b6435917f8deced74667aebb7a85d4e992f Mon Sep 17 00:00:00 2001 From: UGA Innovation Factory Date: Fri, 19 Dec 2025 13:08:59 -0500 Subject: [PATCH] add a reference updater tool --- sw/default.nix | 1 + sw/update-ref.nix | 416 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 417 insertions(+) create mode 100644 sw/update-ref.nix diff --git a/sw/default.nix b/sw/default.nix index 97f8c64..6db06d4 100644 --- a/sw/default.nix +++ b/sw/default.nix @@ -29,6 +29,7 @@ in ./python.nix ./ghostty.nix ./updater.nix + ./update-ref.nix ]; options.athenix.sw = { diff --git a/sw/update-ref.nix b/sw/update-ref.nix new file mode 100644 index 0000000..b9c1a69 --- /dev/null +++ b/sw/update-ref.nix @@ -0,0 +1,416 @@ +{ pkgs, ... }: +{ + environment.systemPackages = [ + pkgs.python3 + (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; } + + # ---- 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 ---- + ATHENIX_DIR="$HOME/athenix" + ATHENIX_REPO="" + ATHENIX_BRANCH="" + + # ---- Git automation options for CURRENT repo ---- + COMMIT_MSG="" + PUSH_SPEC="" + + # ---- Push + mode controls ---- + PUSH_SET=0 # user explicitly set push true/false + DO_PUSH=0 # final push decision + MODE_FORCE="" # "", "local", "remote" (from -l/-r) + + # ---- Required subcommand ---- + TARGET="" # user= OR system=: + + usage() { + cat >&2 <<'EOF' + usage: + update-ref [--athenix-repo=PATH|URL|-R PATH|URL] [--athenix-branch=BRANCH|-b BRANCH] + [-m "msg" | --message "msg"] + [-p[=false] [remote[=URL]] | --push[=false] [remote[=URL]]] + [--make-local|-l] [--make-remote|-r] + user= | system=: + + push default: + - determined from the existing builtins.fetchGit url in the target entry: + remote url -> push=true + file/local -> push=false + - if the entry doesn't exist: push=false unless --make-remote + + EOF + exit 2 + } + + 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" <<'PY' + import sys, re, pathlib + + mode = sys.argv[1] # user | system + path = pathlib.Path(sys.argv[2]) + username = sys.argv[3] # user mode + key = sys.argv[4] # system mode: exact string key + + text = path.read_text() + + def find_fetch_block_user(u): + m = re.search(r'(?s)\n\s*' + re.escape(u) + r'\.external\s*=\s*builtins\.fetchGit\s*\{(.*?)\n\s*\};', text) + return m.group(1) if m else None + + def find_fetch_block_system(k): + m = re.search(r'(?s)\n\s*"' + re.escape(k) + r'"\s*=\s*builtins\.fetchGit\s*\{(.*?)\n\s*\};', text) + return m.group(1) if m else None + + def extract_url(block): + if not block: + return "" + m = re.search(r'url\s*=\s*"([^"]+)"\s*;', block) + return m.group(1) if m else "" + + block = None + if mode == "user": + block = find_fetch_block_user(username) + else: + block = find_fetch_block_system(key) + + print(extract_url(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_REPO="''${1#*=}" + shift + ;; + -R) + [ "$#" -ge 2 ] || usage + ATHENIX_REPO="$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 + # optional next token remote or remote=URL (only if it doesn't look like another flag/subcommand) + if [ "$#" -ge 2 ] && printf "%s" "$2" | grep -qvE '^(user=|system=|--|-l$|-r$)'; then + PUSH_SPEC="$2" + shift 2 + 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 + ;; + + -h|--help) usage ;; + *) die "Unknown argument: $1" ;; + esac + done + + [ -n "$TARGET" ] || die "Missing required subcommand: user= or system=:" + + # ---- Validate athenix checkout ---- + [ -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" + if [ -z "$ATHENIX_BRANCH" ]; then + ATHENIX_BRANCH="$(git -C "$ATHENIX_DIR" rev-parse --abbrev-ref HEAD)" + fi + + # ---- Determine target file + identifiers ---- + FILE="" + MODE="" + 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" + + # ---- Determine existing fetchGit url in the entry (for push default) ---- + EXISTING_URL="" + ENTRY_EXISTS=0 + + if [ "$MODE" = "user" ]; then + EXISTING_URL="$(extract_existing_fetch_url user "$FILE" "$USERNAME" "")" + [ -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 + + # ---- Default push based on existing entry url unless user explicitly set it ---- + if [ "$PUSH_SET" -eq 0 ]; then + if [ "$ENTRY_EXISTS" -eq 1 ] && is_remote_url "$EXISTING_URL"; then + DO_PUSH=1 + else + # entry missing or local/file url + if [ "$MODE_FORCE" = "remote" ]; then + DO_PUSH=1 + else + DO_PUSH=0 + fi + fi + fi + + # If forcing local and user didn't explicitly demand push, push defaults off. + if [ "$MODE_FORCE" = "local" ] && [ "$PUSH_SET" -eq 0 ]; then + DO_PUSH=0 + fi + + # ---- Dirty check 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 + elif [ -n "$COMMIT_MSG" ]; then + # clean tree but message provided: nothing to do + : + 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 + # default: push to upstream, error if none + 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 + + # ---- Always get current HEAD hash (after optional commit/push) ---- + CUR_REV="$(git -C "$CUR_REPO_ROOT" rev-parse HEAD)" + + # ---- Determine url to write into fetchGit (respecting make-local/make-remote) ---- + 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 athenix file ---- + 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 mk_fetch_block(url, rev): + return ( + 'builtins.fetchGit {\n' + f' url = "{url}";\n' + f' rev = "{rev}";\n' + ' submodules=true;\n' + ' }' + ) + + fetch_block = mk_fetch_block(fetch_url, rev) + + def full_hostname(devtype, hostkey): + if hostkey.startswith(devtype) or "-" in hostkey: + return hostkey + if hostkey.isdigit(): + return f"{devtype}{hostkey}" + return f"{devtype}-{hostkey}" + + if mode == "user": + m = re.search(r'(?s)(athenix\.users\s*=\s*\{)(.*?)(\n\s*\};)', text) + if not m: + raise SystemExit("error: could not locate `athenix.users = { ... };` block") + + head, body, tail = m.group(1), m.group(2), m.group(3) + + key_re = re.compile(r'(?s)(\n\s*' + re.escape(username) + r'\.external\s*=\s*)builtins\.fetchGit\s*\{.*?\};') + if key_re.search(body): + body = key_re.sub(lambda mm: mm.group(1) + fetch_block + ';', body) + else: + body = body + f'\n {username}.external = {fetch_block};\n' + + new_text = text[:m.start()] + head + body + tail + text[m.end():] + path.write_text(new_text) + sys.exit(0) + + elif mode == "system": + block_re = re.compile(r'(?s)(\n\s*' + re.escape(devtype) + r'\s*=\s*\{)(.*?)(\n\s*\};)') + m = block_re.search(text) + if not m: + raise SystemExit(f"error: could not locate `{devtype} = {{ ... }};` block in inventory.nix") + + head, body, tail = m.group(1), m.group(2), m.group(3) + + candidates = [hostkey, full_hostname(devtype, hostkey)] + candidates = list(dict.fromkeys(candidates)) + + replaced = False + for k in candidates: + entry_re = re.compile(r'(?s)(\n\s*"' + re.escape(k) + r'"\s*=\s*)builtins\.fetchGit\s*\{.*?\};') + if entry_re.search(body): + body = entry_re.sub(lambda mm: mm.group(1) + fetch_block + ';', body) + replaced = True + break + + if not replaced: + body = body + f'\n "{hostkey}" = {fetch_block};\n' + + new_text = text[:m.start()] + head + body + tail + text[m.end():] + path.write_text(new_text) + sys.exit(0) + + else: + raise SystemExit("error: unknown mode") + PY + + printf "updated %s (athenix branch: %s)\n" "$FILE" "$ATHENIX_BRANCH" >&2 + printf " url = %s\n" "$FETCH_URL" >&2 + printf " rev = %s\n" "$CUR_REV" >&2 + '') + ]; +} \ No newline at end of file -- 2.39.5