-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathinstall.sh
More file actions
executable file
·251 lines (233 loc) · 9.75 KB
/
Copy pathinstall.sh
File metadata and controls
executable file
·251 lines (233 loc) · 9.75 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
#!/usr/bin/env bash
set -euo pipefail
# Stable ad-hoc identity for local macOS installs. TCC keys removable-volume
# and file grants on the designated requirement. The default ad-hoc
# requirement is `cdhash H"..."`, that binary's exact hash, so an Allow
# does not survive the next rebuild even when the identifier stays
# `dev.tracedecay.cli`. The linker default identifier is the hashed deps
# filename (`tracedecay-<hash>`). Release strip runs after the link and
# writes that filename back into the ad-hoc signature. This runs on the
# final file, after that strip, and embeds
# `designated => identifier "dev.tracedecay.cli"`. A Developer ID or other
# team signature is preserved.
TRACEDECAY_MACOS_CODE_SIGN_IDENTIFIER=dev.tracedecay.cli
# Re-sign `target` when it is an unsigned or ad-hoc Mach-O whose designated
# requirement is not the stable identifier. No-op off Darwin and for
# non-Mach-O files (the Linux installer tests install a shell fixture).
stabilize_macos_adhoc_identity() {
local target=$1
if [[ ${TRACEDECAY_ASSUME_DARWIN:-} != 1 && "$(uname -s)" != Darwin ]]; then
return 0
fi
[[ -f $target ]] || {
printf 'tracedecay installer: cannot sign missing binary %s\n' "$target" >&2
return 1
}
local magic
magic=$(od -An -t x1 -N 4 "$target" | tr -d ' \n')
case $magic in
feedfacf | cffaedfe | feedface | cefaedfe | cafebabe | bebafeca | cafebabf | bfbafeca) ;;
*) return 0 ;;
esac
local report ident team designated
# `-r-` prints `designated => ...`. `-dv` does not, so a cdhash requirement
# is invisible there.
report=$(codesign -d --verbose=2 -r- "$target" 2>&1 || true)
if printf '%s\n' "$report" | grep -q '^Authority='; then
return 0
fi
team=$(printf '%s\n' "$report" | sed -n 's/^TeamIdentifier=//p' | head -n 1)
if [[ -n $team && $team != "not set" ]]; then
return 0
fi
ident=$(printf '%s\n' "$report" | sed -n 's/^Identifier=//p' | head -n 1)
designated=$(printf '%s\n' "$report" | sed -n 's/^designated => //p' | head -n 1)
if [[ $ident == "$TRACEDECAY_MACOS_CODE_SIGN_IDENTIFIER" \
&& $designated == "identifier \"${TRACEDECAY_MACOS_CODE_SIGN_IDENTIFIER}\"" ]]; then
return 0
fi
# `-r=designated => ...` is one argument. The `=` is both how the short
# option takes its value and the marker that the value is requirement
# text rather than a path.
codesign --force --sign - \
--identifier "$TRACEDECAY_MACOS_CODE_SIGN_IDENTIFIER" \
-r="designated => identifier \"${TRACEDECAY_MACOS_CODE_SIGN_IDENTIFIER}\"" \
"$target"
}
# The macOS link wrapper sources this file to reuse the function above.
if [[ ${TRACEDECAY_INSTALL_LIBRARY:-} == 1 && ${BASH_SOURCE[0]} != "$0" ]]; then
return 0
fi
repository=${TRACEDECAY_REPOSITORY:-ScriptedAlchemy/tracedecay}
install_dir=${TRACEDECAY_INSTALL_DIR:-${XDG_BIN_HOME:-${HOME}/.local/bin}}
requested_version=${TRACEDECAY_VERSION:-latest}
release_root="https://github.com/${repository}/releases"
fail() {
printf 'tracedecay installer: %s\n' "$*" >&2
exit 1
}
command -v curl >/dev/null 2>&1 || fail "curl is required"
command -v install >/dev/null 2>&1 || fail "install is required"
command -v tar >/dev/null 2>&1 || fail "tar is required"
case "$(uname -s)/$(uname -m)" in
Linux/x86_64 | Linux/amd64)
platform=x86_64-linux
;;
Linux/aarch64 | Linux/arm64)
platform=aarch64-linux
;;
Darwin/arm64 | Darwin/aarch64)
platform=aarch64-macos
;;
*)
fail "unsupported platform: $(uname -s) $(uname -m)"
;;
esac
# release-beta.yml names prerelease archives `tracedecay-beta-<tag>-...`;
# release.yml names stable ones `tracedecay-<tag>-...`.
asset_name_for_tag() {
local candidate=$1
if [[ $candidate == *-beta.* ]]; then
printf 'tracedecay-beta-%s-%s.tar.gz' "$candidate" "$platform"
else
printf 'tracedecay-%s-%s.tar.gz' "$candidate" "$platform"
fi
}
# True when the releases API payload already lists both the platform archive
# and SHA256SUMS for this tag. Release Please can publish a non-draft
# prerelease before release-beta.yml uploads those assets; matching on
# browser_download_url paths skips that half-published window. The payload is
# far larger than a pipe buffer, so piping it into `grep -q` lets the writer
# die of SIGPIPE and `pipefail` turns a match into 141; grep a here-string.
release_has_install_assets() {
local json=$1
local candidate=$2
local candidate_asset=$3
grep -Fq -- "/download/${candidate}/${candidate_asset}" <<<"$json" &&
grep -Fq -- "/download/${candidate}/SHA256SUMS" <<<"$json"
}
# `latest` is the newest published release including prereleases, because the
# 0.1.0 beta line is where tracedecay ships. GitHub's `releases/latest`
# redirect never resolves to a prerelease, so it pinned installs to the last
# stable tag (v0.0.74, 2026-08-19) and the whole beta line was unreachable.
# Walk recent releases and skip any that lack the platform archive + checksum
# (common while a beta build is still uploading). `TRACEDECAY_VERSION=stable`
# opts back into the releases/latest redirect.
case $requested_version in
latest)
tag=
releases_json=$(
curl -fsSL "https://api.github.com/repos/${repository}/releases?per_page=30"
)
while IFS= read -r candidate; do
[[ -n $candidate ]] || continue
candidate_asset=$(asset_name_for_tag "$candidate")
if release_has_install_assets "$releases_json" "$candidate" "$candidate_asset"; then
tag=$candidate
break
fi
done < <(
printf '%s' "$releases_json" |
grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' |
cut -d'"' -f4
)
[[ -n $tag ]] ||
fail "no published release has install assets for ${platform}"
;;
stable)
resolved_url=$(curl -fsSL -o /dev/null -w '%{url_effective}' "${release_root}/latest")
tag=${resolved_url##*/}
;;
*)
tag="v${requested_version#v}"
;;
esac
[[ $tag == v* ]] || fail "GitHub did not return a valid release tag"
asset=$(asset_name_for_tag "$tag")
asset_root="${release_root}/download/${tag}"
tmp_dir=$(mktemp -d)
trap 'rm -rf "$tmp_dir"' EXIT
curl -fsSL "${asset_root}/${asset}" -o "${tmp_dir}/${asset}"
curl -fsSL "${asset_root}/SHA256SUMS" -o "${tmp_dir}/SHA256SUMS"
if ! expected=$(
awk -v asset="$asset" '
$2 == asset || $2 == "*" asset {
matches += 1
digest = $1
fields = NF
}
END {
if (matches != 1 || fields != 2) {
exit 1
}
print digest
}
' "${tmp_dir}/SHA256SUMS"
); then
fail "SHA256SUMS must contain exactly one entry for ${asset}"
fi
[[ $expected =~ ^[[:xdigit:]]{64}$ ]] ||
fail "SHA256SUMS has an invalid digest for ${asset}"
expected=$(printf '%s' "$expected" | tr '[:upper:]' '[:lower:]')
if command -v sha256sum >/dev/null 2>&1; then
actual=$(sha256sum "${tmp_dir}/${asset}" | awk '{print $1}')
elif command -v shasum >/dev/null 2>&1; then
actual=$(shasum -a 256 "${tmp_dir}/${asset}" | awk '{print $1}')
else
fail "sha256sum or shasum is required"
fi
[[ $actual == "$expected" ]] || fail "checksum mismatch for ${asset}"
# SHA256SUMS is uploaded to the same release as the archive, so it proves only
# that the download is intact: whoever can replace one asset can replace both.
# The release workflow's build-provenance attestation proves who built the
# archive, and gh verifies that Sigstore bundle. The accepted identities are
# the ones `tracedecay update` accepts: the channel's release workflow,
# dispatched from master or run on the release tag.
if [[ $tag == *-beta.* ]]; then
signer_workflow="https://github.com/${repository}/.github/workflows/release-beta.yml"
else
signer_workflow="https://github.com/${repository}/.github/workflows/release.yml"
fi
verify_build_provenance() {
local identity
for identity in "${signer_workflow}@refs/heads/master" "${signer_workflow}@refs/tags/${tag}"; do
if gh attestation verify "${tmp_dir}/${asset}" \
--repo "$repository" \
--cert-identity "$identity" \
--cert-oidc-issuer https://token.actions.githubusercontent.com \
>/dev/null 2>>"${tmp_dir}/attestation.log"; then
printf 'Verified build provenance: %s\n' "$identity"
return 0
fi
done
return 1
}
if command -v gh >/dev/null 2>&1; then
verify_build_provenance ||
fail "the build-provenance attestation of ${asset} did not verify as ${signer_workflow}:
$(cat "${tmp_dir}/attestation.log")"
elif [[ ${TRACEDECAY_INSTALL_UNATTESTED:-} == 1 ]]; then
printf 'tracedecay installer: WARNING: gh is not installed, so the build-provenance attestation of %s was NOT verified; only the same-release SHA256SUMS was checked (TRACEDECAY_INSTALL_UNATTESTED=1)\n' \
"$asset" >&2
else
fail "gh (GitHub CLI) is required to verify the build-provenance attestation of ${asset}. Install gh and run \`gh auth login\`, then rerun the installer; or set TRACEDECAY_INSTALL_UNATTESTED=1 to install with only the same-release SHA256SUMS check"
fi
tar -xzf "${tmp_dir}/${asset}" -C "$tmp_dir"
[[ -f ${tmp_dir}/tracedecay ]] || fail "archive does not contain tracedecay"
mkdir -p "$install_dir"
# Agent hooks and MCP hosts exec the installed path at any moment. `install`
# onto it unlinks the old binary and writes the new one in place at mode 0600,
# so a concurrent exec fails or runs a truncated file. Stage a sibling and
# rename it over the path so every exec sees one complete binary.
staged=$(mktemp "${install_dir}/.tracedecay.install.XXXXXX")
trap 'rm -rf "$tmp_dir"; rm -f "$staged"' EXIT
install -m 0755 "${tmp_dir}/tracedecay" "$staged"
# After the archive checksum check. Re-signing changes the installed bytes
# only; the published digest still matches the archive.
stabilize_macos_adhoc_identity "$staged"
mv -f "$staged" "${install_dir}/tracedecay"
printf 'Installed tracedecay %s to %s\n' "${tag#v}" "${install_dir}/tracedecay"
case ":${PATH}:" in
*":${install_dir}:"*) ;;
*) printf 'Add %s to PATH to run tracedecay.\n' "$install_dir" ;;
esac