Skip to content

Commit abfa2c0

Browse files
Load app metadata from companion app_<target>.json file for Rust binaries
1 parent f490198 commit abfa2c0

3 files changed

Lines changed: 430 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.15.0] - 2025-12-23
9+
10+
### Added
11+
12+
- Load app metadata from companion app_<target>.json file for rust binaries
13+
814
## [0.14.0] - 2025-12-03
915

1016
### Added

src/ledgered/binary.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import json
12
import logging
23
from argparse import ArgumentParser
34
from dataclasses import asdict, dataclass
45
from elftools.elf.elffile import ELFFile
56
from pathlib import Path
67
from typing import Optional, Union
78

9+
from ledgered.devices import Devices
810
from ledgered.serializers import Jsonable
911

1012
LEDGER_PREFIX = "ledger."
@@ -46,10 +48,122 @@ def __init__(self, binary_path: Union[str, Path]):
4648
}
4749
self._sections = Sections(**sections)
4850

51+
# Rust apps store app_name/app_version/app_flags in companion JSON file
52+
if self.is_rust_app:
53+
self._load_metadata_from_json()
54+
55+
def _load_metadata_from_json(self) -> None:
56+
"""Load app metadata from companion app_<target>.json file.
57+
58+
Rust applications don't embed app_name, app_version, and app_flags in the ELF.
59+
Instead, these are stored in a JSON file named app_<target>.json in the same directory.
60+
This method is called only for Rust applications.
61+
62+
Note: The target name in the ELF may differ from the JSON filename.
63+
For example, nanos2 -> nanosplus, nanos+ -> nanosplus.
64+
"""
65+
target = self._sections.target
66+
if not target:
67+
logging.warning(
68+
"Rust app detected but no target found, cannot locate companion JSON file"
69+
)
70+
return
71+
72+
# Try multiple naming patterns to find the JSON file
73+
json_path = self._find_json_file(target)
74+
if not json_path:
75+
logging.warning(
76+
"Rust app detected but companion JSON file not found for target '%s' in %s",
77+
target,
78+
self._path.parent,
79+
)
80+
return
81+
82+
try:
83+
logging.info("Loading Rust app metadata from %s", json_path)
84+
with json_path.open("r") as f:
85+
data = json.load(f)
86+
87+
if "name" in data:
88+
self._sections.app_name = data["name"]
89+
logging.debug("Loaded app_name: %s", self._sections.app_name)
90+
91+
if "version" in data:
92+
self._sections.app_version = data["version"]
93+
logging.debug("Loaded app_version: %s", self._sections.app_version)
94+
95+
if "flags" in data:
96+
self._sections.app_flags = data["flags"]
97+
logging.debug("Loaded app_flags: %s", self._sections.app_flags)
98+
99+
except (json.JSONDecodeError, OSError) as e:
100+
logging.error("Failed to load companion JSON file %s: %s", json_path, e)
101+
102+
def _find_json_file(self, target: str) -> Optional[Path]:
103+
"""Find the companion JSON file using multiple naming patterns.
104+
105+
Tries different naming conventions:
106+
1. Exact target name (e.g., nanos2 -> app_nanos2.json)
107+
2. Canonical device name (e.g., nanos2 -> app_nanosp.json)
108+
3. All device aliases (e.g., nanos+, nanosplus for NanoS+)
109+
110+
Args:
111+
target: The target name from the ELF binary
112+
113+
Returns:
114+
Path to the JSON file if found, None otherwise
115+
"""
116+
candidates = [target] # Start with exact target name
117+
118+
try:
119+
device = Devices.get_by_name(target)
120+
# Add canonical device name
121+
candidates.append(device.name)
122+
# Add all known aliases
123+
candidates.extend(device.names)
124+
except KeyError:
125+
logging.debug("Unknown device '%s', trying exact name only", target)
126+
127+
# Try all candidates
128+
for candidate in candidates:
129+
json_path = self._path.parent / f"app_{candidate}.json"
130+
if json_path.exists():
131+
logging.debug("Found JSON file with pattern '%s': %s", candidate, json_path)
132+
return json_path
133+
134+
return None
135+
136+
def _normalize_target_name(self, target: str) -> str:
137+
"""Normalize target name to match JSON filename conventions.
138+
139+
Device names can vary (e.g., nanos2, nanos+, nanosplus all refer to the same device).
140+
The JSON files use a canonical naming (nanosplus, flex, stax, etc.).
141+
142+
Args:
143+
target: The target name from the ELF binary
144+
145+
Returns:
146+
The normalized target name for JSON file lookup
147+
"""
148+
try:
149+
device = Devices.get_by_name(target)
150+
return device.name
151+
except KeyError:
152+
# If device is unknown, return the original target name
153+
logging.debug("Unknown device '%s', using target name as-is", target)
154+
return target
155+
49156
@property
50157
def sections(self) -> Sections:
51158
return self._sections
52159

160+
@property
161+
def is_rust_app(self) -> bool:
162+
"""Returns True if this is a Rust application."""
163+
return (
164+
self._sections.rust_sdk_name is not None or self._sections.rust_sdk_version is not None
165+
)
166+
53167

54168
def set_parser() -> ArgumentParser:
55169
parser = ArgumentParser(

0 commit comments

Comments
 (0)