Repository navigation
Expand file tree
/
Copy pathinstall.sh
More file actions
executable file
·305 lines (282 loc) · 14 KB
/
Copy pathinstall.sh
File metadata and controls
executable file
·305 lines (282 loc) · 14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
#!/bin/sh
# Install a Hanzo native binary.
#
# curl -fsSL https://raw.githubusercontent.com/hanzoai/cli/main/install.sh | sh
#
# Downloads the prebuilt release asset for THIS machine, verifies its sha256, and
# puts it on PATH. Nothing is built, and no package manager is involved. It
# refuses loudly on a platform we do not publish, because a script that
# half-works is worse than one that says why it cannot.
#
# This is the ONE implementation of "fetch a Hanzo binary". It defaults to the
# `hanzo` CLI, and hanzo.sh drives it once per tool by overriding three
# variables rather than carrying a second copy of platform detection, asset
# naming and checksum verification:
#
# HANZO_INSTALL_REPO owning repo (default hanzoai/cli)
# HANZO_INSTALL_BIN binary + asset prefix (default hanzo)
# HANZO_INSTALL_ALIAS second name, same build (default hanzo-cli; "" = none)
# HANZO_INSTALL_PREFIX install dir (default ~/.local/bin)
# HANZO_VERSION pin a tag (default: highest stable semver)
# HANZO_INSTALL_SIGNER the workflow that must have built the asset, for a tool
# whose releases are attested (e.g. .github/workflows/release.yml);
# such a tool is installed from vX.Y.Z tags only
#
# The convention every published Hanzo binary follows, and the only thing this
# needs to know: the asset is <BIN>-<os>-<arch>.tar.gz, it is accompanied by
# <asset>.sha256, and it unpacks to a single file named <BIN>.
set -eu
REPO="${HANZO_INSTALL_REPO:-hanzoai/cli}"
BIN="${HANZO_INSTALL_BIN:-hanzo}"
# The second name this same build installs under: what cloud's control binary
# delegates to. One build, two names — never two versions.
DELEGATE="${HANZO_INSTALL_ALIAS-hanzo-cli}"
PREFIX="${HANZO_INSTALL_PREFIX:-$HOME/.local/bin}"
SIGNER="${HANZO_INSTALL_SIGNER:-}"
STRICT="${HANZO_INSTALL_STRICT:-0}"
die() { printf '\n%s: %s\n' "$BIN" "$1" >&2; exit 1; }
need() { command -v "$1" >/dev/null 2>&1 || die "need $1 on PATH"; }
need curl
need tar
need awk
case "$STRICT" in 0|1) ;; *) die "HANZO_INSTALL_STRICT must be 0 or 1" ;; esac
# Check the verifier before downloading. Strict mode requires repository- and
# tag-bound provenance for every tool, including those without a pinned workflow.
unchecked=""
if [ -n "$SIGNER" ] || [ "$STRICT" = 1 ]; then
if ! command -v gh >/dev/null 2>&1; then
unchecked="needs gh"
elif ! gh auth status >/dev/null 2>&1; then
unchecked="needs a signed-in gh"
elif ! gh attestation verify --help 2>/dev/null | grep -q -- '--source-ref'; then
unchecked="$(gh --version 2>/dev/null | head -1 || true) cannot check an attestation's tag"
fi
if [ "$STRICT" = 1 ] && [ -n "$unchecked" ]; then
die "strict verification unavailable ($unchecked). Install or upgrade GitHub CLI: https://cli.github.com/ and run gh auth login. Nothing was installed."
fi
fi
# A private repo answers 404 to an anonymous asset fetch, so carry a token when
# one is available. Public installs need none.
TOKEN="${HANZO_INSTALL_TOKEN:-${GH_TOKEN:-${GITHUB_TOKEN:-}}}"
if [ -z "$TOKEN" ] && command -v gh >/dev/null 2>&1; then
TOKEN="$(gh auth token 2>/dev/null || true)"
fi
# Never interpolate the token into an argument list: unquoted substitution
# word-splits it into its own argv entry, and curl echoes argv on failure — which
# prints the token. Branch, and keep the header a single quoted argument.
# Whether to draw curl's own progress bar, decided once.
#
# A binary is tens of megabytes and `-s` draws nothing while it comes down, so
# a slow connection is indistinguishable from a hang — which is how somebody
# ends up pressing ctrl-c halfway through an install that was working.
#
# The bar goes to stderr. `curl hanzo.sh | sh` consumes stdout and leaves
# stderr on the terminal, so it is visible in exactly the case this is for.
# Where there is no terminal — CI, a log file, a pipe to tee — a bar is a few
# thousand carriage returns in the transcript, so it stays silent there and
# `-S` keeps errors reported either way.
if [ -t 2 ]; then DL='-#'; else DL='-sS'; fi
get() { # get <url> <dest>
if [ -n "$TOKEN" ]; then
curl -fL $DL -H "Authorization: Bearer $TOKEN" "$1" -o "$2"
else
curl -fL $DL "$1" -o "$2"
fi
}
get_stdout() { # get_stdout <url>
if [ -n "$TOKEN" ]; then
curl -fsSL -H "Authorization: Bearer $TOKEN" "$1"
else
curl -fsSL "$1"
fi
}
# Published targets: linux-{amd64,arm64}, darwin-{amd64,arm64}, windows-amd64.
os="$(uname -s)"
arch="$(uname -m)"
ext=""
case "$os" in
Linux) os=linux ;;
Darwin) os=darwin ;;
MINGW*|MSYS*|CYGWIN*|Windows_NT) os=windows; ext=".exe" ;; # git-bash / msys2
*) die "unsupported OS '$os'." ;;
esac
case "$arch" in
x86_64|amd64) arch=amd64 ;;
aarch64|arm64) arch=arm64 ;;
*) die "unsupported architecture '$arch'." ;;
esac
target="${os}-${arch}"
TAG="${HANZO_VERSION:-}"
# An attested tool ships from `vX.Y.Z` tags only: the form its release workflow
# runs on and its tag ruleset guards. Any other tag in its repository is one no
# release of it made, whatever assets it carries.
form='(rust-)?v'
[ -z "$SIGNER" ] || form='v'
if [ -n "$TAG" ] && [ -n "$SIGNER" ]; then
printf '%s' "$TAG" | grep -Eq '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$' \
|| die "$TAG is not a release tag of $REPO (vX.Y.Z)"
fi
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
asset="${BIN}-${target}.tar.gz"
# The tags come from git's ref advertisement, not the REST API. github.com serves
# it without the API's limit of 60 anonymous calls an hour per address, and
# listing releases through the API cost one call per 100 releases per tool: 8 of
# the 60 for one `curl hanzo.sh | sh`. Behind a shared NAT or on a CI runner the
# limit was spent before the installer ran, and nothing installed.
#
# /latest is not used either: it follows release chronology, which can select an
# older maintenance release over a higher version.
tags() {
url="https://github.com/$REPO.git/info/refs?service=git-upload-pack"
if [ -n "$TOKEN" ]; then
curl -fsS -H "Authorization: Basic $(printf 'x-access-token:%s' "$TOKEN" | base64 | tr -d '\n')" "$url"
else
curl -fsS "$url"
fi
}
# has <tag>: does the release at <tag> carry THIS machine's archive and its
# checksum? A release is not one object per product: MCP also publishes
# independent JavaScript versions in the same repository, and a release whose
# build for one target failed still went out with the others (hanzoai/cli
# v8.5.158 has no linux-amd64, hanzoai/mcp rust-v1.1.23 no darwin). Choosing it
# would end the install on a 404 when the release before it has this machine's
# build. A draft's assets answer 404 until it is published, so a draft is never
# chosen either.
has() {
if [ -z "$TOKEN" ]; then
curl -fs -I -o /dev/null "https://github.com/$REPO/releases/download/$1/$asset.sha256" &&
curl -fs -I -o /dev/null "https://github.com/$REPO/releases/download/$1/$asset"
return
fi
# A private release's download URL answers 404 even with a token; the API
# names its assets.
get_stdout "https://api.github.com/repos/$REPO/releases/tags/$1" > "$tmp/release.json" 2>/dev/null &&
grep -qF "\"$asset\"" "$tmp/release.json" &&
grep -qF "\"$asset.sha256\"" "$tmp/release.json"
}
if [ -z "$TAG" ]; then
tags > "$tmp/refs" || die "could not list the tags of $REPO"
# One ref per pkt-line, "<len><sha> refs/tags/<tag>", with NULs around the
# capability list; a peeled "<tag>^{}" line fails the pattern.
tr '\000' '\n' < "$tmp/refs" | awk -v form="$form" '
$2 ~ ("^refs/tags/" form "(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$") {
tag = substr($2, 11); version = tag; sub("^" form, "", version)
print version "\t" tag
}' | sort -t . -k1,1nr -k2,2nr -k3,3nr | awk '{ print $2 }' > "$tmp/tags"
while read -r candidate; do
if has "$candidate"; then TAG="$candidate"; break; fi
done < "$tmp/tags"
[ -n "$TAG" ] || die "no stable release of $REPO has a build for $target.
If $REPO is private, set GH_TOKEN (or run \`gh auth login\`); or pin HANZO_VERSION=vX.Y.Z."
fi
base="https://github.com/$REPO/releases/download/$TAG"
# A private release's browser download URL is not fetchable with a token; assets
# must come from the API by id, with an octet-stream Accept.
fetch() { # fetch <asset-name> <dest>
if [ -z "$TOKEN" ]; then
get "$base/$1" "$2"
return
fi
# A private release's browser URL is not token-fetchable; assets come from the
# API by id, with an octet-stream Accept.
# Pull the asset id out of the release JSON. GitHub pretty-prints, so collapse
# the newlines FIRST — otherwise each asset object spans many lines and the
# "id" never lands on the same record as the "name" we matched.
id="$(get_stdout "https://api.github.com/repos/$REPO/releases/tags/$TAG" \
| tr -d '\n' | tr '{' '\n' \
| grep -F "\"$1\"" \
| sed -n 's/.*"id": *\([0-9][0-9]*\).*/\1/p' | head -1)"
[ -n "$id" ] || return 1
curl -fL $DL -H "Authorization: Bearer $TOKEN" -H "Accept: application/octet-stream" \
"https://api.github.com/repos/$REPO/releases/assets/$id" -o "$2"
}
printf '%s: %s %s\n' "$BIN" "$TAG" "$target"
fetch "$asset" "$tmp/$asset" \
|| die "no published build for $target at $TAG."
fetch "$asset.sha256" "$tmp/$asset.sha256" \
|| die "release $TAG has no checksum for $target — refusing to install unverified"
# Verify BEFORE unpacking: an unverified binary is not installed, ever.
( cd "$tmp" && \
if command -v sha256sum >/dev/null 2>&1; then sha256sum -c "$asset.sha256";
elif command -v shasum >/dev/null 2>&1; then shasum -a 256 -c "$asset.sha256";
else die "need sha256sum or shasum to verify the download"; fi ) >/dev/null \
|| die "checksum MISMATCH for $asset — refusing to install"
# PROVENANCE, for a tool whose releases are attested. The checksum above came
# from the same release as the archive, so it proves the bytes arrived whole and
# nothing about where they came from. The attestation does: Sigstore's record
# that THIS repository's $SIGNER, run for THIS tag, built exactly these bytes.
# gh carries the verification, so it is checked when a signed-in gh that can
# verify an attestation's tag is here, and a verification that ran and failed
# refuses the install, with gh's reason. Otherwise it is not checked, and the
# line says why and how to check it — never a silent pass.
if [ -n "$SIGNER" ] || [ "$STRICT" = 1 ]; then
if [ -z "$unchecked" ]; then
set -- gh attestation verify "$tmp/$asset" --repo "$REPO" --source-ref "refs/tags/$TAG"
if [ -n "$SIGNER" ]; then set -- "$@" --signer-workflow "$REPO/$SIGNER"; fi
"$@" > "$tmp/provenance" 2>&1 \
|| { sed 's/^/ /' "$tmp/provenance" >&2
die "$asset has no matching provenance for $REPO at $TAG — refusing to install"; }
if [ -n "$SIGNER" ]; then
printf '%s: provenance: %s/%s built %s at %s\n' "$BIN" "$REPO" "$SIGNER" "$asset" "$TAG"
else
printf '%s: provenance: verified %s from %s at %s\n' "$BIN" "$asset" "$REPO" "$TAG"
fi
else
printf '%s: provenance not checked (%s): gh attestation verify %s --repo %s --signer-workflow %s/%s --source-ref refs/tags/%s\n' \
"$BIN" "$unchecked" "$asset" "$REPO" "$REPO" "$SIGNER" "$TAG"
printf '%s: provenance: upgrade GitHub CLI at https://cli.github.com/, run gh auth login, then rerun this installer.\n' "$BIN"
fi
fi
tar -xzf "$tmp/$asset" -C "$tmp"
[ -f "$tmp/$BIN$ext" ] || die "archive did not contain '$BIN$ext'"
mkdir -p "$PREFIX"
mv "$tmp/$BIN$ext" "$PREFIX/$BIN$ext"
chmod 755 "$PREFIX/$BIN$ext"
printf '%s: installed %s\n' "$BIN" "$PREFIX/$BIN$ext"
# The SAME build under a second name. cloud's control binary hands every verb it
# does not own to `hanzo-cli` (it resolves that name before `hanzo`), so the two
# names must never be two versions: install one and not the other, or upgrade one
# and not the other, and a user types `hanzo`, gets delegated, and runs an old
# build with no version anywhere on screen. That is not theoretical — a stale
# twin served ~150 commands that no longer existed, silently.
#
# A symlink makes the skew impossible rather than merely unlikely; where links
# are not available (Windows) a copy still matches by version. A caller that
# wants no second name passes HANZO_INSTALL_ALIAS=''.
if [ -n "$DELEGATE" ]; then
if ln -sf "$BIN$ext" "$PREFIX/$DELEGATE$ext" 2>/dev/null; then
:
else
cp -f "$PREFIX/$BIN$ext" "$PREFIX/$DELEGATE$ext" && chmod 755 "$PREFIX/$DELEGATE$ext"
fi
[ -e "$PREFIX/$DELEGATE$ext" ] \
|| die "could not install the second name $PREFIX/$DELEGATE$ext — a half-install is not an install"
printf '%s: installed %s (the same build, second name)\n' "$BIN" "$PREFIX/$DELEGATE$ext"
fi
# Retire only the symlink this installer used to create for the CLI. A real
# network-node executable (or a link to hanzod) is never removed.
if [ "$BIN" = hanzo ] && [ "$DELEGATE" = hanzo-cli ] && [ -L "$PREFIX/hanzo-node$ext" ]; then
old_target=$(readlink "$PREFIX/hanzo-node$ext" || true)
case "$old_target" in
"hanzo$ext"|"$PREFIX/hanzo$ext") rm "$PREFIX/hanzo-node$ext" ;;
esac
fi
# Whatever PATH says, name the copy that would ACTUALLY run. An earlier entry
# wins, and that precedence is exactly how a stale install hides: the user
# upgrades here and keeps running something else, and re-running the installer
# never fixes it because it keeps writing to a directory that never wins.
found="$(command -v "$BIN" 2>/dev/null || true)"
if [ -n "$found" ] && [ "$found" != "$PREFIX/$BIN$ext" ]; then
printf '%s: WARNING %s comes first on PATH and will run instead of the\n' "$BIN" "$found"
printf ' build just installed at %s. Remove it, or put %s first.\n' "$PREFIX/$BIN$ext" "$PREFIX"
fi
case ":$PATH:" in
*":$PREFIX:"*) ;;
*) printf '%s: %s is not on PATH — add it:\n export PATH="%s:$PATH"\n' "$BIN" "$PREFIX" "$PREFIX" ;;
esac
# `hanzo auth login`, not `hanzo login`: the top-level alias arrived in 8.5.157,
# and a build before it reads an unrecognised first word as a task for the coding
# agent, so it would start a session about the word "login" instead of signing
# anyone in. HANZO_VERSION can pin such a build.
if [ "$BIN" = hanzo ]; then printf 'hanzo: next → hanzo auth login\n'; fi