Repository navigation
fix(cli): wheels start auto-stages SQLite driver to close fresh-VM F8 - #2349
Merged
Merged
Conversation
…le gap Build, install, and ship a Lucee CFML extension that packages the xerial sqlite-jdbc OSGi bundle so Lucee 7 can resolve `org.sqlite.JDBC` for `wheels new` SQLite-by-default datasources without manual JAR drops. This addresses fresh-VM onboarding finding F8 — the SQLite class-load cliff that blocks every fresh `wheels migrate latest` because Lucee 7 ships drivers for MySQL/MSSQL/PostgreSQL/HSQLDB but not SQLite. What's in tools/lucee-extensions/sqlite/: - src/build.properties + src/SQLite.cfc + src/sqlite-jdbc-3.49.1.0.jar (vendored from Maven Central) — extension source. - build.sh produces dist/org.xerial.sqlite-jdbc-3.49.1.0.lex (~14 MB). Mirrors the layout/manifest of lucee/extension-jdbc-postgresql and lucee/extension-jdbc-duckdb but uses bash+zip instead of Ant. - install.sh drops the patched bundle straight into a Lucee server's lucee-server/bundles/ directory — deterministic on Lucee 7 and the recommended path until the deploy/ auto-install quirk is solved. - README documents three install paths (bundle/.lex/admin UI), the upstream upgrade plan, and the OSGi manifest patch. The non-obvious fix: xerial's JAR is a valid OSGi bundle, but it declares `Require-Capability: osgi.ee;version=1.8` (exact match in some resolvers). Felix on Java 21 cannot satisfy that, so the bundle is silently rejected. build.sh patches the JAR's MANIFEST.MF to `(version>=1.8)` (matching PostgreSQL's filter) and drops the `;singleton:=true` directive on Bundle-SymbolicName. Original JAR in src/ is untouched — the patch is applied during build. Verified end-to-end on macOS arm64 + Lucee 7.0.0.395: BEFORE install.sh: dbtest -> FAIL "org.xerial.sqlite-jdbc not available locally" AFTER install.sh: dbtest -> OK datasource=sqliteapp result=1 AFTER install.sh: wheels migrate latest creates tables in db/development.sqlite Follow-ups (separate PRs): 1. Wire `wheels new` (or first `wheels start`) to auto-run install.sh against the freshly-created server so SQLite-by-default works zero-config on fresh VMs. 2. Debug Lucee 7's deploy/ -> failed-to-deploy/ silent quarantine so the canonical deploy/ install path works without falling back to the bundle drop. 3. File upstream PR as lucee/extension-jdbc-sqlite once stable so the Lucee community gets it via the standard update server, and a separate issue with xerial/sqlite-jdbc to relax their OSGi Require-Capability filter. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two compounding bugs caused the dev toolbar / /wheels/info to report "Wheels Version: 0.0.0-dev" on installed (brew/chocolatey) apps even though every release artifact already carries the precise SNAPSHOT build version. Both are fixed here. ## Bug 1 — wheels-module tarball never substitutes module.json release.yml's "Build Wheels Module Tarball" step tars cli/lucli/ directly with no version substitution, while wheels-core/wheels-base (which run through prepare-*.sh) get @build.version@ properly replaced. As a result every wheels-module-*.tar.gz on the GitHub releases page ships with module.json's hardcoded "version": "4.0.0" — even though the tag itself is "v4.0.0-SNAPSHOT+1625". That hardcoded value is then read by BaseModule.version() at runtime and fed into FrameworkInstaller.rewriteVersionPlaceholder as the cliVersion fallback (added in #2343 / #2333), so on installs whose vendor/wheels/box.json went through that fallback path, the framework "version" is the imprecise "4.0.0" instead of the build's "4.0.0-SNAPSHOT+1625". Fix: change cli/lucli/module.json to use "version": "@build.version@" (matching every other manifest in the repo), and add the same find + sed substitution release.yml's other build steps already do, in the "Build Wheels Module Tarball" step before tar/zip. Uses an env: block to satisfy our workflow injection lint and avoids inlining the version into shell commands. ## Bug 2 — runtime fallback only recognizes the monorepo's box.json When vendor/wheels/box.json's @build.version@ leaks through scaffold- time substitution (e.g. user's CLI is pre-#2343, or any other gap in the install pipeline), Global.cfc:$readFrameworkVersion's fallback walks two levels up looking for the wheels-dev/wheels monorepo's box.json — and only synthesizes "<rootversion>-dev" when slug=="wheels" or name=="Wheels.fw". An installed app's app-root box.json comes from wheels-base-template (slug=wheels-base-template), which carries the precise framework SNAPSHOT version stamped at release time — but the fallback rejects it because the monorepo signal is absent. Fall-through returns the bare "0.0.0-dev" sentinel. Fix: extend the fallback so when the monorepo signal is missing AND the parent's box.json self-identifies as wheels-base-template (slug or name match), use its version verbatim (no -dev suffix — that version is the actual installed framework version, not an upcoming dev build). The monorepo signal still wins when both could match; both signals are ignored if their version is itself the unreplaced placeholder. Three new specs in frameworkVersionSpec.cfc cover the new path, including the precedence ordering and the placeholder guard. Existing specs (including the regression for #2291) still pass. ## Verification - Framework suite: 3333 passed (no regressions). - CLI suite: 452 pass, 3 fail — pre-existing DoctorSpec issues (#2260), unrelated. - Local dry-run of the release.yml sed substitution: cli/lucli/module.json picks up MODULE_VERSION, no other false positives in cli/lucli/. Closes #2326.
Closes #2326. After this PR, `wheels new` + `wheels start` works with the zero-config SQLite default datasource on any install path — brew, choco, dev checkout, manual — without the user needing to drop a JAR by hand. ## Background Lucee 7's stock Express distribution ships JDBC drivers for MySQL, MSSQL, PostgreSQL, and HSQLDB but not SQLite. `wheels new`, however, writes SQLite as the zero-config default (`class: "org.sqlite.JDBC"`, `connectionString: "jdbc:sqlite:..."`). Without a SQLite JAR somewhere Lucee can find it (Tomcat classpath via lib/ext/ or an OSGi bundle in lucee-server/bundles/), every fresh app fails on the first request: ClassException: org.sqlite.JDBC The brew formula already drops the upstream xerial JAR into `~/.wheels/express/<lucee>/lib/ext/` from a wrapper script, but that only covers brew installs. Chocolatey, dev checkouts, and manual installs all hit F8 on the first VM cycle. ## What this changes - `cli/lucli/services/BundleStager.cfc` (new) — extracted from Module.cfc for unit-testability. Two methods: - `projectUsesSqliteDatasource(projectRoot)` — true if config/app.cfm references `jdbc:sqlite:`, `org.sqlite.JDBC`, or `org.xerial.sqlite-jdbc`. Detection covers both the legacy app.cfm format (which carried the bundleName hint) and the current format post-#2304 which omits the hint and relies on classpath resolution. - `stageIntoLibExt(bundleSrc, expressRoot, jarFileName)` — copies the JAR into every Lucee version's `lib/ext/`. Idempotent, best-effort, returns `{staged, skipped, failed}` for testability. - `cli/lucli/Module.cfc` — `start()` calls `$ensureWheelsBundles()` both before and after delegating to LuCLI's `server start`. Pre-stage covers the case where the express dir already exists; post-stage covers the very first invocation that just extracted express. Either way, `wheels start` and `wheels start --force` end up with the JAR on the classpath before Lucee resolves any SQLite datasource. `$resolveLucliHome()` follows LuCLI's own resolution order: 1. `$LUCLI_HOME` env var (e.g. brew wrapper exports `$HOME/.wheels`), 2. `$HOME/.<lucli.binary.name>` — picks `~/.wheels` for `wheels` invocations and `~/.lucli` for bare `lucli`, 3. `$HOME/.lucli` fallback. - `cli/lucli/resources/extensions/sqlite/org.xerial.sqlite-jdbc-3.49.1.0.jar` (~13 MB) — patched JAR baked into the wheels-module distribution. Built from `tools/lucee-extensions/sqlite/build.sh` (#2347), which patches upstream's `Require-Capability: osgi.ee;version=1.8` (exact match — fails on Felix + Java 21) to `(version>=1.8)` matching the PostgreSQL bundle's filter. - `cli/lucli/tests/specs/services/BundleStagerSpec.cfc` (new, 10 specs) — detection edge cases, multi-version Express layouts, idempotency, and defensive behavior when the bundle source or express dir is missing. ## Bypass cases (separate follow-ups) - `wheels server start --force` is a LuCLI built-in passthrough (not routed through Module.cfc). Users hit by this can use `wheels start --force` instead. F5 in the journal already tracks updating the error message that points users at the bypass form. - Lucee caches a per-process negative bundle resolution. If a user hits a SQLite endpoint before the JAR is staged, restart the server. The pre-stage in `start()` makes this only matter on a single end-of-train user; the post-stage seeds for the next start. ## Verification - Framework suite: 3333 passed (no regressions). - CLI suite: 462 passed, 3 fail — pre-existing DoctorSpec issues (#2260), unrelated. Of the 462, 10 are new BundleStagerSpec coverage. - End-to-end: on a clean macOS arm64 + JDK 21 env with no SQLite JAR anywhere, `wheels new finalapp` + `wheels start` + `curl /dbtest.cfm` returns `OK datasource=finalapp result=1` zero-config. Pre-stage logs show the JAR copied into `~/.wheels/express/7.0.0.395/lib/ext/`.
bpamiri
added a commit
that referenced
this pull request
Apr 29, 2026
* fix(cli): cfml-escape commit subject baked into BuildInfo.cfc prepare-core.sh sed-substitutes the latest commit subject into BuildInfo.cfc inside a double-quoted CFML string. # is CFML's variable-interpolation delimiter, so a commit subject ending in a parenthesized PR ref like "(#2352)" produces a syntactically broken .cfc that crashes Lucee on the first request — bricking every fresh brew/chocolatey install. Onboarding finding F1. Escape both # → ## and " → "" before the substitution, then escape sed replacement specials (\, &) so the value transports through the existing sed pipeline cleanly. Add an inline post-substitution check that fails the build if any unescaped # or " remains in the produced commitSubject literal — a regression-stop without standing up a new test harness. Verified end-to-end: with COMMIT_SUBJECT="feat(config): introduce BuildInfo.cfc as the runtime version source (#2352)", the produced BuildInfo.cfc reads `commitSubject: "...source (##2352)",` (valid CFML); reverting the escape pipeline triggers the self-check with exit 1. * fix(cli): scaffold writes plural resource name to routes.cfm generateScaffold() takes a singular model name (e.g. "Post") and correctly derives a pluralized form for the controller, views, and migration — but updateRoutes() was being passed the raw singular, producing .resources("post") in routes.cfm. Onboarding finding F4. Wheels' resource convention is plural (PostsController maps to .resources("posts")), so the singular variant either creates a phantom second route that conflicts with any hand-added plural route, or silently fails to bind to the controller scaffolded a few lines earlier. The scaffold runs through cleanly otherwise — this is the one piece that drifted from the convention. Pass pluralName, which is already computed for the model/controller/ view generation right above. Add a regression spec that asserts routes.cfm contains .resources("articles") (plural) and not .resources("article") (singular) after scaffolding "Article". * fix(cli): also stage sqlite driver into per-server lucee-server/bundles/ The previous fix (#2349) staged the SQLite JDBC driver into every Lucee Express install's lib/ext/ — Tomcat's parent classpath. That satisfies Class.forName("org.sqlite.JDBC") but Lucee 7's datasource resolver consults its OSGi bundle loader for driver instantiation, not the Tomcat classloader. Without the bundle in <server>/lucee-server/bundles/, the very first cfquery against a SQLite datasource fails — and the Wheels migrator goes through cfquery, so `wheels migrate latest` blows up on every fresh app. Onboarding finding F2. Other engines (MySQL, Postgres) avoid the issue because their bundles ship inside lucee-7.x.x.jar and Lucee auto-deploys them; the SQLite extension is third-party and doesn't get the same treatment, so seeding bundles/ is on us. Add BundleStager.stageIntoServerBundles() alongside the existing stageIntoLibExt() — same idempotent / best-effort contract, walks LUCLI_HOME/servers/<name>/lucee-server/bundles/. Wire it into $ensureWheelsBundles() so every wheels start populates both targets. The OSGi-symbolic-name filename (org.xerial.sqlite-jdbc-3.49.1.0.jar) is required for Lucee's bundle loader to recognize it. 5 new specs cover detection, idempotency, missing serversRoot, missing bundleSrc, and partial-server-context tolerance. CLI suite 462 → 467 pass on this worktree. * fix(cli): wheels reload purges per-server cfclasses cache LuCLI's dev-server Lucee config defaults to `inspectTemplate=once`, which means Lucee compiles each CFC once, caches the .class on disk under `<server>/lucee-server/context/cfclasses/`, and never rechecks the source timestamp. Wheels' `?reload=true` resets framework state via applicationStop() but does not invalidate Lucee's template cache, so source edits to models, controllers, and config silently keep returning the old compiled class. Even `wheels stop && wheels start` doesn't help — the .class on disk persists. The only path that picks up changes is physically wiping cfclasses. Onboarding finding F5; the single-biggest time sink across the tutorial run. `wheels reload` now physically wipes the cfclasses dir for the project's registered server before triggering the HTTP reload. Order matters: purge first, then reload — the application reset on the next request gets the fresh compile. Extract the deletion into CfclassesPurger.cfc (matching the BundleStager extraction pattern) so the logic is unit-testable without touching a live server. 5 new specs cover happy path, dir preservation, missing path, empty path, and idempotency. Best-effort: per-file failures (Lucee-locked .class mid-compile, Windows file lock) are swallowed rather than aborting the reload — the user is better served by a partial-purge than a full crash. * fix(cli): refuse wheels start/stop in non-wheels-project directories LuCLI's `server start` and `server stop` derive the server name from the cwd basename when no registered server matches. Running `wheels start` or `wheels stop` from a parent directory or unrelated dir (easy to do by accident, especially in shell sessions where cwd resets between commands) silently registers a phantom server context named after that dir. The user ends up with ghost entries like `ws` or `Downloads` in `wheels server list`, with `<unknown>` webroots and no path to clean up. Onboarding finding F6. Add a `$isWheelsProjectDir(path)` helper that checks for `config/settings.cfm` — the file `wheels new` always writes and that no other tool creates (box.json and Application.cfc are not specific enough). - `wheels start` refuses up-front when not in a Wheels project, pointing the user at `wheels new`. - `wheels stop` already had a guard for the case where the cwd has no registered server but other servers are running (reports them); extend it to also refuse fallthrough when no servers are running at all, so we never reach LuCLI's name-from-cwd code path. * fix(cli): wheels browser test defaults to app dir and app test runner Two bugs in `wheels browser test` made it useless from the app for running browser specs. Onboarding finding F11. - Default directory was `wheels.tests.specs.wheelstest`, the framework's internal browser-DSL test directory. Apps live at `tests/specs/browser/`, so the runner reported 0 tests against a fresh app spec. - Test URL hit `/wheels/core/tests`, the framework's core test runner. That endpoint only knows about specs under `vendor/wheels/tests/specs/`. App-side specs are mounted by `/wheels/app/tests`, the app test runner. Default directory is now `tests.specs.browser` and the runner hits `/wheels/app/tests`. `--directory=...` still overrides for advanced use. Two related sub-issues from the same finding (running browser specs corrupts the dev server's cfclasses cache for other specs; WHEELS_ENV=test wheels migrate latest looks for an unconfigured `wheelstestdb_sqlite` datasource) are out of scope for this fix and tracked separately.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #2326. After this PR,
wheels new+wheels startworks with the zero-config SQLite default datasource on any install path (brew, chocolatey, dev checkout, manual) without the user needing to drop a JAR by hand.Background
Lucee 7's stock Express distribution ships JDBC drivers for MySQL, MSSQL, PostgreSQL, and HSQLDB but not SQLite.
wheels newwrites SQLite as the zero-config default. Without a JAR somewhere Lucee can find — Tomcat classpath vialib/ext/, or an OSGi bundle inlucee-server/bundles/— every fresh app fails on the first request withClassException: org.sqlite.JDBC. That's finding F8 in the fresh-VM onboarding journals.The brew formula already drops the upstream xerial JAR into
~/.wheels/express/<lucee>/lib/ext/via its wrapper script, but that only covers brew installs. Chocolatey, dev checkouts, and manual installs all hit F8 on the first VM cycle.What this changes
cli/lucli/services/BundleStager.cfc(new)projectUsesSqliteDatasource(projectRoot)(handles current and legacy app.cfm formats) andstageIntoLibExt(bundleSrc, expressRoot, jarFileName)(idempotent multi-version copy).cli/lucli/Module.cfcstart()calls$ensureWheelsBundles()both before and after delegating to LuCLI'sserver start. Pre-stage covers the case where express already exists; post-stage covers the very first invocation that just extracted express. Either way, the JAR is on the classpath before Lucee resolves any SQLite datasource.cli/lucli/resources/extensions/sqlite/org.xerial.sqlite-jdbc-3.49.1.0.jar(~13 MB)tools/lucee-extensions/sqlite/build.sh(#2347), which patches upstream's strictRequire-Capability: osgi.ee;version=1.8(exact match — fails on Felix + Java 21) to the(version>=1.8)filter PostgreSQL uses.cli/lucli/tests/specs/services/BundleStagerSpec.cfc(new, 10 specs)$resolveLucliHome()follows LuCLI's own resolution order:$LUCLI_HOMEenv var →$HOME/.<lucli.binary.name>(e.g.~/.wheelsforwheels,~/.luclifor barelucli) →$HOME/.luclifallback.Bypass cases (separate follow-ups)
wheels server start --forceis a LuCLI built-in passthrough (not routed through Module.cfc). Users who hit this should usewheels start --forceinstead. F5 in the journal already tracks updating the error message that points users at the bypass form.start()only matters for a hypothetical end-user who races the staging; the post-stage always seeds the next start.Verification
~/.wheels/express/7.0.0.395/lib/ext/.wheels start --forcefollows the same path and works.wheels starton an already-staged express dir is a no-op (skipped, notstaged).🤖 Generated with Claude Code