qwen-code/packages/desktop/scripts/install-app.sh

416 lines
12 KiB
Bash
Executable file

#!/bin/bash
set -e
VERSIONS_URL=""
DOWNLOAD_DIR="$HOME/.craft-agent/downloads"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
BOLD='\033[1m'
NC='\033[0m' # No Color
info() { printf "%b\n" "${BLUE}>${NC} $1"; }
success() { printf "%b\n" "${GREEN}>${NC} $1"; }
warn() { printf "%b\n" "${YELLOW}!${NC} $1"; }
error() { printf "%b\n" "${RED}x${NC} $1"; exit 1; }
error "Desktop installer is disabled until an owned update server is configured."
# Detect OS
OS="$(uname -s)"
case "$OS" in
Darwin) OS_TYPE="darwin" ;;
Linux) OS_TYPE="linux" ;;
*) error "Unsupported operating system: $OS" ;;
esac
# Check for required dependencies
DOWNLOADER=""
if command -v curl >/dev/null 2>&1; then
DOWNLOADER="curl"
elif command -v wget >/dev/null 2>&1; then
DOWNLOADER="wget"
else
error "Either curl or wget is required but neither is installed"
fi
# Check if yq is available (optional, for YAML parsing)
HAS_YQ=false
if command -v yq >/dev/null 2>&1; then
HAS_YQ=true
fi
# Download function that works with both curl and wget
# Usage: download_file <url> [output_file] [show_progress]
download_file() {
local url="$1"
local output="$2"
local show_progress="${3:-false}"
if [ "$DOWNLOADER" = "curl" ]; then
if [ -n "$output" ]; then
if [ "$show_progress" = "true" ]; then
curl -fL --progress-bar -o "$output" "$url"
else
curl -fsSL -o "$output" "$url"
fi
else
curl -fsSL "$url"
fi
elif [ "$DOWNLOADER" = "wget" ]; then
if [ -n "$output" ]; then
if [ "$show_progress" = "true" ]; then
wget --show-progress -q -O "$output" "$url"
else
wget -q -O "$output" "$url"
fi
else
wget -q -O - "$url"
fi
else
return 1
fi
}
# Extract sha512 from YAML for a specific architecture
# YAML format: files array with url, sha512, arch fields
get_sha512_from_yaml() {
local yaml="$1"
local target_arch="$2"
# Find the line with the target arch and extract sha512 from preceding lines
local in_target_block=false
local sha512=""
while IFS= read -r line; do
# Check if we're entering a new file entry
if [[ $line =~ ^[[:space:]]*-[[:space:]]*url: ]]; then
in_target_block=false
sha512=""
fi
# Extract sha512
if [[ $line =~ sha512:[[:space:]]*(.+) ]]; then
sha512="${BASH_REMATCH[1]}"
fi
# Check arch
if [[ $line =~ arch:[[:space:]]*(.+) ]]; then
local arch="${BASH_REMATCH[1]}"
if [ "$arch" = "$target_arch" ] && [ -n "$sha512" ]; then
echo "$sha512"
return 0
fi
fi
done <<< "$yaml"
return 1
}
# Extract filename from YAML for a specific architecture
get_filename_from_yaml() {
local yaml="$1"
local target_arch="$2"
local url=""
while IFS= read -r line; do
# Check if we're entering a new file entry
if [[ $line =~ ^[[:space:]]*-[[:space:]]*url:[[:space:]]*(.+) ]]; then
url="${BASH_REMATCH[1]}"
fi
# Check arch
if [[ $line =~ arch:[[:space:]]*(.+) ]]; then
local arch="${BASH_REMATCH[1]}"
if [ "$arch" = "$target_arch" ] && [ -n "$url" ]; then
echo "$url"
return 0
fi
fi
done <<< "$yaml"
return 1
}
# Detect architecture
case "$(uname -m)" in
x86_64|amd64) arch="x64" ;;
arm64|aarch64) arch="arm64" ;;
*) error "Unsupported architecture: $(uname -m)" ;;
esac
# Set platform-specific variables
if [ "$OS_TYPE" = "darwin" ]; then
platform="darwin-${arch}"
APP_NAME="Qwen Code Desktop.app"
INSTALL_DIR="/Applications"
ext="zip"
yml_file="latest-mac.yml"
else
# Linux only supports x64 currently
if [ "$arch" != "x64" ]; then
error "Linux currently only supports x64 architecture. Your architecture: $arch"
fi
platform="linux-${arch}"
APP_NAME="Qwen-Code-Desktop-x64.AppImage"
INSTALL_DIR="$HOME/.local/bin"
ext="AppImage"
yml_file="latest-linux.yml"
fi
echo ""
info "Detected platform: $platform"
mkdir -p "$DOWNLOAD_DIR"
mkdir -p "$INSTALL_DIR"
# Fetch YAML manifest directly from /electron/latest/ (no version endpoint needed)
info "Fetching release info..."
manifest_yaml=$(download_file "$VERSIONS_URL/latest/$yml_file")
if [ -z "$manifest_yaml" ]; then
error "Failed to fetch release info from $yml_file"
fi
# Extract version from YAML manifest
if [ "$HAS_YQ" = true ]; then
version=$(echo "$manifest_yaml" | yq -r '.version // empty')
else
version=$(echo "$manifest_yaml" | grep -m1 '^version:' | sed 's/^version:[[:space:]]*//')
fi
if [ -z "$version" ]; then
error "Failed to extract version from manifest"
fi
info "Latest version: $version"
# Extract sha512 and filename for our architecture
if [ "$HAS_YQ" = true ]; then
checksum=$(echo "$manifest_yaml" | yq -r ".files[] | select(.arch == \"$arch\") | .sha512")
filename=$(echo "$manifest_yaml" | yq -r ".files[] | select(.arch == \"$arch\") | .url")
else
checksum=$(get_sha512_from_yaml "$manifest_yaml" "$arch")
filename=$(get_filename_from_yaml "$manifest_yaml" "$arch")
fi
# Validate checksum format (SHA512 base64 = 88 characters)
if [ -z "$checksum" ] || [ ${#checksum} -lt 80 ]; then
error "Architecture $arch not found in $yml_file"
fi
# Use default filename if not found
if [ -z "$filename" ]; then
filename="Qwen-Code-Desktop-${arch}.${ext}"
fi
info "Expected sha512: ${checksum:0:20}..."
# Download installer
installer_url="$VERSIONS_URL/latest/$filename"
installer_path="$DOWNLOAD_DIR/$filename"
info "Downloading $filename..."
echo ""
if ! download_file "$installer_url" "$installer_path" true; then
rm -f "$installer_path"
error "Download failed"
fi
echo ""
# Verify checksum (sha512, base64 encoded)
info "Verifying checksum..."
if [ "$OS_TYPE" = "darwin" ]; then
# macOS: shasum outputs hex, convert to base64
actual=$(shasum -a 512 "$installer_path" | cut -d' ' -f1 | xxd -r -p | base64)
else
# Linux: sha512sum outputs hex, convert to base64
actual=$(sha512sum "$installer_path" | cut -d' ' -f1 | xxd -r -p | base64 | tr -d '\n')
fi
if [ "$actual" != "$checksum" ]; then
rm -f "$installer_path"
error "Checksum verification failed\n Expected: $checksum\n Actual: $actual"
fi
success "Checksum verified!"
# Platform-specific installation
if [ "$OS_TYPE" = "darwin" ]; then
# macOS installation (from ZIP)
zip_path="$installer_path"
# Quit the app if it's running (use bundle ID for reliability)
APP_BUNDLE_ID="com.alibaba.qwen-code"
is_app_running() {
[ "$(osascript -e "application id \"$APP_BUNDLE_ID\" is running" 2>/dev/null || echo false)" = "true" ]
}
if is_app_running; then
info "Quitting Qwen Code Desktop..."
osascript -e "tell application id \"$APP_BUNDLE_ID\" to quit" 2>/dev/null || true
# Wait for app to quit (max 5 seconds) - POSIX compatible loop
i=0
while [ $i -lt 10 ]; do
if ! is_app_running; then
break
fi
sleep 0.5
i=$((i + 1))
done
# Force kill if still running
if is_app_running; then
warn "App didn't quit gracefully. Force quitting (unsaved data may be lost)..."
pkill -9 -f "Qwen Code Desktop.app" 2>/dev/null || true
pkill -9 -f "Qwen Code.app" 2>/dev/null || true
# Wait longer for macOS to release file handles
sleep 3
fi
fi
# Remove existing installation if present
if [ -d "$INSTALL_DIR/$APP_NAME" ]; then
info "Removing previous installation..."
rm -rf "$INSTALL_DIR/$APP_NAME"
fi
if [ -d "$INSTALL_DIR/Qwen Code.app" ]; then
info "Removing previous Qwen Code installation..."
rm -rf "$INSTALL_DIR/Qwen Code.app"
fi
# Extract ZIP to temp directory
info "Extracting..."
temp_dir=$(mktemp -d)
if ! unzip -q "$zip_path" -d "$temp_dir"; then
rm -rf "$temp_dir"
rm -f "$zip_path"
error "Failed to extract ZIP"
fi
# Find the .app in the extracted contents
app_source=$(find "$temp_dir" -maxdepth 1 -name "*.app" -type d | head -1)
if [ -z "$app_source" ]; then
rm -rf "$temp_dir"
rm -f "$zip_path"
error "No .app found in ZIP"
fi
# Copy app to /Applications
info "Installing to $INSTALL_DIR..."
cp -R "$app_source" "$INSTALL_DIR/$APP_NAME"
# Clean up
info "Cleaning up..."
rm -rf "$temp_dir"
rm -f "$zip_path"
# Remove quarantine attribute if present
xattr -rd com.apple.quarantine "$INSTALL_DIR/$APP_NAME" 2>/dev/null || true
echo ""
echo "─────────────────────────────────────────────────────────────────────────"
echo ""
success "Installation complete!"
echo ""
printf "%b\n" " Qwen Code Desktop has been installed to ${BOLD}$INSTALL_DIR/$APP_NAME${NC}"
echo ""
printf "%b\n" " You can launch it from ${BOLD}Applications${NC} or by running:"
printf "%b\n" " ${BOLD}open -a 'Qwen Code Desktop'${NC}"
echo ""
else
# Linux installation
appimage_path="$installer_path"
# New paths
APP_DIR="$HOME/.craft-agent/app"
WRAPPER_PATH="$INSTALL_DIR/qwen-code"
APPIMAGE_INSTALL_PATH="$APP_DIR/Qwen-Code-Desktop-x64.AppImage"
# Kill the app if it's running
if pgrep -f "Qwen-Code.*AppImage" >/dev/null 2>&1; then
info "Stopping Qwen Code Desktop..."
pkill -f "Qwen-Code.*AppImage" 2>/dev/null || true
sleep 2
fi
# Create directories
mkdir -p "$APP_DIR"
mkdir -p "$INSTALL_DIR"
# Remove existing AppImage
[ -f "$APPIMAGE_INSTALL_PATH" ] && rm -f "$APPIMAGE_INSTALL_PATH"
[ -f "$APP_DIR/Qwen-Code-x64.AppImage" ] && rm -f "$APP_DIR/Qwen-Code-x64.AppImage"
# Install AppImage
info "Installing AppImage to $APP_DIR..."
mv "$appimage_path" "$APPIMAGE_INSTALL_PATH"
chmod +x "$APPIMAGE_INSTALL_PATH"
# Create wrapper script
info "Creating launcher at $WRAPPER_PATH..."
cat > "$WRAPPER_PATH" << 'WRAPPER_EOF'
#!/bin/bash
# Qwen Code Desktop launcher - handles Linux-specific AppImage issues
APPIMAGE_PATH="$HOME/.craft-agent/app/Qwen-Code-Desktop-x64.AppImage"
ELECTRON_CACHE="$HOME/.config/@qwen-code"
ELECTRON_CACHE_ALT="$HOME/.cache/@qwen-code"
# Verify AppImage exists
if [ ! -f "$APPIMAGE_PATH" ]; then
echo "Error: Qwen Code Desktop not found at $APPIMAGE_PATH"
echo "Reinstall: disabled until an owned update server is configured"
exit 1
fi
# Ensure DISPLAY is set (required for X11)
if [ -z "$DISPLAY" ]; then
export DISPLAY=:0.0
fi
# Clear stale cache referencing AppImage mount paths
# AppImage creates a new /tmp/.mount_Qwen-XXXX each launch, so any cached path is stale
for cache_dir in "$ELECTRON_CACHE" "$ELECTRON_CACHE_ALT"; do
if [ -d "$cache_dir" ] && grep -rq '/tmp/\.mount_Qwen' "$cache_dir" 2>/dev/null; then
rm -rf "$cache_dir"
fi
done
# Set APPIMAGE for auto-update
export APPIMAGE="$APPIMAGE_PATH"
# Launch with --no-sandbox (AppImage extracts to /tmp, losing SUID on chrome-sandbox)
exec "$APPIMAGE_PATH" --no-sandbox "$@"
WRAPPER_EOF
chmod +x "$WRAPPER_PATH"
# Migrate old installation
OLD_APPIMAGE="$INSTALL_DIR/Qwen-Code-x64.AppImage"
[ -f "$OLD_APPIMAGE" ] && rm -f "$OLD_APPIMAGE"
echo ""
echo "─────────────────────────────────────────────────────────────────────────"
echo ""
success "Installation complete!"
echo ""
printf "%b\n" " AppImage: ${BOLD}$APPIMAGE_INSTALL_PATH${NC}"
printf "%b\n" " Launcher: ${BOLD}$WRAPPER_PATH${NC}"
echo ""
printf "%b\n" " Run with: ${BOLD}qwen-code${NC}"
echo ""
printf "%b\n" " Add to PATH if needed:"
printf "%b\n" " ${BOLD}echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> ~/.bashrc${NC}"
echo ""
# FUSE check
if ! command -v fusermount >/dev/null 2>&1; then
warn "FUSE required but not detected."
printf "%b\n" " Install: ${BOLD}sudo apt install fuse libfuse2${NC} (Debian/Ubuntu)"
printf "%b\n" " ${BOLD}sudo dnf install fuse fuse-libs${NC} (Fedora)"
fi
fi