diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 4008f237e2..66b437e86c 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -28,6 +28,7 @@ - [Repository Structure](./development/repo-structure.md) - [Build System](./development/build-system.md) - [Gui Module Build](./development/gui-build-integration.md) +- [Package.xml Schema Extensions](./development/package-xml-schema.md) # Silo Server diff --git a/docs/src/development/package-xml-schema.md b/docs/src/development/package-xml-schema.md new file mode 100644 index 0000000000..f562cd96bb --- /dev/null +++ b/docs/src/development/package-xml-schema.md @@ -0,0 +1,109 @@ +# Package.xml Schema Extensions + +Kindred Create extends FreeCAD's standard `package.xml` addon manifest with a `` element. This element provides metadata for the manifest-driven addon loader: version compatibility, load ordering, dependency declarations, and editing context registration. + +The `` element is ignored by FreeCAD's AddonManager and stock module loader. Addons with this element remain compatible with upstream FreeCAD. + +## Field reference + +| Field | Parsed by loader | Required | Default | Description | +|---|---|---|---|---| +| `min_create_version` | Yes | No | *(none)* | Minimum Kindred Create version. Addon is skipped if the running version is lower. | +| `max_create_version` | Yes | No | *(none)* | Maximum Kindred Create version. Addon is skipped if the running version is higher. | +| `load_priority` | Yes | No | `100` | Integer. Lower values load first. Used as a secondary sort after dependency resolution. | +| `dependencies` | Yes | No | *(none)* | List of addon names (by `` in their `package.xml`) that must load before this one. | +| `sdk_version` | No | No | *(none)* | Required kindred-addon-sdk version. Reserved for future use when the SDK is available. | +| `pure_python` | No | No | `true` | If `false`, the addon requires compiled C++ components. Informational. | +| `contexts` | No | No | *(none)* | Editing contexts this addon registers or injects into. Informational. | + +Fields marked "Parsed by loader" are read by `addon_loader.py` and affect load behavior. Other fields are informational metadata for tooling and documentation. + +## Schema + +```xml + + 0.1.0 + 1.0.0 + 0.1.0 + 100 + true + + sdk + other-addon + + + + + + + +``` + +All child elements are optional. An empty `` element is valid and signals that the addon is Kindred-aware with all defaults. + +### Version fields + +`min_create_version` and `max_create_version` are compared against the running Kindred Create version using semantic versioning (major.minor.patch). If the running version falls outside the declared range, the addon is skipped with a warning in the report view. + +### Load priority + +When multiple addons have no dependency relationship, `load_priority` determines their relative order. Lower values load first. Suggested ranges: + +| Range | Use | +|---|---| +| 0-9 | SDK and core infrastructure | +| 10-49 | Foundation addons that others may depend on | +| 50-99 | Standard addons | +| 100+ | Optional or late-loading addons | + +### Dependencies + +Each `` names another addon by its `` element in `package.xml`. The loader resolves load order using topological sort. If a dependency is not found among discovered addons, the dependent addon is skipped. + +### Contexts + +The `` element documents which editing contexts the addon interacts with. The `action` attribute describes the type of interaction: + +| Action | Meaning | +|---|---| +| `inject` | Addon injects commands into this context's toolbars | +| `register` | Addon registers this as a new context | +| `overlay` | Addon registers an overlay that may apply across contexts | + +A wildcard `id="*"` indicates the addon's overlay applies universally (matched by a condition function rather than a specific context ID). + +## Example: complete package.xml + +```xml + + + MyAddon + Example Kindred Create addon + 0.2.0 + Developer + LGPL-2.1-or-later + https://git.example.com/myaddon + + + + MyAddonWorkbench + ./ + + + + + 0.1.0 + 80 + true + + + + + +``` + +## Backward compatibility + +- The `` element sits outside `` and is not part of FreeCAD's package.xml specification. FreeCAD's `App.Metadata` C++ parser and the AddonManager's Python `MetadataReader` both ignore unknown elements. +- An addon installed in stock FreeCAD will work normally; the `` extensions are simply unused. +- The Kindred Create loader works with or without the `` element. Addons that omit it load with no version constraints and default priority (100). diff --git a/mods/silo b/mods/silo index dc64a66f0f..7a4ed3550a 160000 --- a/mods/silo +++ b/mods/silo @@ -1 +1 @@ -Subproject commit dc64a66f0f397d2f9ba58377d88f4cbafebd408e +Subproject commit 7a4ed3550aea680b6b733753b7d3f32d1878bc4a diff --git a/mods/ztools b/mods/ztools index ef16ecbaa2..29ca89e533 160000 --- a/mods/ztools +++ b/mods/ztools @@ -1 +1 @@ -Subproject commit ef16ecbaa2c57c34286a21b73d3c8a1a2fec7777 +Subproject commit 29ca89e5337428cc4e4a8a5336d3da6f5f78105d diff --git a/src/Mod/Create/addon_loader.py b/src/Mod/Create/addon_loader.py index 5a73f19370..a0983a20fe 100644 --- a/src/Mod/Create/addon_loader.py +++ b/src/Mod/Create/addon_loader.py @@ -381,7 +381,16 @@ def resolve_load_order( ts.add(m.name, *known_deps) try: - order = list(ts.static_order()) + # Process level by level so we can sort within each topological level + ts.prepare() + order = [] + while ts.is_active(): + ready = list(ts.get_ready()) + # Sort each level by (priority, name) for determinism + ready.sort(key=lambda n: (by_name[n].load_priority, n) if n in by_name else (999, n)) + for name in ready: + ts.done(name) + order.extend(ready) except CycleError as e: FreeCAD.Console.PrintWarning( f"Create: Dependency cycle detected: {e}. Falling back to priority order.\n" @@ -391,8 +400,7 @@ def resolve_load_order( key=lambda m: (m.load_priority, m.name), ) - # Filter to actual manifests, preserving topological order - # Secondary sort within independent groups by (priority, name) + # Filter to actual manifests, preserving sorted topological order result = [] for name in order: m = by_name.get(name)