#!/bin/sh # Inspect this script before running it. It installs only SyncX.app; it never # disables Gatekeeper globally and never removes library data or credentials. set -eu syncx_install() { origin='https://www.syncx-app.com' if [ "$(uname -s)" != Darwin ] || [ "$(uname -m)" != arm64 ]; then printf '%s\n' 'This installer currently supports macOS Apple Silicon. See the website for other platforms.' >&2 return 1 fi if [ "$(id -u)" = 0 ]; then printf '%s\n' 'Run this installer as your normal user, without sudo.' >&2 return 1 fi os_version=$(sw_vers -productVersion) os_major=${os_version%%.*} os_rest=${os_version#*.} os_minor=${os_rest%%.*} if [ "$os_major" -lt 13 ] || { [ "$os_major" -eq 13 ] && [ "$os_minor" -lt 3 ]; }; then printf '%s\n' 'SyncX requires macOS 13.3 or later (system WebView).' >&2 return 1 fi if pgrep -x SyncX >/dev/null 2>&1; then printf '%s\n' 'Please quit SyncX before installing or updating.' >&2 return 1 fi install_dir=${SYNCX_INSTALL_DIR:-/Applications} if [ ! -d "$install_dir" ] || [ ! -w "$install_dir" ]; then if [ -n "${SYNCX_INSTALL_DIR:-}" ]; then printf '%s\n' 'SYNCX_INSTALL_DIR must be an existing writable directory.' >&2 return 1 fi install_dir="$HOME/Applications" mkdir -p "$install_dir" fi install_dir=$(cd "$install_dir" && pwd -P) app="$install_dir/SyncX.app" if [ -L "$app" ]; then printf '%s\n' 'Refusing to replace a symlink at SyncX.app.' >&2 return 1 fi umask 077 work=$(mktemp -d "$install_dir/.syncx-install.XXXXXXXX") # work is created by mktemp inside the resolved installation directory. trap 'if [ -n "${backup:-}" ] && [ ! -e "$app" ]; then mv "$backup" "$app"; fi; if [ -n "${work:-}" ] && [ -d "$work" ]; then /bin/rm -rf -- "$work"; fi' EXIT trap 'exit 130' INT trap 'exit 143' TERM printf '%s\n' 'Checking the latest SyncX release...' manifest=$(curl --fail --silent --show-error --proto '=https' --tlsv1.2 --connect-timeout 15 --max-time 60 \ "$origin/api/releases/latest?target=macos-arm64&format=tsv") IFS="$(printf '\t')" read -r version checksum filename download_url expected_size <&2; return 1 ;; esac case "$filename" in SyncX-*-macos-arm64.tar.gz) ;; *) printf '%s\n' 'Unexpected artifact format.' >&2; return 1 ;; esac case "$checksum" in *[!a-f0-9]*|'') return 1 ;; esac [ "${#checksum}" -eq 64 ] || return 1 case "$expected_size" in *[!0-9]*|'') return 1 ;; esac archive="$work/release.tar.gz" printf 'Downloading SyncX %s...\n' "$version" curl --fail --show-error --proto '=https' --tlsv1.2 --connect-timeout 15 --max-time 1800 \ --output "$archive" "$download_url" actual_size=$(wc -c < "$archive" | tr -d ' ') [ "$actual_size" = "$expected_size" ] || { printf '%s\n' 'Download size mismatch.' >&2; return 1; } actual_checksum=$(shasum -a 256 "$archive" | awk '{print $1}') [ "$actual_checksum" = "$checksum" ] || { printf '%s\n' 'SHA-256 verification failed.' >&2; return 1; } # The Wails bundle contains only regular files and directories. Reject archive # links and paths outside the one application before extraction. tar -tzf "$archive" > "$work/members" awk 'BEGIN { ok=1 } /^SyncX\.app\/$/ { next } !/^SyncX\.app\// || /(^|\/)\.\.(\/|$)/ || /\\/ { ok=0 } END { exit !ok }' "$work/members" || return 1 tar -tvzf "$archive" > "$work/types" awk 'substr($0,1,1) != "-" && substr($0,1,1) != "d" { bad=1 } END { exit bad }' "$work/types" || return 1 tar -xzf "$archive" -C "$work" [ -x "$work/SyncX.app/Contents/MacOS/SyncX" ] || return 1 /usr/bin/codesign --verify --deep --strict "$work/SyncX.app" bundle_id=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$work/SyncX.app/Contents/Info.plist") [ "$bundle_id" = 'com.syncx.app' ] || { printf '%s\n' 'Unexpected application identity.' >&2; return 1; } bundle_version=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$work/SyncX.app/Contents/Info.plist") [ "$bundle_version" = "$version" ] || { printf '%s\n' 'Application version does not match release metadata.' >&2; return 1; } printf '%s\n' 'This build is not Apple-notarized. Removing quarantine ONLY from SyncX.app.' /usr/bin/xattr -dr com.apple.quarantine "$work/SyncX.app" 2>/dev/null || true if /usr/bin/xattr -lr "$work/SyncX.app" | grep -q 'com.apple.quarantine'; then printf '%s\n' 'Could not remove SyncX quarantine; installation stopped.' >&2 return 1 fi backup='' if [ -e "$app" ]; then backup="$install_dir/SyncX.app.backup-$(date +%Y%m%d%H%M%S)-$$" mv "$app" "$backup" fi if ! mv "$work/SyncX.app" "$app"; then if [ -n "$backup" ]; then mv "$backup" "$app"; fi printf '%s\n' 'Install failed; the previous application was restored.' >&2 return 1 fi printf 'Installed SyncX %s at %s\n' "$version" "$app" if [ -n "$backup" ]; then printf 'Previous app retained at %s\n' "$backup"; fi printf '%s\n' 'Your SyncX library and account data were not removed.' # SYNCX_NO_OPEN=1 installs without launching, e.g. for scripted setups. if [ "${SYNCX_NO_OPEN:-}" = 1 ] || ! /usr/bin/open "$app"; then printf '%s\n' 'Open SyncX from Applications.' else printf '%s\n' 'Opening SyncX...' fi } # Wrapping execution in a function prevents a truncated curl response from # executing a half-downloaded installation script. syncx_install