#!/bin/sh # Store-served Developer Suite / AppClient bootstrap for Linux x64 and macOS arm64. # https://store.createrun.ai, appclient and linux-x64 are replaced by InstallerController. set -eu store='https://store.createrun.ai' product='appclient' platform='linux-x64' release_key='MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUMU+nRYCGV1E+JC+153R1B9WD3YBqJvgEHLlZvJf/N+NIUKLA1qxGxxcNCMbHkCTK1zGn7XQ3nYInXVY24lf6A==' case "$store:$product:$platform" in https://store.createrun.ai:developersuite:linux-x64|https://store.createrun.ai:appclient:linux-x64|https://store.createrun.ai:developersuite:osx-arm64|https://store.createrun.ai:appclient:osx-arm64|https://store.createrun.com:developersuite:linux-x64|https://store.createrun.com:appclient:linux-x64|https://store.createrun.com:developersuite:osx-arm64|https://store.createrun.com:appclient:osx-arm64) ;; *) echo 'Invalid Store origin, product or platform in bootstrap script.' >&2; exit 1 ;; esac case "$platform" in linux-x64) [ "$(uname -s)" = Linux ] && [ "$(uname -m)" = x86_64 ] || { echo 'This script requires Linux x64.' >&2; exit 1; } ;; osx-arm64) [ "$(uname -s)" = Darwin ] && [ "$(uname -m)" = arm64 ] || { echo 'This script requires macOS arm64.' >&2; exit 1; } ;; esac for command_name in curl python3 openssl dotnet; do command -v "$command_name" >/dev/null 2>&1 || { echo "Required command is missing: $command_name" >&2; exit 1; } done work=$(mktemp -d "${TMPDIR:-/tmp}/createrun-developer-tool.XXXXXXXX") trap 'rm -rf -- "$work"' EXIT HUP INT TERM channel="$store/installer/developer-tools/developersuite/stable/$platform" curl --fail --location --silent --show-error "$channel/latest" -o "$work/latest" version=$(tr -d '\r\n' < "$work/latest") case "$version" in ''|*[!0-9.]*) echo 'The Store returned an invalid Developer Suite version.' >&2; exit 1 ;; esac printf '%s' "$version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' || { echo 'The Store returned an invalid Developer Suite version.' >&2; exit 1; } release="$channel/releases/$version" curl --fail --location --silent --show-error "$release/manifest.json" -o "$work/manifest.json" curl --fail --location --silent --show-error "$release/manifest.json.sig" -o "$work/manifest.json.sig" python3 - "$work" "$version" "$platform" "$release_key" <<'PY' import base64, pathlib, sys work, version, platform, key = sys.argv[1:] root = pathlib.Path(work) (root / 'public.der').write_bytes(base64.b64decode(key, validate=True)) (root / 'signature.der').write_bytes(base64.b64decode((root / 'manifest.json.sig').read_text().strip(), validate=True)) PY openssl pkey -pubin -inform DER -in "$work/public.der" -out "$work/public.pem" >/dev/null 2>&1 openssl dgst -sha256 -verify "$work/public.pem" -signature "$work/signature.der" "$work/manifest.json" >/dev/null || { echo 'Developer Suite manifest signature verification failed.' >&2; exit 1; } file="developersuite-$version-$platform.tar.gz" python3 - "$work/manifest.json" "$version" "$platform" "$file" <<'PY' import json, pathlib, re, sys manifest = json.loads(pathlib.Path(sys.argv[1]).read_text()) expected = {'schema': 'createrun.developer-tool.release@1', 'product': 'developersuite', 'channel': 'stable', 'version': sys.argv[2], 'platform': sys.argv[3], 'file': sys.argv[4]} if any(manifest.get(key) != value for key, value in expected.items()): raise SystemExit('The signed Developer Suite manifest does not match this release.') if not isinstance(manifest.get('size'), int) or not 0 < manifest['size'] <= 300_000_000: raise SystemExit('The signed Developer Suite archive size is invalid.') if not re.fullmatch(r'[a-f0-9]{64}', str(manifest.get('sha256', ''))): raise SystemExit('The signed Developer Suite archive hash is invalid.') PY curl --fail --location --silent --show-error "$store/installer/developer-tools/developersuite/versions/$version/$platform/$file" -o "$work/$file" mkdir "$work/payload" python3 - "$work/manifest.json" "$work/$file" "$work/payload" "$release_key" <<'PY' import hashlib, json, pathlib, sys, tarfile manifest = json.loads(pathlib.Path(sys.argv[1]).read_text()) archive, payload, key = pathlib.Path(sys.argv[2]), pathlib.Path(sys.argv[3]), sys.argv[4] digest = hashlib.sha256() with archive.open('rb') as stream: for block in iter(lambda: stream.read(1024 * 1024), b''): digest.update(block) if archive.stat().st_size != manifest['size'] or digest.hexdigest() != manifest['sha256']: raise SystemExit('The Developer Suite archive size or SHA-256 differs from its signed manifest.') seen, total = set(), 0 with tarfile.open(archive, 'r:gz') as tar: for member in tar: name = member.name[2:] if member.name.startswith('./') else member.name parts = name.split('/') total += member.size if len(seen) >= 20_000 or total > 1_000_000_000 or not member.isfile() or not name or ':' in name or any(part in ('', '.', '..') for part in parts) or name in seen: raise SystemExit('Developer Suite archive contains an unsafe or unsupported entry.') seen.add(name) target = payload.joinpath(*parts) target.parent.mkdir(parents=True, exist_ok=True) with tar.extractfile(member) as source, target.open('xb') as destination: while True: block = source.read(1024 * 1024) if not block: break destination.write(block) if not {'Createrun.DeveloperSuite.dll', 'Installer/DeveloperSuite.Installer.dll', 'developer-tools.spki'} <= seen: raise SystemExit('Developer Suite archive lacks its installer, service or release key.') if (payload / 'developer-tools.spki').read_text().strip() != key: raise SystemExit('Developer Suite archive contains the wrong release key.') PY dotnet "$work/payload/Installer/DeveloperSuite.Installer.dll" --verify-release-key "$release_key" || { echo 'Developer Suite installer release key check failed.' >&2; exit 1; } export CREATERUN_DEVELOPER_TOOLS_STORE_URL="$store" if [ "$product" = appclient ]; then dotnet "$work/payload/Installer/DeveloperSuite.Installer.dll" --cli install-appclient else dotnet "$work/payload/Installer/DeveloperSuite.Installer.dll" --cli install fi echo "$product installation completed. Run the Store health-check guide next."