#!/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 } download_exact() { 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 status=$(curl --fail --silent --show-error --proto '=http,https' \ --write-out '%{http_code}' --output "$destination" "$source_url") || return 1 else status=$(curl --fail --silent --show-error --proto '=https' \ --write-out '%{http_code}' --output "$destination" "$source_url") || return 1 fi case "$status" in 2??) ;; *) rm -f "$destination"; return 1 ;; esac } 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 } configure_path() { case ":${PATH:-}:" in *:"$install_dir":*) return ;; esac home_dir=${HOME:-} if [ -z "$home_dir" ] || [ "$install_dir" != "$home_dir/.local/bin" ]; then echo "flog installer: add $install_dir to PATH in your shell profile to run flog by name" >&2 return fi shell_name=${SHELL:-} shell_name=${shell_name##*/} profile= command_line= reload_line= case "$shell_name" in bash) for candidate in "$home_dir/.bashrc" "$home_dir/.bash_profile"; do if [ ! -L "$candidate" ] && [ -f "$candidate" ] && [ -r "$candidate" ] && [ -w "$candidate" ]; then profile=$candidate break fi done command_line='export PATH="$HOME/.local/bin:$PATH"' reload_line='. "$HOME/.bashrc"' if [ "$profile" = "$home_dir/.bash_profile" ]; then reload_line='. "$HOME/.bash_profile"' fi ;; zsh) profile=$home_dir/.zshrc command_line='export PATH="$HOME/.local/bin:$PATH"' reload_line='. "$HOME/.zshrc"' ;; fish) profile=$home_dir/.config/fish/config.fish command_line='set -gx PATH "$HOME/.local/bin" $PATH' reload_line='source "$HOME/.config/fish/config.fish"' ;; esac if [ -n "$profile" ] && [ ! -L "$profile" ] && [ -f "$profile" ] && [ -r "$profile" ] && [ -w "$profile" ]; then marked_line="$command_line # flog installer PATH" if awk -v expected="$command_line" -v marked="$marked_line" '$0 == expected || $0 == marked { found=1 } END { exit !found }' "$profile"; then echo "flog installer: PATH setup already present in $profile; open a new shell or run: $reload_line" >&2 return fi # Append only: replacing a checked profile would lose edits made before # the rename. As with Bun, a late symlink swap remains a pathname race. if [ ! -L "$profile" ] && [ -f "$profile" ] && [ -w "$profile" ] && printf '\n%s\n' "$marked_line" >>"$profile"; then echo "flog installer: added $install_dir to PATH in $profile; open a new shell or run: $reload_line" >&2 return fi fi # No safe writable regular profile was available; the binary is installed. if [ -n "$profile" ]; then echo "flog installer: add this line to $profile if not already present, then open a new shell:" >&2 else echo 'flog installer: add this line to your shell profile if not already present, then open a new shell:' >&2 fi case "$shell_name" in fish) echo ' set -gx PATH "$HOME/.local/bin" $PATH' >&2 ;; *) echo ' export PATH="$HOME/.local/bin:$PATH"' >&2 ;; esac if [ -n "$profile" ]; then echo "flog installer: after editing, reload with: $reload_line" >&2 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" } resolve_rolling_artifact() { line_count=$(wc -l <"$1") || return 1 awk -F '\t' -v wanted_os="$2" -v wanted_arch="$3" -v expected_lines="$line_count" ' NR == 1 { if ($0 != "flog-rolling-v1") exit 40; next } NR == 2 { if (NF != 2 || $1 != "product" || $2 != "flog") exit 41; next } NR == 3 { if (NF != 2 || $1 != "channel" || $2 != "unstable") exit 42; next } NR == 4 { if (NF != 2 || $1 != "channel-id" || $2 != "3") exit 43; channel_id=$2; next } NR == 5 { if (NF != 2 || $1 != "job" || $2 != "3") exit 44; job=$2; next } NR == 6 { if (NF != 2 || $1 != "run" || $2 !~ /^[1-9][0-9]*$/ || length($2) > 10 || $2 + 0 > 4294967295) exit 45 run=$2; next } NR == 7 { if (NF != 2 || $1 != "source-ref" || $2 != "master") exit 46; next } NR == 8 { if (NF != 2 || $1 != "source-sha" || length($2) != 40 || $2 !~ /^[0-9a-f]+$/) exit 47 source_sha=$2; next } NR == 9 { if (NF != 2 || $1 != "version" || $2 != "0.0.0-dev." substr(source_sha, 1, 12)) exit 48 version=$2; next } NR >= 10 && NR <= 13 { position=NR-9 expected_os=position <= 2 ? "darwin" : "linux" expected_arch=position % 2 ? "amd64" : "arm64" if (NF != 6 || $1 != "artifact" || $2 != expected_os || $3 != expected_arch || $4 !~ /^[1-9][0-9]*$/ || length($4) > 9 || $4 + 0 > 268435456 || length($5) != 64 || $5 !~ /^[0-9a-f]+$/ || $6 == "") exit 49 if ($2 == wanted_os && $3 == wanted_arch) { size=$4; digest=$5; url=$6; selected++ } next } { exit 50 } END { if (NR != expected_lines || NR != 13 || selected != 1) exit 51 print version "\t" source_sha "\t" channel_id "\t" job "\t" run "\t" size "\t" digest "\t" url } ' "$1" || fail "invalid rolling channel manifest" } validate_build_info() { compact=$(tr -d ' \t\r\n' <"$1") || fail "cannot inspect BUILD-INFO.json" expected="{\"version\":\"$2\",\"revision\":\"$3\",\"os\":\"$4\",\"arch\":\"$5\"}" [ "$compact" = "$expected" ] || fail "release archive BUILD-INFO.json does not match its manifest" } 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:-} rolling=0 if [ -z "$version" ] && [ "$channel" = unstable ]; then rolling=1 rolling_file=$work/rolling.v1 download_exact "$releases_url/channels/unstable/rolling.v1" "$rolling_file" || fail "cannot download rolling channel manifest" artifact_record=$(resolve_rolling_artifact "$rolling_file" "$target_os" "$target_arch") tab=$(printf '\t') version=${artifact_record%%"$tab"*}; artifact_record=${artifact_record#*"$tab"} revision=${artifact_record%%"$tab"*}; artifact_record=${artifact_record#*"$tab"} channel_id=${artifact_record%%"$tab"*}; artifact_record=${artifact_record#*"$tab"} rolling_job=${artifact_record%%"$tab"*}; artifact_record=${artifact_record#*"$tab"} rolling_run=${artifact_record%%"$tab"*}; artifact_record=${artifact_record#*"$tab"} expected_size=${artifact_record%%"$tab"*}; artifact_record=${artifact_record#*"$tab"} expected_checksum=${artifact_record%%"$tab"*} artifact_url=${artifact_record#*"$tab"} [ "$expected_checksum" != "$artifact_record" ] || fail "invalid rolling artifact record" elif [ -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" if [ "$rolling" -eq 0 ]; then 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" revision=$(awk -F '\t' '$1 == "revision" { print tolower($2) }' "$release_file") fi 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" if [ "$rolling" -eq 1 ]; then case "$artifact_url" in *\?*) fail "rolling artifact URL must not contain a query" ;; esac case "$artifact_path" in */channels/unstable/rolling/"$channel_id"/runs/"$rolling_run"/artifacts/"$expected_filename") ;; *) fail "rolling artifact URL does not match its channel and Run identity" ;; esac fi archive=$work/flog.tar.gz if [ "$rolling" -eq 1 ]; then download_exact "$artifact_url" "$archive" || fail "cannot download flog $version for $target_os/$target_arch" else download "$artifact_url" "$archive" || fail "cannot download flog $version for $target_os/$target_arch" fi [ "$rolling" -eq 0 ] || [ "$(wc -c <"$archive")" -eq "$expected_size" ] || fail "size mismatch for flog $version $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" build_info=$work/BUILD-INFO.json tar -xzf "$archive" -O BUILD-INFO.json >"$build_info" || fail "cannot extract BUILD-INFO.json" [ "$rolling" -eq 0 ] || validate_build_info "$build_info" "$version" "$revision" "$target_os" "$target_arch" 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" if [ "$rolling" -eq 1 ]; then exact_file=$work/rolling-exact.v1 download_exact "$releases_url/channels/unstable/runs/$rolling_run/rolling.v1" "$exact_file" || fail "cannot revalidate rolling Run $rolling_run" exact_record=$(resolve_rolling_artifact "$exact_file" "$target_os" "$target_arch") [ "$exact_record" = "$version$tab$revision$tab$channel_id$tab$rolling_job$tab$rolling_run$tab$expected_size$tab$expected_checksum$tab$artifact_url" ] || fail "rolling Run $rolling_run changed during installation" fi state_path=$install_dir/flog.update-state rolling_state_ready=0 if [ "$rolling" -eq 1 ]; then binary_checksum=$(checksum "$staged_binary") state_file=$work/update-state if printf '{"schema":1,"channel":"unstable","channelId":%s,"job":%s,"run":%s,"sourceSHA":"%s","version":"%s","binarySHA256":"%s"}\n' \ "$channel_id" "$rolling_job" "$rolling_run" "$revision" "$version" "$binary_checksum" >"$state_file" && chmod 0600 "$state_file"; then rolling_state_ready=1 fi fi mv -f "$staged_binary" "$install_dir/flog" || fail "cannot replace $install_dir/flog" rm -f "$state_path" || true if [ "$rolling_state_ready" -eq 1 ]; then mv -f "$state_file" "$state_path" || true fi trap - EXIT trap - HUP INT TERM cleanup echo "installed flog $version to $install_dir/flog" >&2 configure_path