forked from IrosTheBeggar/mStream
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinstall.sh
More file actions
618 lines (588 loc) · 31.1 KB
/
Copy pathinstall.sh
File metadata and controls
618 lines (588 loc) · 31.1 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
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
#!/bin/sh
# Installs mStream on Linux and macOS.
#
# curl -fsSL https://raw.githubusercontent.com/IrosTheBeggar/mStream/master/install.sh | sh
#
# Picks the release bundle for this machine, checks its sha256 against the
# release's manifest.json, extracts it into a versioned folder, and points a
# `current` link + a `mstream-server` command at it. Configuration, all
# optional, via environment variables (the pipe-to-sh form takes no args):
#
# MSTREAM_VERSION a tag like v6.20.0 (default: latest)
# MSTREAM_INSTALL_DIR root for the app folders (default: per-OS user data
# home, next to where the server keeps its own data)
# MSTREAM_BIN_DIR where the `mstream-server` command goes
# (default: ~/.local/bin)
# MSTREAM_NO_DESKTOP set to 1 to skip the app-menu entry (Linux) /
# ~/Applications copy (macOS). Persisted: scheduled
# re-runs (the in-app updater) keep the choice.
# Set to 0 to re-enable and clear the persisted opt-out.
# MSTREAM_UNINSTALL set to 1 to remove everything this script installed
# (app folders, command, menu entry / ~/Applications
# copy, login item). Your data is left in place.
# MSTREAM_RELEASE_BASE URL serving manifest.json + the zips (default: the
# GitHub release) - for internal mirrors and testing
# MSTREAM_KEY force a bundle key (linux-x64, linux-x64-musl,
# linux-arm64, linux-arm64-musl, darwin-x64,
# darwin-arm64) when auto-detection gets it wrong.
# The key actually installed is persisted and rules
# later runs, so host-probe drift (e.g. gcompat
# adding a glibc loader on musl) never switches
# bundle families on an existing install
# MSTREAM_FORCE set to 1 to replace an already-installed copy of
# the same version (the old one is moved aside)
#
# The bundle is a folder, not one binary: mstream-server + webapp/ + bin/
# sidecars, plus the desktop launcher (mstream-desktop / mStream.app) on the
# targets that ship one. Data (config, db, caches) lives OUTSIDE the install
# folder in the server's data home, so re-running this to upgrade never
# touches your library.
#
# POSIX sh on purpose: NAS boxes and minimal images do not all carry bash.
set -eu
# Everything below runs from main(), called on the LAST line: sh parses the
# whole function before executing any of it, so a `curl | sh` whose stream
# is cut short fails to parse instead of running half an install (the
# rustup / brew / deno installers all do this).
main() {
REPO="IrosTheBeggar/mStream"
VERSION="${MSTREAM_VERSION:-latest}"
BIN_DIR="${MSTREAM_BIN_DIR:-$HOME/.local/bin}"
os=$(uname -s)
arch=$(uname -m)
case "$os" in
Darwin) platform=darwin ;;
Linux) platform=linux ;;
*)
echo "unsupported OS: $os - on Windows, use install.ps1" >&2
exit 1
;;
esac
# Linux: glibc or musl? The glibc bundles cannot exec on Alpine/musl; the
# static musl bundles run anywhere but ship no desktop launcher and no
# server-audio sidecar, so they are only picked when they are the ones that
# will actually run. Decide POSITIVELY for glibc first: a glibc box that has
# the `musl` package installed (musl-tools for Rust/Go cross builds, Arch's
# musl) carries /lib/ld-musl-*.so.1 too, and picking musl there installs a
# server-only bundle whose libstdc++ the musl loader can't even find.
libc=""
if [ "$platform" = linux ]; then
if [ -e /lib64/ld-linux-x86-64.so.2 ] || [ -e /lib/ld-linux-aarch64.so.1 ] \
|| ldd --version 2>&1 | grep -qiE 'glibc|GNU libc'; then
libc=""
elif ldd --version 2>&1 | grep -qi musl \
|| [ -e /lib/ld-musl-x86_64.so.1 ] || [ -e /lib/ld-musl-aarch64.so.1 ]; then
libc="-musl"
fi
fi
# 64-bit kernel, 32-bit userland: uname -m says aarch64 on a Pi 4/400/CM4
# running 32-bit Raspberry Pi OS (the default kernel there is 64-bit), but
# no 64-bit binary can run. Ask the userland, not the kernel.
if [ "$platform" = linux ] && [ "$(getconf LONG_BIT 2>/dev/null || echo 64)" = 32 ]; then
arch="32bit-userland"
fi
case "$platform-$arch" in
darwin-x86_64) key="darwin-x64" ;;
darwin-arm64) key="darwin-arm64" ;;
linux-x86_64 | linux-amd64) key="linux-x64$libc" ;;
linux-aarch64 | linux-arm64) key="linux-arm64$libc" ;;
linux-armv7l | linux-armv6l | linux-32bit-userland)
echo "no 32-bit build - mStream needs a 64-bit OS (on a Pi: the 64-bit Raspberry Pi OS image), or run from source with Node" >&2
exit 1
;;
*)
echo "no prebuilt bundle for $os/$arch - run from source: https://github.com/$REPO/blob/master/docs/install.md" >&2
exit 1
;;
esac
# Escape hatch for exotic setups: name the bundle key yourself.
key="${MSTREAM_KEY:-$key}"
# Where the app folders live: beside the server's own data home so one
# `du` shows all of mStream, and so a future in-app updater and this script
# agree on the layout ($ROOT/<bundle>/ + $ROOT/current -> <bundle>).
if [ -n "${MSTREAM_INSTALL_DIR:-}" ]; then
ROOT="$MSTREAM_INSTALL_DIR"
elif [ "$platform" = darwin ]; then
ROOT="$HOME/Library/Application Support/mStream/app"
else
ROOT="${XDG_DATA_HOME:-$HOME/.local/share}/mstream/app"
fi
# Both dirs become symlink TARGETS below; a relative value would be stored
# literally and dangle from wherever the link lives. Canonicalize.
mkdir -p "$ROOT"
ROOT=$(cd "$ROOT" && pwd -P)
# Choices persisted by an earlier run rule later ones: the in-app updater
# re-runs this script on a schedule with a bare environment, and "I opted
# out of the app-menu entry" or a custom command dir must survive that.
# Env still wins when set; MSTREAM_NO_DESKTOP=0 explicitly re-enables (and
# un-persists) the integration.
if [ -f "$ROOT/.install-options" ]; then
while IFS='=' read -r optk optv; do
case "$optk" in
no_desktop) [ -z "${MSTREAM_NO_DESKTOP:-}" ] && MSTREAM_NO_DESKTOP=1 ;;
bin_dir) [ -z "${MSTREAM_BIN_DIR:-}" ] && [ -n "$optv" ] && BIN_DIR="$optv" ;;
esac
done < "$ROOT/.install-options"
fi
[ "${MSTREAM_NO_DESKTOP:-}" = 0 ] && MSTREAM_NO_DESKTOP=""
mkdir -p "$BIN_DIR"
BIN_DIR=$(cd "$BIN_DIR" && pwd -P)
# The bundle key chosen at FIRST install rules later runs too: the host
# probes above can drift (installing gcompat drops a glibc loader on a musl
# box and flips the detection), and a scheduled re-run must never switch
# bundle families on a live install because of that. An explicit
# MSTREAM_KEY still overrides - and updates the persisted choice below.
if [ -z "${MSTREAM_KEY:-}" ] && [ -f "$ROOT/.bundle-key" ]; then
saved_key=$(head -1 "$ROOT/.bundle-key" | tr -cd 'a-z0-9-')
case " win-x64 linux-x64 linux-arm64 linux-x64-musl linux-arm64-musl darwin-x64 darwin-arm64 " in
*" $saved_key "*) key="$saved_key" ;;
esac
fi
# Per-platform names inside a bundle, and where the server keeps its data
# (which this script never touches - not on install, not on uninstall).
if [ "$platform" = darwin ]; then
launcher_rel="mStream.app/Contents/MacOS/mStream"; server_rel="mStream.app/Contents/MacOS/mstream-server"
data_home="$HOME/Library/Application Support/mStream"
else
launcher_rel="mstream-desktop"; server_rel="mstream-server"
data_home="${XDG_DATA_HOME:-$HOME/.local/share}/mstream"
fi
# Where the login item points right now, if anywhere. The launcher registers
# ONE per-user login item by app name ("mStream"), so a second copy of
# mStream on the machine - one the user extracted by hand, a developer's
# checkout - shares the slot. This script only ever re-points or removes
# a login item that points at a copy IT manages ($ROOT, or the
# ~/Applications copy on macOS); anything else is the user's and is left
# alone, and they are told so.
login_item_target() {
if [ "$platform" = darwin ]; then
f="$HOME/Library/LaunchAgents/mStream.plist"
# First absolute-path <string> in the plist, WHEREVER it sits: the
# launcher's auto-launch crate writes ProgramArguments as ONE line
# (<array><string>/path</string><string>--autostarted</string>...),
# so a ^-anchored per-line match never fires and every login item
# would read as "not ours" - upgrades would silently never re-point
# (measured). Splitting on '<' handles one-line and pretty-printed
# plists alike, and greedy-match order can't pick a later argument.
[ -f "$f" ] && tr '<' '\n' < "$f" | sed -n 's/^string>\(\/.*\)/\1/p' | head -1
else
f="$HOME/.config/autostart/mStream.desktop"
[ -f "$f" ] && sed -n 's/^Exec="\{0,1\}\([^" ]*\).*/\1/p' "$f" | head -1
fi
}
login_item_is_ours() {
t=$(login_item_target)
[ -z "$t" ] && return 1
case "$t" in
"$ROOT/"*) return 0 ;;
"$HOME/Applications/mStream.app/"*) apps_copy_is_ours && return 0 ;;
esac
return 1
}
# Does this script own ~/Applications/mStream.app? The ownership marker is a
# SIBLING file, never inside the bundle: writing anything into Contents/
# invalidates the notarization seal, and the hardened-runtime launcher in a
# seal-broken bundle is SIGKILLed by the kernel at exec - the installed app
# would be dead on arrival (measured; the very bug this marker placement
# once caused). The legacy inner marker is still honored read-only so a copy
# installed by the broken version is recognized as ours and healed by the
# next install run.
apps_marker="$HOME/Applications/.mstream-installer"
apps_copy_is_ours() {
[ -f "$apps_marker" ] && return 0
[ -f "$HOME/Applications/mStream.app/Contents/.mstream-installer" ] && return 0
return 1
}
# -- Uninstall: MSTREAM_UNINSTALL=1 removes everything this script created -
# the app folders, the command link, the app-menu entry / ~/Applications
# copy, and the login item - and nothing else. Your library, config, and
# database in the data home stay put; the last line says where.
if [ -n "${MSTREAM_UNINSTALL:-}" ]; then
if command -v pgrep >/dev/null 2>&1 && pgrep -f "^$ROOT/" >/dev/null 2>&1; then
echo "mStream is running from $ROOT - Quit it from the tray icon first, then re-run" >&2
exit 1
fi
launcher="$ROOT/current/$launcher_rel"
[ "$platform" = darwin ] && apps_copy_is_ours \
&& launcher="$HOME/Applications/mStream.app/Contents/MacOS/mStream"
# Login item first, while a launcher still exists to remove it - but
# only if it points at a copy this script manages.
if [ -x "$launcher" ] && login_item_is_ours; then
"$launcher" --autostart=disable >/dev/null 2>&1 && echo "removed the login item" || true
elif [ -n "$(login_item_target)" ]; then
echo "left the login item alone - it points at $(login_item_target), which this script did not install"
fi
if [ -L "$BIN_DIR/mstream-server" ]; then rm -f "$BIN_DIR/mstream-server"; echo "removed $BIN_DIR/mstream-server"; fi
if [ "$platform" = linux ]; then
f="${XDG_DATA_HOME:-$HOME/.local/share}/applications/mstream.desktop"
if [ -f "$f" ]; then rm -f "$f"; echo "removed the app-menu entry"; fi
else
d="$HOME/Applications/mStream.app"
if [ -L "$d" ]; then rm -f "$d" && echo "removed ~/Applications/mStream.app (link)"
elif [ -e "$d" ] && apps_copy_is_ours; then rm -rf "$d" && echo "removed ~/Applications/mStream.app"
elif [ -e "$d" ]; then echo "left ~/Applications/mStream.app alone (not installed by this script)"
fi
rm -f "$apps_marker"
fi
if [ -d "$ROOT" ]; then rm -rf "$ROOT" && echo "removed $ROOT"; fi
echo "mStream is uninstalled. Your library, config, and database were left in place:"
echo " $data_home"
echo " (delete that folder yourself if you want them gone too)"
exit 0
fi
if [ -n "${MSTREAM_RELEASE_BASE:-}" ]; then
base="${MSTREAM_RELEASE_BASE%/}"
elif [ "$VERSION" = latest ]; then
base="https://github.com/$REPO/releases/latest/download"
else
base="https://github.com/$REPO/releases/download/$VERSION"
fi
# fetch URL DEST: curl or wget, never silent on failure. `wget -q` swallows
# its own "ERROR 404" so a missing asset used to die with no clue; -nv keeps
# errors and drops the progress noise. Callers add the WHY on failure.
fetch() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL -o "$2" "$1"
elif command -v wget >/dev/null 2>&1; then
wget -nv -O "$2" "$1"
else
echo "this installer needs curl or wget" >&2
exit 1
fi
}
command -v unzip >/dev/null 2>&1 || {
echo "this installer needs unzip (debian/ubuntu: sudo apt install unzip; alpine: apk add unzip)" >&2
exit 1
}
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT INT TERM
# The manifest first: it names the exact zip for this version, so the
# script never has to guess the version number embedded in the filename.
echo "fetching manifest ($VERSION)..."
if ! fetch "$base/manifest.json" "$tmp/manifest.json"; then
echo "could not fetch $base/manifest.json" >&2
echo " Releases published before the installer existed have no manifest.json." >&2
echo " Pick a newer MSTREAM_VERSION, or download a bundle by hand from" >&2
echo " https://github.com/$REPO/releases and see docs/install.md." >&2
exit 1
fi
version=$(grep -o '"version": *"[^"]*"' "$tmp/manifest.json" | head -1 | sed 's/.*"\([^"]*\)"$/\1/')
[ -n "$version" ] || { echo "manifest.json is missing a version - refusing to install" >&2; exit 1; }
# apiVersion gates the layout contract between this script and the release.
# This copy may be YEARS old by the time it runs (it rides inside every
# bundle for the in-app updater), so a bumped apiVersion means: do not
# guess at a layout you predate - fetch the current installer instead.
api=$(grep -o '"apiVersion": *[0-9]*' "$tmp/manifest.json" | grep -o '[0-9]*$' || true)
if [ -n "$api" ] && [ "$api" != 1 ]; then
echo "this release uses a newer install layout (apiVersion $api) than this script understands" >&2
echo " re-run the current one-line installer from https://github.com/$REPO (see docs/install.md)" >&2
exit 1
fi
# The version becomes a path component below: keep it to what a release
# tag can contain, so a bad manifest can't steer rm/mv anywhere.
case "$version" in
*[!0-9A-Za-z.-]*|"") echo "manifest.json version '$version' is not a plain version string - refusing" >&2; exit 1 ;;
esac
asset="mStream-$version-$key.zip"
# The hash for this asset, out of the manifest published beside it. No jq
# on a fresh box, so the line is picked apart with the tools sh always has.
expected=$(grep -o "\"file\": *\"$asset\", *\"sha256\": *\"[0-9a-f]*\"" "$tmp/manifest.json" \
| grep -o '[0-9a-f]\{64\}' || true)
if [ -z "$expected" ]; then
echo "manifest.json has no entry for $asset - this release has no bundle for $key" >&2
exit 1
fi
echo "fetching $asset..."
fetch "$base/$asset" "$tmp/$asset" || { echo "download failed: $base/$asset" >&2; exit 1; }
if command -v sha256sum >/dev/null 2>&1; then
actual=$(sha256sum "$tmp/$asset" | cut -d' ' -f1)
else
actual=$(shasum -a 256 "$tmp/$asset" | cut -d' ' -f1)
fi
if [ "$actual" != "$expected" ]; then
echo "sha256 mismatch for $asset - download corrupted, not installing" >&2
echo " expected $expected" >&2
echo " got $actual" >&2
exit 1
fi
# link TARGET DEST: point DEST at TARGET as a symlink. `ln -sfn` alone is a
# trap here: -n only stops it following an existing SYMLINK; if DEST is a
# real directory, ln happily creates the link INSIDE it (DEST/<basename>)
# and exits 0 - a hand-installed ~/Applications/mStream.app would silently
# keep the old version. So: an existing symlink is replaced; an existing
# real directory is left alone and reported (never delete a user's own
# folder); the result is verified.
link() {
if [ -e "$2" ] && [ ! -L "$2" ]; then
echo " note: $2 exists and is not a link - left alone (remove it and re-run to relink)" >&2
return 1
fi
ln -sfn "$1" "$2" && [ "$(readlink "$2")" = "$1" ]
}
bundle="mStream-$version-$key"
mkdir -p "$ROOT"
# Same version already installed? Leave it ALONE. That folder may be live
# (a running server), and under --portable it holds the user's config and
# database - an unconditional rm -rf here would delete a library to
# "upgrade" it to itself. Re-verify against the manifest so a corrupt or
# tampered copy is still caught, then just re-point the links.
if [ -d "$ROOT/$bundle" ] && [ -z "${MSTREAM_FORCE:-}" ]; then
echo "mStream $version is already installed at $ROOT/$bundle - keeping it (set MSTREAM_FORCE=1 to replace)"
installed_fresh=""
else
# A forced replace of the same version moves the old folder aside
# (mv, not rm): whatever state it held survives at a findable name.
if [ -d "$ROOT/$bundle" ]; then
old="$ROOT/$bundle.replaced-$(date +%Y%m%d%H%M%S)"
mv "$ROOT/$bundle" "$old"
echo " previous copy moved to $old"
fi
# Extract beside the final location, then rename into place: a killed
# install leaves a stray .partial folder, never a half-written `current`.
rm -rf "$ROOT/$bundle.partial"
unzip -q "$tmp/$asset" -d "$ROOT/$bundle.partial"
mv "$ROOT/$bundle.partial/$bundle" "$ROOT/$bundle"
rmdir "$ROOT/$bundle.partial"
# Zip mode bits are usually preserved (Info-ZIP records them), but a
# bundle that went through a mode-stripping hop must still run.
chmod 755 "$ROOT/$bundle/mstream-server" 2>/dev/null || true
chmod 755 "$ROOT/$bundle/mstream-desktop" 2>/dev/null || true
chmod 755 "$ROOT/$bundle/mStream.app/Contents/MacOS/"* 2>/dev/null || true
chmod 755 "$ROOT/$bundle"/bin/*/* 2>/dev/null || true
installed_fresh=1
fi
# Which version is running right now, if any? Needed twice below: the
# login item must be re-pointed at the NEW launcher, and the user must be
# told that the running instance stays on the OLD version until they quit
# it - nothing here can (or should) kill their server under them.
running_from=""
if command -v ps >/dev/null 2>&1; then
# `ps -A -o pid=,args=` - the POSIX spelling - NOT `pgrep -a` and NOT
# `ps -ax`: on macOS/BSD pgrep, -a means "include ancestors" and prints
# bare PIDs (no command column - the match silently never fired on
# Macs, measured), and busybox ps (Alpine, most NAS firmware) rejects
# the BSD -ax flags outright ("unrecognized option: x" - the scan was
# silently empty there, measured). -A = every process on busybox,
# procps, and BSD alike; args= puts argv[0] in $2 on all three. Match
# the EXECUTABLE (argv[0]), not the whole command line: a shell whose
# command text merely mentions "mstream-server" (a terminal, a script,
# docker-init) is not mStream.
for pid in $(ps -A -o pid=,args= 2>/dev/null \
| awk '$1 != '"$$"' && ($2 ~ /(^|\/)mstream-server$/ || $2 ~ /(^|\/)mstream-desktop$/ || $2 ~ /\/MacOS\/mStream$/) {print $1}'); do
# The real path of the running binary where the OS will say
# (/proc on Linux); argv[0] otherwise (macOS launches by absolute
# path from LaunchServices, so that is already the full path).
exe=$(readlink "/proc/$pid/exe" 2>/dev/null || ps -o comm= -p "$pid" 2>/dev/null || true)
# Skip the version this run is installing (a real path never goes
# through the `current` symlink, so compare against the bundle dir).
case "$exe" in
"$ROOT/$bundle/"*|"$ROOT/current/"*|"") ;;
*) running_from="$exe"; break ;;
esac
done
fi
# Does the NEW binary actually exec here? Probe it BEFORE `current` moves:
# a bundle this host cannot run (missing runtime library, a raised OS/libc
# floor, a bundle family the box cannot exec) must never take over a
# working install - the in-app updater drives this script on a schedule,
# and a silent flip here IS the next restart's outage. When there is a
# working install to keep, refuse and exit nonzero so the updater reports
# a failed stage instead of a staged landmine. A first install (nothing to
# keep) still proceeds and gets the diagnostic at the end.
new_server_v=$("$ROOT/$bundle/$server_rel" -V 2>/dev/null) || new_server_v=""
if [ -z "$new_server_v" ] && [ -L "$ROOT/current" ] && [ -e "$ROOT/current" ] \
&& [ "$(readlink "$ROOT/current")" != "$ROOT/$bundle" ]; then
echo "the new mstream-server ($version) does not run on this system - keeping the existing install" >&2
"$ROOT/$bundle/$server_rel" -V 2>&1 | head -3 | sed 's/^/ /' >&2
echo " current stays at $(readlink "$ROOT/current"); the new copy is at $ROOT/$bundle" >&2
echo " (missing runtime library? wrong bundle for this machine? see docs/install.md; MSTREAM_KEY forces a bundle)" >&2
exit 1
fi
# `current` -> this version. If a previous manual layout left a REAL
# directory named current, move it aside rather than nest into it.
if [ -e "$ROOT/current" ] && [ ! -L "$ROOT/current" ]; then
mv "$ROOT/current" "$ROOT/current.was-a-directory-$(date +%Y%m%d%H%M%S)"
fi
link "$ROOT/$bundle" "$ROOT/current"
# The choices this run actually used, for the next (possibly scheduled and
# env-bare) run - see the read side at the top. Written only once the new
# version is in place.
printf '%s\n' "$key" > "$ROOT/.bundle-key"
{
[ -n "${MSTREAM_NO_DESKTOP:-}" ] && echo "no_desktop=1"
[ "$BIN_DIR" != "$HOME/.local/bin" ] && printf 'bin_dir=%s\n' "$BIN_DIR"
:
} > "$ROOT/.install-options"
# The command: a symlink, so `mstream-server` on PATH always means `current`
# and an upgrade is one link flip. Data lives in the server's data home,
# never here.
server="$ROOT/current/$server_rel"
link "$server" "$BIN_DIR/mstream-server" || true
# Desktop integration where a launcher shipped.
desktop_note=""
if [ -z "${MSTREAM_NO_DESKTOP:-}" ]; then
if [ "$platform" = linux ] && [ -f "$ROOT/$bundle/mStream.desktop" ]; then
# The app-menu entry: written with printf, not by sed over the
# bundle's template - a ROOT containing '&' or '|' would corrupt or
# abort a sed replacement, and Exec= must be quoted per the XDG spec
# so a path with a space survives the launcher's word split.
apps="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
mkdir -p "$apps"
printf '[Desktop Entry]\nType=Application\nName=mStream\nComment=Self-hosted music streaming server\nExec="%s/current/mstream-desktop"\nTryExec=%s/current/mstream-desktop\nIcon=%s/current/mStream.png\nTerminal=false\nCategories=AudioVideo;Audio;Network;\n' \
"$ROOT" "$ROOT" "$ROOT" > "$apps/mstream.desktop"
chmod 644 "$apps/mstream.desktop"
command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database "$apps" 2>/dev/null || true
desktop_note="app menu: mStream (or run: $ROOT/current/mstream-desktop)"
elif [ "$platform" = darwin ] && [ -d "$ROOT/$bundle/mStream.app" ]; then
# A real COPY of the .app in ~/Applications, not a symlink: Spotlight
# indexes a symlink as a symlink (not an application), Launchpad
# skips it, and LaunchServices treats it as second-class - the same
# lesson Homebrew Cask learned. ditto preserves the bundle's internal
# symlink (Contents/MacOS/webapp -> ../Resources/webapp), xattrs,
# and the notarization staple, so Gatekeeper still sees a sealed,
# stapled app - PROVIDED nothing is ever written inside it: the
# ownership marker lives BESIDE the app (see apps_copy_is_ours), and
# the copy must land byte-identical to the bundle. Only THIS
# installer's copy is ever replaced: a bundle the user put there by
# hand (no marker) is left alone.
mkdir -p "$HOME/Applications"
dest="$HOME/Applications/mStream.app"
# Is anything running from under this path? pgrep matches the COMMAND
# string (argv), which keeps saying the ORIGINAL path even after the
# directory is renamed aside - so a busy $dest also means the .old.*
# asides may be live trees, and none of them can be swept this run.
apps_busy() { command -v pgrep >/dev/null 2>&1 && pgrep -f "^$1/" >/dev/null 2>&1; }
if [ -e "$dest" ] && [ ! -L "$dest" ] && ! apps_copy_is_ours; then
desktop_note="app: $ROOT/current/mStream.app (~/Applications/mStream.app is your own copy - replace it with this one when you're ready)"
elif [ -n "$installed_fresh" ] || [ ! -e "$dest" ]; then
# Sweep asides left by earlier upgrades that ran while mStream was
# live (below) - only once nothing runs from the copy at all.
if ! apps_busy "$dest"; then
for old in "$dest".old.*; do
[ -e "$old" ] || continue
apps_busy "$old" && continue
rm -rf "$old"
done
fi
rm -rf "$dest.installing"
if ditto "$ROOT/$bundle/mStream.app" "$dest.installing" 2>/dev/null; then
# A symlink from an older run of this script gives way too.
[ -L "$dest" ] && rm -f "$dest"
if [ -d "$dest" ]; then
# Replace the directory ENTRY, but never delete a tree a
# live process runs from: the binary itself survives an
# unlink, but the running app's webapp/ and bin/ sidecars
# are read from disk on demand - rm -rf here kills its UI
# mid-flight. Move the old copy aside instead; a later
# idle run sweeps it (above).
if apps_busy "$dest"; then
mv "$dest" "$dest.old.$(date +%Y%m%d%H%M%S).$$"
else
mv "$dest" "$dest.old.$$" && rm -rf "$dest.old.$$"
fi
fi
mv "$dest.installing" "$dest"
# Marker AFTER the copy is in place, and OUTSIDE the bundle.
# (This also heals a copy from the old installer, which wrote
# the marker inside Contents/ and broke the seal - the fresh
# ditto above replaced it wholesale.) Line 2 = the install
# ROOT, so the in-app updater's method detection can find a
# CUSTOM root from a launcher running out of ~/Applications;
# single-line markers from older runs read as version-only.
printf '%s\n%s\n' "$version" "$ROOT" > "$apps_marker"
desktop_note="app: ~/Applications/mStream.app (menu-bar icon; Quick Connect on)"
else
rm -rf "$dest.installing"
desktop_note="app: $ROOT/current/mStream.app (could not copy into ~/Applications)"
fi
else
desktop_note="app: ~/Applications/mStream.app (menu-bar icon; Quick Connect on)"
fi
fi
fi
# The launcher registers ITSELF as the login item, by absolute path - which
# is the VERSIONED folder, not `current`. Without this step an upgrade
# never reaches the login item: next boot silently starts the OLD version
# (and once the old folder is deleted, nothing at all, while the tray still
# says "Start at login" is on). --autostart=enable re-registers using the
# new binary's own path; the launcher's CLI face does this with no tray,
# no server, no window. Only when the user has it enabled - never turn it
# on for them here. Which launcher: the one the user actually opens - the
# ~/Applications copy on macOS when this installer owns it (a stable path
# across upgrades), else the one behind `current`. Ownership means
# apps_copy_is_ours - the SIBLING marker; testing only the legacy inner
# marker here made every current-installer install re-point the login item
# at the versioned folder instead, exactly what this step exists to avoid.
launcher="$ROOT/current/$launcher_rel"
if [ "$platform" = darwin ] && apps_copy_is_ours \
&& [ -x "$HOME/Applications/mStream.app/Contents/MacOS/mStream" ]; then
launcher="$HOME/Applications/mStream.app/Contents/MacOS/mStream"
fi
if [ -x "$launcher" ] && [ -n "$installed_fresh" ]; then
if [ "$("$launcher" --autostart=status 2>/dev/null)" = enabled ]; then
if login_item_is_ours; then
"$launcher" --autostart=enable >/dev/null 2>&1 \
&& echo " login item re-pointed at $version" \
|| echo " note: could not re-point the login item - open mStream once to fix it" >&2
else
# Enabled, but for a copy we don't manage (hand-extracted, a dev
# checkout). Its owner decides; the launcher's own first-run logic
# takes over the moment they open THIS copy and toggle it.
echo " note: the login item points at $(login_item_target) (not managed by this script) - left as is;" >&2
echo " open the new mStream and toggle 'Start at login' to move it" >&2
fi
fi
fi
if [ -n "$installed_fresh" ]; then
echo "installed mStream $version to $ROOT/$bundle"
fi
[ -n "$desktop_note" ] && echo " $desktop_note"
# An instance running from somewhere else stays on its own version until
# it is restarted - the script never kills a user's server. Say so, with
# the exact path, instead of letting "installed" imply "upgraded".
if [ -n "$running_from" ]; then
echo " NOTE: mStream is currently running from $running_from" >&2
echo " it keeps running that version until you Quit it (tray menu) and start" >&2
echo " the new one - the app menu / Start Menu entry now points at $version." >&2
fi
# Report the exec probe (taken BEFORE the `current` flip above). An empty
# result down here means a first install or a same-version re-run of a
# binary that cannot exec - nothing working was displaced, so this stays a
# diagnostic with the fix, exactly as before. Known case: the musl bundle
# (Bun's musl build) links libstdc++/libgcc dynamically, and a minimal
# Alpine has neither (measured on alpine:3.20 - a wall of "Error
# relocating" from the loader).
if [ -n "$new_server_v" ]; then
echo " mstream-server $new_server_v runs"
else
err=$("$server" -V 2>&1 | head -3)
case "$err" in
*"libstdc++"*|*"libgcc"*|*"Error relocating"*|*"Error loading shared library"*)
echo " NOTE: the server needs the C++ runtime, which this system lacks:" >&2
if command -v apk >/dev/null 2>&1; then
echo " apk add libstdc++ libgcc" >&2
else
echo " install libstdc++ / libgcc via your package manager" >&2
fi
;;
*)
echo " NOTE: the server did not run - see $ROOT/current/README.txt and try: $server" >&2
;;
esac
fi
echo " headless: mstream-server (data lives in the mStream data home, not the app folder)"
case ":$PATH:" in
*":$BIN_DIR:"*) ;;
*) echo " note: $BIN_DIR is not on your PATH" ;;
esac
# Old versions are left in place on purpose (rollback = re-point `current`);
# say so once there is more than one, so they don't accumulate unnoticed.
# The wording matters: a version that is still RUNNING must not be deleted
# (its server would lose its UI mid-flight and Restart would fail), and the
# login item is re-pointed only when this run installed something new.
count=$(ls -d "$ROOT"/mStream-*-"$key" 2>/dev/null | wc -l | tr -d ' ')
if [ "$count" -gt 1 ]; then
echo " older versions kept under $ROOT - once mStream is not running from one, it is safe to delete"
fi
}
main "$@"