#!/usr/bin/env bash # SynapseOS Cloud latest. Run as your normal Linux user, not through sudo. set -Eeuo pipefail MANIFEST_URL=https://synapseos.ru/latest/cloud/qcow2/manifest.json BASE_DIRECTORY="${XDG_DATA_HOME:-$HOME/.local/share}/synapseos" HEADLESS=0 DOWNLOAD_ONLY=0 fail() { printf 'Error: %s\n' "$*" >&2; exit 1; } while (($#)); do case "$1" in --headless) HEADLESS=1; shift ;; --download-only) DOWNLOAD_ONLY=1; shift ;; --directory) (($# >= 2)) || fail '--directory needs a path'; BASE_DIRECTORY=$2; shift 2 ;; --help) printf '%s\n' 'Usage: bash synapse-start.sh [--headless] [--download-only] [--directory PATH]' 'Default: install missing tools, download and verify the latest Cloud release, start QEMU.'; exit 0 ;; *) fail "Unknown option: $1" ;; esac done [[ $(uname -s) == Linux ]] || fail 'This launcher requires Linux.' ((EUID != 0)) || fail 'Run without sudo. Only the package installation needs administrator rights.' if ! command -v curl >/dev/null || ! command -v sha256sum >/dev/null || ! command -v qemu-system-x86_64 >/dev/null || ! command -v python3 >/dev/null || ! command -v gzip >/dev/null; then command -v sudo >/dev/null || fail 'Install QEMU, curl, coreutils, Python 3 and gzip with your package manager first.' printf '%s\n' 'Installing missing tools with your distribution package manager (sudo may ask for your password).' if command -v apt-get >/dev/null; then sudo apt-get update sudo apt-get install qemu-system-x86 qemu-utils curl ca-certificates coreutils python3 gzip elif command -v dnf >/dev/null; then sudo dnf install qemu-system-x86 qemu-img curl ca-certificates coreutils python3 gzip elif command -v pacman >/dev/null; then sudo pacman -S --needed qemu-system-x86 qemu-img curl ca-certificates coreutils python gzip else fail 'Supported automatic installation: Debian/Ubuntu, Fedora, Arch. Install QEMU, curl, coreutils, Python 3 and gzip manually on other distributions.' fi fi command -v qemu-system-x86_64 >/dev/null || fail 'QEMU installation did not provide qemu-system-x86_64.' command -v curl >/dev/null || fail 'curl is missing.' command -v sha256sum >/dev/null || fail 'sha256sum is missing.' command -v python3 >/dev/null || fail 'python3 is missing.' command -v gzip >/dev/null || fail 'gzip is missing.' MANIFEST=$(mktemp) trap 'rm -f -- "$MANIFEST"' EXIT curl --fail --silent --show-error --location --retry 3 --connect-timeout 20 --max-time 120 --proto '=https' --proto-redir '=https' --output "$MANIFEST" "$MANIFEST_URL" METADATA=$(python3 - "$MANIFEST" <<'PYTHON' import json,re,sys m=json.load(open(sys.argv[1])) if not re.fullmatch(r"\d+\.\d+\.\d+",m['version']): raise ValueError('Invalid release version') expected="SynapseOS-"+m['version']+"-cloud-x86_64.qcow2" if m['filename']!=expected or m['edition']!='cloud' or m['format']!='qcow2': raise ValueError('Unexpected Cloud image') selected=m.get('compressed') or m for item in (m,selected): if not re.fullmatch(r"[0-9a-f]{64}",item['sha256']): raise ValueError('Invalid SHA-256') if not re.fullmatch(r"/dl/[0-9a-f]{32}\.qcow2(?:/gzip)?",item['url']): raise ValueError('Unexpected download URL') print(m['version']); print(m['sha256']); print('https://synapseos.ru'+selected['url']) print(selected['sha256']); print('gzip' if m.get('compressed') else 'plain') PYTHON ) || fail 'Invalid latest manifest; refusing to start.' mapfile -t FIELDS <<< "$METADATA" VERSION=${FIELDS[0]} SHA256=${FIELDS[1]} IMAGE_URL=${FIELDS[2]} TRANSFER_SHA256=${FIELDS[3]} TRANSFER_FORMAT=${FIELDS[4]} DIRECTORY="$BASE_DIRECTORY/$VERSION/cloud" mkdir -p -- "$DIRECTORY" cd -- "$DIRECTORY" DIRECTORY=$PWD mkdir .launcher-lock 2>/dev/null || fail "Another launcher is using $DIRECTORY. If it was interrupted, remove .launcher-lock after checking that QEMU is stopped." TEMP_FILE='' PACKED_FILE='' cleanup() { rm -f -- "$MANIFEST"; [[ -z $TEMP_FILE ]] || rm -f -- "$TEMP_FILE"; [[ -z $PACKED_FILE ]] || rm -f -- "$PACKED_FILE"; rmdir .launcher-lock; } trap cleanup EXIT trap 'exit 130' INT trap 'exit 143' TERM IMAGE=SynapseOS-$VERSION-cloud-x86_64.qcow2 verified() { [[ -f $1 ]] && [[ $(sha256sum -- "$1" | cut -d ' ' -f 1) == "$SHA256" ]]; } if ! verified "$IMAGE"; then printf 'Downloading SynapseOS Cloud %s...\n' "$VERSION" TEMP_FILE=$(mktemp ./image-download.XXXXXX) curl --fail --location --retry 3 --connect-timeout 20 --proto '=https' --proto-redir '=https' --tlsv1.2 --output "$TEMP_FILE" "$IMAGE_URL" [[ $(sha256sum -- "$TEMP_FILE" | cut -d ' ' -f 1) == "$TRANSFER_SHA256" ]] || fail 'SHA-256 mismatch on downloaded data.' if [[ $TRANSFER_FORMAT == gzip ]]; then PACKED_FILE=$TEMP_FILE TEMP_FILE=$(mktemp ./image-unpacked.XXXXXX) gzip -dc -- "$PACKED_FILE" > "$TEMP_FILE" rm -f -- "$PACKED_FILE" PACKED_FILE='' fi verified "$TEMP_FILE" || fail 'SHA-256 mismatch. The downloaded image will not be started.' mv -f -- "$TEMP_FILE" "$IMAGE" TEMP_FILE='' fi printf 'SHA-256 verified. Image: %s/%s\n' "$DIRECTORY" "$IMAGE" if [[ ! -e disk.qcow2 ]]; then TEMP_FILE=$(mktemp ./working-disk.XXXXXX) cp -- "$IMAGE" "$TEMP_FILE" mv -- "$TEMP_FILE" disk.qcow2 TEMP_FILE='' fi [[ -f disk.qcow2 && ! -L disk.qcow2 ]] || fail 'disk.qcow2 must be a regular working disk.' ((DOWNLOAD_ONLY == 0)) || exit 0 ARGS=(-machine q35 -accel tcg -cpu max -m 512 -smp 2 -drive file=disk.qcow2,format=qcow2,if=virtio -netdev user,id=net0 -device virtio-net-pci,netdev=net0 -serial mon:stdio) if ((HEADLESS)) || [[ -z ${DISPLAY:-}${WAYLAND_DISPLAY:-} ]]; then ARGS+=(-display none); fi printf '%s\n' 'Starting QEMU (512 MiB RAM, 2 virtual CPUs). The working disk is reused; guest file persistence is not yet verified.' 'Before closing: run sync in the guest. Exit QEMU: close its window, or Ctrl+A then X in the terminal.' qemu-system-x86_64 "${ARGS[@]}"