From a7abe9faa7142a5fb895e604128150c3bdded78b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 20:36:30 +0000 Subject: [PATCH 1/3] package: ship the radio model, and find Renode on Windows Every emulated node looks up radioserver before it looks up either emulator, and no bundle has ever contained one: nothing built it, no release published it, and no packaging job staged it. 0.0.1 through 0.0.3 shipped a QEMU and a Renode that nothing could drive, with a README saying emulated boards worked out of the box. All three jobs now build it from meshcore-native - the repository the chip model lives in, so a native node and an emulated one stay the same VirtualSX1262 - and a failure is fatal rather than a warning. The way this stayed hidden for three releases was a green build. lookupTool also learns the versioned renode_*-portable layout by glob. Linux and macOS bridge that with a symlink; a zip cannot carry one, so Windows shipped a Renode two directories from anywhere the search looked. Needs MeshBench/meshcore-native#4, which is what builds radioserver and what makes it compile for Windows at all. --- .github/workflows/package.yml | 77 +++++++++++++++++++++-- README.md | 29 ++++++--- docs/tools-manifest.md | 23 ++++--- internal/firmware/emulated_lookup_test.go | 49 +++++++++++++++ internal/firmware/emulated_node.go | 9 +++ 5 files changed, 164 insertions(+), 23 deletions(-) create mode 100644 internal/firmware/emulated_lookup_test.go diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 509c4ff9..c02199b7 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -30,6 +30,12 @@ on: type: boolean default: true +# The radio model every emulated node starts before it starts an emulator. It +# lives in meshcore-native because the chip model does, and it is built here +# rather than downloaded because no release publishes it. +env: + MESHCORE_NATIVE_REF: main + jobs: linux: # The oldest base that can link. This used to be 24.04 because cimgui-go @@ -126,6 +132,24 @@ jobs: for f in *.tar.gz *.tar.xz; do [ -e "$f" ] || continue; tar xf "$f" && rm "$f"; done ls -la + - name: The radio model + # Every emulated node looks this up *before* either emulator, so a + # bundle without it ships a QEMU and a Renode that nothing can drive. + # 0.0.1 through 0.0.3 shipped exactly that: two emulators, no radio, + # and a README claiming emulated boards worked. + # + # Fatal rather than a warning, for the same reason the Mac's emulator + # fetch is: a bundle that cannot emulate is a broken artifact, and the + # way this stayed hidden was a green build. + run: | + set -e + git clone --quiet --depth 1 --branch "$MESHCORE_NATIVE_REF" \ + https://github.com/MeshBench/meshcore-native.git /tmp/meshcore-native + (cd /tmp/meshcore-native && ./build.sh radioserver out) + cp /tmp/meshcore-native/out/radioserver-linux-amd64 dist/meshbench/radioserver + chmod +x dist/meshbench/radioserver + dist/meshbench/radioserver 2>&1 | head -1 || true + - name: Fonts and fixtures run: | set -e @@ -185,6 +209,11 @@ jobs: qemu-system-xtensa is a symlink on purpose. QEMU resolves its own path to find its data files, so a bare copy of the binary will not run. + radioserver is the SX1262 model the emulated firmware clocks over a + socket. It is the same chip object a native node uses in process, so + an emulated node and a native one are the same radio. Both emulators + need it; neither works without it. + Native MeshCore builds and board images are downloaded on first use and cached under ~/.cache/meshcoresim. Nothing else is needed. @@ -242,7 +271,12 @@ jobs: APP=dist/AppDir mkdir -p $APP/usr/bin $APP/usr/share/applications \ $APP/usr/share/metainfo $APP/usr/share/meshbench - cp dist/meshbench/meshbench $APP/usr/bin/ + # radioserver too: it is 40 KB, and without it an installed + # MeshBench cannot emulate even when the user supplies their own + # QEMU. The emulators themselves stay out of the AppImage and the + # .deb on size grounds - 110 MB against 26 - so those two are the + # application, and the tarball is the batteries-included download. + cp dist/meshbench/meshbench dist/meshbench/radioserver $APP/usr/bin/ cp -r dist/meshbench/fixtures dist/meshbench/fonts \ dist/meshbench/LICENCES $APP/usr/share/meshbench/ cp packaging/meshbench.desktop $APP/usr/share/applications/ @@ -279,7 +313,7 @@ jobs: mkdir -p $d cp packaging/icons/meshbench-${px}.png $d/io.github.meshbench.meshbench.png done - cp $APP/usr/bin/meshbench $DEB/usr/bin/ + cp $APP/usr/bin/meshbench $APP/usr/bin/radioserver $DEB/usr/bin/ cp -r $APP/usr/share/meshbench/* $DEB/usr/share/meshbench/ cp packaging/meshbench.desktop $DEB/usr/share/applications/ cp packaging/io.github.meshbench.meshbench.metainfo.xml $DEB/usr/share/metainfo/ @@ -394,6 +428,16 @@ jobs: ln -sf ../Resources/emulators/qemu/bin/qemu-system-xtensa "$APP/Contents/MacOS/qemu-system-xtensa" [ -f "$APP/Contents/Resources/emulators/Release/osx-arm64/Renode" ] && \ ln -sf ../Resources/emulators/Release/osx-arm64/Renode "$APP/Contents/MacOS/renode" + # The radio model, built here because this is the only arm64 Mac in + # the pipeline and no release publishes one. A real file rather than + # a link: it is 40 KB and resolves nothing from its own path. + git clone --quiet --depth 1 --branch "$MESHCORE_NATIVE_REF" \ + https://github.com/MeshBench/meshcore-native.git /tmp/meshcore-native + rm -rf /tmp/meshcore-native/out + (cd /tmp/meshcore-native && ./build.sh radioserver out) + cp /tmp/meshcore-native/out/radioserver-darwin-arm64 "$APP/Contents/MacOS/radioserver" + chmod +x "$APP/Contents/MacOS/radioserver" + rm -rf /tmp/meshcore-native # Signing has to happen again: the bundle was signed before these # arrived, and adding files to a signed bundle invalidates it. codesign --force --deep --sign - "$APP" @@ -485,6 +529,20 @@ jobs: for f in *.zip; do [ -e "$f" ] || continue; unzip -q "$f" && rm "$f"; done ls -la cd ../.. + + # The radio model, cross-compiled with the same mingw that built the + # application. It is statically linked and wants no DLLs, and it + # takes the TCP transport - the simulator asks for ":0" on Windows + # for both emulators, because Windows has no Unix socket mingw can + # reach. + git clone --quiet --depth 1 --branch "$MESHCORE_NATIVE_REF" \ + https://github.com/MeshBench/meshcore-native.git /tmp/meshcore-native + TARGET_OS=windows TARGET_ARCH=amd64 \ + CXX=x86_64-w64-mingw32-g++ CC=x86_64-w64-mingw32-gcc \ + /tmp/meshcore-native/build.sh radioserver /tmp/meshcore-native/out + cp /tmp/meshcore-native/out/radioserver-windows-amd64.exe \ + dist/meshbench/radioserver.exe + file dist/meshbench/radioserver.exe cat > dist/meshbench/README.txt <<'TXT' MeshBench for Windows @@ -493,10 +551,17 @@ jobs: says which build this is. Native MeshCore firmware is downloaded on first use and cached. - The QEMU and Renode emulators are in this zip, so emulated ESP32 - and nRF52 boards work out of the box. They are MeshBench builds, - not upstream ones - ours carry an SX1262 device and a SEVONPEND - fix that published firmware needs. + + The QEMU and Renode emulators are in this zip, with radioserver.exe, + the SX1262 model they both clock over a socket. All three are found + automatically. The emulators are MeshBench builds and not upstream + ones - ours carry an SX1262 device and a SEVONPEND fix that + published firmware needs. + + Emulated boards on Windows are newer than the rest of this bundle + and have had less time on real hardware than the Linux and macOS + ones. If a board will not start, meshbench.exe workbench run from a + terminal prints what it could not find. Licences are in LICENCES\, and in the application under Help > Licences & attributions. diff --git a/README.md b/README.md index 4f31eac8..c2bf83cb 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,10 @@ sudo apt install ./meshbench_*_amd64.deb meshbench workbench # or find MeshBench in the applications menu ``` -**Tarball** — the same application plus the QEMU and Renode emulators, for -emulating real board firmware offline: +**Tarball** — the same application plus the QEMU and Renode emulators and the +radio model they clock, for emulating real board firmware offline. The +AppImage and the `.deb` carry the radio model but not the emulators, which are +110 MB of the tarball's size: ```bash tar xzf meshbench-linux-x86_64.tar.gz @@ -80,8 +82,10 @@ Unzip `meshbench-*-windows-x86_64.zip` anywhere and run `meshbench.exe`. Windows SmartScreen will warn about an unrecognised publisher for the same reason macOS does — the binary is unsigned. Click **More info → Run anyway**. -Emulated ESP32 and nRF52 boards work on Windows, but the emulators are not in -the zip yet; native firmware, the channel, terrain and planning all are. +The zip carries the QEMU and Renode emulators and the radio model, so emulated +ESP32 and nRF52 boards need nothing else installed. That path is newer on +Windows than on Linux and macOS: if a board will not start, run +`meshbench.exe workbench` from a terminal and it prints what it could not find. ### What arrives later, over the network @@ -230,11 +234,15 @@ at all. ### Setting one up today -Until this is packaged, an emulated node needs binaries that are not on any -distribution: a QEMU carrying our SX1262 device for the ESP32 boards, a Renode -carrying the SEVONPEND fix for the nRF52 ones, and the radio model both talk to. -Put them where the application looks and nothing else is needed — no environment -variables, no flags. +**From a release you do not have to do any of this.** The tarball, the dmg and +the Windows zip carry all three binaries beside the application, where it looks +for them first. + +Building from source is the case below. An emulated node needs binaries that +are on no distribution: a QEMU carrying our SX1262 device for the ESP32 boards, +a Renode carrying the SEVONPEND fix for the nRF52 ones, and the radio model +both talk to. Put them where the application looks and nothing else is needed — +no environment variables, no flags. ```bash mkdir -p ~/.cache/meshcoresim/tools @@ -250,7 +258,8 @@ same tools directory. A symlink is right for QEMU: it finds its own data files by resolving its real path, so a bare copy of the binary will not run. `radioserver` builds from -`meshcore-native` against `VirtualSX1262.cpp` with no other dependencies. +`meshcore-native` with `./build.sh radioserver out` — it wants neither a +MeshCore checkout nor Crypto, only the chip model beside it. Then open the firmware library, download a board image, and set a node's role to it. The library only offers boards with verified wiring, so anything it lists diff --git a/docs/tools-manifest.md b/docs/tools-manifest.md index d02c6429..d94f15e8 100644 --- a/docs/tools-manifest.md +++ b/docs/tools-manifest.md @@ -63,19 +63,28 @@ them dates the installer. ## What a released bundle ships -Linux only for now, by decision: mac and Windows are skipped matrix rows with -reasons in the file, not absences. +All three platforms, as of the packaging work: a Linux tarball, an arm64 dmg +and a Windows zip. meshbench the application qemu-system-xtensa beside it, or symlinked + renode beside it, or symlinked radioserver beside it A **symlink** is correct for QEMU and wrong for a copy: it resolves its own path -to find its data files, so a bare copy of the binary will not run. - -Renode is not bundled yet. It is a 60 MB portable package with its own runtime, -needed only for the nRF52 boards, and the decision on whether that belongs in -the default download is open. +to find its data files, so a bare copy of the binary will not run. The Windows +zip cannot carry a symlink at all, so nothing is linked there and `lookupTool` +searches the emulators' own unpacked layouts — `qemu/bin/` and the versioned +`renode_*-portable/` — instead. + +**radioserver is the one every emulated node needs**, ESP32 or nRF52, and it is +looked up before either emulator. Nothing built it until `build.sh` grew a +`radioserver` target, which is how 0.0.1 through 0.0.3 shipped both emulators +and no radio. + +The AppImage and the `.deb` carry the application and radioserver but not the +emulators: those are 110 MB against a 26 MB AppImage, and the tarball is the +batteries-included download for people who want them. ## What is checked, and when diff --git a/internal/firmware/emulated_lookup_test.go b/internal/firmware/emulated_lookup_test.go new file mode 100644 index 00000000..9759fb9c --- /dev/null +++ b/internal/firmware/emulated_lookup_test.go @@ -0,0 +1,49 @@ +package firmware + +// White-box, because lookupTool is where the bundles are found or not found and +// the layouts it has to know about are not visible from outside the package. + +import ( + "os" + "path/filepath" + "runtime" + "testing" +) + +// Renode unpacks into renode_-portable/, and a zip cannot carry the +// symlink the Linux tarball and the macOS bundle use. The Windows bundle +// therefore ships an emulator that was found by nobody until the versioned +// directory was searched too: the workbench reported "renode not found" with +// renode.exe sitting two directories away. +func TestLookupToolFindsRenodeInItsVersionedDirectory(t *testing.T) { + self, err := os.Executable() + if err != nil { + t.Skipf("no executable path here: %v", err) + } + name := "renode" + if runtime.GOOS == "windows" { + name += ".exe" + } + // Beside the test binary, because that is the directory lookupTool looks + // in - the same rule the shipped binary is subject to. + dir := filepath.Join(filepath.Dir(self), "renode_1.16.1-portable") + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Skipf("cannot write beside the test binary: %v", err) + } + t.Cleanup(func() { os.RemoveAll(dir) }) + bin := filepath.Join(dir, name) + if err := os.WriteFile(bin, []byte("#!/bin/sh\n"), 0o755); err != nil { + t.Fatal(err) + } + + // The environment variable would short-circuit the search this is testing. + t.Setenv(EnvRenode, "") + + got, err := lookupTool(EnvRenode, "renode") + if err != nil { + t.Fatalf("renode not found with %s present: %v", bin, err) + } + if got != bin { + t.Errorf("found %s, wanted the bundled %s", got, bin) + } +} diff --git a/internal/firmware/emulated_node.go b/internal/firmware/emulated_node.go index 4ef56712..cce9f39c 100644 --- a/internal/firmware/emulated_node.go +++ b/internal/firmware/emulated_node.go @@ -347,6 +347,15 @@ func lookupTool(env, name string) (string, error) { subdirs := []string{"", "qemu/bin", "qemu-meshbench/bin"} if self, err := os.Executable(); err == nil { dir := filepath.Dir(self) + // Renode unpacks into a directory carrying its version, so the name + // changes with every release and cannot be listed above. Globbing for + // the shape is what the Linux tarball's symlink step already does; + // this is the same rule on the side that has to find it. + if matches, err := filepath.Glob(filepath.Join(dir, "renode*-portable")); err == nil { + for _, m := range matches { + subdirs = append(subdirs, filepath.Base(m)) + } + } for _, sub := range subdirs { for _, cand := range candidates { if p := filepath.Join(dir, sub, cand); fileExists(p) { From b7199402453a0c96431fd49ea8b55b27a75e8257 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 14 Aug 2026 20:58:06 +0000 Subject: [PATCH 2/3] firmware: say which transport goes with which emulator The comment led with "a TCP port on Windows, a Unix socket everywhere else" and the condition under it then sent Renode over TCP on every platform. Both halves were right; only the summary was wrong. --- internal/firmware/emulated_node.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/firmware/emulated_node.go b/internal/firmware/emulated_node.go index cce9f39c..851060a3 100644 --- a/internal/firmware/emulated_node.go +++ b/internal/firmware/emulated_node.go @@ -195,7 +195,8 @@ func (e *EmulatedNode) Start(ctx context.Context, bridge string) error { if err := os.MkdirAll(e.Dir, 0o755); err != nil { return err } - // A TCP port on Windows, a Unix socket everywhere else. + // A Unix socket for QEMU on Linux and macOS; a TCP port for Renode + // anywhere, and for either emulator on Windows. // // Renode has always used a port: it runs on Mono, whose Unix domain // socket support is not worth betting a node on. QEMU used a socket file, From b093e325f92c19627e690dc17926b6876966b4e8 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 14 Aug 2026 21:39:04 +0000 Subject: [PATCH 3/3] package: fetch radioserver as a release, like the emulators meshcore-native#4 is merged and radioserver-v1 is published there (linux-amd64, darwin-arm64, windows-amd64.exe), built off the merge commit and smoke-tested end to end before publishing. Fetching it now matches how qemu and renode are already staged, rather than cloning meshcore-native and rebuilding from source in every job. meshcore-native has no rolling "latest" release - its automation republishes a per-role MeshCore version there constantly - so the macOS job's curl fetch needed a tag-specific lookup rather than the existing one built around /releases/latest. --- .github/workflows/package.yml | 71 ++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index c02199b7..6ff982d8 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -30,12 +30,6 @@ on: type: boolean default: true -# The radio model every emulated node starts before it starts an emulator. It -# lives in meshcore-native because the chip model does, and it is built here -# rather than downloaded because no release publishes it. -env: - MESHCORE_NATIVE_REF: main - jobs: linux: # The oldest base that can link. This used to be 24.04 because cimgui-go @@ -138,15 +132,19 @@ jobs: # 0.0.1 through 0.0.3 shipped exactly that: two emulators, no radio, # and a README claiming emulated boards worked. # - # Fatal rather than a warning, for the same reason the Mac's emulator - # fetch is: a bundle that cannot emulate is a broken artifact, and the - # way this stayed hidden was a green build. + # Fetched as a release asset, the same as the emulators - radioserver-v1 + # is meshcore-native#4 built once and published, not rebuilt from source + # in every job that needs it. Fatal rather than a warning, for the same + # reason the Mac's emulator fetch is: a bundle that cannot emulate is a + # broken artifact, and the way this stayed hidden was a green build. + env: + GH_TOKEN: ${{ github.token }} run: | set -e - git clone --quiet --depth 1 --branch "$MESHCORE_NATIVE_REF" \ - https://github.com/MeshBench/meshcore-native.git /tmp/meshcore-native - (cd /tmp/meshcore-native && ./build.sh radioserver out) - cp /tmp/meshcore-native/out/radioserver-linux-amd64 dist/meshbench/radioserver + gh release download radioserver-v1 --repo MeshBench/meshcore-native \ + --pattern 'radioserver-linux-amd64' --dir dist/meshbench --clobber \ + || { echo "::error::no radioserver-v1 release for meshcore-native"; exit 1; } + mv dist/meshbench/radioserver-linux-amd64 dist/meshbench/radioserver chmod +x dist/meshbench/radioserver dist/meshbench/radioserver 2>&1 | head -1 || true @@ -415,6 +413,19 @@ jobs: echo "fetching $url" curl -fsSL -O "$url" } + # meshcore-native's "latest" is whichever per-role MeshCore version + # was published most recently, which is not this. radioserver-v1 + # is a fixed tag of its own, so it is fetched by tag rather than by + # riding a /latest that moves out from under it within a day. + fetchTag() { + url=$(curl -fsSL "https://api.github.com/repos/$1/releases/tags/$2" \ + | python3 -c "import sys,json,fnmatch; r=json.load(sys.stdin); print(next((a['browser_download_url'] for a in r['assets'] if fnmatch.fnmatch(a['name'], '$3')), ''))") + if [ -z "$url" ]; then + echo "::error::no asset matching $3 in $1's $2 release"; exit 1 + fi + echo "fetching $url" + curl -fsSL -O "$url" + } APP=dist/macos/MeshBench.app mkdir -p "$APP/Contents/Resources/emulators" cd "$APP/Contents/Resources/emulators" @@ -428,16 +439,11 @@ jobs: ln -sf ../Resources/emulators/qemu/bin/qemu-system-xtensa "$APP/Contents/MacOS/qemu-system-xtensa" [ -f "$APP/Contents/Resources/emulators/Release/osx-arm64/Renode" ] && \ ln -sf ../Resources/emulators/Release/osx-arm64/Renode "$APP/Contents/MacOS/renode" - # The radio model, built here because this is the only arm64 Mac in - # the pipeline and no release publishes one. A real file rather than - # a link: it is 40 KB and resolves nothing from its own path. - git clone --quiet --depth 1 --branch "$MESHCORE_NATIVE_REF" \ - https://github.com/MeshBench/meshcore-native.git /tmp/meshcore-native - rm -rf /tmp/meshcore-native/out - (cd /tmp/meshcore-native && ./build.sh radioserver out) - cp /tmp/meshcore-native/out/radioserver-darwin-arm64 "$APP/Contents/MacOS/radioserver" + # The radio model. A real file rather than a link: it is 40 KB and + # resolves nothing from its own path. + (cd "$APP/Contents/MacOS" && fetchTag MeshBench/meshcore-native radioserver-v1 radioserver-darwin-arm64) + mv "$APP/Contents/MacOS/radioserver-darwin-arm64" "$APP/Contents/MacOS/radioserver" chmod +x "$APP/Contents/MacOS/radioserver" - rm -rf /tmp/meshcore-native # Signing has to happen again: the bundle was signed before these # arrived, and adding files to a signed bundle invalidates it. codesign --force --deep --sign - "$APP" @@ -530,18 +536,15 @@ jobs: ls -la cd ../.. - # The radio model, cross-compiled with the same mingw that built the - # application. It is statically linked and wants no DLLs, and it - # takes the TCP transport - the simulator asks for ":0" on Windows - # for both emulators, because Windows has no Unix socket mingw can - # reach. - git clone --quiet --depth 1 --branch "$MESHCORE_NATIVE_REF" \ - https://github.com/MeshBench/meshcore-native.git /tmp/meshcore-native - TARGET_OS=windows TARGET_ARCH=amd64 \ - CXX=x86_64-w64-mingw32-g++ CC=x86_64-w64-mingw32-gcc \ - /tmp/meshcore-native/build.sh radioserver /tmp/meshcore-native/out - cp /tmp/meshcore-native/out/radioserver-windows-amd64.exe \ - dist/meshbench/radioserver.exe + # The radio model. It takes the TCP transport - the simulator asks + # for ":0" on Windows for both emulators, because Windows has no + # Unix socket mingw can reach. + cd dist/meshbench + gh release download radioserver-v1 --repo MeshBench/meshcore-native \ + --pattern 'radioserver-windows-amd64.exe' --dir . --clobber \ + || { echo "::error::no radioserver-v1 release for meshcore-native"; exit 1; } + mv radioserver-windows-amd64.exe radioserver.exe + cd ../.. file dist/meshbench/radioserver.exe cat > dist/meshbench/README.txt <<'TXT' MeshBench for Windows