JP builds with 3.14 now, so the workflows do too. The lint job moves from 3.10, the
packaging build job and the release's site rebuild and PyPI build from 3.12, and the
installers from 3.10. The test matrix is untouched: it still runs every version from
3.10 up, because 3.10 is still the floor.
The installers' comment said the oldest Python was chosen on purpose, for reach. The
reach that matters on Linux comes from glibc, and that comes from the runner, so the
Linux legs stay on ubuntu-22.04 and the comment now says that. The binaries carry 3.14
from the next release; 0.9.0's were built on 3.10.
The changelog gains the 3.14 entry JP wrote in another session, plus a line for the
downloads.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Windows greets an unsigned one-file PyInstaller build as a stranger every
release: SmartScreen warns, and antivirus engines flag it on sight. A
signature gives Windows a publisher to accumulate trust against instead.
SignPath Foundation issue the certificate free to open-source projects and
sign on their own infrastructure, so no key exists on the runner. Their
condition is that the artifact provably came from this repository's CI,
which is why the binary is handed over as a GitHub artifact id rather than
uploaded -- SignPath reads the workflow run behind it.
Signing sits between the build's smoke test and the naming step, so the
checksum describes the file people actually download. The unsigned draft
is the one artifact the release must not attach, so it is named outside
the meshterm-* pattern the release now downloads by.
Both steps skip wholesale until SIGNPATH_ORGANIZATION_ID is set, so the
Windows binary keeps building unsigned in the meantime and a fork with no
SignPath project of its own is unaffected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The five installers bundled Cascadia's font licence and nothing else --
not MeshTerm's own LICENSE and NOTICE, not the MIT/BSD/PSF notices
Apache-2.0 and those licences all want kept with a redistributed copy.
packaging/notices.py walks the mesh-term distribution's actual runtime
dependency closure through importlib.metadata (markers evaluated for
this platform, the dev extra excluded), gathers each package's licence
text, and fails the build loudly if one is missing rather than shipping
a silent gap -- which is exactly what it found: pyserial and the nine
winrt-* packages bleak's Windows backend pulls in all declare a licence
but ship no licence file in their wheel at all. Their real text, pulled
from each project's own repository, is vendored under
packaging/vendored-licenses/ for those two documented, verified gaps;
anything else missing still stops the build.
The spec generates THIRD-PARTY-NOTICES.txt fresh into the build
directory on every run and bundles it at the archive root alongside
LICENSE and NOTICE. installers.yml uploads the same three files once,
from the linux-x64 leg, so they land beside the downloads on the
release page too, without extracting an installer just to read them.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Hatchling's default sdist is everything git does not ignore, which packed the
Claude skills, the GitHub configuration, the PyInstaller spec, the device-side
scripts and the developer environment template into an archive whose only job
is to build and test the wheel. An exclude list keeps the package, the tests
and their fixture, the manual and the files a build reads. The build backend
floor is hatchling 1.26, the first to write the PEP 639 licence fields into the
metadata; an older one built a wheel that said nothing about the licence.
Workflow comments that reasoned from a private repo's metered minutes are
reworded for the public one, a skill's usage line stops naming one machine's
venv, and the scheduler's lock file is ignored in the tree rather than only in
one clone's info/exclude.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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>
`macos-13` is retired. GitHub's image list starts at macOS 14 now, and the
current x86_64 labels are `macos-15-intel` and `macos-26-intel`.
The failure mode is worth writing down, because it is not the one you would
expect: a job asking for a retired label does not fail. It queues, forever,
and holds the entire run open behind it — so the four legs that had already
built sat there under a run that never finished, and anything waiting on that
workflow would have waited with them. That includes the release workflow, which
calls it, so a tag pushed today would have produced no release and no error to
explain why.
`macos-15-intel` rather than `-26-intel`, for the same reason the Python in this
matrix is 3.10: built against an older system, runs on more machines.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pushing a version tag now builds the binaries and puts them on the Releases page,
with a combined SHA256SUMS and notes lifted out of the changelog section for that
version. It stays inside GitHub — while the repository is private the release is
private too — and it is emphatically not the PyPI publish, which is `release.yml`
and still needs a person to run it on purpose.
`installers.yml` gained a `workflow_call` trigger so the release calls it instead
of keeping a second copy of the matrix. Two matrices would agree for about a month
and then quietly stop, and the files people download should be built exactly the
way the ones CI checks on every push are.
The README now leads its install section with the download rather than with pip,
because most people who want MeshTerm have a radio and a terminal and not a Python
toolchain. It also says plainly that the first launch will be blocked: these builds
are unsigned, macOS will refuse them until the quarantine attribute is cleared, and
Windows will show its SmartScreen warning. Signing costs real money on both
platforms, so the honest move is to say so and give people the checksum file rather
than let them think something is wrong.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first CI run went red on `ruff format --check`, on a tree that was clean on
this machine. Three separate problems, all found by pushing.
`ruff>=0.4` is not a pin. CI installed 0.16.6 where the laptop had 0.15.18, and
0.16 formats Python code blocks inside Markdown while 0.15 leaves them alone — so
the two disagreed about whether the repository was formatted, which is the one
thing a formatter exists to settle. Pinned to the 0.16 line. Dependabot will
offer the next one, and CI will say whether it is safe.
The file it tripped over is `docs/survey-ui-consistency.md`, which quotes four
lines of a dialog call as it looked on 22 August. That excerpt is a fragment —
two arguments, no call around them — so the formatter parsed it as two statements
and rewrote them into a tuple and an assignment that never existed anywhere. The
surveys are dated records of a past state, so `docs/` is now excluded: a
formatter editing a quotation is not tidying, it is falsifying.
And the installer workflow was firing for branches it had no business building.
Dependabot opened three action bumps within a minute of the push; each one edits
`.github/workflows/installers.yml`, each matched its own path filter, and each
started a five-platform build. `branches: [main]` stops it.
Everything else was green on the first try, including the part that was actually
uncertain: all seven test legs passed, macOS among them, and the installers built
on Windows, macOS on Apple silicon, and Linux on both x64 and arm64 — so the
uConsole build works and the ARM runner was available after all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Most people who want MeshTerm have a radio and a terminal, not a Python
toolchain. This is the install for them: one file, download it, run it.
`packaging/meshterm.spec` freezes the app with PyInstaller in console mode —
which is not a detail, since a windowed build of a terminal program starts with
nowhere to draw and exits. The assets land at `meshterm/assets` inside the bundle
so that `Path(__file__).parent.parent / "assets"` keeps resolving exactly as it
does in a checkout, and bleak's per-platform backends are named as hidden imports
because nothing static ever sees the one that actually gets used. UPX is off: a
few megabytes are not worth being flagged by antivirus on a first release.
The build was run here rather than only written. The Windows executable comes out
at 14 MB, runs from a directory with no source tree in sight, and draws the
specimen card — which is the real test, because the specimen reads the asset
files.
`installers.yml` does the same on five machines, because frozen builds cannot be
cross-compiled. Windows, macOS on both Apple silicon and Intel (plenty of people
still have one, and an arm64 build will not run for them), and Linux on x64 and
ARM64 — the last for the uConsole. Each leg runs the binary from a temporary
directory before uploading it, so a build that only works beside its own source
fails on the runner instead of on somebody's laptop, and each one gets a checksum
written beside it. It publishes nothing; attaching these to a release stays a
separate decision.
Also here: `meshterm --help` now opens with the same sentence as the package
summary, rather than a fourth description of the project written in its own way.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>