#!/bin/sh # get.sh - install DIN with one line, and check what was downloaded first. # # curl -fsSL https://dinctrl.com/install | sh # curl -fsSL https://dinctrl.com/install | sh -s -- --agent cursor # curl -fsSL https://dinctrl.com/install | sh -s -- --path # # Every argument is handed to the release's own install.sh unchanged (see # `sh install.sh --help` in the archive: --prefix, --agent, --dry-run, # --no-plugin, --no-start, ...), except one that is this script's own: # # --path also add DIN's bin/ to PATH in your shell's profile file # (without it, the line to do that is printed and nothing is edited) # # WHAT IT DOES, in order, and it stops at the first thing that is not right: # # 1. Checks this is Linux x86-64, the one build published for this shell. # 2. Fetches SHA256SUMS from the download page's own directory # (https://dinctrl.com/download/), the same file the page links. The # archive's name, and so the version, is read out of that list: nothing # here guesses a version or a URL. # 3. With minisign installed, checks SHA256SUMS.minisig against the release # key written into this script. Without it, says so and goes on: the # checksum is still checked. # 4. Downloads the archive and checks its sha256 against the list. # 5. Unpacks it and runs its install.sh. # # Any failed check: the temporary directory is deleted and nothing is installed. # No sudo, ever. DIN installs into your home directory (~/din by default). # # DIN_INSTALL_URL= points it at a different copy of the download page # (a mirror, or a test server). The signing key does not change with it. # # The whole script is one function, called on the LAST line. `curl | sh` runs # what has arrived so far, so a connection that dies halfway must deliver # nothing runnable rather than half of an installer. # The minisign key every DIN release's SHA256SUMS is signed with. The same key # is in RELEASE-SIGNING.pub, README.md, QUICKSTART.md and on the download page. # Written in here, never fetched: a key that came from the server it vouches for # would vouch for whatever is on that server. DIN_RELEASE_PUBKEY='RWRYpMRMG6vJNhzPUkYbbwTX9ebqgd1rW50Rlkv+/BZ4uAPzfda0KTqK' din_get_main() { set -eu page="https://dinctrl.com/download" base="${DIN_INSTALL_URL:-https://dinctrl.com}" base="${base%/}" say() { printf '%s\n' "$*"; } step() { printf ' %s\n' "$*"; } die() { printf 'get.sh: %s\n' "$*" >&2; exit 1; } # ---- our one option, everything else passed through ------------------------ # # "$@" is rebuilt in place without --path, which is the only way to carry an # argument list with its quoting intact in POSIX sh (no arrays). The prefix # and --dry-run are only READ here, for the PATH line at the end. add_path=no dry=no prefix="${DIN_PREFIX:-$HOME/din}" want_prefix=no n=$# while [ "$n" -gt 0 ]; do a="$1"; shift; n=$((n - 1)) if [ "$want_prefix" = yes ]; then prefix="$a"; want_prefix=no; fi case "$a" in --path) add_path=yes; continue ;; --prefix) want_prefix=yes ;; --dry-run) dry=yes ;; esac set -- "$@" "$a" done # ---- 1. which build ---------------------------------------------------------- os="$(uname -s 2>/dev/null || echo unknown)" arch="$(uname -m 2>/dev/null || echo unknown)" case "$os" in Linux) ;; Darwin) die "DIN for macOS is not available yet. There is no macOS build in this release. See $page for what is published." ;; MINGW*|MSYS*|CYGWIN*) die "this is Windows. In PowerShell: irm https://dinctrl.com/install.ps1 | iex" ;; *) die "there is no DIN build for $os. Published: Linux x86-64 and Windows x86-64. See $page" ;; esac case "$arch" in x86_64|amd64) ;; *) die "there is no DIN build for Linux $arch yet, only Linux x86-64. See $page" ;; esac # ---- tools -------------------------------------------------------------------- if command -v curl >/dev/null 2>&1; then fetcher=curl elif command -v wget >/dev/null 2>&1; then fetcher=wget else die "neither curl nor wget is installed, so nothing can be downloaded" fi if command -v sha256sum >/dev/null 2>&1; then hasher="sha256sum" elif command -v shasum >/dev/null 2>&1; then hasher="shasum -a 256" else die "no sha256sum (or shasum) is installed, so the download could not be checked. Nothing was downloaded." fi command -v tar >/dev/null 2>&1 && command -v gzip >/dev/null 2>&1 \ || die "tar and gzip are needed to unpack the download" # An https origin is held to https for every hop, redirects included: a # redirect to plain http would make the checksum list forgeable in transit, # and the checksum is the one check that always runs. fetch() { case "$base" in https://*) if [ "$fetcher" = curl ]; then curl -fsSL --proto '=https' --proto-redir '=https' --tlsv1.2 -o "$2" "$1" else wget -q --https-only -O "$2" "$1" fi ;; *) if [ "$fetcher" = curl ]; then curl -fsSL -o "$2" "$1"; else wget -q -O "$2" "$1"; fi ;; esac } tmp="$(mktemp -d "${TMPDIR:-/tmp}/din-get.XXXXXX")" || die "cannot make a temporary directory" # Deleted on every way out: success, a failed check, or Ctrl-C. trap 'rm -rf "$tmp"' EXIT trap 'exit 130' INT TERM HUP say "DIN one-line install" step "from $base/download/" # ---- 2. the checksum list ---------------------------------------------------- fetch "$base/download/SHA256SUMS" "$tmp/SHA256SUMS" \ || die "could not download $base/download/SHA256SUMS. Nothing was installed." # ---- 3. the signature over it ------------------------------------------------- # # Checked BEFORE a single name is read out of the list, so a forged list never # gets to choose what is downloaded next. With minisign installed a missing # signature is a failure, not a skip: a release we sign that arrives unsigned # is exactly what a tampered mirror looks like. if command -v minisign >/dev/null 2>&1; then fetch "$base/download/SHA256SUMS.minisig" "$tmp/SHA256SUMS.minisig" \ || die "minisign is installed but the release has no signature at $base/download/SHA256SUMS.minisig. Nothing was installed." minisign -Vq -m "$tmp/SHA256SUMS" -x "$tmp/SHA256SUMS.minisig" -P "$DIN_RELEASE_PUBKEY" >/dev/null 2>&1 \ || die "SIGNATURE MISMATCH: SHA256SUMS is not signed by the DIN release key $DIN_RELEASE_PUBKEY. Nothing was installed." step "ok signature: SHA256SUMS is signed by the DIN release key" else step "note signature not checked: install minisign to check it; the checksum was checked" fi # ---- which archive ------------------------------------------------------------- # # Exactly one Linux line, in exactly the shape package-release.sh writes. The # shape check is what keeps a line from the list from being a path: no slash, # no dot-dot, no space can pass it. lines="$(grep -E '^[0-9a-f]{64} din-[0-9][0-9A-Za-z.+-]*-linux-x86_64\.tar\.gz$' "$tmp/SHA256SUMS" || true)" [ -n "$lines" ] || die "SHA256SUMS lists no Linux x86-64 archive. Nothing was installed." [ "$(printf '%s\n' "$lines" | wc -l | tr -d ' ')" = 1 ] \ || die "SHA256SUMS lists more than one Linux x86-64 archive, so which to install is not clear. Nothing was installed." want="${lines%% *}" name="${lines#* }" stem="${name%-linux-x86_64.tar.gz}" step "found $name" # ---- 4. the archive, and its checksum ------------------------------------------ fetch "$base/download/$name" "$tmp/$name" \ || die "could not download $base/download/$name. Nothing was installed." got="$($hasher "$tmp/$name" | cut -d' ' -f1)" [ "$got" = "$want" ] \ || die "CHECKSUM MISMATCH on $name: SHA256SUMS says $want, the download is $got. Nothing was installed." step "ok checksum: $name matches SHA256SUMS" # ---- 5. unpack and install -------------------------------------------------------- mkdir "$tmp/x" ( cd "$tmp/x" && gzip -dc "$tmp/$name" | tar xf - ) \ || die "$name did not unpack. Nothing was installed." [ -f "$tmp/x/$stem/install.sh" ] \ || die "$name has no $stem/install.sh. Nothing was installed." step "ok unpacked $stem/" say # The installer asks which agent to wire when a person is at a terminal, and # it reads the answer from stdin. Under `curl | sh` stdin is THIS SCRIPT, so # it is handed the terminal instead when there is one, and /dev/null when # there is not (a CI job gets the installer's non-interactive default). The # probe is a subshell because a failed redirection on a special builtin # (`:`, `exec`) ends a POSIX shell outright instead of returning non-zero. rc=0 if (exec /dev/null; then sh "$tmp/x/$stem/install.sh" "$@" &2; exit "$rc"; } # ---- PATH --------------------------------------------------------------------------- bin="$prefix/bin" line="export PATH=\"$bin:\$PATH\"" say if [ "$add_path" = no ] || [ "$dry" = yes ]; then say "For this shell:" say " $line" if [ "$add_path" = yes ]; then say "(--path with --dry-run: no profile file was edited)" else say "To add it to your shell's profile for every new shell, run this again with --path." fi return 0 fi case "${SHELL:-}" in */zsh) profile="$HOME/.zshrc" ;; */bash) profile="$HOME/.bashrc" ;; *) profile="$HOME/.profile" ;; esac if [ -f "$profile" ] && grep -qF "$line" "$profile"; then say "PATH: $profile already puts $bin on PATH. Not edited." else printf '\n# added by the DIN installer (dinctrl.com/install --path)\n%s\n' "$line" >> "$profile" \ || die "could not write to $profile. DIN is installed; add this line yourself: $line" say "PATH: added $bin to $profile. New shells have it; for this one:" fi say " $line" } din_get_main "$@"