Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions .github/workflows/build_windows_macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ jobs:
runs-on: 'windows-latest'
strategy:
matrix:
# An exact version keeps the packaged Windows artifact reproducible.
python-version: ['3.14.6']
python-version: ['3.15.2']

outputs:
hashes: ${{ steps.hash.outputs.hashes }}
Expand Down Expand Up @@ -86,12 +85,27 @@ jobs:
cd ardupilot_methodic_configurator
copy ..\\windows\\ardupilot_methodic_configurator.spec
pyinstaller --clean ardupilot_methodic_configurator.spec
if (-not (Test-Path "dist\\ardupilot_methodic_configurator\\_internal\\_tcl_data\\init.tcl") -or
-not (Test-Path "dist\\ardupilot_methodic_configurator\\_internal\\_tk_data\\tk.tcl")) {
throw "PyInstaller did not bundle the Tcl/Tk library data."
if ($LASTEXITCODE -ne 0) {
throw "PyInstaller failed with exit code $LASTEXITCODE."
}
del ardupilot_methodic_configurator.spec

- name: Smoke-test frozen Tcl/Tk initialization
shell: pwsh
timeout-minutes: 2
run: |
$app = Join-Path $PWD "ardupilot_methodic_configurator/dist/ardupilot_methodic_configurator/ardupilot_methodic_configurator.exe"
Remove-Item Env:TCL_LIBRARY, Env:TK_LIBRARY -ErrorAction SilentlyContinue
Push-Location $env:RUNNER_TEMP
try {
& $app --validate-tkinter
if ($LASTEXITCODE -ne 0) {
throw "Frozen Tcl/Tk initialization failed with exit code $LASTEXITCODE."
}
} finally {
Pop-Location
}

- name: Verify frozen plugin and log-analysis modules
run: python scripts/verify_packaged_modules.py ardupilot_methodic_configurator/build/ardupilot_methodic_configurator/PYZ-00.pyz

Expand Down Expand Up @@ -208,7 +222,7 @@ jobs:
runs-on: 'macos-latest'
strategy:
matrix:
python-version: ['3.13']
python-version: ['3.15.2']

permissions:
contents: write
Expand Down Expand Up @@ -312,7 +326,7 @@ jobs:
runs-on: 'ubuntu-22.04' # Use a consistent Linux environment for building the AppImage
strategy:
matrix:
python-version: ['3.14']
python-version: ['3.15.2']

permissions:
contents: write
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pylint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
runs-on: ${{ vars.UBUNTU_RUNNER || 'ubuntu-latest' }}
strategy:
matrix:
python-version: ["3.11", "3.14"]
python-version: ["3.11", "3.15.2"]

steps:
- name: Harden the runner (Audit all outbound calls)
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pyright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
runs-on: ${{ vars.UBUNTU_RUNNER || 'ubuntu-latest' }}
strategy:
matrix:
python-version: ["3.11", "3.14"]
python-version: ["3.11", "3.15.2"]

steps:
- name: Harden the runner (Audit all outbound calls)
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
- ${{ vars.UBUNTU_RUNNER || 'ubuntu-latest' }}
- windows-latest
- macos-latest
python-version: ["3.14"] # to make sure the latest AMC supported python version does not break
python-version: ["3.15.2"] # to make sure the latest AMC supported python version does not break
include:
- os: ${{ vars.UBUNTU_RUNNER || 'ubuntu-latest' }}
python-version: "3.11" # to make sure the minimum AMC supported python version does not break
Expand Down Expand Up @@ -270,7 +270,7 @@ jobs:
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
parallel-finished: true
carryforward: "run-${{ vars.UBUNTU_RUNNER || 'ubuntu-latest' }}-py3.11,run-${{ vars.UBUNTU_RUNNER || 'ubuntu-latest' }}-py3.14,run-windows-latest-py3.14,run-macos-latest-py3.14"
carryforward: "run-${{ vars.UBUNTU_RUNNER || 'ubuntu-latest' }}-py3.11,run-${{ vars.UBUNTU_RUNNER || 'ubuntu-latest' }}-py3.15,run-windows-latest-py3.15,run-macos-latest-py3.15"

# TODO: create a badge that presents the result of the Upload coverage xml report step

Expand Down
21 changes: 20 additions & 1 deletion ardupilot_methodic_configurator/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from logging import warning as logging_warning
from pathlib import Path
from sys import exit as sys_exit
from tkinter import Tk, ttk

import argcomplete

Expand Down Expand Up @@ -131,6 +132,21 @@ def validate_plugin_registration() -> None:
logging_info("Validated registration of all %d plugins", len(expected))


def validate_tkinter_runtime() -> None:
"""Verify Tcl, Tk, and ttk initialize using the runtime's bundled library data."""
root = Tk()
try:
root.withdraw()
ttk.Button(root, text="Packaging validation").pack()
root.update_idletasks()
tcl_version = root.tk.call("info", "patchlevel")
tk_version = root.tk.call("package", "require", "Tk")
tcl_library = root.tk.call("info", "library")
print(f"Validated Tcl {tcl_version} and Tk {tk_version} (Tcl library: {tcl_library})") # noqa: T201
finally:
root.destroy()


class ApplicationState: # pylint: disable=too-few-public-methods
"""Data class to hold application state throughout the startup process."""

Expand Down Expand Up @@ -801,7 +817,10 @@ def parameter_editor_and_uploader(state: ApplicationState) -> None:


def _run_validation_command() -> bool:
"""Handle headless validation modes before normal application startup."""
"""Handle packaging validation modes before normal application startup."""
if sys.argv[1:] == ["--validate-tkinter"]:
validate_tkinter_runtime()
return True
if sys.argv[1:] == ["--validate-plugins"]:
validate_plugin_registration()
return True
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ classifiers = [
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3.15",
"Topic :: Software Development :: Embedded Systems",
"Topic :: System :: Installation/Setup",
"Topic :: Scientific/Engineering",
Expand Down
35 changes: 35 additions & 0 deletions tests/test__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import sys
import tempfile
from pathlib import Path
from tkinter import TclError
from types import ModuleType
from unittest.mock import MagicMock, call, patch

Expand Down Expand Up @@ -714,6 +715,40 @@ def test_bin_log_rejects_pseudo_vehicle_types_explicitly(self, vehicle_type: str

parser_error.assert_called_once_with(f"--vehicle-type {vehicle_type} cannot be used together with --bin-log")

def test_tkinter_validator_initializes_gui_libraries_in_a_child_process(self) -> None:
"""
The packaging validator initializes Tk and ttk and exits without normal startup.

GIVEN a Python environment with working Tcl/Tk libraries.
WHEN the application runs with --validate-tkinter.
THEN the process succeeds without starting the application workflow.
"""
result = subprocess.run(
[sys.executable, "-m", "ardupilot_methodic_configurator", "--validate-tkinter"],
capture_output=True,
check=False,
text=True,
timeout=20,
)

assert result.returncode == 0, result.stderr

def test_tkinter_validator_rejects_unusable_gui_libraries(self) -> None:
"""
A missing Tcl/Tk runtime must fail validation rather than start the application.

GIVEN Tcl/Tk library initialization fails.
WHEN the application runs with --validate-tkinter.
THEN the initialization error propagates and normal startup is never attempted.
"""
with (
patch.object(amc_main.sys, "argv", ["amc.exe", "--validate-tkinter"]),
patch.object(amc_main, "Tk", side_effect=TclError("Missing Tcl/Tk library data")),
patch.object(amc_main, "create_argument_parser", side_effect=AssertionError("Must not start the application")),
pytest.raises(TclError, match="Missing Tcl/Tk library data"),
):
main()

def test_frozen_bin_log_validator_dispatches_without_starting_the_gui(self) -> None:
"""The frozen validator command exits before normal application startup."""
with (
Expand Down
22 changes: 2 additions & 20 deletions windows/ardupilot_methodic_configurator.spec
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,10 @@
from PyInstaller.utils.hooks import collect_submodules
import certifi
import os
from pathlib import Path
import sys


def _find_tcl_tk_library(directory: Path, prefix: str) -> Path:
"""Return the Tcl/Tk library directory shipped with the build Python."""
library_file = "init.tcl" if prefix == "tcl" else "tk.tcl"
candidates = sorted(
(path for path in directory.glob(f"{prefix}*") if path.is_dir() and (path / library_file).is_file()),
reverse=True,
)
if not candidates:
raise SystemExit(f"Could not find {prefix} library data below {directory}")
return candidates[0]


# Set both variables before PyInstaller analyses tkinter so its built-in hook
# collects the library data into _tcl_data and _tk_data. Without this, the
# frozen application contains the DLLs but fails during the tkinter runtime hook.
tcl_root = Path(sys.base_prefix) / "tcl"
os.environ["TCL_LIBRARY"] = str(_find_tcl_tk_library(tcl_root, "tcl"))
os.environ["TK_LIBRARY"] = str(_find_tcl_tk_library(tcl_root, "tk"))
# Let PyInstaller's tkinter hook discover Tcl/Tk data. It supports both
# external script directories and Tcl/Tk 9 archives embedded in the DLLs.

# Path to certifi's CA bundle
certifi_cacert = certifi.where()
Expand Down
Loading