Something went wrong. Try again.
The Monad language. Dependent types, functional programming compiled with LLVM. Hobby project. monad-lang.org
dependent-types language compiler programming-language functional-programming
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444#!/usr/bin/env bash## monadup — manage monad nightly builds from GitHub releases.## Subcommands:# install [nightly-YYYY-MM-DD] Download and install a nightly (latest if no tag)# list List installed nightlies (* marks active)# use nightly-YYYY-MM-DD Switch the active nightly (updates symlink)# default Install latest and set it as active# uninstall nightly-YYYY-MM-DD Remove a downloaded nightly# update Download latest, remove all older ones# self-install Copy this script into ~/.monad/bin/monadup# upgrade Re-download the latest monadup script and replace itself## Environment:# MONAD_HOME Installation root (default: $HOME/.monad)# MONADUP_API_URL Releases API base URL, for testing the install path# against a local fixture (default: GitHub's)## A version directory holds the binary AND the mote sources it needs:## ~/.monad/downloads/<tag>/# monad-nightly-x86_64-linux the compiler# commit.txt what it was built from# init/ std/ runtime/ the mote sources# mote.toml generated: those three as a workspace## The last three are what make the directory a TOOLCHAIN ROOT: a mote in its# own repository can then `monad check`/`test`/`compile` with no compiler# checkout and no path dependencies, because the compiler finds the stdlib# and the C runtime there (`Mote.toolchain_root` reads $MONAD_ROOT, then# $MONAD_HOME/active, then $HOME/.monad/active).## Both `monad` (the nightly binary) and `monadup` (this script) live in# ~/.monad/bin, so adding that one directory to PATH gives access to both.set -euo pipefailREPO="monad-lang/monad"ASSET_NAME="monad-nightly-x86_64-linux"# The mote sources (`init`, `std`, `runtime`) that ship beside the binary.# Both URLs below are derived from the release the binary came from, so this# is the only other name the API lookup needs to know: asset URLs are# `<release base>/<asset name>` for every asset of a release.SRC_ASSET_NAME="monad-src-x86_64-linux.tar.gz"# Overridable so the fetch-and-unpack path can be driven against a local# fixture (`file://` URLs work, `curl -fsSL` reads them) instead of GitHub --# which is the only way this path gets tested at all. See# scripts/check-monadup.sh.API_URL="${MONADUP_API_URL:-https://api.github.com/repos/${REPO}/releases}"RAW_BASE="https://raw.githubusercontent.com/${REPO}/main/scripts/monadup"# ---------------------------------------------------------------------------# Helpers# ---------------------------------------------------------------------------monad_home() { echo "${MONAD_HOME:-$HOME/.monad}"}err() { echo "monadup: error: $*" >&2 exit 1}# Resolve the latest nightly-* tag from the GitHub releases API.# Sets global LATEST_TAG and LATEST_URL.fetch_latest_nightly() { local json json=$(curl -fsSL "$API_URL") || err "failed to query GitHub API (network or rate limit)" parse_release_json "$json" ""}# Fetch a specific release by tag. Sets global LATEST_TAG and LATEST_URL.fetch_specific_release() { local tag="$1" local json json=$(curl -fsSL "${API_URL}/tags/${tag}") || err "failed to fetch release '${tag}'" parse_release_json "$json" "$tag"}# Parse the releases JSON and extract the nightly tag + binary download URL.# Arguments: json, optional expected tag (empty = find latest nightly-*)parse_release_json() { local json="$1" local expected="$2" local tag url if command -v jq >/dev/null 2>&1; then if [[ -n "$expected" ]]; then tag=$(echo "$json" | jq -r '.tag_name') url=$(echo "$json" | jq -r ".assets[] | select(.name == \"${ASSET_NAME}\") | .browser_download_url") else tag=$(echo "$json" | jq -r '.[] | select(.tag_name | startswith("nightly-")) | .tag_name' | head -1) url=$(echo "$json" | jq -r ".[] | select(.tag_name == \"${tag}\") | .assets[] | select(.name == \"${ASSET_NAME}\") | .browser_download_url") fi elif command -v python3 >/dev/null 2>&1; then if [[ -n "$expected" ]]; then tag=$(python3 -c "import json, sysd = json.load(sys.stdin)print(d['tag_name'])" <<< "$json") url=$(python3 -c "import json, sysd = json.load(sys.stdin)for a in d.get('assets', []): if a['name'] == '${ASSET_NAME}': print(a['browser_download_url'])" <<< "$json") else tag=$(python3 -c "import json, sysdata = json.load(sys.stdin)for r in data: if r['tag_name'].startswith('nightly-'): print(r['tag_name']) break" <<< "$json") url=$(python3 -c "import json, sysdata = json.load(sys.stdin)for r in data: if r['tag_name'] == '${tag}': for a in r.get('assets', []): if a['name'] == '${ASSET_NAME}': print(a['browser_download_url'])" <<< "$json") fi else err "neither jq nor python3 is available (need one for JSON parsing)" fi [[ -n "$tag" ]] || err "no nightly release found" [[ -n "$url" ]] || err "no asset '${ASSET_NAME}' found in release '${tag}'" LATEST_TAG="$tag" LATEST_URL="$url"}# ---------------------------------------------------------------------------# Subcommands# ---------------------------------------------------------------------------# Download (if needed) the nightly identified by the already-resolved# LATEST_TAG/LATEST_URL globals. Used by cmd_install, cmd_default,# and cmd_update so none of them re-query the API.do_install() { local home_dir="$1" local dl_dir="${home_dir}/downloads" local target_dir="${dl_dir}/${LATEST_TAG}" local binary="${target_dir}/${ASSET_NAME}" # Checked before anything is written: unpacking the sources is part of the # install now, and discovering `tar` is missing AFTER the binary has landed # would leave a half-installed version directory. command -v tar >/dev/null 2>&1 \ || err "tar is not available (needed to unpack ${SRC_ASSET_NAME}); install tar, or fetch the binary by hand from the release" if [[ -x "$binary" ]]; then echo "monadup: ${LATEST_TAG} already installed" else mkdir -p "$target_dir" echo "monadup: downloading ${LATEST_TAG}..." curl -fsSL -o "$binary" "$LATEST_URL" || err "failed to download binary" curl -fsSL -o "${target_dir}/commit.txt" \ "${LATEST_URL%/"${ASSET_NAME}"}/commit.txt" \ || err "failed to download commit.txt" chmod +x "$binary" echo "monadup: installed ${LATEST_TAG}" fi # Kept OUTSIDE the branch above on purpose: a version installed by an # earlier monadup has the binary and no sources, and re-running `install` # is the obvious way to repair that. do_install_sources "$target_dir" # If no active nightly yet, set this as active. local active_file="${home_dir}/active" if [[ ! -f "$active_file" ]]; then do_use "$home_dir" "$LATEST_TAG" fi # Print version (may fail on non-Nix systems). if [[ -x "$binary" ]]; then "$binary" version 2>/dev/null \ || echo "monadup: note: binary requires Nix to run (known limitation)" fi}# Unpack the mote sources beside the binary, so the version directory is a# toolchain root the compiler can find on its own (`Mote.toolchain_root`,# `lang/src/mote.mo`): `<version dir>/init`, `/std`, `/runtime` plus a# generated root `mote.toml`.## The asset is OPTIONAL and a missing one is NOT an error. Every nightly# published before it existed has the binary only, and a release is a bad# place to make a hard requirement of something a consumer can still work# around -- so say what is missing and what it costs, in the terms of the# thing the user was trying to do.do_install_sources() { local target_dir="$1" # Already unpacked? `init` is the marker: every bundle has it, and nothing # else in a version directory is called that. if [[ -d "${target_dir}/init" ]]; then return 0 fi local url="${LATEST_URL%/"${ASSET_NAME}"}/${SRC_ASSET_NAME}" local tarball="${target_dir}/${SRC_ASSET_NAME}" if ! curl -fsSL -o "$tarball" "$url" 2>/dev/null; then rm -f "$tarball" echo "monadup: note: ${LATEST_TAG} ships no ${SRC_ASSET_NAME}" echo "monadup: the binary is installed and usable inside a compiler checkout," echo "monadup: but 'monad build'/'run'/'test' elsewhere will not find init," echo "monadup: std, llvm or the C runtime: declare [dependencies.runtime] and run" echo "monadup: from a checkout, or install a tag that ships the sources." return 0 fi tar -xzf "$tarball" -C "$target_dir" || err "failed to unpack ${SRC_ASSET_NAME}" rm -f "$tarball" echo "monadup: installed mote sources (init, std, llvm, runtime)"}cmd_install() { local home_dir home_dir=$(monad_home) local tag="${1:-}" if [[ -n "$tag" ]]; then fetch_specific_release "$tag" else fetch_latest_nightly fi do_install "$home_dir"}cmd_list() { local home_dir home_dir=$(monad_home) local dl_dir="${home_dir}/downloads" local active_tag="" if [[ -f "${home_dir}/active" ]]; then active_tag=$(cat "${home_dir}/active") fi if [[ ! -d "$dl_dir" ]]; then echo "monadup: no nightlies installed" return fi local found=0 for dir in "$dl_dir"/nightly-*; do [[ -d "$dir" ]] || continue found=1 local name name=$(basename "$dir") local marker=" " [[ "$name" == "$active_tag" ]] && marker="*" local commit="" if [[ -f "${dir}/commit.txt" ]]; then commit=$(cat "${dir}/commit.txt") fi echo "${marker} ${name} ${commit}" done [[ "$found" -eq 1 ]] || echo "monadup: no nightlies installed"}do_use() { local home_dir="$1" local tag="$2" local target_dir="${home_dir}/downloads/${tag}" local binary="${target_dir}/${ASSET_NAME}" local bin_dir="${home_dir}/bin" local link="${bin_dir}/monad" local active_file="${home_dir}/active" [[ -d "$target_dir" ]] || err "nightly '${tag}' is not installed" [[ -x "$binary" ]] || err "binary missing in '${tag}'" mkdir -p "$bin_dir" ln -sfn "../downloads/${tag}/${ASSET_NAME}" "$link" echo "$tag" > "$active_file"}cmd_use() { local home_dir home_dir=$(monad_home) local tag="${1:-}" [[ -n "$tag" ]] || err "usage: monadup use <nightly-YYYY-MM-DD>" do_use "$home_dir" "$tag" echo "monadup: switched to ${tag}"}cmd_default() { local home_dir home_dir=$(monad_home) fetch_latest_nightly do_install "$home_dir" do_use "$home_dir" "$LATEST_TAG" echo "monadup: default set to ${LATEST_TAG}"}cmd_uninstall() { local home_dir home_dir=$(monad_home) local tag="${1:-}" [[ -n "$tag" ]] || err "usage: monadup uninstall <nightly-YYYY-MM-DD>" local target_dir="${home_dir}/downloads/${tag}" [[ -d "$target_dir" ]] || err "nightly '${tag}' is not installed" local active_tag="" if [[ -f "${home_dir}/active" ]]; then active_tag=$(cat "${home_dir}/active") fi [[ "$tag" != "$active_tag" ]] || err "cannot uninstall active nightly, switch first" rm -rf "$target_dir" echo "monadup: removed ${tag}"}cmd_update() { local home_dir home_dir=$(monad_home) local dl_dir="${home_dir}/downloads" fetch_latest_nightly do_install "$home_dir" local active_tag="" if [[ -f "${home_dir}/active" ]]; then active_tag=$(cat "${home_dir}/active") fi if [[ -d "$dl_dir" ]]; then for dir in "$dl_dir"/nightly-*; do [[ -d "$dir" ]] || continue local name name=$(basename "$dir") [[ "$name" != "$LATEST_TAG" ]] || continue [[ "$name" != "$active_tag" ]] || continue rm -rf "$dir" echo "monadup: pruned ${name}" done fi}cmd_self_install() { local home_dir home_dir=$(monad_home) local bin_dir="${home_dir}/bin" mkdir -p "$bin_dir" # Use cp so the installed copy is independent of where we were invoked from. cp "$0" "${bin_dir}/monadup" chmod +x "${bin_dir}/monadup" echo "monadup: installed to ${bin_dir}/monadup" echo "Add ~/.monad/bin to your PATH:" echo " export PATH=\"\$HOME/.monad/bin:\$PATH\"" echo "Add that line to your ~/.bashrc or ~/.zshrc to make it permanent."}cmd_upgrade() { local home_dir home_dir=$(monad_home) local installed="${home_dir}/bin/monadup" if [[ ! -f "$installed" ]]; then echo "monadup: not self-installed yet; run 'monadup self-install' first" >&2 exit 1 fi local tmp tmp=$(mktemp) curl -fsSL -o "$tmp" "$RAW_BASE" || err "failed to download latest monadup" chmod +x "$tmp" mv "$tmp" "$installed" echo "monadup: upgraded to latest version"}# ---------------------------------------------------------------------------# Main# ---------------------------------------------------------------------------usage() { cat <<EOFmonadup — manage monad nightly buildsUsage: monadup <command> [args]Commands: install [nightly-YYYY-MM-DD] Download and install a nightly (latest if no tag) list List installed nightlies (* marks active) use nightly-YYYY-MM-DD Switch the active nightly default Install latest and set it as active uninstall nightly-YYYY-MM-DD Remove a downloaded nightly update Download latest, remove all older ones self-install Copy this script into ~/.monad/bin/monadup upgrade Re-download the latest monadup and replace itselfEnvironment: MONAD_HOME Installation root (default: \$HOME/.monad) MONADUP_API_URL Releases API base URL (default: GitHub's; set it to test the install path against a local fixture) MONAD_ROOT Read by the COMPILER, not here: a directory holding init/, std/ and runtime/ to resolve the stdlib and the C runtime from. \$MONAD_HOME/downloads/<active tag> is one already, once the sources are installed.Each installed tag lives in \$MONAD_HOME/downloads/<tag>/ as the binary, thecommit it was built from, and the init/std/runtime mote sources. The sourcesmake that directory a toolchain root, so a mote in its own repository canbuild without a compiler checkout. Nightlies published before the sourcesasset existed install the binary alone, and say so.EOF}main() { local subcmd="${1:-}" shift || true case "$subcmd" in install) cmd_install "$@" ;; list) cmd_list "$@" ;; use) cmd_use "$@" ;; default) cmd_default "$@" ;; uninstall) cmd_uninstall "$@" ;; update) cmd_update "$@" ;; self-install) cmd_self_install "$@" ;; upgrade) cmd_upgrade "$@" ;; help|-h|--help) usage; exit 0 ;; "") usage; exit 1 ;; *) err "unknown command '${subcmd}'" ;; esac}main "$@"