"""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 = [""] missing = [] for name, checksum in kits: found = kit_file(name, checksum) if found is None: missing.append(f"{name} {checksum}") else: lines.append(f' ') if missing: raise BuildError("Sedona home has no .kit file for " + ", ".join(missing)) lines.append("") 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()