Kit Manager: detect unsupported kits via platform database

Scan sedona/platforms/db/ to find which native kits are compiled for
the SAX's platform.  Kits with native code for other platforms, kits
providing a PlatformService for a different device, and their
transitive dependents are placed in a "Not supported" section with a
red-cross checkbox that cannot be toggled.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-09-11 14:21:00 +03:00
co-authored by Claude Opus 4.6
parent f1cc15f70f
commit 48dfd82832
3 changed files with 179 additions and 6 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ so later rebinding would only ever be seen by the module that did it.
import configparser
import os
VERSION = "0.0.0.071"
VERSION = "0.0.0.072"
PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties")
DEFAULT_GRID_SIZE = 10
DEFAULT_ADD_TIMESTAMP = True
+25 -5
View File
@@ -37,8 +37,14 @@ TICK_BLOCKS = [
(2, 5), (3, 6), (4, 7), (5, 6), (6, 5), (7, 4), (8, 3), (9, 2),
]
# The cross (X), as filled 2x2 blocks: two diagonal strokes.
CROSS_BLOCKS = [
(2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8), (9, 9),
(9, 2), (8, 3), (7, 4), (6, 5), (5, 6), (4, 7), (3, 8), (2, 9),
]
def make_checkbox_image(checked, enabled):
def make_checkbox_image(checked, enabled, cross=False):
"""A checkbox drawn pixel by pixel, so a locked tick can be greyed out.
Tk has no disabled state for a cell, and a row tag would grey the whole row including
@@ -56,7 +62,10 @@ def make_checkbox_image(checked, enabled):
image.put(border, to=(0, 0, 1, CHECKBOX_SIZE))
image.put(border, to=(CHECKBOX_SIZE - 1, 0, CHECKBOX_SIZE, CHECKBOX_SIZE))
if checked:
if cross:
for x, y in CROSS_BLOCKS:
image.put("#b04040", to=(x, y, x + 2, y + 2))
elif checked:
for x, y in TICK_BLOCKS:
image.put(ink, to=(x, y, x + 2, y + 2))
@@ -134,6 +143,10 @@ def collect_rows():
# clearest place to see that the file points at something missing.
others = [name for name in installed if name not in declared_names]
bad = palette.unsupported_kits()
compatible = [name for name in others if name not in bad]
incompatible = [name for name in others if name in bad]
groups = [
("Installed kits", [
(name, checksum) for name, checksum in declared if name in required
@@ -141,7 +154,8 @@ def collect_rows():
("Installed but not used kits", [
(name, checksum) for name, checksum in declared if name not in required
]),
("Kits available on the computer", [(name, None) for name in others]),
("Kits available on the computer", [(name, None) for name in compatible]),
("Not supported", [(name, None) for name in incompatible]),
]
rows = []
@@ -153,6 +167,7 @@ def collect_rows():
builds = palette.kit_builds(name)
latest = builds[0] if builds else None
is_declared = name in declared_names
is_unsupported = name in bad
options = action_options(name, checksum, is_declared, builds)
if not is_declared:
@@ -168,6 +183,7 @@ def collect_rows():
"kit": name,
"declared": is_declared,
"required": name in required,
"unsupported": is_unsupported,
"checksum": checksum,
"builds": builds,
"latest": latest,
@@ -175,7 +191,6 @@ def collect_rows():
"latest_text": column_label(*latest) if latest else NOT_DECLARED,
"options": options,
"choice": 0,
# The tick reflects the file as it stands: declared kits start ticked.
"checked": is_declared,
})
return rows
@@ -248,6 +263,7 @@ class KitsPanel:
(True, False): make_checkbox_image(True, False),
(False, False): make_checkbox_image(False, False),
}
self.cross_box = make_checkbox_image(False, False, cross=True)
hint = ttk.Label(
self.frame,
@@ -370,6 +386,8 @@ class KitsPanel:
return (row["kit"], row["installed_text"], row["latest_text"], self._action_text(row))
def _row_image(self, row):
if row.get("unsupported"):
return self.cross_box
return self.boxes[(row["checked"], not row["required"])]
def _rebuild_table(self):
@@ -403,7 +421,7 @@ class KitsPanel:
def _toggle_check(self, item):
row = self.row_by_item[item]
if row["required"]:
if row["required"] or row.get("unsupported"):
return
row["checked"] = not row["checked"]
@@ -420,6 +438,8 @@ class KitsPanel:
def _edit_action(self, item):
row = self.row_by_item[item]
if row.get("unsupported"):
return
cell = self.table.bbox(item, "action")
if not cell:
return
+153
View File
@@ -246,9 +246,162 @@ def list_component_types(kit_name):
return sorted(entries)
_unsupported_cache = {} # frozenset(sax kit names) -> set of unsupported kit names
def _platforms_db():
return os.path.join(app_state.SEDONA_HOME, "platforms", "db")
def _platform_native_kits():
"""Return (matching, all_platforms) — two sets of kit names.
*matching* is the union of nativeKit names from platform builds that include
every PlatformService-providing kit the SAX declares. *all_platforms* is the
union across every build in the database regardless of match.
A kit that appears in *all_platforms* but not in *matching* has native code
compiled for a different device and cannot work with this SAX.
"""
sax_names = set(app_state.active_schema_kits.keys())
db = _platforms_db()
if not os.path.isdir(db):
return None, None
sax_platform_kits = set()
for name in sax_names:
kit_dir = os.path.join(_manifests_dir(), name)
if not os.path.isdir(kit_dir):
continue
for entry in os.listdir(kit_dir):
if not (entry.startswith(f"{name}-") and entry.endswith(".xml")):
continue
try:
tree = ET.parse(os.path.join(kit_dir, entry))
except ET.ParseError:
continue
for t in tree.getroot().findall(".//type"):
text = t.get("name", "") + " " + t.get("base", "")
if "PlatformService" in text:
sax_platform_kits.add(name)
break
if not sax_platform_kits:
return None, None
matching = set()
all_platforms = set()
found = False
for vendor in os.listdir(db):
vd = os.path.join(db, vendor)
if not os.path.isdir(vd):
continue
for device in os.listdir(vd):
dd = os.path.join(vd, device)
if not os.path.isdir(dd):
continue
for build in os.listdir(dd):
mf = os.path.join(dd, build, ".par", "platformManifest.xml")
if not os.path.isfile(mf):
continue
try:
tree = ET.parse(mf)
except ET.ParseError:
continue
nk_names = set()
for nk in tree.getroot().findall(".//nativeKit"):
nk_names.add(nk.get("depend", "").split()[0])
all_platforms.update(nk_names)
if sax_platform_kits <= nk_names:
matching.update(nk_names)
found = True
return (matching if found else None), all_platforms
def unsupported_kits():
"""Kit names that cannot work with the open file's platform.
A kit is unsupported when:
- it has native code (hasNatives or listed as nativeKit on some platform)
but is not compiled into any build matching the SAX's device, or
- it provides a PlatformService type for a different device.
Kits that depend on an unsupported kit are also flagged.
"""
sax_names = frozenset(app_state.active_schema_kits.keys())
if sax_names in _unsupported_cache:
return _unsupported_cache[sax_names]
matching_natives, all_natives = _platform_native_kits()
if matching_natives is None:
_unsupported_cache[sax_names] = set()
return set()
unsupported = set()
all_kits = list_kits()
for name in all_kits:
if name in sax_names:
continue
# Native on another platform but not on the matching one.
if name in all_natives and name not in matching_natives:
unsupported.add(name)
continue
kit_dir = os.path.join(_manifests_dir(), name)
if not os.path.isdir(kit_dir):
continue
for entry in os.listdir(kit_dir):
if not (entry.startswith(f"{name}-") and entry.endswith(".xml")):
continue
try:
tree = ET.parse(os.path.join(kit_dir, entry))
except ET.ParseError:
continue
root = tree.getroot()
has_natives = root.get("hasNatives") == "true"
has_platform_type = any(
"PlatformService" in (t.get("name", "") + " " + t.get("base", ""))
for t in root.findall(".//type")
)
if (has_natives or has_platform_type) and name not in matching_natives:
unsupported.add(name)
break
# Transitive: kits that depend on an unsupported kit
changed = True
while changed:
changed = False
for name in all_kits:
if name in unsupported or name in sax_names:
continue
kit_dir = os.path.join(_manifests_dir(), name)
if not os.path.isdir(kit_dir):
continue
for entry in os.listdir(kit_dir):
if not (entry.startswith(f"{name}-") and entry.endswith(".xml")):
continue
try:
tree = ET.parse(os.path.join(kit_dir, entry))
except ET.ParseError:
continue
for dep in tree.getroot().findall(".//depend"):
dep_name = dep.get("on", "").split()[0]
if dep_name in unsupported:
unsupported.add(name)
changed = True
break
_unsupported_cache[sax_names] = unsupported
return unsupported
def invalidate():
"""Clears every cache, for when Sedona home changes or a kit is repinned."""
_versions_cache.clear()
_manifest_cache.clear()
_component_cache.clear()
_builds_cache.clear()
_unsupported_cache.clear()