From 49274ee471bd6b451f6c9688df1c278d149988a0 Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Wed, 20 May 2026 18:38:27 -0700 Subject: [PATCH 1/2] fix(ci): stop double-nesting framework inside Linux .deb/.rpm packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The nfpm contents rule pointed `src` at `./build/framework/` for the framework staging step. `wheels-core-VER.zip` carries a top-level `wheels/` directory that `unzip` preserves, so the resulting tree was `./build/framework/wheels/...`. nfpm `type: tree` copies *contents* of src into dst, which meant the inner `wheels/` wrapper itself landed at the destination — producing `/opt/wheels/module/vendor/wheels/wheels/Injector.cfc` instead of `/opt/wheels/module/vendor/wheels/Injector.cfc`. After the user-side wrapper sync (`/opt/wheels/module/*` → `~/.wheels/modules/wheels/*`) and `wheels new ` copy, every fresh Linux install ended up with the framework one directory level too deep. Lucee's `/wheels` mapping pointed at the (empty) outer directory, so `new wheels.Injector("wheels.Bindings")` in the generated `public/Application.cfc` threw `could not find component or class with name [wheels.Injector]` on the first request. The existing onError handler then dereferenced `application.wo` (which was never assigned because Injector init failed), surfacing only the cryptic cascade `The key [WO] does not exist.` — issue #2773. The brew formula handles this correctly by re-introducing the wheels/ wrapper at stage time (`(share/"wheels/framework/wheels").install Dir["*"]`). Both Linux nfpm configs now pin `src` at `./build/framework/wheels/` so the contents flatten into `/opt/wheels/module/vendor/wheels/` as intended. The published 4.0.1 .deb / .rpm artifacts ship the broken layout (1 .deb download, 0 .rpm at time of fix). A re-released 4.0.2 will be needed to deliver the fix to users — the change here is to the build config only, not to any framework or CLI code. Tests: `vendor/wheels/tests/specs/cli/LinuxPackageStagingSpec.cfc` gains a per-channel `it()` that asserts `src: ./build/framework/wheels/` + `dst: /opt/wheels/module/vendor/wheels/` are paired in each nfpm yaml. Structural assertion follows the existing #2700 pattern (the file already pins four other packaging invariants the same way). Note on local verification: the structural spec was sanity-checked via equivalent grep / perl POSIX patterns over the YAMLs (positive match for the fixed form, zero matches for the buggy form). Running the spec through the CFML runner locally was blocked by a port-8081 collision with two stale wheels server processes from prior dev sessions — CI compat-matrix will run the spec across every engine × DB on this PR. Closes #2773 Signed-off-by: Peter Amiri --- CHANGELOG.md | 8 +++++ .../linux-packages/nfpm-wheels-be.yaml | 9 +++++- .../linux-packages/nfpm-wheels.yaml | 11 ++++++- .../specs/cli/LinuxPackageStagingSpec.cfc | 29 +++++++++++++++++++ 4 files changed, 55 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 219c217a87..ed58d69ee0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,14 @@ All historical references to "CFWheels" in this changelog have been preserved fo ---- +## [Unreleased] + +### Fixed + +- Linux `.deb` / `.rpm` packages double-nested the framework at `/opt/wheels/module/vendor/wheels/wheels/` instead of `/opt/wheels/module/vendor/wheels/`. `wheels-core-VER.zip` carries a top-level `wheels/` directory that `unzip` preserves; the nfpm `type: tree` rule then copied the entire `build/framework/` tree (wrapper and all) into the destination, leaving `Injector.cfc` one level too deep. Every fresh `wheels new` install on Ubuntu/Fedora then crashed on first request with `could not find component or class with name [wheels.Injector]`, cascading into the cryptic `The key [WO] does not exist.` error in `onError`. The brew formula handles this correctly via `(share/"wheels/framework/wheels").install Dir["*"]`; the Linux nfpm configs now pin `src` at `./build/framework/wheels/` to match. Regression spec at `vendor/wheels/tests/specs/cli/LinuxPackageStagingSpec.cfc` (#2773) + +---- + # [4.0.1](https://github.com/wheels-dev/wheels/releases/tag/v4.0.1) => 2026-05-20 > **Wheels 4.0.1** — first patch on the 4.0 line. Hardens Adobe ColdFusion 2023/2025 compatibility (Adobe-specific `cfheader` attributeCollection rejection, `env()` reserved-word parameter, Vite asset-walk array-by-value), fixes the Windows Scoop install regressions (`wheels.cmd` cmd.exe pre-parser, `.zip.sha512` sidecar layout), and adds `viewStyle` framework presets to `paginationNav()` plus plural `mappings` aliases to `package.json`. ~100 PRs since the 4.0.0 GA (2026-05-12). diff --git a/tools/distribution-drafts/linux-packages/nfpm-wheels-be.yaml b/tools/distribution-drafts/linux-packages/nfpm-wheels-be.yaml index 7239b33ccd..2e19d64fe3 100644 --- a/tools/distribution-drafts/linux-packages/nfpm-wheels-be.yaml +++ b/tools/distribution-drafts/linux-packages/nfpm-wheels-be.yaml @@ -34,7 +34,14 @@ contents: - src: ./build/module/ dst: /opt/wheels/module/ type: tree - - src: ./build/framework/ + # Framework source — see nfpm-wheels.yaml for the full explainer of why src + # MUST be ./build/framework/wheels/ (with trailing /wheels/), not bare + # ./build/framework/. Short version: nfpm `type: tree` copies src contents + # into dst, and the wheels-core-VER.zip preserves a top-level wheels/ dir, + # so without the inner /wheels/ we get a double-nested + # /opt/wheels/module/vendor/wheels/wheels/ that breaks runtime resolution + # of `wheels.Injector`. See issue #2773. + - src: ./build/framework/wheels/ dst: /opt/wheels/module/vendor/wheels/ type: tree - src: ./build/sqlite-jdbc.jar diff --git a/tools/distribution-drafts/linux-packages/nfpm-wheels.yaml b/tools/distribution-drafts/linux-packages/nfpm-wheels.yaml index 8239859838..331c4dc859 100644 --- a/tools/distribution-drafts/linux-packages/nfpm-wheels.yaml +++ b/tools/distribution-drafts/linux-packages/nfpm-wheels.yaml @@ -38,7 +38,16 @@ contents: dst: /opt/wheels/module/ type: tree # Framework source — staged into vendor/wheels/ in scaffolded apps. - - src: ./build/framework/ + # IMPORTANT: src points at ./build/framework/wheels/ (the inner directory), + # NOT ./build/framework/. wheels-core-VER.zip has a top-level wheels/ dir + # that `unzip` preserves, leaving the framework at ./build/framework/wheels/. + # nfpm's `type: tree` copies the *contents* of src into dst, so without the + # trailing /wheels/ on src, the framework double-nests as + # /opt/wheels/module/vendor/wheels/wheels/Injector.cfc and Lucee can't + # resolve `wheels.Injector` at app startup. See issue #2773. The brew + # formula handles this the equivalent way — see homebrew-wheels + # Formula/wheels.rb's `(share/"wheels/framework/wheels").install Dir["*"]`. + - src: ./build/framework/wheels/ dst: /opt/wheels/module/vendor/wheels/ type: tree # SQLite JDBC — required at first run by the wrapper script (cliff fix from diff --git a/vendor/wheels/tests/specs/cli/LinuxPackageStagingSpec.cfc b/vendor/wheels/tests/specs/cli/LinuxPackageStagingSpec.cfc index 225c3b1f8b..60d5883c7f 100644 --- a/vendor/wheels/tests/specs/cli/LinuxPackageStagingSpec.cfc +++ b/vendor/wheels/tests/specs/cli/LinuxPackageStagingSpec.cfc @@ -164,6 +164,35 @@ component extends="wheels.WheelsTest" { ); }); + it("stages framework src from ./build/framework/wheels/ so contents flatten under vendor/wheels/", () => { + var src = fileRead(t.path); + // wheels-core-VER.zip has a top-level `wheels/` directory inside it + // (the smoke test asserts this at tools/ci/smoke-test-module.sh:112). + // nfpm `type: tree` copies the *contents* of src into dst, so if src + // points at ./build/framework/ (one level above the inner wheels/), + // the entire wheels/ subdirectory itself lands at dst — producing + // /opt/wheels/module/vendor/wheels/wheels/Injector.cfc instead of + // /opt/wheels/module/vendor/wheels/Injector.cfc. The framework then + // never loads at runtime ("could not find component or class with + // name [wheels.Injector]" — see issue ##2773). + // + // The brew formula handles this by explicitly re-introducing the + // wheels/ wrapper at stage time — see homebrew-wheels Formula/wheels.rb:62 + // — (share/"wheels/framework/wheels").install Dir["*"]. The .deb/.rpm + // equivalent is to point src at the inner wheels/ directory directly. + var pair = reFindNoCase( + "src:[[:space:]]+\./build/framework/wheels/[[:space:]]+dst:[[:space:]]+/opt/wheels/module/vendor/wheels/", + src + ) > 0; + expect(pair).toBeTrue( + t.label & " must declare `src: ./build/framework/wheels/` (with the " + & "trailing /wheels/) for the framework contents entry. Without the " + & "inner /wheels/ segment, nfpm's `type: tree` double-nests the " + & "framework at /opt/wheels/module/vendor/wheels/wheels/, and Lucee " + & "fails to resolve `wheels.Injector` at app startup. See issue ##2773." + ); + }); + }); })(target); } From 5534cbb3f3943d8bc6ddf8c8690ce8867da5d233 Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Wed, 20 May 2026 18:51:02 -0700 Subject: [PATCH 2/2] test(ci): add negative guard for buggy framework src in nfpm yamls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewer A on PR #2776 (wheels-bot) flagged that the new framework-src spec only asserted the *fixed* form was present, without a matching `toBeFalse` for the buggy `./build/framework/` form. The file's existing wrapper-routing checks (lines 60-68 / 81-106) already use a dual- assertion pattern; the new spec was a one-sided outlier. Add the negative guard: if a future copy-paste leaves both the bare `src: ./build/framework/` and the fixed `src: ./build/framework/wheels/` in the same yaml, nfpm would stage both — the bare one reintroduces the double-nesting and breaks every fresh Linux install. The spec now fails loudly in that scenario instead of silently passing on the presence of the fixed entry. The two regexes are mutually exclusive by construction: the positive matches `framework/wheels/` followed by whitespace + `dst:`; the negative matches `framework/` followed *immediately* by whitespace + `dst:`. Since `wheels` isn't whitespace, `[[:space:]]+` can't bridge across it, so the negative regex cannot false-positive on the fixed form. Confirmed via perl POSIX equivalent against both nfpm yamls plus a synthetic buggy fixture. Also adds an inline comment to the positive assertion documenting why `[[:space:]]+` works across the YAML line break (POSIX `[[:space:]]` resolves to Java's `\s` in both Lucee and Adobe CF, which includes `\n`) — addresses Reviewer A's Nit 2 observation that the cross-line match hadn't been locally verified. Signed-off-by: Peter Amiri --- .../specs/cli/LinuxPackageStagingSpec.cfc | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/vendor/wheels/tests/specs/cli/LinuxPackageStagingSpec.cfc b/vendor/wheels/tests/specs/cli/LinuxPackageStagingSpec.cfc index 60d5883c7f..9baa66d562 100644 --- a/vendor/wheels/tests/specs/cli/LinuxPackageStagingSpec.cfc +++ b/vendor/wheels/tests/specs/cli/LinuxPackageStagingSpec.cfc @@ -180,17 +180,40 @@ component extends="wheels.WheelsTest" { // wheels/ wrapper at stage time — see homebrew-wheels Formula/wheels.rb:62 // — (share/"wheels/framework/wheels").install Dir["*"]. The .deb/.rpm // equivalent is to point src at the inner wheels/ directory directly. - var pair = reFindNoCase( + // + // `[[:space:]]+` matches across the YAML line break between the src + // value and `dst:` — POSIX `[[:space:]]` resolves to Java's `\s` in + // both Lucee and Adobe CF, which includes `\n`. + var hasFixedPair = reFindNoCase( "src:[[:space:]]+\./build/framework/wheels/[[:space:]]+dst:[[:space:]]+/opt/wheels/module/vendor/wheels/", src ) > 0; - expect(pair).toBeTrue( + expect(hasFixedPair).toBeTrue( t.label & " must declare `src: ./build/framework/wheels/` (with the " & "trailing /wheels/) for the framework contents entry. Without the " & "inner /wheels/ segment, nfpm's `type: tree` double-nests the " & "framework at /opt/wheels/module/vendor/wheels/wheels/, and Lucee " & "fails to resolve `wheels.Injector` at app startup. See issue ##2773." ); + + // Negative guard: the buggy bare-framework form must not coexist + // with the fixed form. A future copy-paste could leave both entries + // in the file, and nfpm would happily stage both — the bare one + // reintroduces the double-nesting. Pairs with the toBeTrue above + // per the dual-assertion pattern already used by the wrapper-routing + // checks at lines 60-68 / 81-106. + var hasBuggyPair = reFindNoCase( + "src:[[:space:]]+\./build/framework/[[:space:]]+dst:[[:space:]]+/opt/wheels/module/vendor/wheels/", + src + ) > 0; + expect(hasBuggyPair).toBeFalse( + t.label & " must NOT declare `src: ./build/framework/` (without " + & "the trailing /wheels/) for any contents entry targeting " + & "/opt/wheels/module/vendor/wheels/. If both the bare and the " + & "/wheels/-suffixed entries coexist, nfpm stages the inner " + & "wheels/ wrapper as a subdirectory and the framework " + & "double-nests. See issue ##2773." + ); }); });