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>
This commit is contained in:
1 parent
8cc1cae213
commit
141698678e
7 files changed
+125
-18
No files matched your search
@@ -111,5 +111,9 @@ jobs:
|
||||
run: |
|
||||
python -m venv /tmp/fresh
|
||||
/tmp/fresh/bin/pip install --quiet dist/*.whl
|
||||
cd /tmp && /tmp/fresh/bin/meshterm --help > /dev/null
|
||||
echo "the wheel installs and the console script runs"
|
||||
cd /tmp
|
||||
/tmp/fresh/bin/meshterm --help > /dev/null
|
||||
# A tool-provided subcommand, not just --help: the tools are discovered at
|
||||
# runtime, so a broken package still answers --help perfectly well.
|
||||
/tmp/fresh/bin/meshterm contacts --help > /dev/null
|
||||
echo "the wheel installs, the console script runs, and its tools registered"
|
||||
@@ -78,8 +78,14 @@ jobs:
|
||||
# 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
|
||||
echo "runs, and finds its assets"
|
||||
# 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
|
||||
|
||||
@@ -17,6 +17,23 @@ allowed to change behaviour, not just add to it.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- **The downloadable builds were missing every tool.** They started, printed help and drew
|
||||
the specimen card, and had a menu with nothing in it but Quit — no contacts, no map, no
|
||||
trace, no chat. Tools are found at runtime with `pkgutil.iter_modules`, which finds
|
||||
nothing inside a frozen bundle, so none of them registered. The packaging now collects
|
||||
them, and the smoke tests run a tool-provided subcommand instead of only `--help`, which
|
||||
a gutted build passes perfectly well.
|
||||
|
||||
### Added
|
||||
|
||||
- `MESHTERM_HOME` points config and data somewhere other than `~/.meshterm`. Every copy of
|
||||
MeshTerm shared one directory, so trying a downloaded build meant letting it open the
|
||||
same history database as the one you use — the valuable half of an install, with no way
|
||||
to keep a second copy away from it. Also useful for a portable install, or two radios
|
||||
kept apart.
|
||||
|
||||
## [0.2.1] — 2026-09-07
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -58,22 +58,51 @@ your mesh's history is worth.
|
||||
### Download a build
|
||||
|
||||
**[⬇ Latest release](https://github.com/jpmartineau/MeshTerm/releases/latest)** — one file,
|
||||
no Python needed. Windows, macOS (Apple silicon) and Linux (x64 and ARM64, so the uConsole
|
||||
is covered).
|
||||
no Python needed. Windows, macOS (Intel and Apple silicon), and Linux (x64 and ARM64, so
|
||||
the uConsole is covered).
|
||||
|
||||
Download it, make it runnable, and run it:
|
||||
MeshTerm is a terminal program, so **open a terminal and run it from there.** You can
|
||||
double-click it and it will work, but you'll get whatever console your system picks, and
|
||||
if anything goes wrong at startup the window closes before you can read why.
|
||||
|
||||
```bash
|
||||
chmod +x meshterm-* # macOS and Linux only
|
||||
./meshterm-*
|
||||
**Windows** — open Windows Terminal or PowerShell, `cd` to your downloads, then:
|
||||
|
||||
```powershell
|
||||
.\meshterm-0.2.1-windows-x64.exe
|
||||
```
|
||||
|
||||
> **Your system will complain the first time.** These builds aren't code-signed — signing
|
||||
> costs money on both platforms and this is a free side project. macOS says *"cannot be
|
||||
> opened because the developer cannot be verified"*; clear it with
|
||||
> `xattr -d com.apple.quarantine meshterm-*` and run it again. Windows shows *"Windows
|
||||
> protected your PC"*; click **More info → Run anyway**. Each release ships a `SHA256SUMS`
|
||||
> file if you'd rather check the download yourself.
|
||||
**macOS and Linux** — open Terminal, `cd` to your downloads, then:
|
||||
|
||||
```bash
|
||||
chmod +x meshterm-0.2.1-*
|
||||
./meshterm-0.2.1-*
|
||||
```
|
||||
|
||||
Rename it to something you don't mind typing and put it somewhere on your `PATH`, and it
|
||||
becomes just `meshterm` from anywhere.
|
||||
|
||||
> **Your system will complain the first time, and it's right to.** These builds aren't
|
||||
> code-signed — signing costs real money on both platforms and this is a free side
|
||||
> project.
|
||||
>
|
||||
> - **macOS**: *"cannot be opened because the developer cannot be verified"*. Clear it with
|
||||
> `xattr -d com.apple.quarantine meshterm-0.2.1-*` and run it again.
|
||||
> - **Windows**: *"Windows protected your PC"*. Click **More info → Run anyway**.
|
||||
>
|
||||
> Every release ships a `SHA256SUMS` file if you'd rather check the download first.
|
||||
|
||||
> **Trying a build without touching your real data.** MeshTerm keeps everything in
|
||||
> `~/.meshterm` — your history database, contacts, channel keys — and *every* copy of
|
||||
> MeshTerm uses that same folder. Set `MESHTERM_HOME` to try one in isolation:
|
||||
>
|
||||
> ```bash
|
||||
> MESHTERM_HOME=~/meshterm-test ./meshterm-0.2.1-* # macOS, Linux
|
||||
> ```
|
||||
> ```powershell
|
||||
> $env:MESHTERM_HOME = "$HOME\meshterm-test"; .\meshterm-0.2.1-windows-x64.exe
|
||||
> ```
|
||||
>
|
||||
> Same variable if you run two radios and want them kept apart.
|
||||
|
||||
### Or install with pip
|
||||
|
||||
|
||||
+19
-2
@@ -15,6 +15,7 @@ keys used to sit in this file with no screen behind them.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
@@ -26,15 +27,31 @@ else: # pragma: no cover - exercised only on 3.10
|
||||
import tomli as tomllib
|
||||
|
||||
|
||||
#: Points MeshTerm's config and data somewhere other than the home directory. For trying a
|
||||
#: build without letting it near a real history, for a portable install on a stick, and for
|
||||
#: running two radios out of two directories. Read every time rather than cached, so a test
|
||||
#: can move it between cases.
|
||||
CONFIG_DIR_ENV = "MESHTERM_HOME"
|
||||
|
||||
|
||||
def default_config_dir() -> Path:
|
||||
"""Return the directory MeshTerm uses for config and data.
|
||||
|
||||
Resolves to ``.meshterm`` under the OS-defined home directory (``%USERPROFILE%``
|
||||
on Windows, ``$HOME`` on Unix), as reported by :meth:`Path.home`.
|
||||
``$MESHTERM_HOME`` wins if it is set and not empty. Otherwise this resolves to
|
||||
``.meshterm`` under the OS-defined home directory (``%USERPROFILE%`` on Windows,
|
||||
``$HOME`` on Unix), as reported by :meth:`Path.home`.
|
||||
|
||||
The override exists because the database is the valuable part of an install — a
|
||||
running record of everything the radio has overheard — and there was no way to point a
|
||||
second copy of MeshTerm at a different one. A downloaded build would happily open the
|
||||
same 34MB file as the checkout it was built from.
|
||||
|
||||
Returns:
|
||||
The resolved configuration directory path (not guaranteed to exist).
|
||||
"""
|
||||
override = os.environ.get(CONFIG_DIR_ENV, "").strip()
|
||||
if override:
|
||||
return Path(override).expanduser()
|
||||
return Path.home() / ".meshterm"
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,11 @@ datas = [
|
||||
]
|
||||
|
||||
hiddenimports = [
|
||||
# Every tool module. `meshterm.tools` finds these with `pkgutil.iter_modules` at
|
||||
# import time, so nothing static ever names one, and a frozen build without this line
|
||||
# starts with an empty registry: no menu entries but Quit, and no CLI subcommands.
|
||||
# The app launches and looks like it works, which is the worst way for this to fail.
|
||||
*collect_submodules("meshterm.tools"),
|
||||
# bleak picks its backend at runtime by platform, so static analysis never sees the
|
||||
# one that actually gets used. Collect all of them and let the unused ones sit.
|
||||
*collect_submodules("bleak.backends"),
|
||||
|
||||
+30
-1
@@ -8,7 +8,9 @@ from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from meshterm.core.config import Settings
|
||||
import pytest
|
||||
|
||||
from meshterm.core.config import CONFIG_DIR_ENV, Settings, default_config_dir
|
||||
|
||||
|
||||
def test_connect_on_start_defaults_true() -> None:
|
||||
@@ -42,3 +44,30 @@ def test_tcp_profile_defaults_port(tmp_path: Path) -> None:
|
||||
config.write_text('[profiles.wifi]\nhost = "meshcore.local"\n', encoding="utf-8")
|
||||
profile = Settings.load(config).profiles["wifi"]
|
||||
assert profile.is_tcp and profile.tcp_endpoint == "meshcore.local:5000"
|
||||
|
||||
|
||||
def test_config_dir_defaults_to_the_home_directory(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""With no override set, config and data live in ``.meshterm`` under the user's home."""
|
||||
monkeypatch.delenv(CONFIG_DIR_ENV, raising=False)
|
||||
assert default_config_dir() == Path.home() / ".meshterm"
|
||||
|
||||
|
||||
def test_config_dir_follows_the_override(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
"""``$MESHTERM_HOME`` moves the whole directory, so a build can run beside a real one.
|
||||
|
||||
This is what keeps a downloaded binary from opening the same database as the checkout
|
||||
it was built from — the history is the valuable half of an install, and there was no
|
||||
way to point a second copy somewhere else.
|
||||
"""
|
||||
monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path / "elsewhere"))
|
||||
assert default_config_dir() == tmp_path / "elsewhere"
|
||||
|
||||
|
||||
def test_config_dir_ignores_an_empty_override(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""An unset-but-present or whitespace value falls back rather than resolving to nowhere.
|
||||
|
||||
A shell that exports the variable empty is common enough that treating "" as "put the
|
||||
data in the current directory" would be a nasty way to scatter someone's history.
|
||||
"""
|
||||
monkeypatch.setenv(CONFIG_DIR_ENV, " ")
|
||||
assert default_config_dir() == Path.home() / ".meshterm"
|
||||
Reference in new issue
Block a user