Skip to content

fix(cli): scoop wheels.cmd invokes java.exe directly to dodge cmd.exe bat-jar pre-parse#2766

Merged
bpamiri merged 2 commits into
developfrom
fix/bot-2765-scoop-install-published-wheels-cmd-hits-two-window
May 20, 2026
Merged

fix(cli): scoop wheels.cmd invokes java.exe directly to dodge cmd.exe bat-jar pre-parse#2766
bpamiri merged 2 commits into
developfrom
fix/bot-2765-scoop-install-published-wheels-cmd-hits-two-window

Conversation

@wheels-bot
Copy link
Copy Markdown
Contributor

@wheels-bot wheels-bot Bot commented May 20, 2026

Summary

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 (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 with The filename, directory name, or volume label syntax is incorrect. before lucli ever executes.

The fix dispatches lucli via "%JAVA_HOME%\bin\java.exe" -client -jar "%~dp0lucli-<ver>.bat" %* instead of call "%~dp0lucli-<ver>.bat" %*. 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 swap applies to the :deploy_dispatch branch added by #2691, which used the same call pattern with rewritten args.

A JAVA_HOME resolver is also added near the top of the dispatch path. It locates the openjdk21 dependency declared via depends: 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 a scoop install java/openjdk21 install hint.

While in the area, the :deploy_rewrite block is restored 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 re-sync, a future python3 build-manifests.py would wipe both the deploy rewrite AND this fix.

Related Issue

Closes #2765

Type of Change

  • Bug fix
  • New feature
  • Enhancement to existing feature
  • Documentation update
  • Refactoring

Feature Completeness Checklist

  • DCO sign-off -- commit carries Signed-off-by: trailer
  • Tests -- failing → passing spec at vendor/wheels/tests/specs/cli/ScoopWrapperSpec.cfc (new Scoop wrapper (build-manifests.py output) describe block; 9 specs total covering both channels and the source script)
  • Framework Docs -- handled separately by bot-update-docs.yml
  • AI Reference Docs -- handled separately by bot-update-docs.yml
  • CLAUDE.md -- handled separately by bot-update-docs.yml
  • CHANGELOG.md -- entry added under [Unreleased] → Fixed
  • Test runner passes -- curl ".../wheels/core/tests?db=sqlite&format=json&directory=wheels.tests.specs.cli" returns 55 pass, 0 fail, 0 error on the running Lucee 7 + SQLite test server (was 50 pass, 5 fail at 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.cfc exercises four invariants on each of the two committed manifests (wheels-be.json and wheels.json), plus one source-of-truth invariant on build-manifests.py:

  • The wrapper does not dispatch lucli via call "%~dp0lucli-<ver>.bat" (bug 2 — cmd.exe bat-jar pre-parse).
  • The wrapper does invoke "%JAVA_HOME%\bin\java.exe" -client -jar "%~dp0lucli-<ver>.bat".
  • The wrapper does not set JAVA_HOME=%~dp0share\jdk (bug 1 guard — the broken path the published bucket carried).
  • The wrapper resolves JAVA_HOME from the openjdk21 Scoop dependency inside the pre_install template (not just as a top-level depends:).
  • build-manifests.py references %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; full wheels.tests.specs.cli layer is 55 pass, 0 fail, 0 error.

The published wheels-dev/scoop-wheels bucket needs republishing for existing installs to pick up the fix on their next scoop update wheels / scoop update wheels-be.

… 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
Copy link
Copy Markdown
Contributor Author

wheels-bot Bot commented May 20, 2026

Wheels Bot — Docs updated

Added a doc commit to this PR:

  • web/sites/guides/src/content/docs/v4-0-0/start-here/installing.mdx — added scoop bucket add java to the Windows install step; replaced "no separate `java` bucket" prose with accurate description of the `java/openjdk21` dependency and automatic `JAVA_HOME` resolution
  • web/sites/guides/src/content/docs/v4-0-1-snapshot/start-here/installing.mdx — same
  • web/sites/guides/src/content/docs/v4-0-0/command-line-tools/installation.mdx — same, plus added inline comments in the Powershell code block explaining why the `java` bucket is needed
  • web/sites/guides/src/content/docs/v4-0-1-snapshot/command-line-tools/installation.mdx — same

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.

Copy link
Copy Markdown
Contributor Author

@wheels-bot wheels-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. ✓

Copy link
Copy Markdown
Contributor Author

@wheels-bot wheels-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Contributor Author

wheels-bot Bot commented May 20, 2026

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.

Sycophancy

None 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

  • Commit subject "69 chars" is wrong. A writes: "subject 69 chars" for fix(cli): scoop wheels.cmd invokes java.exe directly to dodge cmd.exe bat-jar pre-parse. The description portion is 77 chars; the full header is 87 chars. Neither is 69. The conclusion (valid, under the 100-char limit from commitlint.config.js) is correct, so this is a non-material measurement error rather than a verdict-altering claim. Worth noting so future commit sections don't copy the wrong count.

Missed issues

  • Docs commit not reviewed. The PR head (ec1e9ac3b) is the docs(web/guides): commit added by the docs-update workflow, not the fix(cli): commit A analyzed. A's commits section covers only dd093bf31. The docs commit header (docs(web/guides): add scoop bucket add java step; fix JAVA_HOME prose for Windows install, 89 chars) is within limits, and its DCO trailer should be present since the bot-update-docs workflow authors it with wheels-bot[bot] credentials. Content-wise the docs changes are a straightforward correct description of the manifest behavior. Not a blocking miss, but A should have acknowledged both commits.

Verdict alignment

A's "comment" verdict is consistent with the findings: two non-blocking nits (test coverage gap, CHANGELOG verbosity), no correctness or security defects. Correct call.

Convergence

Aligned on approve. The JAVA_HOME resolver logic, both dispatch-path fixes, exit /b %ERRORLEVEL% sequencing, and source drift guard are all correct as A verified. The hasBatCall negative guard catches the deploy-dispatch regression that the hasDirectJava positive assertion would miss — A's nit is accurate but the spec is still sound. No missed cross-engine concerns apply (cmd.exe wrapper code, not CFML). The docs commit gap is minor and doesn't affect the fix. Joint recommendation: the PR is ready to merge once a human flips the draft flag.

@bpamiri bpamiri marked this pull request as ready for review May 20, 2026 14:29
@bpamiri bpamiri merged commit 9620085 into develop May 20, 2026
15 checks passed
@bpamiri bpamiri deleted the fix/bot-2765-scoop-install-published-wheels-cmd-hits-two-window branch May 20, 2026 14:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Scoop install: published wheels.cmd hits two Windows-only bugs (JAVA_HOME path + cmd.exe parses lucli bat-jar tail)

1 participant