#!/usr/bin/env python3 """Write the ledgerctl install manifest for a built app. cargo-ledger does not emit `app_.json` on macOS, so generate it from the built ELF plus `[package.metadata.ledger]` in Cargo.toml. Everything that can be read from the ELF is read from the ELF: app name, version, api level and target id all come from its `ledger.*` sections, so the manifest cannot drift from the binary it installs. `dataSize` is `_envram_data - _nvram_data`: the NVRAM the app is granted at install time. Getting it too small installs an app that will not launch. Usage: ./scripts/gen-manifest [stax|flex|nanox|nanosplus] (default: stax) """ import json import pathlib import subprocess import sys import tomllib LLVM_NM = "/opt/homebrew/opt/llvm/bin/llvm-nm" LLVM_OBJCOPY = "/opt/homebrew/opt/llvm/bin/llvm-objcopy" # cargo-ledger's Cargo.toml key -> the [package.metadata.ledger.] table DEVICE_TABLE = { "stax": "stax", "flex": "flex", "nanox": "nanox", "nanosplus": "nanosplus", "apex_p": "apex_p", } def elf_section(elf, name): """Read a `ledger.*` metadata string section out of the ELF.""" out = subprocess.run( [LLVM_OBJCOPY, f"--dump-section=ledger.{name}=/dev/stdout", elf, "/dev/null"], capture_output=True, ).stdout return out.decode("utf-8", "replace").strip("\0\n ") def data_size(elf): """NVRAM granted to the app: `_envram_data - _nvram_data`.""" out = subprocess.run([LLVM_NM, elf], capture_output=True, text=True).stdout sym = {} for line in out.splitlines(): parts = line.split() if len(parts) == 3 and parts[2] in ("_nvram_data", "_envram_data"): sym[parts[2]] = int(parts[0], 16) missing = {"_nvram_data", "_envram_data"} - sym.keys() if missing: sys.exit(f"gen-manifest: {elf} has no {', '.join(sorted(missing))} symbol") return sym["_envram_data"] - sym["_nvram_data"] def main(): device = sys.argv[1] if len(sys.argv) > 1 else "stax" root = pathlib.Path(__file__).resolve().parent.parent cargo = tomllib.loads((root / "Cargo.toml").read_text()) pkg = cargo["package"] meta = pkg["metadata"]["ledger"] per_device = meta.get(DEVICE_TABLE.get(device, device), {}) elf = root / "target" / device / "release" / pkg["name"] if not elf.is_file(): sys.exit(f"gen-manifest: {elf} not built: run ./scripts/build {device} first") hex_path = elf.with_suffix(".hex") if not hex_path.is_file(): sys.exit(f"gen-manifest: {hex_path} missing: run ./scripts/build {device} first") icon = per_device.get("icon") if not icon: sys.exit(f"gen-manifest: no icon for '{device}' in [package.metadata.ledger.{device}]") flags = meta.get("flags", "0") manifest = { "name": elf_section(elf, "app_name"), "version": elf_section(elf, "app_version"), "icon": str(root / icon), "targetId": elf_section(elf, "target_id"), "flags": f"0x{int(str(flags), 0):03x}", "apiLevel": elf_section(elf, "api_level"), "derivationPath": {"curves": meta["curve"], "paths": meta["path"]}, "binary": hex_path.name, "dataSize": data_size(elf), } out = elf.parent / f"app_{device}.json" out.write_text(json.dumps(manifest, indent=4) + "\n") print(f"wrote {out.relative_to(root)} " f"(name={manifest['name']} apiLevel={manifest['apiLevel']} " f"dataSize={manifest['dataSize']})") if __name__ == "__main__": main()