chore: import upstream snapshot with attribution
Deploy Worker / deploy (push) Failing after 1s
Shellcheck / Check shell scripts (push) Failing after 1s
CI / Build Packages (amd64 - appimage) (push) Has been skipped
CI / Build Packages (amd64 - deb) (push) Has been skipped
CI / Build Packages (amd64 - rpm) (push) Has been skipped
CI / Build Packages (arm64 - appimage) (push) Has been skipped
CI / Build Packages (arm64 - deb) (push) Has been skipped
CI / Test Flags Parsing (push) Failing after 1s
BATS Tests / BATS unit tests (push) Failing after 1s
CI / Build Packages (arm64 - rpm) (push) Has been skipped
CI / Test Build Artifacts (amd64) (push) Has been skipped
CI / Test Build Artifacts (arm64) (push) Has been skipped
CI / Update APT Repository (push) Has been cancelled
CI / Mirror Official .deb to Release (push) Has been cancelled
CI / Update DNF Repository (push) Has been cancelled
CI / Update AUR Package (push) Has been cancelled
CI / Create Release (push) Has been cancelled
Deploy Worker / deploy (push) Failing after 1s
Shellcheck / Check shell scripts (push) Failing after 1s
CI / Build Packages (amd64 - appimage) (push) Has been skipped
CI / Build Packages (amd64 - deb) (push) Has been skipped
CI / Build Packages (amd64 - rpm) (push) Has been skipped
CI / Build Packages (arm64 - appimage) (push) Has been skipped
CI / Build Packages (arm64 - deb) (push) Has been skipped
CI / Test Flags Parsing (push) Failing after 1s
BATS Tests / BATS unit tests (push) Failing after 1s
CI / Build Packages (arm64 - rpm) (push) Has been skipped
CI / Test Build Artifacts (amd64) (push) Has been skipped
CI / Test Build Artifacts (arm64) (push) Has been skipped
CI / Update APT Repository (push) Has been cancelled
CI / Mirror Official .deb to Release (push) Has been cancelled
CI / Update DNF Repository (push) Has been cancelled
CI / Update AUR Package (push) Has been cancelled
CI / Create Release (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
#===============================================================================
|
||||
# Dependency installation and work-directory/Node/asar bootstrap.
|
||||
#
|
||||
# Sourced by: build.sh
|
||||
# Sourced globals:
|
||||
# build_format, distro_family, work_dir, project_root
|
||||
# Modifies globals:
|
||||
# asar_exec (via setup_asar); PATH is exported (via setup_nodejs)
|
||||
#===============================================================================
|
||||
|
||||
check_dependencies() {
|
||||
echo 'Checking dependencies...'
|
||||
local deps_to_install=''
|
||||
# ar (binutils) plus tar with xz/zstd support unpack the official
|
||||
# .deb without dpkg, so rpm-family and Arch hosts can build too.
|
||||
local all_deps='wget ar tar xz zstd'
|
||||
|
||||
# Add format-specific dependencies
|
||||
case "$build_format" in
|
||||
deb) all_deps="$all_deps dpkg-deb" ;;
|
||||
rpm) all_deps="$all_deps rpmbuild" ;;
|
||||
esac
|
||||
|
||||
# Command-to-package mappings per distro family
|
||||
declare -A debian_pkgs=(
|
||||
[wget]='wget' [ar]='binutils' [tar]='tar'
|
||||
[xz]='xz-utils' [zstd]='zstd'
|
||||
[dpkg-deb]='dpkg-dev' [rpmbuild]='rpm'
|
||||
)
|
||||
declare -A rpm_pkgs=(
|
||||
[wget]='wget' [ar]='binutils' [tar]='tar'
|
||||
[xz]='xz' [zstd]='zstd'
|
||||
[dpkg-deb]='dpkg' [rpmbuild]='rpm-build'
|
||||
)
|
||||
|
||||
local cmd pkg
|
||||
for cmd in $all_deps; do
|
||||
if ! check_command "$cmd"; then
|
||||
case "$distro_family" in
|
||||
debian) pkg="${debian_pkgs[$cmd]}" ;;
|
||||
rpm) pkg="${rpm_pkgs[$cmd]}" ;;
|
||||
*)
|
||||
echo "Warning: Cannot auto-install '$cmd' on unknown distro. Please install manually." >&2
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
# Several commands can map to the same package. Skip if the
|
||||
# package is already queued so the log line stays readable.
|
||||
case " $deps_to_install " in
|
||||
*" $pkg "*) ;;
|
||||
*) deps_to_install="$deps_to_install $pkg" ;;
|
||||
esac
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -n $deps_to_install ]]; then
|
||||
echo "System dependencies needed:$deps_to_install"
|
||||
|
||||
# Determine if we need sudo (skip if already root)
|
||||
local sudo_cmd='sudo'
|
||||
if (( EUID == 0 )); then
|
||||
sudo_cmd=''
|
||||
echo 'Installing as root (no sudo needed)...'
|
||||
else
|
||||
echo 'Attempting to install using sudo...'
|
||||
# Check if we can sudo without a password first
|
||||
if sudo -n true 2>/dev/null; then
|
||||
echo 'Passwordless sudo detected.'
|
||||
elif ! sudo -v; then
|
||||
echo 'Failed to validate sudo credentials. Please ensure you can run sudo.' >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
case "$distro_family" in
|
||||
debian)
|
||||
if ! $sudo_cmd apt update; then
|
||||
echo "Failed to run 'apt update'." >&2
|
||||
exit 1
|
||||
fi
|
||||
# shellcheck disable=SC2086
|
||||
if ! $sudo_cmd apt install -y $deps_to_install; then
|
||||
echo "Failed to install dependencies using 'apt install'." >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
rpm)
|
||||
# shellcheck disable=SC2086
|
||||
if ! $sudo_cmd dnf install -y $deps_to_install; then
|
||||
echo "Failed to install dependencies using 'dnf install'." >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Cannot auto-install dependencies on unknown distro." >&2
|
||||
echo "Please install these packages manually: $deps_to_install" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
echo 'System dependencies installed successfully.'
|
||||
fi
|
||||
}
|
||||
|
||||
setup_work_directory() {
|
||||
rm -rf "$work_dir"
|
||||
mkdir -p "$work_dir" || exit 1
|
||||
}
|
||||
|
||||
setup_nodejs() {
|
||||
section_header 'Node.js Setup'
|
||||
echo 'Checking Node.js version...'
|
||||
|
||||
local node_version_ok=false
|
||||
if command -v node &> /dev/null; then
|
||||
local node_version node_major
|
||||
node_version=$(node --version | cut -d'v' -f2)
|
||||
node_major="${node_version%%.*}"
|
||||
echo "System Node.js version: v$node_version"
|
||||
|
||||
if (( node_major >= 20 )); then
|
||||
echo "System Node.js version is adequate (v$node_version)"
|
||||
node_version_ok=true
|
||||
else
|
||||
echo "System Node.js version is too old (v$node_version). Need v20+"
|
||||
fi
|
||||
else
|
||||
echo 'Node.js not found in system'
|
||||
fi
|
||||
|
||||
if [[ $node_version_ok == true ]]; then
|
||||
section_footer 'Node.js Setup'
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Node.js version inadequate - install locally
|
||||
echo 'Installing Node.js v20 locally in build directory...'
|
||||
|
||||
# Node is build-host tooling: it runs asar here and never ships in
|
||||
# the package, so it is keyed to uname -m, NOT to $architecture —
|
||||
# which --arch can override to the cross-build target (an arm64
|
||||
# node on an amd64 runner dies with an exec-format error).
|
||||
local node_arch host_arch
|
||||
host_arch=$(uname -m)
|
||||
case "$host_arch" in
|
||||
x86_64) node_arch='x64' ;;
|
||||
aarch64) node_arch='arm64' ;;
|
||||
*)
|
||||
echo "Unsupported host architecture for Node.js: $host_arch" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
local node_version_to_install='20.18.1'
|
||||
local node_tarball="node-v${node_version_to_install}-linux-${node_arch}.tar.xz"
|
||||
local node_url="https://nodejs.org/dist/v${node_version_to_install}/${node_tarball}"
|
||||
local node_install_dir="$work_dir/node"
|
||||
|
||||
echo "Downloading Node.js v${node_version_to_install} for ${node_arch}..."
|
||||
cd "$work_dir" || exit 1
|
||||
if ! wget -O "$node_tarball" "$node_url"; then
|
||||
echo "Failed to download Node.js from $node_url" >&2
|
||||
cd "$project_root" || exit 1
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify against official Node.js checksums
|
||||
local shasums_url node_expected_sha256
|
||||
shasums_url="https://nodejs.org/dist/v${node_version_to_install}/SHASUMS256.txt"
|
||||
node_expected_sha256=$(
|
||||
wget -qO- "$shasums_url" \
|
||||
| grep -F "$node_tarball" \
|
||||
| awk '{print $1}'
|
||||
) || true
|
||||
|
||||
if ! verify_sha256 "$work_dir/$node_tarball" \
|
||||
"$node_expected_sha256" 'Node.js tarball'; then
|
||||
cd "$project_root" || exit 1
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo 'Extracting Node.js...'
|
||||
if ! tar -xf "$node_tarball"; then
|
||||
echo 'Failed to extract Node.js tarball' >&2
|
||||
cd "$project_root" || exit 1
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mv "node-v${node_version_to_install}-linux-${node_arch}" "$node_install_dir" || exit 1
|
||||
export PATH="$node_install_dir/bin:$PATH"
|
||||
|
||||
if command -v node &> /dev/null; then
|
||||
echo "Local Node.js installed successfully: $(node --version)"
|
||||
else
|
||||
echo 'Failed to install local Node.js' >&2
|
||||
cd "$project_root" || exit 1
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -f "$node_tarball"
|
||||
cd "$project_root" || exit 1
|
||||
section_footer 'Node.js Setup'
|
||||
}
|
||||
|
||||
setup_asar() {
|
||||
section_header 'Asar Tooling'
|
||||
|
||||
# @electron/asar is only needed while at least one asar patch is
|
||||
# active (see active_patches in scripts/patches/app-asar.sh);
|
||||
# patch_app_asar also uses it to read package.json fields without a
|
||||
# full extract. No Electron install: the official tree ships its own
|
||||
# runtime, so the old pinned-Electron staging is gone.
|
||||
echo "Ensuring local asar installation in $work_dir..."
|
||||
cd "$work_dir" || exit 1
|
||||
|
||||
if [[ ! -f package.json ]]; then
|
||||
echo "Creating temporary package.json in $work_dir for local install..."
|
||||
echo '{"name":"claude-desktop-build","version":"0.0.1","private":true}' > package.json
|
||||
fi
|
||||
|
||||
local asar_bin_path="$work_dir/node_modules/.bin/asar"
|
||||
|
||||
if [[ ! -f $asar_bin_path ]]; then
|
||||
echo "Installing @electron/asar locally into $work_dir..."
|
||||
if ! npm install --no-save @electron/asar; then
|
||||
echo 'Failed to install @electron/asar locally.' >&2
|
||||
cd "$project_root" || exit 1
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo 'Local asar binary already present.'
|
||||
fi
|
||||
|
||||
if [[ -f $asar_bin_path ]]; then
|
||||
asar_exec="$(realpath "$asar_bin_path")"
|
||||
echo "Using asar executable: $asar_exec"
|
||||
else
|
||||
echo "Failed to find asar binary at '$asar_bin_path' after installation attempt." >&2
|
||||
cd "$project_root" || exit 1
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$project_root" || exit 1
|
||||
section_footer 'Asar Tooling'
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
#===============================================================================
|
||||
# Host detection and argument parsing: architecture, distro, requirements,
|
||||
# CLI flag processing.
|
||||
#
|
||||
# Sourced by: build.sh
|
||||
# Sourced globals: (none read on entry)
|
||||
# Modifies globals:
|
||||
# architecture, distro_family, original_user, original_home, project_root,
|
||||
# work_dir, build_format, cleanup_action, perform_cleanup, test_flags_mode,
|
||||
# local_deb_path, release_tag, source_dir
|
||||
#===============================================================================
|
||||
|
||||
detect_architecture() {
|
||||
section_header 'Architecture Detection'
|
||||
echo 'Detecting system architecture...'
|
||||
|
||||
local raw_arch
|
||||
raw_arch=$(uname -m) || {
|
||||
echo 'Failed to detect architecture' >&2
|
||||
exit 1
|
||||
}
|
||||
echo "Detected machine architecture: $raw_arch"
|
||||
|
||||
# Download URLs and SHA256 pins for the official .deb live in
|
||||
# scripts/setup/official-deb.sh; only the arch mapping happens here.
|
||||
case "$raw_arch" in
|
||||
x86_64)
|
||||
architecture='amd64'
|
||||
echo 'Configured for amd64 (x86_64) build.'
|
||||
;;
|
||||
aarch64)
|
||||
architecture='arm64'
|
||||
echo 'Configured for arm64 (aarch64) build.'
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported architecture: $raw_arch. This script supports x86_64 (amd64) and aarch64 (arm64)." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Target Architecture: $architecture"
|
||||
section_footer 'Architecture Detection'
|
||||
}
|
||||
|
||||
detect_distro() {
|
||||
section_header 'Distribution Detection'
|
||||
echo 'Detecting Linux distribution family...'
|
||||
|
||||
if [[ -f /etc/debian_version ]]; then
|
||||
distro_family='debian'
|
||||
echo "Detected Debian-based distribution"
|
||||
echo " Debian version: $(cat /etc/debian_version)"
|
||||
elif [[ -f /etc/fedora-release ]]; then
|
||||
distro_family='rpm'
|
||||
echo "Detected Fedora"
|
||||
echo " $(cat /etc/fedora-release)"
|
||||
elif [[ -f /etc/redhat-release ]]; then
|
||||
distro_family='rpm'
|
||||
echo "Detected Red Hat-based distribution"
|
||||
echo " $(cat /etc/redhat-release)"
|
||||
elif [[ -f /etc/NIXOS ]]; then
|
||||
distro_family='nix'
|
||||
echo "Detected NixOS"
|
||||
else
|
||||
distro_family='unknown'
|
||||
echo "Warning: Could not detect distribution family"
|
||||
echo " AppImage build will still work, but native packages (deb/rpm) may not"
|
||||
fi
|
||||
|
||||
echo "Distribution: $(grep 'PRETTY_NAME' /etc/os-release 2>/dev/null | cut -d'"' -f2 || echo 'Unknown')"
|
||||
echo "Distribution family: $distro_family"
|
||||
section_footer 'Distribution Detection'
|
||||
}
|
||||
|
||||
check_system_requirements() {
|
||||
# Allow running as root in CI/container environments
|
||||
if (( EUID == 0 )); then
|
||||
if [[ -n ${CI:-} || -n ${GITHUB_ACTIONS:-} || -f /.dockerenv ]]; then
|
||||
echo 'Running as root in CI/container environment (allowed)'
|
||||
else
|
||||
echo 'This script should not be run using sudo or as the root user.' >&2
|
||||
echo 'It will use sudo when needed for specific actions (may prompt for password).' >&2
|
||||
echo 'Please run as a normal user.' >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
original_user=$(whoami)
|
||||
original_home=$(getent passwd "$original_user" | cut -d: -f6)
|
||||
if [[ -z $original_home ]]; then
|
||||
echo "Could not determine home directory for user $original_user." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Running as user: $original_user (Home: $original_home)"
|
||||
|
||||
# Check for NVM and source it if found
|
||||
if [[ -d $original_home/.nvm ]]; then
|
||||
echo "Found NVM installation for user $original_user, checking for Node.js 20+..."
|
||||
export NVM_DIR="$original_home/.nvm"
|
||||
if [[ -s $NVM_DIR/nvm.sh ]]; then
|
||||
# shellcheck disable=SC1091
|
||||
\. "$NVM_DIR/nvm.sh"
|
||||
local node_bin_path=''
|
||||
node_bin_path=$(nvm which current | xargs dirname 2>/dev/null || \
|
||||
find "$NVM_DIR/versions/node" -maxdepth 2 -type d -name 'bin' | sort -V | tail -n 1)
|
||||
|
||||
if [[ -n $node_bin_path && -d $node_bin_path ]]; then
|
||||
echo "Adding NVM Node bin path to PATH: $node_bin_path"
|
||||
export PATH="$node_bin_path:$PATH"
|
||||
else
|
||||
echo 'Warning: Could not determine NVM Node bin path.'
|
||||
fi
|
||||
else
|
||||
echo 'Warning: nvm.sh script not found or not sourceable.'
|
||||
fi
|
||||
fi
|
||||
|
||||
echo 'System Information:'
|
||||
echo "Distribution: $(grep 'PRETTY_NAME' /etc/os-release 2>/dev/null | cut -d'"' -f2 || echo 'Unknown')"
|
||||
echo "Distribution family: $distro_family"
|
||||
# No architecture line here: this runs before parse_arguments(), so
|
||||
# $architecture is still the uname -m auto-detection, and printing
|
||||
# it here would read as authoritative right next to the distro
|
||||
# info. The auto-detected value is already logged in
|
||||
# detect_architecture() above; the value after a possible --arch
|
||||
# override is logged at the end of Argument Parsing below, which is
|
||||
# the only trustworthy target-arch line in this output.
|
||||
}
|
||||
|
||||
parse_arguments() {
|
||||
section_header 'Argument Parsing'
|
||||
|
||||
project_root="$(pwd)"
|
||||
work_dir="$project_root/build"
|
||||
# app_staging_dir is derived in build.sh after the official .deb is
|
||||
# extracted (it points into the extracted tree).
|
||||
|
||||
# Set default build format based on detected distro
|
||||
case "$distro_family" in
|
||||
debian) build_format='deb' ;;
|
||||
rpm) build_format='rpm' ;;
|
||||
nix) build_format='nix' ;;
|
||||
*) build_format='appimage' ;;
|
||||
esac
|
||||
|
||||
while (( $# > 0 )); do
|
||||
case "$1" in
|
||||
-a|--arch|-b|--build|-c|--clean|-d|--deb|-r|--release-tag|-s|--source-dir)
|
||||
if [[ -z ${2:-} || $2 == -* ]]; then
|
||||
echo "Error: Argument for $1 is missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
case "$1" in
|
||||
-a|--arch) architecture="$2" ;;
|
||||
-b|--build) build_format="$2" ;;
|
||||
-c|--clean) cleanup_action="$2" ;;
|
||||
-d|--deb) local_deb_path="$2" ;;
|
||||
-r|--release-tag) release_tag="$2" ;;
|
||||
-s|--source-dir) source_dir="$2" ;;
|
||||
esac
|
||||
shift 2
|
||||
;;
|
||||
--test-flags)
|
||||
test_flags_mode=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: $0 [--build deb|rpm|appimage|nix] [--clean yes|no] [--deb /path/to/claude-desktop.deb] [--source-dir /path] [--release-tag TAG] [--arch amd64|arm64] [--test-flags]"
|
||||
echo ' --build: Specify the build format (deb, rpm, appimage, or nix).'
|
||||
echo " Default: auto-detected based on distro (current: $build_format)"
|
||||
echo ' --clean: Specify whether to clean intermediate build files (yes or no). Default: yes'
|
||||
echo ' --deb: Use a local official Claude Desktop .deb instead of downloading'
|
||||
echo ' --source-dir: Path to repo root for scripts/ and assets (default: project root)'
|
||||
echo ' --release-tag: Release tag (e.g., v3.0.0+claude1.17377.2) to append wrapper version to package'
|
||||
echo ' --arch: Override detected target architecture (amd64 or arm64),'
|
||||
echo ' for cross-building (default: auto-detected via uname -m,'
|
||||
echo " current: $architecture)"
|
||||
echo ' --test-flags: Parse flags, print results, and exit without building.'
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
echo 'Use -h or --help for usage information.' >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# source_dir is where scripts/assets live (default: project_root)
|
||||
source_dir="${source_dir:-$project_root}"
|
||||
|
||||
# Validate arguments
|
||||
build_format="${build_format,,}"
|
||||
cleanup_action="${cleanup_action,,}"
|
||||
architecture="${architecture,,}"
|
||||
|
||||
if [[ ! -d $source_dir ]]; then
|
||||
echo "Error: --source-dir path does not exist: $source_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $build_format != 'deb' && $build_format != 'rpm' && $build_format != 'appimage' && $build_format != 'nix' ]]; then
|
||||
echo "Invalid build format specified: '$build_format'. Must be 'deb', 'rpm', 'appimage', or 'nix'." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --arch overrides the uname -m detection in detect_architecture(),
|
||||
# enabling cross-building (e.g. an amd64 CI runner producing an
|
||||
# arm64 package, since repackaging the official .deb is
|
||||
# arch-independent).
|
||||
if [[ $architecture != 'amd64' && $architecture != 'arm64' ]]; then
|
||||
echo "Invalid architecture specified: '$architecture'. Must be 'amd64' or 'arm64'." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Warn if building native package for wrong distro
|
||||
if [[ $build_format == 'deb' && $distro_family != 'debian' ]]; then
|
||||
echo "Warning: Building .deb package on non-Debian system ($distro_family). This may fail." >&2
|
||||
elif [[ $build_format == 'rpm' && $distro_family != 'rpm' ]]; then
|
||||
echo "Warning: Building .rpm package on non-RPM system ($distro_family). This may fail." >&2
|
||||
fi
|
||||
if [[ $cleanup_action != 'yes' && $cleanup_action != 'no' ]]; then
|
||||
echo "Invalid cleanup option specified: '$cleanup_action'. Must be 'yes' or 'no'." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Selected build format: $build_format"
|
||||
echo "Cleanup intermediate files: $cleanup_action"
|
||||
echo "Target Architecture: $architecture"
|
||||
|
||||
[[ $cleanup_action == 'yes' ]] && perform_cleanup=true
|
||||
|
||||
section_footer 'Argument Parsing'
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
#===============================================================================
|
||||
# Official Claude Desktop .deb acquisition: resolve, download, verify,
|
||||
# extract. Replaced the Windows-installer download path in the v3.0.0
|
||||
# official-Linux rebase.
|
||||
#
|
||||
# The official APT repository is plain HTTPS (no bot challenge), so both
|
||||
# resolution and download are curl/wget-able. Extraction deliberately
|
||||
# avoids dpkg-deb so rpm-family and Arch hosts can build: ar + tar handle
|
||||
# every Debian archive member.
|
||||
#
|
||||
# Sourced by: build.sh, tools/patch-necessity-audit.sh
|
||||
# Sourced globals:
|
||||
# work_dir, architecture, local_deb_path (optional), release_tag (optional)
|
||||
# Modifies globals:
|
||||
# claude_extract_dir, version, official_deb_url, official_deb_sha256,
|
||||
# official_deb_filename, official_deb_depends, official_deb_recommends,
|
||||
# resolved_official_version, resolved_official_filename,
|
||||
# resolved_official_sha256, resolved_official_size
|
||||
#===============================================================================
|
||||
|
||||
OFFICIAL_APT_BASE='https://downloads.claude.ai/claude-desktop/apt/stable'
|
||||
|
||||
# Pinned artifact per architecture, seeded from the Packages indexes on
|
||||
# 2026-07-04. Bumped by check-claude-version after the rebase lands.
|
||||
OFFICIAL_DEB_VERSION='1.19367.0'
|
||||
OFFICIAL_DEB_POOL_AMD64='pool/main/c/claude-desktop/claude-desktop_1.19367.0_amd64.deb'
|
||||
OFFICIAL_DEB_SHA256_AMD64='76f570730c1185924e2423c5f88a27bec17c1e7adb055ecd09401a0e93a9299b'
|
||||
OFFICIAL_DEB_POOL_ARM64='pool/main/c/claude-desktop/claude-desktop_1.19367.0_arm64.deb'
|
||||
OFFICIAL_DEB_SHA256_ARM64='98ab0e9e2cf3ecae073a38d81ecabd2b85c7e7fe319cf72df575642915045a83'
|
||||
|
||||
# Set official_deb_url/sha256/filename from the pinned block for the
|
||||
# current (or given) architecture.
|
||||
official_deb_pin() {
|
||||
local arch="${1:-$architecture}"
|
||||
local pool_path
|
||||
|
||||
case "$arch" in
|
||||
amd64)
|
||||
pool_path="$OFFICIAL_DEB_POOL_AMD64"
|
||||
official_deb_sha256="$OFFICIAL_DEB_SHA256_AMD64"
|
||||
;;
|
||||
arm64)
|
||||
pool_path="$OFFICIAL_DEB_POOL_ARM64"
|
||||
official_deb_sha256="$OFFICIAL_DEB_SHA256_ARM64"
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported architecture for official .deb: $arch" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
official_deb_url="$OFFICIAL_APT_BASE/$pool_path"
|
||||
official_deb_filename="${pool_path##*/}"
|
||||
}
|
||||
|
||||
# Query the official Packages index for the newest claude-desktop entry.
|
||||
# Used by CI (check-claude-version) and the doctor drift check, never by
|
||||
# the pinned build itself. Sets resolved_official_{version,filename,
|
||||
# sha256,size}.
|
||||
#
|
||||
# sort -V is sufficient for the upstream scheme (dotted numerics, no
|
||||
# epochs or tildes observed); revisit if upstream ever ships either.
|
||||
resolve_official_deb() {
|
||||
local arch="${1:-$architecture}"
|
||||
local index_url="$OFFICIAL_APT_BASE/dists/stable/main/binary-${arch}/Packages"
|
||||
local newest
|
||||
|
||||
newest=$(curl -fsS --max-time 30 "$index_url" | awk -v RS='' '
|
||||
/^Package: claude-desktop\n/ || $1 == "Package:" {
|
||||
v = f = s = z = ""
|
||||
n = split($0, lines, "\n")
|
||||
for (i = 1; i <= n; i++) {
|
||||
if (lines[i] ~ /^Version: /) v = substr(lines[i], 10)
|
||||
else if (lines[i] ~ /^Filename: /) f = substr(lines[i], 11)
|
||||
else if (lines[i] ~ /^SHA256: /) s = substr(lines[i], 9)
|
||||
else if (lines[i] ~ /^Size: /) z = substr(lines[i], 7)
|
||||
}
|
||||
if (v != "" && f != "" && s != "")
|
||||
printf "%s\t%s\t%s\t%s\n", v, f, s, z
|
||||
}' | sort -V -k1,1 | tail -1)
|
||||
|
||||
if [[ -z $newest ]]; then
|
||||
echo "Could not resolve claude-desktop from $index_url" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
IFS=$'\t' read -r resolved_official_version resolved_official_filename \
|
||||
resolved_official_sha256 resolved_official_size <<< "$newest"
|
||||
|
||||
echo "Newest official $arch package: $resolved_official_version" \
|
||||
"($resolved_official_filename)"
|
||||
}
|
||||
|
||||
# Extract one member family (data.tar.* or control.tar.*) of a Debian
|
||||
# archive into a directory, without dpkg. Handles zst/xz/gz/uncompressed.
|
||||
_extract_deb_member() {
|
||||
local deb_path="$1"
|
||||
local member_prefix="$2"
|
||||
local dest_dir="$3"
|
||||
local member tar_flag
|
||||
|
||||
member=$(ar t "$deb_path" | grep "^${member_prefix}\.tar" | head -1)
|
||||
if [[ -z $member ]]; then
|
||||
echo "No ${member_prefix}.tar member in $deb_path" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "$member" in
|
||||
*.tar.zst) tar_flag='--zstd' ;;
|
||||
*.tar.xz) tar_flag='-J' ;;
|
||||
*.tar.gz) tar_flag='-z' ;;
|
||||
*.tar) tar_flag='' ;;
|
||||
*)
|
||||
echo "Unsupported compression on $member" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
mkdir -p "$dest_dir" || return 1
|
||||
# tar_flag is intentionally unquoted: empty for plain .tar, one
|
||||
# decompression flag otherwise.
|
||||
# shellcheck disable=SC2086
|
||||
ar p "$deb_path" "$member" | tar $tar_flag -x -C "$dest_dir"
|
||||
}
|
||||
|
||||
# Read a single field from an extracted Debian control file. Multi-line
|
||||
# fields (continuation lines) are not needed for the fields we read.
|
||||
_control_field() {
|
||||
local control_path="$1"
|
||||
local field="$2"
|
||||
|
||||
LC_ALL=C grep -oP "^${field}: \K.*" "$control_path"
|
||||
}
|
||||
|
||||
# Download (or copy) the pinned official .deb, verify it, and extract it
|
||||
# into work_dir/claude-extract. The app tree lands at
|
||||
# claude-extract/usr/lib/claude-desktop; package metadata at
|
||||
# claude-extract/DEBIAN-meta/control.
|
||||
fetch_official_deb() {
|
||||
section_header 'Download the official Claude Desktop .deb'
|
||||
|
||||
official_deb_pin || exit 1
|
||||
local claude_deb_path="$work_dir/$official_deb_filename"
|
||||
|
||||
if [[ -n ${local_deb_path:-} ]]; then
|
||||
echo "Using local official .deb: $local_deb_path"
|
||||
if [[ ! -f $local_deb_path ]]; then
|
||||
echo "Local .deb file not found: $local_deb_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
cp "$local_deb_path" "$claude_deb_path" || exit 1
|
||||
echo 'Local .deb copied to build directory'
|
||||
else
|
||||
echo "Downloading official Claude Desktop $OFFICIAL_DEB_VERSION" \
|
||||
"for $architecture..."
|
||||
if ! wget -q --show-progress -O "$claude_deb_path" \
|
||||
"$official_deb_url"; then
|
||||
echo "Failed to download $official_deb_url" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Download complete: $official_deb_filename"
|
||||
|
||||
if ! verify_sha256 "$claude_deb_path" "$official_deb_sha256" \
|
||||
'official Claude Desktop .deb'; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo 'Extracting the official .deb...'
|
||||
claude_extract_dir="$work_dir/claude-extract"
|
||||
mkdir -p "$claude_extract_dir" || exit 1
|
||||
|
||||
_extract_deb_member "$claude_deb_path" data "$claude_extract_dir" || {
|
||||
echo 'Failed to extract data archive from .deb' >&2
|
||||
exit 1
|
||||
}
|
||||
_extract_deb_member "$claude_deb_path" control \
|
||||
"$claude_extract_dir/DEBIAN-meta" || {
|
||||
echo 'Failed to extract control archive from .deb' >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
local control_path="$claude_extract_dir/DEBIAN-meta/control"
|
||||
version=$(_control_field "$control_path" Version)
|
||||
if [[ -z $version ]]; then
|
||||
echo "Could not read Version from $control_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Detected Claude version: $version"
|
||||
|
||||
if [[ -z ${local_deb_path:-} && $version != "$OFFICIAL_DEB_VERSION" ]]; then
|
||||
echo "Warning: control Version ($version) differs from pinned" \
|
||||
"OFFICIAL_DEB_VERSION ($OFFICIAL_DEB_VERSION)" >&2
|
||||
fi
|
||||
|
||||
# The dependency contract differs per arch (e.g. qemu-system-x86 vs
|
||||
# qemu-system-arm); packaging re-emits it verbatim rather than
|
||||
# hardcoding a copy.
|
||||
official_deb_depends=$(_control_field "$control_path" Depends)
|
||||
official_deb_recommends=$(_control_field "$control_path" Recommends)
|
||||
|
||||
if [[ ! -d "$claude_extract_dir/usr/lib/claude-desktop" ]]; then
|
||||
echo 'Extracted tree missing usr/lib/claude-desktop —' \
|
||||
'upstream layout changed?' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract wrapper version from release tag if provided
|
||||
# (e.g., v3.0.0+claude1.17377.2 -> 3.0.0; v3.0.0-rc1+claude1.17377.2
|
||||
# -> 3.0.0-rc1). Deb versions may contain hyphens; rpm.sh sanitizes
|
||||
# the RC suffix (hyphens -> dots) for the RPM Release field.
|
||||
if [[ -n ${release_tag:-} ]]; then
|
||||
local wrapper_version
|
||||
wrapper_version=$(echo "$release_tag" | \
|
||||
LC_ALL=C grep -oP \
|
||||
'^v\K[0-9]+\.[0-9]+\.[0-9]+(?:-rc[0-9]+)?(?=\+claude)')
|
||||
if [[ -n $wrapper_version ]]; then
|
||||
version="${version}-${wrapper_version}"
|
||||
echo "Package version with wrapper suffix: $version"
|
||||
else
|
||||
echo 'Warning: Could not extract wrapper version from' \
|
||||
"release tag: $release_tag" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
section_footer 'Download the official Claude Desktop .deb'
|
||||
}
|
||||
Reference in New Issue
Block a user