set -eu
VERSION='@@VERSION@@'
ASSET_MANIFEST=$(cat <<'NGIT_ASSET_MANIFEST'
@@ASSET_MANIFEST@@
NGIT_ASSET_MANIFEST
)
RECEIPT_FILENAME='.ngit-install-receipt.json'
BINARIES='ngit git-remote-nostr'
FORCE_STANDALONE=0
INSTALL_METHOD=auto
INSTALL_DIR=''
ALLOW_DOWNGRADE=0
REPAIR=0
ngit_stage=''
ngit_lock=''
ngit_committed=0
info() {
printf '[INFO] %s\n' "$1"
}
warn() {
printf '[WARN] %s\n' "$1" >&2
}
fail() {
printf '[ERROR] %s\n' "$1" >&2
exit 1
}
command_exists() {
command -v "$1" >/dev/null 2>&1
}
is_nixos() {
if [ -e /etc/NIXOS ]; then
return 0
fi
[ -r /etc/os-release ] && grep -qE '^ID=("?nixos"?)$' /etc/os-release
}
parse_arguments() {
while [ "$#" -gt 0 ]; do
case "$1" in
--standalone) FORCE_STANDALONE=1; INSTALL_METHOD=standalone ;;
--method)
[ "$#" -ge 2 ] || fail '--method requires cargo or standalone'
shift
case "$1" in cargo|standalone) INSTALL_METHOD=$1 ;; *) fail '--method requires cargo or standalone' ;; esac
;;
--install-dir)
[ "$#" -ge 2 ] || fail '--install-dir requires an absolute directory'
shift
case "$1" in /*) INSTALL_DIR=$1 ;; *) fail '--install-dir requires an absolute directory' ;; esac
;;
--allow-downgrade) ALLOW_DOWNGRADE=1 ;;
--repair) REPAIR=1 ;;
-h|--help)
printf '%s\n' 'usage: install.sh [--method cargo|standalone] [--install-dir DIR] [--repair] [--allow-downgrade] [--standalone]'
printf '%s\n' ' --method cargo upgrade the existing Cargo installation through Cargo'
printf '%s\n' ' --method standalone install a separate standalone copy when another tool owns ngit'
printf '%s\n' ' --install-dir DIR choose the standalone directory (absolute path)'
printf '%s\n' ' --repair replace an invalid receipt in the selected directory'
printf '%s\n' ' --allow-downgrade permit replacing a newer or unrecognized version'
printf '%s\n' ' --standalone use the static musl archive on x86_64 NixOS instead of Nix'
exit 0
;;
*) fail "unknown installer option: $1" ;;
esac
shift
done
}
nixos_install_guidance() {
warn 'NixOS detected; prefer a Nix-managed installation of the latest stable release:'
printf " nix profile add 'git+https://ngit.dev/ngit.git?ref=stable'\n" >&2
warn 'For a user-owned static installation instead, rerun with:'
printf ' curl -fsSL https://ngit.dev/install.sh | bash -s -- --standalone\n' >&2
}
detect_target() {
ngit_os=$(uname -s)
ngit_arch=$(uname -m)
case "$ngit_os:$ngit_arch" in
Linux:x86_64|Linux:amd64)
if is_nixos || ldd --version 2>&1 | grep -qi musl; then
printf '%s\n' 'linux-x86_64-musl'
else
printf '%s\n' 'linux-x86_64-gnu'
fi
;;
Linux:aarch64|Linux:arm64)
is_nixos && return 1
printf '%s\n' 'linux-aarch64-gnu'
;;
Darwin:x86_64|Darwin:arm64|Darwin:aarch64) printf '%s\n' 'darwin-universal' ;;
*) return 1 ;;
esac
}
select_asset() {
ngit_target=$1
printf '%s\n' "$ASSET_MANIFEST" | awk -F '|' -v target="$ngit_target" '
$1 == target && NF == 5 { print; found += 1 }
END { if (found != 1) exit 1 }
'
}
download() {
ngit_url=$1
ngit_destination=$2
if command_exists curl; then
curl --fail --location --proto '=https' --tlsv1.2 \
--output "$ngit_destination" "$ngit_url"
elif command_exists wget; then
wget --https-only --output-document "$ngit_destination" "$ngit_url"
else
fail 'curl or wget is required'
fi
}
sha256() {
if command_exists sha256sum; then
sha256sum "$1" | awk '{ print $1 }'
elif command_exists shasum; then
shasum -a 256 "$1" | awk '{ print $1 }'
else
fail 'sha256sum or shasum is required to verify the release asset'
fi
}
canonical_path() {
ngit_path=$1
ngit_links=0
while :; do
ngit_parent=$(cd -P "$(dirname "$ngit_path")" && pwd) || return 1
ngit_path="$ngit_parent/$(basename "$ngit_path")"
[ -L "$ngit_path" ] || break
ngit_links=$((ngit_links + 1))
[ "$ngit_links" -le 40 ] || return 1
ngit_link=$(readlink "$ngit_path") || return 1
case "$ngit_link" in /*) ngit_path=$ngit_link ;; *) ngit_path="$ngit_parent/$ngit_link" ;; esac
done
printf '%s\n' "$ngit_path"
}
valid_receipt() {
[ -f "$1/$RECEIPT_FILENAME" ] && awk '
{ text = text $0 }
END {
compact = ""
quoted = 0
for (i = 1; i <= length(text); i++) {
char = substr(text, i, 1)
if (char == "\"") quoted = !quoted
if (quoted || char !~ /[[:space:]]/) compact = compact char
}
text = compact
if (text !~ /^\{.*\}$/) exit 1
text = substr(text, 2, length(text) - 2)
count = split(text, fields, ",")
for (i = 1; i <= count; i++) {
if (fields[i] == "\"schema\":1") schema++
else if (fields[i] == "\"method\":\"standalone\"") method++
else if (fields[i] ~ /^"version":"[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.+-]+)?"$/) version++
else exit 1
}
exit !(schema == 1 && method == 1 && version == 1)
}
' "$1/$RECEIPT_FILENAME"
}
inspect_existing_install() {
ngit_existing=''
ngit_existing_dir=''
ngit_cargo_root=''
if command_exists ngit; then
ngit_existing=$(canonical_path "$(command -v ngit)") || fail 'cannot resolve the existing ngit executable'
ngit_existing_dir=$(dirname "$ngit_existing")
case "$ngit_existing" in /nix/store/*) return 0 ;; esac
ngit_root=$(dirname "$ngit_existing_dir")
if [ "$(basename "$ngit_existing_dir")" = bin ] && {
[ -f "$ngit_root/.crates.toml" ] || [ -f "$ngit_root/.crates2.json" ] ||
[ "$ngit_existing_dir" = "${CARGO_HOME:-$HOME/.cargo}/bin" ]; }; then
ngit_cargo_root=$ngit_root
fi
fi
}
find_install_dir() {
if [ -n "$INSTALL_DIR" ]; then
printf '%s\n' "$INSTALL_DIR"
elif [ -n "$ngit_existing_dir" ] && { valid_receipt "$ngit_existing_dir" || { [ "$REPAIR" -eq 1 ] && [ -f "$ngit_existing_dir/$RECEIPT_FILENAME" ]; }; }; then
printf '%s\n' "$ngit_existing_dir"
else
printf '%s\n' "$HOME/.local/bin"
fi
}
check_destination() {
case "$ngit_dir/" in /nix/store/*) fail 'Nix store installations must be updated through Nix' ;; esac
if [ -e "$ngit_dir/$RECEIPT_FILENAME" ] || [ -L "$ngit_dir/$RECEIPT_FILENAME" ]; then
if ! valid_receipt "$ngit_dir"; then
[ "$REPAIR" -eq 1 ] || fail 'invalid installer receipt; use --repair --install-dir DIR to repair this standalone installation'
fi
else
for ngit_binary in $BINARIES; do
if [ -e "$ngit_dir/$ngit_binary" ] || [ -L "$ngit_dir/$ngit_binary" ]; then
fail "refusing to overwrite $ngit_dir/$ngit_binary without an installer receipt; choose a separate --install-dir"
fi
done
fi
for ngit_binary in $BINARIES "$RECEIPT_FILENAME"; do
[ ! -d "$ngit_dir/$ngit_binary" ] || fail "destination is a directory: $ngit_dir/$ngit_binary"
done
}
check_downgrade() {
[ "$ALLOW_DOWNGRADE" -eq 1 ] && return 0
[ -e "$1" ] || [ -L "$1" ] || return 0
ngit_reported=$("$1" --version 2>/dev/null) || fail 'cannot read the installed version; use --allow-downgrade to authorize replacement'
if ! printf '%s\n' "$ngit_reported" | awk -v target="$VERSION" '
/^ngit [0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.+-]+)?$/ {
split($2, old, "."); split(target, new, ".")
for (i = 1; i <= 3; i++) {
if (old[i] + 0 < new[i] + 0) { safe = 1; exit }
if (old[i] + 0 > new[i] + 0) exit
}
safe = 1
}
END { exit !safe }
'; then
fail "installed version '$ngit_reported' is newer than v$VERSION or unrecognized; use --allow-downgrade to authorize replacement"
fi
}
report_path() {
hash -r
for ngit_binary in $BINARIES; do
ngit_active=$(command -v "$ngit_binary" || true)
ngit_resolved=''
if [ -n "$ngit_active" ]; then
ngit_resolved=$(canonical_path "$ngit_active" || true)
fi
if [ "$ngit_resolved" != "$ngit_dir/$ngit_binary" ]; then
warn "$ngit_binary on PATH: ${ngit_active:-not found}; installed copy: $ngit_dir/$ngit_binary"
printf 'Run in your shell: export PATH=%q:"$PATH"; hash -r\n' "$ngit_dir" >&2
warn 'Add the same PATH setting to your shell startup file to keep using this installation.'
fi
done
}
cleanup() {
ngit_status=$?
trap - EXIT HUP INT TERM
if [ -n "$ngit_stage" ] && [ "$ngit_committed" -ne 1 ]; then
ngit_restored=1
for ngit_binary in $BINARIES "$RECEIPT_FILENAME"; do
if [ -f "$ngit_stage/replace-$ngit_binary" ]; then
if [ -e "$ngit_stage/old/$ngit_binary" ] || [ -L "$ngit_stage/old/$ngit_binary" ]; then
mv -f "$ngit_stage/old/$ngit_binary" "$ngit_dir/$ngit_binary" || ngit_restored=0
else
rm -f "$ngit_dir/$ngit_binary" || ngit_restored=0
fi
fi
done
if [ "$ngit_restored" -ne 1 ]; then
warn "Rollback incomplete; original files remain in $ngit_stage/old. Restore them before retrying."
ngit_stage=''
ngit_status=1
fi
fi
[ -z "$ngit_stage" ] || rm -rf "$ngit_stage"
[ -z "$ngit_lock" ] || rmdir "$ngit_lock"
[ -z "${ngit_temp:-}" ] || rm -rf "$ngit_temp"
exit "$ngit_status"
}
install_binaries() {
ngit_stage=$(mktemp -d "$ngit_dir/.ngit-install.XXXXXXXX")
mkdir "$ngit_stage/new" "$ngit_stage/old"
for ngit_binary in $BINARIES; do
ngit_sources=$(find "$ngit_extract" -type f -name "$ngit_binary" -print)
[ "$(printf '%s\n' "$ngit_sources" | awk 'NF { n++ } END { print n+0 }')" -eq 1 ] || fail "release archive must contain exactly one $ngit_binary"
cp "$ngit_sources" "$ngit_stage/new/$ngit_binary"
chmod 0755 "$ngit_stage/new/$ngit_binary"
done
ngit_reported=$("$ngit_stage/new/ngit" --version) || fail 'release ngit failed its version check'
[ "$ngit_reported" = "ngit $VERSION" ] || fail 'release ngit reported an unexpected version'
ngit_remote_reported=$("$ngit_stage/new/git-remote-nostr" --version) || fail 'release git-remote-nostr failed its version check'
[ "$ngit_remote_reported" = "v$VERSION" ] || fail 'release git-remote-nostr reported an unexpected version'
write_receipt "$ngit_stage/new"
for ngit_binary in $BINARIES "$RECEIPT_FILENAME"; do
if [ -e "$ngit_dir/$ngit_binary" ] || [ -L "$ngit_dir/$ngit_binary" ]; then
cp -Pp "$ngit_dir/$ngit_binary" "$ngit_stage/old/$ngit_binary"
fi
done
for ngit_binary in $BINARIES "$RECEIPT_FILENAME"; do
touch "$ngit_stage/replace-$ngit_binary"
mv -f "$ngit_stage/new/$ngit_binary" "$ngit_dir/$ngit_binary"
done
ngit_reported=$("$ngit_dir/ngit" --version) || fail 'installed ngit failed its version check'
[ "$ngit_reported" = "ngit $VERSION" ] || fail 'installed ngit reported an unexpected version'
ngit_remote_reported=$("$ngit_dir/git-remote-nostr" --version) || fail 'installed git-remote-nostr failed its version check'
[ "$ngit_remote_reported" = "v$VERSION" ] || fail 'installed git-remote-nostr reported an unexpected version'
ngit_committed=1
}
write_receipt() {
ngit_receipt_dir=$1
ngit_temporary="$ngit_receipt_dir/$RECEIPT_FILENAME.tmp.$$"
umask 022
printf '{\n "schema": 1,\n "method": "standalone",\n "version": "%s"\n}\n' \
"$VERSION" >"$ngit_temporary"
mv "$ngit_temporary" "$ngit_receipt_dir/$RECEIPT_FILENAME"
}
main() {
parse_arguments "$@"
printf '%s\n' "$VERSION" | awk '/^[0-9]+\.[0-9]+\.[0-9]+$/ { valid=1 } END { exit !valid }' || fail 'installer template was not rendered with a stable release'
inspect_existing_install
if [ "$INSTALL_METHOD" = auto ] && [ -n "$ngit_cargo_root" ] &&
[ ! -e "$ngit_existing_dir/$RECEIPT_FILENAME" ] && [ ! -L "$ngit_existing_dir/$RECEIPT_FILENAME" ]; then
INSTALL_METHOD=cargo
fi
if [ "$INSTALL_METHOD" = cargo ]; then
[ -n "$ngit_cargo_root" ] || fail 'the active ngit is not a recognized Cargo installation'
[ -z "$INSTALL_DIR" ] && [ "$REPAIR" -eq 0 ] || fail '--install-dir and --repair apply only to standalone installation'
check_downgrade "$ngit_existing"
command_exists cargo || fail 'Cargo is not available; install Cargo or rerun with --method standalone to use downloaded binaries'
info "Updating ngit to v$VERSION through Cargo in $ngit_cargo_root (default registry and default features)"
cargo install ngit --locked --version "$VERSION" --root "$ngit_cargo_root"
ngit_dir=$ngit_existing_dir
report_path
exit 0
fi
if is_nixos && [ "$FORCE_STANDALONE" -ne 1 ]; then
nixos_install_guidance
exit 1
fi
if [ "$INSTALL_METHOD" = auto ] && [ -n "$ngit_existing" ] && ! valid_receipt "$ngit_existing_dir" && [ "$REPAIR" -ne 1 ]; then
warn "Existing installation: $ngit_existing"
if [ -f "$ngit_existing_dir/$RECEIPT_FILENAME" ]; then
fail 'invalid standalone receipt; rerun with --repair to repair it'
fi
if [ -n "$ngit_cargo_root" ]; then
warn 'To keep using Cargo: rerun this installer with --method cargo'
fi
fail 'To install a standalone copy with ngit update support: rerun with --method standalone (optionally --install-dir DIR). Package-managed files will not be overwritten.'
fi
if [ -n "$ngit_existing" ]; then
check_downgrade "$ngit_existing"
fi
ngit_dir=$(find_install_dir)
mkdir -p "$ngit_dir" || fail 'cannot create the standalone directory; choose --install-dir DIR'
ngit_dir=$(cd -P "$ngit_dir" && pwd)
case "$ngit_dir/" in /nix/store/*) fail 'Nix store installations must be updated through Nix' ;; esac
mkdir "$ngit_dir/.ngit-install-lock" 2>/dev/null || fail 'another installer is running or left .ngit-install-lock; inspect it before retrying'
ngit_lock="$ngit_dir/.ngit-install-lock"
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' HUP TERM
check_destination
check_downgrade "$ngit_dir/ngit"
ngit_target=$(detect_target) || fail 'this operating system and architecture are not supported by the standalone installer'
ngit_asset=$(select_asset "$ngit_target") || fail "the release has no unique asset for $ngit_target"
IFS='|' read -r _ ngit_url ngit_expected_sha ngit_filename ngit_mime <<EOF
$ngit_asset
EOF
case "$ngit_expected_sha" in
*[!0-9a-f]*|'') fail 'the rendered asset SHA-256 is invalid' ;;
esac
[ "${#ngit_expected_sha}" -eq 64 ] || fail 'the rendered asset SHA-256 is invalid'
ngit_temp=$(mktemp -d)
ngit_archive="$ngit_temp/$ngit_filename"
info "Downloading ngit v$VERSION for $ngit_target"
download "$ngit_url" "$ngit_archive"
ngit_actual_sha=$(sha256 "$ngit_archive")
[ "$ngit_actual_sha" = "$ngit_expected_sha" ] || fail "SHA-256 mismatch: expected $ngit_expected_sha, observed $ngit_actual_sha"
ngit_extract="$ngit_temp/extracted"
mkdir "$ngit_extract"
case "$ngit_filename:$ngit_mime" in
*.tar.gz:*|*.tgz:*|*:application/gzip) tar -xzf "$ngit_archive" -C "$ngit_extract" ;;
*.zip:*|*:application/zip) unzip -q "$ngit_archive" -d "$ngit_extract" ;;
*) fail "unsupported release archive $ngit_filename ($ngit_mime)" ;;
esac
info "Installing ngit and git-remote-nostr to $ngit_dir"
install_binaries
info "Installed ngit v$VERSION to $ngit_dir"
report_path
}
main "$@"