Files
freecad-robust-mcp-fc111/addon/FreecadRobustMCP/path_utils.py
T
8c338f6da7 feat: MCP Bridge Workbench, just command cleanup, testing, etc. (#24)
* 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>
2026-01-10 15:26:33 -08:00

117 lines
3.7 KiB
Python

"""Shared path utilities for the Robust MCP Bridge addon.
SPDX-License-Identifier: MIT
Copyright (c) 2025 Sean P. Kane (GitHub: spkane)
This module provides centralized path-finding functions for locating
the addon directory, icons, and other resources. All path-related
logic is consolidated here to avoid duplication across modules.
NOTE: Using os.path instead of pathlib throughout this module due to
FreeCAD's module loading behavior which can have issues with some
Python features at load time.
"""
from __future__ import annotations
import os # noqa: PTH
import FreeCAD
# Cache for addon path to avoid repeated filesystem lookups
_addon_path_cache: str | None = None
def get_addon_path() -> str:
"""Get the path to this addon's directory.
Uses multiple fallback methods to locate the addon directory:
1. __file__ if available
2. FreeCAD's Mod path + addon name
3. Versioned FreeCAD directory (FreeCAD 1.x: v1-*)
Returns:
The absolute path to the addon directory, or empty string if not found.
Once found, the path is cached for subsequent calls.
"""
global _addon_path_cache
if _addon_path_cache is not None:
return _addon_path_cache
# Method 1: Try __file__
try:
_addon_path_cache = os.path.dirname(os.path.abspath(__file__)) # noqa: PTH100, PTH120
return _addon_path_cache
except NameError:
pass
# Method 2: Use FreeCAD's Mod path + our addon name
try:
mod_path = os.path.join( # noqa: PTH118
FreeCAD.getUserAppDataDir(), "Mod", "FreecadRobustMCP"
)
if os.path.exists(mod_path): # noqa: PTH110
_addon_path_cache = mod_path
return _addon_path_cache
except (OSError, PermissionError) as e:
FreeCAD.Console.PrintWarning(f"Could not access Mod directory: {e}\n")
# Method 3: Try versioned FreeCAD directory (FreeCAD 1.x)
try:
base_path = FreeCAD.getUserAppDataDir()
for item in os.listdir(base_path): # noqa: PTH208
if item.startswith("v1-"):
versioned_mod = os.path.join( # noqa: PTH118
base_path, item, "Mod", "FreecadRobustMCP"
)
if os.path.exists(versioned_mod): # noqa: PTH110
_addon_path_cache = versioned_mod
return _addon_path_cache
except (OSError, PermissionError) as e:
FreeCAD.Console.PrintWarning(f"Could not scan versioned directories: {e}\n")
return ""
def get_icon_path(icon_name: str) -> str:
"""Get the full path to an icon file.
Args:
icon_name: The icon filename or relative path (e.g., "icons/mcp_start.svg")
Returns:
The absolute path to the icon file, or empty string if addon path not found.
"""
addon_path = get_addon_path()
if not addon_path:
return ""
return os.path.join(addon_path, icon_name) # noqa: PTH118
def get_icons_dir() -> str:
"""Get the path to the addon's icons directory.
Returns:
The absolute path to the icons directory, or empty string if not found.
"""
addon_path = get_addon_path()
if addon_path:
icons_dir = os.path.join(addon_path, "icons") # noqa: PTH118
if os.path.isdir(icons_dir): # noqa: PTH112
return icons_dir
return ""
def get_workbench_icon() -> str:
"""Get the path to the workbench's main icon (FreecadRobustMCP.svg).
Returns:
The absolute path to the workbench icon, or empty string if not found.
"""
addon_path = get_addon_path()
if addon_path:
icon_path = os.path.join(addon_path, "FreecadRobustMCP.svg") # noqa: PTH118
if os.path.exists(icon_path): # noqa: PTH110
return icon_path
return ""