#!/bin/sh # install.sh — install the Ludic toolchain. # # curl -fsSL https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh # # That URL is the documentation site, which publishes this file alongside the # pages it is quoted on (see cmd_docs_gen). Everything host-shaped here is a # variable with an environment override, so moving the project — a new domain, # a new forge, a new name for the language — is an edit to the block below and # to the prose that quotes it (CONTRIBUTING.md has the checklist). # # It puts a complete toolchain — the compiler, the `ludic` CLI, the formatter, # the language server, the engine runtime and the bundled ludic.* packages — # under ~/.ludic, and puts ~/.ludic/bin on your PATH. Nothing else on the system # is touched, and uninstalling is `rm -rf ~/.ludic`. # # Where a prebuilt toolchain exists for the platform it is downloaded and # checksum-verified. Where it does not, the source is fetched and bootstrapped # from the compiler's own checked-in LLVM IR seed, which needs only clang and # takes a few seconds — the same path a contributor uses. # # Options (also usable through `ludic upgrade`): # --version X.Y.Z install that release instead of the latest # --dir PATH install root (default: $HOME/.ludic) # --no-modify-path do not touch any shell profile # --from-source skip the prebuilt artifacts and bootstrap from source # --help # # POSIX sh on purpose: this is the first thing a new user runs, and it has to # work on a stock macOS /bin/sh and on a minimal Linux image alike. set -eu REPO_API="${LUDIC_FORGEJO_API:-https://git.workshopsoft.io/api/v1/repos/workshopsoft/ludic}" REPO_URL="${LUDIC_REPO_URL:-https://git.workshopsoft.io/workshopsoft/ludic}" INSTALL_URL="${LUDIC_INSTALL_URL:-https://workshopsoft.pages.workshopsoft.io/ludic/install.sh}" INSTALL_DIR="${LUDIC_INSTALL_DIR:-${HOME}/.ludic}" VERSION="" MODIFY_PATH=1 FROM_SOURCE=0 # ---- output ----------------------------------------------------------------- if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then B=$(printf '\033[1m'); DIM=$(printf '\033[2m'); R=$(printf '\033[0m') GREEN=$(printf '\033[32m'); RED=$(printf '\033[31m') else B=''; DIM=''; R=''; GREEN=''; RED='' fi say() { printf '%s\n' "$*"; } step() { printf '%s==>%s %s\n' "$B" "$R" "$*"; } warn() { printf '%swarning:%s %s\n' "$RED" "$R" "$*" >&2; } die() { printf '%serror:%s %s\n' "$RED" "$R" "$*" >&2; exit 1; } have() { command -v "$1" >/dev/null 2>&1; } usage() { cat </dev/null | first_tag) [ -n "$_v" ] || _v=$(curl -fsSL "${REPO_API}/releases?limit=1" 2>/dev/null | first_tag) printf '%s\n' "$_v" } first_tag() { sed -n 's/.*"tag_name" *: *"v\{0,1\}\([^"]*\)".*/\1/p' | head -1 } sha256_of() { if have sha256sum; then sha256sum "$1" | cut -d' ' -f1 elif have shasum; then shasum -a 256 "$1" | cut -d' ' -f1 else return 1 fi } # download the toolchain tarball for this platform into $TMP, verify it against # the published checksum, and echo its path. Silent failure (returns 1) means # "no prebuilt for this platform" — the caller falls back to source. download_prebuilt() { _name="ludic-${VERSION}-${OS}-${ARCH}.tar.gz" _url="${REPO_URL}/releases/download/v${VERSION}/${_name}" _out="${TMP}/${_name}" fetch "$_url" "$_out" 2>/dev/null || return 1 if fetch "${_url}.sha256" "${_out}.sha256" 2>/dev/null; then _want=$(cut -d' ' -f1 < "${_out}.sha256") _got=$(sha256_of "$_out" || true) if [ -n "$_got" ] && [ "$_want" != "$_got" ]; then die "checksum mismatch for ${_name} (expected ${_want}, got ${_got})" fi [ -n "$_got" ] || warn "no sha256 tool found; the download was not verified" else warn "no published checksum for ${_name}; the download was not verified" fi printf '%s\n' "$_out" } # ---- installing ------------------------------------------------------------- # Replace the install root with the staged tree. The swap is done at the end so # an interrupted download never leaves a half-installed toolchain behind, and a # working install is only removed once the new one is ready to take its place. # Is this directory a usable install root? A toolchain is not just binaries: a # game links against the engine runtime and resolves the bundled packages from # here, so an artifact missing them installs something that cannot build # anything. Older releases predate this layout, which is exactly the case that # must degrade to a source build rather than to a broken ~/.ludic. valid_root() { [ -x "${1}/bin/ludic" ] && [ -x "${1}/bin/ludicc" ] \ && [ -d "${1}/runtime/native" ] && [ -f "${1}/VERSION" ] } install_staged() { _staged="$1" valid_root "$_staged" || die "the staged toolchain is incomplete" mkdir -p "$(dirname "$INSTALL_DIR")" if [ -d "$INSTALL_DIR" ]; then rm -rf "${INSTALL_DIR}.old" mv "$INSTALL_DIR" "${INSTALL_DIR}.old" # The install root is not only the artifact: `ludic add` caches packages in # /store, which is the user's data and takes a network round trip to # rebuild. Carry it across rather than replacing the directory out from under # it. (Anything else here is part of the toolchain and should be replaced.) if [ -d "${INSTALL_DIR}.old/store" ]; then mv "${INSTALL_DIR}.old/store" "${_staged}/store" || warn "could not carry the package store across" fi fi mv "$_staged" "$INSTALL_DIR" rm -rf "${INSTALL_DIR}.old" } # Build the toolchain from the source of a tag: clang assembles the compiler's # own IR seed, that compiler builds the CLI, and the CLI builds the rest. This # is the only step Ludic cannot do for itself, and it is also the whole story of # how the language bootstraps. install_from_source() { have git || die "installing from source needs git" have clang || have cc || die "installing from source needs clang. ${CC_HINT}" step "building from source (no prebuilt toolchain for ${OS}-${ARCH})" _src="${TMP}/src" git clone --quiet --depth 1 --branch "v${VERSION}" "${REPO_URL}.git" "$_src" 2>/dev/null \ || git clone --quiet --depth 1 "${REPO_URL}.git" "$_src" \ || die "could not clone ${REPO_URL}" # Which entry point builds the toolchain depends on the age of the tag: the # contributor tool was split out of the CLI after 0.5.2, so an older release # still bootstraps through main.ludic. A release older than either predates the # CLI entirely, and saying so beats failing deep inside clang. if [ -f "${_src}/tools/ludic-cli/dev.ludic" ]; then _boot='bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev && bin/ludic-dev build' elif [ -f "${_src}/tools/ludic-cli/main.ludic" ]; then _boot='bin/ludicc tools/ludic-cli/main.ludic -o bin/ludic && bin/ludic dev build' else die "release v${VERSION} predates the ludic CLI; install a newer one with --version X.Y.Z" fi CC=clang; have clang || CC=cc ( cd "$_src" \ && mkdir -p bin \ && "$CC" selfhost/ludicc.seed.ll -o bin/ludicc 2>/dev/null \ && eval "$_boot" >/dev/null ) || die "the source build failed" _stage="${TMP}/stage" mkdir -p "${_stage}/bin" cp "${_src}"/bin/ludic "${_src}"/bin/ludicc "${_src}"/bin/ludic-fmt "${_src}"/bin/ludic-lsp "${_stage}/bin/" cp -R "${_src}/runtime" "${_stage}/runtime" cp -R "${_src}/packages" "${_stage}/packages" cp "${_src}/VERSION" "${_src}/LICENSE" "${_src}/README.md" "${_stage}/" install_staged "$_stage" } # ---- PATH ------------------------------------------------------------------- # # "Installed but `ludic` is not a command" is the most common way an installer # fails, so this is deliberate about it. # # The PATH edit itself lives in one file, /env, and each shell profile # gets a single line that sources it. That keeps the change easy to find, easy to # undo (delete the line, delete ~/.ludic), and means a later install does not # accumulate a second export. # # Which profiles: enough to cover every shell the user actually opens, not just # the one $SHELL happens to name. # # ~/.profile sh, dash, and bash login shells that have no .bash_profile. # Created when missing — it is the portable catch-all. # ~/.bash_profile bash login shells (what macOS Terminal starts). Only touched # ~/.bashrc when they already exist: creating .bash_profile would stop # bash from reading ~/.profile and silently break other tools. # ~/.zshenv zsh reads this for *every* invocation, interactive or not, # and never reads ~/.profile. Created when missing. # fish config only when fish is installed. # write /env and /env.fish — the one place the PATH is set write_env_files() { cat > "${INSTALL_DIR}/env" < "${INSTALL_DIR}/env.fish" </dev/null; then ALREADY="${ALREADY} ${_file}" return 0 fi mkdir -p "$(dirname "$_file")" 2>/dev/null || true printf '\n# ludic\n%s\n' "$_line" >> "$_file" 2>/dev/null || { warn "could not write ${_file}" return 0 } TOUCHED="${TOUCHED} ${_file}" } add_to_path() { [ "$MODIFY_PATH" -eq 1 ] || return 0 write_env_files _posix=". \"${INSTALL_DIR}/env\"" add_line "${HOME}/.profile" "$_posix" add_line "${ZDOTDIR:-$HOME}/.zshenv" "$_posix" # bash reads ~/.bashrc for a non-login interactive shell — what most Linux # terminal emulators start — and it is safe to create, since its presence # changes nothing else. ~/.bash_profile is only appended to when it already # exists: creating that one would stop bash reading ~/.profile. [ -f "${HOME}/.bash_profile" ] && add_line "${HOME}/.bash_profile" "$_posix" if [ -f "${HOME}/.bashrc" ] || have bash; then add_line "${HOME}/.bashrc" "$_posix" fi if have fish || [ -f "${HOME}/.config/fish/config.fish" ]; then add_line "${HOME}/.config/fish/config.fish" "source \"${INSTALL_DIR}/env.fish\"" fi return 0 } # ---- go --------------------------------------------------------------------- TMP=$(mktemp -d "${TMPDIR:-/tmp}/ludic-install.XXXXXX") trap 'rm -rf "$TMP"' EXIT INT TERM if [ -z "$VERSION" ]; then step "looking up the latest release" VERSION=$(latest_version || true) [ -n "$VERSION" ] || die "could not determine the latest version (set one with --version X.Y.Z)" fi say " ludic ${B}${VERSION}${R} ${DIM}${OS}-${ARCH} -> ${INSTALL_DIR}${R}" if [ "$FROM_SOURCE" -eq 1 ]; then install_from_source else step "downloading the toolchain" if TARBALL=$(download_prebuilt); then STAGE="${TMP}/stage" mkdir -p "$STAGE" tar -xzf "$TARBALL" -C "$STAGE" # A current artifact unpacks to one top-level directory that is the install # root; an older one unpacks its contents directly. Try both, and if neither # is a usable root the release predates this layout — build from source # instead of installing something that cannot compile a game. ROOTDIR="" if valid_root "$STAGE"; then ROOTDIR="$STAGE" else for _d in "$STAGE"/*; do if [ -d "$_d" ] && valid_root "$_d"; then ROOTDIR="$_d"; break; fi done fi if [ -n "$ROOTDIR" ]; then install_staged "$ROOTDIR" else warn "the ${VERSION} artifact for ${OS}-${ARCH} is not a complete toolchain; building from source" install_from_source fi else install_from_source fi fi TOUCHED='' ALREADY='' add_to_path # ---- report ----------------------------------------------------------------- INSTALLED=$("${INSTALL_DIR}/bin/ludic" version 2>/dev/null || echo "ludic ${VERSION}") say "" say "${GREEN}installed${R} ${INSTALLED} -> ${INSTALL_DIR}" if [ -n "$CC_HINT" ]; then say "" warn "no clang found. Ludic emits LLVM IR and needs clang to assemble and link it." say " ${CC_HINT}" fi if [ "$OS" != darwin ]; then say "" say " ${DIM}note: windowing is macOS-only today. On ${OS}, programs build and run" say " headless (ludic build --headless); a window will not open yet.${R}" fi say "" if [ "$MODIFY_PATH" -eq 1 ]; then [ -n "${TOUCHED# }" ] && say " ${DIM}added ${INSTALL_DIR}/bin to your PATH in:${R}${TOUCHED# }" [ -n "${ALREADY# }" ] && say " ${DIM}already on your PATH in:${R}${ALREADY# }" fi case ":${PATH}:" in *":${INSTALL_DIR}/bin:"*) ;; *) say " ${DIM}this shell was started before that, so for right now:${R}" say " export PATH=\"${INSTALL_DIR}/bin:\$PATH\"" say " ${DIM}(new terminals pick it up on their own)${R}" ;; esac say "" say "next:" say " ${B}ludic new mygame${R} create a project" say " ${B}cd mygame && ludic run${R} build it and play" say " ${B}ludic help${R} everything else"