Skip to content

Add Windows installer, signing, and release build checks - #7

Open
owenpkent wants to merge 7 commits into
timeline-offloadfrom
windows-release-bundle
Open

owenpkent wants to merge 7 commits into
timeline-offloadfrom
windows-release-bundle

Conversation

@owenpkent

@owenpkent owenpkent commented Sep 10, 2026 •

Copy link
Copy Markdown
Owner

What this changes

Offloader can now be built as a Windows x64 desktop/CLI bundle and an NSIS installer for machines without Python. A single version source supplies package metadata, runtime/report versions, and executable/installer versions. Runtime and packaging dependencies are pinned.

The build signs by default with the existing OK Studio certificate, verifies trusted publishers and timestamp coverage, signs the generated uninstaller during NSIS compilation, and signs the setup executable afterward. --no-sign produces development and CI artifacts without accessing the key. Source/file inventories reject stale --skip-build bundles; --verify-only checks existing outputs without the private key, and final checksums cover the installer, portable ZIP, and inventory.

Installed GUI and CLI processes hold lifetime locks that block maintenance while any instance is open. Maintenance blocks new launches, stages and checks replacements, preserves old owned files for rollback, recovers interrupted operations, and removes only inventoried files. Settings, history, and unrelated files are preserved. The wizard includes shortcut choices, silent installation, and an optional Finish launch using the desktop user's token and environment, with no elevated fallback.

CI builds unsigned installers and portable bundles, runs frozen smoke checks, and checks that the source archive includes the Windows builder. Setuptools output moves to .python-build because its default cleanup otherwise omitted the tracked build/windows directory. The earlier file-control test fixes remove disk-speed races without changing production copy or verification behavior.

Stacked on #6; targets timeline-offload. Timeline import remains source-only in this bundle. No release is published and no hardware-key signing was performed.

What you verified

  • Local Windows/Python 3.12.10 suite: 727 passed, 5 skipped, with 85% line coverage. Ruff and diff checks passed.
  • The real unsigned NSIS installer and portable bundle built with NSIS 3.12. Frozen executable versions match the source, and the GUI starts offscreen with isolated settings.
  • The frozen CLI copies disposable data to two destinations, generates all five report formats, re-verifies both copies, and detects a flipped byte.
  • The frozen maintenance helper installs, reinstalls, and uninstalls in temporary folders. Checks cover application/maintenance lock contention and preservation of unrelated files, without registry or shortcut changes on the workstation.
  • Regression tests inject interrupted file moves, interrupted rollback, manifest-update gaps, and cleanup failures. Signing tests cover wrong publishers, missing timestamps, cancellation, and uninstaller finalizer failure without using the hardware key.
  • Signing preflight and read-only verification of existing Microsoft, Python, and Qt dependency signatures passed.
  • Wheel/sdist builds and isolated wheel installation passed. The final source archive contains the Windows builder and NSIS template; bundle provenance and output checksums match the completed build.

Hardware-key signing, the actual NSIS install lifecycle under UAC, alternate administrator credentials and desktop launch, independent clean-machine/real-storage qualification, and the complete third-party license inventory/SBOM remain pending. Hosted CI for this update must be evaluated separately from the local results above.

Checklist

  • pytest passes locally (727 passed, 5 skipped)
  • ruff check src tests scripts build/windows passes
  • Tests cover installation failure, interruption, recovery, and signing refusal
  • Copy verification verdicts and source-media handling are unchanged
  • Data-safety and build/release documentation describe the implemented behavior and remaining limits
  • README measurements and changelog are updated
  • Clean-machine installation and real hardware-key signing qualification

@socket-security

socket-security Bot commented Sep 10, 2026 •

Copy link
Copy Markdown

@owenpkent owenpkent changed the title Build an unsigned Windows desktop and CLI bundle Add Windows release builds and signing plan Sep 10, 2026
Add NSIS packaging, signing and timestamp verification, source and artifact
inventories, and explicit unsigned builds for development and CI. Protect
installation changes with application lifetime locks, inventoried file
ownership, staged replacement, and recoverable rollback. Preserve user state
and launch from Finish with the desktop user's token and environment.

Setuptools silently pruned the tracked build/windows directory from source
archives because it shared the generated build directory. Move generated
files to .python-build and check the Windows builder's presence in CI.

Validated on Windows: 727 tests passed, 5 skipped; Ruff, unsigned NSIS and
portable builds, frozen smoke checks, wheel/sdist builds, isolated wheel
installation, and final inventories/checksums passed. Hardware signing and
clean-machine interactive installation remain unqualified.
@owenpkent owenpkent changed the title Add Windows release builds and signing plan Add Windows installer, signing, and release build checks Sep 11, 2026

@owenpkent owenpkent left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the installation transaction, ownership/reparse checks, installed-instance lock, upgrade recovery, uninstall boundaries, and build/signing integration against this PR's base. A fresh-install rollback defect remains, detailed inline.

A controlled reproduction used only a synthetic payload in workspace scratch and injected a failure at the installation-manifest write. No actual application installation, signing, elevation, or clean-machine qualification was performed. The observed retry failure is independent of those remaining release gates.

destination = _path_under(target, relative)
if _path_exists_or_link(destination):
_assert_file_matches(destination, new.files[relative], root=target)
destination.unlink()

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Remove transaction-created empty directories during fresh-install rollback.

This removes promoted files, but leaves the destination parent directories created during promotion. After a failure writing .offloader-install.json, a synthetic first install rolled back to ['.offloader-install.lock', '_internal']. An immediate retry then failed with InstallationError: refusing to install into a nonempty unowned directory, because _assert_fresh_target() sees the leftover _internal directory without an ownership manifest.

A transient first-install failure therefore cannot be retried through the normal installer, even though recovery reports no outstanding transaction. Track the directories created by this transaction and prune only those that are still empty, stopping at the installation root and preserving unrelated content. Add a regression that injects a failure after a nested file is promoted, then retries the same fresh install successfully.

Rollback removed the files it had promoted and left the directories it had
made to promote them into. After a failure writing the installation manifest
a synthetic first install rolled back to ['.offloader-install.lock',
'_internal'], and the immediate retry failed with "refusing to install into
a nonempty unowned directory", because _assert_fresh_target sees that
leftover directory and no ownership manifest. So a transient first-install
failure could not be retried through the installer at all, even though
recovery reported no outstanding transaction.

The transaction now records each directory it creates on the way to a
promotion, before creating it - a crash in between leaves the marker naming
a directory that is not there, which pruning tolerates, rather than a
directory nothing knows about, which it cannot fix. Rollback prunes exactly
those, deepest first, and only while they are still empty. A directory that
already existed was never recorded and is not touched; one holding a file
that arrived from somewhere else is left with it. Pruning runs last, after
an upgrade has restored its old files, because those refill the same
directories.

Writing the regression turned up a second way the same failure wedged the
install, in the same handler and with the same cause, so it is fixed here
too. The manifest write is what commits an update and it is atomic: either
the new inventory landed or the previous one is still there. Recovery
treated the second as "manifest does not match a committing update" and
raised - and every later install and uninstall recovers first, so a failed
upgrade left an installation that no maintenance operation could get past.
Both outcomes now mean it did not commit, and roll back.

Three regressions: a rolled-back first install retried successfully, a
created directory holding somebody else's file kept along with it, and a
failed upgrade keeping the directories it did not create and still being
upgradable and uninstallable afterwards. The first and third fail without
the fix.
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