#!/bin/sh # matterbox installer — https://matterbox.work # # curl -fsSL https://matterbox.work/install.sh | sh # # Builds matterbox on this machine when it can, because that is the build that # gets the optional features: inline video needs the ffmpeg dev libraries and # the --demo soundtrack needs ALSA, and both are decided at compile time. The # published binaries are pure Go and carry neither. So if this machine has a Go # toolchain, git and make, the source build is what you get; otherwise the # release binary is downloaded and checked against the release's own # checksums.txt. Either way the result lands in ~/.local/bin. # # `make build` works out which features this machine can compile and says why # for the ones it cannot — that output is left on screen, since it tells you # which package to install to get the rest. # # No root, nothing outside your home directory, and no file of yours is # edited: where something needs to go on your PATH or in your shell rc, it is # printed for you to paste rather than appended behind your back. # # --source build from source; fail rather than fall back # --prebuilt download the release binary; do not build # --version v1.2.3 this release rather than the latest (both paths) # --dir PATH install here instead of ~/.local/bin # --no-completion skip installing the shell completion script # --help print this and exit # # The same as environment variables, for piped runs where flags are awkward: # MATTERBOX_SOURCE, MATTERBOX_PREBUILT, MATTERBOX_VERSION, # MATTERBOX_INSTALL_DIR, MATTERBOX_NO_COMPLETION. # # Everything below is a function until the last line, so a download that gets # cut off midway runs nothing at all rather than half an install. set -eu REPO="cornedor/matterbox" BIN="matterbox" SITE="https://matterbox.work" VERSION="${MATTERBOX_VERSION:-}" DESTDIR="${MATTERBOX_INSTALL_DIR:-$HOME/.local/bin}" COMPLETION="${MATTERBOX_NO_COMPLETION:+0}" COMPLETION="${COMPLETION:-1}" MODE=auto [ -n "${MATTERBOX_SOURCE:-}" ] && MODE=source [ -n "${MATTERBOX_PREBUILT:-}" ] && MODE=prebuilt TMPDIR_="" # ---------------------------------------------------------------- output ---- # Colour only when stdout is a terminal. Under `curl | sh` it still is — only # stdin is the pipe — so this stays readable in the case that matters most. if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then B="$(printf '\033[1m')"; DIM="$(printf '\033[2m')" RED="$(printf '\033[31m')"; YLW="$(printf '\033[33m')"; R="$(printf '\033[0m')" else B=""; DIM=""; RED=""; YLW=""; R="" fi say() { printf '%s\n' "$*"; } step() { printf '%s==>%s %s\n' "$B" "$R" "$*"; } note() { printf '%s %s%s\n' "$DIM" "$*" "$R"; } warn() { printf '%swarning:%s %s\n' "$YLW" "$R" "$*" >&2; } die() { printf '%serror:%s %s\n' "$RED" "$R" "$*" >&2; exit 1; } # Piped into sh there is no file to read the header out of, so the short form # is the one most people asking for --help will actually get. usage() { if [ -f "$0" ] && head -1 "$0" | grep -qs '^#!'; then awk 'NR>1 && /^#/ { sub(/^# ?/, ""); print; next } NR>1 { exit }' "$0" return 0 fi say "matterbox installer — $SITE" say "" say " Builds on this machine when it can (that is the build with video and" say " audio in it), downloads the release binary when it cannot." say "" say " --source build from source; fail rather than fall back" say " --prebuilt download the release binary; do not build" say " --version TAG this release rather than the latest" say " --dir PATH install here instead of ~/.local/bin" say " --no-completion skip installing the shell completion script" say "" say " Also MATTERBOX_SOURCE, MATTERBOX_PREBUILT, MATTERBOX_VERSION," say " MATTERBOX_INSTALL_DIR, MATTERBOX_NO_COMPLETION, for piped runs:" say " curl -fsSL $SITE/install.sh | MATTERBOX_PREBUILT=1 sh" } cleanup() { [ -n "$TMPDIR_" ] && rm -rf "$TMPDIR_"; return 0; } # ------------------------------------------------------------ the basics ---- have() { command -v "$1" >/dev/null 2>&1; } need() { have "$1" || die "$1 is required and was not found on this machine"; } parse_args() { while [ $# -gt 0 ]; do case "$1" in --source) MODE=source; shift ;; --prebuilt|--binary) MODE=prebuilt; shift ;; --version) [ $# -ge 2 ] || die "--version needs a tag, e.g. --version v1.0.0" VERSION="$2"; shift 2 ;; --version=*) VERSION="${1#--version=}"; shift ;; --dir|-d) [ $# -ge 2 ] || die "--dir needs a path" DESTDIR="$2"; shift 2 ;; --dir=*) DESTDIR="${1#--dir=}"; shift ;; --no-completion) COMPLETION=0; shift ;; --help|-h) usage; exit 0 ;; *) die "unknown option '$1' (--help lists them)" ;; esac done } # curl is what the copy-paste line uses, so it is the one we expect; wget is # here for the person who fetched this file some other way. detect_downloader() { if have curl; then DL=curl elif have wget; then DL=wget else DL="" fi } fetch() { # fetch URL DEST if [ "$DL" = curl ]; then curl -fsSL --proto '=https' --tlsv1.2 --retry 3 -o "$2" "$1" else wget -q -O "$2" "$1" fi } # ------------------------------------------------------------- platform ---- # GOOS/GOARCH as the release names them. Anything else is a hard stop for the # prebuilt path: a wrong guess there downloads a binary that cannot run. A # source build does not care, so this only refuses once it has to. detect_platform() { os="$(uname -s)" arch="$(uname -m)" case "$os" in Linux) OS=linux ;; Darwin) OS=darwin ;; MINGW*|MSYS*|CYGWIN*) OS=windows ;; *) OS="$os" ;; esac case "$arch" in x86_64|amd64) ARCH=amd64 ;; aarch64|arm64) ARCH=arm64 ;; *) ARCH="$arch" ;; esac # An Apple Silicon Mac running this script under Rosetta reports x86_64, # and would get the slow binary for the rest of its life. Ask the kernel # whether it is translating us and take the native one instead. if [ "$OS" = darwin ] && [ "$ARCH" = amd64 ] && [ "$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)" = 1 ]; then ARCH=arm64 fi PLATFORM="${OS}_${ARCH}" } # Whether the release publishes a build for this machine at all. released_platform() { case "$PLATFORM" in linux_amd64|linux_arm64|darwin_amd64|darwin_arm64) return 0 ;; *) return 1 ;; esac } # What a source build needs beyond the toolchain itself. can_build() { have go && have git && have make } # The tag to install. GitHub redirects /releases/latest to /releases/tag/, # so with curl the answer is in the effective URL — no API call, and so no # rate limit to share with everyone else behind the same address. wget has no # equivalent, so it asks the API and pays that cost. latest_tag() { if [ "$DL" = curl ]; then curl -fsSLI -o /dev/null -w '%{url_effective}' \ --proto '=https' --tlsv1.2 --retry 3 \ "https://github.com/$REPO/releases/latest" 2>/dev/null | sed -n 's#.*/releases/tag/##p' else wget -q -O - "https://api.github.com/repos/$REPO/releases/latest" 2>/dev/null | sed -n 's/.*"tag_name" *: *"\([^"]*\)".*/\1/p' | head -1 fi } # ------------------------------------------------------------- checksums ---- sha256_of() { if have sha256sum; then sha256sum "$1" | cut -d' ' -f1 elif have shasum; then shasum -a 256 "$1" | cut -d' ' -f1 elif have openssl; then openssl dgst -sha256 "$1" | sed 's/.*= *//' else return 1 fi } # The whole point of the exercise: the tarball is not unpacked until its hash # matches the one the release publishes for that exact file name. verify() { # verify TARBALL SUMS ASSET want="$(awk -v f="$3" '$2 == f || $2 == "*" f { print $1; exit }' "$2")" [ -n "$want" ] || die "checksums.txt for $VERSION does not list $3" got="$(sha256_of "$1")" || die "no sha256 tool found (sha256sum, shasum or openssl) — refusing to install unverified" [ "$want" = "$got" ] || die "checksum mismatch for $3 expected $want got $got Do not use this download. Try again, and if it keeps failing say so at https://github.com/$REPO/issues" } # ----------------------------------------------------------- the two paths -- # `make build` is what decides the optional features: scripts/build-tags asks # pkg-config what this machine has and turns on what it can, explaining each # feature it leaves off and which package would fix it. That explanation is # the reason to build here rather than download, so it is left on screen. build_from_source() { src="$TMPDIR_/src" # Both paths should mean the same version of matterbox, so the build # tracks the latest release rather than whatever the default branch is # mid-thought. Before the first release there is no tag to take, and then # the branch is the only thing to build -- which is what a clone and a # `make install` would have given you anyway. if [ -z "$VERSION" ] && [ -n "$DL" ]; then VERSION="$(latest_tag || true)" fi # git is quiet on success and loud on failure, rather than the other way # round: a shallow clone of an annotated tag warns that the tag "is not a # commit", and checking one out prints the detached-HEAD lecture. Neither # means anything here, and both look alarming in an installer. step "fetching the source" err="$TMPDIR_/git.err" if [ -n "$VERSION" ]; then note "$REPO at $VERSION" git -c advice.detachedHead=false clone --quiet --depth 1 \ --branch "$VERSION" "https://github.com/$REPO.git" "$src" 2>"$err" || { cat "$err" >&2 die "could not clone $REPO at $VERSION. Check the tag exists, or leave --version off to build the default branch." } else note "$REPO, default branch (no published release to build)" git clone --quiet --depth 1 "https://github.com/$REPO.git" "$src" 2>"$err" || { cat "$err" >&2 die "could not clone $REPO — is github.com reachable?" } fi step "building (a minute or two, and it downloads Go modules)" ( cd "$src" && make build ) || die "the build failed. The output above says what went wrong. If it is not something you want to chase, the release binary needs no toolchain — rerun with --prebuilt." [ -f "$src/$BIN" ] || die "the build reported success but produced no $BIN binary" BUILT="$src/$BIN" SRCDIR="$src" } download_prebuilt() { [ -n "$DL" ] || die "neither curl nor wget is installed" released_platform || die "the releases do not cover $PLATFORM (they are linux and darwin, amd64 and arm64). Building from source would work here — install Go, git and make." if [ -z "$VERSION" ]; then VERSION="$(latest_tag || true)" [ -n "$VERSION" ] || die "could not work out the latest release. $REPO may have no published release yet, or GitHub is unreachable. Check $SITE and pass a tag directly: --version v1.0.0" fi asset="${BIN}_${VERSION}_${PLATFORM}.tar.gz" base="https://github.com/$REPO/releases/download/$VERSION" step "downloading $VERSION" note "$base/$asset" fetch "$base/$asset" "$TMPDIR_/$asset" || die "could not download $asset. $VERSION may not have a build for $PLATFORM. The releases are at https://github.com/$REPO/releases" fetch "$base/checksums.txt" "$TMPDIR_/checksums.txt" || die "could not download checksums.txt for $VERSION — refusing to install unverified" step "verifying" verify "$TMPDIR_/$asset" "$TMPDIR_/checksums.txt" "$asset" note "sha256 matches the published checksum" mkdir -p "$TMPDIR_/x" tar -xzf "$TMPDIR_/$asset" -C "$TMPDIR_/x" || die "could not unpack $asset" [ -f "$TMPDIR_/x/$BIN" ] || die "$asset does not contain a $BIN binary" BUILT="$TMPDIR_/x/$BIN" SRCDIR="$TMPDIR_/x" } # --------------------------------------------------------------- install ---- # Written next to its destination and moved into place, so the swap is atomic # and upgrading a matterbox that is currently running cannot fail with # 'text file busy' or leave a half-written binary behind. install_binary() { # install_binary SRC mkdir -p "$DESTDIR" 2>/dev/null || die "cannot create $DESTDIR — pass --dir with somewhere you can write" [ -w "$DESTDIR" ] || die "$DESTDIR is not writable by you. Pass --dir with a directory you own, or install it yourself: sudo install -m 0755 /usr/local/bin/$BIN" tmp="$DESTDIR/.$BIN.$$" cp "$1" "$tmp" chmod 0755 "$tmp" mv -f "$tmp" "$DESTDIR/$BIN" } # Apache-2.0 asks that the NOTICE travel with the thing, and the release # tarball carries the licences of everything statically linked into it. Only # the download needs this: a binary you compiled yourself is not a # distribution, and the clone it came from is already gone. install_licenses() { # install_licenses SRCDIR [ -f "$1/THIRD_PARTY_LICENSES" ] || return 0 dir="${XDG_DATA_HOME:-$HOME/.local/share}/$BIN" mkdir -p "$dir" 2>/dev/null || return 0 for f in LICENSE NOTICE THIRD_PARTY_LICENSES; do [ -f "$1/$f" ] && cp "$1/$f" "$dir/$f" 2>/dev/null || true done LICENSEDIR="$dir" } # The binary writes its own completion script, so this is just a matter of # putting it where the shell looks. bash and fish read these directories on # their own; zsh needs an fpath entry, which is a line in a file of yours, so # that one is printed rather than written. install_completion() { [ "$COMPLETION" = 1 ] || return 0 shell="$(basename "${SHELL:-}" 2>/dev/null || true)" case "$shell" in zsh) dir="$HOME/.local/share/zsh/site-functions" mkdir -p "$dir" && "$DESTDIR/$BIN" completion zsh > "$dir/_$BIN" || return 0 note "zsh completion -> $dir/_$BIN" if [ -f "$HOME/.zshrc" ] && grep -qsF "$dir" "$HOME/.zshrc"; then note "~/.zshrc already has it on fpath" else ZSHRC_HINT="$dir" fi ;; bash) dir="$HOME/.local/share/bash-completion/completions" mkdir -p "$dir" && "$DESTDIR/$BIN" completion bash > "$dir/$BIN" || return 0 note "bash completion -> $dir/$BIN" ;; fish) dir="$HOME/.config/fish/completions" mkdir -p "$dir" && "$DESTDIR/$BIN" completion fish > "$dir/$BIN.fish" || return 0 note "fish completion -> $dir/$BIN.fish" ;; esac return 0 } # The quotes sit around the variable and not around the colons: dash fails to # match a quoted string placed immediately after a `*` in a case pattern, and # dash is /bin/sh on Debian and Ubuntu -- which is to say, on most of the # machines this script will ever run on. on_path() { case ":${PATH}:" in *:"$DESTDIR":*) return 0 ;; *) return 1 ;; esac } # ------------------------------------------------------------------ main ---- # Which of the two paths, and why. Said out loud, because "it built here" and # "it downloaded a binary" give you different features and you should not have # to guess which one happened. choose_method() { case "$MODE" in source) can_build || die "--source needs a Go toolchain, git and make on this machine. Install them, or drop --source to download the release binary instead." METHOD=source ;; prebuilt) METHOD=prebuilt ;; *) if can_build; then METHOD=source else METHOD=prebuilt missing="" for t in go git make; do have "$t" || missing="${missing:+$missing, }$t" done note "no $missing here, so this takes the release binary" note "a source build is the one with inline video and --demo audio in it" fi ;; esac } main() { parse_args "$@" need uname; need tar; need mktemp; need awk; need sed detect_downloader detect_platform previous="" if have "$BIN"; then previous="$("$BIN" --version 2>/dev/null | head -1 || true)" fi step "matterbox for $OS/$ARCH" choose_method TMPDIR_="$(mktemp -d "${TMPDIR:-/tmp}/matterbox-install.XXXXXX")" trap cleanup EXIT INT TERM if [ "$METHOD" = source ]; then build_from_source else download_prebuilt fi step "installing to $DESTDIR" install_binary "$BUILT" install_licenses "$SRCDIR" version_out="$("$DESTDIR/$BIN" --version 2>/dev/null || true)" [ -n "$version_out" ] || die "$DESTDIR/$BIN was installed but will not run on this machine" installed="$(printf '%s\n' "$version_out" | head -1)" features="$(printf '%s\n' "$version_out" | sed -n 's/^tags:[[:space:]]*//p')" install_completion say "" if [ -n "$previous" ] && [ "$previous" != "$installed" ]; then say " ${B}$installed${R} ${DIM}(was $previous)${R}" else say " ${B}$installed${R}" fi say " $DESTDIR/$BIN" [ -n "$features" ] && say " ${DIM}features: $features${R}" [ -n "${LICENSEDIR:-}" ] && say " ${DIM}licences in $LICENSEDIR${R}" say "" if on_path; then # Reassigning PATH clears the shell's hash table, so this asks the # filesystem rather than repeating what it remembered a moment ago. PATH="$PATH" found="$(command -v "$BIN" 2>/dev/null || true)" if [ -n "$found" ] && [ "$found" != "$DESTDIR/$BIN" ]; then warn "another $BIN comes earlier on your PATH and is the one that will run: $found Remove it, or move $DESTDIR ahead of it." say "" fi else warn "$DESTDIR is not on your PATH. Add it:" case "$(basename "${SHELL:-sh}")" in zsh) say " echo 'export PATH=\"$DESTDIR:\$PATH\"' >> ~/.zshrc" ;; fish) say " fish_add_path $DESTDIR" ;; *) say " echo 'export PATH=\"$DESTDIR:\$PATH\"' >> ~/.profile" ;; esac say "" fi if [ -n "${ZSHRC_HINT:-}" ]; then say " To turn on completion, add to ~/.zshrc:" say " fpath=($ZSHRC_HINT \$fpath)" say " autoload -Uz compinit && compinit" say "" fi # Same thing `make install` does, and just as optional: it lets the # browser hand the SSO token back to `matterbox login` on its own. if [ "$OS" = linux ]; then "$DESTDIR/$BIN" register-handler >/dev/null 2>&1 && note "registered the mmauth:// login handler" || true say "" fi say " Next: ${B}$BIN welcome${R} ${DIM}— point it at your server and log in${R}" say " Docs: $SITE/docs/" say "" } main "$@"