Merge pull request 'feat(bootstrap): replace exec()-based addon loading with manifest-driven loader' (#256) from feat/manifest-addon-loader into main
All checks were successful
Build and Test / build (push) Successful in 50m29s
All checks were successful
Build and Test / build (push) Successful in 50m29s
Reviewed-on: #256
This commit was merged in pull request #256.
This commit is contained in:
@@ -13,6 +13,7 @@ install(
|
|||||||
FILES
|
FILES
|
||||||
Init.py
|
Init.py
|
||||||
InitGui.py
|
InitGui.py
|
||||||
|
addon_loader.py
|
||||||
kc_format.py
|
kc_format.py
|
||||||
update_checker.py
|
update_checker.py
|
||||||
${CMAKE_CURRENT_BINARY_DIR}/version.py
|
${CMAKE_CURRENT_BINARY_DIR}/version.py
|
||||||
|
|||||||
@@ -1,48 +1,13 @@
|
|||||||
# Kindred Create - Core Module
|
# Kindred Create - Core Module
|
||||||
# Console initialization - loads ztools and Silo addons
|
# Console initialization - loads Kindred addons via manifest-driven loader
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import FreeCAD
|
import FreeCAD
|
||||||
|
|
||||||
|
try:
|
||||||
|
from addon_loader import load_addons
|
||||||
|
|
||||||
def setup_kindred_addons():
|
load_addons(gui=False)
|
||||||
"""Add Kindred Create addon paths and load their Init.py files."""
|
except Exception as e:
|
||||||
# Get the FreeCAD home directory (where src/Mod/Create is installed)
|
FreeCAD.Console.PrintWarning(f"Create: Addon loader failed: {e}\n")
|
||||||
home = FreeCAD.getHomePath()
|
|
||||||
mods_dir = os.path.join(home, "mods")
|
|
||||||
|
|
||||||
# Define built-in addons with their paths relative to mods/
|
|
||||||
addons = [
|
|
||||||
("ztools", "ztools/ztools"), # mods/ztools/ztools/
|
|
||||||
("silo", "silo/freecad"), # mods/silo/freecad/
|
|
||||||
]
|
|
||||||
|
|
||||||
for name, subpath in addons:
|
|
||||||
addon_path = os.path.join(mods_dir, subpath)
|
|
||||||
if os.path.isdir(addon_path):
|
|
||||||
# Add to sys.path if not already present
|
|
||||||
if addon_path not in sys.path:
|
|
||||||
sys.path.insert(0, addon_path)
|
|
||||||
|
|
||||||
# Execute Init.py if it exists
|
|
||||||
init_file = os.path.join(addon_path, "Init.py")
|
|
||||||
if os.path.isfile(init_file):
|
|
||||||
try:
|
|
||||||
with open(init_file) as f:
|
|
||||||
exec_globals = globals().copy()
|
|
||||||
exec_globals["__file__"] = init_file
|
|
||||||
exec_globals["__name__"] = name
|
|
||||||
exec(compile(f.read(), init_file, "exec"), exec_globals)
|
|
||||||
FreeCAD.Console.PrintLog(f"Create: Loaded {name} Init.py\n")
|
|
||||||
except Exception as e:
|
|
||||||
FreeCAD.Console.PrintWarning(
|
|
||||||
f"Create: Failed to load {name}: {e}\n"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
FreeCAD.Console.PrintLog(f"Create: Addon path not found: {addon_path}\n")
|
|
||||||
|
|
||||||
|
|
||||||
setup_kindred_addons()
|
|
||||||
FreeCAD.Console.PrintLog("Create module initialized\n")
|
FreeCAD.Console.PrintLog("Create module initialized\n")
|
||||||
|
|||||||
@@ -1,50 +1,16 @@
|
|||||||
# Kindred Create - Core Module
|
# Kindred Create - Core Module
|
||||||
# GUI initialization - loads ztools and Silo workbenches
|
# GUI initialization - loads Kindred addon workbenches via manifest-driven loader
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import FreeCAD
|
import FreeCAD
|
||||||
import FreeCADGui
|
import FreeCADGui
|
||||||
|
|
||||||
|
try:
|
||||||
|
from addon_loader import load_addons
|
||||||
|
|
||||||
def setup_kindred_workbenches():
|
load_addons(gui=True)
|
||||||
"""Load Kindred Create addon workbenches."""
|
except Exception as e:
|
||||||
home = FreeCAD.getHomePath()
|
FreeCAD.Console.PrintWarning(f"Create: Addon GUI loader failed: {e}\n")
|
||||||
mods_dir = os.path.join(home, "mods")
|
|
||||||
|
|
||||||
addons = [
|
|
||||||
("ztools", "ztools/ztools"),
|
|
||||||
("silo", "silo/freecad"),
|
|
||||||
]
|
|
||||||
|
|
||||||
for name, subpath in addons:
|
|
||||||
addon_path = os.path.join(mods_dir, subpath)
|
|
||||||
if os.path.isdir(addon_path):
|
|
||||||
# Ensure path is in sys.path
|
|
||||||
if addon_path not in sys.path:
|
|
||||||
sys.path.insert(0, addon_path)
|
|
||||||
|
|
||||||
# Execute InitGui.py if it exists
|
|
||||||
init_gui_file = os.path.join(addon_path, "InitGui.py")
|
|
||||||
if os.path.isfile(init_gui_file):
|
|
||||||
try:
|
|
||||||
with open(init_gui_file) as f:
|
|
||||||
exec_globals = globals().copy()
|
|
||||||
exec_globals["__file__"] = init_gui_file
|
|
||||||
exec_globals["__name__"] = name
|
|
||||||
exec(
|
|
||||||
compile(f.read(), init_gui_file, "exec"),
|
|
||||||
exec_globals,
|
|
||||||
)
|
|
||||||
FreeCAD.Console.PrintLog(f"Create: Loaded {name} workbench\n")
|
|
||||||
except Exception as e:
|
|
||||||
FreeCAD.Console.PrintWarning(
|
|
||||||
f"Create: Failed to load {name} GUI: {e}\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
setup_kindred_workbenches()
|
|
||||||
FreeCAD.Console.PrintLog("Create GUI module initialized\n")
|
FreeCAD.Console.PrintLog("Create GUI module initialized\n")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
493
src/Mod/Create/addon_loader.py
Normal file
493
src/Mod/Create/addon_loader.py
Normal file
@@ -0,0 +1,493 @@
|
|||||||
|
# Kindred Create - Manifest-driven addon loader
|
||||||
|
#
|
||||||
|
# Replaces the hard-coded exec() loading in Init.py/InitGui.py with a
|
||||||
|
# pipeline that scans mods/, parses package.xml manifests, validates
|
||||||
|
# compatibility, resolves dependency order, and exposes a runtime registry.
|
||||||
|
|
||||||
|
import enum
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import FreeCAD
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Data structures
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class AddonState(enum.Enum):
|
||||||
|
DISCOVERED = "discovered"
|
||||||
|
VALIDATED = "validated"
|
||||||
|
LOADED = "loaded"
|
||||||
|
SKIPPED = "skipped"
|
||||||
|
FAILED = "failed"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AddonManifest:
|
||||||
|
"""Parsed addon metadata from package.xml plus Kindred extensions."""
|
||||||
|
|
||||||
|
# Identity (from package.xml standard fields)
|
||||||
|
name: str
|
||||||
|
version: str
|
||||||
|
description: str = ""
|
||||||
|
|
||||||
|
# Paths (resolved during discovery)
|
||||||
|
package_xml_path: str = ""
|
||||||
|
addon_root: str = ""
|
||||||
|
workbench_path: str = ""
|
||||||
|
|
||||||
|
# Kindred extensions (from <kindred> element, all optional)
|
||||||
|
min_create_version: Optional[str] = None
|
||||||
|
max_create_version: Optional[str] = None
|
||||||
|
load_priority: int = 100
|
||||||
|
dependencies: list[str] = field(default_factory=list)
|
||||||
|
has_kindred_element: bool = False
|
||||||
|
|
||||||
|
# Runtime state
|
||||||
|
state: AddonState = AddonState.DISCOVERED
|
||||||
|
error: str = ""
|
||||||
|
load_time_ms: float = 0.0
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (
|
||||||
|
f"AddonManifest(name={self.name!r}, version={self.version!r}, state={self.state.value})"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Addon registry
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class AddonRegistry:
|
||||||
|
"""Runtime registry of discovered addons and their load status."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._addons: dict[str, AddonManifest] = {}
|
||||||
|
self._load_order: list[str] = []
|
||||||
|
|
||||||
|
def register(self, manifest: AddonManifest):
|
||||||
|
self._addons[manifest.name] = manifest
|
||||||
|
|
||||||
|
def set_load_order(self, names: list[str]):
|
||||||
|
self._load_order = list(names)
|
||||||
|
|
||||||
|
def get(self, name: str) -> Optional[AddonManifest]:
|
||||||
|
return self._addons.get(name)
|
||||||
|
|
||||||
|
def all(self) -> list[AddonManifest]:
|
||||||
|
return list(self._addons.values())
|
||||||
|
|
||||||
|
def loaded(self) -> list[AddonManifest]:
|
||||||
|
return [m for m in self._by_load_order() if m.state == AddonState.LOADED]
|
||||||
|
|
||||||
|
def failed(self) -> list[AddonManifest]:
|
||||||
|
return [m for m in self._addons.values() if m.state == AddonState.FAILED]
|
||||||
|
|
||||||
|
def skipped(self) -> list[AddonManifest]:
|
||||||
|
return [m for m in self._addons.values() if m.state == AddonState.SKIPPED]
|
||||||
|
|
||||||
|
def is_loaded(self, name: str) -> bool:
|
||||||
|
m = self._addons.get(name)
|
||||||
|
return m is not None and m.state == AddonState.LOADED
|
||||||
|
|
||||||
|
def _by_load_order(self) -> list[AddonManifest]:
|
||||||
|
ordered = []
|
||||||
|
for name in self._load_order:
|
||||||
|
m = self._addons.get(name)
|
||||||
|
if m is not None:
|
||||||
|
ordered.append(m)
|
||||||
|
# Include any addons not in load order (shouldn't happen, but safe)
|
||||||
|
seen = set(self._load_order)
|
||||||
|
for name, m in self._addons.items():
|
||||||
|
if name not in seen:
|
||||||
|
ordered.append(m)
|
||||||
|
return ordered
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
loaded = len(self.loaded())
|
||||||
|
total = len(self._addons)
|
||||||
|
names = ", ".join(m.name for m in self.loaded())
|
||||||
|
return f"AddonRegistry({loaded}/{total} loaded: {names})"
|
||||||
|
|
||||||
|
|
||||||
|
# Module-level singleton
|
||||||
|
_registry: Optional[AddonRegistry] = None
|
||||||
|
|
||||||
|
# Legacy load order for backward compatibility when no <kindred> elements exist.
|
||||||
|
# Once addons declare <kindred> in their package.xml (issue #252), this is ignored.
|
||||||
|
_LEGACY_ORDER = ["ztools", "silo"]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Discovery
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def scan_addons(mods_dir: str) -> list[AddonManifest]:
|
||||||
|
"""Scan mods/ for directories containing package.xml at depth 1-2."""
|
||||||
|
manifests = []
|
||||||
|
|
||||||
|
if not os.path.isdir(mods_dir):
|
||||||
|
FreeCAD.Console.PrintLog(f"Create: mods directory not found: {mods_dir}\n")
|
||||||
|
return manifests
|
||||||
|
|
||||||
|
for entry in os.listdir(mods_dir):
|
||||||
|
entry_path = os.path.join(mods_dir, entry)
|
||||||
|
if not os.path.isdir(entry_path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check depth 1: mods/<addon>/package.xml
|
||||||
|
pkg_xml = os.path.join(entry_path, "package.xml")
|
||||||
|
if os.path.isfile(pkg_xml):
|
||||||
|
manifests.append(
|
||||||
|
AddonManifest(
|
||||||
|
name="",
|
||||||
|
version="",
|
||||||
|
package_xml_path=pkg_xml,
|
||||||
|
addon_root=entry_path,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check depth 2: mods/<addon>/<subdir>/package.xml
|
||||||
|
for sub in os.listdir(entry_path):
|
||||||
|
sub_path = os.path.join(entry_path, sub)
|
||||||
|
if not os.path.isdir(sub_path):
|
||||||
|
continue
|
||||||
|
pkg_xml = os.path.join(sub_path, "package.xml")
|
||||||
|
if os.path.isfile(pkg_xml):
|
||||||
|
manifests.append(
|
||||||
|
AddonManifest(
|
||||||
|
name="",
|
||||||
|
version="",
|
||||||
|
package_xml_path=pkg_xml,
|
||||||
|
addon_root=sub_path,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return manifests
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Parsing
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# FreeCAD package.xml namespace
|
||||||
|
_PKG_NS = "https://wiki.freecad.org/Package_Metadata"
|
||||||
|
|
||||||
|
|
||||||
|
def _find(parent, tag):
|
||||||
|
"""Find a child element, trying with and without namespace."""
|
||||||
|
el = parent.find(f"{{{_PKG_NS}}}{tag}")
|
||||||
|
if el is None:
|
||||||
|
el = parent.find(tag)
|
||||||
|
return el
|
||||||
|
|
||||||
|
|
||||||
|
def _findall(parent, tag):
|
||||||
|
"""Find all child elements, trying with and without namespace."""
|
||||||
|
els = parent.findall(f"{{{_PKG_NS}}}{tag}")
|
||||||
|
if not els:
|
||||||
|
els = parent.findall(tag)
|
||||||
|
return els
|
||||||
|
|
||||||
|
|
||||||
|
def _text(parent, tag, default=""):
|
||||||
|
"""Get text content of a child element."""
|
||||||
|
el = _find(parent, tag)
|
||||||
|
return el.text.strip() if el is not None and el.text else default
|
||||||
|
|
||||||
|
|
||||||
|
def parse_manifest(manifest: AddonManifest):
|
||||||
|
"""Parse package.xml into the manifest, including <kindred> extensions."""
|
||||||
|
try:
|
||||||
|
tree = ET.parse(manifest.package_xml_path)
|
||||||
|
root = tree.getroot()
|
||||||
|
except ET.ParseError as e:
|
||||||
|
manifest.state = AddonState.FAILED
|
||||||
|
manifest.error = f"XML parse error: {e}"
|
||||||
|
FreeCAD.Console.PrintWarning(f"Create: Failed to parse {manifest.package_xml_path}: {e}\n")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Standard fields
|
||||||
|
manifest.name = _text(root, "name") or os.path.basename(manifest.addon_root)
|
||||||
|
manifest.version = _text(root, "version", "0.0.0")
|
||||||
|
manifest.description = _text(root, "description")
|
||||||
|
|
||||||
|
# Resolve workbench path from <content><workbench><subdirectory>
|
||||||
|
content = _find(root, "content")
|
||||||
|
if content is not None:
|
||||||
|
workbench = _find(content, "workbench")
|
||||||
|
if workbench is not None:
|
||||||
|
subdir = _text(workbench, "subdirectory", ".")
|
||||||
|
# Normalize: strip leading ./
|
||||||
|
if subdir.startswith("./"):
|
||||||
|
subdir = subdir[2:]
|
||||||
|
if subdir == "" or subdir == ".":
|
||||||
|
manifest.workbench_path = manifest.addon_root
|
||||||
|
else:
|
||||||
|
manifest.workbench_path = os.path.join(manifest.addon_root, subdir)
|
||||||
|
|
||||||
|
# Fallback: use addon_root if no workbench subdirectory found
|
||||||
|
if not manifest.workbench_path:
|
||||||
|
manifest.workbench_path = manifest.addon_root
|
||||||
|
|
||||||
|
# Kindred extensions (optional)
|
||||||
|
kindred = _find(root, "kindred")
|
||||||
|
if kindred is not None:
|
||||||
|
manifest.has_kindred_element = True
|
||||||
|
manifest.min_create_version = _text(kindred, "min_create_version") or None
|
||||||
|
manifest.max_create_version = _text(kindred, "max_create_version") or None
|
||||||
|
priority_str = _text(kindred, "load_priority")
|
||||||
|
if priority_str:
|
||||||
|
try:
|
||||||
|
manifest.load_priority = int(priority_str)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
deps = _find(kindred, "dependencies")
|
||||||
|
if deps is not None:
|
||||||
|
for dep in _findall(deps, "dependency"):
|
||||||
|
if dep.text and dep.text.strip():
|
||||||
|
manifest.dependencies.append(dep.text.strip())
|
||||||
|
|
||||||
|
FreeCAD.Console.PrintLog(
|
||||||
|
f"Create: Parsed {manifest.name} v{manifest.version} from {manifest.package_xml_path}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Validation
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_version(v: str) -> tuple:
|
||||||
|
"""Parse a version string into a comparable tuple of ints."""
|
||||||
|
try:
|
||||||
|
return tuple(int(x) for x in v.split("."))
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
return (0, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_manifest(manifest: AddonManifest, create_version: str) -> bool:
|
||||||
|
"""Check version compatibility and path existence. Returns True if valid."""
|
||||||
|
if manifest.state == AddonState.FAILED:
|
||||||
|
return False
|
||||||
|
|
||||||
|
cv = _parse_version(create_version)
|
||||||
|
|
||||||
|
if manifest.min_create_version:
|
||||||
|
if cv < _parse_version(manifest.min_create_version):
|
||||||
|
manifest.state = AddonState.SKIPPED
|
||||||
|
manifest.error = (
|
||||||
|
f"Requires Create >= {manifest.min_create_version}, running {create_version}"
|
||||||
|
)
|
||||||
|
FreeCAD.Console.PrintWarning(f"Create: Skipping {manifest.name}: {manifest.error}\n")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if manifest.max_create_version:
|
||||||
|
if cv > _parse_version(manifest.max_create_version):
|
||||||
|
manifest.state = AddonState.SKIPPED
|
||||||
|
manifest.error = (
|
||||||
|
f"Requires Create <= {manifest.max_create_version}, running {create_version}"
|
||||||
|
)
|
||||||
|
FreeCAD.Console.PrintWarning(f"Create: Skipping {manifest.name}: {manifest.error}\n")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not os.path.isdir(manifest.workbench_path):
|
||||||
|
manifest.state = AddonState.SKIPPED
|
||||||
|
manifest.error = f"Workbench path not found: {manifest.workbench_path}"
|
||||||
|
FreeCAD.Console.PrintWarning(f"Create: Skipping {manifest.name}: {manifest.error}\n")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# At least one of Init.py or InitGui.py must exist
|
||||||
|
has_init = os.path.isfile(os.path.join(manifest.workbench_path, "Init.py"))
|
||||||
|
has_gui = os.path.isfile(os.path.join(manifest.workbench_path, "InitGui.py"))
|
||||||
|
if not has_init and not has_gui:
|
||||||
|
manifest.state = AddonState.SKIPPED
|
||||||
|
manifest.error = f"No Init.py or InitGui.py in {manifest.workbench_path}"
|
||||||
|
FreeCAD.Console.PrintWarning(f"Create: Skipping {manifest.name}: {manifest.error}\n")
|
||||||
|
return False
|
||||||
|
|
||||||
|
manifest.state = AddonState.VALIDATED
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Dependency resolution
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_load_order(manifests: list[AddonManifest], mods_dir: str) -> list[AddonManifest]:
|
||||||
|
"""Sort addons by dependencies, then by (load_priority, name).
|
||||||
|
|
||||||
|
If no addons declare a <kindred> element, fall back to the legacy
|
||||||
|
hard-coded order for backward compatibility.
|
||||||
|
"""
|
||||||
|
if not manifests:
|
||||||
|
return []
|
||||||
|
|
||||||
|
by_name = {m.name: m for m in manifests}
|
||||||
|
any_kindred = any(m.has_kindred_element for m in manifests)
|
||||||
|
|
||||||
|
if not any_kindred:
|
||||||
|
# Legacy fallback: use hard-coded order matched by directory name
|
||||||
|
return _legacy_order(manifests, mods_dir)
|
||||||
|
|
||||||
|
# Topological sort with graphlib
|
||||||
|
from graphlib import CycleError, TopologicalSorter
|
||||||
|
|
||||||
|
ts = TopologicalSorter()
|
||||||
|
for m in manifests:
|
||||||
|
# Only include dependencies that are actually discovered
|
||||||
|
known_deps = [d for d in m.dependencies if d in by_name]
|
||||||
|
unknown_deps = [d for d in m.dependencies if d not in by_name]
|
||||||
|
for dep in unknown_deps:
|
||||||
|
m.state = AddonState.SKIPPED
|
||||||
|
m.error = f"Missing dependency: {dep}"
|
||||||
|
FreeCAD.Console.PrintWarning(f"Create: Skipping {m.name}: {m.error}\n")
|
||||||
|
if m.state != AddonState.SKIPPED:
|
||||||
|
ts.add(m.name, *known_deps)
|
||||||
|
|
||||||
|
try:
|
||||||
|
order = list(ts.static_order())
|
||||||
|
except CycleError as e:
|
||||||
|
FreeCAD.Console.PrintWarning(
|
||||||
|
f"Create: Dependency cycle detected: {e}. Falling back to priority order.\n"
|
||||||
|
)
|
||||||
|
return sorted(
|
||||||
|
[m for m in manifests if m.state != AddonState.SKIPPED],
|
||||||
|
key=lambda m: (m.load_priority, m.name),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Filter to actual manifests, preserving topological order
|
||||||
|
# Secondary sort within independent groups by (priority, name)
|
||||||
|
result = []
|
||||||
|
for name in order:
|
||||||
|
m = by_name.get(name)
|
||||||
|
if m is not None and m.state != AddonState.SKIPPED:
|
||||||
|
result.append(m)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _legacy_order(manifests: list[AddonManifest], mods_dir: str) -> list[AddonManifest]:
|
||||||
|
"""Order addons using the legacy hard-coded list, matched by directory path."""
|
||||||
|
by_dir = {}
|
||||||
|
for m in manifests:
|
||||||
|
# Extract the top-level directory name under mods/
|
||||||
|
rel = os.path.relpath(m.addon_root, mods_dir)
|
||||||
|
top_dir = rel.split(os.sep)[0]
|
||||||
|
by_dir[top_dir] = m
|
||||||
|
|
||||||
|
ordered = []
|
||||||
|
for dir_name in _LEGACY_ORDER:
|
||||||
|
if dir_name in by_dir:
|
||||||
|
ordered.append(by_dir.pop(dir_name))
|
||||||
|
|
||||||
|
# Append any addons not in the legacy list (alphabetically)
|
||||||
|
for name in sorted(by_dir.keys()):
|
||||||
|
ordered.append(by_dir[name])
|
||||||
|
|
||||||
|
return ordered
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Loading
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _load_addon(manifest: AddonManifest, gui: bool = False):
|
||||||
|
"""Execute an addon's Init.py or InitGui.py."""
|
||||||
|
init_file = "InitGui.py" if gui else "Init.py"
|
||||||
|
filepath = os.path.join(manifest.workbench_path, init_file)
|
||||||
|
|
||||||
|
if not os.path.isfile(filepath):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ensure workbench path is in sys.path
|
||||||
|
if manifest.workbench_path not in sys.path:
|
||||||
|
sys.path.insert(0, manifest.workbench_path)
|
||||||
|
|
||||||
|
start = time.monotonic()
|
||||||
|
try:
|
||||||
|
with open(filepath) as f:
|
||||||
|
exec_globals = globals().copy()
|
||||||
|
exec_globals["__file__"] = filepath
|
||||||
|
exec_globals["__name__"] = manifest.name
|
||||||
|
exec(compile(f.read(), filepath, "exec"), exec_globals)
|
||||||
|
elapsed = (time.monotonic() - start) * 1000
|
||||||
|
if not gui:
|
||||||
|
manifest.load_time_ms = elapsed
|
||||||
|
else:
|
||||||
|
manifest.load_time_ms += elapsed
|
||||||
|
manifest.state = AddonState.LOADED
|
||||||
|
FreeCAD.Console.PrintLog(f"Create: Loaded {manifest.name} {init_file} ({elapsed:.0f}ms)\n")
|
||||||
|
except Exception as e:
|
||||||
|
manifest.state = AddonState.FAILED
|
||||||
|
manifest.error = str(e)
|
||||||
|
FreeCAD.Console.PrintWarning(f"Create: Failed to load {manifest.name}: {e}\n")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Top-level API
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _get_create_version() -> str:
|
||||||
|
try:
|
||||||
|
from version import VERSION
|
||||||
|
|
||||||
|
return VERSION
|
||||||
|
except ImportError:
|
||||||
|
return "0.0.0"
|
||||||
|
|
||||||
|
|
||||||
|
def load_addons(gui: bool = False):
|
||||||
|
"""Load Kindred addons from mods/.
|
||||||
|
|
||||||
|
Called twice: once from Init.py (gui=False) to run each addon's Init.py,
|
||||||
|
and once from InitGui.py (gui=True) to run each addon's InitGui.py.
|
||||||
|
The gui=False call runs the full pipeline and builds the registry.
|
||||||
|
The gui=True call reuses the registry and loads InitGui.py for each
|
||||||
|
successfully loaded addon.
|
||||||
|
"""
|
||||||
|
global _registry
|
||||||
|
|
||||||
|
mods_dir = os.path.join(FreeCAD.getHomePath(), "mods")
|
||||||
|
|
||||||
|
if not gui:
|
||||||
|
# Full pipeline: scan -> parse -> validate -> sort -> load -> register
|
||||||
|
manifests = scan_addons(mods_dir)
|
||||||
|
|
||||||
|
for m in manifests:
|
||||||
|
parse_manifest(m)
|
||||||
|
|
||||||
|
create_version = _get_create_version()
|
||||||
|
validated = [m for m in manifests if validate_manifest(m, create_version)]
|
||||||
|
ordered = resolve_load_order(validated, mods_dir)
|
||||||
|
|
||||||
|
_registry = AddonRegistry()
|
||||||
|
for m in manifests:
|
||||||
|
_registry.register(m)
|
||||||
|
_registry.set_load_order([m.name for m in ordered])
|
||||||
|
FreeCAD.KindredAddons = _registry
|
||||||
|
|
||||||
|
for m in ordered:
|
||||||
|
_load_addon(m, gui=False)
|
||||||
|
else:
|
||||||
|
# GUI phase: reuse registry, load InitGui.py in load order
|
||||||
|
if _registry is None:
|
||||||
|
FreeCAD.Console.PrintWarning(
|
||||||
|
"Create: Addon registry not initialized, skipping GUI load\n"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
for m in _registry.loaded():
|
||||||
|
_load_addon(m, gui=True)
|
||||||
Reference in New Issue
Block a user