#!/bin/sh # Venutian installer for macOS and Linux. # # curl -fsSL https://venutian.org/install.sh | sh # # The same command installs, upgrades and repairs. It detects your system, # reads https://venutian.org/releases/latest.json, downloads the matching # release over HTTPS, verifies its SHA 256, unpacks it into # ~/.venutian/versions//, points ~/.venutian/current at it in one # rename, links `venutian` into ~/.local/bin, and installs the daily update # notifier. It keeps the previous version for `venutian-notifier rollback`. # # Options (environment variables, so they work through a pipe): # VENUTIAN_VERSION=x.y.z install this version instead of the latest # VENUTIAN_CHANNEL=nightly follow daily builds instead of stable # VENUTIAN_HOME= install somewhere other than ~/.venutian # VENUTIAN_NO_MODIFY_PATH=1 leave your shell profile alone # VENUTIAN_NO_UPDATE_CHECK=1 install without the update notifier # VENUTIAN_DRY_RUN=1 print every step and change nothing # DO_NOT_TRACK=1 download straight from /downloads/, uncounted # # Everything lives in one function called on the last line, so a truncated # download never runs half a script. Output is plain ASCII. VI_SITE=https://venutian.org VI_MANUAL=https://venutian.org/download/ VI_ONE_LINER='curl -fsSL https://venutian.org/install.sh | sh' VI_PARAGRAPH='Venutian checks venutian.org once a day for new releases and libraries and tells you here. It sends nothing about you. Turn it off: venutian-notifier off' vi_say() { printf '%s\n' "$*"; } vi_step() { printf '%s\n' "- $*"; } vi_err() { printf '%s\n' "venutian install: $*" >&2 exit 1 } vi_flag() { case ${1-} in '' | 0 | false | FALSE | False | no | off) return 1 ;; esac return 0 } vi_dry() { vi_flag "${VENUTIAN_DRY_RUN-}"; } vi_has() { command -v "$1" >/dev/null 2>&1; } vi_okv() { printf '%s\n' "$1" | awk '{ exit !(length($0) <= 64 && $0 ~ /^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$/) }' } # The notifier files. The site build inlines them here from # source/installers/notifier/ (step 70), so the served install.sh is one file. vi_payload() { vi_emit "$1/notifier.sh" <<'VENUTIAN_NOTIFIER_PAYLOAD_EOF' #!/bin/sh # Venutian update notifier. # # Tells you, in your own terminal, when a new Venutian release or new and # updated libraries are out. It reads one static file once a day # (https://venutian.org/api/v1/updates.json) and sends nothing about you: # no machine id, no install id, no cookie, no project names. # # The wrapper (~/.local/bin/venutian) calls two subcommands after every # toolchain run: # after-run print the waiting notice on stderr (once a day; security # advisories on every run), then start a background refresh # when the last check is older than the interval # maybe-print and maybe-refresh do the two halves separately. # People call it through venutian-notifier: # status, check --now, off, on, channel stable|nightly, # libraries [--fetch ], desktop on|off, rollback # # POSIX sh only. Every field from the server is validated before use and every # sentence is composed here, so a hostile manifest can never print an escape # sequence, a fake command or a foreign URL. NOTIFIER_VERSION=1.0.0 SITE=https://venutian.org UPGRADE_CMD='curl -fsSL https://venutian.org/install.sh | sh' DOWNLOAD_PAGE=https://venutian.org/download/ MANIFEST_CAP=262144 PACKAGES_CAP=4194304 LC_ALL=C export LC_ALL umask 022 # =========================================================================== # Locations # =========================================================================== # Prints the directory part of a path (dirname without a fork). nf_dirname() { case $1 in */*) nf_dn=${1%/*}; printf '%s\n' "${nf_dn:-/}" ;; *) printf '.\n' ;; esac } # Prints the physical directory holding $1, following any symlink chain. nf_real_dir() { nf_src=$1 while [ -h "$nf_src" ]; do nf_dir=$(cd -P "$(nf_dirname "$nf_src")" 2>/dev/null && pwd) || return 1 nf_lnk=$(readlink "$nf_src") || return 1 case $nf_lnk in /*) nf_src=$nf_lnk ;; *) nf_src=$nf_dir/$nf_lnk ;; esac done case $nf_src in */*) nf_dn=${nf_src%/*} ;; *) nf_dn=. ;; esac cd -P "${nf_dn:-/}" 2>/dev/null && pwd } nf_init_paths() { [ -n "${HOME-}" ] || return 1 NDIR=$(nf_real_dir "$0") || NDIR= NF_SELF=$0 if [ -n "$NDIR" ] && [ -f "$NDIR/notifier.sh" ]; then NF_SELF=$NDIR/notifier.sh; fi case $NDIR in */lib/notifier) VH=${NDIR%/lib/notifier} ;; *) VH=${VENUTIAN_HOME:-$HOME/.venutian} ;; esac case ${XDG_STATE_HOME-} in /*) nf_state_root=$XDG_STATE_HOME ;; *) nf_state_root=$HOME/.local/state ;; esac case ${XDG_CONFIG_HOME-} in /*) nf_conf_root=$XDG_CONFIG_HOME ;; *) nf_conf_root=$HOME/.config ;; esac case ${XDG_CACHE_HOME-} in /*) nf_cache_root=$XDG_CACHE_HOME ;; *) nf_cache_root=$HOME/.cache ;; esac ST=$nf_state_root/venutian/notifier CONF=$nf_conf_root/venutian/config CACHE=$nf_cache_root/venutian INSTALLED=$ST/installed.json return 0 } # VENUTIAN_BASE_URL is honoured only for the local test server on 127.0.0.1, # so it can never point the notifier anywhere else in the field. nf_init_base() { BASE=$SITE LOOPBACK=0 nf_b=${VENUTIAN_BASE_URL-} nf_b=${nf_b%/} case $nf_b in http://127.0.0.1) BASE=$nf_b; LOOPBACK=1 ;; http://127.0.0.1:*) nf_port=${nf_b#http://127.0.0.1:} case $nf_port in '' | *[!0-9]*) ;; *) if [ "${#nf_port}" -le 5 ]; then BASE=$nf_b; LOOPBACK=1; fi ;; esac ;; esac } nf_platform() { case $(uname -s 2>/dev/null) in Darwin) OS=macos ;; Linux) OS=linux ;; *) OS=unix ;; esac case $(uname -m 2>/dev/null) in arm64 | aarch64) ARCH=arm64 ;; x86_64 | amd64) ARCH=x86_64 ;; *) ARCH=other ;; esac } # =========================================================================== # Small helpers # =========================================================================== nf_now() { date +%s; } nf_today() { date -u +%Y-%m-%d; } nf_is_uint() { case $1 in '' | *[!0-9]*) return 1 ;; esac [ "${#1}" -le 12 ] } # True when an opt out variable is set to anything except empty, 0 or false. nf_flag_set() { case $1 in '' | 0 | false | FALSE | False | no | off) return 1 ;; esac return 0 } # Writes stdin to $1 through a temp file and a rename, so readers never see a # half written file. nf_atomic_write() { nf_t=$1.tmp.$$ cat >"$nf_t" 2>/dev/null && mv -f "$nf_t" "$1" 2>/dev/null && return 0 rm -f "$nf_t" 2>/dev/null return 1 } # Records a check now: last-check (epoch seconds) and next-check (when the # next one is due). The wrapper reads next-check to skip starting the # notifier at all on most runs. nf_mark_checked() { nf_mc=$(nf_now) nf_is_uint "$nf_mc" || return 1 nf_confv interval_hours printf '%s\n' "$nf_mc" | nf_atomic_write "$ST/last-check" printf '%s\n' "$((nf_mc + nf_v * 3600))" | nf_atomic_write "$ST/next-check" } # Removes every byte below 0x20 except newline, and DEL and every byte above # 0x7e, from stdin. Output is plain printable ASCII. nf_clean() { tr -d '\000-\011\013-\037\177-\377' } # =========================================================================== # Config (~/.config/venutian/config): key = value lines # =========================================================================== nf_conf_raw() { [ -f "$CONF" ] || return 0 awk -v k="$1" ' { line = $0 sub(/#.*/, "", line) i = index(line, "=") if (!i) next key = substr(line, 1, i - 1); val = substr(line, i + 1) gsub(/^[ \t]+|[ \t\r]+$/, "", key); gsub(/^[ \t]+|[ \t\r]+$/, "", val) if (key == k) r = val } END { print r }' "$CONF" 2>/dev/null } # nf_conf key -> validated value or the default, on stdout nf_conf() { nf_confv "$1" printf '%s\n' "$nf_v" } # nf_confv key -> the same value in $nf_v without a subshell. The wrapper # path calls it on every interactive run, so it forks nothing when there is # no config file (the default). nf_confv() { nf_v= if [ -f "$CONF" ]; then nf_v=$(nf_conf_raw "$1"); fi case $1 in update_check | libraries) case $nf_v in on | off) ;; *) nf_v=on ;; esac ;; desktop) case $nf_v in on | off) ;; *) nf_v=off ;; esac ;; channel) case $nf_v in stable | nightly) ;; *) nf_v= ;; esac ;; interval_hours) if nf_is_uint "$nf_v"; then [ "$nf_v" -ge 1 ] || nf_v=1 [ "$nf_v" -le 8760 ] || nf_v=8760 else nf_v=24 fi ;; esac } nf_conf_set() { mkdir -p "$(dirname "$CONF")" 2>/dev/null || return 1 { if [ -f "$CONF" ]; then awk -v k="$1" ' { line = $0; sub(/#.*/, "", line) i = index(line, "=") if (i) { key = substr(line, 1, i - 1); gsub(/^[ \t]+|[ \t\r]+$/, "", key); if (key == k) next } print }' "$CONF" else printf '%s\n' "# Venutian notifier settings. venutian-notifier writes this file." fi printf '%s = %s\n' "$1" "$2" } | nf_atomic_write "$CONF" } # =========================================================================== # Silencers # =========================================================================== nf_ci() { for nf_var in "${CI-}" "${CONTINUOUS_INTEGRATION-}" "${BUILD_NUMBER-}" \ "${GITHUB_ACTIONS-}" "${GITLAB_CI-}" "${TF_BUILD-}" "${BUILDKITE-}" \ "${JENKINS_URL-}" "${CIRCLECI-}" "${TRAVIS-}" "${TEAMCITY_VERSION-}" \ "${CODEBUILD_BUILD_ID-}"; do nf_flag_set "$nf_var" && return 0 done return 1 } # True when the notifier must stay silent and offline. nf_silenced() { nf_flag_set "${VENUTIAN_NO_UPDATE_CHECK-}" && return 0 nf_flag_set "${DO_NOT_TRACK-}" && return 0 nf_ci && return 0 nf_confv update_check [ "$nf_v" = off ] && return 0 return 1 } # =========================================================================== # JSON, validation and version order (awk, portable to BWK awk, mawk, gawk # and busybox awk: no interval expressions, no gawk extensions) # =========================================================================== # Flattens a JSON document into "pathvalue" lines (object keys and array # indexes joined by dots). Strings are kept raw, escapes included, and later # rejected by the validators. A raw control byte inside a string, trailing # garbage or nesting deeper than 24 makes it exit 3. # shellcheck disable=SC2016 # an awk program, not shell NF_JSON_AWK=' BEGIN { for (i = 1; i < 32; i++) ctl = ctl sprintf("%c", i); err = 0 } { buf = buf $0 "\n" } function ws( c) { while (pos <= n) { c = substr(buf, pos, 1) if (c == " " || c == "\t" || c == "\n" || c == "\r") pos++ else break } } function str( c, st) { pos++; st = pos while (pos <= n) { c = substr(buf, pos, 1) if (c == "\\") { pos += 2; continue } if (c == "\"") { pos++; return substr(buf, st, pos - st - 1) } if (index(ctl, c)) { err = 1; return "" } pos++ } err = 1; return "" } function val(p, d, c, k, i, st) { if (err) return if (d > 24) { err = 1; return } ws(); c = substr(buf, pos, 1) if (c == "{") { pos++; ws() if (substr(buf, pos, 1) == "}") { pos++; return } for (;;) { ws() if (substr(buf, pos, 1) != "\"") { err = 1; return } k = str(); if (err) return ws() if (substr(buf, pos, 1) != ":") { err = 1; return } pos++ val((p == "") ? k : (p "." k), d + 1); if (err) return ws(); c = substr(buf, pos, 1); pos++ if (c == "}") return if (c != ",") { err = 1; return } } } if (c == "[") { pos++; ws(); i = 0 if (substr(buf, pos, 1) == "]") { pos++; return } for (;;) { val((p == "") ? i : (p "." i), d + 1); if (err) return i++ ws(); c = substr(buf, pos, 1); pos++ if (c == "]") return if (c != ",") { err = 1; return } } } if (c == "\"") { k = str(); if (!err) print p "\t" k; return } st = pos while (pos <= n && index("+-.0123456789Eaeflnrstu", substr(buf, pos, 1))) pos++ if (pos == st) { err = 1; return } print p "\t" substr(buf, st, pos - st) } END { n = length(buf); pos = 1 val("", 0) ws() if (!err && pos <= n) err = 1 if (err) exit 3 } ' # Shared awk functions: validators, version order and advisory ranges. # shellcheck disable=SC2016 # an awk program, not shell NF_LIB_AWK=' function okv(v) { return length(v) <= 64 && v ~ /^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$/ } function okn(v) { return length(v) <= 64 && v ~ /^[a-z][a-z0-9_-]*$/ } function okd(v) { return v ~ /^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$/ } function oka(v) { return length(v) <= 32 && v ~ /^VSA-[0-9][0-9][0-9][0-9]-[0-9][0-9][0-9]+$/ } function oku(v) { return length(v) <= 12 && v ~ /^[0-9]+$/ } function okh(v) { return length(v) == 64 && v ~ /^[0-9a-f]+$/ } function okf(v) { return length(v) <= 128 && v ~ /^[A-Za-z0-9][A-Za-z0-9._-]*$/ } function oks(v) { return v == "low" || v == "medium" || v == "high" || v == "critical" } function opof(t) { if (substr(t, 1, 2) == "<=" || substr(t, 1, 2) == ">=") return substr(t, 1, 2) if (substr(t, 1, 1) == "<" || substr(t, 1, 1) == ">" || substr(t, 1, 1) == "=") return substr(t, 1, 1) return "" } function okr(v, n, parts, i, t) { if (length(v) > 160) return 0 n = split(v, parts, " ") if (n < 1 || n > 4) return 0 for (i = 1; i <= n; i++) { t = parts[i]; t = substr(t, length(opof(t)) + 1); if (!okv(t)) return 0 } return 1 } function vcmp(a, b, ap, bp, i, x, y, na, nb, ia, ib) { ap = ""; bp = "" i = index(a, "-"); if (i) { ap = substr(a, i + 1); a = substr(a, 1, i - 1) } i = index(b, "-"); if (i) { bp = substr(b, i + 1); b = substr(b, 1, i - 1) } split(a, x, "."); split(b, y, ".") for (i = 1; i <= 3; i++) { if (x[i] + 0 < y[i] + 0) return -1 if (x[i] + 0 > y[i] + 0) return 1 } if (ap == bp) return 0 if (ap == "") return 1 if (bp == "") return -1 na = split(ap, x, "."); nb = split(bp, y, ".") for (i = 1; i <= na && i <= nb; i++) { if (x[i] == y[i]) continue ia = (x[i] ~ /^[0-9]+$/); ib = (y[i] ~ /^[0-9]+$/) if (ia && ib) return (x[i] + 0 < y[i] + 0) ? -1 : 1 if (ia) return -1 if (ib) return 1 return (x[i] < y[i]) ? -1 : 1 } if (na < nb) return -1 if (na > nb) return 1 return 0 } function inrange(v, r, n, parts, i, t, op, c) { n = split(r, parts, " ") for (i = 1; i <= n; i++) { t = parts[i]; op = opof(t); t = substr(t, length(op) + 1) if (op == "") op = "=" c = vcmp(v, t) if (op == "<" && c >= 0) return 0 if (op == "<=" && c > 0) return 0 if (op == ">" && c <= 0) return 0 if (op == ">=" && c < 0) return 0 if (op == "=" && c != 0) return 0 } return 1 } ' # Reads flattened updates.json and prints validated records only: # S G C # P A # Anything that fails a pattern is dropped. Exit 3 when schema is not 1. # shellcheck disable=SC2016 # an awk program, not shell NF_MANIFEST_AWK=' BEGIN { FS = "\t" } NF != 2 { next } $1 == "schema" { schema = $2; next } $1 == "channels.stable.version" { if (okv($2) && st == "") st = $2; next } $1 == "channels.nightly.version" { if (okv($2) && ng == "") ng = $2; next } $1 == "catalog_rev" { if (oku($2)) rev = $2; next } { k = split($1, p, ".") if (k == 3 && p[1] == "packages" && oku(p[2]) && p[2] + 0 < 5000) { i = p[2] + 1; if (i > np) np = i if (!((i, p[3]) in pk)) pk[i, p[3]] = $2 } else if (k == 3 && p[1] == "advisories" && oku(p[2]) && p[2] + 0 < 200) { i = p[2] + 1; if (i > na) na = i if (!((i, p[3]) in ad)) ad[i, p[3]] = $2 } } END { if (schema != "1") exit 3 if (st != "") print "S\t" st if (ng != "") print "G\t" ng if (rev != "") print "C\t" rev for (i = 1; i <= np; i++) { nm = pk[i, "name"]; v = pk[i, "version"] if (!okn(nm) || !okv(v) || (nm in seen)) continue seen[nm] = 1 a = pk[i, "added"]; if (!okd(a)) a = "-" u = pk[i, "updated"]; if (!okd(u)) u = "-" print "P\t" nm "\t" v "\t" a "\t" u } for (i = 1; i <= na; i++) { id = ad[i, "id"]; r = ad[i, "affects"]; s = ad[i, "severity"] if (!oka(id) || !okr(r)) continue if (!oks(s)) s = "high" print "A\t" id "\t" r "\t" s } } ' # Reads flattened installed.json and prints validated records: # IV IH IO IR IC # IT IP # shellcheck disable=SC2016 # an awk program, not shell NF_INSTALLED_AWK=' BEGIN { FS = "\t" } NF != 2 { next } $1 == "version" { if (okv($2)) print "IV\t" $2; next } $1 == "channel" { if ($2 == "stable" || $2 == "nightly") print "IH\t" $2; next } $1 == "source" { if ($2 == "install" || $2 == "direct") print "IO\t" $2; next } $1 == "previous" { if (okv($2)) print "IR\t" $2; next } $1 == "catalog_rev" { if (oku($2)) print "IC\t" $2; next } $1 == "installed_at" { if ($2 ~ /^[0-9TZ:-]+$/ && length($2) <= 32) print "IT\t" $2; next } { k = split($1, p, ".") if (k == 3 && p[1] == "packages" && oku(p[2])) { i = p[2] + 1; if (i > np) np = i; pk[i, p[3]] = $2 } } END { for (i = 1; i <= np; i++) if (okn(pk[i, "name"]) && okv(pk[i, "version"])) print "IP\t" pk[i, "name"] "\t" pk[i, "version"] } ' # Reads a flattened venutian.lock.json and prints "L " for # every locked package that records a version. Accepts the map form # {"vjson": {"version": "0.3.0"}} and the list form {"packages": [...]}. # shellcheck disable=SC2016 # an awk program, not shell NF_LOCK_AWK=' BEGIN { FS = "\t" } NF != 2 { next } { k = split($1, p, ".") if (k == 2 && p[2] == "version") { if (okn(p[1]) && okv($2)) print "L\t" p[1] "\t" $2 } else if (k == 3 && p[1] == "packages" && p[3] == "version" && okn(p[2])) { if (okv($2)) print "L\t" p[2] "\t" $2 } else if (k == 3 && p[1] == "packages" && oku(p[2])) { i = p[2] + 1; if (i > np) np = i; pk[i, p[3]] = $2 } } END { for (i = 1; i <= np; i++) if (okn(pk[i, "name"]) && okv(pk[i, "version"])) print "L\t" pk[i, "name"] "\t" pk[i, "version"] } ' # nf_flat : flattened JSON on stdout, non zero on any problem. nf_flat() { [ -f "$1" ] || return 1 nf_sz=$(wc -c <"$1" 2>/dev/null | tr -d ' ') || return 1 nf_is_uint "$nf_sz" || return 1 [ "$nf_sz" -le "$2" ] || return 1 tr -d '\000' <"$1" | awk "$NF_JSON_AWK" 2>/dev/null } # nf_parse : validated records on stdout. nf_parse() { nf_flat "$1" "$2" >"$ST/.flat.$$" || { rm -f "$ST/.flat.$$"; return 1; } awk "$NF_LIB_AWK$3" "$ST/.flat.$$" 2>/dev/null nf_rc=$? rm -f "$ST/.flat.$$" return $nf_rc } # =========================================================================== # Network: one plain HTTPS GET with an ETag, 5 seconds, capped size # =========================================================================== nf_installed_version() { nf_iv= if [ -f "$ST/installed.tsv" ]; then nf_iv=$(awk -F '\t' '$1 == "IV" { print $2; exit }' "$ST/installed.tsv" 2>/dev/null) fi [ -n "$nf_iv" ] || nf_iv=unknown printf '%s\n' "$nf_iv" } # nf_fetch [etag file] # Returns 0 for fresh content in , 2 for 304 Not Modified, 1 otherwise. nf_fetch() { nf_url=$1; nf_out=$2; nf_cap=$3; nf_max=$4; nf_etf=${5-} nf_ua="venutian-notifier/1 ($OS; $ARCH; $(nf_installed_version))" nf_hdr=$nf_out.h rm -f "$nf_out" "$nf_hdr" if command -v curl >/dev/null 2>&1; then nf_proto='=https' [ "$LOOPBACK" = 1 ] && nf_proto='=http,https' nf_etag= if [ -n "$nf_etf" ] && [ -f "$nf_etf" ]; then IFS= read -r nf_etag <"$nf_etf" || : case $nf_etag in \"*\" | W/\"*\") ;; *) nf_etag= ;; esac case $nf_etag in *[!\!-~]*) nf_etag= ;; esac [ "${#nf_etag}" -le 200 ] || nf_etag= fi if [ -n "$nf_etag" ]; then curl -sS --max-time "$nf_max" --proto "$nf_proto" --tlsv1.2 -A "$nf_ua" \ -H "If-None-Match: $nf_etag" -D "$nf_hdr" -o - "$nf_url" 2>/dev/null | head -c "$((nf_cap + 1))" >"$nf_out" 2>/dev/null else curl -sS --max-time "$nf_max" --proto "$nf_proto" --tlsv1.2 -A "$nf_ua" \ -D "$nf_hdr" -o - "$nf_url" 2>/dev/null | head -c "$((nf_cap + 1))" >"$nf_out" 2>/dev/null fi nf_code=$(awk '/^HTTP\// { c = $2 } END { print c }' "$nf_hdr" 2>/dev/null) elif command -v wget >/dev/null 2>&1; then # wget has no If-None-Match shortcut here; the file is small and static. # GNU wget refuses plain http redirects with --https-only; busybox wget # lacks the flag and gets https URLs only, because BASE is https. if [ "$LOOPBACK" = 0 ] && wget --help 2>&1 | grep 'https-only' >/dev/null; then wget -q --https-only -T "$nf_max" -U "$nf_ua" -O - "$nf_url" 2>/dev/null | head -c "$((nf_cap + 1))" >"$nf_out" 2>/dev/null else wget -q -T "$nf_max" -U "$nf_ua" -O - "$nf_url" 2>/dev/null | head -c "$((nf_cap + 1))" >"$nf_out" 2>/dev/null fi nf_code=200 [ -s "$nf_out" ] || nf_code= else return 1 fi case $nf_code in 304) rm -f "$nf_out"; return 2 ;; 200) ;; *) rm -f "$nf_out" "$nf_hdr"; return 1 ;; esac nf_sz=$(wc -c <"$nf_out" 2>/dev/null | tr -d ' ') if ! nf_is_uint "$nf_sz" || [ "$nf_sz" -gt "$nf_cap" ] || [ "$nf_sz" -eq 0 ]; then rm -f "$nf_out" "$nf_hdr" return 1 fi return 0 } # Prints the validated ETag from a saved header file, or nothing. nf_etag_of() { [ -f "$1" ] || return 0 awk ' tolower(substr($0, 1, 5)) == "etag:" { v = substr($0, 6); gsub(/^[ \t]+|[ \t\r]+$/, "", v); e = v } END { if (length(e) <= 200 && (e ~ /^"[!#-~]*"$/ || e ~ /^W\/"[!#-~]*"$/)) print e }' "$1" 2>/dev/null } # =========================================================================== # Check: fetch, validate, compare, precompose # =========================================================================== nf_ensure_state() { mkdir -p "$ST" 2>/dev/null } # Refreshes installed.tsv from installed.json (validated fields only). nf_load_installed() { [ -f "$INSTALLED" ] || return 1 nf_parse "$INSTALLED" 1048576 "$NF_INSTALLED_AWK" | nf_atomic_write "$ST/installed.tsv" grep '^IV ' "$ST/installed.tsv" >/dev/null 2>&1 } # Refreshes manifest.tsv from the cached updates.json. nf_load_manifest() { [ -f "$ST/updates.json" ] || return 1 nf_parse "$ST/updates.json" "$MANIFEST_CAP" "$NF_MANIFEST_AWK" >"$ST/manifest.tsv.new" || { rm -f "$ST/manifest.tsv.new" return 1 } mv -f "$ST/manifest.tsv.new" "$ST/manifest.tsv" } nf_channel() { nf_ch=$(nf_conf channel) if [ -z "$nf_ch" ]; then nf_ch=$(awk -F '\t' '$1 == "IH" { print $2; exit }' "$ST/installed.tsv" 2>/dev/null) fi case $nf_ch in nightly) ;; *) nf_ch=stable ;; esac printf '%s\n' "$nf_ch" } # Compares the manifest with the install and writes pending.tsv: # R N U # A # then precomposes notice.txt and advisory.txt. nf_compute() { nf_load_installed || return 1 nf_load_manifest || return 1 # Library baseline: what the install recorded; when it recorded nothing # (the manifest could not be read at install time), the first manifest # seen becomes the baseline, so nobody is told about 113 "new" libraries. if ! grep '^IP ' "$ST/installed.tsv" >/dev/null 2>&1 && [ ! -f "$ST/baseline.tsv" ]; then awk -F '\t' '$1 == "P" { print "BP\t" $2 "\t" $3 }' "$ST/manifest.tsv" | nf_atomic_write "$ST/baseline.tsv" fi nf_chan=$(nf_channel) { cat "$ST/installed.tsv" [ -f "$ST/baseline.tsv" ] && cat "$ST/baseline.tsv" cat "$ST/manifest.tsv" } | awk -F '\t' -v chan="$nf_chan" "$NF_LIB_AWK"' $1 == "IV" { iv = $2; next } $1 == "IC" { irev = $2; next } $1 == "IP" || $1 == "BP" { if (!($2 in known)) known[$2] = $3; next } $1 == "S" { st = $2; next } $1 == "G" { ng = $2; next } $1 == "C" { mrev = $2; next } $1 == "P" { np++; pn[np] = $2; pv[np] = $3; next } $1 == "A" { na++; aid[na] = $2; ar[na] = $3; as[na] = $4; next } END { nv = st if (chan == "nightly" && ng != "") nv = ng if (iv != "" && nv != "" && vcmp(nv, iv) > 0) print "R\t" nv "\t" iv newok = (irev == "" || mrev == "" || mrev + 0 > irev + 0) for (i = 1; i <= np; i++) { if (!(pn[i] in known)) { if (newok) print "N\t" pn[i] "\t" pv[i] } else if (vcmp(pv[i], known[pn[i]]) > 0) print "U\t" pn[i] "\t" pv[i] "\t" known[pn[i]] } for (i = 1; i <= na; i++) if (iv != "" && inrange(iv, ar[i])) print "A\t" aid[i] "\t" as[i] "\t" iv }' >"$ST/pending.tsv.new" # New information may be shown again today; the same news only once a day. if ! cmp -s "$ST/pending.tsv.new" "$ST/pending.tsv"; then rm -f "$ST/shown"; fi mv -f "$ST/pending.tsv.new" "$ST/pending.tsv" nf_compose_advisory | nf_atomic_write "$ST/advisory.txt" [ -s "$ST/advisory.txt" ] || rm -f "$ST/advisory.txt" nf_compose_notice all "" | nf_atomic_write "$ST/notice.txt" [ -s "$ST/notice.txt" ] || rm -f "$ST/notice.txt" return 0 } nf_compose_advisory() { [ -f "$ST/pending.tsv" ] || return 0 awk -F '\t' -v upg="$UPGRADE_CMD" ' $1 == "A" { print " Security advisory for Venutian " $4 ": " $2 ", severity " $3 "." print " Upgrade: " upg print " Details: https://venutian.org/security/" $2 "/" }' "$ST/pending.tsv" | nf_clean } # nf_compose_notice # Composes the notice from pending.tsv, the project comparison and fixed # sentences. "unseen" leaves out library items already shown once. nf_compose_notice() { [ -f "$ST/pending.tsv" ] || return 0 nf_libs=$(nf_conf libraries) nf_direct=0 grep '^IO direct$' "$ST/installed.tsv" >/dev/null 2>&1 && nf_direct=1 { if [ "$1" = unseen ] && [ -f "$ST/libs-seen" ]; then awk -F '\t' '{ print "X\t" $1 "\t" $2 }' "$ST/libs-seen" fi cat "$ST/pending.tsv" if [ -n "$2" ] && [ -f "$2" ]; then cat "$2"; fi } | awk -F '\t' -v libs="$nf_libs" -v direct="$nf_direct" -v upg="$UPGRADE_CMD" \ -v dl="$DOWNLOAD_PAGE" ' function names(a, c, s, i) { s = "" for (i = 1; i <= c && i <= 5; i++) s = s ((i > 1) ? ", " : "") a[i] if (c > 5) s = s ", and " (c - 5) " more" return s } $1 == "X" { seen[$2 "\t" $3] = 1; next } $1 == "R" { rel = 1; rv = $2; ov = $3; next } $1 == "N" { if (!(($2 "\t" $3) in seen)) N[++nn] = $2 " " $3; next } $1 == "U" { if (!(($2 "\t" $3) in seen)) U[++nu] = $2 " " $3; next } $1 == "J" { J[++nj] = $2 " " $3 " can go to " $4; next } END { if (libs != "on") { nn = 0; nu = 0; nj = 0 } lib = (nn || nu) any = rel || lib || nj if (!any) exit 0 if (rel) print " Venutian " rv " is out (you have " ov ")." if (lib) { l = " " if (nn) l = l "New libraries: " names(N, nn) "." if (nu) l = l ((nn) ? " " : "") "Updated: " names(U, nu) "." print l } if (nj) print " This project: " names(J, nj) "." if (rel) print " Upgrade: " upg if (rel && direct == 1) print " Download: " dl if (lib || nj) print " Libraries: venutian-notifier libraries" if (rel) print " Notes: https://venutian.org/releases/" rv "/" else print " Notes: https://venutian.org/releases/#libraries" print " Quiet: venutian-notifier off" }' | nf_clean } # Compares ./venutian.lock.json (read, never written) with the registry and # writes "J " records to $1. nf_project_records() { : >"$1" [ -f venutian.lock.json ] || return 0 [ -f "$ST/manifest.tsv" ] || return 0 [ "$(nf_conf libraries)" = on ] || return 0 nf_parse venutian.lock.json "$MANIFEST_CAP" "$NF_LOCK_AWK" >"$1.l" 2>/dev/null || { rm -f "$1.l" return 0 } cat "$1.l" "$ST/manifest.tsv" | awk -F '\t' "$NF_LIB_AWK"' $1 == "L" { if (!($2 in lk)) { nl++; ln[nl] = $2; lk[$2] = $3 }; next } $1 == "P" { reg[$2] = $3; next } END { for (i = 1; i <= nl; i++) { n = ln[i] if ((n in reg) && vcmp(reg[n], lk[n]) > 0) print "J\t" n "\t" lk[n] "\t" reg[n] } }' >"$1" 2>/dev/null rm -f "$1.l" } # nf_check [quiet]: one fetch of updates.json, then compute. Returns 0 when a # manifest (fresh or cached) was compared, 1 when nothing could be read. nf_check() { nf_ensure_state || return 1 nf_mark_checked nf_fetch "$BASE/api/v1/updates.json" "$ST/updates.json.new" "$MANIFEST_CAP" 5 "$ST/etag" case $? in 0) if nf_parse "$ST/updates.json.new" "$MANIFEST_CAP" "$NF_MANIFEST_AWK" >/dev/null; then mv -f "$ST/updates.json.new" "$ST/updates.json" nf_e=$(nf_etag_of "$ST/updates.json.new.h") if [ -n "$nf_e" ]; then printf '%s\n' "$nf_e" | nf_atomic_write "$ST/etag"; else rm -f "$ST/etag"; fi else rm -f "$ST/updates.json.new" fi ;; 2) ;; *) rm -f "$ST/updates.json.new" ;; esac rm -f "$ST/updates.json.new.h" nf_compute } # =========================================================================== # The two wrapper hooks # =========================================================================== nf_maybe_print() { [ -t 2 ] || return 0 nf_silenced && return 0 [ -f "$INSTALLED" ] || return 0 nf_print_notice } # Prints what is waiting (the checks above already passed). Advisories on # every run, the notice once a day. nf_print_notice() { # From here on nothing of ours may reach the terminal except the notice. exec 3>&2 2>/dev/null if [ -s "$ST/advisory.txt" ]; then nf_clean <"$ST/advisory.txt" >&3 fi nf_td=${NF_TODAY:-$(nf_today)} nf_shown= [ -f "$ST/shown" ] && IFS= read -r nf_shown <"$ST/shown" [ "$nf_shown" = "$nf_td" ] && return 0 nf_ensure_state nf_project_records "$ST/project.$$" nf_compose_notice unseen "$ST/project.$$" >"$ST/notice.$$" rm -f "$ST/project.$$" # Today is settled either way: a notice was shown, or there was nothing to # show (a refresh with new information clears this again). printf '%s\n' "$nf_td" | nf_atomic_write "$ST/shown" if [ -s "$ST/notice.$$" ]; then cat "$ST/notice.$$" >&3 # A library is announced once; the release line repeats daily until you # upgrade. nf_confv libraries if [ "$nf_v" = on ]; then awk -F '\t' '$1 == "N" || $1 == "U" { print $2 "\t" $3 }' "$ST/pending.tsv" >>"$ST/libs-seen" fi fi rm -f "$ST/notice.$$" return 0 } nf_lock() { nf_ensure_state || return 1 if mkdir "$ST/.lock" 2>/dev/null; then return 0; fi # A lock older than a minute belongs to a refresh that died. if [ -n "$(find "$ST/.lock" -prune -mmin +1 2>/dev/null)" ]; then rmdir "$ST/.lock" 2>/dev/null mkdir "$ST/.lock" 2>/dev/null && return 0 fi return 1 } nf_unlock() { rmdir "$ST/.lock" 2>/dev/null; } nf_due() { nf_last=0 if [ -f "$ST/last-check" ]; then IFS= read -r nf_last <"$ST/last-check" || :; fi nf_is_uint "$nf_last" || nf_last=0 nf_n=${NF_NOW:-$(nf_now)} nf_is_uint "$nf_n" || return 1 nf_confv interval_hours nf_iv_s=$((nf_v * 3600)) [ "$nf_last" -gt "$nf_n" ] && return 0 [ $((nf_n - nf_last)) -ge "$nf_iv_s" ] } # The wrapper's single hook after every interactive run: print what is # waiting, then start a detached refresh when the last check is older than # the interval. One shell start and one date call on the common path. nf_after_run() { [ -t 2 ] || return 0 nf_silenced && return 0 [ -f "$INSTALLED" ] || return 0 nf_clock=$(date -u '+%s %Y-%m-%d') NF_NOW=${nf_clock%% *} NF_TODAY=${nf_clock#* } nf_print_notice nf_due || return 0 ( trap '' HUP /bin/sh "$NF_SELF" maybe-refresh /dev/null 2>&1 & ) >/dev/null 2>&1 return 0 } nf_maybe_refresh() { nf_silenced && return 0 [ -f "$INSTALLED" ] || return 0 nf_due || return 0 nf_lock || return 0 trap 'nf_unlock' EXIT trap 'nf_unlock; exit 1' TERM INT HUP # The whole refresh is killed after 10 seconds. nf_me=$$ (sleep 10 && kill -TERM "$nf_me") >/dev/null 2>&1 & nf_dog=$! nf_check >/dev/null 2>&1 kill "$nf_dog" >/dev/null 2>&1 nf_unlock trap - EXIT TERM INT HUP return 0 } # =========================================================================== # installed.json (written here for install.sh and notify.sh alike) # =========================================================================== nf_valid_version() { printf '%s\n' "$1" | awk "$NF_LIB_AWK"'{ exit !okv($0) }' } # Escapes a string for a JSON literal and drops control bytes. nf_json_str() { printf '%s' "$1" | nf_clean | tr -d '\n' | awk ' { s = s $0 } END { gsub(/\\/, "\\\\", s); gsub(/"/, "\\\"", s); printf "\"%s\"", s }' } # nf_write_installed # holds "nameversion" lines (already validated). nf_write_installed() { nf_at=$(date -u +%Y-%m-%dT%H:%M:%SZ) { printf '{\n "schema": 1,\n' printf ' "version": "%s",\n "channel": "%s",\n "source": "%s",\n' "$1" "$2" "$3" [ -n "$4" ] && printf ' "previous": "%s",\n' "$4" printf ' "os": "%s",\n "arch": "%s",\n "notifier": "%s",\n' "$OS" "$ARCH" "$NOTIFIER_VERSION" nf_is_uint "$6" && printf ' "catalog_rev": %s,\n' "$6" printf ' "installed_at": "%s",\n' "$nf_at" printf ' "toolchain": %s,\n' "$(nf_json_str "$5")" printf ' "packages": [' if [ -s "$7" ]; then awk -F '\t' '{ printf "%s\n {\"name\": \"%s\", \"version\": \"%s\"}", (NR > 1 ? "," : ""), $1, $2 }' "$7" printf '\n ' fi printf ']\n}\n' } | nf_atomic_write "$INSTALLED" } # The installer hook: setup [previous] [toolchain] # Records the install and takes the registry as it is right now as the # library baseline. The first run after an install stays quiet. nf_cmd_setup() { nf_sv=${1-}; nf_sc=${2-stable}; nf_so=${3-install}; nf_sp=${4-}; nf_stc=${5-} nf_valid_version "$nf_sv" || return 2 case $nf_sc in stable | nightly) ;; *) nf_sc=stable ;; esac case $nf_so in install | direct) ;; *) nf_so=install ;; esac if [ -n "$nf_sp" ] && ! nf_valid_version "$nf_sp"; then nf_sp=; fi [ "$nf_sp" = "$nf_sv" ] && nf_sp= nf_ensure_state || return 1 rm -f "$ST/pending.tsv" "$ST/notice.txt" "$ST/advisory.txt" "$ST/shown" \ "$ST/libs-seen" "$ST/baseline.tsv" "$ST/manifest.tsv" if ! nf_flag_set "${VENUTIAN_NO_UPDATE_CHECK-}" && ! nf_flag_set "${DO_NOT_TRACK-}"; then rm -f "$ST/etag" if nf_fetch "$BASE/api/v1/updates.json" "$ST/updates.json.new" "$MANIFEST_CAP" 5 "" && nf_parse "$ST/updates.json.new" "$MANIFEST_CAP" "$NF_MANIFEST_AWK" >/dev/null; then mv -f "$ST/updates.json.new" "$ST/updates.json" nf_e=$(nf_etag_of "$ST/updates.json.new.h") [ -n "$nf_e" ] && printf '%s\n' "$nf_e" | nf_atomic_write "$ST/etag" fi rm -f "$ST/updates.json.new" "$ST/updates.json.new.h" fi : >"$ST/pkgs.$$" nf_rev= if nf_load_manifest; then awk -F '\t' '$1 == "P" { print $2 "\t" $3 }' "$ST/manifest.tsv" >"$ST/pkgs.$$" nf_rev=$(awk -F '\t' '$1 == "C" { print $2 }' "$ST/manifest.tsv") fi nf_write_installed "$nf_sv" "$nf_sc" "$nf_so" "$nf_sp" "$nf_stc" "$nf_rev" "$ST/pkgs.$$" nf_rc=$? rm -f "$ST/pkgs.$$" nf_mark_checked nf_compute >/dev/null 2>&1 return $nf_rc } # =========================================================================== # Commands for people # =========================================================================== nf_fmt_time() { nf_is_uint "$1" || { echo never; return; } date -u -r "$1" '+%Y-%m-%d %H:%M UTC' 2>/dev/null || date -u -d "@$1" '+%Y-%m-%d %H:%M UTC' 2>/dev/null || echo "at epoch $1" } nf_require_install() { if [ ! -f "$INSTALLED" ] || ! nf_load_installed; then echo "No Venutian install is recorded here. Install: $UPGRADE_CMD" exit 1 fi } nf_cmd_status() { nf_require_install nf_load_manifest >/dev/null 2>&1 && nf_compute >/dev/null 2>&1 nf_v=$(nf_installed_version) nf_ch=$(nf_channel) nf_last= [ -f "$ST/last-check" ] && IFS= read -r nf_last <"$ST/last-check" nf_when=$(nf_fmt_time "$nf_last") nf_new=$(awk -F '\t' '$1 == "R" { print $2 }' "$ST/pending.tsv" 2>/dev/null) if [ "$(nf_conf update_check)" = off ]; then echo "Venutian $nf_v ($nf_ch). Update checks are off." elif [ -n "$nf_new" ]; then echo "Venutian $nf_v ($nf_ch). $nf_new is available. Last checked $nf_when." else echo "Venutian $nf_v ($nf_ch). Up to date. Last checked $nf_when." fi nf_nn=$(grep -c '^N ' "$ST/pending.tsv" 2>/dev/null) nf_nu=$(grep -c '^U ' "$ST/pending.tsv" 2>/dev/null) nf_is_uint "$nf_nn" || nf_nn=0 nf_is_uint "$nf_nu" || nf_nu=0 if [ "$(nf_conf libraries)" = off ]; then echo "Library notices are off. Release and advisory notices still show." elif [ "$nf_nn" -gt 0 ] || [ "$nf_nu" -gt 0 ]; then echo "Libraries: $nf_nn new, $nf_nu updated. List them: venutian-notifier libraries" else echo "Libraries: nothing new since your install." fi if [ -s "$ST/advisory.txt" ]; then nf_clean <"$ST/advisory.txt"; fi if [ "$(nf_conf desktop)" = on ]; then echo "Desktop notifications are on." else echo "Desktop notifications are off." fi if nf_silenced && [ "$(nf_conf update_check)" = on ]; then echo "This shell silences notices (VENUTIAN_NO_UPDATE_CHECK, DO_NOT_TRACK or a CI variable)." fi } nf_cmd_check_now() { nf_require_install nf_ensure_state nf_check >/dev/null 2>&1 if [ ! -f "$ST/manifest.tsv" ]; then echo "Could not reach venutian.org. Try again later." return 1 fi nf_project_records "$ST/project.$$" nf_compose_notice all "$ST/project.$$" >"$ST/notice.$$" rm -f "$ST/project.$$" if [ -s "$ST/advisory.txt" ]; then nf_clean <"$ST/advisory.txt"; fi if [ -s "$ST/notice.$$" ]; then cat "$ST/notice.$$" else echo "Venutian $(nf_installed_version) ($(nf_channel)). Up to date. Last checked $(nf_fmt_time "$(nf_now)")." fi rm -f "$ST/notice.$$" return 0 } nf_cmd_libraries() { case ${1-} in on | off) nf_conf_set libraries "$1" || return 1 echo "Library notices are $1." return 0 ;; --fetch) nf_cmd_fetch "${2-}" return $? ;; '') ;; *) echo "Usage: venutian-notifier libraries [--fetch ] | libraries on|off" >&2 return 2 ;; esac nf_require_install if [ ! -f "$ST/updates.json" ]; then nf_check >/dev/null 2>&1; fi if ! nf_compute >/dev/null 2>&1; then echo "Could not reach venutian.org. Try again later." return 1 fi nf_project_records "$ST/project.$$" cat "$ST/pending.tsv" "$ST/project.$$" | awk -F '\t' -v iv="$(nf_installed_version)" ' $1 == "N" { N[++nn] = sprintf(" %-20s https://venutian.org/packages/%s/", $2 " " $3, $2); next } $1 == "U" { U[++nu] = sprintf(" %-20s https://venutian.org/packages/%s/ (you have %s)", $2 " " $3, $2, $4); next } $1 == "J" { J[++nj] = " " $2 " " $3 " can go to " $4; next } END { if (!nn && !nu) print "No new or updated libraries since Venutian " iv "." if (nn) { print "New libraries:"; for (i = 1; i <= nn; i++) print N[i] } if (nu) { print "Updated libraries:"; for (i = 1; i <= nu; i++) print U[i] } if (nj) { print "This project (venutian.lock.json):"; for (i = 1; i <= nj; i++) print J[i] } if (nn || nu || nj) print "Fetch one: venutian-notifier libraries --fetch " }' | nf_clean rm -f "$ST/project.$$" } nf_sha256() { if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" 2>/dev/null | awk '{ print $1 }' elif command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" 2>/dev/null | awk '{ print $1 }' elif command -v openssl >/dev/null 2>&1; then openssl dgst -sha256 "$1" 2>/dev/null | awk '{ print $NF }' fi } # Reads a flattened packages.json and prints "F " # for the package named by -v want. # shellcheck disable=SC2016 # an awk program, not shell NF_PKGINDEX_AWK=' BEGIN { FS = "\t" } NF != 2 { next } { k = split($1, p, ".") if (k == 3 && p[1] == "packages" && oku(p[2])) { i = p[2]; f = p[3] } else if (k == 2 && oku(p[1])) { i = p[1]; f = p[2] } else next if (!((i, f) in pk)) pk[i, f] = $2 if (i + 0 > mx) mx = i + 0 } END { for (i = 0; i <= mx; i++) { if (pk[i, "name"] != want) continue v = pk[i, "latest_version"]; if (!okv(v)) v = pk[i, "version"] h = pk[i, "sha256"]; a = pk[i, "archive"] sub(/^.*\//, "", a) if (okv(v) && okh(h) && okf(a)) { print "F\t" v "\t" h "\t" a; exit 0 } } exit 1 } ' nf_cmd_fetch() { nf_name=$1 case $nf_name in '' | *[!a-z0-9_-]* | [!a-z]*) echo "Usage: venutian-notifier libraries --fetch " >&2 return 2 ;; esac [ "${#nf_name}" -le 64 ] || return 2 nf_ensure_state mkdir -p "$CACHE" 2>/dev/null if ! nf_fetch "$BASE/api/v1/packages.json" "$CACHE/packages.json.new" "$PACKAGES_CAP" 20 ""; then rm -f "$CACHE/packages.json.new.h" echo "Could not reach venutian.org. Try again later." return 1 fi rm -f "$CACHE/packages.json.new.h" mv -f "$CACHE/packages.json.new" "$CACHE/packages.json" nf_rec=$(nf_flat "$CACHE/packages.json" "$PACKAGES_CAP" | awk -v want="$nf_name" "$NF_LIB_AWK$NF_PKGINDEX_AWK") if [ -z "$nf_rec" ]; then echo "The registry has no library named $nf_name. Browse them all: https://venutian.org/packages/" return 1 fi nf_fv=$(printf '%s\n' "$nf_rec" | awk -F '\t' '{ print $2 }') nf_fh=$(printf '%s\n' "$nf_rec" | awk -F '\t' '{ print $3 }') nf_fa=$(printf '%s\n' "$nf_rec" | awk -F '\t' '{ print $4 }') nf_dir=$CACHE/packages/$nf_name/$nf_fv mkdir -p "$nf_dir" || return 1 nf_fetch "$BASE/downloads/packages/$nf_name/$nf_fv/$nf_fa" "$nf_dir/$nf_fa.part" 268435456 300 "" nf_frc=$? rm -f "$nf_dir/$nf_fa.part.h" if [ $nf_frc -ne 0 ]; then echo "Could not download $nf_name $nf_fv. Try again later." return 1 fi if [ "$(nf_sha256 "$nf_dir/$nf_fa.part")" != "$nf_fh" ]; then rm -f "$nf_dir/$nf_fa.part" echo "The download of $nf_name $nf_fv did not match its SHA 256 and was deleted." return 1 fi mv -f "$nf_dir/$nf_fa.part" "$nf_dir/$nf_fa" echo "Fetched $nf_name $nf_fv and verified its SHA 256." case $nf_dir in *[!A-Za-z0-9_./-]*) nf_q=$(printf '%s' "$nf_dir/$nf_fa" | sed "s/'/'\\\\''/g") printf '%s\n' "Add it to a project: venutian install '$nf_q'" | nf_clean ;; *) echo "Add it to a project: venutian install $nf_dir/$nf_fa" ;; esac } # =========================================================================== # Opt in desktop notifications (research 12.6): a daily per user job # =========================================================================== NF_LABEL=org.venutian.notifier nf_desktop_files() { NF_PLIST=$HOME/Library/LaunchAgents/$NF_LABEL.plist nf_sd=$nf_conf_root/systemd/user NF_UNIT=$nf_sd/venutian-notifier.service NF_TIMER=$nf_sd/venutian-notifier.timer } # Runs a command, or only prints it under VENUTIAN_DRY_RUN=1. nf_do() { if nf_flag_set "${VENUTIAN_DRY_RUN-}"; then echo "dry run: $*" return 0 fi "$@" } nf_write_file() { if nf_flag_set "${VENUTIAN_DRY_RUN-}"; then echo "dry run: write $1" cat >/dev/null return 0 fi mkdir -p "$(dirname "$1")" && nf_atomic_write "$1" } nf_desktop_on() { nf_desktop_files nf_self=$NDIR/notifier.sh case $OS in macos) nf_write_file "$NF_PLIST" < Label$NF_LABEL ProgramArguments /bin/sh$nf_selfdesktop-run StartCalendarInterval Hour10Minute0 RunAtLoad StandardOutPath/dev/null StandardErrorPath/dev/null EOF nf_uid=$(id -u) nf_do launchctl bootout "gui/$nf_uid/$NF_LABEL" >/dev/null 2>&1 nf_do launchctl bootstrap "gui/$nf_uid" "$NF_PLIST" >/dev/null 2>&1 || nf_do launchctl load -w "$NF_PLIST" >/dev/null 2>&1 || return 1 ;; linux) if command -v systemctl >/dev/null 2>&1 && systemctl --user show-environment >/dev/null 2>&1; then nf_write_file "$NF_UNIT" </dev/null 2>&1 nf_do systemctl --user enable --now venutian-notifier.timer >/dev/null 2>&1 || return 1 elif command -v crontab >/dev/null 2>&1; then nf_cron=$( (crontab -l 2>/dev/null | grep -v '# venutian-notifier$') ; echo "17 10 * * * /bin/sh '$nf_self' desktop-run >/dev/null 2>&1 # venutian-notifier") if nf_flag_set "${VENUTIAN_DRY_RUN-}"; then echo "dry run: crontab gets: $nf_cron" | tail -n 1 else printf '%s\n' "$nf_cron" | crontab - || return 1 fi else echo "No systemd user session or crontab found, so there is no daily job to register." return 1 fi ;; *) return 1 ;; esac nf_flag_set "${VENUTIAN_DRY_RUN-}" || nf_conf_set desktop on } nf_desktop_off() { nf_desktop_files case $OS in macos) nf_do launchctl bootout "gui/$(id -u)/$NF_LABEL" >/dev/null 2>&1 || nf_do launchctl unload -w "$NF_PLIST" >/dev/null 2>&1 nf_do rm -f "$NF_PLIST" ;; linux) if [ -f "$NF_TIMER" ]; then nf_do systemctl --user disable --now venutian-notifier.timer >/dev/null 2>&1 nf_do rm -f "$NF_TIMER" "$NF_UNIT" nf_do systemctl --user daemon-reload >/dev/null 2>&1 fi if command -v crontab >/dev/null 2>&1 && crontab -l 2>/dev/null | grep '# venutian-notifier$' >/dev/null; then if nf_flag_set "${VENUTIAN_DRY_RUN-}"; then echo "dry run: remove the venutian-notifier line from crontab" else crontab -l 2>/dev/null | grep -v '# venutian-notifier$' | crontab - fi fi ;; esac nf_flag_set "${VENUTIAN_DRY_RUN-}" || nf_conf_set desktop off return 0 } # The daily job: check, and post one native notification when there is news # that has not been posted before. The text is composed from validated # versions, names and ids only. nf_desktop_run() { nf_silenced && return 0 [ "$(nf_conf desktop)" = on ] || return 0 [ -f "$INSTALLED" ] || return 0 nf_lock || return 0 nf_check >/dev/null 2>&1 nf_unlock [ -f "$ST/pending.tsv" ] || return 0 nf_msg=$(awk -F '\t' -v libs="$(nf_conf libraries)" ' $1 == "A" { a = "Security advisory " $2 " affects your Venutian " $4 "." } $1 == "R" { r = "Venutian " $2 " is out (you have " $3 ")." } $1 == "N" || $1 == "U" { l++ } END { m = a if (r != "") m = m ((m != "") ? " " : "") r if (libs == "on" && l) m = m ((m != "") ? " " : "") l " library updates." print m }' "$ST/pending.tsv" | nf_clean | tr -d '\042\134') [ -n "$nf_msg" ] || return 0 nf_last_msg= [ -f "$ST/desktop-shown" ] && IFS= read -r nf_last_msg <"$ST/desktop-shown" [ "$nf_last_msg" = "$nf_msg" ] && return 0 printf '%s\n' "$nf_msg" | nf_atomic_write "$ST/desktop-shown" case $OS in macos) nf_do osascript -e "display notification \"$nf_msg\" with title \"Venutian\"" >/dev/null 2>&1 ;; linux) command -v notify-send >/dev/null 2>&1 && nf_do notify-send "Venutian" "$nf_msg" >/dev/null 2>&1 ;; esac return 0 } # =========================================================================== # Rollback: switch current back to the kept previous version # =========================================================================== # Replaces the symlink $2 with one pointing at $1 in a single rename. nf_swap_link() { nf_tmp=$2.new.$$ rm -f "$nf_tmp" ln -s "$1" "$nf_tmp" || return 1 # GNU and busybox mv take -T, BSD and macOS mv take -h; both rename the # link itself instead of moving into the directory it points at. mv -T -f "$nf_tmp" "$2" 2>/dev/null && return 0 mv -h -f "$nf_tmp" "$2" 2>/dev/null && return 0 rm -f "$2" && mv -f "$nf_tmp" "$2" } nf_cmd_rollback() { nf_require_install nf_prev=$(awk -F '\t' '$1 == "IR" { print $2; exit }' "$ST/installed.tsv") nf_cur=$(nf_installed_version) if ! grep '^IO install$' "$ST/installed.tsv" >/dev/null 2>&1 || [ -z "$nf_prev" ] || [ ! -d "$VH/versions/$nf_prev" ]; then echo "No previous version is kept to roll back to." return 1 fi if nf_flag_set "${VENUTIAN_DRY_RUN-}"; then echo "dry run: point $VH/current at versions/$nf_prev" return 0 fi nf_swap_link "versions/$nf_prev" "$VH/current" || return 1 awk -F '\t' '$1 == "IP" { print $2 "\t" $3 }' "$ST/installed.tsv" >"$ST/pkgs.$$" nf_rev=$(awk -F '\t' '$1 == "IC" { print $2; exit }' "$ST/installed.tsv") nf_write_installed "$nf_prev" "$(nf_channel)" install "$nf_cur" "$VH/current/bin/venutian" "$nf_rev" "$ST/pkgs.$$" rm -f "$ST/pkgs.$$" "$ST/shown" nf_compute >/dev/null 2>&1 echo "Rolled back to $nf_prev." } # =========================================================================== # Entry # =========================================================================== nf_usage() { cat <<'EOF' venutian-notifier: update notices for Venutian releases and libraries venutian-notifier status what is installed, what is waiting venutian-notifier check --now check right now and print the result venutian-notifier off | on turn the daily check off or on venutian-notifier channel stable follow stable releases (or: nightly) venutian-notifier libraries list new and updated libraries venutian-notifier libraries --fetch download one and verify its SHA 256 venutian-notifier libraries off | on library notices off or on venutian-notifier desktop on | off a daily desktop notification venutian-notifier rollback go back to the previous version The check reads https://venutian.org/api/v1/updates.json at most once a day and sends nothing about you. VENUTIAN_NO_UPDATE_CHECK=1 or DO_NOT_TRACK=1 also silence it, and it is quiet in CI and when stderr is not a terminal. EOF } nf_main() { nf_init_paths || exit 0 nf_init_base nf_platform nf_cmd=${1-help} [ $# -gt 0 ] && shift case $nf_cmd in after-run) nf_after_run; exit 0 ;; maybe-print) nf_maybe_print; exit 0 ;; maybe-refresh) nf_maybe_refresh; exit 0 ;; setup) nf_cmd_setup "$@"; exit $? ;; flat) # For install.sh and notify.sh: the same hardened JSON reader. nf_is_uint "${2-}" || exit 2 nf_flat "${1-}" "$2" exit $? ;; status) nf_cmd_status ;; check) case ${1-} in --now | '') nf_cmd_check_now; exit $? ;; *) nf_usage >&2; exit 2 ;; esac ;; off) nf_conf_set update_check off || exit 1 # A marker beside the state, so the wrapper skips the notifier without # parsing the config on every run. nf_ensure_state && : >"$ST/off" echo "Update checks are off. Turn them back on: venutian-notifier on" ;; on) nf_conf_set update_check on || exit 1 rm -f "$ST/off" echo "Update checks are on. They run at most once a day." ;; channel) case ${1-} in stable | nightly) nf_conf_set channel "$1" || exit 1 if [ -f "$INSTALLED" ]; then nf_compute >/dev/null 2>&1; fi echo "Following the $1 channel." ;; '') nf_require_install; echo "Following the $(nf_channel) channel." ;; *) echo "Usage: venutian-notifier channel stable|nightly" >&2; exit 2 ;; esac ;; libraries) nf_cmd_libraries "$@"; exit $? ;; desktop) case ${1-} in on) nf_desktop_on || { echo "Could not register the daily job."; exit 1; } echo "Desktop notifications are on." ;; off) nf_desktop_off; echo "Desktop notifications are off." ;; *) echo "Usage: venutian-notifier desktop on|off" >&2; exit 2 ;; esac ;; desktop-run) nf_desktop_run; exit 0 ;; rollback) nf_cmd_rollback; exit $? ;; version | --version) echo "venutian-notifier $NOTIFIER_VERSION" ;; help | -h | --help) nf_usage ;; *) nf_usage >&2; exit 2 ;; esac exit 0 } nf_main "$@" VENUTIAN_NOTIFIER_PAYLOAD_EOF vi_emit "$1/venutian-wrapper.sh" <<'VENUTIAN_NOTIFIER_PAYLOAD_EOF' #!/bin/sh # venutian-notifier-wrapper 1 # The `venutian` command on your PATH. It runs the real toolchain, keeps its # exit code, and afterwards shows the daily update notice on stderr (only in # a terminal, never in CI, never when you opted out). The check itself runs # in the background, so your command is never slowed by the network. # # Installed as a symlink: ~/.local/bin/venutian -> ~/.venutian/lib/notifier/venutian-wrapper.sh # Find the notifier directory through the symlink chain. vw_src=$0 while [ -h "$vw_src" ]; do case $vw_src in */*) vw_dir=${vw_src%/*} ;; *) vw_dir=. ;; esac vw_dir=$(cd -P "${vw_dir:-/}" 2>/dev/null && pwd) || break vw_lnk=$(readlink "$vw_src") || break case $vw_lnk in /*) vw_src=$vw_lnk ;; *) vw_src=$vw_dir/$vw_lnk ;; esac done case $vw_src in */*) vw_nd=${vw_src%/*} ;; *) vw_nd=. ;; esac vw_nd=$(cd -P "${vw_nd:-/}" 2>/dev/null && pwd) || vw_nd= case $vw_nd in */lib/notifier) VENUTIAN_HOME=${vw_nd%/lib/notifier} ;; *) VENUTIAN_HOME=${VENUTIAN_HOME:-$HOME/.venutian} vw_nd=$VENUTIAN_HOME/lib/notifier ;; esac # The toolchain: the managed install, or the one notify.sh found. vw_bin=$VENUTIAN_HOME/current/bin/venutian if [ ! -x "$vw_bin" ] && [ -f "$vw_nd/toolchain" ]; then IFS= read -r vw_bin <"$vw_nd/toolchain" || : fi if [ ! -x "$vw_bin" ]; then echo "venutian: the toolchain is missing. Reinstall: curl -fsSL https://venutian.org/install.sh | sh" >&2 exit 127 fi "$vw_bin" "$@" vw_rc=$? # Only people at a terminal hear about updates; scripts and pipes never do. [ -t 2 ] || exit "$vw_rc" [ -f "$vw_nd/notifier.sh" ] || exit "$vw_rc" # The opt outs that need no file, read the way the notifier reads them # (it checks these and a few more CI variables again). for vw_opt in "${VENUTIAN_NO_UPDATE_CHECK-}" "${DO_NOT_TRACK-}" "${CI-}" "${CONTINUOUS_INTEGRATION-}" \ "${BUILD_NUMBER-}" "${GITHUB_ACTIONS-}" "${GITLAB_CI-}" "${TF_BUILD-}" "${BUILDKITE-}" "${JENKINS_URL-}"; do case $vw_opt in '' | 0 | false | FALSE | False | no | off) ;; *) exit "$vw_rc" ;; esac done # Fast path: when today's notice is settled, no advisory is waiting and the # next check is not due, there is nothing to do, so the notifier never starts. case ${XDG_STATE_HOME-} in /*) vw_st=$XDG_STATE_HOME ;; *) vw_st=$HOME/.local/state ;; esac vw_st=$vw_st/venutian/notifier [ -f "$vw_st/off" ] && exit "$vw_rc" if [ ! -s "$vw_st/advisory.txt" ] && [ -f "$vw_st/shown" ] && [ -f "$vw_st/next-check" ]; then vw_shown= vw_next= IFS= read -r vw_shown <"$vw_st/shown" IFS= read -r vw_next <"$vw_st/next-check" vw_clock=$(date -u '+%s %Y-%m-%d' 2>/dev/null) case $vw_next in '' | *[!0-9]*) ;; *) if [ "${vw_clock#* }" = "$vw_shown" ] && [ "${vw_clock%% *}" -lt "$vw_next" ]; then exit "$vw_rc" fi ;; esac fi # after-run prints what is waiting and starts the daily refresh detached. /bin/sh "$vw_nd/notifier.sh" after-run || : exit "$vw_rc" VENUTIAN_NOTIFIER_PAYLOAD_EOF vi_emit "$1/venutian-notifier" <<'VENUTIAN_NOTIFIER_PAYLOAD_EOF' #!/bin/sh # venutian-notifier: control the Venutian update notices. # Run `venutian-notifier help` for the commands. # # Installed as a symlink: ~/.local/bin/venutian-notifier -> ~/.venutian/lib/notifier/venutian-notifier vn_src=$0 while [ -h "$vn_src" ]; do vn_dir=$(cd -P "$(dirname "$vn_src")" 2>/dev/null && pwd) || break vn_lnk=$(readlink "$vn_src") || break case $vn_lnk in /*) vn_src=$vn_lnk ;; *) vn_src=$vn_dir/$vn_lnk ;; esac done vn_nd=$(cd -P "$(dirname "$vn_src")" 2>/dev/null && pwd) || vn_nd= if [ ! -f "$vn_nd/notifier.sh" ]; then vn_nd=${VENUTIAN_HOME:-$HOME/.venutian}/lib/notifier fi if [ ! -f "$vn_nd/notifier.sh" ]; then echo "venutian-notifier: notifier.sh is missing. Reinstall: curl -fsSL https://venutian.org/install.sh | sh" >&2 exit 1 fi case ${1-} in after-run | maybe-print | maybe-refresh | setup | desktop-run) echo "venutian-notifier: $1 is internal. Run venutian-notifier help." >&2 exit 2 ;; esac exec /bin/sh "$vn_nd/notifier.sh" "$@" VENUTIAN_NOTIFIER_PAYLOAD_EOF return 0 } vi_emit() { cat >"$1" || return 1 chmod 755 "$1" } # Writes notifier.sh, venutian-wrapper.sh and venutian-notifier into $1. vi_write_notifier() { mkdir -p "$1" || return 1 if vi_payload "$1"; then return 0; fi # Running the unbuilt source from a checkout: take the files beside it. vi_srcdir= if [ -f "$0" ]; then vi_srcdir=$(cd "$(dirname "$0")" 2>/dev/null && pwd)/notifier; fi if [ -n "$vi_srcdir" ] && [ -f "$vi_srcdir/notifier.sh" ]; then for vi_f in notifier.sh venutian-wrapper.sh venutian-notifier; do vi_emit "$1/$vi_f" <"$vi_srcdir/$vi_f" || return 1 done return 0 fi return 1 } # =========================================================================== # 1. Detect # =========================================================================== vi_detect() { vi_uname_s=$(uname -s 2>/dev/null) vi_uname_m=$(uname -m 2>/dev/null) case $vi_uname_s in Darwin) VI_OS=macos ;; Linux) VI_OS=linux ;; *) vi_err "Venutian installs with this script on macOS and Linux; this system reports '$vi_uname_s'. Downloads: $VI_MANUAL" ;; esac case $vi_uname_m in arm64 | aarch64 | armv8* | arm64e) VI_ARCH=arm64 ;; x86_64 | amd64) VI_ARCH=x86_64 ;; *) vi_err "No Venutian build for the '$vi_uname_m' processor. Downloads: $VI_MANUAL" ;; esac # A shell running under Rosetta reports x86_64 on Apple silicon. if [ "$VI_OS" = macos ] && [ "$VI_ARCH" = x86_64 ] && [ "$(sysctl -n sysctl.proc_translated 2>/dev/null)" = 1 ]; then VI_ARCH=arm64 fi VI_LIBC= if [ "$VI_OS" = linux ]; then VI_LIBC=glibc if ldd --version 2>&1 | grep -qi musl; then VI_LIBC=musl; fi fi } # =========================================================================== # Network # =========================================================================== vi_init_base() { VI_BASE=$VI_SITE VI_LOOPBACK=0 if [ -n "${VENUTIAN_BASE_URL-}" ]; then vi_b=${VENUTIAN_BASE_URL%/} case $vi_b in http://127.0.0.1) VI_BASE=$vi_b; VI_LOOPBACK=1 ;; http://127.0.0.1:*) vi_p=${vi_b#http://127.0.0.1:} case $vi_p in '' | *[!0-9]*) ;; *) if [ "${#vi_p}" -le 5 ]; then VI_BASE=$vi_b; VI_LOOPBACK=1; fi ;; esac ;; esac if [ "$VI_LOOPBACK" = 0 ]; then vi_say "Ignoring VENUTIAN_BASE_URL: only http://127.0.0.1 is accepted. Using $VI_SITE." fi fi } # vi_get : HTTPS only (plain http only for the 127.0.0.1 test server). vi_get() { if vi_has curl; then if [ "$VI_LOOPBACK" = 1 ]; then curl -fsSL --proto '=http,https' --proto-redir '=http,https' --retry 2 \ -A "venutian-install/1 ($VI_OS; $VI_ARCH)" -o "$2" "$1" &1 | grep -q 'https-only'; then wget -q --https-only -U "venutian-install/1 ($VI_OS; $VI_ARCH)" -O "$2" "$1" "$VI_TMP/latest.flat" || vi_err "The release manifest from $VI_BASE could not be read. Run the command again in a minute." vi_pick=$(awk -F '\t' -v ch="$VI_CHANNEL" -v os="$VI_OS" -v arch="$VI_ARCH" -v libc="$VI_LIBC" ' function okv(v) { return length(v) <= 64 && v ~ /^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$/ } function okh(v) { return length(v) == 64 && v ~ /^[0-9a-f]+$/ } function okf(v) { return length(v) <= 128 && v ~ /^[A-Za-z0-9][A-Za-z0-9._-]*$/ } NF != 2 { next } $1 == ch ".version" { ver = $2; next } { k = split($1, p, ".") if (k == 4 && p[1] == ch && p[2] == "assets" && p[3] ~ /^[0-9]+$/ && length(p[3]) < 4) { i = p[3] + 1; if (i > n) n = i if (!((i, p[4]) in a)) a[i, p[4]] = $2 } } END { if (!okv(ver)) exit 3 best = 0; score = 0 for (i = 1; i <= n; i++) { if (a[i, "os"] != os || a[i, "arch"] != arch || a[i, "kind"] != "tar.gz") continue if (!okf(a[i, "file"]) || !okh(a[i, "sha256"])) continue s = 1 if (((i, "libc") in a) && a[i, "libc"] != "") { if (a[i, "libc"] == libc) s = 2; else s = 0 } if (s > score) { score = s; best = i } } if (!best) { print ver; exit 4 } print ver "\t" a[best, "file"] "\t" a[best, "sha256"] }' "$VI_TMP/latest.flat") case $? in 0) ;; 3) if [ "$VI_CHANNEL" = nightly ]; then vi_err "No daily build is published right now. Install stable: $VI_ONE_LINER" fi vi_err "The release manifest names no $VI_CHANNEL version. Run the command again in a minute." ;; *) vi_err "Venutian $vi_pick has no build for $VI_OS $VI_ARCH. Downloads: $VI_MANUAL" ;; esac VI_VERSION=$(printf '%s\n' "$vi_pick" | awk -F '\t' '{ print $1 }') VI_FILE=$(printf '%s\n' "$vi_pick" | awk -F '\t' '{ print $2 }') VI_SHA=$(printf '%s\n' "$vi_pick" | awk -F '\t' '{ print $3 }') } # VENUTIAN_VERSION pins a release: its SHA256SUMS names the files and hashes. vi_resolve_pinned() { VI_VERSION=$1 vi_okv "$VI_VERSION" || vi_err "VENUTIAN_VERSION must look like 1.2.3; got '$VI_VERSION'." vi_get "$VI_BASE/downloads/$VI_VERSION/SHA256SUMS" "$VI_TMP/SHA256SUMS" || vi_err "Venutian $VI_VERSION was not found at $VI_BASE. Every release: $VI_SITE/releases/" vi_pick=$(awk -v os="$VI_OS" -v arch="$VI_ARCH" -v libc="$VI_LIBC" ' { h = $1; f = $2; sub(/^\*/, "", f) if (length(h) != 64 || h !~ /^[0-9a-f]+$/) next if (length(f) > 128 || f !~ /^[A-Za-z0-9][A-Za-z0-9._-]*$/) next if (f == "venutian-" os "-" arch "-" libc ".tar.gz") { m = f "\t" h } else if (f == "venutian-" os "-" arch ".tar.gz") { g = f "\t" h } } END { if (m != "") print m; else if (g != "") print g; else exit 4 }' "$VI_TMP/SHA256SUMS") || vi_err "Venutian $VI_VERSION has no build for $VI_OS $VI_ARCH. Downloads: $VI_MANUAL" VI_FILE=$(printf '%s\n' "$vi_pick" | awk -F '\t' '{ print $1 }') VI_SHA=$(printf '%s\n' "$vi_pick" | awk -F '\t' '{ print $2 }') } # =========================================================================== # 5. Place # =========================================================================== # Replaces the symlink $2 with one pointing at $1 in a single rename. vi_swap_link() { vi_tmpl=$2.new.$$ rm -f "$vi_tmpl" ln -s "$1" "$vi_tmpl" || return 1 mv -T -f "$vi_tmpl" "$2" 2>/dev/null && return 0 mv -h -f "$vi_tmpl" "$2" 2>/dev/null && return 0 rm -f "$2" && mv -f "$vi_tmpl" "$2" } # The version the existing install runs, if any. vi_existing() { VI_PREV= VI_PREV_CHANNEL= vi_state=${XDG_STATE_HOME:-$HOME/.local/state} case $vi_state in /*) ;; *) vi_state=$HOME/.local/state ;; esac VI_STATE_DIR=$vi_state/venutian/notifier if [ -h "$VI_HOME/current" ]; then vi_l=$(readlink "$VI_HOME/current") vi_l=${vi_l%/} vi_l=${vi_l##*/} if vi_okv "$vi_l" && [ -d "$VI_HOME/versions/$vi_l" ]; then VI_PREV=$vi_l; fi fi VI_RECORDED_PREV= if [ -f "$VI_STATE_DIR/installed.json" ]; then VI_RECORDED_PREV=$(awk -F '"' '$2 == "previous" { print $4; exit }' "$VI_STATE_DIR/installed.json" 2>/dev/null) if ! vi_okv "$VI_RECORDED_PREV" || [ ! -d "$VI_HOME/versions/$VI_RECORDED_PREV" ]; then VI_RECORDED_PREV=; fi VI_PREV_CHANNEL=$(awk '/"channel"/ { if ($0 ~ /"nightly"/) print "nightly"; else print "stable"; exit }' "$VI_STATE_DIR/installed.json" 2>/dev/null) fi } vi_unpack() { mkdir -p "$VI_TMP/unpack" || return 1 (cd "$VI_TMP/unpack" && tar -xzf "$VI_TMP/$VI_FILE") || vi_err "The download could not be unpacked. Run the command again." VI_ROOT= if [ -f "$VI_TMP/unpack/bin/venutian" ]; then VI_ROOT=$VI_TMP/unpack else for vi_d in "$VI_TMP"/unpack/*; do if [ -d "$vi_d" ] && [ -f "$vi_d/bin/venutian" ]; then VI_ROOT=$vi_d; break; fi done fi [ -n "$VI_ROOT" ] || vi_err "The download has no bin/venutian inside. Downloads: $VI_MANUAL" } vi_place() { mkdir -p "$VI_HOME/versions" || vi_err "Could not create $VI_HOME/versions." vi_stage=$VI_HOME/versions/.partial.$$ rm -rf "$vi_stage" mv "$VI_ROOT" "$vi_stage" 2>/dev/null || cp -R "$VI_ROOT" "$vi_stage" || vi_err "Could not copy the toolchain into $VI_HOME/versions." chmod 755 "$vi_stage/bin/"* 2>/dev/null vi_dest=$VI_HOME/versions/$VI_VERSION if [ -e "$vi_dest" ]; then rm -rf "$VI_HOME/versions/.old.$$" mv "$vi_dest" "$VI_HOME/versions/.old.$$" || vi_err "Could not replace $vi_dest." fi mv "$vi_stage" "$vi_dest" || vi_err "Could not move the toolchain into $vi_dest." rm -rf "$VI_HOME/versions/.old.$$" vi_swap_link "versions/$VI_VERSION" "$VI_HOME/current" || vi_err "Could not point $VI_HOME/current at $VI_VERSION." } # 9. Keep: the current version and the one before it; older ones go. vi_prune() { for vi_d in "$VI_HOME"/versions/* "$VI_HOME"/versions/.partial.* "$VI_HOME"/versions/.old.*; do [ -d "$vi_d" ] || continue vi_n=${vi_d##*/} [ "$vi_n" = "$VI_VERSION" ] && continue [ -n "$VI_PREV" ] && [ "$vi_n" = "$VI_PREV" ] && continue rm -rf "$vi_d" done } # =========================================================================== # 6. Link and PATH # =========================================================================== # Puts a symlink named $2 pointing at $1, moving a foreign file aside. vi_link() { if [ -e "$2" ] && [ ! -h "$2" ]; then if grep 'venutian-notifier-wrapper' "$2" >/dev/null 2>&1; then rm -f "$2" else mv -f "$2" "$2.before-venutian" || return 1 vi_say "Moved the existing $2 to $2.before-venutian." fi fi vi_swap_link "$1" "$2" } vi_profile() { vi_sh=${SHELL##*/} case $vi_sh in zsh) VI_PROFILE=${ZDOTDIR:-$HOME}/.zshrc; VI_PROFILE_KIND=posix ;; bash) if [ "$VI_OS" = macos ]; then VI_PROFILE=$HOME/.bash_profile; else VI_PROFILE=$HOME/.bashrc; fi VI_PROFILE_KIND=posix ;; fish) VI_PROFILE=${XDG_CONFIG_HOME:-$HOME/.config}/fish/conf.d/venutian.fish; VI_PROFILE_KIND=fish ;; *) VI_PROFILE=$HOME/.profile; VI_PROFILE_KIND=posix ;; esac } vi_path() { case ":${PATH-}:" in *":$VI_BIN:"*) return 0 ;; esac vi_profile if vi_flag "${VENUTIAN_NO_MODIFY_PATH-}"; then vi_say "Add $VI_BIN to your PATH to run venutian: export PATH=\"$VI_BIN:\$PATH\"" return 0 fi if [ -f "$VI_PROFILE" ] && grep 'Added by the Venutian installer' "$VI_PROFILE" >/dev/null 2>&1; then vi_say "Open a new terminal to put $VI_BIN on your PATH (set up in $VI_PROFILE)." return 0 fi if vi_dry; then vi_step "dry run: add $VI_BIN to PATH in $VI_PROFILE" return 0 fi mkdir -p "$(dirname "$VI_PROFILE")" 2>/dev/null # The profile line expands $PATH when the shell starts, not now. # shellcheck disable=SC2016 if [ "$VI_PROFILE_KIND" = fish ]; then printf '\n# Added by the Venutian installer\ncontains %s $PATH; or set -gx PATH %s $PATH\n' "$VI_BIN" "$VI_BIN" >>"$VI_PROFILE" else printf '\n# Added by the Venutian installer\nexport PATH="%s:$PATH"\n' "$VI_BIN" >>"$VI_PROFILE" fi || { vi_say "Could not write $VI_PROFILE. Add this line to your shell profile: export PATH=\"$VI_BIN:\$PATH\"" return 0 } vi_say "Added $VI_BIN to PATH in $VI_PROFILE. Open a new terminal, or run: export PATH=\"$VI_BIN:\$PATH\"" } # Warns when another venutian comes first on the PATH. vi_shadow_check() { vi_old_ifs=$IFS IFS=: for vi_dir in ${PATH-}; do [ -n "$vi_dir" ] || continue if [ -x "$vi_dir/venutian" ]; then if [ "$vi_dir" != "$VI_BIN" ]; then vi_say "Note: $vi_dir/venutian comes before $VI_BIN on your PATH. Put $VI_BIN first to run this install." fi break fi done IFS=$vi_old_ifs } # =========================================================================== # Main # =========================================================================== vi_main() { [ -n "${HOME-}" ] || vi_err "HOME is not set." umask 022 LC_ALL=C export LC_ALL vi_detect vi_init_base VI_HOME=${VENUTIAN_HOME:-$HOME/.venutian} case $VI_HOME in /*) ;; *) vi_err "VENUTIAN_HOME must be an absolute path." ;; esac VI_HOME=${VI_HOME%/} VI_BIN=$HOME/.local/bin vi_existing VI_CHANNEL=${VENUTIAN_CHANNEL:-${VI_PREV_CHANNEL:-stable}} case $VI_CHANNEL in stable | nightly) ;; *) vi_err "VENUTIAN_CHANNEL is stable or nightly; got '$VI_CHANNEL'." ;; esac vi_has tar || vi_err "tar is required. Install it and run the command again." VI_TMP=$(mktemp -d 2>/dev/null || mktemp -d -t venutian) || vi_err "Could not create a temporary directory." trap 'rm -rf "$VI_TMP"' EXIT trap 'rm -rf "$VI_TMP"; exit 130' INT TERM HUP vi_write_notifier "$VI_TMP/notifier" || vi_err "This copy of install.sh is incomplete. Use: $VI_ONE_LINER" vi_say "Venutian installer" vi_step "System: $VI_OS $VI_ARCH${VI_LIBC:+ ($VI_LIBC)}" # 2. Resolve if [ -n "${VENUTIAN_VERSION-}" ]; then vi_resolve_pinned "$VENUTIAN_VERSION" else vi_resolve_latest fi VI_VIA=install [ -n "$VI_PREV" ] && VI_VIA=upgrade # Reinstalling the same version keeps the rollback target it already had. if [ -n "$VI_PREV" ] && [ "$VI_PREV" = "$VI_VERSION" ]; then VI_PREV=$VI_RECORDED_PREV; fi if vi_flag "${DO_NOT_TRACK-}" || vi_flag "${VENUTIAN_NO_ANALYTICS-}"; then VI_URL=$VI_BASE/downloads/$VI_VERSION/$VI_FILE else VI_URL="$VI_BASE/dl/$VI_VERSION/$VI_FILE?via=$VI_VIA" fi if [ "$VI_VIA" = upgrade ]; then vi_step "Release: $VI_VERSION ($VI_CHANNEL), upgrading an existing install" else vi_step "Release: $VI_VERSION ($VI_CHANNEL)" fi if vi_dry; then vi_step "dry run: download $VI_URL" vi_step "dry run: verify SHA 256 $VI_SHA" vi_step "dry run: unpack into $VI_HOME/versions/$VI_VERSION and point $VI_HOME/current at it" if vi_flag "${VENUTIAN_NO_UPDATE_CHECK-}"; then vi_step "dry run: link $VI_BIN/venutian to $VI_HOME/current/bin/venutian (no notifier)" else vi_step "dry run: install the notifier into $VI_HOME/lib/notifier" vi_step "dry run: link $VI_BIN/venutian and $VI_BIN/venutian-notifier" fi vi_path vi_step "dry run: keep $VI_VERSION${VI_PREV:+ and $VI_PREV}, remove older versions" vi_say "Dry run finished. Nothing was changed." return 0 fi # 3. Download vi_step "Downloading $VI_FILE" vi_get "$VI_URL" "$VI_TMP/$VI_FILE" || vi_err "The download from $VI_BASE failed. Check your connection and run the command again." # 4. Verify vi_got=$(vi_sha256 "$VI_TMP/$VI_FILE") if [ "$vi_got" != "$VI_SHA" ]; then vi_err "SHA 256 mismatch for $VI_FILE (expected $VI_SHA, got $vi_got). Nothing was installed." fi vi_step "Verified SHA 256 $VI_SHA" # 5. Place vi_unpack vi_place vi_step "Installed into $VI_HOME/versions/$VI_VERSION" # 6 and 7. Link, notifier mkdir -p "$VI_BIN" || vi_err "Could not create $VI_BIN." vi_nd=$VI_HOME/lib/notifier if vi_flag "${VENUTIAN_NO_UPDATE_CHECK-}"; then vi_link "$VI_HOME/current/bin/venutian" "$VI_BIN/venutian" || vi_err "Could not link $VI_BIN/venutian." if [ -h "$VI_BIN/venutian-notifier" ]; then rm -f "$VI_BIN/venutian-notifier"; fi vi_step "Linked $VI_BIN/venutian (update notifier not installed)" else mkdir -p "$vi_nd" || vi_err "Could not create $vi_nd." for vi_f in notifier.sh venutian-wrapper.sh venutian-notifier; do if ! { cp "$VI_TMP/notifier/$vi_f" "$vi_nd/.$vi_f.$$" && chmod 755 "$vi_nd/.$vi_f.$$" && mv -f "$vi_nd/.$vi_f.$$" "$vi_nd/$vi_f"; }; then vi_err "Could not install $vi_nd/$vi_f." fi done rm -f "$vi_nd/toolchain" vi_link "$vi_nd/venutian-wrapper.sh" "$VI_BIN/venutian" || vi_err "Could not link $VI_BIN/venutian." vi_link "$vi_nd/venutian-notifier" "$VI_BIN/venutian-notifier" || vi_err "Could not link $VI_BIN/venutian-notifier." /bin/sh "$vi_nd/notifier.sh" setup "$VI_VERSION" "$VI_CHANNEL" install "$VI_PREV" \ "$VI_HOME/current/bin/venutian" /dev/null 2>&1 || : vi_step "Linked $VI_BIN/venutian and $VI_BIN/venutian-notifier" fi vi_path vi_shadow_check # 9. Keep vi_prune # 8. Tell vi_say "" if [ -n "$VI_PREV" ] && [ "$VI_PREV" != "$VI_VERSION" ]; then vi_say "Venutian $VI_VERSION is installed (was $VI_PREV)." else vi_say "Venutian $VI_VERSION is installed." fi vi_say " Toolchain: $VI_HOME/versions/$VI_VERSION" vi_say " Command: $VI_BIN/venutian" vi_say " Start: venutian run hello.ven" if [ -n "$VI_PREV" ] && [ "$VI_PREV" != "$VI_VERSION" ] && ! vi_flag "${VENUTIAN_NO_UPDATE_CHECK-}"; then vi_say " Go back: venutian-notifier rollback" fi if ! vi_flag "${VENUTIAN_NO_UPDATE_CHECK-}"; then vi_say "" vi_say "$VI_PARAGRAPH" fi return 0 } vi_main "$@"