From 48dfd82832024c4036a8cf1ff7f293589d9ae584 Mon Sep 17 00:00:00 2001 From: Arda Aydin Date: Fri, 11 Sep 2026 14:21:00 +0300 Subject: [PATCH] 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 --- app_state.py | 2 +- manage_kits.py | 30 ++++++++-- palette.py | 153 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 179 insertions(+), 6 deletions(-) diff --git a/app_state.py b/app_state.py index 09d74a2..7178c39 100644 --- a/app_state.py +++ b/app_state.py @@ -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 diff --git a/manage_kits.py b/manage_kits.py index 22cff04..6b0c642 100644 --- a/manage_kits.py +++ b/manage_kits.py @@ -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 diff --git a/palette.py b/palette.py index 796b440..663eaea 100644 --- a/palette.py +++ b/palette.py @@ -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()