#!/bin/sh # shellcheck shell=sh # # microManager installer for macOS and Linux (Windows: irm https://micromanager.win/install.ps1 | iex). # # curl -fsSL https://micromanager.win/install.sh | sh # # On a Mac, in order: # 1. picks the Apple Silicon (arm64) or Intel (x64) build, # 2. reads the release manifest (latest-mac.yml) and downloads the matching zip, # 3. checks the zip's SHA-512 against the manifest, # 4. checks the app's code signature and Apple developer team, # 5. installs it as /Applications/microManager.app (asking first if a copy is running), # keeping the previous copy until the new one is in place, # 6. opens it. # It uses sudo only when your user cannot write to the install folder. # # On Linux (x64), in order: # 1. reads the release manifest (latest-linux.yml) and downloads the AppImage, # 2. checks its SHA-512 against the manifest, # 3. installs it as ~/.local/bin/micromanager, replacing any previous copy in one step, # 4. adds microManager to your applications menu with its icon (under ~/.local/share), # 5. opens it when there is a display. # It never uses sudo. The app updates itself in place from then on. # # Settings (environment variables): # MICROMANAGER_INSTALL_DIR=~/Applications install somewhere else (default /Applications on a Mac, # ~/.local/bin on Linux) # MICROMANAGER_NO_OPEN=1 do not open microManager when done # MICROMANAGER_ASSUME_YES=1 Mac: quit and replace a running copy without asking # MICROMANAGER_BASE_URL=https://... release server (default https://micromanager.win) # MICROMANAGER_ALLOW_UNSIGNED=1 Mac: skip the signature check (for testing builds only) # # Everything runs inside main(), called on the last line, so a partly downloaded copy of this # script does nothing. set -eu # pipefail where the shell supports it (macOS /bin/sh is bash, which does; POSIX added it in 2024). # shellcheck disable=SC3040 if (set -o pipefail) 2>/dev/null; then set -o pipefail; fi APP_NAME="microManager" EXPECTED_TEAM_ID="5SQZ2AF63B" # The Linux executable, desktop entry and icon name (electron-builder's linux.executableName). LINUX_NAME="micromanager" say() { printf '%s\n' "microManager: $*" } fail() { printf '%s\n' "microManager: error: $*" >&2 exit 1 } need() { command -v "$1" >/dev/null 2>&1 || fail "this installer needs '$1', which was not found." } detect_mac_arch() { machine=$(uname -m) case "$machine" in arm64) echo arm64 ;; x86_64) # An x86_64 shell on Apple Silicon is running under Rosetta: install the native build. if [ "$(sysctl -in sysctl.proc_translated 2>/dev/null || echo 0)" = "1" ]; then echo arm64 else echo x64 fi ;; *) fail "unsupported CPU architecture: $machine" ;; esac } # Prints " " for every file listed in one of electron-builder's update manifests # (latest-mac.yml, latest-linux.yml). manifest_files() { awk -v q="'" ' function clean(value) { sub(/^[^:]*:[ \t]*/, "", value) gsub(/"/, "", value) gsub(q, "", value) gsub(/[ \t\r]+$/, "", value) return value } /^[ \t]*-[ \t]*url:/ { url = clean($0); next } /^[ \t]*sha512:/ && url != "" { print url " " clean($0); url = ""; next } ' "$1" } # Prints " " for the zip that matches the architecture in latest-mac.yml. Falls back # to a universal build when there is no build for that architecture. find_mac_zip() { manifest_files "$1" | awk -v arch="$2" ' $1 ~ /\.zip$/ { is_arm = ($1 ~ /arm64/) is_universal = ($1 ~ /universal/) if ((arch == "arm64" && is_arm) || (arch == "x64" && !is_arm && !is_universal)) { print found = 1 exit } if (is_universal && universal == "") universal = $0 } END { if (!found && universal != "") print universal } ' } # Prints " " for the AppImage in latest-linux.yml. find_appimage() { manifest_files "$1" | awk '$1 ~ /\.AppImage$/ { print; exit }' } manifest_version() { awk -v q="'" '/^version:/ { v = $0 sub(/^version:[ \t]*/, "", v) gsub(/"/, "", v) gsub(q, "", v) gsub(/[ \t\r]+$/, "", v) print v exit }' "$1" } download() { # $1 url, $2 output file, $3 "quiet" or "progress" if [ "$secure_transport" = 1 ]; then set -- "$1" "$2" "$3" --proto '=https' --tlsv1.2 else set -- "$1" "$2" "$3" fi url=$1 output=$2 style=$3 shift 3 if [ "$style" = progress ] && [ -t 2 ]; then curl -fL --retry 3 --progress-bar "$@" -o "$output" "$url" else curl -fsSL --retry 3 "$@" -o "$output" "$url" fi } # Downloads and reads the manifest of one release folder: sets releases_url, manifest and version. read_manifest() { # $1 release folder (mac, linux), $2 manifest file name releases_url="$base_url/releases/$1" manifest="$work_dir/$2" download "$releases_url/$2" "$manifest" quiet || fail "could not download $releases_url/$2" version=$(manifest_version "$manifest") [ -n "$version" ] || fail "the release manifest has no version." } # Downloads the file named in a " " manifest entry and checks its SHA-512 (base64, # as electron-builder writes it). Sets downloaded to its path. download_verified() { # $1 manifest entry file_name=${1%% *} expected_sha=${1#* } case "$file_name" in */* | .* | "") fail "unexpected file name in the release manifest: $file_name" ;; esac [ -n "$expected_sha" ] && [ "$expected_sha" != "$file_name" ] || fail "the release manifest has no checksum for $file_name." say "downloading $APP_NAME $version" downloaded="$work_dir/$file_name" download "$releases_url/$file_name" "$downloaded" progress || fail "could not download $releases_url/$file_name" actual_sha=$(openssl dgst -sha512 -binary "$downloaded" | openssl base64 -A) if [ "$actual_sha" != "$expected_sha" ]; then fail "the download's SHA-512 does not match the release manifest; nothing was installed." fi say "checksum verified" } run_privileged() { if [ "$use_sudo" = 1 ]; then sudo "$@" else "$@" fi } # Removes this run's temporary files, and a half-copied install if it stopped midway. cleanup() { if [ -n "${work_dir:-}" ]; then rm -rf "$work_dir"; fi if [ -n "${staging:-}" ] && [ -e "$staging" ]; then run_privileged rm -rf "$staging" || true; fi } app_is_running() { pgrep -x "$1" >/dev/null 2>&1 } confirm_replacing_running_app() { if [ "${MICROMANAGER_ASSUME_YES:-0}" = 1 ]; then answer=y elif (exec /dev/null; then printf '%s' "microManager is running. Quit it and install the new version? [y/N] " >/dev/tty read -r answer /dev/null 2>&1 || true waited=0 while app_is_running "$APP_NAME"; do if [ "$waited" -ge 20 ]; then fail "microManager did not quit within 20 seconds; quit it yourself and run the installer again." fi sleep 1 waited=$((waited + 1)) done } install_mac() { need ditto install_dir=${MICROMANAGER_INSTALL_DIR:-/Applications} if [ ! -d "$install_dir" ]; then mkdir -p "$install_dir" 2>/dev/null || fail "could not create $install_dir" fi if [ ! -w "$install_dir" ]; then need sudo say "$install_dir is not writable by $(id -un), so sudo will ask for your password to install there." use_sudo=1 fi target="$install_dir/$APP_NAME.app" staging="$install_dir/.$APP_NAME.app.installing.$$" backup="$install_dir/.$APP_NAME.app.previous.$$" arch=$(detect_mac_arch) say "checking the newest release for $arch Macs" read_manifest mac latest-mac.yml entry=$(find_mac_zip "$manifest" "$arch") [ -n "$entry" ] || fail "version $version has no build for $arch Macs yet. See https://micromanager.win/install#macos" download_verified "$entry" extract_dir="$work_dir/extracted" mkdir -p "$extract_dir" ditto -x -k "$downloaded" "$extract_dir" || fail "could not unpack $file_name" new_app="$extract_dir/$APP_NAME.app" [ -d "$new_app" ] || fail "$file_name does not contain $APP_NAME.app" if [ "${MICROMANAGER_ALLOW_UNSIGNED:-0}" = 1 ]; then say "skipping the signature check because MICROMANAGER_ALLOW_UNSIGNED=1" else need codesign codesign --verify --deep --strict "$new_app" >/dev/null 2>&1 || fail "the app's code signature is not valid; nothing was installed." team=$(codesign -dv "$new_app" 2>&1 | sed -n 's/^TeamIdentifier=//p') [ "$team" = "$EXPECTED_TEAM_ID" ] || fail "the app is signed by team '$team', not microManager's team $EXPECTED_TEAM_ID; nothing was installed." say "signature verified" fi if app_is_running "$APP_NAME"; then confirm_replacing_running_app fi # Copy the new app next to the old one, swap them, and only then delete the old one. run_privileged rm -rf "$staging" run_privileged ditto "$new_app" "$staging" || fail "could not copy the app into $install_dir" if [ -e "$target" ]; then run_privileged mv "$target" "$backup" || { run_privileged rm -rf "$staging" fail "could not move the existing $target aside; it is unchanged." } fi if ! run_privileged mv "$staging" "$target"; then if [ -e "$backup" ]; then run_privileged mv "$backup" "$target" || true; fi run_privileged rm -rf "$staging" fail "could not move the new app into place; the previous version was put back." fi run_privileged rm -rf "$backup" say "installed $APP_NAME $version at $target" if [ "${MICROMANAGER_NO_OPEN:-0}" = 1 ]; then say "done. Open it from $install_dir when you are ready." else open "$target" || say "installed, but could not open it; open it from $install_dir." fi } # A path as one quoted argument of a desktop entry's Exec key: \ " ` and $ are escaped for the # quoting and then the backslashes again for the string value, and % is doubled (freedesktop spec). desktop_exec_argument() { printf '"%s"' "$(printf '%s' "$1" | sed -e 's/\\/\\\\\\\\/g' -e 's/["`$]/\\\\&/g' -e 's/%/%%/g')" } # Adds microManager to the applications menu: the desktop entry and icons the AppImage carries, # with Exec pointing at the installed file. A failure here leaves a working install, so it only warns. integrate_linux_desktop() { data_home=${XDG_DATA_HOME:-$HOME/.local/share} extract_dir="$work_dir/appimage" mkdir -p "$extract_dir" # The AppImage runtime extracts matching files into ./squashfs-root without mounting anything. if ! (cd "$extract_dir" && "$target" --appimage-extract "$LINUX_NAME.desktop" && "$target" --appimage-extract "usr/share/icons/hicolor/*/apps/$LINUX_NAME.png") >/dev/null 2>&1; then say "installed, but could not read the desktop entry from the AppImage; it is not in your applications menu." return 0 fi entry_file="$extract_dir/squashfs-root/$LINUX_NAME.desktop" if [ ! -f "$entry_file" ]; then say "installed, but the AppImage has no desktop entry; it is not in your applications menu." return 0 fi icons_from="$extract_dir/squashfs-root/usr/share/icons/hicolor" if [ -d "$icons_from" ]; then for icon in "$icons_from"/*/apps/"$LINUX_NAME.png"; do [ -f "$icon" ] || continue size_dir=${icon#"$icons_from"/} size_dir=${size_dir%%/*} mkdir -p "$data_home/icons/hicolor/$size_dir/apps" cp "$icon" "$data_home/icons/hicolor/$size_dir/apps/$LINUX_NAME.png" done fi # AppRun inside the AppImage adds --no-sandbox by itself when the system has no user namespaces # for Chromium's sandbox, so the entry starts the AppImage with no flags of its own. # Through the environment: awk -v would undo the backslash escaping. EXEC_LINE="Exec=$(desktop_exec_argument "$target") %U" awk ' /^Exec=/ { print ENVIRON["EXEC_LINE"]; next } /^TryExec=/ { next } { print } ' "$entry_file" >"$work_dir/$LINUX_NAME.desktop" mkdir -p "$data_home/applications" mv -f "$work_dir/$LINUX_NAME.desktop" "$data_home/applications/$LINUX_NAME.desktop" if command -v update-desktop-database >/dev/null 2>&1; then update-desktop-database "$data_home/applications" >/dev/null 2>&1 || true fi if command -v gtk-update-icon-cache >/dev/null 2>&1; then gtk-update-icon-cache -q -t -f "$data_home/icons/hicolor" >/dev/null 2>&1 || true fi say "added microManager to your applications menu" } install_linux() { machine=$(uname -m) case "$machine" in x86_64 | amd64) ;; *) fail "there is no microManager build for $machine Linux yet (x64 only). See https://micromanager.win/install#linux" ;; esac install_dir=${MICROMANAGER_INSTALL_DIR:-$HOME/.local/bin} mkdir -p "$install_dir" 2>/dev/null || fail "could not create $install_dir" [ -w "$install_dir" ] || fail "$install_dir is not writable by $(id -un); choose another MICROMANAGER_INSTALL_DIR (on Linux it installs for your user only)." target="$install_dir/$LINUX_NAME" case "$target" in *[[:cntrl:]]*) fail "the install path contains control characters; choose another MICROMANAGER_INSTALL_DIR." ;; esac staging="$install_dir/.$LINUX_NAME.installing.$$" say "checking the newest release for Linux" read_manifest linux latest-linux.yml entry=$(find_appimage "$manifest") [ -n "$entry" ] || fail "version $version has no AppImage yet. See https://micromanager.win/install#linux" download_verified "$entry" # Copy next to the target and rename over it: a running copy keeps its file, and the target is # never half-written. A name without the version is what the app's updater replaces in place. cp "$downloaded" "$staging" || fail "could not copy the AppImage into $install_dir" chmod 755 "$staging" mv -f "$staging" "$target" || fail "could not move the AppImage into place at $target" say "installed $APP_NAME $version at $target" integrate_linux_desktop case ":${PATH:-}:" in *":$install_dir:"*) ;; *) say "$install_dir is not on your PATH; add it to run $LINUX_NAME from a terminal." ;; esac if app_is_running "$LINUX_NAME"; then say "microManager is running; quit and reopen it to use version $version." elif [ ! -e /dev/fuse ] || ! { command -v fusermount3 || command -v fusermount; } >/dev/null 2>&1; then # The AppImage mounts itself with FUSE; without it, it can still unpack itself each time it starts. say "this system has no FUSE, which the AppImage uses to start: install your distribution's fuse3 package, or run $target --appimage-extract-and-run" elif [ "${MICROMANAGER_NO_OPEN:-0}" = 1 ] || [ -z "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ]; then say "done. Open it from your applications menu or run $target." else nohup "$target" >/dev/null 2>&1 & fi } main() { case "$(uname -s)" in Darwin) platform=mac ;; Linux) platform=linux ;; MINGW* | MSYS* | CYGWIN*) fail "on Windows, run this in PowerShell instead: irm https://micromanager.win/install.ps1 | iex" ;; *) fail "this installer runs on macOS and Linux; see https://micromanager.win/install for other systems." ;; esac for tool in curl awk openssl; do need "$tool"; done base_url=${MICROMANAGER_BASE_URL:-https://micromanager.win} base_url=${base_url%/} case "$base_url" in https://*) secure_transport=1 ;; http://127.0.0.1:* | http://localhost:*) secure_transport=0 ;; *) fail "MICROMANAGER_BASE_URL must be an https:// URL." ;; esac use_sudo=0 work_dir=$(mktemp -d "${TMPDIR:-/tmp}/micromanager-install.XXXXXX") trap cleanup EXIT trap 'exit 130' INT TERM if [ "$platform" = mac ]; then install_mac else install_linux fi } main "$@"