Skip to content

feat(installer): tag-triggered Windows installer, unsigned and without ffmpeg - #2

Merged
joepetjr merged 2 commits into
mainfrom
feat/windows-installer
Aug 3, 2026
Merged

feat(installer): tag-triggered Windows installer, unsigned and without ffmpeg#2
joepetjr merged 2 commits into
mainfrom
feat/windows-installer

Conversation

@joepetjr

@joepetjr joepetjr commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Pushing a v* tag now builds a real Windows installer on windows-latest and attaches it to that
tag's GitHub release. PyInstaller freezes a one-file keycut.exe; Inno Setup wraps it, installs to
Program Files, puts the directory on PATH and registers an uninstaller in Add/Remove Programs.

This is the reference implementation for the same pattern in redactcam, agent-audit and
claude-batch-runner, so it is deliberately explicit.

What it does not do, said out loud

  • It does not bundle ffmpeg. keycut shells out to it; bundling is a size and licensing problem.
    The installer's finish page and the README both state the requirement, and the README gives the
    winget install -e --id Gyan.FFmpeg line. An installer that produces a tool which cannot run is
    the failure mode this avoids.
  • The build is unsigned. No code-signing certificate exists. The README says SmartScreen will
    warn and describes what the user will see. There is no signing step that would no-op or fail.
  • No version, release date, download count or support claim is invented anywhere.

Design calls

  • gh release over softprops/action-gh-releasegh ships on the runner, so this is one
    fewer third-party action to pin and audit. The create-if-missing branch is three explicit lines.
  • System PATH (HKLM), not per-user. An admin Program Files install that writes HKCU has a real
    failure mode: a standard user elevating with a different admin's credentials writes to that
    admin's hive, and the invoking user never gets the entry. HKLM has no such case.
  • The tag must agree with pyproject.toml or the build fails. Otherwise Add/Remove Programs
    carries a version the packaged code never claimed, for as long as it stays installed. Practical
    consequence: the first tag must be v0.1.0, or bump pyproject.toml first.
  • AppVersion has no default — compiling without /DAppVersion= fails at ISCC rather than
    quietly emitting keycut-setup-0.0.0-dev.exe.
  • No ffmpeg detection in the installer, only a statement. A where-style check cannot be
    tested from here, and a check that false-negatives puts a wrong claim in front of the user, which
    is worse than a static true one.
  • The [Registry] PATH line is one long line on purpose. It was written with ISPP \
    line-continuation, which nothing in this repo can compile-test, and a wrapped line that
    mis-parses would corrupt the machine PATH. One line cannot.

Test plan

Verified here:

  • python -m PyInstaller --onefile --console --name keycut installer/keycut_launcher.py → build
    succeeds, 7.5 MB binary. (python -m pyinstaller lowercase is not a module and fails — the
    workflow uses the capitalised form.)
  • dist/keycut --help on the frozen binary → usage, exit 0. dist/keycut foo.mp4 out.mp4
    at least one --range is required, exit 1, so CLI logic is reached inside the freeze.
  • PyInstaller's warn-keycut.txt shows no keycut-related missing modules → no --hidden-import.
  • yaml.safe_load on release.yml → parses; trigger {'push': {'tags': ['v*']}}, windows-latest.
  • The version-guard one-liner returns 0.1.0 verbatim.
  • .iss is pure ASCII (grep -P '[^\x00-\x7F]' empty) — Inno reads a BOM-less script as ANSI.
  • ruff check . clean; pytest -q135 passed, re-run unpiped by the dispatcher after the
    line-continuation change.

Every Inno directive was read against jrsoftware.org rather than written from memory, including
ArchitecturesInstallIn64BitMode=x64compatible (plain x64 is deprecated since Inno 6.3) and the
exact signatures of RegQueryStringValue / RegWriteExpandStringValue / Pos / Delete.

Unverified until the first real tag build

Nothing here runs Windows, so the Windows half is unrun by construction:

  1. The .iss has never been through ISCC — directives are documentation-checked only.
  2. ArchitecturesInstallIn64BitMode=x64compatible needs Inno ≥6.3. choco install innosetup gets
    current Inno so this should hold; if it ever fails, {autopf} silently becomes Program Files (x86).
  3. The ISCC discovery fallback path — it throws loudly if neither branch hits, so it fails visibly.
  4. The PATH add/remove Pascal. The index arithmetic was traced by hand for first, middle, last and
    single-entry cases; it has never executed. Uninstall paths are where this kind of code bites.
  5. The Windows PyInstaller build itself. Only the Linux freeze is proven, and antivirus
    false-positives on one-file PyInstaller binaries would surface here first.
  6. Finish-page rendering (%n breaks, [name] expansion).

Diff 246 sum (cap 300). src/, tests/ and ci.yml untouched.

…t ffmpeg

Pushing a v* tag now builds a PyInstaller one-file keycut.exe on a Windows
runner, wraps it in an Inno Setup installer, and attaches
keycut-setup-<version>.exe to that tag's GitHub release.

The installer puts keycut in Program Files, appends that directory to the
machine PATH, and registers an uninstaller in Add/Remove Programs. Its version
comes from the tag, and the workflow refuses a tag that disagrees with
pyproject.toml, so the number Add/Remove Programs carries cannot be a guess.

Two things it does not do, said in the installer's finish page and next to the
README download rather than left for the user to find out: it does not bundle
ffmpeg (an ffmpeg build dwarfs keycut, and which licence one falls under
depends on how it was configured), and it is not code-signed, so SmartScreen
will warn. No signing step is wired up, because there is no certificate for it
to use.

PyInstaller is aimed at installer/keycut_launcher.py because the console entry
point is a module function, not a script on disk. Inno Setup is installed with
choco rather than assumed present on the runner image.
@joepetjr
joepetjr merged commit 9525110 into main Aug 3, 2026
3 checks passed
@joepetjr
joepetjr deleted the feat/windows-installer branch August 3, 2026 22:12
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