#!/bin/sh # Demiurge installer for macOS and Linux (REQ-DIST-002, ADR-018, doc/08-distribution.md ยง2). # # curl -fsSL https://get.demiurge.xnovainternational.com | sh # curl -fsSL https://get.demiurge.xnovainternational.com | sh -s -- --channel stable # curl -fsSL https://get.demiurge.xnovainternational.com | sh -s -- --version 0.2.0 --no-path --prefix /opt/demiurge --silent # curl -fsSL https://get.demiurge.xnovainternational.com | sh -s -- stable (or a version: 0.2.0) # # Steps: detect OS and architecture (Rosetta 2 reports x86_64 but installs the arm64 build; musl # libc selects the -musl target), fetch the channel manifest (or the manifest of --version), download # the gzip artifact, verify its SHA-256 and the SHA-256 of the decompressed binary against the # manifest, then hand over to `demiurge install` from the verified binary (which verifies the signed # manifest with its embedded release key, places the version under ~/.demiurge and sets up PATH), # run `demiurge doctor` and print how to start. The origin is anonymous: no token, no install id. # # Environment: DEMIURGE_INSTALL_BASE_URL (origin, default below), DEMIURGE_INSTALL_TARGET (force a # target such as linux-x64; for tests and unusual systems), DEMIURGE_INSTALL_SILENT=1 (as --silent). set -eu DEFAULT_BASE_URL="https://get.demiurge.xnovainternational.com" say() { if [ "$silent" != 1 ]; then printf '%s\n' "$*"; fi } fail() { printf 'demiurge installer: %s\n' "$*" >&2 exit 1 } usage() { cat <<'EOF' Usage: install.sh [--channel latest|stable|beta] [--version x.y.z] [--no-path] [--prefix ] [--silent] install.sh [latest|stable|beta|x.y.z] EOF } have() { command -v "$1" >/dev/null 2>&1 } download() { # download if have curl; then case "$1" in https://*) curl -fsSL --proto '=https' --proto-redir '=https' --retry 3 -o "$2" "$1" || return 1 ;; *) curl -fsSL --retry 3 -o "$2" "$1" || return 1 ;; esac elif have wget; then wget -q -O "$2" "$1" || return 1 else fail "curl or wget is required" fi } sha256_of() { if have sha256sum; then sha256sum "$1" | cut -d ' ' -f 1 elif have shasum; then shasum -a 256 "$1" | cut -d ' ' -f 1 elif have openssl; then openssl dgst -sha256 -r "$1" | cut -d ' ' -f 1 else fail "sha256sum, shasum or openssl is required to verify the download" fi } is_musl() { for loader in /lib/ld-musl-*; do if [ -e "$loader" ]; then return 0; fi done if have ldd && ldd --version 2>&1 | grep -qi musl; then return 0; fi return 1 } detect_target() { if [ -n "${DEMIURGE_INSTALL_TARGET:-}" ]; then printf '%s\n' "$DEMIURGE_INSTALL_TARGET" return fi os=$(uname -s) arch=$(uname -m) case "$os" in Darwin) os=darwin ;; Linux) os=linux ;; MINGW* | MSYS* | CYGWIN*) fail "on Windows use PowerShell: irm $base_url/win | iex" ;; *) fail "unsupported operating system $os" ;; esac case "$arch" in x86_64 | amd64) arch=x64 ;; arm64 | aarch64) arch=arm64 ;; *) fail "unsupported architecture $arch" ;; esac # A shell running under Rosetta 2 reports x86_64; the native arm64 build is the right one. if [ "$os" = darwin ] && [ "$arch" = x64 ] && [ "$(sysctl -n sysctl.proc_translated 2>/dev/null || true)" = 1 ]; then arch=arm64 fi if [ "$os" = linux ] && is_musl; then printf 'linux-%s-musl\n' "$arch" return fi printf '%s-%s\n' "$os" "$arch" } # manifest_field : top-level string field of the canonical manifest JSON. manifest_field() { sed -n "s/^ \"$2\": \"\([^\"]*\)\",\{0,1\}$/\1/p" "$1" | head -n 1 } # target_field : string field inside "targets" -> "". target_field() { awk -v target="\"$2\": {" -v field="\"$3\":" ' index($0, target) == 5 { inside = 1; next } inside && /^ }/ { exit } inside && index($0, field) == 7 { value = substr($0, 7 + length(field)) gsub(/^ *"|",?$/, "", value) print value exit } ' "$1" } main() { channel=latest version="" no_path=0 prefix="" silent="${DEMIURGE_INSTALL_SILENT:-0}" while [ $# -gt 0 ]; do case "$1" in --channel) [ $# -ge 2 ] || fail "--channel needs a value"; channel=$2; shift 2 ;; --channel=*) channel=${1#*=}; shift ;; --version) [ $# -ge 2 ] || fail "--version needs a value"; version=$2; shift 2 ;; --version=*) version=${1#*=}; shift ;; --prefix) [ $# -ge 2 ] || fail "--prefix needs a value"; prefix=$2; shift 2 ;; --prefix=*) prefix=${1#*=}; shift ;; --no-path) no_path=1; shift ;; --silent | -q) silent=1; shift ;; -h | --help) usage; exit 0 ;; latest | stable | beta) channel=$1; shift ;; [0-9]*) version=$1; shift ;; *) usage >&2; fail "unknown argument $1" ;; esac done version=${version#v} case "$channel" in latest | stable | beta) ;; *) fail "unknown channel $channel (latest, stable, beta)" ;; esac if [ -n "$version" ] && ! printf '%s' "$version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then fail "invalid version $version" fi base_url=${DEMIURGE_INSTALL_BASE_URL:-$DEFAULT_BASE_URL} base_url=${base_url%/} target=$(detect_target) || exit 1 tmp=$(mktemp -d 2>/dev/null || mktemp -d -t demiurge-install) trap 'rm -rf "$tmp"' EXIT trap 'exit 130' INT TERM if [ -n "$version" ]; then manifest_url="$base_url/releases/$version/manifest.json" else manifest_url="$base_url/channels/$channel.json" fi download "$manifest_url" "$tmp/manifest.json" || fail "could not download $manifest_url" release=$(manifest_field "$tmp/manifest.json" version) [ -n "$release" ] || fail "$manifest_url is not a Demiurge release manifest" if [ -n "$version" ] && [ "$release" != "$version" ]; then fail "$manifest_url names version $release, not $version"; fi path=$(target_field "$tmp/manifest.json" "$target" path) sha256=$(target_field "$tmp/manifest.json" "$target" sha256) binary_sha256=$(target_field "$tmp/manifest.json" "$target" binarySha256) if [ -z "$path" ] || [ -z "$sha256" ] || [ -z "$binary_sha256" ]; then case "$target" in *-musl) fail "Demiurge $release has no build for $target (musl libc) yet; use a glibc-based system or image" ;; *) fail "Demiurge $release has no build for $target" ;; esac fi case "$path" in releases/"$release"/"$target"/*.gz) ;; *) fail "unexpected artifact path $path in the manifest" ;; esac say "Downloading Demiurge $release ($target)..." download "$base_url/$path" "$tmp/demiurge.gz" || fail "could not download $base_url/$path" actual=$(sha256_of "$tmp/demiurge.gz") [ "$actual" = "$sha256" ] || fail "checksum mismatch for $path (expected $sha256, got $actual); nothing was installed" gzip -dc "$tmp/demiurge.gz" >"$tmp/demiurge" || fail "could not decompress $path" actual=$(sha256_of "$tmp/demiurge") [ "$actual" = "$binary_sha256" ] || fail "binary checksum mismatch (expected $binary_sha256, got $actual); nothing was installed" chmod 755 "$tmp/demiurge" say "Verified SHA-256 $sha256" set -- install --channel "$channel" if [ -n "$version" ]; then set -- "$@" --version "$version"; fi if [ "$no_path" = 1 ]; then set -- "$@" --no-path; fi if [ -n "$prefix" ]; then set -- "$@" --prefix "$prefix"; fi # `demiurge install` defaults its prefix to the configuration directory (DEMIURGE_HOME or ~/.demiurge). home_dir="${DEMIURGE_HOME:-$HOME/.demiurge}" launcher="${prefix:-$home_dir}/bin/demiurge" status=0 # With DEMIURGE_INSTALL_BASE_URL, the binary verifies against the same origin (it honours # DEMIURGE_UPDATE_BASE_URL on the dev profile only; elsewhere it keeps the embedded origin). # DEMIURGE_INSTALLER=1: this script runs `doctor` and prints the start hint itself. if [ -n "${DEMIURGE_INSTALL_BASE_URL:-}" ]; then output=$(DEMIURGE_INSTALLER=1 DEMIURGE_UPDATE_BASE_URL="$base_url" "$tmp/demiurge" "$@" 2>&1) || status=$? else output=$(DEMIURGE_INSTALLER=1 "$tmp/demiurge" "$@" 2>&1) || status=$? fi if [ "$status" -eq 0 ]; then if [ -n "$output" ]; then say "$output"; fi elif printf '%s' "$output" | grep -q "not available in this build"; then # This binary predates `demiurge install`: place it the way `install` would, without PATH edits. root="${prefix:-$home_dir}" mkdir -p "$root/versions/$release" "$root/bin" cp "$tmp/demiurge" "$root/versions/$release/demiurge" chmod 755 "$root/versions/$release/demiurge" ln -sfn "$root/versions/$release/demiurge" "$root/bin/demiurge" ln -sfn "$root/versions/$release/demiurge" "$root/bin/dmg" say "This Demiurge build has no 'install' command yet; installed to $root/versions/$release (launchers in $root/bin)." if [ "$no_path" != 1 ]; then say "Add it to your PATH: export PATH=\"$root/bin:\$PATH\""; fi else printf '%s\n' "$output" >&2 fail "'demiurge install' failed (exit $status)" fi if [ -x "$launcher" ]; then doctor=$launcher; else doctor="$tmp/demiurge"; fi if [ "$silent" != 1 ]; then "$doctor" doctor || true say "" say "Demiurge $release is installed. Start with: demiurge setup (then run 'demiurge' or 'dmg' in a project folder)" else "$doctor" doctor >/dev/null 2>&1 || true fi } main "$@"