Skip to content

Sync stable packaging pipeline into dev (#527 back-merge) - #531

Merged
mberrys merged 73 commits into
devfrom
feature/sync-stable-packaging-into-dev
Sep 4, 2026
Merged

Sync stable packaging pipeline into dev (#527 back-merge)#531
mberrys merged 73 commits into
devfrom
feature/sync-stable-packaging-into-dev

Conversation

@mberrys

@mberrys mberrys commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Why

#527 was opened against stable, not dev. Nothing on dev changed when it merged, so the Windows MSI on dev is failing for exactly the reasons it was before — dev is 59 commits behind stable and still carries the manual windeployqt / linuxdeployqt staging that #527 replaced with the CMake-generated Qt deploy-script closure.

This merges stable into dev so the integration branch is on the current packaging pipeline.

Conflicts

Three files conflicted; all resolved in favour of stable, because dev's side is exactly the superseded approach:

File Resolution
.github/workflows/WindowsInstall.yml Keep the pull_request: trigger so packaging runs per PR instead of only on manual dispatch. dev's only change here (#509) added a comment, not a competing trigger.
.github/workflows/LinuxInstall.yml Drop the hand-rolled Linux Deploy Qt step; qt_generate_deploy_qml_app_script stages the closure now.
scripts/ci/test_workflow_contracts.py Take the inverted assertions that require the manual deploy tooling to be absent.

dev's own work — the gh-243 budget exhaustion corpus and the LoopLibCore budget/preflight changes behind it — merges cleanly and is preserved.

Verification

  • python3 -m unittest scripts.ci.test_workflow_contracts — 16 tests, pass
  • python3 scripts/agent/check-change.py --base origin/devchangelog, source_integrity, architecture_catalog, policy_adapters and every format: check pass except one noted below. The build:* and focused_tests checks report fail only because no configured build tree exists in the agent container (/home/user/loop/build is not a directory); configuring one requires approval under the verification budget. clang_tidy is incomplete for the same reason. These are not proven and need a real build.

Known open failure — not fixed here

Windows_MSI is still expected to fail at the relocated-tree LoopEditor --quick-smoke step with exit -1073741819 (0xC0000005, access violation, no output). That failure is inherited from stable: #527's own Windows_MSI check was red on run 33830959878 when it merged. It got past the loop-default.json profile-resolution bug it was fixing and died one step later. Diagnosing it needs a Windows runner; a dispatch on this merge's SHA is the next step.

Pre-existing issue noticed

LoopLibCore/sources/pdftextlayoutgenerator.h:44 fails clang-format --dry-run --Werror on dev and on stable alike, and the file here is byte-identical to stable. Left untouched deliberately — it is a protected path and reformatting it would widen this merge.

🤖 Generated with Claude Code

https://claude.ai/code/session_01APUGt8sqXNZwQjki6B92S3


Generated by Claude Code

mberrys and others added 30 commits September 2, 2026 02:23
- LoopLibCore: pass PDFProcessingBudget to PDFTextLayoutGenerator during searchDocumentText to enforce hostile-workload limits
- LoopEditor QuickOutlineModel: expose page role from PDFOutlineItem destination and route via implemented goToPage/goToOutlinePage
- LoopEditor DocumentPane/Host: make searchPanelVisible one-shot via acknowledgeSearchPanel to prevent repeated reveal on presentationChanged
- UnitTests: bump catalog implemented count 16 -> 25 to match shell-implemented find/layout commands added in unstable
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
Promotes `unstable` into `stable` for the 0.2.1 release line.

**Merge commit:** `2ab848b7253714a735640067031eaf45f29a7d65`

This is the Session 07 package-workflow SHA after PR #490 landed on
`unstable`.
<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a
href="https://cursor.com/agents/bc-add9255c-ab75-4ce7-a6ef-326374bfe342?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/background-agent?bcId=bc-add9255c-ab75-4ce7-a6ef-326374bfe342&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img
alt="Open in Cursor" width="131" height="28"
src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>
AppImage packaging failed because appstreamcli could not reach the
legacy mberrys/Loop-pdf homepage, bugtracker, and help URLs (404).
Update metainfo to the canonical studio-berry/loop repository.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Windows MSI packaging failed because sentry-cli uploaded PDBs to
berry-studios/loop-pdf, which does not exist on de.sentry.io. The org
project slug is loupe-pdf (project id 4511866328449104, matching the
compile-time DSN).

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
…tract

sentry-cli accepts project id 4511866328449104 (same as compile-time DSN)
instead of the legacy slug, avoiding the banned product token in tracked
source while still uploading to the correct berry-studios EU project.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
No Loop-named project slug exists on de.sentry.io; loop-pdf is absent and
the legacy slug is policy-banned. Derive the numeric project id from the
canonical LOOP_SENTRY_DSN in CMakeLists.txt instead of hardcoding slugs in
workflows or tracked defaults.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
…521)

<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Summary

Session 07 packaging fixes on one branch for merge into `stable`.

### Linux AppImage
AppStream metainfo URLs → `studio-berry/loop` (fixes `url-not-reachable`
on legacy `mberrys/Loop-pdf`).

### Windows MSI — Sentry debug upload
`loop-pdf` slug does not exist on `de.sentry.io`. The real project is id
**`4511866328449104`** (matches compile-time DSN in `CMakeLists.txt`).

`sentry-cli debug-files upload` accepts that numeric project id.
Workflows and script default now use `4511866328449104` instead of any
legacy slug, satisfying `check_loop_identity` (banned legacy product
token) while uploading to the correct `berry-studios` EU project.

## Verification (local)
- `python3 scripts/ci/check_loop_identity.py` — pass
- `python3 -m unittest discover -s scripts/ci -p 'test_*.py'` — pass
(223 tests)
- `python3 scripts/ci/check_supply_chain_pins.py` — pass
- `appstreamcli validate --pedantic` on appdata.xml — pass

## Scope
Minimal packaging/config only. Not merged; parent handles merge +
package re-dispatch.
<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a
href="https://cursor.com/agents/bc-9291ca40-dca3-4c3a-9911-540c440e8f37?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/background-agent?bcId=bc-9291ca40-dca3-4c3a-9911-540c440e8f37&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img
alt="Open in Cursor" width="131" height="28"
src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>
vcpkg's gperf port requires autoconf, autoconf-archive, automake, and
libtool (libtoolize) from the system package manager. Without them the
Linux_AppImage workflow fails at 'VCPKG: Install Project Dependencies'
when building fontconfig's gperf dependency.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
The version and capabilities probes run before $pdfTool was assigned,
causing Windows_MSI to fail at Validate staged install tree under
Set-StrictMode. Move the Join-Path assignment above the first use.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
…age-proof

# Conflicts:
#	.github/workflows/LinuxInstall.yml
Combine Linux AppImage workflow deps: keep autoconf/automake/libtool for
gperf/vcpkg (#523) and libfontconfig1-dev from stable. Retain stable's
VCPKG_BINARY_SOURCES cache wiring.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
…dio-berry/loop into cdx/session-07-package-proof

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Remove continue-on-error from the Sentry debug-file upload steps in
WindowsInstall.yml and reusable-windows.yml. Project targeting already
resolves the numeric id from LOOP_SENTRY_DSN; a 404 must fail the job
instead of producing a green MSI with a skipped upload.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Install autoconf, autoconf-archive, automake, and libtool on reusable-linux
and fuzz apt steps so vcpkg can build gperf via fontconfig, matching
LinuxInstall.yml (#523).

Fixes Release Gate linux/build and fuzz_regression failures on PR #527.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Stage libqoffscreen.so (and optional libqminimal.so) under
install/usr/lib/platforms and write install/usr/bin/qt.conf before
linuxdeployqt so PdfTool preflight works when the smoke harness sets
QT_QPA_PLATFORM=offscreen and strips developer Qt paths.

Matches the Windows MSI pattern of a self-describing staged tree and
the prior Session 07 offscreen staging work that did not land on this
branch.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
claude and others added 15 commits September 3, 2026 19:46
Six test targets still linked LoopEditorQuick, which this change folded
into the LoopEditor executable. CMake treats an unknown target name in
target_link_libraries as a raw -l flag, so configure kept succeeding and
the breakage stayed invisible until the widgets-free gate stopped failing
first and the build actually reached these targets:

  tst_quickdocumentmodeltest.cpp:2:10: fatal error: quickdocumentmodel.h:
  No such file or directory

LoopEditorQuick had carried its source directory as a PUBLIC include, so
retiring it took the LoopEditor headers off every dependent's include path.

Compile the LoopEditor sources each test needs directly into it and add the
LoopEditor include root, matching how ProductQuickAccessibilitySmoke
already reaches the same sources now that no library exports them.

Each target gets only what its test includes, so the deliberately minimal
link lines are preserved: UnitTestsDocumentViewSession keeps Core/Gui/Test
with no QML engine, per architecture invariant I24. UnitTestsQuickAccessibility
also drops LoopEditorQuickplugin and LoopEditorQuickplugin_init, retired
with the library.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UwdQdPY7TZ1fQ1cvpNrpaq
Packaging only ran on workflow_dispatch with a hand-supplied source_sha, so
no pull request ever proved the MSI or the AppImage. A one-character CMake
path bug reached this branch and survived three days of green push CI
because nothing automatic ever ran cmake --install on Windows.

Add a pull_request trigger to both packaging workflows, resolving the
checkout to the dispatch input or the PR head. The exact-SHA verification is
unchanged and still runs on both paths: a PR head SHA is a full 40-character
SHA, so the integrity check holds. Narrowing this with a paths: filter is a
one-line edit if the runner cost bites.

Also defuse three assertions that report ordinary work as a contract breach:

- The packaging build command was pinned as an exact string, -j6 and target
  order included. Pin the target list instead, which is the actual contract.
- counts["targets"] was pinned to a literal. It is the size of an inventory
  that grows with ordinary development, and validate_contract() already
  checks what the evidence means, so drop it. The plugin ledger and the UI
  form ledger are finite, so keep those but assert the set and the ledger
  length rather than a hand-maintained number: a thirteenth plugin now names
  itself instead of arriving as "13 != 12".
- The supply-chain policy required a linuxdeployqt pin while the workflow
  contract forbade the workflows from mentioning the tool. It is no longer
  used, so drop the pin and its required-tools entry.

ubuntu-22.04 stays pinned deliberately: it sets the AppImage glibc floor.
It now carries a comment saying so, so retiring it reads as the decision it
is rather than an assertion to satisfy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UwdQdPY7TZ1fQ1cvpNrpaq
The AppImage job now reaches packaging and installs cleanly, and fails on
the next thing: forty-odd Qt plugins reported as Loop artifacts.

  product-surface: unmanifested first-party artifact:
    usr/lib/qml/QtQuick/Controls/libqtquickcontrols2plugin.so

Qt names its plugins lib<something>plugin.so, and the first-party glob
lib*Plugin* is matched case-insensitively, so it claims every one of them.
What used to stop that is ignored_artifact_paths, but its patterns describe
a flat layout (qml/*, platforms/*) while the deploy script stages modules at
usr/lib/qml/<Module>/... and plugins/<type>/..., which those patterns never
match. Under linuxdeployqt none of this was in the tree at verification time,
so the mismatch could not show.

Name the deployed layout: usr/lib/qml/* and plugins/*. Loop's own plugins
install to usr/lib/loop and usr/lib/pdfplugins, so neither pattern reaches
them, and a regression test pins that in both directions.

This narrows only the first-party classification. Detection of forbidden
installed artifacts runs against every file in the tree and is not gated by
it, so the guarantee that a retired plugin cannot ship is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UwdQdPY7TZ1fQ1cvpNrpaq
Five of the six retargeted tests build. UnitTestsShellKeyboard does not:

  LoopEditor/editorhost.cpp:30:
  LoopLibQuick/sources/loopcanvasitem.h:38:10:
  fatal error: QQuickItem: No such file or directory

editorhost.cpp includes loopcanvasitem.h, which includes <QQuickItem>, and
this target's link line names Qt6::Qml but not Qt6::Quick. It compiled before
because the retired LoopEditorQuick linked Qt6::Quick PUBLIC and passed it
down; LoopLibQuick does not. Naming it restores exactly the environment the
target already had, and matches UnitTestsEditorHost, which compiles the same
sources and builds.

Qt6::Widgets stays absent, which is the invariant this file is protecting:
I21 for the interaction tests and I25 for UnitTestsQuickCanvas both turn on
the missing Widgets link, not on Quick.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UwdQdPY7TZ1fQ1cvpNrpaq
The AppImage job now builds, installs and clears both verification gates,
and fails one step later:

  scripts/smoke-test-install.ps1:80
  (Join-Path $env:ProgramFiles "share\loop\profiles")
  Cannot bind argument to parameter 'Path' because it is null.

This step is new here; stable never ran smoke-test-install.ps1 on Linux.
The script is Windows-shaped throughout -- $env:ProgramFiles, backslash path
literals that become literal filenames on Linux, .exe and .dll suffixes at ten
call sites, and an OCR sidecar block that invokes a .cmd mock with no Linux
equivalent. Making it portable is real work with judgement calls in it, not a
null guard, and it is not what this change is for.

Remove the step and leave Linux where stable had it. Windows keeps its own
relocated-install smoke unchanged, and Linux keeps the AppImage smoke test
that runs against the packaged artifact.

Cross-platform smoke coverage for the relocated Linux tree stays worth having
and is filed as follow-up.

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

Three CI failures, two root causes, verified against run 33808874249/33808874196:

1. Windows MSB3191 + Linux QML No-such-file (one root):
   qt_add_qml_module QML_FILES used ../LoopEditor/qml/*.qml. The '..'
   makes Qt derive qmlcache dir ProductQuickAccessibilitySmoke_.._LoopEditor,
   which MSBuild rejects (MSB3191), and misplaces the embedded resource as
   qrc:/qt/qml/Loop/LoopEditor/qml/Main.qml so loadFromModule Loop.Quick/Main
   exits 2 on Linux. Fix: mirror the six QML files under
   ProductQuickAccessibilitySmoke/qml/ (byte-identical, LoopEditor/qml stays
   the single source of truth) and list them without '..'. Proven locally
   with Qt 6.11.1/MinGW: fixed layout configures, builds with a clean
   ProofSmoke_qml qmlcache dir, and loadFromModule resolves LOAD_OK;
   the '..' layout fails Qt file staging on Windows by construction.

2. Windows relocated/staged/MSI smoke Could not locate loop-default.json:
   Resolve-ProfilesDir assumed one depth and its comment named a deleted
   plugin owner. Fix: bounded upward walk (InstallDir + 3 ancestors x
   share/usr-share) covering staged bindir, relocated bindir and MSI
   install-root layouts, keeping the legacy ProgramFiles fallback; comment
   now names PdfTool as the install owner. The smoke also accepts an
   install root for binaries (MSI .../LOOP -> .../LOOP/usr/bin) via a
   resolved binDir used by every binary-relative path.

3. Relocated-tree InstallDir pointed at a phantom level:
   Copy-Item to a nonexistent destination copies contents directly, so the
   tree lands at relocated/usr/bin, not relocated/install/usr/bin.
   The workflow now passes the real path.

Out of scope, intentionally untouched: heat already harvests the whole
build/install tree (profiles reach the MSI); MSIX/ZIP pack only usr/bin
because AppxManifest.xml must sit at the package root and MSIX is not
Session 07 gate evidence per docs/PLATFORM_SUPPORT.md.
Bring the Session 07 package-proof work (#527) onto the integration
branch. #527 was opened against stable, so dev never received any of it
and still carried the manual windeployqt/linuxdeployqt staging that #527
replaced with the CMake-generated Qt deploy-script closure.

Conflicts resolved in favour of stable in all three files, since dev's
side is exactly the superseded approach:
- .github/workflows/WindowsInstall.yml: keep the pull_request trigger so
  packaging runs per PR instead of only on manual dispatch. dev's only
  change here (#509) added a comment, not a competing trigger.
- .github/workflows/LinuxInstall.yml: drop the hand-rolled 'Linux Deploy
  Qt' step; qt_generate_deploy_qml_app_script now stages the closure.
- scripts/ci/test_workflow_contracts.py: take the inverted assertions
  that require the manual deploy tooling to be absent.

dev's own work (the gh-243 budget exhaustion corpus and the LoopLibCore
budget/preflight changes behind it) merges cleanly and is preserved.

Windows_MSI is still expected to fail at the relocated-tree
LoopEditor --quick-smoke step (exit -1073741819 / 0xC0000005). That
failure is inherited from stable, where #527 merged with it red, and is
not resolved by this merge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APUGt8sqXNZwQjki6B92S3
Merging stable into dev combined dev's budget-exhaustion corpus with
stable's Loop rename contract, and three checks went red on the merge
commit: source_integrity, policy, and agent-fast / build.

Two leftovers, both flagged as "legacy product token":

- reusable-linux.yml:79 and reusable-windows.yml:92 ran the corpus
  verification step with `working-directory: loupe` while checkout uses
  `path: loop` and every sibling step says `loop`. This was not only a
  token: agent-fast / build died with "An error occurred trying to start
  process '/usr/bin/bash' with working directory
  '/home/runner/work/loop/loop/loupe'. No such file or directory".

- The budget-exhaustion corpus schema kind was still
  `loupe-processing-budget-exhaustion-corpus`. Renamed to `loop-` in the
  three places that carry it -- the generator constant, the manifest
  fixture and the test assertion. Nothing outside the repository reads
  it, so the three move together and schema_version stays at 2.

Verified:
- python3 scripts/ci/check_loop_identity.py -- passed
- python3 scripts/ci/test_check_loop_identity.py -- OK
- python3 -m unittest discover -s scripts/ci -p 'test_*.py' -- 227 tests, OK
  (this is the suite the failing `policy` check runs)
- python3 scripts/budget_exhaustion/generate_corpus.py --check -- corpus
  up to date, 13 fixtures
- clang-format --dry-run --Werror on the touched test -- clean
- both reusable workflows and manifest.json re-parse

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APUGt8sqXNZwQjki6B92S3
Removing linuxdeployqt left no step to copy the desktop entry, icon, and
AppRun into the install root. appimagetool aborts with 'Desktop file not
found' when only usr/share/applications holds the .desktop file.

Add scripts/prepare-appdir.sh and invoke it before Pack AppImage.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
- Deploy only qsqlite from the SQL driver set; optional vendor drivers
  (Mimer/OCI) reference libraries we do not ship and fail package-boundary
  inspection.
- Prune non-QSQLITE drivers in prepare-appdir.sh as a belt-and-suspenders
  guard before AppImage packing.
- Search install-root plugins/lib/qml paths before QGuiApplication starts so
  Windows relocated-tree smoke resolves the Qt closure without developer env.
- Fix clang-format violation in pdftextlayoutgenerator.h blocking agent-fast.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Attach a parent console for --quick-smoke on Windows so QT_DEBUG_PLUGINS
output from the relocated install smoke does not fault in a GUI-subsystem
binary. Defer the normal-shell QQmlApplicationEngine until after the smoke
path returns, install the Loop.Canvas QML plugin with the packaged import
tree, and fix the clang-format violation on pdftextlayoutgenerator.h that
was blocking agent-fast.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Revert install(TARGETS LoopLibQuickplugin) from LoopLibQuick, which
failed Phase 5 contract validation on Windows and could not resolve the
plugin artifact during Linux cmake --install.

LoopEditor now copies the plugin, qmldir, and qmltypes into
usr/lib/qml/Loop/Canvas with install(FILES), which Phase 5 does not
treat as an unmanifested product install target.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
CI packaging builds LoopEditor explicitly but not the Loop.Canvas plugin
module. install(FILES) then failed because libLoopLibQuickplugin.so was
never linked. Add an explicit LoopEditor dependency on LoopLibQuickplugin.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>

mberrys commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Status from my side, plus a root-cause finding on the Windows failure that I don't think has been named yet.

What I fixed

source_integrity and policy are green as of af3ef19. Both were the same cause: merging stable in combined dev's budget-exhaustion corpus with stable's Loop rename contract, surfacing five legacy tokens. working-directory: loupe in reusable-linux.yml:79 / reusable-windows.yml:92 was also what killed agent-fast / build outright (An error occurred trying to start process '/usr/bin/bash' with working directory '.../loop/loupe'. No such file or directory).

The Windows failure has changed character

It is no longer the LoopEditor --quick-smoke access violation that #527 merged with. On 16e6382 the relocated-tree smoke dies earlier:

OK: Report schema
Exception: scripts/smoke-test-install.ps1:210
PdfTool version probe failed with exit code -1073741515

-1073741515 is 0xC0000135STATUS_DLL_NOT_FOUND, not the 0xC0000005 access violation seen before. On run 33830959878 (#527's head) that same probe passed: OK: PdfTool reports a canonical semantic version. So the relocated tree has gone from "PdfTool loads, editor crashes" to "PdfTool cannot resolve its DLLs at all".

Why, reading the tree at 63e3f5b

The only Qt deployment left anywhere in the repo is:

LoopEditor/CMakeLists.txt:90:  qt_generate_deploy_qml_app_script(TARGET LoopEditor ...)
  • CMakeLists.txt:46 still defines option(LOOP_INSTALL_QT_DEPENDENCIES ...), and WindowsInstall.yml still passes -DLOOP_INSTALL_QT_DEPENDENCIES=ON, but the install(DIRECTORY ${LOOP_QT_ROOT}/...) block that used to act on that option was deleted on stable in 784f419 ("deploy Qt closure from installed tree"). The option is now close to a no-op at the root level.
  • grep -rn windeployqt .github/workflows/*.yml returns nothing — the manual staging step went away with the same change.

That leaves the entire Windows Qt runtime closure generated from one target, LoopEditor. PdfTool is a separate executable that is not a deploy-script target, so nothing guarantees its runtime dependencies are staged into usr/bin. That matches the symptom exactly: the first thing the smoke runs from the relocated tree is PdfTool.exe --version, and it fails to load.

Worth checking whether the recent install(FILES) / plugin-ordering changes shifted what lands in usr/bin, since the PdfTool probe passing on #527's head and failing now is a regression within this branch, not an inherited failure.

Not pushing

CI on 63e3f5b is still in flight and the last five commits are active work from another agent on these same files. I'm not pushing a competing fix on top of an in-flight run — say the word and I'll take it, otherwise I'll keep watching and report.


Generated by Claude Code

agent-fast / build runs scripts/agent/check-change.py, and on 63e3f5b it
was the only red check left: build_ubuntu, fuzz, CodeQL, source_integrity,
policy and architecture-docs all passed, so the code compiles and the
tests run. The remaining failure was the format gate:

  format:LoopLibCore/sources/pdftextlayoutgenerator.h => fail
  pdftextlayoutgenerator.h:44:6: error: code should be clang-formatted

The violation is not new -- the file is identically unformatted on dev and
on stable. It only became blocking here because check-change.py formats
the files a change *touches*, and this is the first change whose diff
includes that header (the stable merge brings it in). So neither branch
ever evaluated it, and this PR cannot go green while it stands.

The fix is what clang-format emits, three lines, on an empty constructor
body: `m_features(features) {}` becomes an opened brace pair on its own
lines. No semantic change, and formatting a touched file is inside the
autonomous verification budget.

Verified: clang-format --dry-run --Werror clean on the file, and
check-change.py --base origin/dev now reports no non-environmental
failures (the build:* and focused_tests entries still fail only because
this container has no configured build tree).

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

mberrys commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

agent-fast / build is still red on 7f29bd4, and my clang-format fix was necessary but not sufficient. Narrowing it down, with what I can and cannot see.

What the job actually did

From the job's step list, not the log:

  • Step 33 "Build project" — success (11:32→11:39)
  • Step 43 "Prove change with agent-fast" — failure, 11:39:17 → 12:13:25 (34 minutes)

So the tree compiles; the failure is inside scripts/agent/check-change.py.

What the evidence JSON shows

The tail of the emitted report is unambiguous on the checks it covers:

  • focused_testspass100% tests passed, 0 tests failed out of 44
  • every clang_tidy:* entry visible → pass
  • the build:* entries visible (UnitTestsViewportController, UnitTestsWorkloadEnvelope) → pass
  • trailer: "risk": "high", "status": "fail", comparison_base_sha: 1c0481b

status is fail only when some entry has result == "fail" (check-change.py:415). So exactly one check in the array is red, and it is above the portion I can reach.

Why I can't name it

The checks array holds roughly 70 entries — 4 leading checks, 9 format:*, ~54 build:*, 9 clang_tidy:*, focused_tests — and the clang_tidy entries embed thousands of lines of warning output each. The failing entry sits somewhere in the ~1700 log lines above the tail. The log tooling I have exposes only tail_lines, with no offset or search, so I cannot slice the middle out without pulling the entire job log.

What I could rule out locally, running the same script against the same tree at comparison_base_sha 1c0481b: changelog, source_integrity, architecture_catalog, policy_adapters and all nine format:* checks pass (clang-format 18.1.3, same major as the ubuntu-24.04 runner). Locally the build:* and focused_tests entries fail only because this container has no configured build tree, which is exactly the region that masks the CI failure from me.

So my best remaining hypothesis is a single build:<target> entry — a per-target cmake --build that "Build project" and build_ubuntu don't cover. I am not going to push a fix on a hypothesis; I've already been wrong once assuming the format gate was the whole story.

What would settle it in seconds

Opening the job and searching the "Prove change with agent-fast" output for "result": "fail" names the check directly. If someone pastes that entry here — or re-runs with the evidence artifact upload not gated behind the step succeeding — I'll take the fix from there.

Unrelated and still open: the build_windows PdfTool DLL-not-found diagnosis in my earlier comment stands.


Generated by Claude Code

…legible

Every Windows relocated-tree failure has reported an empty message:

  LoopEditor native Quick startup failed with exit code -1073741819:

with nothing after the colon, even though the workflow step sets
QT_DEBUG_PLUGINS=1 and QML_IMPORT_TRACE=1. That is not a capture bug in
the script -- it is the subsystem.

LoopEditor is built WIN32_EXECUTABLE ON (LoopEditor/CMakeLists.txt:74),
so it is a Windows GUI-subsystem binary with no console attached. Qt's
default message handler routes qDebug/qWarning/qFatal to
OutputDebugString rather than stderr in that case, so `2>&1` captures
nothing and the plugin/QML trace never reaches the log. Every diagnosis
of this crash so far has had to work without the one thing that would
explain it.

QT_FORCE_STDERR_LOGGING=1 makes the default handler write to stderr
regardless of console attachment. Set it around the --quick-smoke
invocations, saved and restored with the other Qt variables the script
already scrubs, so the next run reports the actual QML module, plugin or
fatal error behind the 0xC0000005.

This changes no packaging or install behaviour; it only makes the
existing failure legible.

Also of note for whoever picks this up: PdfTool now passes the relocated
smoke on 7f29bd4 (version and capabilities probes both OK). The earlier
0xC0000135 STATUS_DLL_NOT_FOUND is fixed, so the PdfTool deploy-closure
theory in PR comment 5539411918 is obsolete -- the remaining blocker is
the original LoopEditor Quick startup access violation.

Verified: python3 -m unittest discover -s scripts/ci -p 'test_*.py' --
227 tests OK; check_loop_identity passed; check-change.py --base
origin/dev reports no non-environmental failures.

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

mberrys commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Correcting my earlier comment, and pushing the reason nobody has been able to read this crash.

PdfTool is fixed — my previous diagnosis is obsolete

On 7f29bd4 the relocated smoke gets further than it did on 16e6382:

OK: PdfTool reports a canonical semantic version
OK: PdfTool capabilities report the Loop PdfTool identity
OK: legacy editor executable absent
OK: OcrPlugin.dll absent (V1 CLI-only OCR surface, MIC-343)
Exception: scripts/smoke-test-install.ps1:309
LoopEditor native Quick startup failed with exit code -1073741819

The 0xC0000135 STATUS_DLL_NOT_FOUND on PdfTool.exe --version is gone — the recent packaging commits resolved it. So the PdfTool deploy-closure fix I proposed in #531 (comment) is no longer the right change; please disregard that recommendation. I was about to push it and stopped after re-reading the current log.

The remaining blocker is the original failure: LoopEditor --quick-smoke exiting -1073741819 (0xC0000005, access violation) — the same one #527 merged with.

Why this crash has been undiagnosable

Note what follows the colon in that message: nothing. The captured output is empty on every occurrence, even though the workflow step sets QT_DEBUG_PLUGINS=1 and QML_IMPORT_TRACE=1.

That is not the script failing to capture. LoopEditor is built WIN32_EXECUTABLE ON (LoopEditor/CMakeLists.txt:74) — a Windows GUI-subsystem binary with no console. Qt's default message handler routes qDebug/qWarning/qFatal to OutputDebugString instead of stderr when no console is attached, so 2>&1 collects nothing and the plugin and QML import traces never reach the log. Every attempt at this crash, mine included, has been working without the one output that would explain it.

What I pushed (2edd2fc)

QT_FORCE_STDERR_LOGGING=1 around the --quick-smoke invocations, saved and restored alongside the Qt variables the script already scrubs. Eight lines, no packaging or install behaviour changed — it only makes the existing failure legible, so the next Windows run should name the QML module, plugin, or fatal error behind the access violation.

Verified before pushing: python3 -m unittest discover -s scripts/ci -p 'test_*.py' — 227 tests OK; check_loop_identity passed; check-change.py --base origin/dev reports no non-environmental failures.

agent-fast / build is still red for a separate reason I could not isolate — see my previous comment; that one still needs the failing "result": "fail" entry from the job output.


Generated by Claude Code

cursoragent and others added 2 commits September 4, 2026 12:25
Relocated install smoke runs LoopEditor as a GUI-subsystem binary.
QT_DEBUG_PLUGINS=1 makes Qt emit plugin diagnostics to a console that is
not attached, which crashes with 0xC0000005 before --quick-smoke can run.

Drop the debug env vars from the Windows workflow step and strip them in
smoke-test-install.ps1 alongside the other developer Qt paths. Keep the
Windows console attach fallback for manual diagnostics.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
agent-fast / build has been red on every commit of this branch since the
stable merge. The cause, from the check-change.py evidence report:

  "name": "build:LoopEditorQuick",
  "command": ["cmake","--build",".../build","--target","LoopEditorQuick", ...],
  "output": "gmake: *** No rule to make target 'LoopEditorQuick'.  Stop.",
  "reason": "exit code 2",
  "result": "fail",
  "duration_ms": 5

Exactly one of the 72 checks fails; the other 71 pass, focused_tests
included (44/44). "Build project" succeeds in the same job, so nothing is
actually broken -- check-change.py is asked to build a target that no
longer exists and gmake refuses in 5 ms.

This is merge integration damage. stable retired the LoopEditorQuick
library in 78e275e ("rebuild the Quick unit tests without the retired
LoopEditorQuick") and folded its QML module into the LoopEditor target
(qt_add_qml_module(LoopEditor ...), LoopEditor/CMakeLists.txt:39). dev
still defines the old target (add_library(LoopEditorQuick STATIC) at
LoopEditor/CMakeLists.txt:25) and dev's agent-policy.json still names it.
Merging the two produced a tree where the target is gone but the policy
still asks for it -- which is why the check is green on dev and red here.

Removing it from the quick module's target list loses no coverage: the
QML module is part of LoopEditor, and LoopEditor is still in the list.
Nothing else in the repository references the target.

Verified:
- python3 scripts/agent/generate-adapters.py --write -- "policy and
  generated adapters are current"; only agent-policy.json changed content
- check-change.py --base origin/dev -- targets are now LoopEditor,
  LoopLibCore, LoopLibInteraction, LoopLibQuick, PdfTool,
  ProductQuickAccessibilitySmoke, UnitTests (no LoopEditorQuick),
  policy_adapters passes, no non-environmental failures
- python3 -m unittest discover -s scripts/ci -- 227 tests OK
- python3 -m unittest scripts.agent.test_check_change -- 19 tests OK

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

mberrys commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Found and fixed the agent-fast / build failure I asked about earlier — no need for anyone to dig out that entry.

The failing check

Exactly one of the 72 checks in the check-change.py report fails; the other 71 pass, focused_tests included at 44/44:

{
  "name": "build:LoopEditorQuick",
  "command": ["cmake", "--build", ".../build", "--target", "LoopEditorQuick", "--config", "Release"],
  "output": "gmake: *** No rule to make target 'LoopEditorQuick'.  Stop.",
  "reason": "exit code 2",
  "result": "fail",
  "duration_ms": 5
}

"Build project" succeeds in the same job. Nothing is broken — check-change.py is asked to build a target that no longer exists, and gmake refuses in 5 ms.

It's merge integration damage

  • stable retired the LoopEditorQuick library in 78e275e ("rebuild the Quick unit tests without the retired LoopEditorQuick") and folded its QML module into the LoopEditor target itself (qt_add_qml_module(LoopEditor ...), LoopEditor/CMakeLists.txt:39).
  • dev still defines the old target (add_library(LoopEditorQuick STATIC), LoopEditor/CMakeLists.txt:25) and still names it in agent-policy.json.

Merging the two produced a tree where the target is gone but the policy still asks for it. That is exactly why this check is green on dev and has been red on every single commit of this branch since the merge — including before any of the packaging work started.

Fix (1f2ed65)

Removed "LoopEditorQuick" from the quick module's target list in agent-policy.json. One line. No coverage lost: the QML module is built as part of LoopEditor, which stays in the list, and nothing else in the repository references the retired target.

Verified: generate-adapters.py --write reports policy and adapters current (only agent-policy.json changed content); check-change.py --base origin/dev now targets LoopEditor, LoopLibCore, LoopLibInteraction, LoopLibQuick, PdfTool, ProductQuickAccessibilitySmoke, UnitTests with policy_adapters passing and no non-environmental failures; 227 CI policy tests OK; 19 test_check_change tests OK.

Also this round

build_windows on 50a730e failed at step 13, VCPKG: Install project dependencies, before build or smoke — so neither the console fix nor QT_FORCE_STDERR_LOGGING was exercised, and the 0xC0000005 remains unproven either way. That commit touches no vcpkg input and the identical manifest succeeded on the parent commit 40 minutes earlier, so I treated it as a transient and spent the one re-run on it. If dependency install fails again on the same commit, it is real and I'll root-cause it rather than re-run.


Generated by Claude Code

cursoragent and others added 3 commits September 4, 2026 13:49
Adding install-root usr/lib to QCoreApplication::libraryPaths() on Windows
makes Qt treat shipped product DLLs as plugins, crashing LoopEditor
--quick-smoke with 0xC0000005 before any stderr output.

Resolve QML imports via dedicated paths instead of reusing libraryPaths.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
Headless relocated smoke runs LoopEditor with QT_QPA_PLATFORM=offscreen.
WINDOWS-subsystem binaries crash with 0xC0000005 before Quick startup;
PdfTool and ProductQuickAccessibilitySmoke already use console subsystem.

Also take a minimal --quick-smoke path and rely on qt.conf beside the exe
for Windows plugin discovery instead of scanning install-root plugin paths.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
windeployqt stages qoffscreen under install-root plugins/, but the
Windows-only setLibraryPaths(exeDir) shortcut made Qt scan usr/bin for
plugins and fault with 0xC0000005 before Quick smoke could log anything.

Use packagedLibraryPaths on all platforms, skip adding usr/bin on Windows,
and run --quick-smoke through the normal parser path again.

Co-authored-by: michael berry <mberrys@users.noreply.github.com>
@mberrys
mberrys marked this pull request as ready for review September 4, 2026 15:50
@mberrys
mberrys merged commit 83a4ea5 into dev Sep 4, 2026
13 of 14 checks passed

mberrys commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

⚠️ Flagging b8a8cc4 before this merges — now that the PR is out of draft, it would ship a user-visible Windows regression, and it did not fix the failure it was written for.

What it changes

set(_loop_editor_win32_executable ON)
if(WIN32 AND LOOP_LOOP_DISTRIBUTION)
    set(_loop_editor_win32_executable OFF)
endif()

LOOP_LOOP_DISTRIBUTION is option(LOOP_LOOP_DISTRIBUTION "Loop release profile" OFF) (CMakeLists.txt:101), and it is passed =ON only by the MSI packaging build (WindowsInstall.yml:270). The ordinary Windows CI build passes =OFF (reusable-windows.yml:283).

So the condition applies to exactly one build: the release profile that produces the shipped MSI. Users who install Loop and launch the editor get a console window behind it, every launch. Dev and CI builds keep WIN32_EXECUTABLE ON and are unaffected — the polarity is inverted from what you'd want, since the shipped binary is the one that most needs to stay GUI-subsystem.

And it didn't fix the smoke

On b8a8cc4 itself, step 17 "Smoke relocated installed tree" still failed, one second in (14:33 → 15:24, job 101060439890). Build succeeded, smoke failed anyway. So the subsystem change costs a product regression and has not been shown to buy anything.

The stated rationale — "WINDOWS-subsystem binaries crash with 0xC0000005 [under QT_QPA_PLATFORM=offscreen]" — also isn't a general Qt property; GUI-subsystem Qt apps run fine on the offscreen platform. The empty diagnostics that made this look plausible are, I think, the console/stdio issue from my earlier comment rather than the subsystem itself.

Suggested alternative

The commit message notes that ProductQuickAccessibilitySmoke is already a console-subsystem binary and already loads the same Loop.Quick module. That is the natural thing to exercise Quick startup in the relocated tree — it is a test binary, so making it console-subsystem costs nothing. Keeping LoopEditor GUI-subsystem everywhere and pointing the relocated smoke at the existing console harness gets the same signal without changing what ships.

If the editor itself must be smoke-tested, gate the subsystem on a dedicated build option rather than on the release profile, so the flag that means "this is the build we ship" is never the flag that means "make it a console app".

I have not reverted it: 6f94f95 builds on top and its run is still in flight, and I would rather not blind-revert something that may be load-bearing for a green Windows build. Your call.


Generated by Claude Code

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.

3 participants