Skip to content
Merged
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
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def stamp_api_version():

setup(
name="science-synapse",
version="2.7.4",
version="2.7.5",
description="Client library and CLI for the Synapse API",
author="Science Team",
author_email="team@science.xyz",
Expand Down
21 changes: 16 additions & 5 deletions synapse/cli/gateware.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,14 @@ def run_gateware_build(
) -> str:
"""Invoke ``axon-peripheral-sdk build`` inside the gateware container.

Returns the absolute path to the newest ``sdk_*.bit`` emitted under
``<peripheral_dir>/src/gateware/build/bitstreams/``.
Returns the absolute path to the newest ``sdk_*_extracted.bit`` emitted
under ``<peripheral_dir>/src/gateware/build/bitstreams/``. This is the
*extracted* bitstream variant the SDK writes alongside the raw
``sdk_*.bit``; the extracted form is what gets flashed to the probe, and
it carries its own same-stem ``.summary.json`` (same schema as the raw
one). Selecting it specifically — rather than the broader ``sdk_*.bit``
glob, which would also match the extracted file and pick by mtime —
keeps the choice deterministic.

Raises:
LicenseUnsetError: if ``LM_LICENSE_FILE`` is unset (propagated from
Expand Down Expand Up @@ -162,13 +168,18 @@ def run_gateware_build(
subprocess.run(argv, check=True)

bit_glob = os.path.join(
abs_peripheral_dir, "src", "gateware", "build", "bitstreams", "sdk_*.bit"
abs_peripheral_dir,
"src",
"gateware",
"build",
"bitstreams",
"sdk_*_extracted.bit",
)
matches = glob.glob(bit_glob)
if not matches:
raise FileNotFoundError(
"axon-peripheral-sdk build completed but no sdk_*.bit was emitted "
"under src/gateware/build/bitstreams/"
"axon-peripheral-sdk build completed but no sdk_*_extracted.bit was "
"emitted under src/gateware/build/bitstreams/"
)

matches.sort(key=os.path.getmtime, reverse=True)
Expand Down
65 changes: 53 additions & 12 deletions synapse/tests/cli/test_gateware_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@
-w /home/workspace <license-args> <image_tag> /bin/bash -lc
'axon-peripheral-sdk build --project src/gateware'`.
3. Non-zero exit -> raises subprocess.CalledProcessError.
4. After success, globs <peripheral_dir>/src/gateware/build/bitstreams/sdk_*.bit
and returns the newest by mtime (warns on multi-match).
5. Empty glob -> FileNotFoundError with message mentioning "sdk_*.bit".
4. After success, globs
<peripheral_dir>/src/gateware/build/bitstreams/sdk_*_extracted.bit
and returns the newest by mtime (warns on multi-match). The *extracted*
variant is what gets flashed; the raw sdk_*.bit is ignored.
5. Empty glob -> FileNotFoundError with message mentioning
"sdk_*_extracted.bit".

Sub-phase 4.4 (Tester): the xfail marker is removed — these tests now run
as live AC-6 acceptance gates and must fail until the Implementer lands
Expand Down Expand Up @@ -75,9 +78,9 @@ def test_runner_builds_docker_run_argv_with_project_flag(

def fake_run(argv, *args, **kwargs):
recorded.append(list(argv))
# Drop a fake .bit so the post-run glob succeeds.
# Drop a fake extracted .bit so the post-run glob succeeds.
bs = _bitstreams_dir(pd)
bit = os.path.join(bs, "sdk_topbuild.bit")
bit = os.path.join(bs, "sdk_topbuild_extracted.bit")
with open(bit, "w") as fp:
fp.write("bitstream")
return subprocess.CompletedProcess(argv, 0, b"", b"")
Expand Down Expand Up @@ -119,8 +122,8 @@ def fake_run(argv, *args, **kwargs):
sdk_cmd = argv[bash_idx + 2]
assert sdk_cmd == "axon-peripheral-sdk build --project src/gateware"

# And the returned path is the .bit we dropped.
assert result.endswith("sdk_topbuild.bit")
# And the returned path is the extracted .bit we dropped.
assert result.endswith("sdk_topbuild_extracted.bit")


# ---------------------------------------------------------------------------
Expand All @@ -136,7 +139,7 @@ def test_runner_forwards_floating_license_arg(gateware, peripheral_dir, monkeypa
def fake_run(argv, *args, **kwargs):
recorded.append(list(argv))
bs = _bitstreams_dir(pd)
with open(os.path.join(bs, "sdk_topbuild.bit"), "w") as fp:
with open(os.path.join(bs, "sdk_topbuild_extracted.bit"), "w") as fp:
fp.write("x")
return subprocess.CompletedProcess(argv, 0, b"", b"")

Expand Down Expand Up @@ -195,11 +198,11 @@ def fake_run(argv, *args, **kwargs): # pragma: no cover - must NOT be called
def test_runner_returns_newest_bit_when_multiple_emitted(
gateware, peripheral_dir, monkeypatch
):
"""Case 11: glob with two .bit files of different mtimes -> newest wins."""
"""Case 11: glob with two extracted .bit files of different mtimes -> newest wins."""
pd, license_file = peripheral_dir
bs = _bitstreams_dir(pd)
older = os.path.join(bs, "sdk_old.bit")
newer = os.path.join(bs, "sdk_new.bit")
older = os.path.join(bs, "sdk_old_extracted.bit")
newer = os.path.join(bs, "sdk_new_extracted.bit")
with open(older, "w") as fp:
fp.write("old")
time.sleep(0.05)
Expand Down Expand Up @@ -250,4 +253,42 @@ def fake_run(argv, *args, **kwargs):
env={"LM_LICENSE_FILE": str(license_file)},
)

assert "sdk_*.bit" in str(excinfo.value)
assert "sdk_*_extracted.bit" in str(excinfo.value)


# ---------------------------------------------------------------------------
# Extracted-bitstream selection: the raw sdk_*.bit is ignored
# ---------------------------------------------------------------------------


def test_runner_ignores_raw_bit_and_picks_extracted(
gateware, peripheral_dir, monkeypatch
):
"""When both the raw sdk_*.bit and its sdk_*_extracted.bit sit side by
side, the runner must select the extracted variant — even when the raw
.bit is newer by mtime (the raw glob would otherwise win on mtime).
"""
pd, license_file = peripheral_dir
bs = _bitstreams_dir(pd)
raw = os.path.join(bs, "sdk_topbuild.bit")
extracted = os.path.join(bs, "sdk_topbuild_extracted.bit")
with open(raw, "w") as fp:
fp.write("raw")
with open(extracted, "w") as fp:
fp.write("extracted")
# Make the raw .bit strictly newer so a mtime-only selection would pick it.
os.utime(extracted, (1_000_000, 1_000_000))
os.utime(raw, (2_000_000, 2_000_000))

def fake_run(argv, *args, **kwargs):
return subprocess.CompletedProcess(argv, 0, b"", b"")

monkeypatch.setattr(gateware.subprocess, "run", fake_run)

result = gateware.run_gateware_build(
str(pd),
"myplugin-gateware:latest-arm64",
env={"LM_LICENSE_FILE": str(license_file)},
)

assert os.path.abspath(result) == os.path.abspath(extracted)
Loading