feat(bootstrap): replace exec()-based addon loading with manifest-driven loader
All checks were successful
Build and Test / build (pull_request) Successful in 34m4s
All checks were successful
Build and Test / build (pull_request) Successful in 34m4s
Add addon_loader.py implementing a six-stage pipeline: scan mods/ for package.xml files, parse standard fields and optional <kindred> extensions via ElementTree, validate version compatibility, resolve load order via graphlib.TopologicalSorter (with legacy fallback), exec() Init.py/InitGui.py, and populate a runtime AddonRegistry exposed as FreeCAD.KindredAddons. Replace hard-coded addon lists in Init.py and InitGui.py with calls to addon_loader.load_addons(). All QTimer-based Silo integration code in InitGui.py is unchanged. Backward compatible: addons without <kindred> elements load with no constraints using the existing ztools-then-silo order. Closes #248
This commit is contained in:
@@ -13,6 +13,7 @@ install(
|
||||
FILES
|
||||
Init.py
|
||||
InitGui.py
|
||||
addon_loader.py
|
||||
kc_format.py
|
||||
update_checker.py
|
||||
${CMAKE_CURRENT_BINARY_DIR}/version.py
|
||||
|
||||
@@ -1,48 +1,13 @@
|
||||
# Kindred Create - Core Module
|
||||
# Console initialization - loads ztools and Silo addons
|
||||
|
||||
import os
|
||||
import sys
|
||||
# Console initialization - loads Kindred addons via manifest-driven loader
|
||||
|
||||
import FreeCAD
|
||||
|
||||
try:
|
||||
from addon_loader import load_addons
|
||||
|
||||
def setup_kindred_addons():
|
||||
"""Add Kindred Create addon paths and load their Init.py files."""
|
||||
# Get the FreeCAD home directory (where src/Mod/Create is installed)
|
||||
home = FreeCAD.getHomePath()
|
||||
mods_dir = os.path.join(home, "mods")
|
||||
load_addons(gui=False)
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintWarning(f"Create: Addon loader failed: {e}\n")
|
||||
|
||||
# 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")
|
||||
|
||||
@@ -1,50 +1,16 @@
|
||||
# Kindred Create - Core Module
|
||||
# GUI initialization - loads ztools and Silo workbenches
|
||||
|
||||
import os
|
||||
import sys
|
||||
# GUI initialization - loads Kindred addon workbenches via manifest-driven loader
|
||||
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
|
||||
try:
|
||||
from addon_loader import load_addons
|
||||
|
||||
def setup_kindred_workbenches():
|
||||
"""Load Kindred Create addon workbenches."""
|
||||
home = FreeCAD.getHomePath()
|
||||
mods_dir = os.path.join(home, "mods")
|
||||
load_addons(gui=True)
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintWarning(f"Create: Addon GUI loader failed: {e}\n")
|
||||
|
||||
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")
|
||||
|
||||
|
||||
|
||||
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