Put with Files: choose kits, as Manage Kits does, and rebuild kits.scode
A kits step after the zip is chosen, laid out as the Manage Kits panel: installed kits (what the app needs, greyed), installed but not used, kits available on the computer, not supported; the Action cell offers Keep at / Upgrade to / Downgrade to / Add at / REMOVE; Upgrade all kits to latest version. Kits not in the backup can be added, with their dependencies. The ticked kits make app.sab's schema (encode_sax takes the kit list, as Ontrol's PutApp rewrites it). When they are not the device's kits, or Rebuild kits.scode is ticked, kits.scode is built for them with Ontrol's sedonac from the Niagara install, after checking the natives the kits call against the device's platform manifest (PutApp.checkNativeCompatibility). - kit_build.py: platform manifest lookup, native check, sedonac run - palette.kit_versions: a checksum released more than once stands for its highest version (ontrolPlatform 1.0.1, not 1.0.0) - palette.unsupported_kits takes the kit list to judge against - sab_app: a type from a kit the app does not list is a clear error Tested on a P-ION: a rebuild for its own kits is byte-identical to the one it runs; adding and then removing the logic kit both come back running. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+265
-83
@@ -26,7 +26,10 @@ import re
|
|||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
import app_state
|
import app_state
|
||||||
|
import kit_build
|
||||||
|
import palette
|
||||||
import sab_app
|
import sab_app
|
||||||
|
import schema
|
||||||
import sox
|
import sox
|
||||||
from ox_widget_properties import IMAGE_EXTENSIONS, ORION_KIT, ORION_PLATFORM_TYPE, RION_KIT
|
from ox_widget_properties import IMAGE_EXTENSIONS, ORION_KIT, ORION_PLATFORM_TYPE, RION_KIT
|
||||||
|
|
||||||
@@ -64,8 +67,16 @@ _PLATFORMS = (_ORION, _RION, _PION)
|
|||||||
|
|
||||||
|
|
||||||
def platform_of(app):
|
def platform_of(app):
|
||||||
kits = app.kit_names()
|
return platform_of_kits(app.kit_names())
|
||||||
return next((p for p in _PLATFORMS if p.kit in kits), None)
|
|
||||||
|
|
||||||
|
def platform_of_kits(kit_names):
|
||||||
|
kit_names = set(kit_names)
|
||||||
|
return next((p for p in _PLATFORMS if p.kit in kit_names), None)
|
||||||
|
|
||||||
|
|
||||||
|
def platform_label(platform):
|
||||||
|
return platform.name if platform else "no platform this editor knows"
|
||||||
|
|
||||||
|
|
||||||
def backup_folder():
|
def backup_folder():
|
||||||
@@ -278,7 +289,8 @@ def get_with_file(client, path, status=None):
|
|||||||
# --- Put with Files ---------------------------------------------------------------------
|
# --- Put with Files ---------------------------------------------------------------------
|
||||||
#
|
#
|
||||||
# Follows Ontrol's Workbench tool (ontrolSedonaOx BPutWithFilesTool, ontrolSedonanet
|
# Follows Ontrol's Workbench tool (ontrolSedonaOx BPutWithFilesTool, ontrolSedonanet
|
||||||
# PutApp): the backup's app.sax is encoded against the device's kits; app.sab goes as
|
# PutApp): the backup's app.sax is encoded for the chosen kit builds, kits.scode built
|
||||||
|
# for them with sedonac when they are not the device's (kit_build); both go as
|
||||||
# '.writing' renamed to '.stage'; the platform service's deleteFile clears fonts/* and
|
# '.writing' renamed to '.stage'; the platform service's deleteFile clears fonts/* and
|
||||||
# images/*; then the images, the fonts and the boot logo go. Fonts come from Sedona
|
# images/*; then the images, the fonts and the boot logo go. Fonts come from Sedona
|
||||||
# home's bank in the cut the device draws with, images in the form its screen takes.
|
# home's bank in the cut the device draws with, images in the form its screen takes.
|
||||||
@@ -302,25 +314,13 @@ def kits_fit(app_kits, device_kits):
|
|||||||
return all(any(d == (n, _checksum(c)) for d in device) for n, c in app_kits)
|
return all(any(d == (n, _checksum(c)) for d in device) for n, c in app_kits)
|
||||||
|
|
||||||
|
|
||||||
def kit_differences(app_kits, device_kits):
|
|
||||||
"""Lines telling why an app's kits do not fit the device's."""
|
|
||||||
on_device = {n: _checksum(c) for n, c in device_kits}
|
|
||||||
lines = []
|
|
||||||
for name, cks in app_kits:
|
|
||||||
cks = _checksum(cks)
|
|
||||||
if name not in on_device:
|
|
||||||
lines.append(f"{name}: not on the device")
|
|
||||||
elif on_device[name] != cks:
|
|
||||||
lines.append(f"{name}: {on_device[name]:08x} on the device, {cks:08x} in the backup")
|
|
||||||
return lines or ["The same kits, in a different order"]
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceTarget:
|
class DeviceTarget:
|
||||||
"""What a put needs to know of the connected device."""
|
"""What a put needs to know of the connected device."""
|
||||||
|
|
||||||
def __init__(self, platform, kits, name=None, portrait=False,
|
def __init__(self, platform, kits, name=None, portrait=False,
|
||||||
platform_comp_id=None, delete_slot_id=None):
|
platform_comp_id=None, delete_slot_id=None, platform_id=""):
|
||||||
self.platform = platform # one of _PLATFORMS, or None
|
self.platform = platform # one of _PLATFORMS, or None
|
||||||
|
self.platform_id = platform_id # e.g. "ontrol-pion-b427a5b9"
|
||||||
self.kits = kits # [(name, checksum)] in kit-id order
|
self.kits = kits # [(name, checksum)] in kit-id order
|
||||||
self.name = name # the app's deviceName
|
self.name = name # the app's deviceName
|
||||||
self.portrait = portrait # Orion's screenOrientation
|
self.portrait = portrait # Orion's screenOrientation
|
||||||
@@ -353,7 +353,7 @@ def device_target(ds):
|
|||||||
platform = (next((p for p in _PLATFORMS if platform_id.startswith(p.platform_id)), None)
|
platform = (next((p for p in _PLATFORMS if platform_id.startswith(p.platform_id)), None)
|
||||||
or next((p for p in _PLATFORMS if p.kit in kit_names), None))
|
or next((p for p in _PLATFORMS if p.kit in kit_names), None))
|
||||||
|
|
||||||
target = DeviceTarget(platform, kits)
|
target = DeviceTarget(platform, kits, platform_id=client.read_version().platform_id)
|
||||||
comp = client.cache_get(0)
|
comp = client.cache_get(0)
|
||||||
if comp is not None:
|
if comp is not None:
|
||||||
client.update([comp], sox.CONFIG)
|
client.update([comp], sox.CONFIG)
|
||||||
@@ -450,8 +450,8 @@ class PutPlan:
|
|||||||
self.problems = [] # any of these stops the put
|
self.problems = [] # any of these stops the put
|
||||||
self.warnings = [] # worth confirming, but the put can go on
|
self.warnings = [] # worth confirming, but the put can go on
|
||||||
self.app_source = "" # how app.sab was come by, for the summary
|
self.app_source = "" # how app.sab was come by, for the summary
|
||||||
self.reencoded = [] # kits app.sax was encoded against other builds of
|
self.kit_changes = [] # each chosen kit build that is not the device's
|
||||||
self.kit_changes = [] # set when the backup's kits.scode has to go too
|
self.scode_source = "" # where kits.scode comes from, when it goes
|
||||||
self.app_files = [] # PutFile to '<name>.writing', renamed to '.stage'
|
self.app_files = [] # PutFile to '<name>.writing', renamed to '.stage'
|
||||||
self.media = [] # PutFile: images, fonts, logo
|
self.media = [] # PutFile: images, fonts, logo
|
||||||
self.converted = 0 # images made from the backup's image bank
|
self.converted = 0 # images made from the backup's image bank
|
||||||
@@ -466,39 +466,176 @@ class PutPlan:
|
|||||||
return not self.problems
|
return not self.problems
|
||||||
|
|
||||||
|
|
||||||
def _label(platform):
|
|
||||||
return platform.name if platform else "no platform this editor knows"
|
|
||||||
|
|
||||||
|
|
||||||
def _app_portrait(app):
|
def _app_portrait(app):
|
||||||
comp = next((c for c in app.comps.values() if c["type"] == ORION_PLATFORM_TYPE), None)
|
comp = next((c for c in app.comps.values() if c["type"] == ORION_PLATFORM_TYPE), None)
|
||||||
return bool(comp and app.prop(comp, "screenOrientation", False))
|
return bool(comp and app.prop(comp, "screenOrientation", False))
|
||||||
|
|
||||||
|
|
||||||
def _sax_kits(sax):
|
class KitRow:
|
||||||
|
"""One kit in the kits step of a put: the device's build, the backup's, and what to put.
|
||||||
|
|
||||||
|
`included` rows are what goes: into app.sab's schema and, when it is rebuilt, into
|
||||||
|
kits.scode. A `required` row -- a kit the app's components name, or one an included
|
||||||
|
kit depends on -- is always included.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name, device, backup, builds, used=False, locked=False,
|
||||||
|
unsupported=False):
|
||||||
|
self.name = name
|
||||||
|
self.device = device # checksum hex the device runs, or None
|
||||||
|
self.backup = backup # checksum hex the backup's schema names, or None
|
||||||
|
self.builds = builds # [(version, checksum hex)] Sedona home has, newest first
|
||||||
|
self.used = used # the app's components name a type of it
|
||||||
|
self.locked = locked # the backup holds only app.sab: nothing can change
|
||||||
|
self.unsupported = unsupported # cannot run on the device's platform
|
||||||
|
self.required = used
|
||||||
|
self.included = used or (device is not None and not unsupported) or \
|
||||||
|
(backup is not None and not unsupported)
|
||||||
|
self.use = self._default() # the chosen checksum hex
|
||||||
|
|
||||||
|
def _default(self):
|
||||||
|
"""The device's build, when Sedona home can describe it; then the backup's; then
|
||||||
|
the newest."""
|
||||||
|
known = {c for _, c in self.builds}
|
||||||
|
if self.locked:
|
||||||
|
return self.backup
|
||||||
|
for checksum in (self.device, self.backup):
|
||||||
|
if checksum in known:
|
||||||
|
return checksum
|
||||||
|
return self.builds[0][1] if self.builds else self.device or self.backup
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latest(self):
|
||||||
|
return self.builds[0][1] if self.builds else None
|
||||||
|
|
||||||
|
|
||||||
|
def _hex(checksum):
|
||||||
|
return f"{_checksum(checksum):08x}"
|
||||||
|
|
||||||
|
|
||||||
|
def _app_source(z, names):
|
||||||
|
"""(sax entry or None, sab entry or None) of the backup's app."""
|
||||||
|
sax = "app.sax" if "app.sax" in names else next(
|
||||||
|
(n for n in sorted(names) if n.endswith(".sax") and "/" not in n), None)
|
||||||
|
sab = "device/" + APP_SAB
|
||||||
|
return sax, sab if sab in names else None
|
||||||
|
|
||||||
|
|
||||||
|
def _backup_kits(path):
|
||||||
|
"""([(kit, checksum or None)] the backup's schema names, {kits its components use},
|
||||||
|
whether it holds only app.sab). Raises ValueError when the zip holds no app."""
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
schema_elem = ET.fromstring(sax).find("schema")
|
with zipfile.ZipFile(path) as z:
|
||||||
return [] if schema_elem is None else [k.get("name") for k in schema_elem.findall("kit")]
|
names = {n for n in z.namelist() if not n.endswith("/")}
|
||||||
|
sax_name, sab_name = _app_source(z, names)
|
||||||
|
if sax_name:
|
||||||
|
root = ET.fromstring(z.read(sax_name))
|
||||||
|
schema_elem = root.find("schema")
|
||||||
|
kits = [(k.get("name"), k.get("checksum"))
|
||||||
|
for k in (schema_elem.findall("kit") if schema_elem is not None else ())]
|
||||||
|
used = {c.get("type", "").split("::")[0] for c in root.iter("comp")}
|
||||||
|
return kits, used | {"sys"}, False
|
||||||
|
if sab_name:
|
||||||
|
app = sab_app.SabApp(z.read(sab_name))
|
||||||
|
used = {c["type"].split("::")[0] for c in app.comps.values()}
|
||||||
|
return app.kit_parts, used, True
|
||||||
|
raise ValueError("The zip holds no app: neither app.sax nor device/app.sab.")
|
||||||
|
|
||||||
|
|
||||||
def plan_put(path, target):
|
def kit_rows(path, target):
|
||||||
"""Reads the zip at `path` and checks it against the device `target`."""
|
"""Every kit the kits step lists, in Manage Kits' groups (see KitRow.group).
|
||||||
|
|
||||||
|
The kits the backup's app needs, those its schema or the device holds without the
|
||||||
|
app needing them, and every other kit Sedona home has. Each defaults to the device's
|
||||||
|
build. Raises ValueError when the zip holds no readable app.
|
||||||
|
"""
|
||||||
|
kits, used, locked = _backup_kits(path)
|
||||||
|
device = {n: _hex(c) for n, c in target.kits}
|
||||||
|
backup = {n: _hex(c) if c else None for n, c in kits}
|
||||||
|
names = list(dict.fromkeys([n for n, _ in kits] + list(device) + palette.list_kits()))
|
||||||
|
unsupported = palette.unsupported_kits(set(backup) | set(device))
|
||||||
|
rows = []
|
||||||
|
for name in names:
|
||||||
|
builds = [(v, _hex(c)) for v, c in palette.kit_builds(name)]
|
||||||
|
if locked and name not in backup:
|
||||||
|
continue # nothing can be added to a bare app.sab
|
||||||
|
rows.append(KitRow(name, device.get(name), backup.get(name), builds,
|
||||||
|
used=name in used, locked=locked,
|
||||||
|
unsupported=name in unsupported and name not in used))
|
||||||
|
settle_required(rows)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def settle_required(rows):
|
||||||
|
"""Marks every kit an included kit depends on as required -- and so included, at its
|
||||||
|
default build -- all the way down, as Manage Kits counts dependencies."""
|
||||||
|
by_name = {r.name: r for r in rows}
|
||||||
|
for row in rows:
|
||||||
|
row.required = row.used
|
||||||
|
pending = [r for r in rows if r.included or r.used]
|
||||||
|
while pending:
|
||||||
|
row = pending.pop()
|
||||||
|
for dep in schema.read_kit_dependencies(row.name, row.use):
|
||||||
|
dep_row = by_name.get(dep)
|
||||||
|
if dep_row is not None and not dep_row.required:
|
||||||
|
dep_row.required = dep_row.included = True
|
||||||
|
pending.append(dep_row)
|
||||||
|
for row in rows:
|
||||||
|
if row.used:
|
||||||
|
row.included = True
|
||||||
|
|
||||||
|
|
||||||
|
KIT_GROUPS = ("Installed kits", "Installed but not used kits",
|
||||||
|
"Kits available on the computer", "Not supported")
|
||||||
|
|
||||||
|
|
||||||
|
def group(row):
|
||||||
|
"""The Manage Kits section a row is listed under: the app needs it; the device or
|
||||||
|
the backup holds it without the app needing it; Sedona home has it; or it cannot run
|
||||||
|
on the device."""
|
||||||
|
if row.unsupported:
|
||||||
|
return KIT_GROUPS[3]
|
||||||
|
if row.required:
|
||||||
|
return KIT_GROUPS[0]
|
||||||
|
if row.device or row.backup:
|
||||||
|
return KIT_GROUPS[1]
|
||||||
|
return KIT_GROUPS[2]
|
||||||
|
|
||||||
|
|
||||||
|
def chosen_kits(rows):
|
||||||
|
"""[(name, checksum hex)] of the included rows."""
|
||||||
|
return [(r.name, r.use) for r in rows if r.included]
|
||||||
|
|
||||||
|
|
||||||
|
def plan_put(path, target, rows=None, rebuild=False):
|
||||||
|
"""Reads the zip at `path` and checks it against the device `target`.
|
||||||
|
|
||||||
|
rows are kit_rows() as the kits step left them; None takes the defaults. The
|
||||||
|
included kits make app.sab's schema; kits.scode is built for them when they are not
|
||||||
|
the device's kits, or when `rebuild` asks for it anyway.
|
||||||
|
"""
|
||||||
plan = PutPlan(path)
|
plan = PutPlan(path)
|
||||||
try:
|
try:
|
||||||
z = zipfile.ZipFile(path)
|
z = zipfile.ZipFile(path)
|
||||||
except (OSError, zipfile.BadZipFile) as e:
|
except (OSError, zipfile.BadZipFile) as e:
|
||||||
plan.problems.append(f"Not a readable zip: {e}")
|
plan.problems.append(f"Not a readable zip: {e}")
|
||||||
return plan
|
return plan
|
||||||
|
if rows is None:
|
||||||
|
try:
|
||||||
|
rows = kit_rows(path, target)
|
||||||
|
except Exception as e:
|
||||||
|
plan.problems.append(str(e))
|
||||||
|
return plan
|
||||||
with z:
|
with z:
|
||||||
names = {n for n in z.namelist() if not n.endswith("/")}
|
names = {n for n in z.namelist() if not n.endswith("/")}
|
||||||
app, backup_portrait = _plan_app(plan, z, names, target)
|
app, backup_portrait = _plan_app(plan, z, names, target, rows, rebuild)
|
||||||
if app is None:
|
if app is None:
|
||||||
return plan
|
return plan
|
||||||
|
|
||||||
platform = platform_of(app)
|
platform = platform_of(app)
|
||||||
if platform is None or platform is not target.platform:
|
if platform is None or platform is not target.platform:
|
||||||
plan.problems.append(f"The backup is from {_label(platform)}; the device is "
|
plan.problems.append(f"The backup is from {platform_label(platform)}; the device "
|
||||||
f"{_label(target.platform)}.")
|
f"is {platform_label(target.platform)}.")
|
||||||
return plan
|
return plan
|
||||||
|
|
||||||
zip_name = app.prop(app.app, "deviceName", "") or ""
|
zip_name = app.prop(app.app, "deviceName", "") or ""
|
||||||
@@ -524,63 +661,108 @@ def plan_put(path, target):
|
|||||||
return plan
|
return plan
|
||||||
|
|
||||||
|
|
||||||
def _plan_app(plan, z, names, target):
|
def _plan_app(plan, z, names, target, rows, rebuild):
|
||||||
"""Settles app.sab, Ontrol's way first: the backup's app.sax encoded against the
|
"""Settles app.sab and kits.scode, as Ontrol's PutApp does.
|
||||||
device's kits. Failing that, the backup's own app.sab when the device's kits can
|
|
||||||
load it, or with the backup's kits.scode. Returns (decoded app, its portrait)."""
|
|
||||||
sax_name = "app.sax" if "app.sax" in names else next(
|
|
||||||
(n for n in sorted(names) if n.endswith(".sax") and "/" not in n), None)
|
|
||||||
sab_name, scode_name = "device/" + APP_SAB, "device/" + KITS_SCODE
|
|
||||||
device_cks = {n: f"{_checksum(c):08x}" for n, c in target.kits}
|
|
||||||
sax_problem = None
|
|
||||||
|
|
||||||
if sax_name:
|
The backup's app.sax is encoded for the chosen kit builds (a backup with only
|
||||||
sax = z.read(sax_name)
|
device/app.sab goes as it is). When the device's kits.scode cannot load that app, or
|
||||||
|
a rebuild is asked for, kits.scode is built for the chosen builds with sedonac after
|
||||||
|
a check of their natives against the device's VM; without sedonac, the backup's own
|
||||||
|
kits.scode can stand in when the chosen builds are the backup's. Returns (decoded
|
||||||
|
app, whether the backup came from a portrait screen).
|
||||||
|
"""
|
||||||
|
sax_name, sab_name = _app_source(z, names)
|
||||||
|
chosen = chosen_kits(rows)
|
||||||
|
original = None
|
||||||
|
if sab_name:
|
||||||
try:
|
try:
|
||||||
missing = [k for k in _sax_kits(sax) if k not in device_cks]
|
original = sab_app.SabApp(z.read(sab_name))
|
||||||
if missing:
|
|
||||||
raise sab_app.SabError("the device lacks the kits " + ", ".join(missing))
|
|
||||||
sab = sab_app.encode_sax(sax, device_cks)
|
|
||||||
app = sab_app.SabApp(sab)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
sax_problem = f"{sax_name} cannot be encoded for this device: {e}"
|
if not sax_name:
|
||||||
else:
|
|
||||||
original = sab_app.SabApp(z.read(sab_name)) if sab_name in names else None
|
|
||||||
if original is not None:
|
|
||||||
plan.reencoded = [line for line in kit_differences(original.kit_parts,
|
|
||||||
target.kits)
|
|
||||||
if "on the device," in line]
|
|
||||||
plan.app_source = f"{sax_name}, encoded for the device's kits"
|
|
||||||
plan.app_files.append(PutFile(APP_SAB, sab, sax_name))
|
|
||||||
return app, _app_portrait(original or app)
|
|
||||||
|
|
||||||
if sab_name not in names:
|
|
||||||
plan.problems.append(sax_problem or "The zip holds no app: neither app.sax nor "
|
|
||||||
f"{sab_name}.")
|
|
||||||
return None, False
|
|
||||||
sab = z.read(sab_name)
|
|
||||||
try:
|
|
||||||
app = sab_app.SabApp(sab)
|
|
||||||
except Exception as e:
|
|
||||||
plan.problems += [p for p in (sax_problem,) if p]
|
|
||||||
plan.problems.append(f"{sab_name} cannot be read: {e}")
|
plan.problems.append(f"{sab_name} cannot be read: {e}")
|
||||||
return None, False
|
return None, False
|
||||||
if kits_fit(app.kit_parts, target.kits):
|
|
||||||
plan.app_source = sab_name
|
if sax_name:
|
||||||
elif scode_name in names:
|
try:
|
||||||
plan.kit_changes = kit_differences(app.kit_parts, target.kits)
|
sab = sab_app.encode_sax(z.read(sax_name), kits=chosen)
|
||||||
plan.app_source = f"{sab_name}, with the backup's {KITS_SCODE}"
|
except Exception as e:
|
||||||
plan.app_files.append(PutFile(KITS_SCODE, z.read(scode_name), scode_name))
|
plan.problems.append(f"{sax_name} cannot be encoded for the chosen kits: {e}")
|
||||||
else:
|
|
||||||
plan.problems += [p for p in (sax_problem,) if p]
|
|
||||||
plan.problems.append(f"The device's kits cannot load {sab_name}, and the backup "
|
|
||||||
f"has no {scode_name}:")
|
|
||||||
plan.problems += [" " + line for line in kit_differences(app.kit_parts, target.kits)]
|
|
||||||
return None, False
|
return None, False
|
||||||
if sax_problem:
|
plan.app_source = f"{sax_name}, encoded for the chosen kits"
|
||||||
plan.warnings.append(sax_problem)
|
source = sax_name
|
||||||
plan.app_files.append(PutFile(APP_SAB, sab, sab_name))
|
else:
|
||||||
return app, _app_portrait(app)
|
sab = z.read(sab_name)
|
||||||
|
plan.app_source = sab_name
|
||||||
|
source = sab_name
|
||||||
|
app = sab_app.SabApp(sab)
|
||||||
|
app_kits = app.kit_parts
|
||||||
|
|
||||||
|
plan.kit_changes = [line for line in map(kit_change, rows) if line]
|
||||||
|
device_kits = {(n, _checksum(c)) for n, c in target.kits}
|
||||||
|
if sax_name:
|
||||||
|
same = {(n, _checksum(c)) for n, c in app_kits} == device_kits
|
||||||
|
else:
|
||||||
|
same = kits_fit(app_kits, target.kits)
|
||||||
|
if rebuild or not same:
|
||||||
|
scode = _plan_scode(plan, z, names, target, app_kits)
|
||||||
|
if scode is None:
|
||||||
|
return None, False
|
||||||
|
plan.app_files.append(scode)
|
||||||
|
plan.app_files.append(PutFile(APP_SAB, sab, source))
|
||||||
|
return app, _app_portrait(original or app)
|
||||||
|
|
||||||
|
|
||||||
|
def kit_change(row):
|
||||||
|
"""How a row changes the device's kits, or None."""
|
||||||
|
def label(checksum):
|
||||||
|
version = palette.version_for_checksum(row.name, checksum)
|
||||||
|
return f"{version} ({checksum})" if version else checksum
|
||||||
|
if row.device is None:
|
||||||
|
return f"{row.name}: added at {label(row.use)}" if row.included else None
|
||||||
|
if not row.included:
|
||||||
|
return f"{row.name}: removed"
|
||||||
|
if row.use != row.device:
|
||||||
|
return f"{row.name}: {label(row.device)} -> {label(row.use)}"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _plan_scode(plan, z, names, target, app_kits):
|
||||||
|
"""kits.scode for the app's kits: built by sedonac, or the backup's own."""
|
||||||
|
manifest = kit_build.platform_manifest(target.platform_id)
|
||||||
|
backup_scode = "device/" + KITS_SCODE
|
||||||
|
backup_kits = None
|
||||||
|
if "device/" + APP_SAB in names:
|
||||||
|
try:
|
||||||
|
backup_kits = sab_app.SabApp(z.read("device/" + APP_SAB)).kit_parts
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
why = None
|
||||||
|
if manifest is None:
|
||||||
|
why = (f"Sedona home has no platform manifest for {target.platform_id or 'the device'}"
|
||||||
|
" (platforms/db), so kits.scode cannot be built or checked for it.")
|
||||||
|
else:
|
||||||
|
problems = kit_build.native_problems(app_kits, manifest)
|
||||||
|
if problems:
|
||||||
|
plan.problems.append("The chosen kits call native methods the device's VM "
|
||||||
|
"does not have:")
|
||||||
|
plan.problems += [" " + p for p in problems]
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
data = kit_build.build_kits_scode(app_kits, manifest)
|
||||||
|
except kit_build.BuildError as e:
|
||||||
|
why = str(e)
|
||||||
|
else:
|
||||||
|
plan.scode_source = "built by sedonac for the chosen kits"
|
||||||
|
return PutFile(KITS_SCODE, data, "sedonac")
|
||||||
|
|
||||||
|
if backup_kits is not None and backup_scode in names and \
|
||||||
|
[(n, _checksum(c)) for n, c in backup_kits] == \
|
||||||
|
[(n, _checksum(c)) for n, c in app_kits]:
|
||||||
|
plan.warnings.append(why + " The backup's own kits.scode goes instead.")
|
||||||
|
plan.scode_source = "the backup's, unchecked against the device's VM"
|
||||||
|
return PutFile(KITS_SCODE, z.read(backup_scode), backup_scode)
|
||||||
|
plan.problems.append(why)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _plan_images(plan, z, names, platform, target, same_screen):
|
def _plan_images(plan, z, names, platform, target, same_screen):
|
||||||
|
|||||||
+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.166"
|
VERSION = "0.0.0.167"
|
||||||
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
|
||||||
|
|||||||
+190
@@ -0,0 +1,190 @@
|
|||||||
|
"""Building a device's kits.scode, as Ontrol's PutApp does it with sedonac.
|
||||||
|
|
||||||
|
The editor carries no Sedona compiler: Ontrol's own sedonac is run through Java, from
|
||||||
|
the Niagara install next to Sedona home (Niagara 4's ontrolSedonanet jars, or Niagara
|
||||||
|
AX's sedona/sedonac jars -- both build the same image). Its input is a kits.xml naming
|
||||||
|
each kit at its version, with the target platform's endian, block and ref sizes taken
|
||||||
|
from the platform manifest in Sedona home's platform db.
|
||||||
|
|
||||||
|
Before a build the kits' calls into native code are checked against the platform's
|
||||||
|
SVM, as PutApp.checkNativeCompatibility does: an scode calling a native the device's
|
||||||
|
VM lacks would not start.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
import app_state
|
||||||
|
import palette
|
||||||
|
|
||||||
|
|
||||||
|
class BuildError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _home():
|
||||||
|
return app_state.SEDONA_HOME
|
||||||
|
|
||||||
|
|
||||||
|
# --- kits -------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def kit_file(name, checksum):
|
||||||
|
"""(path, version) of the .kit for a kit build; the highest version when one checksum
|
||||||
|
was released more than once. None when Sedona home has no such .kit."""
|
||||||
|
want = int(checksum, 16) if isinstance(checksum, str) else checksum & 0xFFFFFFFF
|
||||||
|
pattern = re.compile(rf"^{re.escape(name)}-([0-9a-fA-F]+)-(.+)\.kit$")
|
||||||
|
found = []
|
||||||
|
for path in glob.glob(os.path.join(_home(), "kits", name, f"{name}-*.kit")):
|
||||||
|
match = pattern.match(os.path.basename(path))
|
||||||
|
if match and int(match.group(1), 16) == want:
|
||||||
|
found.append((palette.version_sort_key(match.group(2)), path, match.group(2)))
|
||||||
|
if not found:
|
||||||
|
return None
|
||||||
|
_, path, version = max(found)
|
||||||
|
return path, version
|
||||||
|
|
||||||
|
|
||||||
|
# --- the target platform ----------------------------------------------------------------
|
||||||
|
|
||||||
|
def platform_manifest(platform_id):
|
||||||
|
"""The platformManifest.xml in Sedona home's platform db for a platform id, or None."""
|
||||||
|
if not platform_id:
|
||||||
|
return None
|
||||||
|
# os.walk, not glob: the manifests sit in hidden .par folders glob skips.
|
||||||
|
for folder, _, files in os.walk(os.path.join(_home(), "platforms", "db")):
|
||||||
|
if "platformManifest.xml" not in files:
|
||||||
|
continue
|
||||||
|
path = os.path.join(folder, "platformManifest.xml")
|
||||||
|
try:
|
||||||
|
if ET.parse(path).getroot().get("platformId") == platform_id:
|
||||||
|
return path
|
||||||
|
except ET.ParseError:
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
_CLASS = re.compile(r"^\s*(?:[a-z]+\s+)*class\s+(\w+)", re.M)
|
||||||
|
_NATIVE = re.compile(r"\bnative\b[^=\n]*?\b(\w+)\s*\([^)]*\)\s*=\s*(\d+::\d+)")
|
||||||
|
_CALL = re.compile(r"\bCallNative\w*\s+(\w+::\w+\.\w+)")
|
||||||
|
|
||||||
|
|
||||||
|
def native_problems(kits, manifest_path):
|
||||||
|
"""Lines naming each native the kits call that the platform's SVM does not provide.
|
||||||
|
|
||||||
|
kits is [(name, checksum)]. Read from the kits' .ir files: a native's id from its
|
||||||
|
declaration, the natives in use from the CallNative ops.
|
||||||
|
"""
|
||||||
|
svm = {e.get("id"): e.get("qname")
|
||||||
|
for e in ET.parse(manifest_path).getroot().iter("nativeMethod")}
|
||||||
|
declared, called = {}, set()
|
||||||
|
for name, checksum in kits:
|
||||||
|
found = kit_file(name, checksum)
|
||||||
|
if found is None:
|
||||||
|
continue # reported by the build
|
||||||
|
with zipfile.ZipFile(found[0]) as z:
|
||||||
|
for entry in z.namelist():
|
||||||
|
if not entry.endswith(".ir"):
|
||||||
|
continue
|
||||||
|
text = z.read(entry).decode("utf-8", "replace")
|
||||||
|
classes = [(m.start(), m.group(1)) for m in _CLASS.finditer(text)]
|
||||||
|
for m in _NATIVE.finditer(text):
|
||||||
|
owner = [c for pos, c in classes if pos < m.start()]
|
||||||
|
if owner:
|
||||||
|
declared[f"{name}::{owner[-1]}.{m.group(1)}"] = m.group(2)
|
||||||
|
called.update(_CALL.findall(text))
|
||||||
|
problems = []
|
||||||
|
for qname in sorted(called):
|
||||||
|
nid = declared.get(qname)
|
||||||
|
if nid is None or nid not in svm:
|
||||||
|
problems.append(f"{qname} ({nid or 'not declared'}) is not in the device's VM")
|
||||||
|
elif svm[nid] != qname:
|
||||||
|
problems.append(f"{qname} ({nid}) is {svm[nid]} in the device's VM")
|
||||||
|
return problems
|
||||||
|
|
||||||
|
|
||||||
|
# --- the compiler -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _java():
|
||||||
|
found = shutil.which("java")
|
||||||
|
if found:
|
||||||
|
return found
|
||||||
|
candidates = []
|
||||||
|
if os.environ.get("JAVA_HOME"):
|
||||||
|
candidates.append(os.path.join(os.environ["JAVA_HOME"], "bin", "java.exe"))
|
||||||
|
niagara_root = os.path.dirname(os.path.abspath(_home()))
|
||||||
|
candidates += sorted(glob.glob(os.path.join(niagara_root, "Niagara-*", "jre", "bin",
|
||||||
|
"java.exe")), reverse=True)
|
||||||
|
for root in (os.environ.get("ProgramFiles", r"C:\Program Files"),):
|
||||||
|
candidates += sorted(glob.glob(os.path.join(root, "*", "*", "bin", "java.exe")),
|
||||||
|
reverse=True)
|
||||||
|
return next((c for c in candidates if os.path.isfile(c)), None)
|
||||||
|
|
||||||
|
|
||||||
|
def _classpath():
|
||||||
|
"""sedonac and the sedona runtime from the Niagara install beside Sedona home."""
|
||||||
|
niagara_root = os.path.dirname(os.path.abspath(_home()))
|
||||||
|
for niagara in sorted(glob.glob(os.path.join(niagara_root, "Niagara-*")), reverse=True):
|
||||||
|
modules = os.path.join(niagara, "modules")
|
||||||
|
for jars in (("ontrolSedonanet-rt.jar", "ontrolSedonanet-wb.jar"),
|
||||||
|
("sedona.jar", "sedonac.jar")):
|
||||||
|
paths = [os.path.join(modules, j) for j in jars]
|
||||||
|
if all(os.path.isfile(p) for p in paths):
|
||||||
|
return os.pathsep.join(paths)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def toolchain():
|
||||||
|
"""(java, classpath) for running sedonac; raises BuildError saying what is missing."""
|
||||||
|
java, classpath = _java(), _classpath()
|
||||||
|
if not java:
|
||||||
|
raise BuildError("No Java runtime was found (java on the PATH, JAVA_HOME, or "
|
||||||
|
"Niagara's jre).")
|
||||||
|
if not classpath:
|
||||||
|
raise BuildError("sedonac was not found: no Niagara install beside Sedona home "
|
||||||
|
f"({os.path.dirname(os.path.abspath(_home()))}) has Ontrol's "
|
||||||
|
"ontrolSedonanet jars or sedona.jar + sedonac.jar.")
|
||||||
|
return java, classpath
|
||||||
|
|
||||||
|
|
||||||
|
def build_kits_scode(kits, manifest_path):
|
||||||
|
"""Compiles kits.scode for [(name, checksum)] on the platform; returns its bytes."""
|
||||||
|
plat = ET.parse(manifest_path).getroot()
|
||||||
|
lines = ["<sedonaCode"]
|
||||||
|
for attr in ("endian", "blockSize", "refSize", "debug", "test", "armDouble"):
|
||||||
|
lines.append(f' {attr}="{plat.get(attr)}"')
|
||||||
|
lines += [' main="sys::Sys.main"', ">"]
|
||||||
|
missing = []
|
||||||
|
for name, checksum in kits:
|
||||||
|
found = kit_file(name, checksum)
|
||||||
|
if found is None:
|
||||||
|
missing.append(f"{name} {checksum}")
|
||||||
|
else:
|
||||||
|
lines.append(f' <depend on="{name} {found[1]}=" />')
|
||||||
|
if missing:
|
||||||
|
raise BuildError("Sedona home has no .kit file for " + ", ".join(missing))
|
||||||
|
lines.append("</sedonaCode>")
|
||||||
|
|
||||||
|
java, classpath = toolchain()
|
||||||
|
with tempfile.TemporaryDirectory(prefix="sedona_editor_scode_") as work:
|
||||||
|
with open(os.path.join(work, "kits.xml"), "w", encoding="utf-8") as f:
|
||||||
|
f.write("\n".join(lines) + "\n")
|
||||||
|
try:
|
||||||
|
run = subprocess.run(
|
||||||
|
[java, f"-Dsedona.home={_home()}", "-cp", classpath, "sedonac.Main",
|
||||||
|
"kits.xml"],
|
||||||
|
cwd=work, capture_output=True, text=True, timeout=180,
|
||||||
|
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
|
||||||
|
except (OSError, subprocess.TimeoutExpired) as e:
|
||||||
|
raise BuildError(f"sedonac could not run: {e}") from e
|
||||||
|
scode = os.path.join(work, "kits.scode")
|
||||||
|
if run.returncode != 0 or not os.path.isfile(scode):
|
||||||
|
tail = "\n".join((run.stdout + run.stderr).strip().splitlines()[-12:])
|
||||||
|
raise BuildError(f"sedonac failed:\n{tail}")
|
||||||
|
with open(scode, "rb") as f:
|
||||||
|
return f.read()
|
||||||
@@ -17,6 +17,7 @@ import live_hooks
|
|||||||
import manage_kits
|
import manage_kits
|
||||||
import palette
|
import palette
|
||||||
import properties
|
import properties
|
||||||
|
import put_kits_dialog
|
||||||
import sax_file
|
import sax_file
|
||||||
import schema
|
import schema
|
||||||
import sox
|
import sox
|
||||||
@@ -781,10 +782,11 @@ def _report_backup(result):
|
|||||||
def do_put_with_files():
|
def do_put_with_files():
|
||||||
"""App > Put with Files: puts a Get with Files zip back onto the connected device.
|
"""App > Put with Files: puts a Get with Files zip back onto the connected device.
|
||||||
|
|
||||||
The zip is checked against the device first: the same platform, and an app the
|
As Ontrol's tool does: the kits the app needs are listed to choose builds for (the
|
||||||
device's kits can run -- the backup's app.sax encoded for them, as Ontrol's tool
|
device's by default), the backup's app.sax is encoded for them, and kits.scode is
|
||||||
does, or its app.sab. The app goes staged, so the device takes it up when
|
rebuilt with sedonac when they are not the device's. The zip is checked against the
|
||||||
restarted, which is offered once the files are in.
|
device on the way. The app goes staged, so the device takes it up when restarted,
|
||||||
|
which is offered once the files are in.
|
||||||
"""
|
"""
|
||||||
ds = app_state.device_session
|
ds = app_state.device_session
|
||||||
if not app_state.live_mode or ds is None or not ds.is_connected:
|
if not app_state.live_mode or ds is None or not ds.is_connected:
|
||||||
@@ -808,19 +810,51 @@ def do_put_with_files():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
messagebox.showerror("Put with Files", f"Could not read the device:{NL}{NL}{e}")
|
messagebox.showerror("Put with Files", f"Could not read the device:{NL}{NL}{e}")
|
||||||
return
|
return
|
||||||
plan = app_backup.plan_put(path, target)
|
try:
|
||||||
|
rows = app_backup.kit_rows(path, target)
|
||||||
|
except Exception as e:
|
||||||
|
messagebox.showerror("Put with Files", f"{os.path.basename(path)}:{NL}{NL}{e}")
|
||||||
|
return
|
||||||
|
backup_platform = app_backup.platform_of_kits(r.name for r in rows if r.backup or r.used)
|
||||||
|
if backup_platform is None or backup_platform is not target.platform:
|
||||||
|
messagebox.showerror(
|
||||||
|
"Put with Files",
|
||||||
|
f"{os.path.basename(path)} cannot be put on this device:{NL}{NL}"
|
||||||
|
f"The backup is from {app_backup.platform_label(backup_platform)}; the device "
|
||||||
|
f"is {app_backup.platform_label(target.platform)}.")
|
||||||
|
return
|
||||||
|
rebuild = put_kits_dialog.ask(app_state.window, rows)
|
||||||
|
if rebuild is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
def planned(plan, error):
|
||||||
|
if error is not None:
|
||||||
|
messagebox.showerror("Put with Files", f"Preparing the put failed:{NL}{NL}{error}")
|
||||||
|
return
|
||||||
if not plan.ok:
|
if not plan.ok:
|
||||||
app_state.set_status(f"{os.path.basename(path)} does not match the device", warn=True)
|
app_state.set_status(f"{os.path.basename(path)} cannot be put on the device",
|
||||||
|
warn=True)
|
||||||
messagebox.showerror(
|
messagebox.showerror(
|
||||||
"Put with Files",
|
"Put with Files",
|
||||||
f"{os.path.basename(path)} cannot be put on this device:{NL}{NL}" +
|
f"{os.path.basename(path)} cannot be put on this device:{NL}{NL}" +
|
||||||
NL.join(plan.problems))
|
NL.join(plan.problems))
|
||||||
return
|
return
|
||||||
if not messagebox.askokcancel("Put with Files", _describe_put(plan),
|
if messagebox.askokcancel("Put with Files", _describe_put(plan),
|
||||||
icon=messagebox.WARNING, default=messagebox.CANCEL):
|
icon=messagebox.WARNING, default=messagebox.CANCEL):
|
||||||
return
|
_put(plan)
|
||||||
|
|
||||||
def finish(_result, error):
|
def prepare(status):
|
||||||
|
status("Preparing the app" + (" and building kits.scode" if rebuild or any(
|
||||||
|
map(app_backup.kit_change, rows)) else "") + " ...", None)
|
||||||
|
return app_backup.plan_put(path, target, rows, rebuild)
|
||||||
|
|
||||||
|
def _put(plan):
|
||||||
|
_run_with_progress("Put with Files",
|
||||||
|
lambda status: app_backup.put_with_files(ds.sox_client, plan,
|
||||||
|
target, status),
|
||||||
|
lambda result, error: finish(plan, result, error))
|
||||||
|
|
||||||
|
def finish(plan, _result, error):
|
||||||
if error is not None:
|
if error is not None:
|
||||||
app_state.set_status(f"Put with Files failed: {error}", warn=True)
|
app_state.set_status(f"Put with Files failed: {error}", warn=True)
|
||||||
messagebox.showerror(
|
messagebox.showerror(
|
||||||
@@ -839,10 +873,7 @@ def do_put_with_files():
|
|||||||
if _invoke_app_action("restart"):
|
if _invoke_app_action("restart"):
|
||||||
app_state.set_status(f"Put {os.path.basename(path)}; the device is restarting")
|
app_state.set_status(f"Put {os.path.basename(path)}; the device is restarting")
|
||||||
|
|
||||||
_run_with_progress("Put with Files",
|
_run_with_progress("Put with Files", prepare, planned)
|
||||||
lambda status: app_backup.put_with_files(ds.sox_client, plan, target,
|
|
||||||
status),
|
|
||||||
finish)
|
|
||||||
|
|
||||||
|
|
||||||
def _describe_put(plan):
|
def _describe_put(plan):
|
||||||
@@ -854,12 +885,9 @@ def _describe_put(plan):
|
|||||||
(" Its fonts and images are deleted and replaced by the backup's."
|
(" Its fonts and images are deleted and replaced by the backup's."
|
||||||
if plan.has_screen else ""), "",
|
if plan.has_screen else ""), "",
|
||||||
f" app.sab, from {plan.app_source}"]
|
f" app.sab, from {plan.app_source}"]
|
||||||
lines += [f" {line}" for line in plan.reencoded]
|
if plan.scode_source:
|
||||||
if plan.kit_changes:
|
lines.append(f" kits.scode, {plan.scode_source}")
|
||||||
lines += [" kits.scode, since the device's kits cannot load the app:"]
|
|
||||||
lines += [f" {line}" for line in plan.kit_changes]
|
lines += [f" {line}" for line in plan.kit_changes]
|
||||||
lines += [" The device's firmware must support these kits, or the app will not "
|
|
||||||
"start."]
|
|
||||||
if not plan.has_screen:
|
if not plan.has_screen:
|
||||||
return NL.join(lines + ([""] + plan.warnings if plan.warnings else []))
|
return NL.join(lines + ([""] + plan.warnings if plan.warnings else []))
|
||||||
lines.append(f" {images} images" +
|
lines.append(f" {images} images" +
|
||||||
|
|||||||
+16
-7
@@ -67,7 +67,12 @@ def kit_versions(kit_name):
|
|||||||
for entry in os.listdir(kit_dir):
|
for entry in os.listdir(kit_dir):
|
||||||
match = pattern.match(entry)
|
match = pattern.match(entry)
|
||||||
if match:
|
if match:
|
||||||
versions[_norm_checksum(match.group(1))] = match.group(2)
|
# A checksum released more than once (ontrolPlatform 1.0.0 and 1.0.1)
|
||||||
|
# stands for its highest version, as the devices report it.
|
||||||
|
key, version = _norm_checksum(match.group(1)), match.group(2)
|
||||||
|
if (key not in versions or
|
||||||
|
version_sort_key(version) > version_sort_key(versions[key])):
|
||||||
|
versions[key] = version
|
||||||
|
|
||||||
_versions_cache[kit_name] = versions
|
_versions_cache[kit_name] = versions
|
||||||
return versions
|
return versions
|
||||||
@@ -269,7 +274,7 @@ def _platforms_db():
|
|||||||
return os.path.join(app_state.SEDONA_HOME, "platforms", "db")
|
return os.path.join(app_state.SEDONA_HOME, "platforms", "db")
|
||||||
|
|
||||||
|
|
||||||
def _platform_native_kits():
|
def _platform_native_kits(sax_names=None):
|
||||||
"""Return (matching, all_platforms) — two sets of kit names.
|
"""Return (matching, all_platforms) — two sets of kit names.
|
||||||
|
|
||||||
*matching* is the union of nativeKit names from platform builds that include
|
*matching* is the union of nativeKit names from platform builds that include
|
||||||
@@ -278,8 +283,10 @@ def _platform_native_kits():
|
|||||||
|
|
||||||
A kit that appears in *all_platforms* but not in *matching* has native code
|
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.
|
compiled for a different device and cannot work with this SAX.
|
||||||
|
|
||||||
|
sax_names stands in for the open file's kits when given.
|
||||||
"""
|
"""
|
||||||
sax_names = set(app_state.active_schema_kits.keys())
|
sax_names = set(app_state.active_schema_kits.keys() if sax_names is None else sax_names)
|
||||||
db = _platforms_db()
|
db = _platforms_db()
|
||||||
if not os.path.isdir(db):
|
if not os.path.isdir(db):
|
||||||
return None, None
|
return None, None
|
||||||
@@ -335,8 +342,9 @@ def _platform_native_kits():
|
|||||||
return (matching if found else None), all_platforms
|
return (matching if found else None), all_platforms
|
||||||
|
|
||||||
|
|
||||||
def unsupported_kits():
|
def unsupported_kits(sax_names=None):
|
||||||
"""Kit names that cannot work with the open file's platform.
|
"""Kit names that cannot work with the open file's platform -- or with the platform
|
||||||
|
of the kits in `sax_names`, when given.
|
||||||
|
|
||||||
A kit is unsupported when:
|
A kit is unsupported when:
|
||||||
- it has native code (hasNatives or listed as nativeKit on some platform)
|
- it has native code (hasNatives or listed as nativeKit on some platform)
|
||||||
@@ -345,11 +353,12 @@ def unsupported_kits():
|
|||||||
|
|
||||||
Kits that depend on an unsupported kit are also flagged.
|
Kits that depend on an unsupported kit are also flagged.
|
||||||
"""
|
"""
|
||||||
sax_names = frozenset(app_state.active_schema_kits.keys())
|
sax_names = frozenset(app_state.active_schema_kits.keys() if sax_names is None
|
||||||
|
else sax_names)
|
||||||
if sax_names in _unsupported_cache:
|
if sax_names in _unsupported_cache:
|
||||||
return _unsupported_cache[sax_names]
|
return _unsupported_cache[sax_names]
|
||||||
|
|
||||||
matching_natives, all_natives = _platform_native_kits()
|
matching_natives, all_natives = _platform_native_kits(sax_names)
|
||||||
if matching_natives is None:
|
if matching_natives is None:
|
||||||
_unsupported_cache[sax_names] = set()
|
_unsupported_cache[sax_names] = set()
|
||||||
return set()
|
return set()
|
||||||
|
|||||||
@@ -0,0 +1,242 @@
|
|||||||
|
"""The kits step of App > Put with Files, laid out as the Manage Kits panel is.
|
||||||
|
|
||||||
|
The same groups (kits the app needs, kits declared without being used, kits available,
|
||||||
|
kits not supported), the same tick column and the same Action cell: "Keep at",
|
||||||
|
"Upgrade to", "Downgrade to", "Add at", "REMOVE". Installed is what the device runs;
|
||||||
|
Backup is what the backup's app was saved with.
|
||||||
|
|
||||||
|
What is ticked goes: into app.sab's schema and, when that is not the device's kits,
|
||||||
|
into a kits.scode built for them (Ontrol's PutApp does the same). "Rebuild kits.scode"
|
||||||
|
forces the build when nothing changed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import messagebox, ttk
|
||||||
|
|
||||||
|
import app_backup
|
||||||
|
import manage_kits
|
||||||
|
from manage_kits import NOT_DECLARED, SECTION_TAG, column_label, make_checkbox_image
|
||||||
|
|
||||||
|
REMOVE_ACTION = "REMOVE from device"
|
||||||
|
|
||||||
|
|
||||||
|
def _options(row):
|
||||||
|
"""The Action choices for a row, as (label, checksum) pairs."""
|
||||||
|
if row.device:
|
||||||
|
return [(label, checksum or row.device) for label, checksum in
|
||||||
|
manage_kits.action_options(row.name, row.device, True, row.builds)]
|
||||||
|
return [(f"Add at {manage_kits.build_label(v, c)}", c) for v, c in row.builds]
|
||||||
|
|
||||||
|
|
||||||
|
def _build_text(row, checksum):
|
||||||
|
if not checksum:
|
||||||
|
return NOT_DECLARED
|
||||||
|
version = next((v for v, c in row.builds if c == checksum), "")
|
||||||
|
return column_label(version, checksum)
|
||||||
|
|
||||||
|
|
||||||
|
class _Dialog:
|
||||||
|
def __init__(self, parent, rows, title):
|
||||||
|
self.rows = rows
|
||||||
|
self.result = None
|
||||||
|
self.original = [(r.included, r.use) for r in rows]
|
||||||
|
|
||||||
|
self.top = top = tk.Toplevel(parent)
|
||||||
|
top.title(f"{title} - Kits")
|
||||||
|
top.transient(parent)
|
||||||
|
top.grab_set()
|
||||||
|
top.geometry(f"+{parent.winfo_x() + 60}+{parent.winfo_y() + 40}")
|
||||||
|
frame = ttk.Frame(top, padding=10)
|
||||||
|
frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
columns = ("name", "installed", "backup", "latest", "action")
|
||||||
|
table_frame = ttk.Frame(frame)
|
||||||
|
table_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
self.table = ttk.Treeview(table_frame, columns=columns, show="tree headings",
|
||||||
|
selectmode="browse", height=20)
|
||||||
|
self.table.heading("#0", text="")
|
||||||
|
self.table.column("#0", width=32, minwidth=32, stretch=False, anchor="center")
|
||||||
|
for column, heading, width in [("name", "Kit name", 190),
|
||||||
|
("installed", "Installed", 105),
|
||||||
|
("backup", "Backup", 105),
|
||||||
|
("latest", "Latest", 105),
|
||||||
|
("action", "Action", 215)]:
|
||||||
|
self.table.heading(column, text=heading, anchor="w")
|
||||||
|
self.table.column(column, width=width, minwidth=width, anchor="w", stretch=False)
|
||||||
|
vscroll = ttk.Scrollbar(table_frame, orient=tk.VERTICAL, command=self.table.yview)
|
||||||
|
self.table.configure(yscrollcommand=vscroll.set)
|
||||||
|
vscroll.pack(side=tk.RIGHT, fill=tk.Y)
|
||||||
|
self.table.pack(fill=tk.BOTH, expand=True)
|
||||||
|
self.table.tag_configure(SECTION_TAG, background="#dcdcdc")
|
||||||
|
|
||||||
|
self.boxes = {(checked, enabled): make_checkbox_image(checked, enabled)
|
||||||
|
for checked in (True, False) for enabled in (True, False)}
|
||||||
|
self.cross_box = make_checkbox_image(False, False, cross=True)
|
||||||
|
|
||||||
|
locked = any(r.locked for r in rows)
|
||||||
|
hint = ("The backup holds no app.sax, so its app goes with the kits it was saved "
|
||||||
|
"with." if locked else
|
||||||
|
"A tick means the kit goes to the device. Grey ticks are needed by the app "
|
||||||
|
"and cannot be removed; untick another to remove it, tick one to add it. "
|
||||||
|
"Click the Action cell to choose a build. Installed is the device's build. "
|
||||||
|
"Kits other than the device's mean a new kits.scode.")
|
||||||
|
ttk.Label(frame, text=hint, foreground="#666666", wraplength=780
|
||||||
|
).pack(anchor="w", pady=(8, 0))
|
||||||
|
|
||||||
|
self.rebuild = tk.BooleanVar(value=False)
|
||||||
|
self.rebuild_check = ttk.Checkbutton(frame, text="Rebuild kits.scode",
|
||||||
|
variable=self.rebuild)
|
||||||
|
self.rebuild_check.pack(anchor="w", pady=(6, 0))
|
||||||
|
|
||||||
|
button_row = ttk.Frame(frame)
|
||||||
|
button_row.pack(fill=tk.X, pady=(10, 0))
|
||||||
|
upgrade = ttk.Button(button_row, text="Upgrade all kits to latest version",
|
||||||
|
command=self._upgrade_all)
|
||||||
|
upgrade.pack(side=tk.LEFT)
|
||||||
|
if locked:
|
||||||
|
upgrade.state(["disabled"])
|
||||||
|
ttk.Button(button_row, text="Cancel", command=lambda: self._close(False)
|
||||||
|
).pack(side=tk.RIGHT)
|
||||||
|
ttk.Button(button_row, text="Next", command=lambda: self._close(True)
|
||||||
|
).pack(side=tk.RIGHT, padx=6)
|
||||||
|
|
||||||
|
self.row_by_item = {}
|
||||||
|
self._editor = None
|
||||||
|
self.table.bind("<Button-1>", self._on_click)
|
||||||
|
self.table.bind("<MouseWheel>", lambda _e: self._close_editor(), add="+")
|
||||||
|
top.protocol("WM_DELETE_WINDOW", lambda: self._close(False))
|
||||||
|
self._rebuild_table()
|
||||||
|
|
||||||
|
# --- the table ---
|
||||||
|
|
||||||
|
def _action_text(self, row):
|
||||||
|
if row.unsupported:
|
||||||
|
return "Not supported"
|
||||||
|
if not row.included:
|
||||||
|
return REMOVE_ACTION if row.device else NOT_DECLARED
|
||||||
|
return next((label for label, c in _options(row) if c == row.use),
|
||||||
|
_build_text(row, row.use))
|
||||||
|
|
||||||
|
def _values(self, row):
|
||||||
|
latest = row.builds[0] if row.builds else None
|
||||||
|
return (row.name, _build_text(row, row.device), _build_text(row, row.backup),
|
||||||
|
column_label(*latest) if latest else NOT_DECLARED, self._action_text(row))
|
||||||
|
|
||||||
|
def _image(self, row):
|
||||||
|
if row.unsupported:
|
||||||
|
return self.cross_box
|
||||||
|
return self.boxes[(row.included, not (row.required or row.locked))]
|
||||||
|
|
||||||
|
def _rebuild_table(self):
|
||||||
|
self._close_editor()
|
||||||
|
top = self.table.yview()[0]
|
||||||
|
self.table.delete(*self.table.get_children())
|
||||||
|
self.row_by_item.clear()
|
||||||
|
for title in app_backup.KIT_GROUPS:
|
||||||
|
members = [r for r in self.rows if app_backup.group(r) == title]
|
||||||
|
if not members:
|
||||||
|
continue
|
||||||
|
self.table.insert("", "end", values=(title, "", "", "", ""), tags=(SECTION_TAG,))
|
||||||
|
for row in members:
|
||||||
|
item = self.table.insert("", "end", image=self._image(row),
|
||||||
|
values=self._values(row))
|
||||||
|
self.row_by_item[item] = row
|
||||||
|
self.table.yview_moveto(top)
|
||||||
|
|
||||||
|
# Kits other than the device's cannot run on its kits.scode.
|
||||||
|
changed = any(app_backup.kit_change(r) for r in self.rows)
|
||||||
|
if changed:
|
||||||
|
self.rebuild.set(True)
|
||||||
|
self.rebuild_check.state(["disabled"])
|
||||||
|
else:
|
||||||
|
self.rebuild_check.state(["!disabled"])
|
||||||
|
|
||||||
|
def _changed(self):
|
||||||
|
app_backup.settle_required(self.rows)
|
||||||
|
self._rebuild_table()
|
||||||
|
|
||||||
|
# --- editing ---
|
||||||
|
|
||||||
|
def _close_editor(self):
|
||||||
|
if self._editor is not None:
|
||||||
|
self._editor.destroy()
|
||||||
|
self._editor = None
|
||||||
|
|
||||||
|
def _on_click(self, event):
|
||||||
|
self._close_editor()
|
||||||
|
if self.table.identify_region(event.x, event.y) not in ("tree", "cell"):
|
||||||
|
return
|
||||||
|
item = self.table.identify_row(event.y)
|
||||||
|
row = self.row_by_item.get(item)
|
||||||
|
if row is None or row.locked or row.unsupported:
|
||||||
|
return
|
||||||
|
column = self.table.identify_column(event.x)
|
||||||
|
if column == "#0":
|
||||||
|
self._toggle(row)
|
||||||
|
elif column == "#5":
|
||||||
|
self._edit_action(item, row)
|
||||||
|
|
||||||
|
def _toggle(self, row):
|
||||||
|
if row.required:
|
||||||
|
return
|
||||||
|
if not row.included and not row.builds:
|
||||||
|
messagebox.showwarning("Put with Files", f"Sedona home holds no build of "
|
||||||
|
f"'{row.name}' to add it at.", parent=self.top)
|
||||||
|
return
|
||||||
|
row.included = not row.included
|
||||||
|
if row.included and not row.use:
|
||||||
|
row.use = row.latest
|
||||||
|
self._changed()
|
||||||
|
|
||||||
|
def _edit_action(self, item, row):
|
||||||
|
options = _options(row)
|
||||||
|
if not options:
|
||||||
|
return
|
||||||
|
cell = self.table.bbox(item, "action")
|
||||||
|
if not cell:
|
||||||
|
return
|
||||||
|
labels = [label for label, _ in options]
|
||||||
|
variable = tk.StringVar(value=self._action_text(row))
|
||||||
|
combo = ttk.Combobox(self.table, textvariable=variable, values=labels,
|
||||||
|
state="readonly")
|
||||||
|
combo.place(x=cell[0], y=cell[1], width=cell[2], height=cell[3])
|
||||||
|
self._editor = combo
|
||||||
|
|
||||||
|
def commit(_event=None):
|
||||||
|
if variable.get() in labels:
|
||||||
|
row.use = options[labels.index(variable.get())][1]
|
||||||
|
row.included = True
|
||||||
|
self._close_editor()
|
||||||
|
self._changed()
|
||||||
|
|
||||||
|
combo.bind("<<ComboboxSelected>>", commit)
|
||||||
|
combo.bind("<Escape>", lambda _e: self._close_editor())
|
||||||
|
combo.focus_set()
|
||||||
|
combo.after_idle(lambda: combo.event_generate("<Down>"))
|
||||||
|
|
||||||
|
def _upgrade_all(self):
|
||||||
|
for row in self.rows:
|
||||||
|
if row.included and row.latest and not row.locked:
|
||||||
|
row.use = row.latest
|
||||||
|
self._changed()
|
||||||
|
|
||||||
|
def _close(self, ok):
|
||||||
|
self._close_editor()
|
||||||
|
if ok:
|
||||||
|
self.result = bool(self.rebuild.get())
|
||||||
|
else:
|
||||||
|
for row, (included, use) in zip(self.rows, self.original):
|
||||||
|
row.included, row.use = included, use
|
||||||
|
app_backup.settle_required(self.rows)
|
||||||
|
self.top.grab_release()
|
||||||
|
self.top.destroy()
|
||||||
|
|
||||||
|
|
||||||
|
def ask(parent, rows, title="Put with Files"):
|
||||||
|
"""Shows the kits step; returns whether to rebuild kits.scode, or None when cancelled.
|
||||||
|
|
||||||
|
The rows are updated in place with what was chosen.
|
||||||
|
"""
|
||||||
|
dialog = _Dialog(parent, rows, title)
|
||||||
|
dialog.top.wait_window()
|
||||||
|
return dialog.result
|
||||||
+12
-6
@@ -80,6 +80,9 @@ def _resolve_slots(qname, types_by_q, cache):
|
|||||||
"""The type's slots, base chain first, indexed by slot id."""
|
"""The type's slots, base chain first, indexed by slot id."""
|
||||||
if qname in cache:
|
if qname in cache:
|
||||||
return cache[qname]
|
return cache[qname]
|
||||||
|
if qname not in types_by_q:
|
||||||
|
raise SabError(f"type {qname} is not in the app's kits -- a kit build that "
|
||||||
|
f"needs kit '{qname.split('::')[0]}', which the app does not list?")
|
||||||
t = types_by_q[qname]
|
t = types_by_q[qname]
|
||||||
base = t.get("base")
|
base = t.get("base")
|
||||||
out = list(_resolve_slots(base, types_by_q, cache)) if base else []
|
out = list(_resolve_slots(base, types_by_q, cache)) if base else []
|
||||||
@@ -505,21 +508,24 @@ def _encode_value(out, slot, raw):
|
|||||||
raise SabError(f"unhandled slot type {vtype}")
|
raise SabError(f"unhandled slot type {vtype}")
|
||||||
|
|
||||||
|
|
||||||
def encode_sax(sax, checksums=None):
|
def encode_sax(sax, checksums=None, kits=None):
|
||||||
"""app.sax (text or bytes) -> app.sab bytes, as `sedonac app.sax` writes it.
|
"""app.sax (text or bytes) -> app.sab bytes, as `sedonac app.sax` writes it.
|
||||||
|
|
||||||
checksums, {kit name: checksum hex}, stands in for the sax's own -- how an app goes
|
checksums, {kit name: checksum hex}, stands in for the sax's own -- how an app goes
|
||||||
to a device whose kits are other builds than the ones it was saved with. Every
|
to a device whose kits are other builds than the ones it was saved with. kits,
|
||||||
kit's manifest must be in Sedona home under the checksum used.
|
[(name, checksum hex)], replaces the schema altogether, as Ontrol's PutApp rewrites
|
||||||
|
it for the kits chosen to put. Every kit's manifest must be in Sedona home under
|
||||||
|
the checksum used.
|
||||||
"""
|
"""
|
||||||
root = ET.fromstring(sax)
|
root = ET.fromstring(sax)
|
||||||
schema_elem, app_elem = root.find("schema"), root.find("app")
|
schema_elem, app_elem = root.find("schema"), root.find("app")
|
||||||
if schema_elem is None or app_elem is None:
|
if schema_elem is None or app_elem is None:
|
||||||
raise SabError("not a Sedona app: no <schema> or <app>")
|
raise SabError("not a Sedona app: no <schema> or <app>")
|
||||||
|
if kits is None:
|
||||||
|
kits = [(k.get("name"), (checksums or {}).get(k.get("name")) or k.get("checksum"))
|
||||||
|
for k in schema_elem.findall("kit")]
|
||||||
parts = []
|
parts = []
|
||||||
for k in schema_elem.findall("kit"):
|
for name, cks in kits:
|
||||||
name = k.get("name")
|
|
||||||
cks = (checksums or {}).get(name) or k.get("checksum")
|
|
||||||
if not cks:
|
if not cks:
|
||||||
raise SabError(f"no checksum for kit '{name}'")
|
raise SabError(f"no checksum for kit '{name}'")
|
||||||
parts.append((name, f"{int(cks, 16):08x}"))
|
parts.append((name, f"{int(cks, 16):08x}"))
|
||||||
|
|||||||
Reference in New Issue
Block a user