Repository navigation
Expand file tree
/
Copy pathinstall.sh
More file actions
executable file
·301 lines (279 loc) · 12.7 KB
/
Copy pathinstall.sh
File metadata and controls
executable file
·301 lines (279 loc) · 12.7 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
#!/bin/sh
# Dexaflow installer: downloads the release archive for this OS/arch, verifies
# the cosign signature of checksums.txt (when cosign is installed) and the
# archive's checksum, installs the binaries (plus leoflow* links to them, the names
# before the rename) and runs `dexaflow setup` to bootstrap the managed runtime
# (Python, workspace).
#
# curl -fsSL https://raw.githubusercontent.com/dexadata/dexaflow/main/install.sh | sh
#
# Environment overrides (the LEOFLOW_* names are still accepted):
# DEXAFLOW_VERSION=vX.Y.Z pin a specific release (default: latest)
# DEXAFLOW_NO_SETUP=1 install binaries only, skip `dexaflow setup`
# DEXAFLOW_INSTALL_DIR=~/.dexaflow/bin
# DEXAFLOW_INSTALL_REQUIRE_SIGNATURE=1 fail when cosign is not installed
# DEXAFLOW_INSTALL_SKIP_SIGNATURE=1 do not verify the signature
set -eu
REPO="dexadata/dexaflow"
# Choose where to put the binaries. Prefer a directory ALREADY on PATH so the
# user needs no `source`/new shell — the common "command not found" trap. Order:
# an explicit override; /usr/local/bin when writable (root); ~/.local/bin when
# it is already on PATH; otherwise the managed ~/.dexaflow/bin (we then edit the
# profile). ON_PATH=1 means no profile edit is needed.
resolve_install_dir() {
if [ -n "${DEXAFLOW_INSTALL_DIR:-${LEOFLOW_INSTALL_DIR:-}}" ]; then
printf '%s' "${DEXAFLOW_INSTALL_DIR:-$LEOFLOW_INSTALL_DIR}"
return
fi
if [ -d /usr/local/bin ] && [ -w /usr/local/bin ]; then
printf '/usr/local/bin'
return
fi
case ":${PATH}:" in
*":${HOME}/.local/bin:"*) printf '%s' "${HOME}/.local/bin"; return ;;
esac
printf '%s' "${HOME}/.dexaflow/bin"
}
# An install from before the rename keeps its state, and possibly its binaries,
# in ~/.leoflow. Make ~/.dexaflow a link to it (as the binaries themselves do,
# config.HomeDirIn) so an upgrade lands in the same directory that is already on
# PATH, instead of a new copy shadowed by the old one.
if [ -d "${HOME}/.leoflow" ] && [ ! -e "${HOME}/.dexaflow" ] && [ ! -L "${HOME}/.dexaflow" ]; then
ln -s .leoflow "${HOME}/.dexaflow" 2>/dev/null || true
fi
INSTALL_DIR="$(resolve_install_dir)"
info() { printf '\033[36m==>\033[0m %s\n' "$1"; }
err() { printf '\033[31merror:\033[0m %s\n' "$1" >&2; exit 1; }
have() { command -v "$1" >/dev/null 2>&1; }
# ── Detect platform (Go binaries are static, so libc does not matter here) ──
os=$(uname -s)
case "$os" in
Linux) os=linux ;;
Darwin) os=darwin ;;
*) err "unsupported OS '$os' (Dexaflow ships linux and darwin; on Windows use WSL2)" ;;
esac
arch=$(uname -m)
case "$arch" in
x86_64 | amd64) arch=amd64 ;;
aarch64 | arm64) arch=arm64 ;;
*) err "unsupported architecture '$arch'" ;;
esac
# ── Pick a downloader ──
if have curl; then
dl() { curl -fsSL "$1" -o "$2"; }
fetch() { curl -fsSL "$1"; }
elif have wget; then
dl() { wget -qO "$2" "$1"; }
fetch() { wget -qO - "$1"; }
else
err "need curl or wget to download Dexaflow"
fi
# tar extracts the release archive (tar -xzf below). Minimal images (e.g. openSUSE
# Leap) ship without it; check up front so we fail with a clear, actionable
# message instead of a raw "tar: command not found" mid-install.
have tar || err "need tar to extract the release archive (install it, e.g. 'apk add tar', 'zypper in tar', 'dnf install tar')"
# ── Signature policy (#1563) ──
# checksums.txt comes from the same release as the archive, so the checksum
# alone catches a broken download, not a tampered release. The release workflow
# signs checksums.txt with cosign (keyless), and with cosign installed this
# script verifies that signature before trusting a single checksum in it, and
# aborts on any failure. Without cosign it says so in one line and goes on, as
# it always has: making cosign mandatory would break every existing
# `curl | sh` on a machine without it. REQUIRE makes a missing cosign fatal;
# SKIP is for a machine with cosign that cannot reach Sigstore.
require_sig="${DEXAFLOW_INSTALL_REQUIRE_SIGNATURE:-${LEOFLOW_INSTALL_REQUIRE_SIGNATURE:-}}"
skip_sig="${DEXAFLOW_INSTALL_SKIP_SIGNATURE:-${LEOFLOW_INSTALL_SKIP_SIGNATURE:-}}"
if [ "$require_sig" = 1 ] && [ "$skip_sig" = 1 ]; then
err "DEXAFLOW_INSTALL_REQUIRE_SIGNATURE=1 and DEXAFLOW_INSTALL_SKIP_SIGNATURE=1 contradict each other; set one"
fi
sig_docs="https://dexaflow.dexadata.ai/get-started/installation/#verifying-the-download"
# cosign 1.x has no --certificate-identity, so it cannot pin the signer.
cosign_usable() {
have cosign || return 1
case "$(cosign version 2>/dev/null | awk '/GitVersion/ {print $2}')" in
v1.* | 1.* | v0.* | 0.*) return 1 ;;
esac
return 0
}
# The release workflow that signed a tag: dexadata/dexaflow from v0.5.0 on,
# neochaotic/leoflow for the releases cut before the repository moved. The
# identity is exact, workflow file and tag included, so a signature made by any
# other workflow, branch or tag of either repository does not verify.
signer_identity() { # <tag>
_sv="${1#v}"
_maj="${_sv%%.*}"
_rest="${_sv#*.}"
_min="${_rest%%.*}"
_srepo="dexadata/dexaflow"
case "${_maj}${_min}" in
'' | *[!0-9]*) ;;
*) [ "$_maj" -eq 0 ] && [ "$_min" -lt 5 ] && _srepo="neochaotic/leoflow" ;;
esac
printf 'https://github.com/%s/.github/workflows/release.yaml@refs/tags/%s' "$_srepo" "$1"
}
verify_checksums_signature() { # <dir holding checksums.txt> <base url> <tag>
if [ "$skip_sig" = 1 ]; then
info "signature of checksums.txt was not checked (DEXAFLOW_INSTALL_SKIP_SIGNATURE=1)"
return
fi
if ! cosign_usable; then
[ "$require_sig" = 1 ] && err "DEXAFLOW_INSTALL_REQUIRE_SIGNATURE=1, but cosign v2 or later is not on PATH (install it: https://docs.sigstore.dev/cosign/system_config/installation/)"
info "signature of checksums.txt was not checked: cosign is not installed (to check it, see ${sig_docs})"
return
fi
dl "${2}/checksums.txt.sig" "${1}/checksums.txt.sig" || err "downloading checksums.txt.sig failed; not installing an unsigned release"
dl "${2}/checksums.txt.pem" "${1}/checksums.txt.pem" || err "downloading checksums.txt.pem failed; not installing an unsigned release"
identity="$(signer_identity "$3")"
info "verifying the signature of checksums.txt (cosign)..."
if ! cosign verify-blob \
--certificate "${1}/checksums.txt.pem" \
--signature "${1}/checksums.txt.sig" \
--certificate-identity "$identity" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
"${1}/checksums.txt" >"${1}/cosign.log" 2>&1; then
cat "${1}/cosign.log" >&2
err "signature verification of checksums.txt failed (cosign output above); nothing was installed. Expected signer: ${identity}. If cosign could not reach Sigstore, see ${sig_docs}"
fi
info "signature verified (${identity})"
}
# ── Resolve version ──
# Pin any version (incl. a pre-release once stable exists): LEOFLOW_VERSION=...
version="${DEXAFLOW_VERSION:-${LEOFLOW_VERSION:-}}"
if [ -z "$version" ]; then
info "resolving latest release..."
# Prefer the latest STABLE release: /releases/latest excludes pre-releases AND
# drafts, so once a stable ships, users get it by default (the right behavior).
version=$(fetch "https://api.github.com/repos/${REPO}/releases/latest" 2>/dev/null \
| grep '"tag_name"' | head -1 | sed 's/.*: *"//; s/".*//')
# No stable yet (the pre-alpha phase): fall back to the highest published
# pre-release. Don't trust the list order (a retracted draft reorders it) —
# take the highest tag with sort -V over the non-draft list.
if [ -z "$version" ]; then
version=$(fetch "https://api.github.com/repos/${REPO}/releases?per_page=50" \
| grep '"tag_name"' | sed 's/.*: *"//; s/".*//' | sort -V | tail -1)
fi
[ -n "$version" ] || err "could not resolve the latest release tag (set DEXAFLOW_VERSION)"
fi
# GoReleaser archive names drop the leading 'v' from the version.
ver_nov=$(printf '%s' "$version" | sed 's/^v//')
base="https://github.com/${REPO}/releases/download/${version}"
info "installing Dexaflow ${version} (${os}/${arch})"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
dl "${base}/checksums.txt" "${tmp}/checksums.txt" || err "downloading checksums.txt failed"
verify_checksums_signature "$tmp" "$base" "$version"
# Releases before the rename ship leoflow_* archives with leoflow* binaries.
archive="dexaflow_${ver_nov}_${os}_${arch}.tar.gz"
name=dexaflow
if ! grep -q " ${archive}\$" "${tmp}/checksums.txt"; then
archive="leoflow_${ver_nov}_${os}_${arch}.tar.gz"
name=leoflow
fi
dl "${base}/${archive}" "${tmp}/${archive}" || err "downloading ${archive} failed"
info "verifying checksum..."
expected=$(grep " ${archive}\$" "${tmp}/checksums.txt" | awk '{print $1}')
[ -n "$expected" ] || err "no checksum entry for ${archive}"
if have sha256sum; then
actual=$(sha256sum "${tmp}/${archive}" | awk '{print $1}')
elif have shasum; then
actual=$(shasum -a 256 "${tmp}/${archive}" | awk '{print $1}')
else
err "need sha256sum or shasum to verify the download"
fi
[ "$actual" = "$expected" ] || err "checksum mismatch for ${archive} (got ${actual}, want ${expected})"
# ── Extract and install ──
tar -xzf "${tmp}/${archive}" -C "$tmp"
mkdir -p "$INSTALL_DIR"
for suffix in "" -server -agent; do
bin="${name}${suffix}"
[ -f "${tmp}/${bin}" ] || err "archive is missing ${bin}"
install -m 0755 "${tmp}/${bin}" "${INSTALL_DIR}/${bin}"
# The pre-rename names keep working: scripts and habits that call leoflow*
# reach the same binaries.
if [ "$name" = dexaflow ]; then
ln -sf "${bin}" "${INSTALL_DIR}/leoflow${suffix}"
fi
done
# The MCP server ships in the same archive from v0.4.5 on and was dropped on the
# floor here (#976). Older archives lack it, so it is installed when present.
if [ -f "${tmp}/${name}-mcp" ]; then
install -m 0755 "${tmp}/${name}-mcp" "${INSTALL_DIR}/${name}-mcp"
if [ "$name" = dexaflow ]; then
ln -sf "${name}-mcp" "${INSTALL_DIR}/leoflow-mcp"
fi
fi
info "installed binaries to ${INSTALL_DIR}"
# ── PATH ──
# Persist INSTALL_DIR on PATH in the user's shell profile so `leoflow` works in
# new shells without a manual step. Idempotent; opt out with LEOFLOW_NO_PATH=1.
add_to_profile() {
dir="$1"
# Detect the user's login shell from /etc/passwd, not $SHELL: under
# `curl | sh` the script runs in /bin/sh and $SHELL is unreliable (it caused
# the PATH to land in ~/.profile while the user's bash reads ~/.bashrc).
login_shell="${SHELL:-}"
if command -v getent >/dev/null 2>&1; then
ls_shell=$(getent passwd "$(id -un)" 2>/dev/null | cut -d: -f7)
[ -n "$ls_shell" ] && login_shell="$ls_shell"
fi
case "$(basename "${login_shell:-sh}")" in
zsh) profile="${HOME}/.zshrc" ;;
bash) profile="${HOME}/.bashrc" ;;
*) profile="${HOME}/.profile" ;;
esac
if [ -f "$profile" ] && grep -qF "$dir" "$profile" 2>/dev/null; then
info "${dir} already on PATH in ${profile}"
return
fi
printf '\n# added by dexaflow install.sh\nexport PATH="%s:$PATH"\n' "$dir" >>"$profile" || {
info "could not update ${profile}; add this to your shell profile:"
printf ' export PATH="%s:$PATH"\n' "$dir"
return
}
info "added ${dir} to PATH in ${profile}"
PATH_PROFILE="$profile"
}
case ":${PATH}:" in
*":${INSTALL_DIR}:"*) ;;
*)
if [ "${DEXAFLOW_NO_PATH:-${LEOFLOW_NO_PATH:-}}" = "1" ]; then
info "add ${INSTALL_DIR} to your PATH:"
printf ' export PATH="%s:$PATH"\n' "$INSTALL_DIR"
else
add_to_profile "$INSTALL_DIR"
fi
# Make the CLI usable for the rest of this script too.
export PATH="${INSTALL_DIR}:${PATH}"
;;
esac
# ── Bootstrap the managed runtime ──
if [ "${DEXAFLOW_NO_SETUP:-${LEOFLOW_NO_SETUP:-}}" = "1" ]; then
info "skipping setup (DEXAFLOW_NO_SETUP=1); run '${INSTALL_DIR}/${name} setup' when ready"
else
info "running '${name} setup'..."
# Attach the controlling terminal so the interactive setup wizard (where your
# DAGs live, how tasks run, the admin login) can prompt even under `curl | sh`,
# where stdin is the piped script, not a TTY. With no terminal (CI) or
# LEOFLOW_NONINTERACTIVE=1, fall back to non-interactive defaults.
#
# Probe by actually OPENING /dev/tty in a subshell — a `[ -r /dev/tty ]` test
# passes on the device node's permissions even in a CI container that has no
# controlling terminal, where the `</dev/tty` redirect then fails ("No such
# device or address") and aborts the installer.
if [ "${DEXAFLOW_NONINTERACTIVE:-${LEOFLOW_NONINTERACTIVE:-}}" != "1" ] && (exec 3</dev/tty) 2>/dev/null; then
"${INSTALL_DIR}/${name}" setup </dev/tty
else
"${INSTALL_DIR}/${name}" setup
fi
fi
printf '\n'
info "Dexaflow Lite is installed."
if [ -n "${PATH_PROFILE:-}" ]; then
info "next steps:"
printf ' 1) reload your shell: source %s (or open a new terminal)\n' "$PATH_PROFILE"
printf ' 2) start Dexaflow: %s lite\n' "$name"
else
info "next: ${name} lite"
fi
printf ' %s lite scaffolds a starter DAG (if needed), prints the URL + login,\n' "$name"
printf ' and hot-reloads on save. Add --host 0.0.0.0 to reach it from your network.\n'