#!/bin/sh # nomus installer: ONE script for macOS and Linux (it detects which). # # curl -fsSL https://agrohi.com/nomus/install | sh # curl -fsSL https://agrohi.com/nomus/install | sh -s -- --help # # Read it before you run it: # # curl -fsSL https://agrohi.com/nomus/install | less # # What it does # macOS 14.2+: downloads the universal nomus DMG over https (from # dl.agrohi.com, Agrohi's download host, or agrohi.com/nomus for older # versions), checks its sha256 against the published SHA256SUMS, checks the app's # code signature (pinned to the nomus developer team), copies nomus.app into # /Applications (or ~/Applications if /Applications is not writable; no sudo), # and opens it. It does NOT install the microphone driver: nomus does that # itself, with one administrator prompt, the first time you turn on # Microphone. # Debian/Ubuntu (amd64, arm64): downloads the .deb, checks its sha256 (and its # update signature if minisign is installed), and runs # `sudo apt-get install` on it. sudo is used for that step only. # # Options # uninstall remove nomus (macOS: asks before removing the driver) # --check compare the installed version with the latest; change # nothing. Exit 0 up to date, 1 not installed or older, 2 could # not check. # --chrome print the Chrome extension link; offers to open it (y/N) # --version X install version X instead of the latest # --no-open do not open nomus after installing (macOS) # --dry-run print every action, change nothing # --help this help # # Environment (for testing) # NOMUS_BASE_URL default https://agrohi.com/nomus. Plain http is accepted only # for 127.0.0.1, localhost or [::1]. # NOMUS_APP_DIR macOS install folder, default /Applications. # NOMUS_HAL_DIR macOS driver folder checked by uninstall, default # /Library/Audio/Plug-Ins/HAL. # # Everything is inside main(), which is called on the last line, so a download # cut off part way cannot run a partial script: the shell stops at the # unfinished function and nothing has happened yet. # # No analytics: the only requests are to NOMUS_BASE_URL (the update manifest # and SHA256SUMS) and to the installer file's own address. That address comes # from latest.json or a "# url" line in SHA256SUMS (else NOMUS_BASE_URL/download/) # and must be https on agrohi.com or one of its subdomains (dl.agrohi.com), or # NOMUS_BASE_URL/download/; anything else is refused. NOMUS_TEAM_ID="U365D8CG4Q" NOMUS_BUNDLE_ID="com.agrohi.nomus" NOMUS_CHROME_URL="https://chromewebstore.google.com/detail/enmkngmoakghoclllgkmoehngicaoafn" # minisign public key of the nomus updater (the "pubkey" in tauri.conf.json), # base64 of the .pub file, as Tauri stores it. NOMUS_UPDATER_PUBKEY="dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDVBMDRCNjQ2REI2NjVBQ0MKUldUTVdtYmJScllFV25aMFFVUTg3ZmRvZmE4cjNlZzMvTDVOdmpTd2g0RVBweExDM2xQZS9iRnIK" say() { printf 'nomus: %s\n' "$*"; } step() { printf '\n==> %s\n' "$*"; } ok() { printf ' ok: %s\n' "$*"; } note() { printf ' %s\n' "$*"; } warn() { printf ' warning: %s\n' "$*" >&2; } die() { printf '\nnomus: error: %s\n' "$*" >&2 exit "${DIE_RC:-1}" } dry() { [ "$DRY_RUN" = 1 ]; } would() { printf ' [dry run] would %s\n' "$*"; } # The first thing anyone sees: both platforms and what each one does. platforms() { cat <<'EOF' One installer for macOS and Linux. It detects which one this is. macOS 14.2+ (Apple silicon and Intel) Downloads the universal DMG, checks its sha256 and the app's code signature (nomus developer team), copies nomus.app into /Applications (or ~/Applications), opens it. No sudo. The microphone driver is installed later by nomus itself, with one password prompt. Linux: Debian, Ubuntu and relatives (amd64, arm64) Downloads the .deb, checks its sha256 (and its signature if minisign is installed), installs it with apt (sudo for that step only). The nomus microphone is a PipeWire virtual source: no root needed to use it. Other Linux: manual steps are printed. Chrome: --chrome prints the link. EOF } usage() { platforms cat <<'EOF' curl -fsSL https://agrohi.com/nomus/install | sh curl -fsSL https://agrohi.com/nomus/install | sh -s -- [command] [options] Commands install install or upgrade nomus (the default) uninstall remove nomus; on macOS, asks before removing the microphone driver (that step needs sudo) Options --check installed version vs latest, changes nothing (exit 0 up to date, 1 not installed or older, 2 could not check) --chrome print the Chrome extension link; asks y/N before opening it --version X install version X instead of the latest --no-open do not open nomus after installing (macOS) --dry-run print every action, change nothing -h, --help show this help Environment NOMUS_BASE_URL where to download from (default https://agrohi.com/nomus) NOMUS_APP_DIR macOS install folder (default /Applications; falls back to ~/Applications when it is not writable) Read the script first: curl -fsSL https://agrohi.com/nomus/install | less EOF } # Ask a yes/no question on the terminal. Works under `curl ... | sh`, where # stdin is the script, by reading /dev/tty. No terminal means "no". ask_yes_no() { printf '%s ' "$1" >&2 ans="" if { read -r ans /dev/null; then :; else printf '\n' >&2 note "(no terminal to ask on; taking that as no)" return 1 fi case $ans in y | Y | yes | YES | Yes) return 0 ;; *) return 1 ;; esac } has_tty() { [ -e /dev/tty ] && { : /dev/null } # Chrome extension: print the link (and the 0.5.0 note). Never opens a browser # unasked: only on a terminal, only after a "y". chrome_notes() { cat </dev/null 2>&1; then _opener=open elif command -v xdg-open >/dev/null 2>&1; then _opener=xdg-open; fi printf 'nomus Chrome extension:\n %s\n' "$NOMUS_CHROME_URL" printf "'Clean my mic' (0.5.0) is not in the Chrome Web Store yet; the store version does not have it.\n" if dry; then [ -n "$_opener" ] && would "ask whether to open it with $_opener" return 0 fi if [ -n "$_opener" ] && has_tty; then if ask_yes_no "Open it in your browser now? [y/N]"; then "$_opener" "$NOMUS_CHROME_URL" >/dev/null 2>&1 || warn "could not open it; copy the link above" fi fi return 0 } # fetch URL OUTFILE [progress] fetch() { if [ "$ALLOW_HTTP" = 1 ]; then set -- "$1" "$2" "${3:-}" --proto '=http,https' --proto-redir '=http,https' else set -- "$1" "$2" "${3:-}" --proto '=https' --proto-redir '=https' --tlsv1.2 fi _url=$1 _out=$2 _prog=$3 shift 3 if [ -n "$_prog" ] && [ -t 2 ]; then curl --fail --location --retry 2 --connect-timeout 20 --progress-bar "$@" -o "$_out" "$_url" else curl --fail --location --retry 2 --connect-timeout 20 --silent --show-error "$@" -o "$_out" "$_url" fi } sha256_of() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}' elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | awk '{print $1}' else die "neither sha256sum nor shasum is available to check the download" fi } # JSON without jq: the manifests are small and flat. Put every {, } and , on # its own line so this also works if the file is ever minified. json_lines() { awk '{ gsub(/[{},]/, "\n&\n"); print }' "$1" } manifest_version() { json_lines "$1" | sed -n 's/^[[:space:]]*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1 } # Print "url signature" for the first platform entry whose url ends in $2. manifest_entry() { json_lines "$1" | awk -v suffix="$2" ' /^[{]/ { url = ""; sig = "" } /"url"[[:space:]]*:/ { s = $0; sub(/^[^:]*:[[:space:]]*"/, "", s); sub(/".*/, "", s); url = s } /"signature"[[:space:]]*:/ { s = $0; sub(/^[^:]*:[[:space:]]*"/, "", s); sub(/".*/, "", s); sig = s } /^[}]/ { if (url != "" && substr(url, length(url) - length(suffix) + 1) == suffix) { print url, sig; exit } url = ""; sig = "" }' } check_version_string() { case $1 in '' | *[!0-9A-Za-z.+-]* | [!0-9]*) die "not a valid version: '$1'" ;; esac } # Expected sha256 of file $2 from SHA256SUMS file $1. sum_for() { awk -v f="$2" '$2 == f || $2 == "*" f { print $1; exit }' "$1" } verify_sha256() { _file=$1 _name=$2 _sums=$3 _want=$(sum_for "$_sums" "$_name") [ -n "$_want" ] || die "$_name is not listed in SHA256SUMS; not installing it" case $_want in *[!0-9a-f]*) die "SHA256SUMS has a malformed entry for $_name" ;; esac [ ${#_want} -eq 64 ] || die "SHA256SUMS has a malformed entry for $_name" _got=$(sha256_of "$_file") if [ "$_got" != "$_want" ]; then die "checksum mismatch for $_name expected $_want got $_got The download is damaged or was changed. Nothing was installed." fi ok "sha256 $_got matches SHA256SUMS" } # The download address of file $2: the manifest's url ($3, may be empty), else # a "# url
" line in SHA256SUMS ($1), else NOMUS_BASE_URL/download/. # Sets ART_URL. Accepted only if it ends in / and is https on agrohi.com # or a subdomain of it, or is NOMUS_BASE_URL/download/ (the default, and # the local test server). artifact_url() { _sums=$1 _f=$2 _murl=${3:-} ART_URL=$_murl if [ -z "$ART_URL" ]; then ART_URL=$(awk -v f="$_f" '$1 == "#" && $2 == "url" && $3 == f { print $4; exit }' "$_sums") fi [ -n "$ART_URL" ] || ART_URL="$BASE_URL/download/$_f" case $ART_URL in */"$_f") ;; *) die "the address for $_f does not end in its file name: $ART_URL" ;; esac case $ART_URL in "$BASE_URL"/download/"$_f") return 0 ;; esac case $ART_URL in *[!A-Za-z0-9._~:/+-]* | *[!:]//*) die "refusing the download address for $_f (unexpected characters): $ART_URL" ;; esac case $ART_URL in https://*) ;; *) die "refusing a non-https download address: $ART_URL" ;; esac _rest=${ART_URL#https://} _host=${_rest%%/*} case $_host in '' | *[!a-z0-9.-]* | .* | *..*) die "refusing the download address $ART_URL (unexpected host)" ;; agrohi.com | *.agrohi.com) ;; *) die "refusing the download address $ART_URL: only agrohi.com and its subdomains are trusted" ;; esac } cleanup() { if [ -n "${MOUNT_POINT:-}" ] && [ "${MOUNTED:-0}" = 1 ]; then hdiutil detach "$MOUNT_POINT" -quiet >/dev/null 2>&1 || hdiutil detach "$MOUNT_POINT" -force -quiet >/dev/null 2>&1 || true MOUNTED=0 fi if [ -n "${WORK_DIR:-}" ] && [ -d "$WORK_DIR" ]; then rm -rf "$WORK_DIR" fi } make_work_dir() { WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/nomus-install.XXXXXX") || die "could not create a temporary folder" trap cleanup EXIT trap 'cleanup; exit 130' INT trap 'cleanup; exit 143' TERM } # Compare an installed version ($1, may be empty) with the latest in the # manifest at $2. Exit 0 same, 1 different or not installed. check_report() { _inst=$1 _manifest=$2 DIE_RC=2 make_work_dir fetch "$_manifest" "$WORK_DIR/latest.json" || die "could not download $_manifest" _latest=$(manifest_version "$WORK_DIR/latest.json") check_version_string "$_latest" DIE_RC=1 printf 'installed: %s\nlatest: %s\n' "${_inst:-not installed}" "$_latest" if [ "$_inst" = "$_latest" ]; then say "up to date" exit 0 elif [ -z "$_inst" ]; then say "not installed. Install it: curl -fsSL https://agrohi.com/nomus/install | sh" else say "a newer version is available. Upgrade: curl -fsSL https://agrohi.com/nomus/install | sh" fi exit 1 } # ---------------------------------------------------------------- macOS ---- mac_check() { _inst="" for _d in "$APP_DIR" "$HOME/Applications"; do if [ -d "$_d/nomus.app" ]; then _inst=$(mac_app_version "$_d/nomus.app"); break; fi done check_report "$_inst" "$BASE_URL/releases/darwin/$ARCH/latest.json" } mac_app_version() { plutil -extract CFBundleShortVersionString raw -o - "$1/Contents/Info.plist" 2>/dev/null || true } mac_running_from() { pgrep -f "$1/nomus.app/Contents/MacOS/" >/dev/null 2>&1 } # Ask a running nomus (the copy in folder $1) to quit the normal way, the same # as Quit in its menu, so it can put the default input back. Never kill -9. mac_quit_app() { _dir=$1 mac_running_from "$_dir" || return 0 step "nomus is running from $_dir" if dry; then would "ask it to quit (osascript: tell application \"$_dir/nomus.app\" to quit)" return 0 fi if [ -e /dev/tty ] && { : /dev/null; then ask_yes_no " It has to quit before it can be replaced or removed. Quit nomus now? [Y/n]" || case $ans in '') : ;; *) die "nomus is still running. Quit it from its menu bar icon, then run this again." ;; esac else note "Asking it to quit, as if you chose Quit from its menu." fi osascript -e "tell application \"$_dir/nomus.app\" to quit" >/dev/null 2>&1 || true _i=0 while mac_running_from "$_dir"; do _i=$((_i + 1)) [ $_i -le 20 ] || die "nomus did not quit. Quit it from its menu bar icon, then run this again." sleep 1 done ok "nomus quit" } mac_check_os() { _v=$(sw_vers -productVersion 2>/dev/null) || die "could not read the macOS version (sw_vers)" _major=${_v%%.*} case $_v in *.*) _rest=${_v#*.}; _minor=${_rest%%.*} ;; *) _minor=0 ;; esac case $_major$_minor in *[!0-9]*) die "could not understand the macOS version '$_v'" ;; esac if [ "$_major" -gt 14 ] || { [ "$_major" -eq 14 ] && [ "$_minor" -ge 2 ]; }; then ok "macOS $_v ($ARCH)" else die "nomus needs macOS 14.2 or newer (it uses the Core Audio process tap added in 14.2). This Mac has $_v." fi } mac_pick_dir() { DEST=$APP_DIR if [ ! -d "$DEST" ] && [ "$APP_DIR_SET" = 1 ]; then if dry; then would "create $DEST" return 0 fi mkdir -p "$DEST" 2>/dev/null || true fi if [ -d "$DEST" ] && [ -w "$DEST" ]; then return 0 fi _fallback="$HOME/Applications" note "$DEST is not writable without an administrator; using $_fallback instead (no sudo)." DEST=$_fallback if [ ! -d "$DEST" ]; then if dry; then would "create $DEST"; else mkdir -p "$DEST" || die "could not create $DEST"; fi fi if ! dry; then [ -w "$DEST" ] || die "$DEST is not writable either"; fi } mac_summary() { cat </dev/null 2>&1 || die "hdiutil not found" command -v ditto >/dev/null 2>&1 || die "ditto not found" command -v codesign >/dev/null 2>&1 || die "codesign not found" make_work_dir step "Finding the version to install" if [ -n "$WANT_VERSION" ]; then VERSION=$WANT_VERSION ok "version $VERSION (asked for with --version)" else _manifest="$BASE_URL/releases/darwin/$ARCH/latest.json" fetch "$_manifest" "$WORK_DIR/latest.json" || die "could not download $_manifest" VERSION=$(manifest_version "$WORK_DIR/latest.json") check_version_string "$VERSION" ok "latest is $VERSION ($_manifest)" fi check_version_string "$VERSION" DMG="nomus_${VERSION}_universal.dmg" fetch "$BASE_URL/download/SHA256SUMS" "$WORK_DIR/SHA256SUMS" || die "could not download $BASE_URL/download/SHA256SUMS" [ -n "$(sum_for "$WORK_DIR/SHA256SUMS" "$DMG")" ] || die "$DMG is not published (not in SHA256SUMS). Check the version." ok "$DMG is listed in SHA256SUMS" artifact_url "$WORK_DIR/SHA256SUMS" "$DMG" "" DMG_URL=$ART_URL # Already installed? _existing="" for _d in "$APP_DIR" "$HOME/Applications"; do if [ -d "$_d/nomus.app" ]; then _existing="$_d"; break; fi done if [ -n "$_existing" ]; then _have=$(mac_app_version "$_existing/nomus.app") if [ "$_have" = "$VERSION" ]; then step "nomus $VERSION is already installed in $_existing" ok "nothing to do" DEST=$_existing mac_open mac_done_notes return 0 fi note "Found nomus ${_have:-(unknown version)} in $_existing; it will be replaced by $VERSION." fi step "Choosing the install folder" mac_pick_dir ok "$DEST" if [ -n "$_existing" ] && [ "$_existing" != "$DEST" ]; then warn "an older nomus stays in $_existing (that folder is not writable without an administrator). Remove it there to avoid opening the old one." fi step "Downloading $DMG" if dry; then would "download $DMG_URL to a temporary folder" would "check its sha256 against SHA256SUMS: $(sum_for "$WORK_DIR/SHA256SUMS" "$DMG")" would "mount it read-only (hdiutil attach -nobrowse -readonly) and check the code signature" mac_quit_app "$DEST" would "copy nomus.app to $DEST/nomus.app (ditto), replacing any older copy" would "remove the quarantine flag: xattr -dr com.apple.quarantine $DEST/nomus.app" would "unmount the DMG and delete the temporary folder" mac_open mac_done_notes return 0 fi note "from $DMG_URL" fetch "$DMG_URL" "$WORK_DIR/$DMG" progress || die "could not download $DMG_URL" ok "downloaded ($(wc -c <"$WORK_DIR/$DMG" | tr -d ' ') bytes)" step "Checking the download" # This sha256 comes from the same server as the DMG, so it protects against # a damaged or truncated download, not against a compromised server. The # code-signature check below is what ties the app to the nomus developer: # a replaced app would have to be signed by Apple-issued certificates of # team $NOMUS_TEAM_ID. (The minisign signature in latest.json covers the # updater .app.tar.gz, not this DMG, so there is nothing for minisign to # check here.) verify_sha256 "$WORK_DIR/$DMG" "$DMG" "$WORK_DIR/SHA256SUMS" step "Mounting the DMG (read-only, hidden from Finder)" MOUNT_POINT="$WORK_DIR/mnt" mkdir -p "$MOUNT_POINT" if hdiutil attach -nobrowse -readonly -noautoopen -mountpoint "$MOUNT_POINT" "$WORK_DIR/$DMG" \ "$WORK_DIR/hdiutil.log" 2>&1; then MOUNTED=1 ok "mounted" else sed 's/^/ /' "$WORK_DIR/hdiutil.log" >&2 die "could not mount $DMG" fi _src="$MOUNT_POINT/nomus.app" [ -d "$_src" ] || die "the DMG has no nomus.app" step "Checking the app's code signature" _req="anchor apple generic and certificate leaf[subject.OU] = \"$NOMUS_TEAM_ID\" and identifier \"$NOMUS_BUNDLE_ID\"" if codesign --verify --deep --strict -R="$_req" "$_src" 2>"$WORK_DIR/codesign.log"; then ok "signed by team $NOMUS_TEAM_ID, identifier $NOMUS_BUNDLE_ID" else sed 's/^/ /' "$WORK_DIR/codesign.log" >&2 die "the app in the DMG is not signed by the nomus developer. Nothing was installed." fi _src_version=$(mac_app_version "$_src") [ "$_src_version" = "$VERSION" ] || die "the DMG holds version '$_src_version', expected $VERSION" ok "version $_src_version" mac_quit_app "$DEST" step "Installing nomus.app into $DEST" _new="$DEST/.nomus.app.new.$$" _old="$DEST/.nomus.app.old.$$" rm -rf "$_new" "$_old" if ! ditto "$_src" "$_new"; then rm -rf "$_new" die "could not copy nomus.app into $DEST" fi if [ -e "$DEST/nomus.app" ]; then mv "$DEST/nomus.app" "$_old" || { rm -rf "$_new"; die "could not move the old nomus.app aside"; } fi if ! mv "$_new" "$DEST/nomus.app"; then [ -e "$_old" ] && mv "$_old" "$DEST/nomus.app" rm -rf "$_new" die "could not put the new nomus.app in place; the old one was kept" fi rm -rf "$_old" # curl does not set the quarantine flag, so there should be none. Clear it # anyway in case an older copy had it. xattr -dr com.apple.quarantine "$DEST/nomus.app" 2>/dev/null || true if xattr -r "$DEST/nomus.app" 2>/dev/null | grep -q com.apple.quarantine; then warn "the quarantine flag is still set; run: xattr -dr com.apple.quarantine \"$DEST/nomus.app\"" else ok "no quarantine flag" fi if codesign --verify --deep --strict "$DEST/nomus.app" 2>/dev/null; then ok "installed copy passes codesign --verify --deep --strict" else die "the installed copy fails codesign --verify; remove $DEST/nomus.app and try again" fi step "Cleaning up" hdiutil detach "$MOUNT_POINT" -quiet >/dev/null 2>&1 || hdiutil detach "$MOUNT_POINT" -force -quiet >/dev/null 2>&1 || warn "could not unmount $MOUNT_POINT" MOUNTED=0 ok "DMG unmounted" step "Installed nomus $VERSION in $DEST/nomus.app" mac_open mac_done_notes } mac_open() { if [ "$NO_OPEN" = 1 ]; then note "Not opening nomus (--no-open). Open it from $DEST." return 0 fi if dry; then would "open $DEST/nomus.app" return 0 fi if open "$DEST/nomus.app"; then ok "opened nomus"; else warn "could not open $DEST/nomus.app; open it from Finder"; fi } mac_done_notes() { cat < Privacy & Security > Screen & System Audio Recording). 2. Turn Microphone on in nomus. One password prompt installs the microphone driver (this script does not). 3. In your call app (Zoom, Meet, Teams ...) choose "nomus Microphone" as the microphone. WhatsApp and FaceTime follow the system default input, so they only use it if the Experimental "default input" switch in nomus is on. - nomus updates itself from now on (About > Check for updates). EOF chrome_notes cat </dev/null && [ ! -e "$_d/nomus.app" ]; then ok "removed" else warn "could not remove $_d/nomus.app (it needs an administrator). Drag it to the Trash in Finder." fi done [ "$_found" = 1 ] || { step "Removing the app"; ok "nomus.app is not installed in $APP_DIR or ~/Applications"; } step "Microphone driver" if [ ! -e "$_driver" ]; then ok "not installed ($_driver); nothing to remove" else note "Found $_driver. Removing it needs sudo and restarts Core Audio" note "(sound stops for a second; apps using audio may need a moment)." note "Commands: sudo rm -rf \"$_driver\" && sudo killall coreaudiod" if dry; then would "ask, then run: sudo rm -rf \"$_driver\" && sudo killall coreaudiod" elif ask_yes_no " Remove the nomus microphone driver now? [y/N]"; then if sudo rm -rf "$_driver" && sudo killall coreaudiod; then ok "driver removed and Core Audio restarted" else die "could not remove the driver; run the commands above yourself" fi else note "Kept the driver. Remove it later with the commands above." fi fi step "Done" note "To delete your settings too: rm -rf ~/Library/Application\\ Support/$NOMUS_BUNDLE_ID" } # ---------------------------------------------------------------- Linux ---- linux_manual() { cat >&2 <_.deb Fedora, Arch, openSUSE and others have no package yet. Chrome users can use the extension instead (run this script with --chrome). EOF exit 2 } as_root() { if [ "$(id -u)" = 0 ]; then "$@"; else sudo "$@"; fi } linux_need_sudo() { [ "$(id -u)" = 0 ] && return 0 command -v sudo >/dev/null 2>&1 || die "this step needs root and sudo is not installed; run it as root" note "$1 needs administrator rights, so sudo may ask for your password now." note "sudo is used for this step only." } linux_installed_version() { command -v dpkg-query >/dev/null 2>&1 || return 0 dpkg-query -W -f='${Status} ${Version}\n' nomus 2>/dev/null | awk '$1 == "install" && $3 == "installed" { print $4 }' || true } linux_check() { _da=$(dpkg --print-architecture 2>/dev/null || true) case $_da in amd64) ARCH=x86_64 ;; arm64) ARCH=aarch64 ;; esac check_report "$(linux_installed_version)" "$BASE_URL/releases/linux/$ARCH/latest.json" } linux_summary() { cat <.deb (sudo for this step only). EOF } linux_install() { [ "$DRY_RUN" = 1 ] && say "dry run: nothing will be changed." if ! command -v apt-get >/dev/null 2>&1 || ! command -v dpkg >/dev/null 2>&1; then say "this Linux system has no apt/dpkg." linux_manual fi # The package architecture is what dpkg says (a 32-bit userland on a 64-bit # kernel must not get a 64-bit .deb). _da=$(dpkg --print-architecture 2>/dev/null || true) case $_da in amd64) ARCH=x86_64 DEB_ARCH=amd64 ;; arm64) ARCH=aarch64 DEB_ARCH=arm64 ;; *) die "this system's package architecture is '${_da:-unknown}'. nomus has .deb packages for amd64 and arm64 only." ;; esac linux_summary step "Checking this system" ok "Linux $ARCH with apt ($DEB_ARCH)" make_work_dir step "Finding the version to install" _sig="" _murl="" if [ -n "$WANT_VERSION" ]; then check_version_string "$WANT_VERSION" VERSION=$WANT_VERSION DEB="nomus_${VERSION}_${DEB_ARCH}.deb" ok "version $VERSION (asked for with --version)" else _manifest="$BASE_URL/releases/linux/$ARCH/latest.json" fetch "$_manifest" "$WORK_DIR/latest.json" || die "could not download $_manifest" VERSION=$(manifest_version "$WORK_DIR/latest.json") check_version_string "$VERSION" _entry=$(manifest_entry "$WORK_DIR/latest.json" "_${DEB_ARCH}.deb") [ -n "$_entry" ] || die "the manifest has no .deb for $DEB_ARCH" _murl=${_entry%% *} DEB=$(basename "$_murl") _sig=${_entry#* } [ "$_sig" != "$_entry" ] || _sig="" ok "latest is $VERSION ($_manifest)" fi case $DEB in nomus_*_amd64.deb | nomus_*_arm64.deb) ;; *) die "unexpected file name in the manifest: $DEB" ;; esac case $DEB in *[!A-Za-z0-9._+-]*) die "unexpected file name in the manifest: $DEB" ;; esac fetch "$BASE_URL/download/SHA256SUMS" "$WORK_DIR/SHA256SUMS" || die "could not download $BASE_URL/download/SHA256SUMS" [ -n "$(sum_for "$WORK_DIR/SHA256SUMS" "$DEB")" ] || die "$DEB is not published (not in SHA256SUMS). Check the version." ok "$DEB is listed in SHA256SUMS" artifact_url "$WORK_DIR/SHA256SUMS" "$DEB" "$_murl" DEB_URL=$ART_URL _have=$(linux_installed_version) if [ "$_have" = "$VERSION" ]; then step "nomus $VERSION is already installed" ok "nothing to do" linux_done_notes return 0 fi [ -n "$_have" ] && note "Found nomus $_have; it will be upgraded to $VERSION." step "Downloading $DEB" if dry; then would "download $DEB_URL to a temporary folder" would "check its sha256 against SHA256SUMS: $(sum_for "$WORK_DIR/SHA256SUMS" "$DEB")" would "run: sudo apt-get install -y /$DEB" return 0 fi note "from $DEB_URL" fetch "$DEB_URL" "$WORK_DIR/$DEB" progress || die "could not download $DEB_URL" ok "downloaded ($(wc -c <"$WORK_DIR/$DEB" | tr -d ' ') bytes)" step "Checking the download" # The sha256 comes from the same server as the .deb: it catches a damaged # download, not a compromised server. The minisign signature (made with the # nomus updater key, whose public half is in this script) does tie the file # to the nomus release key, so it is checked when minisign is installed. verify_sha256 "$WORK_DIR/$DEB" "$DEB" "$WORK_DIR/SHA256SUMS" if [ -z "$_sig" ]; then note "signature check skipped (no signature for --version installs)" elif ! command -v minisign >/dev/null 2>&1; then note "signature check skipped (minisign is not installed; optional)" else printf '%s' "$NOMUS_UPDATER_PUBKEY" | base64 -d >"$WORK_DIR/nomus.pub" 2>/dev/null || die "could not decode the public key" printf '%s' "$_sig" | base64 -d >"$WORK_DIR/$DEB.minisig" 2>/dev/null || die "could not decode the signature from the manifest" if minisign -V -q -p "$WORK_DIR/nomus.pub" -x "$WORK_DIR/$DEB.minisig" -m "$WORK_DIR/$DEB" >/dev/null 2>&1; then ok "minisign signature is valid (nomus updater key)" else die "the minisign signature does not match $DEB. Nothing was installed." fi fi step "Installing $DEB" # apt reads the file as its _apt user; let it. chmod 755 "$WORK_DIR" chmod 644 "$WORK_DIR/$DEB" linux_need_sudo "Installing a .deb" if as_root apt-get install -y "$WORK_DIR/$DEB" Check for updates). EOF chrome_notes cat </dev/null 2>&1 || { say "this system has no dpkg; remove nomus with your package manager."; exit 2; } step "Removing the nomus package" _have=$(linux_installed_version) if [ -z "$_have" ]; then ok "nomus is not installed; nothing to do" return 0 fi if dry; then would "run: sudo apt-get remove -y nomus (installed: $_have)" return 0 fi linux_need_sudo "Removing a package" if as_root apt-get remove -y nomus &2 die "unknown option: $1" ;; esac shift done if [ -n "$WANT_VERSION" ]; then check_version_string "$WANT_VERSION"; fi # Where to download from. https only, except an explicit local test server. ALLOW_HTTP=0 BASE_URL=${NOMUS_BASE_URL:-https://agrohi.com/nomus} while :; do case $BASE_URL in */) BASE_URL=${BASE_URL%/} ;; *) break ;; esac done case $BASE_URL in https://?*) ;; http://127.0.0.1 | http://127.0.0.1[:/]* | http://localhost | http://localhost[:/]* | "http://[::1]" | "http://[::1]"[:/]*) if [ -n "${NOMUS_BASE_URL:-}" ]; then ALLOW_HTTP=1; else die "plain http is not allowed"; fi ;; *) die "NOMUS_BASE_URL must start with https:// (plain http only for 127.0.0.1 or localhost): $BASE_URL" ;; esac if [ "$COMMAND" = chrome ]; then chrome_cmd exit 0 fi command -v curl >/dev/null 2>&1 || die "curl is needed to download nomus (install it with your package manager, then run this again)" APP_DIR_SET=0 [ -n "${NOMUS_APP_DIR:-}" ] && APP_DIR_SET=1 APP_DIR=${NOMUS_APP_DIR:-/Applications} APP_DIR=${APP_DIR%/} [ -n "$APP_DIR" ] || APP_DIR=/ : "${HOME:?HOME is not set}" case $(uname -m) in arm64 | aarch64) ARCH=aarch64 DEB_ARCH=arm64 ;; x86_64 | amd64) ARCH=x86_64 DEB_ARCH=amd64 ;; *) die "this processor ($(uname -m)) is not supported. nomus runs on 64-bit Arm and x86." ;; esac OS=$(uname -s) if [ "$OS" = Darwin ]; then # A shell running under Rosetta reports x86_64 on an Apple silicon Mac. if [ "$(sysctl -n sysctl.proc_translated 2>/dev/null || true)" = 1 ] || [ "$(sysctl -n hw.optional.arm64 2>/dev/null || true)" = 1 ]; then ARCH=aarch64 DEB_ARCH=arm64 fi fi case $OS in Darwin) _where="macOS $ARCH" ;; Linux) _where="Linux $ARCH" ;; *) _where=$OS ;; esac printf 'nomus installer: macOS and Linux, one script. This machine: %s.\n\n' "$_where" platforms say "running: $COMMAND (from $BASE_URL)" case $OS in Darwin) case $COMMAND in uninstall) mac_uninstall ;; check) mac_check ;; *) mac_install ;; esac ;; Linux) case $COMMAND in uninstall) linux_uninstall ;; check) linux_check ;; *) linux_install ;; esac ;; *) die "this installer supports macOS and Linux. On $OS, use the Chrome extension: $NOMUS_CHROME_URL (or run this script with --chrome on a Mac or Linux box)" ;; esac } main "$@"