Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.15.0] - 2025-12-23

### Added

- Load app metadata from companion app_<target>.json file for rust binaries

## [0.14.0] - 2025-12-03

### Added
Expand Down
114 changes: 114 additions & 0 deletions src/ledgered/binary.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import json
import logging
from argparse import ArgumentParser
from dataclasses import asdict, dataclass
from elftools.elf.elffile import ELFFile
from pathlib import Path
from typing import Optional, Union

from ledgered.devices import Devices
from ledgered.serializers import Jsonable

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

# Rust apps store app_name/app_version/app_flags in companion JSON file
if self.is_rust_app:
self._load_metadata_from_json()

def _load_metadata_from_json(self) -> None:
"""Load app metadata from companion app_<target>.json file.

Rust applications don't embed app_name, app_version, and app_flags in the ELF.
Instead, these are stored in a JSON file named app_<target>.json in the same directory.
This method is called only for Rust applications.

Note: The target name in the ELF may differ from the JSON filename.
For example, nanos2 -> nanosplus, nanos+ -> nanosplus.
"""
target = self._sections.target
if not target:
logging.warning(
"Rust app detected but no target found, cannot locate companion JSON file"
)
return

# Try multiple naming patterns to find the JSON file
json_path = self._find_json_file(target)
if not json_path:
logging.warning(
"Rust app detected but companion JSON file not found for target '%s' in %s",
target,
self._path.parent,
)
return

try:
logging.info("Loading Rust app metadata from %s", json_path)
with json_path.open("r") as f:
data = json.load(f)

if "name" in data:
self._sections.app_name = data["name"]
logging.debug("Loaded app_name: %s", self._sections.app_name)

if "version" in data:
self._sections.app_version = data["version"]
logging.debug("Loaded app_version: %s", self._sections.app_version)

if "flags" in data:
self._sections.app_flags = data["flags"]
logging.debug("Loaded app_flags: %s", self._sections.app_flags)

except (json.JSONDecodeError, OSError) as e:
logging.error("Failed to load companion JSON file %s: %s", json_path, e)

def _find_json_file(self, target: str) -> Optional[Path]:
"""Find the companion JSON file using multiple naming patterns.

Tries different naming conventions:
1. Exact target name (e.g., nanos2 -> app_nanos2.json)
2. Canonical device name (e.g., nanos2 -> app_nanosp.json)
3. All device aliases (e.g., nanos+, nanosplus for NanoS+)

Args:
target: The target name from the ELF binary

Returns:
Path to the JSON file if found, None otherwise
"""
candidates = [target] # Start with exact target name

try:
device = Devices.get_by_name(target)
# Add canonical device name
candidates.append(device.name)
# Add all known aliases
candidates.extend(device.names)
except KeyError:
logging.debug("Unknown device '%s', trying exact name only", target)

# Try all candidates
for candidate in candidates:
json_path = self._path.parent / f"app_{candidate}.json"
if json_path.exists():
logging.debug("Found JSON file with pattern '%s': %s", candidate, json_path)
return json_path

return None

def _normalize_target_name(self, target: str) -> str:
"""Normalize target name to match JSON filename conventions.

Device names can vary (e.g., nanos2, nanos+, nanosplus all refer to the same device).
The JSON files use a canonical naming (nanosplus, flex, stax, etc.).

Args:
target: The target name from the ELF binary

Returns:
The normalized target name for JSON file lookup
"""
try:
device = Devices.get_by_name(target)
return device.name
except KeyError:
# If device is unknown, return the original target name
logging.debug("Unknown device '%s', using target name as-is", target)
return target

@property
def sections(self) -> Sections:
return self._sections

@property
def is_rust_app(self) -> bool:
"""Returns True if this is a Rust application."""
return (
self._sections.rust_sdk_name is not None or self._sections.rust_sdk_version is not None
)


def set_parser() -> ArgumentParser:
parser = ArgumentParser(
Expand Down
Loading
Loading