Files
MeshTerm/packaging/meshterm.spec
T
Jean-Pierre MartineauandClaude Opus 5.5 99b991707e Comments of the scripts and the packaging files are in ASD-STE100
The Python scripts, the packaging code, and the full-line comments of the
shell scripts. Heredocs, marker lines, licence lines, and code do not
change. The help text of type-text.py now starts with a complete STE
sentence, and uart1-mux.py now says three writes, as the code does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 21:37:03 -04:00

171 lines
7.7 KiB
RPMSpec

# SPDX-License-Identifier: Apache-2.0
# PyInstaller spec for the downloadable builds. Run from the repository root:
#
# pyinstaller packaging/meshterm.spec
#
# The build is one file, in console mode. Console mode is necessary: MeshTerm is a terminal
# program. A windowed build has no place to draw, and it exits at once.
#
# Build on the machine that is the target. There is no cross-compiling here. The Windows
# build comes from a Windows runner, and the macOS build comes from macOS. This is the
# purpose of the workflow.
import re
import sys
from PyInstaller.utils.hooks import collect_submodules
# `packaging/notices.py` builds THIRD-PARTY-NOTICES.txt at build time. It is next to this
# spec and not under `meshterm/`, because it must not be importable from the installed
# package. Only the freezer imports it. The same is true of `entry.py` above. `SPECPATH`
# is one of the names that PyInstaller adds to the namespace of a .spec that runs. It is
# the directory that holds this file. PyInstaller resolves it from the path that was given
# to `pyinstaller`, not from the cwd of the process. Thus the spec finds `notices.py`
# with any way of starting it.
sys.path.insert(0, SPECPATH)
import notices # noqa: E402 - the import must come after the sys.path change above
# The build makes this file new each time. It is not in git. It depends on the
# distributions that are installed in this interpreter. A copy that is not current can
# become wrong when the licence text of a dependency changes upstream and nobody notices.
# `workpath` is the temporary directory of PyInstaller for this type of build-time
# artefact. `--clean` cleans it, and nobody can mistake it for a source file. The file goes
# to `<workpath>/<specname>/THIRD-PARTY-NOTICES.txt`. PyInstaller adds the name of the spec,
# without its extension, to the workpath that it receives (refer to `build()` in
# `PyInstaller/building/build_main.py`). For this spec, this is `build/meshterm/`.
third_party_notices = notices.write_third_party_notices(
os.path.join(workpath, "THIRD-PARTY-NOTICES.txt")
)
# The version resource of the Windows build. It is the Details tab of the Properties of the
# file, and the name and version that Windows shows for it. Without it, the .exe is only
# "meshterm.exe" with no product, and SignPath refuses to sign it. The signing configuration
# of SignPath checks the product name and version that this resource has. Thus a file
# without them cannot pass as a MeshTerm release. The spec builds the resource from the
# `__version__` of the package. It does not keep a second copy, because a second copy can
# become wrong. The other platforms have no such resource, and PyInstaller gives a warning
# if the spec gives one there.
version_info = None
if sys.platform == "win32":
from PyInstaller.utils.win32.versioninfo import (
FixedFileInfo,
StringFileInfo,
StringStruct,
StringTable,
VarFileInfo,
VarStruct,
VSVersionInfo,
)
# This is the checkout that PyInstaller freezes. It is not a MeshTerm that the
# interpreter has installed. `Analysis` below also reads its sources from here.
sys.path.insert(0, os.path.dirname(SPECPATH))
import meshterm # noqa: E402 - the import must come after the sys.path change above
# The fixed half of the resource has exactly four numbers: 0.10.2 becomes (0, 10, 2, 0).
# A suffix such as `rc1` has no place there, and the spec removes it. The text fields
# below keep the version exactly as written.
numbers = [int(n) for n in re.match(r"\d+(?:\.\d+)*", meshterm.__version__)[0].split(".")]
numbers = tuple((numbers + [0, 0, 0, 0])[:4])
version_info = VSVersionInfo(
ffi=FixedFileInfo(filevers=numbers, prodvers=numbers),
kids=[
StringFileInfo(
[
# 0409 is US English, and 04B0 shows that the strings are Unicode.
# Nearly all Windows programs declare this pair.
StringTable(
"040904B0",
[
StringStruct("CompanyName", meshterm.__author__),
StringStruct("FileDescription", "MeshTerm"),
StringStruct("FileVersion", meshterm.__version__),
StringStruct("InternalName", "meshterm"),
StringStruct("LegalCopyright", meshterm.copyright_notice()),
StringStruct("OriginalFilename", "meshterm.exe"),
StringStruct("ProductName", "MeshTerm"),
StringStruct("ProductVersion", meshterm.__version__),
],
)
]
),
VarFileInfo([VarStruct("Translation", [0x0409, 1200])]),
],
)
# The assets must go to `meshterm/assets`. `ui/about.py` finds them with
# `Path(__file__).parent.parent / "assets"`, and this path must resolve in the bundle in the
# same way as in a checkout.
datas = [
("../meshterm/assets", "meshterm/assets"),
# The node of the SPI radio runs by path under another Python. This is the Python that
# has the radio library, and this build does not have it. Thus the node must exist as a
# source file, next to the place where `spiradio.node_script()` looks for it. Bytecode in
# the archive is not sufficient.
("../meshterm/core/radionode.py", "meshterm/core"),
# A build of one file is a copy of MeshTerm. Apache-2.0 §4(a) and §4(d) require the
# LICENSE and the NOTICE of MeshTerm with each copy. They must not be inside the assets
# of the app. They must be at the root of the bundle, as they are at the root of the
# repository.
("../LICENSE", "."),
("../NOTICE", "."),
# The notice of each MIT, BSD, and PSF dependency must go with the copies in the same
# way. Refer to packaging/notices.py for how the build makes it.
(str(third_party_notices), "."),
]
hiddenimports = [
# Each tool module. `meshterm.tools` finds these with `pkgutil.iter_modules` at import
# time, so no static code names a tool module. A frozen build without this line starts
# with an empty registry: no menu entries except Quit, and no CLI subcommands. The app
# starts and looks correct, and this is the worst way for the failure to appear.
*collect_submodules("meshterm.tools"),
# bleak chooses its backend at run time, by platform. Thus static analysis never sees
# the backend that the app uses. Collect all the backends, and keep the unused ones.
*collect_submodules("bleak.backends"),
# The same is true for the transports of the companion library.
*collect_submodules("meshcore"),
# Rich and prompt_toolkit both use some modules dynamically.
"rich.console",
"prompt_toolkit.output.win32",
"prompt_toolkit.output.vt100",
]
a = Analysis(
["entry.py"],
pathex=[".."],
binaries=[],
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
# The app does not use these modules, and each one adds a GUI toolkit or a test framework.
excludes=["tkinter", "pytest", "IPython", "matplotlib", "numpy", "PIL"],
noarchive=False,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name="meshterm",
debug=False,
bootloader_ignore_signals=False,
strip=False,
# UPX is off on purpose. It saves a few megabytes, but antivirus software often flags
# the result. For a first release, this is not a good trade.
upx=False,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
version=version_info,
)