Skip to content

Commit da9058f

Browse files
bpamiriclaude
andauthored
fix(cli): wheels start auto-stages SQLite driver to close fresh-VM F8 (#2349)
* feat(cli): add Wheels SQLite Lucee extension (.lex) for fresh-VM bundle 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> * fix(cli): close 0.0.0-dev version display gap on installed Wheels apps 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. * fix(cli): wheels start auto-stages SQLite driver to close fresh-VM F8 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/`. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4f1914d commit da9058f

4 files changed

Lines changed: 374 additions & 0 deletions

File tree

‎cli/lucli/Module.cfc‎

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,13 +360,32 @@ component extends="modules.BaseModule" {
360360

361361
out("Starting Wheels server...", "cyan");
362362

363+
// Stage required JDBC drivers into the Lucee Express lib/ext/ before
364+
// LuCLI provisions/boots Lucee. Without this, fresh `wheels new` apps
365+
// with the SQLite-by-default datasource hit a class-load failure on
366+
// the first request because the Lucee Express distribution doesn't
367+
// ship the SQLite driver and not every install path (chocolatey,
368+
// dev checkout, manual) drops the JAR via a wrapper script. See
369+
// GH #2326 (F8). Pre-stage (this call) covers the first-start case
370+
// where the express dir already exists; post-stage (after server
371+
// start, below) covers the case where express was extracted by this
372+
// very LuCLI invocation.
373+
$ensureWheelsBundles();
374+
363375
// Delegate to LuCLI's server start command
364376
var cmdArgs = ["start"];
365377

366378
// Pass through any extra args (--port, --version, etc.)
367379
cmdArgs.append(args, true);
368380

369381
executeCommand("server", cmdArgs, variables.projectRoot);
382+
383+
// Post-stage. If the express dir didn't exist at pre-stage time
384+
// (very first LuCLI run on a fresh VM), the start command above just
385+
// extracted it. Seed the JAR now so the *next* `wheels start` works
386+
// zero-config without the user knowing the difference.
387+
$ensureWheelsBundles();
388+
370389
return "";
371390
}
372391

@@ -3672,6 +3691,100 @@ component extends="modules.BaseModule" {
36723691
printCreated(appName & "/vendor/wheels/");
36733692
}
36743693

3694+
// ─────────────────────────────────────────────────
3695+
// Datasource bundle staging — fresh-VM F8 fix
3696+
// ─────────────────────────────────────────────────
3697+
3698+
/**
3699+
* Stage required JDBC drivers into every Lucee Express install's
3700+
* `lib/ext/` (Tomcat classpath) so Lucee can resolve datasource driver
3701+
* classes at boot time without hitting class-load failures. Lucee 7's
3702+
* stock Express distribution ships drivers for MySQL/MSSQL/PostgreSQL/
3703+
* HSQLDB but not SQLite — yet `wheels new` writes SQLite as the zero-
3704+
* config default datasource. Without this stage, every fresh app fails
3705+
* on first request with `ClassException: org.sqlite.JDBC`.
3706+
*
3707+
* Idempotent: if the JAR already exists in lib/ext/, skip. Best-effort:
3708+
* any I/O error is swallowed because we'd rather defer to LuCLI's normal
3709+
* server start error reporting than block on bundle staging.
3710+
*
3711+
* The bundled JAR at cli/lucli/resources/extensions/sqlite/ is the
3712+
* upstream xerial sqlite-jdbc with a relaxed `Require-Capability` header
3713+
* (Felix on Java 21 fails on upstream's strict `osgi.ee;version=1.8`
3714+
* exact-match). See tools/lucee-extensions/sqlite/build.sh for the
3715+
* patch logic. The JAR works equally well in lib/ext/ (Tomcat classpath)
3716+
* and lucee-server/bundles/ (OSGi); we target lib/ext/ here to mirror
3717+
* the brew formula's drop strategy and avoid the per-server `--force`
3718+
* wipe that bundles/ is subject to.
3719+
*/
3720+
private void function $ensureWheelsBundles() {
3721+
try {
3722+
var stager = new services.BundleStager();
3723+
if (!stager.projectUsesSqliteDatasource(variables.projectRoot)) return;
3724+
3725+
var bundleSrc = variables.moduleRoot & "resources/extensions/sqlite/org.xerial.sqlite-jdbc-3.49.1.0.jar";
3726+
if (!fileExists(bundleSrc)) {
3727+
// Dev checkout without the bundle baked in. Skip silently —
3728+
// release tarballs always include it.
3729+
return;
3730+
}
3731+
3732+
var lucliHome = $resolveLucliHome();
3733+
if (!len(lucliHome)) return;
3734+
3735+
// Stage into every Lucee Express install's lib/ext/ — that's the
3736+
// Tomcat classpath, where Lucee 7 finds JDBC drivers when the
3737+
// datasource config doesn't carry an OSGi `bundleName` hint
3738+
// (current `wheels new`-generated app.cfm omits the hint per
3739+
// GH #2304). Mirrors the brew/chocolatey wrapper's drop strategy
3740+
// so dev-checkout, manual-install, and any other path that skips
3741+
// the package wrapper still gets a working zero-config SQLite.
3742+
stager.stageIntoLibExt(
3743+
bundleSrc = bundleSrc,
3744+
expressRoot = lucliHome & "/express",
3745+
jarFileName = "sqlite-jdbc-3.49.1.0.jar"
3746+
);
3747+
} catch (any e) {
3748+
// Stay out of the way — let LuCLI's server start surface the real
3749+
// error if the bundle was actually needed and we couldn't stage.
3750+
}
3751+
}
3752+
3753+
/**
3754+
* Resolve the LuCLI home root. Order of resolution:
3755+
* 1. $LUCLI_HOME if set (e.g. brew wrapper exports $HOME/.wheels).
3756+
* 2. $HOME/.<lucli.binary.name> — LuCLI auto-roots to ~/.<binary> when
3757+
* invoked under a symlinked binary name. `wheels` resolves to
3758+
* ~/.wheels/, bare `lucli` resolves to ~/.lucli/.
3759+
* 3. $HOME/.lucli — final fallback.
3760+
*/
3761+
private string function $resolveLucliHome() {
3762+
var javaSystem = createObject("java", "java.lang.System");
3763+
// 1. Explicit override.
3764+
try {
3765+
var override = javaSystem.getenv("LUCLI_HOME");
3766+
if (!isNull(override) && len(override)) return override;
3767+
} catch (any e) {}
3768+
// 2. Per-binary-name default. LuCLI's bootstrap exports
3769+
// -Dlucli.binary.name=<name> based on how it was invoked, and
3770+
// home defaults to ~/.<binary-name>/. So `wheels` invocations
3771+
// resolve to ~/.wheels/ and bare `lucli` invocations to ~/.lucli/.
3772+
// Match that resolution exactly so we stage into the same tree
3773+
// LuCLI itself uses (catalina.base / catalina.home).
3774+
try {
3775+
var userHome = javaSystem.getProperty("user.home");
3776+
if (isNull(userHome) || !len(userHome)) return "";
3777+
try {
3778+
var binaryName = javaSystem.getProperty("lucli.binary.name");
3779+
if (!isNull(binaryName) && len(binaryName)) {
3780+
return userHome & "/." & binaryName;
3781+
}
3782+
} catch (any e) {}
3783+
return userHome & "/.lucli";
3784+
} catch (any e) {}
3785+
return "";
3786+
}
3787+
36753788
/**
36763789
* Resolve the path to a vendor/wheels framework source directory, checking
36773790
* (in order): the WHEELS_FRAMEWORK_PATH env var, the resolved project
Binary file not shown.
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/**
2+
* Datasource-bundle staging helpers for `wheels start`.
3+
*
4+
* Lucee 7's stock Express distribution ships JDBC drivers for MySQL, MSSQL,
5+
* PostgreSQL, and HSQLDB but not SQLite — yet `wheels new` writes SQLite as
6+
* the zero-config default datasource. Without a JAR somewhere Lucee can find
7+
* (Tomcat classpath via lib/ext/, or an OSGi bundle), every fresh app fails
8+
* on first request with `ClassException: org.sqlite.JDBC`. See GH #2326 (F8).
9+
*
10+
* Isolated from Module.cfc so tests can exercise the behavior without
11+
* instantiating the full LuCLI module (which depends on a `modules.BaseModule`
12+
* mapping that only exists when LuCLI is hosting the runtime).
13+
*/
14+
component {
15+
16+
public function init() {
17+
return this;
18+
}
19+
20+
/**
21+
* True if the project's `config/app.cfm` declares any kind of SQLite
22+
* datasource. Detection covers both the legacy app.cfm format that
23+
* carried `bundleName: "org.xerial.sqlite-jdbc"` and the current format
24+
* (post-#2304) that omits the bundleName hint and relies on classpath
25+
* resolution of `org.sqlite.JDBC`.
26+
*
27+
* @projectRoot Absolute path to the project root (where config/app.cfm
28+
* lives). Empty / missing path → returns false.
29+
*/
30+
public boolean function projectUsesSqliteDatasource(required string projectRoot) {
31+
if (!len(arguments.projectRoot)) return false;
32+
var appCfm = arguments.projectRoot & "/config/app.cfm";
33+
if (!fileExists(appCfm)) return false;
34+
var content = fileRead(appCfm);
35+
return findNoCase("jdbc:sqlite:", content) > 0
36+
|| findNoCase("org.sqlite.JDBC", content) > 0
37+
|| findNoCase("org.xerial.sqlite-jdbc", content) > 0;
38+
}
39+
40+
/**
41+
* Copy the given JAR into every Lucee Express install's `lib/ext/`
42+
* directory beneath `expressRoot`. Idempotent — skips destinations that
43+
* already have the JAR. Best-effort — silently swallows per-version
44+
* copy failures so a permission issue on one Lucee version doesn't
45+
* block staging for others.
46+
*
47+
* Returns a struct documenting what happened, useful for tests and
48+
* future logging:
49+
* { staged: [list of dest paths written],
50+
* skipped: [list of dest paths that already existed],
51+
* failed: [list of dest paths we couldn't write] }
52+
*
53+
* @bundleSrc Absolute path to the source JAR (e.g. inside the
54+
* wheels-module distribution at
55+
* cli/lucli/resources/extensions/sqlite/<jar>.jar).
56+
* @expressRoot Absolute path to LUCLI_HOME/express/ — staging looks at
57+
* every immediate child directory and treats it as a Lucee
58+
* version.
59+
* @jarFileName Filename to use at the destination (typically the
60+
* upstream Maven artifact name, e.g.
61+
* `sqlite-jdbc-3.49.1.0.jar`, since Tomcat's classpath
62+
* loader doesn't care about OSGi symbolic-name renaming).
63+
*/
64+
public struct function stageIntoLibExt(
65+
required string bundleSrc,
66+
required string expressRoot,
67+
required string jarFileName
68+
) {
69+
var result = { staged: [], skipped: [], failed: [] };
70+
if (!fileExists(arguments.bundleSrc)) return result;
71+
if (!directoryExists(arguments.expressRoot)) return result;
72+
73+
var versions = directoryList(arguments.expressRoot, false, "name");
74+
for (var v in versions) {
75+
var libExt = arguments.expressRoot & "/" & v & "/lib/ext";
76+
if (!directoryExists(libExt)) continue;
77+
var dest = libExt & "/" & arguments.jarFileName;
78+
if (fileExists(dest)) {
79+
arrayAppend(result.skipped, dest);
80+
continue;
81+
}
82+
try {
83+
fileCopy(arguments.bundleSrc, dest);
84+
arrayAppend(result.staged, dest);
85+
} catch (any e) {
86+
arrayAppend(result.failed, dest);
87+
}
88+
}
89+
return result;
90+
}
91+
92+
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/**
2+
* Regression coverage for GH #2326 (F8) — fresh `wheels new` apps fail to
3+
* resolve `org.sqlite.JDBC` because Lucee 7's stock Express distribution
4+
* doesn't ship the SQLite driver. BundleStager is what `wheels start`
5+
* delegates to before booting Lucee; it stages the patched JAR into every
6+
* Lucee Express install's lib/ext/ so the Tomcat classpath can resolve it.
7+
*/
8+
component extends="wheels.wheelstest.system.BaseSpec" {
9+
10+
function beforeAll() {
11+
variables.stager = new cli.lucli.services.BundleStager();
12+
}
13+
14+
private string function makeTempProject(required string appCfmContent) {
15+
var root = getTempDirectory() & "wheels-bundlestager-#createUUID()#";
16+
directoryCreate(root, true);
17+
directoryCreate(root & "/config", true);
18+
fileWrite(root & "/config/app.cfm", arguments.appCfmContent);
19+
return root;
20+
}
21+
22+
private string function makeTempBundleJar() {
23+
var jar = getTempDirectory() & "wheels-bundle-src-#createUUID()#.jar";
24+
fileWrite(jar, "fake jar content");
25+
return jar;
26+
}
27+
28+
private string function makeTempExpressRoot(required array versions) {
29+
var root = getTempDirectory() & "wheels-express-#createUUID()#";
30+
directoryCreate(root, true);
31+
for (var v in arguments.versions) {
32+
directoryCreate(root & "/" & v & "/lib/ext", true);
33+
}
34+
return root;
35+
}
36+
37+
function run() {
38+
describe("BundleStager.projectUsesSqliteDatasource", () => {
39+
40+
it("returns true when app.cfm references the jdbc:sqlite: URL prefix (current format, post-##2304)", () => {
41+
var p = makeTempProject('<cfscript>
42+
this.datasources["app"] = {
43+
class: "org.sqlite.JDBC",
44+
connectionString: "jdbc:sqlite:" & expandPath("../db/app.sqlite")
45+
};
46+
</cfscript>');
47+
try {
48+
expect(variables.stager.projectUsesSqliteDatasource(p)).toBeTrue();
49+
} finally {
50+
directoryDelete(p, true);
51+
}
52+
});
53+
54+
it("returns true when app.cfm references the OSGi bundleName hint (legacy format)", () => {
55+
var p = makeTempProject('<cfscript>
56+
this.datasources["app"] = {
57+
class: "org.sqlite.JDBC",
58+
bundleName: "org.xerial.sqlite-jdbc",
59+
connectionString: "jdbc:sqlite:" & expandPath("../db/app.sqlite")
60+
};
61+
</cfscript>');
62+
try {
63+
expect(variables.stager.projectUsesSqliteDatasource(p)).toBeTrue();
64+
} finally {
65+
directoryDelete(p, true);
66+
}
67+
});
68+
69+
it("returns false for non-SQLite datasources (regression — don't stage for postgres/mysql apps)", () => {
70+
var p = makeTempProject('<cfscript>
71+
this.datasources["app"] = {
72+
class: "org.postgresql.Driver",
73+
connectionString: "jdbc:postgresql://localhost:5432/app"
74+
};
75+
</cfscript>');
76+
try {
77+
expect(variables.stager.projectUsesSqliteDatasource(p)).toBeFalse();
78+
} finally {
79+
directoryDelete(p, true);
80+
}
81+
});
82+
83+
it("returns false when projectRoot is empty", () => {
84+
expect(variables.stager.projectUsesSqliteDatasource("")).toBeFalse();
85+
});
86+
87+
it("returns false when config/app.cfm is missing", () => {
88+
var root = getTempDirectory() & "wheels-bundlestager-empty-#createUUID()#";
89+
directoryCreate(root, true);
90+
try {
91+
expect(variables.stager.projectUsesSqliteDatasource(root)).toBeFalse();
92+
} finally {
93+
directoryDelete(root, true);
94+
}
95+
});
96+
});
97+
98+
describe("BundleStager.stageIntoLibExt", () => {
99+
100+
it("copies the bundle JAR into every Lucee version's lib/ext/", () => {
101+
var jar = makeTempBundleJar();
102+
var express = makeTempExpressRoot(["7.0.0.395", "6.2.2.91"]);
103+
try {
104+
var r = variables.stager.stageIntoLibExt(jar, express, "sqlite-jdbc-3.49.1.0.jar");
105+
expect(arrayLen(r.staged)).toBe(2);
106+
expect(arrayLen(r.skipped)).toBe(0);
107+
expect(arrayLen(r.failed)).toBe(0);
108+
expect(fileExists(express & "/7.0.0.395/lib/ext/sqlite-jdbc-3.49.1.0.jar")).toBeTrue();
109+
expect(fileExists(express & "/6.2.2.91/lib/ext/sqlite-jdbc-3.49.1.0.jar")).toBeTrue();
110+
} finally {
111+
fileDelete(jar);
112+
directoryDelete(express, true);
113+
}
114+
});
115+
116+
it("is idempotent — already-staged JARs report as 'skipped' (no overwrite)", () => {
117+
var jar = makeTempBundleJar();
118+
var express = makeTempExpressRoot(["7.0.0.395"]);
119+
try {
120+
variables.stager.stageIntoLibExt(jar, express, "sqlite-jdbc-3.49.1.0.jar");
121+
var r2 = variables.stager.stageIntoLibExt(jar, express, "sqlite-jdbc-3.49.1.0.jar");
122+
expect(arrayLen(r2.staged)).toBe(0);
123+
expect(arrayLen(r2.skipped)).toBe(1);
124+
expect(arrayLen(r2.failed)).toBe(0);
125+
} finally {
126+
fileDelete(jar);
127+
directoryDelete(express, true);
128+
}
129+
});
130+
131+
it("returns empty result when expressRoot is missing (no Lucee installed yet)", () => {
132+
var jar = makeTempBundleJar();
133+
try {
134+
var r = variables.stager.stageIntoLibExt(jar, "/nonexistent/path", "sqlite-jdbc-3.49.1.0.jar");
135+
expect(arrayLen(r.staged)).toBe(0);
136+
} finally {
137+
fileDelete(jar);
138+
}
139+
});
140+
141+
it("returns empty result when bundleSrc is missing (dev checkout without baked-in JAR)", () => {
142+
var express = makeTempExpressRoot(["7.0.0.395"]);
143+
try {
144+
var r = variables.stager.stageIntoLibExt("/nonexistent.jar", express, "sqlite-jdbc-3.49.1.0.jar");
145+
expect(arrayLen(r.staged)).toBe(0);
146+
expect(arrayLen(r.skipped)).toBe(0);
147+
} finally {
148+
directoryDelete(express, true);
149+
}
150+
});
151+
152+
it("skips Express versions that don't have a lib/ext/ directory", () => {
153+
var jar = makeTempBundleJar();
154+
var express = getTempDirectory() & "wheels-express-partial-#createUUID()#";
155+
directoryCreate(express, true);
156+
directoryCreate(express & "/7.0.0.395/lib/ext", true);
157+
directoryCreate(express & "/broken-version", true); // no lib/ext
158+
try {
159+
var r = variables.stager.stageIntoLibExt(jar, express, "sqlite-jdbc-3.49.1.0.jar");
160+
expect(arrayLen(r.staged)).toBe(1);
161+
expect(fileExists(express & "/7.0.0.395/lib/ext/sqlite-jdbc-3.49.1.0.jar")).toBeTrue();
162+
} finally {
163+
fileDelete(jar);
164+
directoryDelete(express, true);
165+
}
166+
});
167+
});
168+
}
169+
}

0 commit comments

Comments
 (0)