Files
MeshTerm/.github/workflows/installers.yml
T
Jean-Pierre MartineauandClaude Opus 5 141698678e The downloadable builds actually have the app in them
The Windows build launched, printed help, drew the specimen card, and showed a
menu containing one item: Quit. Not a permissions problem and nothing to do with
Bluetooth — the tools were simply not in the bundle.

`meshterm/tools/__init__.py` finds every tool with `pkgutil.iter_modules` and
imports it so its `@register` decorator fires. Nothing static ever names a tool
module, so PyInstaller had no reason to include one, and the registry came up
empty: no menu entries, no CLI subcommands, an app that starts and does nothing.
`collect_submodules("meshterm.tools")` fixes it, which is the same treatment
`bleak.backends` already had for the same reason — the spec knew about other
people's dynamic imports and not about ours.

The worse half is that CI said this was fine. The smoke test ran `--help` and
`specimen`, and a build with no tools at all passes both. It now runs a
tool-provided subcommand, so an empty registry fails the pipeline instead of
reaching a release page. Green tests that cannot see the defect are worse than no
tests, because they are believed.

`MESHTERM_HOME` comes out of the same session. Every copy of MeshTerm reads and
writes `~/.meshterm`, so trying a downloaded build meant pointing it at the same
34MB history database as the checkout it was built from, with no way to keep them
apart. The variable moves the whole directory. An empty or whitespace value falls
back rather than resolving to the current directory, because a shell exporting it
blank is common and scattering someone's history into their downloads folder is a
poor way to find that out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 13:49:37 -04:00

110 lines
4.6 KiB
YAML

# Standalone builds — a single file you download and run, with no Python installed.
#
# For most people who want MeshTerm, this is the install. They have a radio and a
# terminal, not a Python toolchain.
#
# Frozen builds cannot be cross-compiled: each one is made on the system it targets, so
# the matrix is the point of this file rather than an optimisation. Everything here
# uploads artifacts and nothing publishes — attaching them to a release stays a separate,
# deliberate act.
name: Installers
on:
workflow_dispatch:
# Callable, so the release workflow builds these binaries rather than keeping a second
# copy of the matrix that would drift from this one.
workflow_call:
# Only when the packaging itself changes. Three operating systems is not something to
# spend on every commit while the repository is private and minutes are metered.
push:
# `main` only. Without this, every Dependabot branch that touches this file — which
# is every action bump — starts a five-platform build, and three of those legs are
# billed at 2x and 10x.
branches: [main]
paths:
- "packaging/**"
- ".github/workflows/installers.yml"
permissions:
contents: read
concurrency:
group: installers-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: ${{ matrix.label }}
runs-on: ${{ matrix.os }}
timeout-minutes: 25
strategy:
fail-fast: false
matrix:
include:
- { os: windows-latest, label: windows-x64, ext: ".exe" }
- { os: macos-latest, label: macos-arm64, ext: "" }
# Apple silicon builds do not run on Intel Macs, and plenty of people still
# have one. `macos-15-intel` rather than `-26-intel` for the same reason the
# Python here is 3.10: built against an older system, runs on more machines.
# (Not `macos-13` — that image is retired, and a job asking for it does not
# fail, it queues forever and holds the whole run open behind it.)
- { os: macos-15-intel, label: macos-x64, ext: "" }
- { os: ubuntu-22.04, label: linux-x64, ext: "" }
# For the uConsole and other 64-bit ARM handhelds. ARM runners are free on
# public repositories; on a private one this leg may sit unclaimed until the
# repo is public. Drop the line if it blocks you.
- { os: ubuntu-22.04-arm, label: linux-arm64, ext: "" }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
# Oldest supported version on purpose: a binary built against an older glibc
# and an older Python runs on more machines than one built against the newest.
python-version: "3.10"
- run: pip install . pyinstaller
- name: Build
working-directory: packaging
run: pyinstaller --clean --noconfirm meshterm.spec
- name: Check it actually runs
shell: bash
run: |
bin="packaging/dist/meshterm${{ matrix.ext }}"
chmod +x "$bin" || true
# Run it from somewhere else entirely, so a build that only works next to the
# source tree fails here rather than on someone's laptop.
cd "$(mktemp -d)"
"$GITHUB_WORKSPACE/$bin" --help > /dev/null
# Reads the asset files, so a bundle missing its pages fails here.
"$GITHUB_WORKSPACE/$bin" specimen > /dev/null
# And a tool-provided subcommand, which is the check that matters. Every tool is
# found at runtime by `pkgutil.iter_modules`, so a bundle that omits them still
# starts, still prints help, still draws the specimen — and has an empty menu
# with nothing in it but Quit. `--help` passing proves almost nothing on its own.
"$GITHUB_WORKSPACE/$bin" contacts --help > /dev/null
echo "runs, finds its assets, and its tools registered"
- name: Name it after the version and the platform
shell: bash
run: |
version=$(python -c "import meshterm; print(meshterm.__version__)")
out="meshterm-${version}-${{ matrix.label }}${{ matrix.ext }}"
mv "packaging/dist/meshterm${{ matrix.ext }}" "$out"
# A checksum beside the binary, so anyone can tell they got what we built.
if command -v sha256sum > /dev/null; then
sha256sum "$out" > "$out.sha256"
else
shasum -a 256 "$out" > "$out.sha256"
fi
cat "$out.sha256"
- uses: actions/upload-artifact@v4
with:
name: meshterm-${{ matrix.label }}
path: |
meshterm-*
if-no-files-found: error