Skip to content

feat(windows): ship MSIX instead of appx; add headless smoke test - #451

Merged
tlecomte merged 12 commits into
masterfrom
windows-misx-smoketest
Aug 8, 2026
Merged

feat(windows): ship MSIX instead of appx; add headless smoke test #451
tlecomte merged 12 commits into
masterfrom
windows-misx-smoketest

Conversation

@tlecomte

@tlecomte tlecomte commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Build a msix package instead of appx: msix is a more modern, more generic packaging solution. Both can be published to the Microsoft store similarly.

Also add a headless smoke test like we did for AppImage in #450

tlecomte added 12 commits August 8, 2026 14:01
- installer/appxmanifest.xml: ProcessorArchitecture amd64 (was x86, wrong
  for the 64-bit PyInstaller build), newer Windows 10 min/max baseline,
  updated comments to reflect MSIX.
- install-windows.ps1: emit friture-$version.msix via MakeAppx (was .appx),
  save the manifest as AppxManifest.xml (MSIX canonical name).
- build.yml: add 'Verify app launches' smoke-test step for Windows
  (frozen dist/friture/friture.exe, offscreen Qt, full-init assertion via
  the cross-platform smoke_test.py), a platformdirs-based log capture/upload,
  switch the artifact/upload from .appx to .msix, and add MSIX to the Store
  release files. (Store ingestion signs the package; CI builds an unsigned
  MSIX for packaging validation only.)
…back)

os.killpg/signal.SIGKILL are POSIX-only; on Windows they raise
AttributeError. Extract _kill_proc() that tries the process-group kill on
POSIX and falls back to proc.kill() otherwise, so the Windows smoke test
can terminate a hung release build instead of crashing the harness.
sounddevice.query_devices(kind='output') raises PortAudioError ('Error
querying device -1') on a headless host with no default output device,
which crashed AudioBackend() -> Friture() before init completed. Mirror
the existing guard in get_input_devices() so a missing default output
device degrades to an empty device list instead of raising.
…dless)

On a host with no default input/output device (e.g. the Windows CI
runner, unlike Linux which loads a PulseAudio monitor source), the
frozen app could not start:

* get_readable_devices_list() / get_readable_output_devices_list()
  called sounddevice.query_devices(kind=...) unconditionally and, when
  it raised PortAudioError, logged via logger.exception() -- emitting a
  'Traceback' line that trips the smoke test's FATAL_MARKERS. Guard the
  kind= calls and log at debug instead; mirror get_input_devices()'s
  early return when there are no devices.
* SettingsDialog popped a blocking QMessageBox.critical() + sys.exit(1)
  on the empty device list. Under the offscreen Qt platform (the CI
  smoke test / any headless host) that modal blocks forever with no
  user to dismiss it, preventing init from reaching 'Init finished' and
  before any QML is shown. In headless mode, log a warning and continue
  with an empty device set instead; interactive desktop users keep the
  existing message+exit behaviour.

Neither change affects machines that have a real audio input device.
… the static call)

PyQt6 moved QGuiApplication.platformName() from a static method to an
instance method, so QtCore.QCoreApplication.platformName() raised
AttributeError during SettingsDialog construction on a headless Windows
runner, crashing Friture() before full init. Use the existing
QApplication.instance() pattern (already used elsewhere in the codebase)
to query the offscreen platform.
…rtup

SettingsDialog.restoreState() did themeButtonGroup.button(id).setChecked(True)
with the id read back from QSettings (AudioBackend group). On a system whose
stored value is absent, stale, or outside {0,1,2}, button() returns None and
QML/.setChecked raises AttributeError: 'NoneType' object has no attribute
'setChecked' -- crashing Friture() during restoreAppState() before
'Init finished'.

This reproduces on a fresh Windows CI runner with no audio (unlike Linux,
whose PulseAudio monitor source provides a default input): the headless
no-device path runs SettingsDialog.__init__ (which returns early, leaving
the comboBox empty) and restoreState is still invoked from the analyzer.

Guard the button(id) lookups (both theme and input-type groups): if no
button carries the stored id, fall back to the System (0) button instead
of crashing. Real interactive users see no behaviour change (their stored
0/1/2 still maps to a button); only a bad/sentinel id is rescued.
Root cause of the Windows headless startup crash (now visible in the
smoke log as 'No theme button for id 0'):

SettingsDialog.__init__ early-returns in the no-input-device / offscreen
branch, which on a headless Windows runner is the common path (unlike
Linux, which loads a PulseAudio monitor source as its default input).
That early return happened BEFORE the themeButtonGroup.setId(...) calls,
so on the no-device path the button group had no IDs -- restoreState()'s
button(0) returned None, and .setChecked() raised AttributeError.

Move the idToggled connect + the three setId(0/1/2) calls to right after
setupUi(), before the device-list early return, so the button group is
always populated. The defensive None-guard in restoreState() stays as
defense-in-depth. No behaviour change for interactive desktop users
(their button IDs were already set).
@tlecomte
tlecomte marked this pull request as ready for review August 8, 2026 19:25
@tlecomte
tlecomte merged commit ed47917 into master Aug 8, 2026
4 checks passed
@tlecomte
tlecomte deleted the windows-misx-smoketest branch August 8, 2026 19:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant