275 lines
9.4 KiB
Python
275 lines
9.4 KiB
Python
"""
|
||
Acroname **on-device Reflex** (embedded language on BrainStem modules).
|
||
|
||
This module does **not** run Reflex on the hub; it helps you **compile** ``.reflex`` sources to
|
||
``.map`` files using the BrainStem Development Kit **arc** compiler. Load maps with Acroname
|
||
**ReflexLoader** or **HubTool** (see their docs).
|
||
|
||
The pip ``brainstem`` package is the host Python API; **arc** ships with the separate dev kit.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import shutil
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
# Primary language reference (path may move; search Acroname “Reflex language” if stale).
|
||
REFLEX_DOC_URL = "https://acroname.com/reference/brainstem/reflex/"
|
||
# Full SDK tarball includes ``bin/arc``, Reflex API ``*.reflex`` headers, HubTool, etc.
|
||
BRAINSTEM_SDK_DOWNLOAD_URL = "https://acroname.com/software/brainstem-development-kit"
|
||
|
||
|
||
def _dedupe_paths(paths: list[Path]) -> list[Path]:
|
||
seen: set[str] = set()
|
||
out: list[Path] = []
|
||
for p in paths:
|
||
try:
|
||
key = str(p.resolve())
|
||
except OSError:
|
||
key = str(p)
|
||
if key not in seen:
|
||
seen.add(key)
|
||
out.append(p)
|
||
return out
|
||
|
||
|
||
def _env_path_list(key: str) -> list[Path]:
|
||
raw = os.environ.get(key, "").strip()
|
||
if not raw:
|
||
return []
|
||
return [Path(p).expanduser() for p in raw.split(os.pathsep) if p.strip()]
|
||
|
||
|
||
def _fiwi_repo_brainstem_sdk_roots() -> list[Path]:
|
||
"""
|
||
Optional checkout next to this repo: ``FiWiManager/brainstem_sdk`` (and nested ``BrainStem2``
|
||
after extracting the official ``.tgz``).
|
||
"""
|
||
root = Path(__file__).resolve().parent.parent
|
||
out: list[Path] = []
|
||
b = root / "brainstem_sdk"
|
||
if b.is_dir():
|
||
out.append(b)
|
||
inner = b / "BrainStem2"
|
||
if inner.is_dir():
|
||
out.append(inner)
|
||
return out
|
||
|
||
|
||
def brainstem_dev_roots() -> list[Path]:
|
||
"""Likely BrainStem2 checkout or SDK install roots (for ``bin/arc`` and reflex API includes)."""
|
||
roots: list[Path] = []
|
||
for key in (
|
||
"BRAINSTEM2_DEV_ROOT",
|
||
"BRAINSTEM_DEV_ROOT",
|
||
"ACRONAME_BRAINSTEM_ROOT",
|
||
"BRAINSTEM_ROOT",
|
||
"REFLEX_SDK_ROOT",
|
||
"FIWI_BRAINSTEM_SDK",
|
||
):
|
||
roots.extend(_env_path_list(key))
|
||
roots.extend(_fiwi_repo_brainstem_sdk_roots())
|
||
return _dedupe_paths(roots)
|
||
|
||
|
||
def _likely_sdk_roots_under_home() -> list[Path]:
|
||
"""Common extract locations when the user has not set env vars (Linux/macOS)."""
|
||
home = Path.home()
|
||
names = (
|
||
"BrainStem2",
|
||
"brainstem2",
|
||
"BrainStemDevelopment",
|
||
"brainstem-development-kit",
|
||
"acroname-brainstem",
|
||
)
|
||
out: list[Path] = []
|
||
for name in names:
|
||
p = home / name
|
||
if p.is_dir():
|
||
out.append(p)
|
||
# Nested layout: ~/Downloads/BrainStem2-2.12.2-Linux/BrainStem2
|
||
try:
|
||
for child in home.iterdir():
|
||
if not child.is_dir():
|
||
continue
|
||
if "BrainStem" in child.name or "brainstem" in child.name.lower():
|
||
nested = child / "BrainStem2"
|
||
if nested.is_dir():
|
||
out.append(nested)
|
||
out.append(child)
|
||
except OSError:
|
||
pass
|
||
return _dedupe_paths(out)
|
||
|
||
|
||
def default_reflex_include_dirs() -> list[Path]:
|
||
"""
|
||
``-I`` paths for ``#include <aUSBHub3p.reflex>``-style API headers.
|
||
|
||
Set ``FIWI_REFLEX_INCLUDE`` to ``os.pathsep``-separated directories (required if arc is not
|
||
configured elsewhere). Optionally set ``BRAINSTEM2_DEV_ROOT`` to a dev kit tree containing
|
||
``api/lib/BrainStem2`` (or similar) with ``*.reflex`` API files.
|
||
"""
|
||
inc: list[Path] = []
|
||
inc.extend(_env_path_list("FIWI_REFLEX_INCLUDE"))
|
||
for root in brainstem_dev_roots():
|
||
for sub in (
|
||
"api/lib/BrainStem2",
|
||
"lib/BrainStem2",
|
||
"BrainStem2/api/lib/BrainStem2",
|
||
"include/reflex",
|
||
):
|
||
p = root / sub
|
||
if p.is_dir():
|
||
inc.append(p)
|
||
return _dedupe_paths(inc)
|
||
|
||
|
||
def find_arc() -> str | None:
|
||
"""Return executable path for **arc**, or None."""
|
||
explicit = os.environ.get("FIWI_REFLEX_ARC", "").strip()
|
||
if explicit:
|
||
p = Path(explicit).expanduser()
|
||
if p.is_file() and os.access(p, os.X_OK):
|
||
return str(p)
|
||
return None
|
||
w = shutil.which("arc")
|
||
if w:
|
||
return w
|
||
roots = _dedupe_paths(brainstem_dev_roots() + _likely_sdk_roots_under_home())
|
||
for root in roots:
|
||
cand = root / "bin" / "arc"
|
||
if cand.is_file() and os.access(cand, os.X_OK):
|
||
return str(cand)
|
||
return None
|
||
|
||
|
||
def print_arc_missing_help() -> None:
|
||
print(
|
||
"The Reflex compiler **arc** is not on PATH and was not found under common install paths.\n"
|
||
"\n"
|
||
"It is **not** installed by ``pip install brainstem``. Get the BrainStem **Development Kit** "
|
||
"(Linux .tgz), extract it, then either:\n"
|
||
"\n"
|
||
f" • Add the kit’s ``bin`` to PATH (contains ``arc``, ``ReflexLoader``, ``HubTool``), or\n"
|
||
" • export BRAINSTEM2_DEV_ROOT=/path/to/extracted/BrainStem2\n"
|
||
" (FiWi searches $BRAINSTEM2_DEV_ROOT/bin/arc and api/lib/BrainStem2 for includes), or\n"
|
||
" • export FIWI_REFLEX_ARC=/full/path/to/arc\n"
|
||
"\n"
|
||
f"Download: {BRAINSTEM_SDK_DOWNLOAD_URL}\n"
|
||
"\n"
|
||
"If you keep the SDK under ``FiWiManager/brainstem_sdk``, extract the Linux ``.tgz`` there "
|
||
"so you get ``brainstem_sdk/BrainStem2/bin/arc`` (the ``api/`` tree alone does not ship "
|
||
"``arc``).\n"
|
||
"\n"
|
||
"Check: python3 fiwi.py reflex which\n",
|
||
file=sys.stderr,
|
||
flush=True,
|
||
)
|
||
|
||
|
||
def reflex_compile(
|
||
source: Path,
|
||
output_map: Path | None,
|
||
*,
|
||
extra_argv: list[str] | None = None,
|
||
verbose: bool = False,
|
||
) -> int:
|
||
"""
|
||
Run **arc** to compile ``source`` → ``output_map`` (default: same basename, ``.map``).
|
||
|
||
``extra_argv`` are appended before the output/source (for unusual arc builds). Returns
|
||
subprocess exit code.
|
||
"""
|
||
arc = find_arc()
|
||
if not arc:
|
||
print("fiwi reflex: no 'arc' executable.", file=sys.stderr, flush=True)
|
||
print_arc_missing_help()
|
||
return 1
|
||
src = source.expanduser().resolve()
|
||
if not src.is_file():
|
||
print(f"fiwi reflex: source not found: {src}", file=sys.stderr, flush=True)
|
||
return 1
|
||
out = (
|
||
output_map.expanduser().resolve()
|
||
if output_map is not None
|
||
else src.with_suffix(".map")
|
||
)
|
||
includes = default_reflex_include_dirs()
|
||
argv: list[str] = [arc]
|
||
for d in includes:
|
||
argv.extend(["-I", str(d.resolve())])
|
||
if extra_argv:
|
||
argv.extend(extra_argv)
|
||
argv.extend(["-o", str(out), str(src)])
|
||
if verbose:
|
||
print(subprocess.list2cmdline(argv), flush=True)
|
||
return subprocess.run(argv, cwd=str(src.parent)).returncode
|
||
|
||
|
||
def print_reflex_cli_help() -> None:
|
||
print(
|
||
"On-device Reflex (Acroname) — compile only; load .map with ReflexLoader / HubTool.\n"
|
||
"\n"
|
||
"Usage:\n"
|
||
" fiwi.py reflex help | doc | which\n"
|
||
" fiwi.py reflex compile <source.reflex> [output.map] [-- <extra arc args...>]\n"
|
||
"\n"
|
||
"Environment:\n"
|
||
" FIWI_REFLEX_ARC Full path to arc (if not on PATH)\n"
|
||
" FIWI_REFLEX_INCLUDE Include dirs for aUSBHub*.reflex APIs (os.pathsep-separated)\n"
|
||
" BRAINSTEM2_DEV_ROOT BrainStem Development Kit root (finds bin/arc, api/lib/BrainStem2)\n"
|
||
" REFLEX_SDK_ROOT, FIWI_BRAINSTEM_SDK — same idea as BRAINSTEM2_DEV_ROOT\n"
|
||
"\n"
|
||
f"SDK (includes arc): {BRAINSTEM_SDK_DOWNLOAD_URL}\n"
|
||
"\n"
|
||
"Example:\n"
|
||
" export BRAINSTEM2_DEV_ROOT=~/BrainStem2\n"
|
||
" fiwi.py reflex compile reflex/inrush.reflex reflex/inrush.map\n"
|
||
" ./scripts/compile_inrush_reflex.sh\n"
|
||
f"\nLanguage reference: {REFLEX_DOC_URL}\n"
|
||
)
|
||
|
||
|
||
def cli_main(argv: list[str]) -> int:
|
||
"""Entry for ``fiwi.py reflex …`` (invoked before BrainStem Python load)."""
|
||
if not argv or argv[0].lower() in ("help", "-h", "--help"):
|
||
print_reflex_cli_help()
|
||
return 0
|
||
cmd = argv[0].lower()
|
||
if cmd == "doc":
|
||
print(REFLEX_DOC_URL, flush=True)
|
||
return 0
|
||
if cmd == "which":
|
||
arc = find_arc()
|
||
print(f"arc: {arc or '(not found)'}", flush=True)
|
||
incs = default_reflex_include_dirs()
|
||
if incs:
|
||
print("include (-I):", flush=True)
|
||
for d in incs:
|
||
print(f" {d}", flush=True)
|
||
else:
|
||
print("include (-I): (none — set FIWI_REFLEX_INCLUDE or BRAINSTEM2_DEV_ROOT)", flush=True)
|
||
return 0
|
||
if cmd != "compile":
|
||
print(f"fiwi reflex: unknown subcommand {argv[0]!r}", file=sys.stderr, flush=True)
|
||
print("Try: fiwi.py reflex help", file=sys.stderr, flush=True)
|
||
return 2
|
||
rest = argv[1:]
|
||
if not rest:
|
||
print("fiwi reflex: compile requires <source.reflex>", file=sys.stderr, flush=True)
|
||
return 2
|
||
extra: list[str] = []
|
||
for i, a in enumerate(rest):
|
||
if a == "--":
|
||
extra = rest[i + 1 :]
|
||
rest = rest[:i]
|
||
break
|
||
src_s = rest[0]
|
||
out_s = rest[1] if len(rest) >= 2 else None
|
||
verbose = os.environ.get("FIWI_REFLEX_VERBOSE", "").strip() in ("1", "yes", "true")
|
||
return reflex_compile(Path(src_s), Path(out_s) if out_s else None, extra_argv=extra or None, verbose=verbose)
|