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:
+266
-84
@@ -26,7 +26,10 @@ import re
|
||||
import zipfile
|
||||
|
||||
import app_state
|
||||
import kit_build
|
||||
import palette
|
||||
import sab_app
|
||||
import schema
|
||||
import sox
|
||||
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):
|
||||
kits = app.kit_names()
|
||||
return next((p for p in _PLATFORMS if p.kit in kits), None)
|
||||
return platform_of_kits(app.kit_names())
|
||||
|
||||
|
||||
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():
|
||||
@@ -278,7 +289,8 @@ def get_with_file(client, path, status=None):
|
||||
# --- Put with Files ---------------------------------------------------------------------
|
||||
#
|
||||
# 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
|
||||
# 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.
|
||||
@@ -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)
|
||||
|
||||
|
||||
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:
|
||||
"""What a put needs to know of the connected device."""
|
||||
|
||||
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_id = platform_id # e.g. "ontrol-pion-b427a5b9"
|
||||
self.kits = kits # [(name, checksum)] in kit-id order
|
||||
self.name = name # the app's deviceName
|
||||
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)
|
||||
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)
|
||||
if comp is not None:
|
||||
client.update([comp], sox.CONFIG)
|
||||
@@ -450,8 +450,8 @@ class PutPlan:
|
||||
self.problems = [] # any of these stops the put
|
||||
self.warnings = [] # worth confirming, but the put can go on
|
||||
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 = [] # set when the backup's kits.scode has to go too
|
||||
self.kit_changes = [] # each chosen kit build that is not the device's
|
||||
self.scode_source = "" # where kits.scode comes from, when it goes
|
||||
self.app_files = [] # PutFile to '<name>.writing', renamed to '.stage'
|
||||
self.media = [] # PutFile: images, fonts, logo
|
||||
self.converted = 0 # images made from the backup's image bank
|
||||
@@ -466,39 +466,176 @@ class PutPlan:
|
||||
return not self.problems
|
||||
|
||||
|
||||
def _label(platform):
|
||||
return platform.name if platform else "no platform this editor knows"
|
||||
|
||||
|
||||
def _app_portrait(app):
|
||||
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))
|
||||
|
||||
|
||||
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
|
||||
schema_elem = ET.fromstring(sax).find("schema")
|
||||
return [] if schema_elem is None else [k.get("name") for k in schema_elem.findall("kit")]
|
||||
with zipfile.ZipFile(path) as z:
|
||||
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):
|
||||
"""Reads the zip at `path` and checks it against the device `target`."""
|
||||
def kit_rows(path, 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)
|
||||
try:
|
||||
z = zipfile.ZipFile(path)
|
||||
except (OSError, zipfile.BadZipFile) as e:
|
||||
plan.problems.append(f"Not a readable zip: {e}")
|
||||
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:
|
||||
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:
|
||||
return plan
|
||||
|
||||
platform = platform_of(app)
|
||||
if platform is None or platform is not target.platform:
|
||||
plan.problems.append(f"The backup is from {_label(platform)}; the device is "
|
||||
f"{_label(target.platform)}.")
|
||||
plan.problems.append(f"The backup is from {platform_label(platform)}; the device "
|
||||
f"is {platform_label(target.platform)}.")
|
||||
return plan
|
||||
|
||||
zip_name = app.prop(app.app, "deviceName", "") or ""
|
||||
@@ -524,63 +661,108 @@ def plan_put(path, target):
|
||||
return plan
|
||||
|
||||
|
||||
def _plan_app(plan, z, names, target):
|
||||
"""Settles app.sab, Ontrol's way first: the backup's app.sax encoded against the
|
||||
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
|
||||
def _plan_app(plan, z, names, target, rows, rebuild):
|
||||
"""Settles app.sab and kits.scode, as Ontrol's PutApp does.
|
||||
|
||||
The backup's app.sax is encoded for the chosen kit builds (a backup with only
|
||||
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:
|
||||
original = sab_app.SabApp(z.read(sab_name))
|
||||
except Exception as e:
|
||||
if not sax_name:
|
||||
plan.problems.append(f"{sab_name} cannot be read: {e}")
|
||||
return None, False
|
||||
|
||||
if sax_name:
|
||||
sax = z.read(sax_name)
|
||||
try:
|
||||
missing = [k for k in _sax_kits(sax) if k not in device_cks]
|
||||
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)
|
||||
sab = sab_app.encode_sax(z.read(sax_name), kits=chosen)
|
||||
except Exception as e:
|
||||
sax_problem = f"{sax_name} cannot be encoded for this device: {e}"
|
||||
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}")
|
||||
return None, False
|
||||
if kits_fit(app.kit_parts, target.kits):
|
||||
plan.app_source = sab_name
|
||||
elif scode_name in names:
|
||||
plan.kit_changes = kit_differences(app.kit_parts, target.kits)
|
||||
plan.app_source = f"{sab_name}, with the backup's {KITS_SCODE}"
|
||||
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}")
|
||||
return None, False
|
||||
plan.app_source = f"{sax_name}, encoded for the chosen kits"
|
||||
source = sax_name
|
||||
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
|
||||
if sax_problem:
|
||||
plan.warnings.append(sax_problem)
|
||||
plan.app_files.append(PutFile(APP_SAB, sab, sab_name))
|
||||
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):
|
||||
|
||||
Reference in New Issue
Block a user