Skip to content

Fix seven findings in the AppImage and Windows build scripts - #33

Merged
ami3go merged 9 commits into
mainfrom
fix/build-script-findings
Aug 20, 2026
Merged

Fix seven findings in the AppImage and Windows build scripts#33
ami3go merged 9 commits into
mainfrom
fix/build-script-findings

Conversation

@ami3go

@ami3go ami3go commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Review of the packaging path — the three PyInstaller specs, the AppImage assembly in ci.yml, and the Linux installer. Seven findings, one commit each.

1. Every Windows build left an untracked binary in the repo

Both Windows specs decoded droidwebdisplay.ico.base64 into packaging/windows/droidwebdisplay.ico — a path not gitignored. git status came back dirty after any build, and a routine git add -A would commit a generated artifact. The icon now lands in PyInstaller's workpath (already covered by **/build/).

2. The AppImage's version was never verified

The Windows job checks PE FileVersion/ProductVersion against VERSION and fails on mismatch. Linux had no equivalent — yet release.yml renames DroidWebDisplay-ci-linux-x86_64.AppImage to DroidWebDisplay-v<version>-linux-x86_64.AppImage and publishes it, so the filename asserted a version nothing had checked and its hash went into SHA256SUMS.txt on that basis. A new step extracts the built AppImage and compares the VERSION it actually bundles.

3. upx=True on Linux was dead config on a live hazard

upx is installed on neither runner, so PyInstaller logged "UPX is not available" and carried on — the setting was untested, and would have activated unreviewed the moment upx appeared on an image. UPX-compressing Qt binaries is a known cause of corrupt executables and AV false positives. Both Windows specs already used upx=False.

4. The installed Linux desktop entry had no icon

droidwebdisplay.desktop.in carried no Icon= key and install.sh never put the SVG on the icon search path, so a system install showed a generic placeholder — while the AppImage inlines its own desktop file that does set Icon=droidwebdisplay. Two definitions, and the installed one was the poorer. install.sh now installs the SVG into XDG hicolor/scalable/apps; uninstall.sh removes it.

5. The Linux spec silently built a worse Windows exe

DroidWebDisplay.spec had an if sys.platform == "win32" branch CI never reaches, producing an exe with no icon and no version resource — precisely what ci.yml's PE check exists to reject. It now refuses to run on Windows and names the correct specs.

6. Three specs, ~90% duplicated

The bundled-data list, ADB discovery and hidden-import computation were repeated verbatim across all three. _dwd_common.py now owns them; each spec keeps only its platform guard and its EXE/COLLECT shape. 339 lines → 345 across four files, but the duplication is gone.

Verified by evaluating all three specs against a stubbed PyInstaller, reproducing pre-refactor inputs exactly: Linux 10 datas / 1 binary / no icon / COLLECT DroidWebDisplay; Windows onefile 10 datas / 3 binaries / icon + version / no COLLECT; onedir the same plus COLLECT DroidWebDisplayWindowsOnedir.

7. A prerelease VERSION crashed the Windows build

int(part) for part in VERSION.split(".") raises on 0.11.8-rc.1. This project has published -rc tags, so it's a shape it uses — it just hasn't reached VERSION yet. The numeric VS_FIXEDFILEINFO tuple now takes the leading MAJOR.MINOR.PATCH while the version strings keep the full text.

Verification

No PyInstaller, node or pytest in the authoring environment, so the packages were not built here. All three specs parse, the stubbed evaluation matches prior behaviour, and both shell scripts pass bash -n.

Added tests/packaging/test_pyinstaller_spec_inputs.py, which executes the shared module rather than grepping the specs — a spec that stops bundling a file fails there instead of passing a source-text match. It skips where PyInstaller isn't installed.

CI is the real test here: it builds the AppImage and both Windows packages, and now verifies the AppImage version too.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XfT5Z3Z24bjC8qtqERGpMQ

ami3go and others added 9 commits August 20, 2026 14:59
Both Windows specs decoded droidwebdisplay.ico.base64 into
packaging/windows/droidwebdisplay.ico, a path that is not gitignored. Every
Windows build therefore left an untracked binary in the checkout, so
`git status` came back dirty and a routine `git add -A` would commit a
generated artifact.

The icon now lands in PyInstaller's workpath (./build/..., already covered by
the `**/build/` rule), falling back to ROOT/build when workpath is absent.
The tracked .base64 remains the single source.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfT5Z3Z24bjC8qtqERGpMQ
The Windows job checks PE FileVersion and ProductVersion against the VERSION
file and fails the build on a mismatch. Linux had no equivalent, yet
release.yml renames DroidWebDisplay-ci-linux-x86_64.AppImage to
DroidWebDisplay-v<version>-linux-x86_64.AppImage and publishes it, so the
filename asserted a version nothing had verified and its hash went into
SHA256SUMS.txt on that basis.

The new step extracts the built AppImage, compares the VERSION it actually
bundles against the tree, and confirms adb is present -- matching what the
Windows job already does for its two packages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfT5Z3Z24bjC8qtqERGpMQ
DroidWebDisplay.spec set upx=True on both EXE and COLLECT while both Windows
specs use upx=False. upx is not installed on the Linux runner -- it appears in
neither apt-get list -- so PyInstaller logged that UPX was unavailable and
carried on. The setting was therefore untested configuration that would begin
compressing binaries unreviewed the moment upx landed on an image, and
UPX-compressing Qt binaries is a known cause of corrupt executables and
antivirus false positives.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfT5Z3Z24bjC8qtqERGpMQ
droidwebdisplay.desktop.in carried no Icon key and install.sh never placed
droidwebdisplay.svg on the icon search path, so a system install produced a
launcher entry with a generic placeholder. The AppImage build meanwhile
inlines its own desktop file that does set Icon=droidwebdisplay -- two
definitions of the same entry, and the installed one was the poorer.

The template now sets Icon=droidwebdisplay, install.sh copies the SVG into
XDG hicolor/scalable/apps and refreshes the icon cache when the tool exists,
and uninstall.sh removes it again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfT5Z3Z24bjC8qtqERGpMQ
DroidWebDisplay.spec carried an `if sys.platform == "win32"` branch that CI
never reaches -- Windows builds use the two dedicated specs. That branch
produced a onefile exe with no icon and no VS version resource, which is
precisely the binary ci.yml's PE metadata check exists to reject, so anyone
running this spec on Windows got a package that would fail the gate for
reasons the spec itself caused.

It now refuses to run on Windows and names the specs to use instead. The
platform-conditional adb name list and the duplicated EXE block collapse to
the single Linux form.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfT5Z3Z24bjC8qtqERGpMQ
Both specs did `int(part) for part in VERSION.split(".")`, so a VERSION of
0.11.8-rc.1 raised ValueError and failed the Windows build outright. This
project has already published -rc tags (v0.11.2-rc.1, stable-v0.11.2-rc.3),
so that is a shape it uses -- it simply has not reached the VERSION file yet.

The numeric VS_FIXEDFILEINFO tuple now comes from the leading MAJOR.MINOR.PATCH
and ignores any suffix, while the FileVersion and ProductVersion strings keep
the full VERSION text. A value that does not start with three numbers is still
rejected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfT5Z3Z24bjC8qtqERGpMQ
The Linux, Windows onefile and Windows onedir specs repeated the bundled-data
list, the ADB discovery and the hidden-import computation verbatim -- 339 lines
of which roughly 90% was duplicated. Adding a bundled file meant editing three
places, and missing one produced a package broken on a single platform only.

packaging/pyinstaller/_dwd_common.py now owns those inputs plus the VERSION
parsing and the Windows icon decode. Each spec keeps only what genuinely
differs: its platform guard, and its EXE/COLLECT shape. Spec files are exec'd
rather than imported, so they reach the module through SPECPATH on sys.path;
nothing in it is imported by the application.

Verified by evaluating all three specs against a stubbed PyInstaller, which
reproduces the pre-refactor inputs exactly: Linux gets 10 datas, 1 binary, no
icon or version resource, COLLECT DroidWebDisplay; Windows onefile gets 10
datas, 3 binaries, icon and version, no COLLECT; Windows onedir the same plus
COLLECT DroidWebDisplayWindowsOnedir. The Linux spec still refuses to run on
win32.

Added tests/packaging/test_pyinstaller_spec_inputs.py, which executes the
module rather than grepping the specs -- a spec that stops bundling a file
fails there instead of passing a source-text match. It skips when PyInstaller
is absent, since only the packaging jobs install it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfT5Z3Z24bjC8qtqERGpMQ
CI caught both. Neither is a behaviour regression; both are the same pattern
this codebase keeps hitting, where a test asserts source text and so breaks on
a refactor that changed nothing a user can observe.

test_packaging.py::test_windows_package_is_windowed_desktop_host read the
win32 branch of DroidWebDisplay.spec and asserted console=False inside it.
That branch was dead -- CI never reached it, and it built an exe with no icon
and no version resource, exactly what the PE metadata check rejects. Removing
it broke this test, which is to say the test existed to protect it. It now
asserts the property it is named for against the two specs Windows actually
builds, and that the Linux spec redirects Windows builds to them.

test_windows_package_hardening.py asserted the literal
`with_suffix(".ico.base64")` appeared in each Windows spec. That pinned how
the icon path was spelled rather than that the icon comes from the tracked
base64 source. It now asserts the specs delegate to common.windows_icon, that
the shared module names the .base64 source, and that neither spec writes back
to packaging/windows/droidwebdisplay.ico -- which is the actual property worth
protecting, since that path is not gitignored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfT5Z3Z24bjC8qtqERGpMQ
…setup form

Reported: first-run setup correctly shows the full form with PIN and Confirm
PIN, but locking afterwards showed that same setup form rather than a simple
unlock prompt.

The gate already renders two modes -- #renderGate(configured) sets the title,
the submit label, and hides the Confirm PIN row -- but the
droidwebdisplay-auth-required listener only did:

    this.elements.gate.hidden = false;

It never re-rendered, so the gate reappeared in whatever mode it was last
drawn. After first-run setup that is setup mode, leaving "Create bridge PIN"
and a visible Confirm PIN box on screen for what is actually a login. The
submit handler reads #status.configured, which is true by then, so it did
perform a login -- the form simply described the wrong operation. A fresh page
load looked right, because ensureAuthenticated() renders from a fresh status;
only a lock within the same page session showed it.

The listener now re-reads /api/v1/auth/status and re-renders from it. The
server is the authority on whether a PIN exists, so this is also correct after
a PIN change or a revocation from another browser. If the status request fails
the last known value is used, so the user is never left without a way back in,
and if it reports the session is somehow still valid the app is shown instead.

Guarded against re-entry: several in-flight requests can each answer 401 at
once, and #renderGate clears the PIN input, so a burst would wipe the field
under someone already typing. The gate re-renders only when it is currently
hidden.

authStatus is a public request, so its own 401 cannot re-dispatch
droidwebdisplay-auth-required; there is no event loop here.

No node here, so dist/assets/auth-controller.js was hand-applied and the
manifest regenerated. The new layout test asserts the listener delegates, that
the handler re-reads status and re-renders, that the re-entry guard is present,
and that the old unhide-only listener is gone. Not verified in a browser: the
lock-and-reopen sequence itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfT5Z3Z24bjC8qtqERGpMQ
@ami3go
ami3go merged commit 569eb26 into main Aug 20, 2026
3 checks passed
@ami3go
ami3go deleted the fix/build-script-findings branch August 20, 2026 13:39
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