#!/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 "$@"