#!/bin/sh set -eu LC_ALL=C export LC_ALL fail() { echo "flog installer: $*" >&2 exit 1 } validate_version() { case "$1" in ""|*[!A-Za-z0-9.+-]*|*/*) fail "invalid version '$1'" ;; esac } validate_url() { case "$1" in *[[:space:]]*) fail "refusing URL containing whitespace" ;; *#*) fail "refusing URL containing a fragment" ;; esac case "$1" in https://*) ;; http://*) [ "${FLOG_ALLOW_HTTP:-0}" = 1 ] || fail "refusing insecure URL $1" ;; *) fail "refusing non-HTTP(S) URL $1" ;; esac } download() { source_url=$1 destination=$2 validate_url "$source_url" command -v curl >/dev/null 2>&1 || fail "curl is required" if [ "${FLOG_ALLOW_HTTP:-0}" = 1 ]; then curl --fail --location --silent --show-error \ --proto '=http,https' --proto-redir '=http,https' \ --output "$destination" "$source_url" else curl --fail --location --silent --show-error \ --proto '=https' --proto-redir '=https' \ --output "$destination" "$source_url" fi } checksum() { 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 fail "sha256sum or shasum is required" fi } resolve_channel() { line_count=$(wc -l <"$1") || return 1 awk -F '\t' -v selected="$2" -v expected_lines="$line_count" ' function valid_number(value, digit, position, maximum) { if (value != "0" && value !~ /^[1-9][0-9]*$/) return 0 if (length(value) < 16) return 1 if (length(value) > 16) return 0 maximum = "9007199254740991" for (position = 1; position <= 16; position++) { digit = substr(value, position, 1) if (digit < substr(maximum, position, 1)) return 1 if (digit > substr(maximum, position, 1)) return 0 } return 1 } function valid_identifiers(value, prerelease, count, position, identifiers) { if (value == "") return 0 count = split(value, identifiers, "[.]") for (position = 1; position <= count; position++) { if (identifiers[position] == "" || identifiers[position] !~ /^[0-9A-Za-z-]+$/) return 0 if (prerelease && identifiers[position] ~ /^[0-9]+$/ && length(identifiers[position]) > 1 && substr(identifiers[position], 1, 1) == "0") return 0 } return 1 } function valid_version(value, build_at, build, prerelease_at, prerelease, count, core) { build_at = index(value, "+") if (build_at) { build = substr(value, build_at + 1) value = substr(value, 1, build_at - 1) if (!valid_identifiers(build, 0)) return 0 } prerelease_at = index(value, "-") if (prerelease_at) { prerelease = substr(value, prerelease_at + 1) value = substr(value, 1, prerelease_at - 1) if (!valid_identifiers(prerelease, 1)) return 0 } count = split(value, core, "[.]") return count == 3 && valid_number(core[1]) && valid_number(core[2]) && valid_number(core[3]) } function is_prerelease(value) { sub(/\+.*/, "", value) return index(value, "-") != 0 } NR == 1 { if ($0 != "flog-channels-v1") exit 20; next } $1 == "stable" { if (NF != 2 || stable_seen++ || !valid_version($2) || is_prerelease($2)) exit 21 if (selected == "stable") { value = $2; selected_seen++ } next } $1 == "prerelease" { if (NF != 3 || $2 == "stable" || $2 !~ /^[a-z][a-z0-9-]*$/ || prerelease_seen[$2]++ || !valid_version($3) || !is_prerelease($3)) exit 22 if (selected == $2) { value = $3; selected_seen++ } next } { exit 23 } END { if (NR != expected_lines || NR < 2 || stable_seen != 1 || selected_seen != 1) exit 24 print value } ' "$1" || fail "invalid channel manifest or unknown channel '$2'" } resolve_artifact() { line_count=$(wc -l <"$1") || return 1 awk -F '\t' -v wanted_version="$2" -v wanted_os="$3" -v wanted_arch="$4" -v expected_lines="$line_count" ' function valid_number(value, digit, position, maximum) { if (value != "0" && value !~ /^[1-9][0-9]*$/) return 0 if (length(value) < 16) return 1 if (length(value) > 16) return 0 maximum = "9007199254740991" for (position = 1; position <= 16; position++) { digit = substr(value, position, 1) if (digit < substr(maximum, position, 1)) return 1 if (digit > substr(maximum, position, 1)) return 0 } return 1 } function valid_identifiers(value, prerelease, count, position, identifiers) { if (value == "") return 0 count = split(value, identifiers, "[.]") for (position = 1; position <= count; position++) { if (identifiers[position] == "" || identifiers[position] !~ /^[0-9A-Za-z-]+$/) return 0 if (prerelease && identifiers[position] ~ /^[0-9]+$/ && length(identifiers[position]) > 1 && substr(identifiers[position], 1, 1) == "0") return 0 } return 1 } function valid_version(value, build_at, build, prerelease_at, prerelease, count, core) { build_at = index(value, "+") if (build_at) { build = substr(value, build_at + 1) value = substr(value, 1, build_at - 1) if (!valid_identifiers(build, 0)) return 0 } prerelease_at = index(value, "-") if (prerelease_at) { prerelease = substr(value, prerelease_at + 1) value = substr(value, 1, prerelease_at - 1) if (!valid_identifiers(prerelease, 1)) return 0 } count = split(value, core, "[.]") return count == 3 && valid_number(core[1]) && valid_number(core[2]) && valid_number(core[3]) } NR == 1 { if ($0 != "flog-release-v1") exit 30; next } $1 == "version" { if (NF != 2 || version_seen++) exit 31 version = $2 next } $1 == "revision" { if (NF != 2 || revision_seen++ || length($2) != 40 || $2 !~ /^[0-9a-fA-F]+$/) exit 32 next } $1 == "artifact" { key = $2 "/" $3 if (NF != 5 || (key != "darwin/amd64" && key != "darwin/arm64" && key != "linux/amd64" && key != "linux/arm64") || length($4) != 64 || $4 !~ /^[0-9a-fA-F]+$/ || seen[key]++) exit 33 artifact_count++ if ($2 == wanted_os && $3 == wanted_arch) { checksum = tolower($4) url = $5 selected++ } next } { exit 34 } END { if (NR != expected_lines || version_seen != 1 || version != wanted_version || !valid_version(version) || revision_seen != 1 || artifact_count != 4 || selected != 1 || url == "") exit 35 print checksum "\t" url } ' "$1" || fail "invalid release manifest for $2" } channel=${FLOG_CHANNEL:-stable} case "$channel" in [a-z]*) ;; *) fail "invalid channel '$channel'" ;; esac case "$channel" in *[!a-z0-9-]*) fail "invalid channel '$channel'" ;; esac if [ -n "${FLOG_VERSION:-}" ] && [ -n "${FLOG_CHANNEL:-}" ]; then fail "FLOG_VERSION and FLOG_CHANNEL cannot be used together" fi kernel=$(uname -s) || fail "cannot detect operating system" machine=$(uname -m) || fail "cannot detect architecture" case "$kernel" in Linux) target_os=linux ;; Darwin) target_os=darwin ;; *) fail "unsupported operating system $kernel" ;; esac case "$machine" in x86_64|amd64) target_arch=amd64 ;; arm64|aarch64) target_arch=arm64 ;; *) fail "unsupported architecture $machine" ;; esac releases_url=${FLOG_RELEASES_URL:-https://flog.run/releases} releases_url=${releases_url%/} validate_url "$releases_url" install_dir=${FLOG_INSTALL_DIR:-${HOME:?HOME is required}/.local/bin} case "$install_dir" in /*) ;; *) fail "installation directory must be absolute" ;; esac mkdir -p "$install_dir" || fail "cannot create installation directory $install_dir" umask 077 work=$(mktemp -d "$install_dir/.flog-install.XXXXXX") || fail "cannot stage installation in $install_dir" cleanup() { rm -rf "$work" } trap cleanup EXIT trap 'exit 130' HUP INT TERM version=${FLOG_VERSION:-} if [ -z "$version" ]; then channels_file=$work/channels.v1 download "$releases_url/channels.v1" "$channels_file" || fail "cannot download channel manifest" version=$(resolve_channel "$channels_file" "$channel") fi validate_version "$version" release_file=$work/release.v1 download "$releases_url/$version/manifest.v1" "$release_file" || fail "cannot download release manifest for $version" artifact_record=$(resolve_artifact "$release_file" "$version" "$target_os" "$target_arch") tab=$(printf '\t') expected_checksum=${artifact_record%%"$tab"*} artifact_url=${artifact_record#*"$tab"} [ "$expected_checksum" != "$artifact_record" ] || fail "invalid artifact record" validate_url "$artifact_url" artifact_path=${artifact_url%%\?*} expected_filename="flog-$version-$target_os-$target_arch.tar.gz" [ "${artifact_path##*/}" = "$expected_filename" ] || fail "artifact URL has an unexpected filename" archive=$work/flog.tar.gz download "$artifact_url" "$archive" || fail "cannot download flog $version for $target_os/$target_arch" actual_checksum=$(checksum "$archive") [ "$actual_checksum" = "$expected_checksum" ] || fail "checksum mismatch for flog $version $target_os/$target_arch" entries=$(tar -tzf "$archive") || fail "cannot inspect release archive" [ "$entries" = "BUILD-INFO.json LICENSE flog" ] || fail "release archive has unexpected entries" staged_binary=$work/flog tar -xzf "$archive" -O flog >"$staged_binary" || fail "cannot extract flog executable" [ -s "$staged_binary" ] || fail "release archive contains an empty executable" chmod 0755 "$staged_binary" || fail "cannot make flog executable" mv -f "$staged_binary" "$install_dir/flog" || fail "cannot replace $install_dir/flog" trap - EXIT trap - HUP INT TERM cleanup echo "installed flog $version to $install_dir/flog" >&2