Fix: Pin runtime and agent artifacts
This commit is contained in:
+273
-82
@@ -107,72 +107,273 @@ install_list() {
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Node and pnpm, through nvm ----------------------------------------------
|
||||
#
|
||||
# nvm is a shell function rather than a binary, so it has to be sourced before
|
||||
# it can be used at all -- and its script reads variables that `set -u` above
|
||||
# treats as fatal, so the strictness is lifted for exactly that source and put
|
||||
# straight back.
|
||||
#
|
||||
# Deliberately not dnf's nodejs: config/bash/shell switches Node per project
|
||||
# from .nvmrc, and a system Node earlier on PATH would win every switch, leaving
|
||||
# `nvm use` looking like it did nothing.
|
||||
#
|
||||
# pnpm goes inside the nvm-managed Node rather than beside it as its own dnf
|
||||
# package, so it travels with the version it belongs to instead of outliving it.
|
||||
# --- Reviewed language runtimes and agent tools ------------------------------
|
||||
|
||||
_record_installer_failure() {
|
||||
local component="$1"
|
||||
log "$component install did not complete; continuing"
|
||||
softly_failed+=("$component")
|
||||
return 1
|
||||
}
|
||||
|
||||
_set_artifact_arch() {
|
||||
local machine_arch
|
||||
machine_arch="$(uname -m)" || return 1
|
||||
case "$machine_arch" in
|
||||
x86_64) artifact_arch=X86_64 ;;
|
||||
aarch64) artifact_arch=AARCH64 ;;
|
||||
*) log "Unsupported architecture: $machine_arch"; return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
_archive_member_is_safe() {
|
||||
local member="$1" expected_top="$2"
|
||||
[[ -n "$member" && "$member" != /* && "$member" != *'//'*
|
||||
&& ! "$member" =~ (^|/)\.\.?(/|$)
|
||||
&& ( "$member" == "$expected_top" || "$member" == "$expected_top/" \
|
||||
|| "$member" == "$expected_top/"* ) ]]
|
||||
}
|
||||
|
||||
_tree_links_stay_inside() {
|
||||
local root="$1" link resolved
|
||||
while IFS= read -r -d '' link; do
|
||||
resolved="$(realpath -m -- "$link")" || return 1
|
||||
[[ "$resolved" == "$root" || "$resolved" == "$root/"* ]] || return 1
|
||||
done < <(find "$root" -type l -print0)
|
||||
}
|
||||
|
||||
_atomic_symlink() {
|
||||
local target="$1" destination="$2" directory temporary
|
||||
directory="$(dirname -- "$destination")"
|
||||
mkdir -p -- "$directory" || return 1
|
||||
temporary="$(mktemp "$directory/.$(basename -- "$destination").link.XXXXXX")" || return 1
|
||||
rm -f -- "$temporary" || return 1
|
||||
ln -s -- "$target" "$temporary" || return 1
|
||||
if ! mv -Tf -- "$temporary" "$destination"; then
|
||||
rm -f -- "$temporary"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
_load_nvm() {
|
||||
local nvm_script="$PANAMA_SYSTEM_ETC/profile.d/nvm.sh"
|
||||
[[ -s "$nvm_script" ]] || return 1
|
||||
set +u
|
||||
# shellcheck source=/dev/null
|
||||
source "$nvm_script"
|
||||
set -u
|
||||
declare -F nvm >/dev/null
|
||||
}
|
||||
|
||||
_install_node() {
|
||||
local artifact_arch machine_arch archive_top parent target stage archive extract listing member
|
||||
_set_artifact_arch || return 1
|
||||
_load_nvm || return 1
|
||||
case "$artifact_arch" in
|
||||
X86_64) machine_arch=x64 ;;
|
||||
AARCH64) machine_arch=arm64 ;;
|
||||
esac
|
||||
archive_top="node-v${INSTALLER_PROVENANCE[NODE_VERSION]}-linux-$machine_arch"
|
||||
parent="${NVM_DIR:-$HOME/.nvm}/versions/node"
|
||||
target="$parent/v${INSTALLER_PROVENANCE[NODE_VERSION]}"
|
||||
if [[ -e "$target" || -L "$target" ]]; then
|
||||
[[ -d "$target" && ! -L "$target" && -x "$target/bin/node"
|
||||
&& "$($target/bin/node --version 2>/dev/null)" == "v${INSTALLER_PROVENANCE[NODE_VERSION]}" ]] \
|
||||
|| return 1
|
||||
nvm alias default "${INSTALLER_PROVENANCE[NODE_VERSION]}" >/dev/null 2>&1 || return 1
|
||||
return 0
|
||||
fi
|
||||
mkdir -p -- "$parent" || return 1
|
||||
stage="$(mktemp -d "$parent/.v${INSTALLER_PROVENANCE[NODE_VERSION]}.stage.XXXXXX")" \
|
||||
|| return 1
|
||||
chmod 0700 "$stage"
|
||||
archive="$stage/artifact"
|
||||
extract="$stage/extract"
|
||||
mkdir -m 0700 "$extract" || { rm -rf -- "$stage"; return 1; }
|
||||
if ! download_sha256 "${INSTALLER_PROVENANCE[NODE_${artifact_arch}_URL]}" \
|
||||
"${INSTALLER_PROVENANCE[NODE_${artifact_arch}_SHA256]}" \
|
||||
"${INSTALLER_PROVENANCE[NODE_${artifact_arch}_MAX_BYTES]}" "$archive"; then
|
||||
rm -rf -- "$stage"
|
||||
return 1
|
||||
fi
|
||||
listing="$(tar -tJf "$archive")" || { rm -rf -- "$stage"; return 1; }
|
||||
[[ -n "$listing" ]] || { rm -rf -- "$stage"; return 1; }
|
||||
while IFS= read -r member; do
|
||||
_archive_member_is_safe "$member" "$archive_top" \
|
||||
|| { rm -rf -- "$stage"; return 1; }
|
||||
done <<<"$listing"
|
||||
tar -xJf "$archive" --no-same-owner --no-same-permissions -C "$extract" \
|
||||
|| { rm -rf -- "$stage"; return 1; }
|
||||
[[ -d "$extract/$archive_top" && ! -L "$extract/$archive_top"
|
||||
&& -x "$extract/$archive_top/bin/node"
|
||||
&& "$($extract/$archive_top/bin/node --version 2>/dev/null)" \
|
||||
== "v${INSTALLER_PROVENANCE[NODE_VERSION]}" ]] \
|
||||
|| { rm -rf -- "$stage"; return 1; }
|
||||
_tree_links_stay_inside "$extract/$archive_top" \
|
||||
|| { rm -rf -- "$stage"; return 1; }
|
||||
mv -- "$extract/$archive_top" "$target" || { rm -rf -- "$stage"; return 1; }
|
||||
rm -rf -- "$stage"
|
||||
nvm alias default "${INSTALLER_PROVENANCE[NODE_VERSION]}" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
install_node() {
|
||||
_install_node || _record_installer_failure Node
|
||||
}
|
||||
|
||||
# Kept as the call-site name used by the desktop-first ordering contract.
|
||||
setup_node() {
|
||||
if [[ -s /etc/profile.d/nvm.sh ]]; then
|
||||
log "Installing the latest Node LTS through nvm"
|
||||
set +u
|
||||
# shellcheck source=/dev/null
|
||||
source /etc/profile.d/nvm.sh
|
||||
if nvm install --lts >/dev/null 2>&1; then
|
||||
nvm alias default 'lts/*' >/dev/null 2>&1 || true
|
||||
npm install -g pnpm >/dev/null 2>&1 || { log "pnpm did not install"; softly_failed+=("pnpm"); }
|
||||
log "Node $(node --version 2>/dev/null) with pnpm $(pnpm --version 2>/dev/null)"
|
||||
else
|
||||
log "nvm could not install Node; skipping"; softly_failed+=("Node (nvm)")
|
||||
fi
|
||||
set -u
|
||||
else
|
||||
log "nvm is not installed, so Node was not set up"
|
||||
fi
|
||||
install_node
|
||||
}
|
||||
|
||||
# --- Applications no repository packages -------------------------------------
|
||||
#
|
||||
# Everything else Panama installs comes from dnf or Flathub. These do not
|
||||
# exist in either, so each is an explicit exception with a reason, and each is
|
||||
# skipped when already present so a re-run costs nothing.
|
||||
#
|
||||
# None of them pins a version. sunhat pinned URLs -- upscayl 2.11.5, LACT 0.5.4,
|
||||
# a fedora-40 RPM -- and every one of them was a 404 within a release cycle. An
|
||||
# installer that resolves "latest" keeps working; one that names a version rots.
|
||||
#
|
||||
# A failure here is logged and stepped over rather than aborting: an
|
||||
# unreachable third-party host should not cost the rest of the run.
|
||||
install_pnpm() {
|
||||
if sudo dnf install -y pnpm >/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
_record_installer_failure pnpm
|
||||
}
|
||||
|
||||
_install_bun() {
|
||||
local artifact_arch archive_top target bin_link parent stage archive listing
|
||||
local staged_binary version_dir
|
||||
_set_artifact_arch || return 1
|
||||
case "$artifact_arch" in
|
||||
X86_64) archive_top=bun-linux-x64 ;;
|
||||
AARCH64) archive_top=bun-linux-aarch64 ;;
|
||||
esac
|
||||
version_dir="$HOME/.bun/versions/${INSTALLER_PROVENANCE[BUN_VERSION]}"
|
||||
target="$version_dir/bin/bun"
|
||||
bin_link="$HOME/.bun/bin/bun"
|
||||
if [[ -e "$version_dir" || -L "$version_dir" ]]; then
|
||||
[[ -d "$version_dir" && ! -L "$version_dir" && -x "$target"
|
||||
&& "$($target --version 2>/dev/null)" == "${INSTALLER_PROVENANCE[BUN_VERSION]}" ]] \
|
||||
|| return 1
|
||||
[[ -L "$bin_link" && "$(readlink -- "$bin_link")" == "$target" ]] \
|
||||
|| _atomic_symlink "$target" "$bin_link"
|
||||
return
|
||||
fi
|
||||
parent="$HOME/.bun/versions"
|
||||
mkdir -p -- "$parent" || return 1
|
||||
stage="$(mktemp -d "$parent/.${INSTALLER_PROVENANCE[BUN_VERSION]}.stage.XXXXXX")" \
|
||||
|| return 1
|
||||
chmod 0700 "$stage"
|
||||
archive="$stage/artifact"
|
||||
if ! download_sha256 "${INSTALLER_PROVENANCE[BUN_${artifact_arch}_URL]}" \
|
||||
"${INSTALLER_PROVENANCE[BUN_${artifact_arch}_SHA256]}" \
|
||||
"${INSTALLER_PROVENANCE[BUN_${artifact_arch}_MAX_BYTES]}" "$archive"; then
|
||||
rm -rf -- "$stage"
|
||||
return 1
|
||||
fi
|
||||
listing="$(unzip -Z1 "$archive")" || { rm -rf -- "$stage"; return 1; }
|
||||
[[ "$listing" == "$archive_top/bun" ]] || { rm -rf -- "$stage"; return 1; }
|
||||
_archive_member_is_safe "$listing" "$archive_top" \
|
||||
|| { rm -rf -- "$stage"; return 1; }
|
||||
mkdir -m 0700 "$stage/extract" "$stage/version" "$stage/version/bin" \
|
||||
|| { rm -rf -- "$stage"; return 1; }
|
||||
unzip -q "$archive" -d "$stage/extract" || { rm -rf -- "$stage"; return 1; }
|
||||
staged_binary="$stage/extract/$archive_top/bun"
|
||||
[[ -f "$staged_binary" && ! -L "$staged_binary" && -x "$staged_binary"
|
||||
&& "$($staged_binary --version 2>/dev/null)" == "${INSTALLER_PROVENANCE[BUN_VERSION]}" ]] \
|
||||
|| { rm -rf -- "$stage"; return 1; }
|
||||
mv -- "$staged_binary" "$stage/version/bin/bun" \
|
||||
|| { rm -rf -- "$stage"; return 1; }
|
||||
mv -- "$stage/version" "$version_dir" || { rm -rf -- "$stage"; return 1; }
|
||||
rm -rf -- "$stage"
|
||||
_atomic_symlink "$target" "$bin_link"
|
||||
}
|
||||
|
||||
# Bun: the JavaScript runtime and package manager. No RPM, no flatpak.
|
||||
install_bun() {
|
||||
if [[ -x "$HOME/.bun/bin/bun" ]]; then
|
||||
log "Bun already installed at \"$HOME/.bun/bin/bun\""
|
||||
else
|
||||
log "Installing Bun via curl..."
|
||||
curl -fsSL https://bun.sh/install | bash > /dev/null 2>&1 || { log "Bun install failed; skipping"; softly_failed+=("Bun"); }
|
||||
fi
|
||||
_install_bun || _record_installer_failure Bun
|
||||
}
|
||||
|
||||
# Codex: OpenAI's CLI. Distributed through npm, which is why this runs after
|
||||
# setup_node -- the nvm-managed Node is the one it should land in.
|
||||
install_codex() {
|
||||
if command -v codex >/dev/null 2>&1; then
|
||||
log "Codex already installed at \"$(command -v codex)\""
|
||||
elif command -v npm >/dev/null 2>&1; then
|
||||
log "Installing Codex via npm..."
|
||||
npm install -g @openai/codex >/dev/null 2>&1 || { log "Codex install failed; skipping"; softly_failed+=("Codex"); }
|
||||
else
|
||||
log "npm is not available, so Codex was not installed"; softly_failed+=("Codex")
|
||||
_codex_version_matches() {
|
||||
local binary="$1" output version_pattern
|
||||
output="$($binary --version 2>/dev/null)" || return 1
|
||||
version_pattern="${INSTALLER_PROVENANCE[CODEX_VERSION]//./\\.}"
|
||||
[[ "$output" =~ (^|[^0-9])${version_pattern}([^0-9]|$) ]]
|
||||
}
|
||||
|
||||
_install_codex() {
|
||||
local artifact_arch machine_arch archive_name version_dir target bin_link parent
|
||||
local stage archive listing staged_binary
|
||||
_set_artifact_arch || return 1
|
||||
case "$artifact_arch" in
|
||||
X86_64) machine_arch=x86_64 ;;
|
||||
AARCH64) machine_arch=aarch64 ;;
|
||||
esac
|
||||
archive_name="codex-$machine_arch-unknown-linux-musl"
|
||||
version_dir="$HOME/.local/lib/panama/codex/${INSTALLER_PROVENANCE[CODEX_VERSION]}"
|
||||
target="$version_dir/codex"
|
||||
bin_link="$HOME/.local/bin/codex"
|
||||
if [[ -e "$version_dir" || -L "$version_dir" ]]; then
|
||||
[[ -d "$version_dir" && ! -L "$version_dir" && -x "$target" ]] || return 1
|
||||
_codex_version_matches "$target" || return 1
|
||||
[[ -L "$bin_link" && "$(readlink -- "$bin_link")" == "$target" ]] \
|
||||
|| _atomic_symlink "$target" "$bin_link"
|
||||
return
|
||||
fi
|
||||
parent="$HOME/.local/lib/panama/codex"
|
||||
mkdir -p -- "$parent" || return 1
|
||||
stage="$(mktemp -d "$parent/.${INSTALLER_PROVENANCE[CODEX_VERSION]}.stage.XXXXXX")" \
|
||||
|| return 1
|
||||
chmod 0700 "$stage"
|
||||
archive="$stage/artifact"
|
||||
if ! download_sha256 "${INSTALLER_PROVENANCE[CODEX_${artifact_arch}_URL]}" \
|
||||
"${INSTALLER_PROVENANCE[CODEX_${artifact_arch}_SHA256]}" \
|
||||
"${INSTALLER_PROVENANCE[CODEX_${artifact_arch}_MAX_BYTES]}" "$archive"; then
|
||||
rm -rf -- "$stage"
|
||||
return 1
|
||||
fi
|
||||
listing="$(tar -tzf "$archive")" || { rm -rf -- "$stage"; return 1; }
|
||||
[[ "$listing" == "$archive_name" ]] || { rm -rf -- "$stage"; return 1; }
|
||||
[[ "$listing" != /* && ! "$listing" =~ (^|/)\.\.?(/|$) ]] \
|
||||
|| { rm -rf -- "$stage"; return 1; }
|
||||
mkdir -m 0700 "$stage/extract" "$stage/version" \
|
||||
|| { rm -rf -- "$stage"; return 1; }
|
||||
tar -xzf "$archive" --no-same-owner --no-same-permissions -C "$stage/extract" \
|
||||
|| { rm -rf -- "$stage"; return 1; }
|
||||
staged_binary="$stage/extract/$archive_name"
|
||||
[[ -f "$staged_binary" && ! -L "$staged_binary" && -x "$staged_binary" ]] \
|
||||
|| { rm -rf -- "$stage"; return 1; }
|
||||
_codex_version_matches "$staged_binary" \
|
||||
|| { rm -rf -- "$stage"; return 1; }
|
||||
mv -- "$staged_binary" "$stage/version/codex" \
|
||||
|| { rm -rf -- "$stage"; return 1; }
|
||||
mv -- "$stage/version" "$version_dir" || { rm -rf -- "$stage"; return 1; }
|
||||
rm -rf -- "$stage"
|
||||
_atomic_symlink "$target" "$bin_link"
|
||||
}
|
||||
|
||||
install_codex() {
|
||||
_install_codex || _record_installer_failure Codex
|
||||
}
|
||||
|
||||
_install_rustdesk() {
|
||||
local artifact_arch installed_version="" work rpm_path status=0
|
||||
_set_artifact_arch || return 1
|
||||
if [[ "$artifact_arch" == AARCH64 ]]; then
|
||||
log "RustDesk ${INSTALLER_PROVENANCE[RUSTDESK_VERSION]} has no reviewed aarch64 RPM"
|
||||
return 1
|
||||
fi
|
||||
installed_version="$(rpm -q --queryformat '%{VERSION}' rustdesk 2>/dev/null)" || true
|
||||
if [[ "$installed_version" == "${INSTALLER_PROVENANCE[RUSTDESK_VERSION]}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
work="$(mktemp -d)" || return 1
|
||||
chmod 0700 "$work"
|
||||
rpm_path="$work/rustdesk.rpm"
|
||||
if ! download_sha256 "${INSTALLER_PROVENANCE[RUSTDESK_X86_64_URL]}" \
|
||||
"${INSTALLER_PROVENANCE[RUSTDESK_X86_64_SHA256]}" \
|
||||
"${INSTALLER_PROVENANCE[RUSTDESK_X86_64_MAX_BYTES]}" "$rpm_path"; then
|
||||
rm -rf -- "$work"
|
||||
return 1
|
||||
fi
|
||||
sudo dnf install -y --setopt=localpkg_gpgcheck=1 "$rpm_path" >/dev/null || status=$?
|
||||
rm -rf -- "$work"
|
||||
return "$status"
|
||||
}
|
||||
|
||||
install_rustdesk() {
|
||||
_install_rustdesk || _record_installer_failure RustDesk
|
||||
}
|
||||
|
||||
# --- What was stepped over ---------------------------------------------------
|
||||
@@ -713,7 +914,7 @@ ensure_flathub_remote() {
|
||||
return "$status"
|
||||
}
|
||||
|
||||
install_claude_code() {
|
||||
_install_claude_code() {
|
||||
local work staged_key staged_repo status
|
||||
if command -v claude >/dev/null 2>&1; then
|
||||
log "Claude Code already installed at \"$(command -v claude)\""
|
||||
@@ -750,6 +951,10 @@ install_claude_code() {
|
||||
return "$status"
|
||||
}
|
||||
|
||||
install_claude_code() {
|
||||
_install_claude_code || _record_installer_failure "Claude Code"
|
||||
}
|
||||
|
||||
_claude_desktop_manual() {
|
||||
log "Claude Desktop is optional; configure its reviewed local-key repository manually to install it"
|
||||
}
|
||||
@@ -811,10 +1016,13 @@ if [[ "$ROLE" == server ]]; then
|
||||
sudo dnf update -y --refresh > /dev/null
|
||||
install_list core-packages "Core"
|
||||
install_list server-packages "Server"
|
||||
set +e
|
||||
setup_node
|
||||
install_pnpm
|
||||
install_bun
|
||||
install_claude_code
|
||||
install_codex
|
||||
set -e
|
||||
report_soft_failures
|
||||
exit 0
|
||||
fi
|
||||
@@ -931,10 +1139,13 @@ else
|
||||
log "Package list was not in specified path: $DEV_FILE"
|
||||
fi
|
||||
|
||||
set +e
|
||||
setup_node
|
||||
install_pnpm
|
||||
install_bun
|
||||
install_claude_code
|
||||
install_codex
|
||||
set -e
|
||||
|
||||
# Claude Desktop remains optional. Panama never downloads its community setup
|
||||
# script; only a repository an operator has already configured with the exact
|
||||
@@ -944,29 +1155,9 @@ if ! install_claude_desktop_if_trusted; then
|
||||
softly_failed+=("Claude Desktop")
|
||||
fi
|
||||
|
||||
# RustDesk: remote desktop. The flatpak cannot register the root-owned system
|
||||
# service that unattended access needs -- see panama-doctor's rustdesk check --
|
||||
# so this takes the RPM. The download URL is resolved from the latest release
|
||||
# rather than written down, so it does not go stale.
|
||||
if rpm -q rustdesk >/dev/null 2>&1; then
|
||||
log "RustDesk already installed"
|
||||
else
|
||||
log "Resolving the latest RustDesk release..."
|
||||
# `|| true` because a failed curl -- unauthenticated GitHub API calls get
|
||||
# rate-limited -- would otherwise trip set -e and kill the stage before the
|
||||
# empty-result fallback below could do its job.
|
||||
rustdesk_url="$(curl -fsSL https://api.github.com/repos/rustdesk/rustdesk/releases/latest 2>/dev/null \
|
||||
| jq -r --arg arch "$(uname -m)" '.assets[].browser_download_url | select(test($arch + "\\.rpm$")) | select(test("suse") | not)' \
|
||||
| head -1 || true)"
|
||||
if [[ -n "$rustdesk_url" ]]; then
|
||||
log "Installing RustDesk from $rustdesk_url"
|
||||
# The RPM ships rustdesk.service already enabled, which is what provides
|
||||
# unattended access; Panama deliberately does not start it a second time.
|
||||
sudo dnf install -y "$rustdesk_url" > /dev/null || { log "RustDesk install failed; skipping"; softly_failed+=("RustDesk"); }
|
||||
else
|
||||
log "Could not resolve a RustDesk release; skipping"; softly_failed+=("RustDesk")
|
||||
fi
|
||||
fi
|
||||
# The RPM ships rustdesk.service already enabled, which is what provides
|
||||
# unattended access; Panama deliberately does not start it a second time.
|
||||
install_rustdesk || true
|
||||
|
||||
# --- Install Flatpak Packages ---
|
||||
FLATPAK_FILE="$PANAMA_PATH/setup/packages/flatpak-packages"
|
||||
|
||||
Reference in New Issue
Block a user