fix(cli): scoop wheels.cmd invokes java.exe directly to dodge cmd.exe bat-jar pre-parse#2766
Conversation
… bat-jar pre-parse The Scoop-installed wheels.cmd wrapper failed on every invocation on at least one Windows 11 build (10.0.26200.8457). cmd.exe pre-parses the entire body of a `call <bat>` target looking for labels and control flow before running it; the lucli-<ver>.bat artifact is a bat-jar concatenation (bat preamble + :JAR_BOUNDARY + raw JAR ZIP bytes, ~915 KB), and the pre-parser trips on bytes inside the ZIP tail and aborts with `The filename, directory name, or volume label syntax is incorrect.` before lucli ever executes. Dispatch lucli via `"%JAVA_HOME%\bin\java.exe" -client -jar "%~dp0lucli-<ver>.bat" %*` instead. java reads the JAR via stream and skips the bat preamble in front of the ZIP central directory, sidestepping cmd.exe's parser entirely. The same fix applies to the `:deploy_dispatch` branch added by PR #2691, which used the same `call` pattern with rewritten args. Add a JAVA_HOME resolver near the top of the dispatch path that locates the openjdk21 dependency declared via `depends: java/openjdk21` -- preferring `%SCOOP%\apps\openjdk21\current` (the canonical Scoop layout) and falling back to the sibling-app form `%~dp0..\..\openjdk21\current`. Fail fast with a `scoop install java/openjdk21` hint when neither is found. Also restores the :deploy_rewrite block to build-manifests.py so the Python source-of-truth matches the published JSON (the block was added direct-to-JSON in #2691 and never made it back to the generator); without this, a future `python3 build-manifests.py` would wipe both the deploy rewrite AND this fix. Closes #2765. Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
… for Windows install The Scoop packages declare `depends: java/openjdk21` — Scoop's `depends:` does not auto-add the dependency bucket, so the `java` bucket must be added explicitly before `scoop install wheels`. The `wheels.cmd` wrapper now resolves `JAVA_HOME` from that dependency automatically (#2765/#2766). Corrects the "no separate java bucket, no JAVA_HOME setup" claim that was wrong since the openjdk21 dependency was introduced. Signed-off-by: wheels-bot[bot] <wheels-bot[bot]@users.noreply.github.com> Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Wheels Bot — Docs updatedAdded a doc commit to this PR:
Both guides previously claimed the packages "bundle OpenJDK 21 inline — no separate `java` bucket, no `JAVA_HOME` setup." That has been incorrect since the manifests switched to `depends: java/openjdk21` — Scoop's `depends:` declaration does not auto-add the source bucket, so users hit `Couldn't find manifest for 'openjdk21' from 'java' bucket` on a fresh install. This PR's `JAVA_HOME` resolver inside `wheels.cmd` makes the inline claim doubly wrong; the prose now reflects both the bucket prerequisite and the automatic resolution. |
There was a problem hiding this comment.
Wheels Bot — Reviewer A
TL;DR: PR #2766 correctly fixes a Windows-specific bat-jar pre-parser crash that broke every wheels invocation on at least one Windows 11 build. The three-part fix — swap call for direct java.exe -jar, add a JAVA_HOME resolver, and sync build-manifests.py to prevent future drift — is sound. Test coverage is solid (9 specs, follows the established IIFE-for-loop-capture pattern from LinuxPackageStagingSpec.cfc). No correctness, cross-engine, or security issues. Two minor nits below; verdict is comment (no blocking changes required).
Correctness
JAVA_HOME resolver order is correct. if not defined JAVA_HOME ( if defined SCOOP if exist "..." ...) correctly short-circuits when %SCOOP% is undefined and falls back to the sibling %~dp0..\..\openjdk21\current path — which resolves to %SCOOP%\apps\openjdk21\current from the canonical Scoop layout. Pre-existing JAVA_HOME in the environment is respected. ✓
Both dispatch paths are fixed. The normal %* path and the :deploy_dispatch path with !WHEELS_DEPLOY_ARGS! both use "%JAVA_HOME%\bin\java.exe" -client -jar "%~dp0lucli-0.3.7.bat". Verified in both wheels.json and wheels-be.json. ✓
exit /b %ERRORLEVEL% propagates correctly. The two lines are not inside a parenthesized block, so cmd.exe reads and percent-expands them sequentially — %ERRORLEVEL% is updated by the preceding java.exe line before the exit line is read. ✓
Source drift guard is in place. build-manifests.py --check exits non-zero if a regen would change the committed JSONs. The spec pins both JSONs AND the script against the fix, so the three-way drift is caught from two directions. ✓
Tests
Nit — the hasDirectJava assertion covers both dispatch paths with a single findNoCase, but doesn't verify them independently.
vendor/wheels/tests/specs/cli/ScoopWrapperSpec.cfc:70–86
var hasDirectJava = findNoCase(
"\""%JAVA_HOME%\\bin\\java.exe\"" -client -jar \""%~dp0lucli-",
src
) > 0;findNoCase returns on the first match, so if only the normal dispatch path were fixed and the :deploy_dispatch path still used call, this assertion would pass. The negative guard at line 61–68 (hasBatCall) is stricter and would catch the regression on either path. In practice both paths are fixed, so this is not a real defect — but a future maintainer who partially back-ports might be surprised. Consider splitting into two it() blocks (one for normal dispatch, one for :deploy_dispatch) or adding a second findNoCase on :deploy_dispatch + %JAVA_HOME%. Not a blocker.
IIFE pattern (ScoopWrapperSpec.cfc:51, LinuxPackageStagingSpec.cfc:118) is the established project idiom for loop-variable capture in BDD specs. No concern. ✓
Spec extends wheels.WheelsTest (BDD, not legacy RocketUnit). ✓
PR reports 55 pass, 0 fail, 0 error on Lucee 7 + SQLite after the fix. The "beforehand 2 pass, 7 fail" baseline arithmetic checks out (4 assertions × 2 manifests + 1 source-script = 9 total; bug-1 guard was already passing = 2 pass; remaining 7 fail before fix). ✓
Docs
Nit — CHANGELOG.md entry is unusually long.
CHANGELOG.md (the new [Unreleased] → Fixed bullet): the entry is thorough but at ~700 characters it is significantly longer than any adjacent bullet. A reader scanning the changelog for a quick understanding of what changed gets more narrative than they need. Consider trimming to one or two sentences covering the core fix, with the Closes #2765 pointer for context. Not a convention violation, just a maintainability nudge.
Commits
fix(cli): scoop wheels.cmd invokes java.exe directly to dodge cmd.exe bat-jar pre-parse — type fix, scope cli, subject 69 chars, not ALL-CAPS. Body explains the why (cmd.exe pre-parser trips on bat-jar ZIP tail bytes) and references the related PRs (#2691, #2765). DCO trailer present: Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>. ✓
Security
No new attack surface introduced. The wrapper reads %SCOOP% (an env var set by the Scoop installer, not user-supplied input) and performs if exist checks before assigning. No shell injection risk in the generator or test spec. ✓
There was a problem hiding this comment.
Wheels Bot — Reviewer A
TL;DR: The previous review (for dd093bf31) covered the core fix — replacing call lucli-<ver>.bat with direct java.exe -jar dispatch — and found no blocking issues. One commit has been added since then: ec1e9ac3b (docs(web/guides): add scoop bucket add java step; fix JAVA_HOME prose for Windows install), a pure documentation update. That commit is clean. Verdict: comment (no blocking changes required; the new commit has one minor nit).
Docs
New commit adds the missing scoop bucket add java prerequisite. Four MDX files are updated consistently — both v4-0-0 (current GA) and v4-0-1-snapshot pages for start-here/installing.mdx and command-line-tools/installation.mdx. The bucket-add step is placed correctly between scoop install git and scoop bucket add wheels, which is the right order. The inline comment explains why the step is needed (Scoop's depends: does not auto-add the dependency bucket) — good for users who might wonder why it is not automatic.
Prose correction is accurate.
web/sites/guides/src/content/docs/v4-0-0/command-line-tools/installation.mdx (and the three parallel files):
-Both packages bundle OpenJDK 21 inline — no separate `java` bucket, no `JAVA_HOME` setup.
+Both packages install OpenJDK 21 as a Scoop dependency (`java/openjdk21`) — the `java` bucket
+added above is required. The `wheels.cmd` wrapper resolves `JAVA_HOME` from the installed
+dependency automatically, so no manual setup is needed.The old claim ("inline … no separate java bucket") was inaccurate from the moment depends: java/openjdk21 was introduced; the new text matches the actual wheels.cmd resolver logic. ✓
Nit — double DCO sign-off on ec1e9ac3b.
Signed-off-by: wheels-bot[bot] <wheels-bot[bot]@users.noreply.github.com>
Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
The DCO app checks that the commit author's email appears in a Signed-off-by trailer. The author is claude[bot] and the second trailer covers it — the DCO check will pass. The first trailer (wheels-bot[bot]) is redundant but harmless. Not a blocker; just cosmetically noisy in git log.
Commits
docs(web/guides): add scoop bucket add java step; fix JAVA_HOME prose for Windows install — type docs, scope web/guides, subject 64 chars, not ALL-CAPS. Body explains the why and references the parent issues. DCO trailer for commit author (claude[bot]) is present. ✓
The first commit (fix(cli)) was reviewed in full in the prior round — no new concerns.
Cross-engine
No CFML changes in the new commit. Not applicable.
Security
No new attack surface. Not applicable.
Wheels Bot — Reviewer B (round 1)A's review is technically sound and the "comment" verdict is correct. One minor factual error in the commits section (character count), one missed commit, and both of A's nits check out. No sycophancy, no false positives on the core claims. Aligning on approve. SycophancyNone detected. A justified every positive claim with a specific diff line or code reference, and chose "comment" rather than "approve" despite no blocking issues. False positives
Missed issues
Verdict alignmentA's "comment" verdict is consistent with the findings: two non-blocking nits (test coverage gap, CHANGELOG verbosity), no correctness or security defects. Correct call. ConvergenceAligned on approve. The JAVA_HOME resolver logic, both dispatch-path fixes, |
Summary
The Scoop-installed
wheels.cmdwrapper failed on every invocation on at least one Windows 11 build (10.0.26200.8457).cmd.exepre-parses the entire body of acall <bat>target looking for labels and control flow before running it; thelucli-<ver>.batartifact is a bat-jar concatenation (small bat preamble +:JAR_BOUNDARY+ raw JAR ZIP bytes, ~915 KB), and the pre-parser trips on a byte sequence inside the ZIP tail and aborts withThe filename, directory name, or volume label syntax is incorrect.beforelucliever executes.The fix dispatches
luclivia"%JAVA_HOME%\bin\java.exe" -client -jar "%~dp0lucli-<ver>.bat" %*instead ofcall "%~dp0lucli-<ver>.bat" %*.javareads the JAR via stream and skips the bat preamble in front of the ZIP central directory, sidestepping cmd.exe's parser entirely. The same swap applies to the:deploy_dispatchbranch added by #2691, which used the samecallpattern with rewritten args.A
JAVA_HOMEresolver is also added near the top of the dispatch path. It locates theopenjdk21dependency declared viadepends: java/openjdk21— preferring%SCOOP%\apps\openjdk21\current(canonical Scoop layout) and falling back to the sibling-app form%~dp0..\..\openjdk21\current. If neither is found the wrapper fails fast with ascoop install java/openjdk21install hint.While in the area, the
:deploy_rewriteblock is restored tobuild-manifests.pyso the Python source-of-truth matches the published JSON (the block was added direct-to-JSON in #2691 and never made it back to the generator). Without this re-sync, a futurepython3 build-manifests.pywould wipe both the deploy rewrite AND this fix.Related Issue
Closes #2765
Type of Change
Feature Completeness Checklist
Signed-off-by:trailervendor/wheels/tests/specs/cli/ScoopWrapperSpec.cfc(newScoop wrapper (build-manifests.py output)describe block; 9 specs total covering both channels and the source script)bot-update-docs.ymlbot-update-docs.ymlbot-update-docs.yml[Unreleased]→ Fixedcurl ".../wheels/core/tests?db=sqlite&format=json&directory=wheels.tests.specs.cli"returns55 pass, 0 fail, 0 erroron the running Lucee 7 + SQLite test server (was50 pass, 5 failat baseline with the new spec asserting against the still-broken manifests)Test Plan
Failing-then-passing spec captured against the running Lucee 7 + SQLite test server.
The new
ScoopWrapperSpec.cfcexercises four invariants on each of the two committed manifests (wheels-be.jsonandwheels.json), plus one source-of-truth invariant onbuild-manifests.py:call "%~dp0lucli-<ver>.bat"(bug 2 — cmd.exe bat-jar pre-parse)."%JAVA_HOME%\bin\java.exe" -client -jar "%~dp0lucli-<ver>.bat".JAVA_HOME=%~dp0share\jdk(bug 1 guard — the broken path the published bucket carried).JAVA_HOMEfrom theopenjdk21Scoop dependency inside thepre_installtemplate (not just as a top-leveldepends:).build-manifests.pyreferences%JAVA_HOME%in its wrapper template, so a future regen can't silently wipe the fix from both JSONs.Baseline (before fix): 2 pass, 7 fail in the new spec — the four "must-have direct-java + openjdk21" assertions fail on both channels, plus the source-script
%JAVA_HOME%assertion. After fix: 9 pass, 0 fail; fullwheels.tests.specs.clilayer is55 pass, 0 fail, 0 error.The published
wheels-dev/scoop-wheelsbucket needs republishing for existing installs to pick up the fix on their nextscoop update wheels/scoop update wheels-be.