#!/bin/sh # # Redrob Code installer. # # curl -fsSL https://console.redrob.ai/code/install.sh | sh # # It works out your OS and CPU, downloads the matching build, checks it against the published # checksum, and puts the redrob-code binary in $HOME/.redrob/bin, with redrob # beside it as the command you run. It then adds that directory to your PATH in your shell profile, # once, so a new terminal finds redrob without you editing anything. # # It downloads from one place: the base URL below, which Console writes into this file. If that is # empty, this copy of the script was served by a deployment that has no download host configured, and # it stops rather than guessing at one. It does not look for a release on any other host. # # Environment: # REDROB_CODE_DOWNLOAD_BASE Where the builds are. Overrides the value in this file. # REDROB_CODE_VERSION Which build to fetch. Default: latest # REDROB_CODE_INSTALL_DIR Where to put the binary. Default: $HOME/.redrob/bin # REDROB_CODE_BIN A redrob-code you already built. Installed instead of downloading. # REDROB_CODE_SKIP_CHECKSUM Set to 1 to accept a build with no published checksum. # REDROB_CODE_NO_PATH Set to 1 to install without touching any shell profile. set -eu DOWNLOAD_BASE="${REDROB_CODE_DOWNLOAD_BASE:-https://github.com/redrob-labs/redrob-code/releases}" VERSION="${REDROB_CODE_VERSION:-latest}" INSTALL_DIR="${REDROB_CODE_INSTALL_DIR:-$HOME/.redrob/bin}" BIN_NAME="redrob-code" CMD_NAME="redrob" PATH_MARKER="# added by the Redrob Code installer" CONSOLE_URL="https://console.redrob.ai" # Set by configure_path when it writes a profile, so the closing message can be specific. PATH_WRITTEN=0 say() { printf '%s\n' "$*" } warn() { printf '%s\n' "$*" >&2 } die() { printf '%s\n' "$*" >&2 exit 1 } usage() { say "Installs Redrob Code into $INSTALL_DIR as $CMD_NAME and $BIN_NAME, and puts that directory" say "on your PATH by adding one line to your shell profile." say "" say "Usage: curl -fsSL https://console.redrob.ai/code/install.sh | sh" say "" say "Environment:" say " REDROB_CODE_DOWNLOAD_BASE where the builds are" say " REDROB_CODE_VERSION which build to fetch (default $VERSION)" say " REDROB_CODE_INSTALL_DIR where to put the binary (default $INSTALL_DIR)" say " REDROB_CODE_BIN install a redrob-code you already have" say " REDROB_CODE_SKIP_CHECKSUM set to 1 to accept a build with no published checksum" say " REDROB_CODE_NO_PATH set to 1 to leave your shell profile alone" say "" say "On Windows, this script is not the installer. Run this in PowerShell instead:" say " irm $CONSOLE_URL/code/install.ps1 | iex" say "" say "The page explaining all of this is at $CONSOLE_URL/code" } case "${1:-}" in -h | --help) usage exit 0 ;; "") ;; *) die "Unknown argument: $1. Run with --help." ;; esac # The Windows hand-off, in one place because more than one thing here detects Windows. # # There is a signed Windows build; this script is not how it is installed. It links two names to one # file and appends a line to a shell profile, and neither of those is Windows, so it names the # installer that does the same job there rather than refusing as though nothing were published. windows_installer() { warn "This is the macOS and Linux installer, and Redrob Code does have a Windows build." warn "" warn "Run this in PowerShell instead:" warn " irm $CONSOLE_URL/code/install.ps1 | iex" warn "" warn "Git Bash, MSYS and Cygwin take the PowerShell installer too: what this script does - a hard" warn "link between two names, and a line in a shell profile - does not survive outside that shell." warn "$CONSOLE_URL/code shows that command with the Windows tab selected." exit 1 } # Windows that did not answer to `uname -s`: Command Prompt sets OS, and WINDIR or SystemRoot is set # whatever the shell. Consulted only after uname has failed to name a system we publish for, so a # machine with a stray variable of that name is never diverted. looks_like_windows() { if [ "${OS:-}" = "Windows_NT" ] || [ -n "${WINDIR:-}" ] || [ -n "${SystemRoot:-}" ]; then return 0 fi case "$(uname -o 2>/dev/null || echo unknown)" in Msys | Cygwin | *Windows*) return 0 ;; esac return 1 } detect_platform() { uname_s="$(uname -s 2>/dev/null || echo unknown)" uname_m="$(uname -m 2>/dev/null || echo unknown)" case "$uname_s" in Darwin) OS="darwin" ;; Linux) OS="linux" ;; MINGW* | MSYS* | CYGWIN* | Windows_NT | Windows*) windows_installer ;; *) # A shell on Windows with no usable uname reports "unknown", which is not a missing build: it # is the one platform this script hands over rather than refuses. if looks_like_windows; then windows_installer fi die "Redrob Code does not have a build for $uname_s. macOS, Linux and Windows are the builds there are." ;; esac case "$uname_m" in x86_64 | amd64) ARCH="x64" ;; arm64 | aarch64) ARCH="arm64" ;; *) die "Redrob Code does not have a build for $uname_m. x86_64 and arm64 are the two builds there are." ;; esac } fetch_to() { fetch_url="$1" fetch_dest="$2" if command -v curl >/dev/null 2>&1; then curl -fsSL "$fetch_url" -o "$fetch_dest" elif command -v wget >/dev/null 2>&1; then wget -q -O "$fetch_dest" "$fetch_url" else die "Neither curl nor wget is on this machine, so nothing can be downloaded." fi } sha256_of() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d ' ' -f 1 elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | cut -d ' ' -f 1 else die "Neither sha256sum nor shasum is on this machine, so the download cannot be verified." fi } install_binary() { mkdir -p "$INSTALL_DIR" cp "$1" "$INSTALL_DIR/$BIN_NAME" chmod 755 "$INSTALL_DIR/$BIN_NAME" # The command people actually type. A hard link so both names are one file and cannot disagree # about which build is installed; a copy where linking is not allowed, which is the same bytes. rm -f "$INSTALL_DIR/$CMD_NAME" ln "$INSTALL_DIR/$BIN_NAME" "$INSTALL_DIR/$CMD_NAME" 2>/dev/null || cp "$INSTALL_DIR/$BIN_NAME" "$INSTALL_DIR/$CMD_NAME" || die "Could not install $INSTALL_DIR/$CMD_NAME beside $BIN_NAME." chmod 755 "$INSTALL_DIR/$CMD_NAME" } on_path() { case ":$PATH:" in *":$INSTALL_DIR:"*) return 0 ;; *) return 1 ;; esac } # The install directory as it should be written into a profile: through $HOME when it is under it, # so the line keeps working if the home directory is ever mounted somewhere else. profile_dir_form() { case "$INSTALL_DIR" in "$HOME"/*) printf '$HOME%s\n' "${INSTALL_DIR#"$HOME"}" ;; *) printf '%s\n' "$INSTALL_DIR" ;; esac } # Whether this profile already puts the directory on the PATH: our own marker, or any mention of the # directory, so a line somebody wrote by hand is left alone rather than duplicated. profile_has_path() { [ -f "$1" ] || return 1 if grep -F "$PATH_MARKER" "$1" >/dev/null 2>&1; then return 0 fi if grep -F "$INSTALL_DIR" "$1" >/dev/null 2>&1; then return 0 fi if grep -F "$(profile_dir_form)" "$1" >/dev/null 2>&1; then return 0 fi return 1 } # Appends the PATH line to one profile. Never truncates and never rewrites: an existing file is only # ever added to, and only when nothing in it mentions this directory already. append_path_line() { append_file="$1" append_kind="$2" if profile_has_path "$append_file"; then PATH_WRITTEN=1 say " $append_file already has it." return 0 fi append_parent="$(dirname "$append_file")" mkdir -p "$append_parent" 2>/dev/null || true { printf '\n%s\n' "$PATH_MARKER" if [ "$append_kind" = fish ]; then printf 'fish_add_path "%s"\n' "$(profile_dir_form)" else printf 'export PATH="%s:$PATH"\n' "$(profile_dir_form)" fi } >>"$append_file" 2>/dev/null || { warn "Could not write $append_file, so your PATH was left alone." return 1 } PATH_WRITTEN=1 say " $append_file" return 0 } # Which profile the shell in $SHELL actually reads. Login and interactive files differ per shell, so # bash gets both: .bashrc for the interactive shells people work in, .profile for a login shell that # does not read it. configure_path() { if [ -z "${HOME:-}" ]; then warn "HOME is not set, so there is no shell profile to add $INSTALL_DIR to." return 0 fi case "$(basename "${SHELL:-}" 2>/dev/null || echo unknown)" in zsh) append_path_line "${ZDOTDIR:-$HOME}/.zshrc" posix || true ;; bash) append_path_line "$HOME/.bashrc" posix || true append_path_line "$HOME/.profile" posix || true ;; fish) append_path_line "${XDG_CONFIG_HOME:-$HOME/.config}/fish/config.fish" fish || true ;; *) append_path_line "$HOME/.profile" posix || true ;; esac } # A script run through a pipe cannot change the PATH of the shell that started it: it is a child, and # the export dies with it. So the profile is written for every shell after this one, and the line to # paste is printed for the one the reader is sitting in. setup_path() { if on_path; then return 0 fi if [ "${REDROB_CODE_NO_PATH:-}" = "1" ]; then say "" say "$INSTALL_DIR is not on your PATH, and REDROB_CODE_NO_PATH=1, so no profile was changed." printf ' export PATH="%s:$PATH"\n' "$INSTALL_DIR" return 0 fi say "" say "$INSTALL_DIR is not on your PATH, so this went into your shell profile:" configure_path say "" if [ "$PATH_WRITTEN" = 1 ]; then say "That applies to new terminals. For the one you are in, run this:" else say "Nothing could be written, so add this to your shell profile yourself:" fi printf ' export PATH="%s:$PATH"\n' "$INSTALL_DIR" say "Then $CMD_NAME works here too. A new terminal needs nothing." } next_steps() { say "" say "Next: Redrob is prepaid, so a new workspace pays before anything will answer." say " Add credit: $CONSOLE_URL/start" say " How it works: $CONSOLE_URL/guide" say " Building on it: $CONSOLE_URL/docs" say "" say "Signing this machine in to a workspace is built. Run this, choose Connect Redrob, and approve" say "the code it shows at $CONSOLE_URL/connect:" say " $CMD_NAME providers login --provider redrob" say "A key pasted from $CONSOLE_URL/api-keys still works, and is the fallback for a machine that" say "cannot reach Console." } no_download_host() { warn "No download host is configured for this script, so it has nothing to fetch." warn "" warn "Redrob Code is published and this command installs it from a correctly configured Console." warn "This copy of the script arrived without a download host, which is a misconfigured deployment" warn "rather than a missing build. Nothing was installed and nothing was changed." warn "" warn "Two ways to install the engine from here:" warn " 1. Build it from a local redrob-code checkout, then install that binary with this script:" warn " REDROB_CODE_BIN=/path/to/redrob-code sh install.sh" warn " 2. Run the binary from the checkout directly, without installing it." warn "" warn "If you host builds yourself, point the script at them:" warn " REDROB_CODE_DOWNLOAD_BASE=https://example.com/redrob-code sh install.sh" warn "" warn "Credit is at $CONSOLE_URL/start, the API at $CONSOLE_URL/docs, and $CONSOLE_URL/code says" warn "what this script does when it is configured." exit 1 } if [ -n "${REDROB_CODE_BIN:-}" ]; then [ -f "$REDROB_CODE_BIN" ] || die "REDROB_CODE_BIN is $REDROB_CODE_BIN, which is not a file." [ -x "$REDROB_CODE_BIN" ] || die "REDROB_CODE_BIN is $REDROB_CODE_BIN, which is not executable." install_binary "$REDROB_CODE_BIN" say "Installed $INSTALL_DIR/$BIN_NAME from REDROB_CODE_BIN, and $CMD_NAME beside it." setup_path next_steps exit 0 fi [ -n "$DOWNLOAD_BASE" ] || no_download_host detect_platform # `latest` is a redirect on a release, not a directory, so it has to become a real version before # anything can be fetched. Resolved once, and reported, so the rest of the run names one build. if [ "$VERSION" = "latest" ]; then VERSION_URL="$DOWNLOAD_BASE/latest/download/VERSION" TMP_VERSION="$(mktemp 2>/dev/null || mktemp -t redrob-code-version)" fetch_to "$VERSION_URL" "$TMP_VERSION" || die "Could not read $VERSION_URL, so there is no way to tell which version is newest. $CONSOLE_URL/code says what is published." VERSION="$(tr -d '\r\n[:space:]' <"$TMP_VERSION")" rm -f "$TMP_VERSION" [ -n "$VERSION" ] || die "$VERSION_URL is empty, so it does not name a version. Nothing was installed." case "$VERSION" in *[!0-9.]*) die "$VERSION_URL does not name a version: $VERSION. Nothing was installed." ;; esac fi VERSION="${VERSION#v}" ARCHIVE_EXT=".zip" if [ "$OS" = "linux" ]; then ARCHIVE_EXT=".tar.gz" fi ASSET="$CMD_NAME-$OS-$ARCH$ARCHIVE_EXT" ASSET_URL="$DOWNLOAD_BASE/download/v$VERSION/$ASSET" CHECKSUM_URL="$DOWNLOAD_BASE/download/v$VERSION/SHA256SUMS" TMP_DIR="$(mktemp -d 2>/dev/null || mktemp -d -t redrob-code)" trap 'rm -rf "$TMP_DIR"' EXIT HUP INT TERM say "Downloading $BIN_NAME $VERSION for $OS/$ARCH." fetch_to "$ASSET_URL" "$TMP_DIR/$ASSET" || die "Could not download $ASSET_URL. If that build does not exist, $CONSOLE_URL/code lists what does." # One checksum file per release covering every archive in it, rather than a sidecar per asset. The # line for this asset is selected by name: a release carries a dozen archives and verifying against # the wrong line would pass for the wrong reason. if fetch_to "$CHECKSUM_URL" "$TMP_DIR/SHA256SUMS" 2>/dev/null; then EXPECTED="$(awk -v want="$ASSET" '$2 == want || $2 == "*" want { print $1; exit }' "$TMP_DIR/SHA256SUMS" | tr -d '\r\n')" [ -n "$EXPECTED" ] || die "SHA256SUMS at $CHECKSUM_URL has no line for $ASSET, so the download cannot be verified. Nothing was installed." ACTUAL="$(sha256_of "$TMP_DIR/$ASSET")" [ "$EXPECTED" = "$ACTUAL" ] || die "Checksum mismatch for $ASSET. Expected $EXPECTED and got $ACTUAL. Nothing was installed." say "Checksum verified." elif [ "${REDROB_CODE_SKIP_CHECKSUM:-}" = "1" ]; then warn "No checksums published at $CHECKSUM_URL. Installing anyway: REDROB_CODE_SKIP_CHECKSUM=1." else die "No checksums published at $CHECKSUM_URL, so this download cannot be verified. Nothing was installed. Set REDROB_CODE_SKIP_CHECKSUM=1 to install it anyway." fi mkdir -p "$TMP_DIR/unpacked" case "$ASSET" in *.tar.gz) command -v tar >/dev/null 2>&1 || die "$ASSET is a tar.gz and tar is not on this machine." tar -xzf "$TMP_DIR/$ASSET" -C "$TMP_DIR/unpacked" || die "$ASSET is not a readable tar.gz archive." ;; *.zip) if command -v unzip >/dev/null 2>&1; then unzip -q "$TMP_DIR/$ASSET" -d "$TMP_DIR/unpacked" || die "$ASSET is not a readable zip archive." elif command -v bsdtar >/dev/null 2>&1; then bsdtar -xf "$TMP_DIR/$ASSET" -C "$TMP_DIR/unpacked" || die "$ASSET is not a readable zip archive." else die "$ASSET is a zip and neither unzip nor bsdtar is on this machine." fi ;; *) die "$ASSET is not an archive this script knows how to open." ;; esac # The binary inside the archive is named for the command, not for $BIN_NAME. Both names are looked # for so an archive built either way still installs. BINARY="$(find "$TMP_DIR/unpacked" -type f \( -name "$CMD_NAME" -o -name "$BIN_NAME" \) 2>/dev/null | head -n 1)" [ -n "$BINARY" ] || die "$ASSET does not contain a $CMD_NAME binary." install_binary "$BINARY" say "Installed $INSTALL_DIR/$BIN_NAME, and $CMD_NAME beside it as the command you run." setup_path next_steps