Skip to content

Commit fd8025c

Browse files
committed
refactor installer for portable platform detection
1 parent fe7cda8 commit fd8025c

1 file changed

Lines changed: 281 additions & 59 deletions

File tree

install.sh

Lines changed: 281 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,298 @@
1-
#!/usr/bin/env bash
2-
set -e
1+
#!/bin/sh
2+
set -eu
33

44
REPO="dsecurity49/safe-migrate"
55
BIN_NAME="safe-migrate"
66

7-
echo "Detecting operating system and architecture..."
7+
VERSION="latest"
8+
TARGET_OVERRIDE=""
9+
PREFIX_OVERRIDE=""
10+
INSTALL_DIR_OVERRIDE=""
11+
FORCE=0
12+
DRY_RUN=0
13+
VERBOSE=0
814

9-
# Detect OS
10-
OS="$(uname -s)"
11-
case "${OS}" in
12-
Linux*) OS_TARGET="unknown-linux-musl";;
13-
Darwin*) OS_TARGET="apple-darwin";;
14-
*) echo "Unsupported OS: ${OS}. Please compile from source."; exit 1;;
15-
esac
15+
log() { printf '%s\n' "$*" >&2; }
16+
warn() { printf 'Warning: %s\n' "$*" >&2; }
17+
die() { printf 'Error: %s\n' "$*" >&2; exit 1; }
18+
19+
need() {
20+
command -v "$1" >/dev/null 2>&1 || die "$1 is required"
21+
}
22+
23+
run() {
24+
if [ "$DRY_RUN" -eq 1 ]; then
25+
log "[dry-run] $*"
26+
else
27+
"$@"
28+
fi
29+
}
30+
31+
normalize_version() {
32+
case "$1" in
33+
latest) printf '%s\n' latest ;;
34+
v*) printf '%s\n' "$1" ;;
35+
*) printf 'v%s\n' "$1" ;;
36+
esac
37+
}
38+
39+
detect_linux_flavor() {
40+
case "${TERMUX_VERSION:-}:${ANDROID_ROOT:-}" in
41+
:) ;;
42+
*:*) printf '%s\n' musl; return ;;
43+
esac
44+
45+
case "${PREFIX:-}" in
46+
/data/data/com.termux/*) printf '%s\n' musl; return ;;
47+
esac
48+
49+
if command -v ldd >/dev/null 2>&1 && ldd --version 2>&1 | grep -qi musl; then
50+
printf '%s\n' musl
51+
return
52+
fi
53+
54+
printf '%s\n' gnu
55+
}
56+
57+
build_url() {
58+
version="$1"
59+
target="$2"
60+
61+
if [ "$version" = latest ]; then
62+
printf 'https://github.com/%s/releases/latest/download/%s-%s.tar.gz' \
63+
"$REPO" "$BIN_NAME" "$target"
64+
else
65+
printf 'https://github.com/%s/releases/download/%s/%s-%s.tar.gz' \
66+
"$REPO" "$version" "$BIN_NAME" "$target"
67+
fi
68+
}
69+
70+
target_candidates() {
71+
os="$1"
72+
arch="$2"
73+
74+
case "$TARGET_OVERRIDE" in
75+
"")
76+
;;
77+
*)
78+
printf '%s\n' "$TARGET_OVERRIDE"
79+
return
80+
;;
81+
esac
82+
83+
case "$os" in
84+
Darwin)
85+
case "$arch" in
86+
x86_64|amd64) arch="x86_64" ;;
87+
aarch64|arm64) arch="aarch64" ;;
88+
*) die "Unsupported architecture: $arch" ;;
89+
esac
90+
printf '%s\n' "${arch}-apple-darwin"
91+
;;
92+
Linux)
93+
case "$arch" in
94+
x86_64|amd64) arch="x86_64" ;;
95+
aarch64|arm64) arch="aarch64" ;;
96+
*) die "Unsupported architecture: $arch" ;;
97+
esac
98+
99+
if [ "$(detect_linux_flavor)" = musl ]; then
100+
printf '%s\n' "${arch}-unknown-linux-musl ${arch}-unknown-linux-gnu"
101+
else
102+
printf '%s\n' "${arch}-unknown-linux-gnu ${arch}-unknown-linux-musl"
103+
fi
104+
;;
105+
*)
106+
die "Unsupported OS: $os"
107+
;;
108+
esac
109+
}
110+
111+
download_asset() {
112+
version="$1"
113+
target="$2"
114+
url="$(build_url "$version" "$target")"
115+
out="${TMP_DIR}/${BIN_NAME}-${target}.tar.gz"
116+
part="${out}.partial"
117+
118+
[ "$VERBOSE" -eq 1 ] && log "Trying ${target}: ${url}"
119+
120+
rm -f "$part"
121+
122+
if ! curl -#fL -C - -o "$part" "$url"; then
123+
rm -f "$part"
124+
return 1
125+
fi
126+
127+
if ! tar -tzf "$part" >/dev/null 2>&1; then
128+
rm -f "$part"
129+
return 1
130+
fi
131+
132+
mv "$part" "$out"
133+
printf '%s\n' "$out"
134+
}
16135

17-
# Detect Architecture
136+
pick_install_dir() {
137+
if [ -n "$INSTALL_DIR_OVERRIDE" ]; then
138+
printf '%s\n' "$INSTALL_DIR_OVERRIDE"
139+
return
140+
fi
141+
142+
if [ -n "$PREFIX_OVERRIDE" ]; then
143+
printf '%s\n' "${PREFIX_OVERRIDE%/}/bin"
144+
return
145+
fi
146+
147+
case "${TERMUX_VERSION:-}:${ANDROID_ROOT:-}" in
148+
:) ;;
149+
*:*)
150+
if [ -n "${PREFIX:-}" ]; then
151+
printf '%s/bin\n' "$PREFIX"
152+
return
153+
fi
154+
;;
155+
esac
156+
157+
if [ -w "/usr/local/bin" ]; then
158+
printf '%s\n' "/usr/local/bin"
159+
return
160+
fi
161+
162+
printf '%s\n' "${HOME}/.local/bin"
163+
}
164+
165+
usage() {
166+
cat <<EOF
167+
Usage: ${0##*/} [options]
168+
169+
Options:
170+
--version <tag> Install a specific version (default: latest)
171+
--target <triplet> Force a release target, e.g. aarch64-unknown-linux-musl
172+
--prefix <dir> Install under this prefix, e.g. /usr/local or \$HOME/.local
173+
--install-dir <dir> Install into this exact directory
174+
--force Overwrite an existing binary
175+
--dry-run Show actions without changing anything
176+
-v, --verbose Show download attempts
177+
-h, --help Show this help
178+
EOF
179+
}
180+
181+
while [ $# -gt 0 ]; do
182+
case "$1" in
183+
--version)
184+
[ $# -ge 2 ] || die "--version requires a value"
185+
VERSION="$2"
186+
shift 2
187+
;;
188+
--target)
189+
[ $# -ge 2 ] || die "--target requires a value"
190+
TARGET_OVERRIDE="$2"
191+
shift 2
192+
;;
193+
--prefix)
194+
[ $# -ge 2 ] || die "--prefix requires a value"
195+
PREFIX_OVERRIDE="$2"
196+
shift 2
197+
;;
198+
--install-dir)
199+
[ $# -ge 2 ] || die "--install-dir requires a value"
200+
INSTALL_DIR_OVERRIDE="$2"
201+
shift 2
202+
;;
203+
--force)
204+
FORCE=1
205+
shift
206+
;;
207+
--dry-run)
208+
DRY_RUN=1
209+
shift
210+
;;
211+
-v|--verbose)
212+
VERBOSE=1
213+
shift
214+
;;
215+
-h|--help)
216+
usage
217+
exit 0
218+
;;
219+
*)
220+
die "Unknown argument: $1"
221+
;;
222+
esac
223+
done
224+
225+
need curl
226+
need tar
227+
need uname
228+
need sed
229+
need head
230+
need grep
231+
need mktemp
232+
need find
233+
need cp
234+
need chmod
235+
need mkdir
236+
need mv
237+
238+
VERSION="$(normalize_version "$VERSION")"
239+
240+
log "Detecting operating system and architecture..."
241+
242+
OS="$(uname -s)"
18243
ARCH="$(uname -m)"
19-
case "${ARCH}" in
20-
x86_64|amd64) ARCH_TARGET="x86_64";;
21-
aarch64|arm64) ARCH_TARGET="aarch64";;
22-
*) echo "Unsupported architecture: ${ARCH}. Please compile from source."; exit 1;;
23-
esac
24244

25-
TARGET="${ARCH_TARGET}-${OS_TARGET}"
26-
echo "Target detected: ${TARGET}"
27-
echo "Fetching latest release version..."
245+
TMP_DIR="$(mktemp -d)"
246+
trap 'rm -rf "$TMP_DIR"' EXIT HUP INT TERM
28247

29-
# Get latest release tag from GitHub API
30-
VERSION=$(curl -sL "https://api.github.com/repos/${REPO}/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
248+
CANDIDATES="$(target_candidates "$OS" "$ARCH")"
31249

32-
if [ -z "$VERSION" ]; then
33-
echo "Error: Failed to fetch the latest version. Check your internet connection or GitHub API limits."
34-
exit 1
35-
fi
250+
TAR_FILE=""
251+
SELECTED_TARGET=""
36252

37-
DOWNLOAD_URL="https://github.com/dsecurity49/safe-migrate/releases/download/${VERSION}/${BIN_NAME}-${TARGET}.tar.gz"
38-
TMP_DIR=$(mktemp -d)
39-
TAR_FILE="${TMP_DIR}/${BIN_NAME}.tar.gz"
253+
for candidate in $CANDIDATES; do
254+
if TAR_FILE="$(download_asset "$VERSION" "$candidate")"; then
255+
SELECTED_TARGET="$candidate"
256+
break
257+
fi
258+
done
40259

41-
echo "Downloading ${BIN_NAME} ${VERSION}..."
42-
curl -sL "$DOWNLOAD_URL" -o "$TAR_FILE"
260+
[ -n "$TAR_FILE" ] || {
261+
printf 'Error: no matching release asset found for %s\n' "$VERSION" >&2
262+
printf 'Tried targets:\n' >&2
263+
for t in $CANDIDATES; do
264+
printf ' - %s\n' "$t" >&2
265+
done
266+
exit 1
267+
}
43268

44-
if ! file "$TAR_FILE" | grep -q "gzip compressed data"; then
45-
echo "Error: Downloaded file is not a valid archive. The release might still be building on GitHub Actions. Try again in a few minutes."
46-
exit 1
47-
fi
269+
log "Selected target: ${SELECTED_TARGET}"
270+
log "Extracting archive..."
271+
run tar -xzf "$TAR_FILE" -C "$TMP_DIR"
48272

49-
echo "Extracting archive..."
50-
tar -xzf "$TAR_FILE" -C "$TMP_DIR"
51-
52-
# Determine install location based on permissions
53-
if [ -w "/usr/local/bin" ] || sudo -n true 2>/dev/null; then
54-
INSTALL_DIR="/usr/local/bin"
55-
echo "Moving binary to ${INSTALL_DIR}..."
56-
sudo mv "${TMP_DIR}/${BIN_NAME}" "${INSTALL_DIR}/${BIN_NAME}"
57-
sudo chmod +x "${INSTALL_DIR}/${BIN_NAME}"
58-
else
59-
INSTALL_DIR="${HOME}/.local/bin"
60-
echo "No sudo privileges detected. Installing to ${INSTALL_DIR}..."
61-
mkdir -p "${INSTALL_DIR}"
62-
mv "${TMP_DIR}/${BIN_NAME}" "${INSTALL_DIR}/${BIN_NAME}"
63-
chmod +x "${INSTALL_DIR}/${BIN_NAME}"
64-
65-
if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
66-
echo "Note: ${INSTALL_DIR} is not in your PATH. You may need to add it."
67-
fi
273+
BIN_PATH="$(find "$TMP_DIR" -type f -name "$BIN_NAME" | head -n 1 || true)"
274+
[ -n "$BIN_PATH" ] || die "Binary not found inside archive"
275+
276+
INSTALL_DIR="$(pick_install_dir)"
277+
DEST="${INSTALL_DIR}/${BIN_NAME}"
278+
279+
if [ -e "$DEST" ] && [ "$FORCE" -ne 1 ]; then
280+
die "${DEST} already exists. Use --force to overwrite."
68281
fi
69282

70-
# Cleanup
71-
rm -rf "$TMP_DIR"
283+
run mkdir -p "$INSTALL_DIR"
284+
[ -w "$INSTALL_DIR" ] || die "No write permission for ${INSTALL_DIR}. Use --install-dir <dir> or --prefix <dir>."
285+
286+
log "Installing to ${INSTALL_DIR}..."
287+
run cp "$BIN_PATH" "$DEST"
288+
run chmod +x "$DEST"
289+
290+
case ":$PATH:" in
291+
*":$INSTALL_DIR:"*) ;;
292+
*) warn "${INSTALL_DIR} is not in your PATH" ;;
293+
esac
72294

73-
echo "----------------------------------------"
74-
echo "${BIN_NAME} ${VERSION} installed successfully!"
75-
echo "Run 'safe-migrate --help' to get started."
76-
echo "----------------------------------------"
295+
log "----------------------------------------"
296+
log "${BIN_NAME} ${VERSION} installed successfully!"
297+
log "Run '${BIN_NAME} --help' to get started."
298+
log "----------------------------------------"

0 commit comments

Comments
 (0)