#!/bin/sh set -eu repository="chasebryan/centl" channel=${CENTL_CHANNEL:-} release_version="latest" release_base_url=${CENTL_RELEASE_BASE_URL:-} archive_path="" prefix=${CENTL_PREFIX:-} configure_path=true usage() { cat <<'EOF' Install CENTL on GNU/Linux. Usage: ./install [--channel oasis|mirage] [--version VERSION] [--release-base-url URL] [--prefix PATH] [--archive PATH] [--no-path] --channel CHANNEL install Oasis (stable) or Mirage (development) --version VERSION install a specific Oasis version or Mirage build (default: latest for the selected channel) --release-base-url URL use URL/vVERSION as a custom release directory (HTTPS or file://; requires explicit --version) --prefix PATH install below PATH (default: ~/.local) --archive PATH install and verify a local Linux release archive --no-path do not update a shell startup file when PATH needs setup --help show this help Environment: CENTL_CHANNEL same as --channel CENTL_RELEASE_BASE_URL same as --release-base-url CENTL_PREFIX same as --prefix When both channels are installed they coexist. Oasis provides the conventional centl, centl-physics, and centl-sci commands. Channel-specific launchers are: oasis-centl mirage-centl oasis-centl-physics mirage-centl-physics oasis-centl-sci mirage-centl-sci EOF } fail() { printf 'centl install: %s\n' "$1" >&2 exit 1 } shell_quote() { escaped=$(printf '%s' "$1" | sed "s/'/'\\\\''/g") printf "'%s'" "$escaped" } while [ "$#" -gt 0 ]; do case "$1" in --channel) [ "$#" -ge 2 ] || fail "--channel needs a value" channel=$2 shift 2 ;; --version) [ "$#" -ge 2 ] || fail "--version needs a value" release_version=$2 shift 2 ;; --release-base-url) [ "$#" -ge 2 ] || fail "--release-base-url needs a URL" release_base_url=$2 shift 2 ;; --prefix) [ "$#" -ge 2 ] || fail "--prefix needs a path" prefix=$2 shift 2 ;; --archive) [ "$#" -ge 2 ] || fail "--archive needs a path" archive_path=$2 shift 2 ;; --no-path) configure_path=false shift ;; --help|-h) usage exit 0 ;; *) fail "unknown option: $1" ;; esac done if [ -z "$channel" ]; then if [ -t 0 ] && [ -t 1 ]; then printf '%s\n' 'Choose a CENTL channel:' printf '%s\n' ' 1) Oasis - qualified stable product (recommended)' printf '%s\n' ' 2) Mirage - development and experimental product' printf 'Selection [1]: ' IFS= read -r selection || selection="" case "$selection" in ''|1|oasis|Oasis|OASIS) channel=oasis ;; 2|mirage|Mirage|MIRAGE) channel=mirage ;; *) fail "unknown channel selection: $selection" ;; esac else channel=oasis fi fi case "$channel" in oasis|mirage) ;; *) fail "--channel must be oasis or mirage" ;; esac [ -n "$prefix" ] || { [ -n "${HOME:-}" ] || fail "HOME is not set; use --prefix PATH" prefix="$HOME/.local" } case "$prefix" in *' '*) fail "the installation prefix cannot contain a newline" ;; /*) ;; *) prefix="$(pwd)/$prefix" ;; esac if [ -n "$archive_path" ] && [ -n "$release_base_url" ]; then fail "--archive and --release-base-url are mutually exclusive" fi if [ -n "$release_base_url" ]; then case "$release_base_url" in *' '*) fail "the release base URL cannot contain a newline" ;; https://*|file://*) ;; *) fail "the release base URL must use https:// or file://" ;; esac [ "$release_version" != latest ] || fail "a custom release base URL requires an explicit --version" fi case $(uname -s) in Linux) platform=linux ;; Darwin) fail "macOS is currently unsupported; CENTL native releases are GNU/Linux only" ;; *) fail "CENTL native releases currently support GNU/Linux only" ;; esac case $(uname -m) in x86_64|amd64) architecture=x86_64 ;; *) fail "Linux prebuilt releases currently support x86_64 only: $(uname -m)" ;; esac asset="centl-${platform}-${architecture}.tar.gz" temporary=$(mktemp -d "${TMPDIR:-/tmp}/centl-install.XXXXXXXX") staging="" cleanup() { rm -rf "$temporary" if [ -n "$staging" ] && [ -d "$staging" ]; then rm -rf "$staging" fi } trap cleanup EXIT HUP INT TERM archive="$temporary/$asset" checksum="$temporary/$asset.sha256" requested_release="" source_label="local archive" if [ -n "$archive_path" ]; then [ -f "$archive_path" ] || fail "archive not found: $archive_path" [ -f "$archive_path.sha256" ] || fail "checksum not found: $archive_path.sha256" cp "$archive_path" "$archive" cp "$archive_path.sha256" "$checksum" else command -v curl >/dev/null 2>&1 || fail "curl is required to download CENTL" if [ -n "$release_base_url" ]; then requested_release=${release_version#v} case "$requested_release" in ''|*/*|*..*|*[!0-9A-Za-z.+_-]*) fail "the requested release version is invalid" ;; esac base_url="${release_base_url%/}/v$requested_release" source_label="$base_url" else channel_root="https://raw.githubusercontent.com/$repository/distribution/channels/$channel" if [ "$channel" = oasis ]; then if [ "$release_version" = latest ]; then curl --fail --silent --show-error --location \ "$channel_root/LATEST" --output "$temporary/LATEST" requested_release=$(sed -n '1p' "$temporary/LATEST") else requested_release=${release_version#v} fi case "$requested_release" in ''|*/*|*..*|*[!0-9A-Za-z.+_-]*) fail "the requested Oasis version is invalid" ;; esac base_url="$channel_root/v$requested_release" source_label="Oasis v$requested_release" else if [ "$release_version" = latest ]; then base_url="$channel_root/latest" source_label="latest Mirage build" else requested_release=${release_version#v} case "$requested_release" in ''|*/*|*..*|*[!0-9A-Za-z.+_-]*) fail "the requested Mirage build is invalid" ;; esac base_url="$channel_root/builds/$requested_release" source_label="Mirage build $requested_release" fi fi fi printf 'Downloading CENTL %s channel (%s) for %s-%s...\n' \ "$channel" "$source_label" "$platform" "$architecture" curl --fail --silent --show-error --location \ "$base_url/$asset" --output "$archive" curl --fail --silent --show-error --location \ "$base_url/$asset.sha256" --output "$checksum" fi expected=$(awk 'NR == 1 { print $1 }' "$checksum") case "$expected" in *[!0-9a-fA-F]*|'') fail "the release checksum is malformed" ;; esac [ "${#expected}" -eq 64 ] || fail "the release checksum is malformed" expected=$(printf '%s' "$expected" | tr 'A-F' 'a-f') if command -v sha256sum >/dev/null 2>&1; then actual=$(sha256sum "$archive" | awk '{ print $1 }') elif command -v shasum >/dev/null 2>&1; then actual=$(shasum -a 256 "$archive" | awk '{ print $1 }') else fail "sha256sum or shasum is required to verify CENTL" fi [ "$actual" = "$expected" ] || fail "release checksum verification failed" command -v tar >/dev/null 2>&1 || fail "tar is required to install CENTL" tar -tvzf "$archive" | while IFS= read -r listing; do entry_type=${listing%"${listing#?}"} case "$entry_type" in -|d) ;; *) fail "the release archive contains a link or unsupported filesystem entry" ;; esac done tar -tzf "$archive" | while IFS= read -r member; do case "$member" in /*|../*|*/../*|*/..) fail "the release archive contains an unsafe path" ;; centl|centl/*) ;; *) fail "the release archive has an unexpected layout" ;; esac done extracted="$temporary/extracted" mkdir "$extracted" tar --no-same-owner --no-same-permissions -xzf "$archive" -C "$extracted" package="$extracted/centl" [ -x "$package/bin/centl" ] || fail "the release contains no CENTL launcher" [ -x "$package/libexec/centl" ] || fail "the release contains no CENTL executable" [ -f "$package/VERSION" ] || fail "the release contains no version metadata" physics_available=false if [ -e "$package/bin/centl-physics" ] || [ -e "$package/libexec/centl-physics" ]; then [ -x "$package/bin/centl-physics" ] || fail "the release contains an invalid CENTL Physics launcher" [ -x "$package/libexec/centl-physics" ] || fail "the release contains an invalid CENTL Physics executable" physics_available=true fi sci_available=false if [ -e "$package/bin/centl-sci" ] || [ -e "$package/libexec/centl-sci" ]; then [ -x "$package/bin/centl-sci" ] || fail "the release contains an invalid CENTL-SCi launcher" [ -x "$package/libexec/centl-sci" ] || fail "the release contains an invalid CENTL-SCi executable" sci_available=true fi version=$(sed -n '1p' "$package/VERSION") case "$version" in ''|*/*|*..*|*[!0-9A-Za-z.+_-]*) fail "the release version is invalid" ;; esac if [ "$channel" = oasis ] && [ -n "$requested_release" ]; then [ "$version" = "$requested_release" ] || fail "requested $requested_release but the archive contains $version" fi hash_short=$(printf '%s' "$actual" | cut -c1-12) if [ "$channel" = oasis ]; then install_id=$version else install_id="$version-$hash_short" fi versions="$prefix/share/centl/channels/$channel" target="$versions/$install_id" bin_directory="$prefix/bin" channel_centl="$bin_directory/$channel-centl" channel_physics="$bin_directory/$channel-centl-physics" channel_sci="$bin_directory/$channel-centl-sci" for command_path in "$channel_centl" "$channel_physics" "$channel_sci"; do if [ -e "$command_path" ] && [ ! -L "$command_path" ]; then fail "$command_path already exists and is not a symbolic link" fi done if [ "$channel" = oasis ]; then for command_path in "$bin_directory/centl" "$bin_directory/centl-physics" "$bin_directory/centl-sci"; do if [ -e "$command_path" ] && [ ! -L "$command_path" ]; then fail "$command_path already exists and is not a symbolic link" fi done fi mkdir -p "$versions" "$bin_directory" if [ -e "$target" ]; then fail "CENTL $channel build $install_id is already installed at $target" fi staging=$(mktemp -d "$versions/.centl-$install_id.XXXXXXXX") cp -R "$package/." "$staging/" if ! runtime_version=$("$staging/bin/centl" --version 2>&1); then fail "the CENTL executable cannot run on this system: $runtime_version" fi [ "$runtime_version" = "centl $version" ] || fail "the executable reports '$runtime_version', expected 'centl $version'" if [ "$physics_available" = true ]; then physics_smoke=$("$staging/bin/centl-physics" convert 100 cm m 2>&1) || fail "the CENTL Physics executable cannot run on this system: $physics_smoke" [ "$physics_smoke" = 1 ] || fail "the CENTL Physics executable failed its unit-conversion smoke test" fi if [ "$sci_available" = true ]; then sci_smoke=$("$staging/bin/centl-sci" 'What is 0.1 plus 0.2?' 2>&1) || fail "the CENTL-SCi executable cannot run on this system: $sci_smoke" [ "$sci_smoke" = '3/10' ] || fail "the CENTL-SCi executable failed its exact-arithmetic smoke test" sci_repl_smoke=$(printf ':exit\n' | "$staging/bin/centl-sci" --repl 2>&1) || fail "the CENTL-SCi REPL cannot start on this system: $sci_repl_smoke" printf '%s\n' "$sci_repl_smoke" | grep -Eq '^CENTL-SCi v' || fail "the CENTL-SCi REPL did not report its identity" printf '%s\n' "$sci_repl_smoke" | grep -Fqx 'Free for science.' || fail "the CENTL-SCi REPL did not report its scientific identity" fi mv "$staging" "$target" staging="" ln -sfn "$target/bin/centl" "$channel_centl" if [ "$physics_available" = true ]; then ln -sfn "$target/bin/centl-physics" "$channel_physics" fi if [ "$sci_available" = true ]; then ln -sfn "$target/bin/centl-sci" "$channel_sci" fi if [ "$channel" = oasis ]; then ln -sfn "$target/bin/centl" "$bin_directory/centl" if [ "$physics_available" = true ]; then ln -sfn "$target/bin/centl-physics" "$bin_directory/centl-physics" fi if [ "$sci_available" = true ]; then ln -sfn "$target/bin/centl-sci" "$bin_directory/centl-sci" fi fi path_profile="" case ":${PATH:-}:" in *":$bin_directory:"*) path_active=true ;; *) path_active=false ;; esac if [ "$path_active" = false ] && [ "$configure_path" = true ] && [ -n "${HOME:-}" ]; then shell_path=${SHELL:-} shell_name=${shell_path##*/} case "$shell_name" in bash) profile="$HOME/.bashrc" ;; zsh) profile="$HOME/.zshrc" ;; *) profile="$HOME/.profile" ;; esac marker="# CENTL PATH: $bin_directory" if [ ! -f "$profile" ] || ! grep -Fqx "$marker" "$profile"; then quoted_bin=$(shell_quote "$bin_directory") { printf '\n%s\n' "$marker" printf 'export PATH=%s:"$PATH"\n' "$quoted_bin" } >> "$profile" fi path_profile=$profile fi printf 'Installed CENTL %s channel, build %s at %s\n' "$channel" "$install_id" "$target" printf 'Channel command: %s\n' "$channel_centl" if [ "$physics_available" = true ]; then printf 'Channel physics command: %s\n' "$channel_physics" fi if [ "$sci_available" = true ]; then printf 'Channel scientific command: %s\n' "$channel_sci" fi if [ "$channel" = oasis ]; then printf 'Stable command: %s\n' "$bin_directory/centl" fi if [ "$path_active" = false ] && [ -n "$path_profile" ]; then printf 'PATH configured in %s for new shells.\n' "$path_profile" printf 'Open a new terminal to use the installed commands.\n' elif [ "$path_active" = false ]; then printf 'Add %s to PATH to use the installed commands.\n' "$bin_directory" fi if [ "$channel" = oasis ]; then printf '\nOasis is ready. Start with: centl-sci\n' else printf '\nMirage is ready. Start with: mirage-centl-sci\n' fi