* fix: lots of fixes and name refactoring * feat: Add workbench preferences * fix: MCP bridge status widget and just command fixes * fix(tests): Use the correct mesa-glx package * fix(ci): Add fontconfig to GUI test dependencies FreeCAD GUI was failing to start with: "Fontconfig error: Cannot load default config file: No such file" Added fontconfig and fonts-dejavu-core packages to the GUI test job dependencies to resolve the font configuration issue. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor(addon): Extract path utilities into shared module Create path_utils.py module that consolidates duplicated path-finding logic from commands.py and InitGui.py: - get_addon_path(): Find addon directory with caching and fallbacks - get_icon_path(): Get full path to an icon file - get_icons_dir(): Get path to icons directory - get_workbench_icon(): Get path to workbench main icon This removes ~100 lines of duplicated code while preserving the same behavior including _addon_path_cache and all fallback methods. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(addon): Prevent stale plugin state on startup failure The StartMCPBridgeCommand.Activated method could leave _mcp_plugin in a partially initialized state if FreecadMCPPlugin.start() failed after the plugin was instantiated. Changes: - Create plugin in a local variable first - Only assign to _mcp_plugin after start() succeeds - Explicitly clear _mcp_plugin and _running_config in exception handlers to ensure clean state for subsequent retry attempts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: Lot of broad improvements * fix(ci): Use blocking headless_server.py for GUI tests The GUI test was using startup_bridge.py which is non-blocking (designed for interactive use). For CI, even in GUI mode, we need the blocking headless_server.py that calls run_forever() to keep FreeCAD running. GUI features are still available since we use the 'freecad' executable instead of 'freecadcmd'. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor(addon): Rename headless_server.py to blocking_bridge.py The old name was misleading because: - It works with both GUI (freecad) and headless (freecadcmd) modes - The key characteristic is that it BLOCKS with run_forever() New naming convention clarifies the difference: - blocking_bridge.py: Starts bridge and blocks (for CI, servers) - startup_bridge.py: Starts bridge and returns (for interactive GUI) Updated all references across: - GitHub workflow (macro-test.yaml) - Just commands (freecad.just) - Unit tests (test_addon_structure.py) - Documentation (5 files) - CLAUDE.md Also improved the script to detect GUI mode dynamically using FreeCAD.GuiUp and display the appropriate status message. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(just): Remove erroneous rm of startup_bridge.py on error The startup script is now a permanent source file in the repository, not a generated temporary file. The rm -f would have deleted source code if FreeCAD wasn't found. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: General improvements * fix: Lots of general fixes and only stable to PyPi * fix: small cleanup * fix: Small fixes and hopefully fixes the GUI tests * fix: Add proper library paths for FreeCAD GUI in CI - Create wrapper scripts instead of symlinks for AppImage binaries - Set LD_LIBRARY_PATH, QT_PLUGIN_PATH for GUI mode - Add diagnostic output to identify startup failures * fix: Use apprun for GUI tests in CI * fix: Improving Xvfb tests * fix: GUI tests worlk * chore: remove invalid --no-splash comments * fix: ARM64 architecture support and other fixes * fix: cleanup * test: just commands test suite * test: improve just command tests * fix: more general improvements * fix: more cleanup * fix: more updates * fix: small tweaks --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
293 lines
10 KiB
Bash
Executable File
293 lines
10 KiB
Bash
Executable File
#!/bin/bash
|
|
# Setup FreeCAD AppImage - replicates .github/actions/setup-freecad/action.yaml
|
|
set -euo pipefail
|
|
|
|
# Validate required variables have safe defaults
|
|
FREECAD_TAG="${FREECAD_TAG:-1.0.2}"
|
|
APPIMAGE_DIR="${APPIMAGE_DIR:-$HOME/freecad-appimage}"
|
|
# Optional SHA256 checksum for verification (if provided, download will be verified)
|
|
APPIMAGE_SHA256="${APPIMAGE_SHA256:-}"
|
|
|
|
# Validate APPIMAGE_DIR is set and non-empty after defaults
|
|
if [[ -z "$APPIMAGE_DIR" ]]; then
|
|
echo "ERROR: APPIMAGE_DIR is empty - cannot determine installation path"
|
|
exit 1
|
|
fi
|
|
|
|
# Marker file to indicate complete installation
|
|
MARKER_FILE="$APPIMAGE_DIR/.freecad_installed"
|
|
# Lock file for atomic operations (prevents race conditions in parallel CI)
|
|
LOCK_FILE="$APPIMAGE_DIR/.freecad_install.lock"
|
|
# Derive APPIMAGE_PATH early for cleanup function
|
|
APPIMAGE_PATH="$APPIMAGE_DIR/FreeCAD.AppImage"
|
|
|
|
# Track whether installation completed successfully
|
|
INSTALL_SUCCESSFUL=false
|
|
|
|
# Cleanup function to remove partial artifacts on failure
|
|
cleanup_on_error() {
|
|
local exit_code=$?
|
|
if [[ $exit_code -ne 0 ]] && [[ "$INSTALL_SUCCESSFUL" != "true" ]]; then
|
|
echo "ERROR: Installation failed (exit code $exit_code), cleaning up partial artifacts..."
|
|
rm -f "$MARKER_FILE" 2>/dev/null || true
|
|
rm -f "$APPIMAGE_PATH" 2>/dev/null || true
|
|
rm -rf "$APPIMAGE_DIR/squashfs-root" 2>/dev/null || true
|
|
rm -f "$LOCK_FILE" 2>/dev/null || true
|
|
fi
|
|
}
|
|
trap cleanup_on_error EXIT
|
|
|
|
# Detect if running in CI environment
|
|
is_ci_environment() {
|
|
# Check common CI environment variables
|
|
[[ -n "${CI:-}" ]] || \
|
|
[[ -n "${GITHUB_ACTIONS:-}" ]] || \
|
|
[[ -n "${GITLAB_CI:-}" ]] || \
|
|
[[ -n "${TRAVIS:-}" ]] || \
|
|
[[ -n "${CIRCLECI:-}" ]] || \
|
|
[[ -n "${JENKINS_URL:-}" ]] || \
|
|
[[ -n "${BUILDKITE:-}" ]]
|
|
}
|
|
|
|
# In CI, require SHA256 checksum for security
|
|
if is_ci_environment && [[ -z "$APPIMAGE_SHA256" ]]; then
|
|
echo "ERROR: APPIMAGE_SHA256 is required in CI environments for security verification"
|
|
echo "Set the APPIMAGE_SHA256 environment variable to the expected checksum"
|
|
exit 1
|
|
fi
|
|
|
|
echo "=== Setting up FreeCAD $FREECAD_TAG ==="
|
|
|
|
# Create directory for lock file
|
|
mkdir -p "$APPIMAGE_DIR"
|
|
|
|
# Use flock for atomic marker file operations (prevents race conditions in parallel CI)
|
|
# The lock is held for the entire installation process
|
|
exec 200>"$LOCK_FILE"
|
|
if ! flock -n 200; then
|
|
echo "Another installation is in progress, waiting for lock..."
|
|
flock 200
|
|
# Re-check marker file after acquiring lock (another process may have completed)
|
|
if [[ -f "$MARKER_FILE" ]] && grep -q "^${FREECAD_TAG}$" "$MARKER_FILE" 2>/dev/null; then
|
|
echo "FreeCAD $FREECAD_TAG already set up by another process, skipping"
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
# Check for complete setup using marker file
|
|
if [[ -f "$MARKER_FILE" ]]; then
|
|
# Verify marker file contains expected version
|
|
if grep -q "^${FREECAD_TAG}$" "$MARKER_FILE" 2>/dev/null; then
|
|
echo "FreeCAD $FREECAD_TAG already set up (marker file present), skipping"
|
|
exit 0
|
|
else
|
|
echo "Different FreeCAD version detected, will reinstall"
|
|
fi
|
|
fi
|
|
|
|
# Check for and clean up partial installations
|
|
PARTIAL_INSTALL=false
|
|
if [[ -f "/usr/local/bin/freecad" ]] && [[ ! -f "/usr/local/bin/freecadcmd" ]]; then
|
|
echo "Warning: Partial installation detected (freecad exists but freecadcmd missing)"
|
|
PARTIAL_INSTALL=true
|
|
elif [[ ! -f "/usr/local/bin/freecad" ]] && [[ -f "/usr/local/bin/freecadcmd" ]]; then
|
|
echo "Warning: Partial installation detected (freecadcmd exists but freecad missing)"
|
|
PARTIAL_INSTALL=true
|
|
fi
|
|
|
|
if [[ "$PARTIAL_INSTALL" == "true" ]]; then
|
|
echo "Cleaning up partial installation..."
|
|
# Remove wrapper scripts if they exist (use sudo if needed)
|
|
if [[ $EUID -ne 0 ]] && command -v sudo &>/dev/null; then
|
|
sudo rm -f /usr/local/bin/freecad /usr/local/bin/freecadcmd 2>/dev/null || true
|
|
else
|
|
rm -f /usr/local/bin/freecad /usr/local/bin/freecadcmd 2>/dev/null || true
|
|
fi
|
|
# Remove marker file to force full reinstall
|
|
rm -f "$MARKER_FILE" 2>/dev/null || true
|
|
fi
|
|
|
|
# Detect architecture
|
|
ARCH=$(uname -m)
|
|
case "$ARCH" in
|
|
x86_64)
|
|
ARCH_SUFFIX="x86_64"
|
|
;;
|
|
aarch64|arm64)
|
|
ARCH_SUFFIX="aarch64"
|
|
;;
|
|
*)
|
|
echo "ERROR: Unsupported architecture: $ARCH"
|
|
exit 1
|
|
;;
|
|
esac
|
|
echo "Detected architecture: $ARCH -> Linux-$ARCH_SUFFIX"
|
|
|
|
# Use direct download URL to avoid GitHub API rate limits
|
|
# Format: FreeCAD_1.0.2-conda-Linux-aarch64-py311.AppImage
|
|
APPIMAGE_URL="https://github.com/FreeCAD/FreeCAD/releases/download/${FREECAD_TAG}/FreeCAD_${FREECAD_TAG}-conda-Linux-${ARCH_SUFFIX}-py311.AppImage"
|
|
APPIMAGE_NAME="FreeCAD_${FREECAD_TAG}-conda-Linux-${ARCH_SUFFIX}-py311.AppImage"
|
|
# APPIMAGE_PATH is defined earlier for use in cleanup_on_error trap
|
|
|
|
echo "FreeCAD release: $FREECAD_TAG"
|
|
echo "AppImage URL: $APPIMAGE_URL"
|
|
echo "AppImage name: $APPIMAGE_NAME"
|
|
|
|
# Download
|
|
mkdir -p "$APPIMAGE_DIR"
|
|
if [ ! -f "$APPIMAGE_PATH" ]; then
|
|
echo "Downloading FreeCAD AppImage..."
|
|
curl -L --retry 3 --retry-delay 5 --retry-all-errors --connect-timeout 30 --max-time 600 \
|
|
-f -o "$APPIMAGE_PATH" \
|
|
"$APPIMAGE_URL"
|
|
|
|
# Verify download succeeded and file exists
|
|
if [[ ! -f "$APPIMAGE_PATH" ]]; then
|
|
echo "ERROR: Download failed - file not found at $APPIMAGE_PATH"
|
|
exit 1
|
|
fi
|
|
|
|
# Verify SHA256 checksum if provided
|
|
if [[ -n "$APPIMAGE_SHA256" ]]; then
|
|
echo "Verifying SHA256 checksum..."
|
|
COMPUTED_SHA256=$(sha256sum "$APPIMAGE_PATH" | awk '{print $1}')
|
|
if [[ "$COMPUTED_SHA256" != "$APPIMAGE_SHA256" ]]; then
|
|
echo "ERROR: SHA256 checksum mismatch!"
|
|
echo " Expected: $APPIMAGE_SHA256"
|
|
echo " Computed: $COMPUTED_SHA256"
|
|
echo "Removing corrupted/tampered download..."
|
|
rm -f "$APPIMAGE_PATH"
|
|
exit 1
|
|
fi
|
|
echo "SHA256 checksum verified successfully"
|
|
else
|
|
echo "Note: No APPIMAGE_SHA256 provided, skipping checksum verification"
|
|
fi
|
|
fi
|
|
chmod +x "$APPIMAGE_PATH"
|
|
|
|
# Extract with proper error handling
|
|
cd "$APPIMAGE_DIR"
|
|
if [ ! -d "squashfs-root" ]; then
|
|
echo "Extracting AppImage..."
|
|
EXTRACTION_ERR=$(mktemp)
|
|
# Capture exit code directly to avoid shell negation issues with $?
|
|
EXTRACTION_EXIT_CODE=0
|
|
./FreeCAD.AppImage --appimage-extract > /dev/null 2>"$EXTRACTION_ERR" || EXTRACTION_EXIT_CODE=$?
|
|
if [[ $EXTRACTION_EXIT_CODE -ne 0 ]]; then
|
|
echo "ERROR: AppImage extraction failed with exit code $EXTRACTION_EXIT_CODE"
|
|
if [[ -s "$EXTRACTION_ERR" ]]; then
|
|
echo "Extraction stderr:"
|
|
cat "$EXTRACTION_ERR"
|
|
fi
|
|
rm -f "$EXTRACTION_ERR"
|
|
exit 1
|
|
fi
|
|
# Check for any stderr output even on success
|
|
if [[ -s "$EXTRACTION_ERR" ]]; then
|
|
echo "Warning: Extraction produced stderr output:"
|
|
cat "$EXTRACTION_ERR"
|
|
fi
|
|
rm -f "$EXTRACTION_ERR"
|
|
fi
|
|
|
|
# Verify extraction produced expected structure
|
|
if [ ! -d "squashfs-root/usr/bin" ]; then
|
|
echo "ERROR: Extracted AppImage missing expected structure (squashfs-root/usr/bin not found)"
|
|
exit 1
|
|
fi
|
|
|
|
echo "Checking AppImage structure..."
|
|
# Display directory contents for diagnostic purposes (not parsed programmatically)
|
|
# shellcheck disable=SC2012 # ls output piped to head for display only, not parsed
|
|
ls -la "$APPIMAGE_DIR/squashfs-root/" 2>/dev/null | head -20
|
|
|
|
# Create wrapper scripts using AppRun
|
|
echo "Creating wrapper scripts..."
|
|
|
|
# Derive APPDIR_PATH from APPIMAGE_DIR for consistency
|
|
APPDIR_PATH="$APPIMAGE_DIR/squashfs-root"
|
|
|
|
# Helper function to install wrapper script
|
|
# Uses sudo only if necessary (not root and sudo exists)
|
|
install_wrapper() {
|
|
local wrapper_name="$1"
|
|
local wrapper_content="$2"
|
|
local wrapper_path="/usr/local/bin/$wrapper_name"
|
|
local temp_file
|
|
|
|
temp_file=$(mktemp)
|
|
echo "$wrapper_content" > "$temp_file"
|
|
chmod +x "$temp_file"
|
|
|
|
# Install with sudo if not root and sudo is available
|
|
if [[ $EUID -ne 0 ]]; then
|
|
if command -v sudo &>/dev/null; then
|
|
sudo mv "$temp_file" "$wrapper_path"
|
|
sudo chmod +x "$wrapper_path"
|
|
else
|
|
echo "ERROR: Not running as root and sudo not available, cannot install to $wrapper_path"
|
|
rm -f "$temp_file"
|
|
exit 1
|
|
fi
|
|
else
|
|
mv "$temp_file" "$wrapper_path"
|
|
chmod +x "$wrapper_path"
|
|
fi
|
|
}
|
|
|
|
# freecadcmd wrapper - avoid trailing colon in LD_LIBRARY_PATH
|
|
FREECADCMD_WRAPPER="#!/bin/bash
|
|
export APPDIR=\"$APPDIR_PATH\"
|
|
export LD_LIBRARY_PATH=\"\$APPDIR/usr/lib\${LD_LIBRARY_PATH:+:\$LD_LIBRARY_PATH}\"
|
|
exec \"\$APPDIR/usr/bin/freecadcmd\" \"\$@\""
|
|
|
|
install_wrapper "freecadcmd" "$FREECADCMD_WRAPPER"
|
|
|
|
# freecad (GUI) wrapper - avoid trailing colon in LD_LIBRARY_PATH
|
|
FREECAD_WRAPPER="#!/bin/bash
|
|
export APPDIR=\"$APPDIR_PATH\"
|
|
export LD_LIBRARY_PATH=\"\$APPDIR/usr/lib\${LD_LIBRARY_PATH:+:\$LD_LIBRARY_PATH}\"
|
|
exec \"\$APPDIR/usr/bin/freecad\" \"\$@\""
|
|
|
|
install_wrapper "freecad" "$FREECAD_WRAPPER"
|
|
|
|
echo "Wrapper scripts created at /usr/local/bin/freecad{,cmd}"
|
|
|
|
# Verify installation - fail the script if verification fails
|
|
# Use explicit paths to avoid PATH resolution issues
|
|
echo "=== Verifying FreeCAD installation ==="
|
|
|
|
# Check wrapper exists before running
|
|
if [[ ! -x /usr/local/bin/freecadcmd ]]; then
|
|
echo "ERROR: freecadcmd wrapper not found or not executable at /usr/local/bin/freecadcmd"
|
|
exit 1
|
|
fi
|
|
|
|
echo "--- freecadcmd --version ---"
|
|
if ! /usr/local/bin/freecadcmd --version; then
|
|
echo "ERROR: freecadcmd version check failed"
|
|
exit 1
|
|
fi
|
|
|
|
echo "--- freecadcmd Python test ---"
|
|
if ! /usr/local/bin/freecadcmd -c "import sys; print(f'FreeCAD Python: {sys.version}')"; then
|
|
echo "ERROR: FreeCAD Python test failed"
|
|
exit 1
|
|
fi
|
|
|
|
echo "--- FreeCAD module import test ---"
|
|
if ! /usr/local/bin/freecadcmd -c "import FreeCAD; print(f'FreeCAD version: {FreeCAD.Version()}')"; then
|
|
echo "ERROR: FreeCAD module import failed - FreeCAD bindings may be missing or corrupted"
|
|
exit 1
|
|
fi
|
|
|
|
# Create marker file to indicate successful installation
|
|
echo "$FREECAD_TAG" > "$MARKER_FILE"
|
|
echo "Created marker file: $MARKER_FILE"
|
|
|
|
# Mark installation as successful (prevents cleanup_on_error from removing artifacts)
|
|
INSTALL_SUCCESSFUL=true
|
|
|
|
echo "=== FreeCAD setup complete ==="
|