Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2b18923b7 | ||
|
|
e070bd0f1e |
+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.168"
|
||||||
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
|
||||||
@@ -616,27 +617,35 @@ def _invoke_app_action(slot_name):
|
|||||||
if slot is None:
|
if slot is None:
|
||||||
app_state.set_status(f"Action slot '{slot_name}' not found on app", warn=True)
|
app_state.set_status(f"Action slot '{slot_name}' not found on app", warn=True)
|
||||||
return False
|
return False
|
||||||
goes_down = slot_name in ("hibernate", "quit", "restart", "reboot")
|
goes_down = slot_name in _GOES_DOWN
|
||||||
try:
|
try:
|
||||||
ds.sox_client.invoke(0, slot["id"])
|
# A device going down often does so before it answers: give the request a few
|
||||||
app_state.set_status(f"Invoked '{slot_name}' on device")
|
# seconds (time for DASP to resend it once), then disconnect either way rather
|
||||||
|
# than wait for the session to drop.
|
||||||
|
ds.sox_client.invoke(0, slot["id"], timeout=_GOES_DOWN_WAIT if goes_down else 30.0)
|
||||||
except sox.SoxException as e:
|
except sox.SoxException as e:
|
||||||
# A P-ION restarts before it answers: no reply is what a restart looks like.
|
refused = "timeout" not in str(e).lower() and "closed" not in str(e).lower()
|
||||||
if not (goes_down and "timeout" in str(e).lower()):
|
if not goes_down or refused:
|
||||||
app_state.set_status(f"Invoke '{slot_name}' failed: {e}", warn=True)
|
app_state.set_status(f"Invoke '{slot_name}' failed: {e}", warn=True)
|
||||||
return False
|
return False
|
||||||
app_state.set_status(f"Invoked '{slot_name}'; the device went down without replying")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app_state.set_status(f"Invoke '{slot_name}' failed: {e}", warn=True)
|
app_state.set_status(f"Invoke '{slot_name}' failed: {e}", warn=True)
|
||||||
return False
|
return False
|
||||||
if slot_name == "save":
|
if slot_name == "save":
|
||||||
app_state.mark_clean()
|
app_state.mark_clean()
|
||||||
|
app_state.set_status(f"Invoked '{slot_name}' on device")
|
||||||
elif goes_down:
|
elif goes_down:
|
||||||
|
host = app_state.device_host
|
||||||
do_device_disconnect()
|
do_device_disconnect()
|
||||||
|
app_state.set_status(f"Sent '{slot_name}' to {host} and disconnected")
|
||||||
|
else:
|
||||||
|
app_state.set_status(f"Invoked '{slot_name}' on device")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
_APP_ACTIONS = ["save", "hibernate", "quit", "restart", "reboot"]
|
_APP_ACTIONS = ["save", "hibernate", "quit", "restart", "reboot"]
|
||||||
|
_GOES_DOWN = ("hibernate", "quit", "restart", "reboot") # the device stops answering
|
||||||
|
_GOES_DOWN_WAIT = 3.0 # seconds to wait for its reply before disconnecting
|
||||||
|
|
||||||
|
|
||||||
def do_get_with_file():
|
def do_get_with_file():
|
||||||
@@ -781,10 +790,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 +818,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(
|
||||||
@@ -837,12 +879,10 @@ def do_put_with_files():
|
|||||||
f"{len(plan.app_files) + len(plan.media)} files written to the device."
|
f"{len(plan.app_files) + len(plan.media)} files written to the device."
|
||||||
f"{NL}{NL}Restart the device now to complete provisioning?"):
|
f"{NL}{NL}Restart the device now to complete provisioning?"):
|
||||||
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, disconnected")
|
||||||
|
|
||||||
_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 +894,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}"))
|
||||||
|
|||||||
@@ -667,13 +667,13 @@ class SoxClient:
|
|||||||
|
|
||||||
# ── Invoke Action ───────────────────────────────────────────
|
# ── Invoke Action ───────────────────────────────────────────
|
||||||
|
|
||||||
def invoke(self, comp_id, slot_id, type_id=None, arg=None):
|
def invoke(self, comp_id, slot_id, type_id=None, arg=None, timeout=30.0):
|
||||||
req = _prepare_request(ord('i'))
|
req = _prepare_request(ord('i'))
|
||||||
req.write_u2(comp_id)
|
req.write_u2(comp_id)
|
||||||
req.write_u1(slot_id)
|
req.write_u1(slot_id)
|
||||||
if arg is not None and type_id is not None:
|
if arg is not None and type_id is not None:
|
||||||
encode_value(req, type_id, arg)
|
encode_value(req, type_id, arg)
|
||||||
res = self.request(req)
|
res = self.request(req, timeout)
|
||||||
res.check_response('I')
|
res.check_response('I')
|
||||||
|
|
||||||
# ── Add Component ───────────────────────────────────────────
|
# ── Add Component ───────────────────────────────────────────
|
||||||
@@ -1062,17 +1062,17 @@ class SoxClient:
|
|||||||
|
|
||||||
# ── Networking ──────────────────────────────────────────────
|
# ── Networking ──────────────────────────────────────────────
|
||||||
|
|
||||||
def request(self, req):
|
def request(self, req, timeout=30.0):
|
||||||
results = self.requests([req])
|
results = self.requests([req], timeout)
|
||||||
return results[0]
|
return results[0]
|
||||||
|
|
||||||
def requests(self, reqs):
|
def requests(self, reqs, timeout=30.0):
|
||||||
"""Sends the batch and waits for every reply."""
|
"""Sends the batch and waits for every reply."""
|
||||||
if self.is_closed:
|
if self.is_closed:
|
||||||
raise SoxException("SoxClient closed")
|
raise SoxException("SoxClient closed")
|
||||||
|
|
||||||
if len(reqs) > 255:
|
if len(reqs) > 255:
|
||||||
return self._chunk_requests(reqs)
|
return self._chunk_requests(reqs, timeout)
|
||||||
|
|
||||||
with self._lock:
|
with self._lock:
|
||||||
for i, req in enumerate(reqs):
|
for i, req in enumerate(reqs):
|
||||||
@@ -1085,7 +1085,7 @@ class SoxClient:
|
|||||||
for req in reqs:
|
for req in reqs:
|
||||||
self._session.send(req.to_bytes())
|
self._session.send(req.to_bytes())
|
||||||
|
|
||||||
deadline = time.monotonic() + 30.0
|
deadline = time.monotonic() + timeout
|
||||||
while True:
|
while True:
|
||||||
remaining = deadline - time.monotonic()
|
remaining = deadline - time.monotonic()
|
||||||
if remaining <= 0:
|
if remaining <= 0:
|
||||||
@@ -1107,11 +1107,11 @@ class SoxClient:
|
|||||||
self._responses = None
|
self._responses = None
|
||||||
raise SoxException("Request timeout")
|
raise SoxException("Request timeout")
|
||||||
|
|
||||||
def _chunk_requests(self, reqs):
|
def _chunk_requests(self, reqs, timeout=30.0):
|
||||||
results = []
|
results = []
|
||||||
for i in range(0, len(reqs), 255):
|
for i in range(0, len(reqs), 255):
|
||||||
chunk = reqs[i:i + 255]
|
chunk = reqs[i:i + 255]
|
||||||
results.extend(self.requests(chunk))
|
results.extend(self.requests(chunk, timeout))
|
||||||
return results
|
return results
|
||||||
|
|
||||||
# called by receiver thread
|
# called by receiver thread
|
||||||
|
|||||||
Reference in New Issue
Block a user