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:
+1
-1
@@ -8,7 +8,7 @@ so later rebinding would only ever be seen by the module that did it.
|
|||||||
import configparser
|
import configparser
|
||||||
import os
|
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")
|
PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties")
|
||||||
DEFAULT_GRID_SIZE = 10
|
DEFAULT_GRID_SIZE = 10
|
||||||
DEFAULT_ADD_TIMESTAMP = True
|
DEFAULT_ADD_TIMESTAMP = True
|
||||||
|
|||||||
+25
-5
@@ -37,8 +37,14 @@ TICK_BLOCKS = [
|
|||||||
(2, 5), (3, 6), (4, 7), (5, 6), (6, 5), (7, 4), (8, 3), (9, 2),
|
(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.
|
"""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
|
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=(0, 0, 1, CHECKBOX_SIZE))
|
||||||
image.put(border, to=(CHECKBOX_SIZE - 1, 0, CHECKBOX_SIZE, 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:
|
for x, y in TICK_BLOCKS:
|
||||||
image.put(ink, to=(x, y, x + 2, y + 2))
|
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.
|
# clearest place to see that the file points at something missing.
|
||||||
others = [name for name in installed if name not in declared_names]
|
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 = [
|
groups = [
|
||||||
("Installed kits", [
|
("Installed kits", [
|
||||||
(name, checksum) for name, checksum in declared if name in required
|
(name, checksum) for name, checksum in declared if name in required
|
||||||
@@ -141,7 +154,8 @@ def collect_rows():
|
|||||||
("Installed but not used kits", [
|
("Installed but not used kits", [
|
||||||
(name, checksum) for name, checksum in declared if name not in required
|
(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 = []
|
rows = []
|
||||||
@@ -153,6 +167,7 @@ def collect_rows():
|
|||||||
builds = palette.kit_builds(name)
|
builds = palette.kit_builds(name)
|
||||||
latest = builds[0] if builds else None
|
latest = builds[0] if builds else None
|
||||||
is_declared = name in declared_names
|
is_declared = name in declared_names
|
||||||
|
is_unsupported = name in bad
|
||||||
options = action_options(name, checksum, is_declared, builds)
|
options = action_options(name, checksum, is_declared, builds)
|
||||||
|
|
||||||
if not is_declared:
|
if not is_declared:
|
||||||
@@ -168,6 +183,7 @@ def collect_rows():
|
|||||||
"kit": name,
|
"kit": name,
|
||||||
"declared": is_declared,
|
"declared": is_declared,
|
||||||
"required": name in required,
|
"required": name in required,
|
||||||
|
"unsupported": is_unsupported,
|
||||||
"checksum": checksum,
|
"checksum": checksum,
|
||||||
"builds": builds,
|
"builds": builds,
|
||||||
"latest": latest,
|
"latest": latest,
|
||||||
@@ -175,7 +191,6 @@ def collect_rows():
|
|||||||
"latest_text": column_label(*latest) if latest else NOT_DECLARED,
|
"latest_text": column_label(*latest) if latest else NOT_DECLARED,
|
||||||
"options": options,
|
"options": options,
|
||||||
"choice": 0,
|
"choice": 0,
|
||||||
# The tick reflects the file as it stands: declared kits start ticked.
|
|
||||||
"checked": is_declared,
|
"checked": is_declared,
|
||||||
})
|
})
|
||||||
return rows
|
return rows
|
||||||
@@ -248,6 +263,7 @@ class KitsPanel:
|
|||||||
(True, False): make_checkbox_image(True, False),
|
(True, False): make_checkbox_image(True, False),
|
||||||
(False, False): make_checkbox_image(False, False),
|
(False, False): make_checkbox_image(False, False),
|
||||||
}
|
}
|
||||||
|
self.cross_box = make_checkbox_image(False, False, cross=True)
|
||||||
|
|
||||||
hint = ttk.Label(
|
hint = ttk.Label(
|
||||||
self.frame,
|
self.frame,
|
||||||
@@ -370,6 +386,8 @@ class KitsPanel:
|
|||||||
return (row["kit"], row["installed_text"], row["latest_text"], self._action_text(row))
|
return (row["kit"], row["installed_text"], row["latest_text"], self._action_text(row))
|
||||||
|
|
||||||
def _row_image(self, row):
|
def _row_image(self, row):
|
||||||
|
if row.get("unsupported"):
|
||||||
|
return self.cross_box
|
||||||
return self.boxes[(row["checked"], not row["required"])]
|
return self.boxes[(row["checked"], not row["required"])]
|
||||||
|
|
||||||
def _rebuild_table(self):
|
def _rebuild_table(self):
|
||||||
@@ -403,7 +421,7 @@ class KitsPanel:
|
|||||||
|
|
||||||
def _toggle_check(self, item):
|
def _toggle_check(self, item):
|
||||||
row = self.row_by_item[item]
|
row = self.row_by_item[item]
|
||||||
if row["required"]:
|
if row["required"] or row.get("unsupported"):
|
||||||
return
|
return
|
||||||
|
|
||||||
row["checked"] = not row["checked"]
|
row["checked"] = not row["checked"]
|
||||||
@@ -420,6 +438,8 @@ class KitsPanel:
|
|||||||
|
|
||||||
def _edit_action(self, item):
|
def _edit_action(self, item):
|
||||||
row = self.row_by_item[item]
|
row = self.row_by_item[item]
|
||||||
|
if row.get("unsupported"):
|
||||||
|
return
|
||||||
cell = self.table.bbox(item, "action")
|
cell = self.table.bbox(item, "action")
|
||||||
if not cell:
|
if not cell:
|
||||||
return
|
return
|
||||||
|
|||||||
+153
@@ -246,9 +246,162 @@ def list_component_types(kit_name):
|
|||||||
return sorted(entries)
|
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():
|
def invalidate():
|
||||||
"""Clears every cache, for when Sedona home changes or a kit is repinned."""
|
"""Clears every cache, for when Sedona home changes or a kit is repinned."""
|
||||||
_versions_cache.clear()
|
_versions_cache.clear()
|
||||||
_manifest_cache.clear()
|
_manifest_cache.clear()
|
||||||
_component_cache.clear()
|
_component_cache.clear()
|
||||||
_builds_cache.clear()
|
_builds_cache.clear()
|
||||||
|
_unsupported_cache.clear()
|
||||||
|
|||||||
Reference in New Issue
Block a user