@downdraft/cli 0.4.3 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@downdraft/cli",
3
- "version": "0.4.3",
3
+ "version": "0.4.6",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/knackstedt/downdraft.git",
@@ -12,7 +12,7 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "@aws-sdk/client-s3": "^3.700.0",
15
- "@downdraft/engine": "^0.4.3",
15
+ "@downdraft/engine": "^0.4.6",
16
16
  "eta": "^3.5.0",
17
17
  "jimp": "^1.6.1",
18
18
  "resedit": "3.1.0"
@@ -59,7 +59,7 @@ const modeDefines = {
59
59
  "import.meta.env.MODE": JSON.stringify(modeArg),
60
60
  ...(retainMcp ? {} : { __DD_MCP_STRIP__: "true" }),
61
61
  };
62
- const minSdk = flagValue("min-sdk") ?? "30";
62
+ const minSdkFlag = flagValue("min-sdk") ?? "30";
63
63
  // --node-flavor=lite selects build/dist/android-lite/<abi>/libnode.so
64
64
  // (NODEJS_MOBILE_FLAVOR=lite from build-android.mjs: no intl/inspector/
65
65
  // sqlite/amaro — ~20MB smaller binary, slightly lower PSS).
@@ -73,6 +73,23 @@ const excludeLibs = (flagValue("exclude-libs") ?? "").split(",").map((s) => s.tr
73
73
  // Packaged .so's are stripped by default (debug info never maps — pure APK
74
74
  // size). --no-strip keeps symbols for on-device native debugging.
75
75
  const doStrip = !argv.includes("--no-strip");
76
+ // --webui=wpe stages the heavyweight web-UI backend: libdowndraft_webui_wpe
77
+ // (via the crate loop below) PLUS the vendored wpe-android AAR runtime —
78
+ // its jniLibs, dex'd WKRuntime bootstrap classes, WPE process service
79
+ // declarations, and AssetManager payload (gio modules, injected bundles).
80
+ const webuiBackend = flagValue("webui") ?? "none";
81
+ if (!["none", "wpe", "auto"].includes(webuiBackend)) {
82
+ console.error(`[package-mobile] --webui=${webuiBackend} unsupported on android (wpe|none|auto)`);
83
+ process.exit(1);
84
+ }
85
+ const wpeAndroid = webuiBackend !== "none";
86
+ // Debug/bisect override: replace the Application class the manifest names
87
+ // (e.g. --wpe-app-class=com.downdraft.stub.App to keep WPE packaged but
88
+ // inert until native code loads it). Anything other than the default
89
+ // compiles a generated no-op Application into the dex.
90
+ const wpeAppClass = flagValue("wpe-app-class") ?? "org.wpewebkit.WPEApplication";
91
+ // The wpe-android AAR declares minSdk 31 — clamp up when it's staged.
92
+ const minSdk = wpeAndroid && Number(minSdkFlag) < 31 ? "31" : minSdkFlag;
76
93
  // --env KEY=VALUE (repeatable) → dd-env.txt in the bundle; the shell
77
94
  // setenv()s each before node::Start so process.env sees them.
78
95
  const envPairs = argv.filter((a) => a.startsWith("--env=")).map((a) => a.slice(6));
@@ -709,6 +726,22 @@ for (const abi of abis.values()) {
709
726
  throw new Error(`libnode.so for ${abi} not found — build via packages/node-mobile/scripts/build-android.mjs`);
710
727
  }
711
728
 
729
+ // WPE runtime — the whole AAR jni/<abi>/ tree (libWPEWebKit-2.0, libwpe,
730
+ // libWPEBackend-android, glib/soup/gstreamer, libc++_shared, …) rides in
731
+ // jniLibs so every soname resolves from the app's native lib dir.
732
+ if (wpeAndroid) {
733
+ const wpeJni = join(monorepoRoot,
734
+ "packages/engine/libraries/webui/vendor/wpe-android/jni", abi);
735
+ if (!existsSync(wpeJni)) {
736
+ throw new Error(`wpe-android runtime missing at ${wpeJni} — run ` +
737
+ `packages/engine/libraries/webui/scripts/fetch-wpe-android.mjs`);
738
+ }
739
+ for (const f of readdirSync(wpeJni).filter((f) => f.endsWith(".so"))) {
740
+ copyFileSync(join(wpeJni, f), join(libDir, f));
741
+ }
742
+ console.log(`[package-mobile] staged wpe-android runtime → lib/${abi}/`);
743
+ }
744
+
712
745
  // Ensure the engine crates are built for this target.
713
746
  run("node", [join(monorepoRoot, "scripts/build-native.mjs"), `--target=${triple}`, "--profile=release"], {
714
747
  cwd: monorepoRoot,
@@ -783,6 +816,86 @@ for (const abi of abis.values()) {
783
816
 
784
817
  // ── APK assembly ──
785
818
 
819
+ // WPE Java runtime — dex the AAR's classes.jar (WKRuntime bootstrap,
820
+ // WPEServices, the WPEApplication class the manifest names) and stage its
821
+ // AssetManager payload (gio modules, fontconfig, injected-bundles —
822
+ // WKRuntime.initialize copies them to filesDir at first run).
823
+ let wpeServicesXml = "";
824
+ const wpeResZip = join(stageRoot, "wpe-res.zip");
825
+ if (wpeAndroid) {
826
+ const vendor = join(monorepoRoot, "packages/engine/libraries/webui/vendor/wpe-android");
827
+ if (!existsSync(join(vendor, "classes.jar"))) {
828
+ throw new Error(`wpe-android classes.jar missing at ${vendor} — run ` +
829
+ `packages/engine/libraries/webui/scripts/fetch-wpe-android.mjs`);
830
+ }
831
+ const d8 = join(bt, "d8");
832
+ if (!existsSync(d8)) throw new Error(`missing build-tool d8 in ${bt}`);
833
+ const dexInputs = [join(vendor, "classes.jar")];
834
+ // Java support classes compiled alongside the AAR dex:
835
+ // - WpeAppComponentFactory: libWPEAndroidCommon.so embeds a full static
836
+ // libc++ and exports all 915 of its symbols. WPEApplication's static
837
+ // init System.loadLibrary("WPEAndroidRuntime") pulls it into the
838
+ // classloader namespace's global group BEFORE libc++_shared.so ever
839
+ // loads, so libnode's undefined libc++ symbols (operator new/delete,
840
+ // exception machinery, std::*) bind to the wrong copy and node::Start
841
+ // dies on a C++ exception ~ms in. An AppComponentFactory is the one
842
+ // hook that runs before the Application class initializes — its
843
+ // instantiateApplication preloads libc++_shared so it wins the
844
+ // global-group lookup in every app process.
845
+ // - optional stub Application (--wpe-app-class): keeps WPE packaged
846
+ // but inert for bisecting (DD_LIB_PRELOAD in the shell then drives
847
+ // the load order manually).
848
+ const javaSrcRoot = join(stageRoot, "dd-java-src");
849
+ const javaSrcFiles = [];
850
+ const writeJava = (fqcn, body) => {
851
+ const pkg = fqcn.slice(0, fqcn.lastIndexOf("."));
852
+ const cls = fqcn.slice(fqcn.lastIndexOf(".") + 1);
853
+ const dir = join(javaSrcRoot, ...pkg.split("."));
854
+ mkdirSync(dir, { recursive: true });
855
+ writeFileSync(join(dir, `${cls}.java`), body);
856
+ javaSrcFiles.push(join(dir, `${cls}.java`));
857
+ };
858
+ writeJava("org.downdraft.webui.WpeAppComponentFactory",
859
+ `package org.downdraft.webui;
860
+ public class WpeAppComponentFactory extends android.app.AppComponentFactory {
861
+ @Override
862
+ public android.app.Application instantiateApplication(
863
+ ClassLoader cl, String className)
864
+ throws InstantiationException, IllegalAccessException,
865
+ ClassNotFoundException {
866
+ System.loadLibrary("c++_shared");
867
+ return super.instantiateApplication(cl, className);
868
+ }
869
+ }
870
+ `);
871
+ if (wpeAppClass !== "org.wpewebkit.WPEApplication") {
872
+ writeJava(wpeAppClass,
873
+ `package ${wpeAppClass.slice(0, wpeAppClass.lastIndexOf("."))};\n` +
874
+ `public class ${wpeAppClass.slice(wpeAppClass.lastIndexOf(".") + 1)} ` +
875
+ `extends android.app.Application {}\n`);
876
+ console.log(`[package-mobile] wpe-android: stub application class ${wpeAppClass}`);
877
+ }
878
+ const javaOut = join(stageRoot, "dd-java-classes");
879
+ run("javac", ["-cp", androidJar, "-d", javaOut, ...javaSrcFiles]);
880
+ run("jar", ["cf", join(stageRoot, "dd-java.jar"), "-C", javaOut, "."]);
881
+ dexInputs.push(join(stageRoot, "dd-java.jar"));
882
+ run(d8, ["--release", "--min-api", minSdk, "--lib", androidJar,
883
+ "--output", stageRoot, ...dexInputs]);
884
+ cpSync(join(vendor, "assets"), join(stageRoot, "assets"), { recursive: true });
885
+ // WPEServices WebProcess/NetworkProcess slot declarations, verbatim from
886
+ // the AAR manifest — the service subprocesses are how WPEWebKit isolates
887
+ // its render/network work on Android.
888
+ const aarManifest = readFileSync(join(vendor, "AndroidManifest.xml"), "utf-8");
889
+ wpeServicesXml = aarManifest.match(/<service\b[\s\S]*?\/>/g)?.join("\n") ?? "";
890
+ // AAR res/ — R references in classes.jar resolve through resources.arsc.
891
+ const resDir = join(vendor, "res");
892
+ if (existsSync(resDir) && readdirSync(resDir).length) {
893
+ run(join(bt, "aapt2"), ["compile", "--dir", resDir, "-o", wpeResZip]);
894
+ }
895
+ console.log(`[package-mobile] wpe-android: dex'd classes.jar, staged ` +
896
+ `runtime assets + ${wpeServicesXml.split("<service").length - 1} service slots`);
897
+ }
898
+
786
899
  const manifestTpl = readFileSync(join(monorepoRoot, "packages/android-shell/AndroidManifest.xml"), "utf-8");
787
900
  const manifest = manifestTpl
788
901
  .replaceAll("@APP_ID@", appId)
@@ -790,7 +903,17 @@ const manifest = manifestTpl
790
903
  .replaceAll("@VERSION_CODE@", String(versionCode))
791
904
  .replaceAll("@VERSION_NAME@", versionName.replaceAll("&", "&amp;").replaceAll("<", "&lt;"))
792
905
  .replaceAll("@MIN_SDK@", minSdk)
793
- .replaceAll("@DEBUGGABLE@", modeArg === "dev" ? "true" : "false");
906
+ .replaceAll("@DEBUGGABLE@", modeArg === "dev" ? "true" : "false")
907
+ .replaceAll("@HAS_CODE@", wpeAndroid ? "true" : "false")
908
+ // WPEApplication's static init detects process kind (main/web/network)
909
+ // and loads the right per-process libs — required for service spawning.
910
+ .replaceAll("@APPLICATION_NAME_ATTR@",
911
+ wpeAndroid ? `android:name="${wpeAppClass}"` : "")
912
+ // Loads libc++_shared before WPEApplication's static init can pull in
913
+ // libWPEAndroidCommon's embedded-libc++ exports (see dd-java comment).
914
+ .replaceAll("@APP_COMPONENT_FACTORY_ATTR@",
915
+ wpeAndroid ? 'android:appComponentFactory="org.downdraft.webui.WpeAppComponentFactory"' : "")
916
+ .replaceAll("@EXTRA_APPLICATION_XML@", wpeServicesXml);
794
917
  writeFileSync(join(stageRoot, "AndroidManifest.xml"), manifest);
795
918
 
796
919
  const unsigned = join(stageRoot, "unsigned.apk");
@@ -802,12 +925,15 @@ run(join(bt, "aapt2"), [
802
925
  "-I", androidJar,
803
926
  "--manifest", join(stageRoot, "AndroidManifest.xml"),
804
927
  "-A", join(stageRoot, "assets"),
928
+ ...(existsSync(wpeResZip) ? ["-R", wpeResZip] : []),
805
929
  "--auto-add-overlay",
806
930
  ]);
807
931
 
808
932
  // jniLibs → lib/<abi>/*.so inside the APK (extractNativeLibs=true allows
809
- // compressed storage — plain zip is correct here).
810
- run("zip", ["-qr", unsigned, "lib"], { cwd: stageRoot });
933
+ // compressed storage — plain zip is correct here). classes.dex goes in as
934
+ // a raw root entry when the WPE backend ships its Java bootstrap.
935
+ const zipEntries = ["lib", ...(wpeAndroid ? ["classes.dex"] : [])];
936
+ run("zip", ["-qr", unsigned, ...zipEntries], { cwd: stageRoot });
811
937
  run(join(bt, "zipalign"), ["-f", "-p", "4", unsigned, aligned]);
812
938
 
813
939
  // Sign with the standard Android debug key unless a keystore is provided.
package/src/dev.ts CHANGED
@@ -166,6 +166,14 @@ async function devNative(parsed: any): Promise<void> {
166
166
  process.exit(1);
167
167
  }
168
168
 
169
+ // WebUI backend: --webui flag → DD_WEBUI_BACKEND → config webui.backend.
170
+ // Forwarded into the shell env; the host resolves final precedence
171
+ // (createWebUi opts still outrank it) inside libraries/webui.
172
+ const webuiBackend = (parsed.flags.webui as string | undefined)
173
+ || process.env.DD_WEBUI_BACKEND
174
+ || ((readGameConfig(gameDir).webui as { backend?: string } | undefined)?.backend);
175
+ if (webuiBackend) env.DD_WEBUI_BACKEND = webuiBackend;
176
+
169
177
  env.DD_GAME_DIR = gameDir;
170
178
  env.DD_ENTRY = nativeEntry;
171
179
  env.DD_RUNTIME = runtime;
package/src/release.ts CHANGED
@@ -58,6 +58,7 @@ interface ReleaseArgs {
58
58
  verbose: boolean;
59
59
  runtime: string | null;
60
60
  format: string | null;
61
+ webui: string | null;
61
62
  }
62
63
 
63
64
  // ---------------------------------------------------------------------------
@@ -103,6 +104,7 @@ function parseReleaseArgs(args: string[]): ReleaseArgs {
103
104
  verbose: parsed.flags.verbose as boolean,
104
105
  runtime: (parsed.flags.runtime as string) || null,
105
106
  format: (parsed.flags.format as string) || null,
107
+ webui: (parsed.flags.webui as string) || null,
106
108
  };
107
109
  }
108
110
 
@@ -271,6 +273,7 @@ function packageNative(
271
273
  `--product-name=${info.productName}`,
272
274
  `--product-version=${info.version}`,
273
275
  `--app-id=${info.appId}`,
276
+ ...(opts.webui ? [`--webui=${opts.webui}`] : []),
274
277
  ...(opts.retainMcp ? ["--mcp"] : []),
275
278
  entry,
276
279
  resolve(opts.out),
@@ -294,6 +297,7 @@ function packageNative(
294
297
  `--mode=${opts.mode}`,
295
298
  `--product-name=${info.productName}`,
296
299
  `--product-version=${info.version}`,
300
+ ...(opts.webui ? [`--webui=${opts.webui}`] : []),
297
301
  ...(opts.retainMcp ? ["--mcp"] : []),
298
302
  entry,
299
303
  outfile,
@@ -348,6 +352,7 @@ function packageAndroid(
348
352
  ...(opts.libs ? [`--libs=${opts.libs}`] : []),
349
353
  ...(opts.excludeLibs ? [`--exclude-libs=${opts.excludeLibs}`] : []),
350
354
  ...(opts.noStrip ? ["--no-strip"] : []),
355
+ ...(opts.webui ? [`--webui=${opts.webui}`] : []),
351
356
  ...(opts.retainMcp ? ["--mcp"] : []),
352
357
  entry,
353
358
  outfile,
package/src/usage.ts CHANGED
@@ -60,6 +60,7 @@ export const COMMANDS: CommandEntry[] = [
60
60
  { name: "skip-build", type: "boolean", description: "Alias for --stage=package" },
61
61
  { name: "build-only", type: "boolean", description: "Alias for --stage=build" },
62
62
  { name: "mcp", type: "boolean", description: "Retain the MCP automation endpoint in the packaged binary" },
63
+ { name: "webui", type: "string", enum: ["cef", "wpe", "none", "auto"], description: "Heavyweight web UI backend to stage: cef (Chromium — desktop default), wpe (embedded WebKit — linux/android), none, or auto (default: webui.backend in downdraft.config.json, else auto)" },
63
64
  { name: "verbose", alias: "v", type: "boolean", description: "Verbose logging" },
64
65
  ],
65
66
  },
@@ -97,6 +98,7 @@ export const COMMANDS: CommandEntry[] = [
97
98
  { name: "engine-dist", type: "boolean", description: "Resolve engine packages from their pre-built dist/ tree (default outside the monorepo; externalized = near-zero transform cost, no engine-file HMR)" },
98
99
  { name: "native", type: "boolean", description: "Back-compat alias — native is the default runtime" },
99
100
  { name: "native-debug", type: "boolean", description: "Load debug native libraries (built via build-native.mjs --debug) — sets DD_NATIVE_PROFILE=debug for the spawned runtime" },
101
+ { name: "webui", type: "string", enum: ["cef", "wpe", "none", "auto"], description: "Heavyweight web UI backend: cef (Chromium — desktop default), wpe (embedded WebKit — linux dev/android), none, or auto (default: DD_WEBUI_BACKEND, webui.backend in downdraft.config.json, else auto)" },
100
102
  { name: "verbose", alias: "v", type: "boolean", description: "Verbose logging" },
101
103
  ],
102
104
  },