diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64c8d46..cbddc14 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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" diff --git a/.github/workflows/installers.yml b/.github/workflows/installers.yml index 1fe1fd7..eda7daf 100644 --- a/.github/workflows/installers.yml +++ b/.github/workflows/installers.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c67b41..33b5a35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index f084eb9..2c740c0 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/meshterm/core/config.py b/meshterm/core/config.py index 1a77a21..ab72b60 100644 --- a/meshterm/core/config.py +++ b/meshterm/core/config.py @@ -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" diff --git a/packaging/meshterm.spec b/packaging/meshterm.spec index 6be50f7..420acd6 100644 --- a/packaging/meshterm.spec +++ b/packaging/meshterm.spec @@ -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"), diff --git a/tests/test_config.py b/tests/test_config.py index cda3c91..f2b3755 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -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"