@downdraft/cli 0.3.0 → 0.4.0-alpha.0

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.3.0",
3
+ "version": "0.4.0-alpha.0",
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.3.0",
15
+ "@downdraft/engine": "^0.4.0-alpha.0",
16
16
  "eta": "^3.5.0",
17
17
  "jimp": "^1.6.1",
18
18
  "resedit": "3.1.0"
@@ -6,8 +6,9 @@
6
6
  //
7
7
  // --runtime=bun|node|deno JS host embedded in the package (default:
8
8
  // package.json build.runtime, else bun)
9
- // --format=<csv> dir | deb | appimage | flatpak (default:
10
- // package.json build.linux.target, else dir)
9
+ // --format=<csv> dir | deb | appimage | flatpak | rpm
10
+ // (default: package.json build.linux.target,
11
+ // else dir)
11
12
  // --mode=dev|debug|prod bundle mode (default: prod)
12
13
  // --product-name=<name> display name (default: pkg.productName)
13
14
  // --product-version=<v> version (default: pkg.version)
@@ -49,7 +50,8 @@
49
50
  // linux: { target: ["deb","AppImage","flatpak"], category, maintainer,
50
51
  // synopsis, description, icon },
51
52
  // deb: { section, priority, depends: [] },
52
- // flatpak:{ runtime, runtimeVersion, sdk, finishArgs: [] }
53
+ // flatpak:{ runtime, runtimeVersion, sdk, finishArgs: [] },
54
+ // rpm: { license, release, depends: [] }
53
55
  // }
54
56
  //
55
57
  // Formats:
@@ -58,6 +60,8 @@
58
60
  // assembled with dpkg-deb
59
61
  // appimage — AppDir + AppRun + appimagetool (downloaded+cached on demand)
60
62
  // flatpak — generated manifest + flatpak-builder + build-bundle
63
+ // rpm — same payload layout as deb, assembled with rpmbuild from a
64
+ // generated .spec + tarball Source0
61
65
  // ============================================================================
62
66
 
63
67
  import { spawnSync } from "node:child_process";
@@ -82,7 +86,7 @@ const [entry, outdirArg] = positional;
82
86
 
83
87
  if (!entry || !outdirArg) {
84
88
  console.error(
85
- "usage: bun package-desktop.mjs [--runtime=bun|node|deno] [--format=dir|deb|appimage|flatpak,...] " +
89
+ "usage: bun package-desktop.mjs [--runtime=bun|node|deno] [--format=dir|deb|appimage|flatpak|rpm,...] " +
86
90
  "[--mode=...] [--product-name=] [--product-version=] [--app-id=] [--executable-name=] [--icon=] " +
87
91
  "[--node-bin=] [--deno-bin=] [--mcp] <entry.ts> <outdir>");
88
92
  process.exit(1);
@@ -101,10 +105,10 @@ if (!["bun", "node", "deno"].includes(runtime)) {
101
105
  }
102
106
  const formats = (flagValue("format") ?? (Array.isArray(linuxCfg.target) ? linuxCfg.target.join(",") : linuxCfg.target) ?? "dir")
103
107
  .split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
104
- const KNOWN_FORMATS = new Set(["dir", "deb", "appimage", "flatpak"]);
108
+ const KNOWN_FORMATS = new Set(["dir", "deb", "appimage", "flatpak", "rpm"]);
105
109
  for (const f of formats.values()) {
106
110
  if (!KNOWN_FORMATS.has(f)) {
107
- console.error(`unknown --format "${f}" — expected dir, deb, appimage, or flatpak`);
111
+ console.error(`unknown --format "${f}" — expected dir, deb, appimage, flatpak, or rpm`);
108
112
  process.exit(1);
109
113
  }
110
114
  }
@@ -655,8 +659,13 @@ if (runtime === "bun") {
655
659
  `# ${productName} — ${runtime} runtime launcher (package-desktop.mjs)\n` +
656
660
  `SELF="$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || echo "$0")"\n` +
657
661
  `DIR="$(dirname "$SELF")"\n` +
662
+ `export DD_APP_ID="${appId}"\n` +
663
+ (iconPath ? `export DD_APP_ICON="$DIR/icon.png"\n` : "") +
658
664
  `exec "$DIR/${runtime}" ${rtArgs} "$DIR/bundle/index.js" "$@"\n`);
659
665
  chmodSync(join(appdir, exeName), 0o755);
666
+ // Staged at appdir root so the launcher's DD_APP_ICON resolves for all
667
+ // formats (deb installs appdir to /usr/lib, AppImage cpSync's it wholesale).
668
+ if (iconPath) copyFileSync(iconPath, join(appdir, "icon.png"));
660
669
 
661
670
  // Engine cdylibs (bun's path stages these inside package-native).
662
671
  await stageEngineLibs(nativeDir);
@@ -721,6 +730,7 @@ function desktopFile(iconName = exeName) {
721
730
  `Comment=${synopsis}\n` +
722
731
  `Exec=${exeName}\n` +
723
732
  `Icon=${iconName}\n` +
733
+ `StartupWMClass=${appId}\n` +
724
734
  `Terminal=false\n` +
725
735
  `Categories=${category}\n`);
726
736
  }
@@ -918,6 +928,75 @@ if (formats.includes("flatpak")) {
918
928
  artifacts.push(`flatpak ${out}`);
919
929
  }
920
930
 
931
+ // ── Format: rpm ──
932
+
933
+ if (formats.includes("rpm")) {
934
+ const rpmCfg = build.rpm ?? {};
935
+ if (!runOut("sh", ["-c", "command -v rpmbuild"])) {
936
+ throw new Error("rpmbuild not installed — required for the rpm format (apt install rpm)");
937
+ }
938
+ const stage = mkdtempSync(join(tmpdir(), "dd-rpm-"));
939
+ const topdir = join(stage, "rpmbuild");
940
+ for (const d of ["BUILD", "RPMS", "SOURCES", "SPECS"]) {
941
+ mkdirSync(join(topdir, d), { recursive: true });
942
+ }
943
+
944
+ // rpm versions can't contain "-" — "~" is rpm's "sorts before" marker,
945
+ // so 1.2.3-beta.1 → 1.2.3~beta.1 still orders below the final release.
946
+ const rpmVersion = version.replace(/-/g, "~");
947
+
948
+ // Source0 tarball — %setup unpacks it into BUILD/payload and %install
949
+ // copies the staged appdir + packaging metadata into %{buildroot}.
950
+ const payload = join(stage, "payload");
951
+ mkdirSync(payload, { recursive: true });
952
+ cpSync(appdir, join(payload, "appdir"), { recursive: true, verbatimSymlinks: true });
953
+ writeFileSync(join(payload, `${appId}.desktop`), desktopFile());
954
+ writeFileSync(join(payload, `${appId}.metainfo.xml`), metainfoXml());
955
+ if (iconPath) await iconSet(join(payload, "hicolor"));
956
+ run("tar", ["-czf", join(topdir, "SOURCES", `${exeName}-${rpmVersion}.tar.gz`), "-C", stage, "payload"]);
957
+
958
+ const depends = (rpmCfg.depends ?? ["glibc", "libstdc++", "vulkan-loader"]).join(", ");
959
+ writeFileSync(join(topdir, "SPECS", `${exeName}.spec`),
960
+ `# ${productName} — generated by package-desktop.mjs\n` +
961
+ // A `bun build --compile` binary appends its embedded bundle past the
962
+ // ELF sections — brp-strip/__os_install_post would rewrite the file
963
+ // and silently drop it. AutoReqProv is off: the binary is self-
964
+ // contained, so only explicit Requires apply.
965
+ `%define debug_package %{nil}\n` +
966
+ `%global __os_install_post %{nil}\n` +
967
+ `AutoReqProv: no\n` +
968
+ `Name: ${exeName}\n` +
969
+ `Version: ${rpmVersion}\n` +
970
+ `Release: ${rpmCfg.release ?? "1%{?dist}"}\n` +
971
+ `Summary: ${synopsis}\n` +
972
+ `License: ${rpmCfg.license ?? "Proprietary"}\n` +
973
+ `Requires: ${depends}\n` +
974
+ `Source0: %{name}-%{version}.tar.gz\n` +
975
+ `\n%description\n${description}\n` +
976
+ `\n%prep\n%setup -q -n payload\n` +
977
+ `\n%build\n# prebuilt binary — nothing to compile\n` +
978
+ `\n%install\n` +
979
+ `mkdir -p %{buildroot}/usr/lib/%{name} %{buildroot}/usr/bin %{buildroot}%{_datadir}/applications %{buildroot}%{_datadir}/metainfo` +
980
+ (iconPath ? ` %{buildroot}%{_datadir}/icons\n` : "\n") +
981
+ `cp -a appdir/. %{buildroot}/usr/lib/%{name}/\n` +
982
+ `ln -s ../lib/%{name}/%{name} %{buildroot}/usr/bin/%{name}\n` +
983
+ `install -Dm644 ${appId}.desktop %{buildroot}%{_datadir}/applications/${appId}.desktop\n` +
984
+ `install -Dm644 ${appId}.metainfo.xml %{buildroot}%{_datadir}/metainfo/${appId}.metainfo.xml\n` +
985
+ (iconPath ? `cp -a hicolor %{buildroot}%{_datadir}/icons/\n` : "") +
986
+ `\n%files\n` +
987
+ `/usr/lib/%{name}\n` +
988
+ `/usr/bin/%{name}\n` +
989
+ `%{_datadir}/applications/${appId}.desktop\n` +
990
+ `%{_datadir}/metainfo/${appId}.metainfo.xml\n` +
991
+ (iconPath ? `%{_datadir}/icons/hicolor/*/apps/%{name}.png\n` : ""));
992
+
993
+ run("rpmbuild", ["-bb", "--define", `_topdir ${topdir}`, join(topdir, "SPECS", `${exeName}.spec`)]);
994
+ const built = readdirSync(join(topdir, "RPMS", "x86_64")).find((f) => f.endsWith(".rpm"));
995
+ const out = join(outdir, `${exeName}-${version}-linux-${runtime}.x86_64.rpm`);
996
+ copyFileSync(join(topdir, "RPMS", "x86_64", built), out);
997
+ artifacts.push(`rpm ${out}`);
998
+ }
999
+
921
1000
  // ── koffi prebuild fetch (same mechanism as package-mobile) ──
922
1001
 
923
1002
  async function fetchKoffiPrebuild(pkgName, ver) {
@@ -51,6 +51,14 @@ const modeArg = flagValue("mode") ?? "prod";
51
51
  const doInstall = argv.includes("--install") || argv.includes("--launch");
52
52
  const doLaunch = argv.includes("--launch");
53
53
  const retainMcp = argv.includes("--mcp");
54
+ // Games bundled for the shell run under libnode, where `import.meta.env` is
55
+ // undefined — replace the Vite-ism statically so game code can use it.
56
+ const modeDefines = {
57
+ "import.meta.env.DEV": modeArg === "dev" ? "true" : "false",
58
+ "import.meta.env.PROD": modeArg === "dev" ? "false" : "true",
59
+ "import.meta.env.MODE": JSON.stringify(modeArg),
60
+ ...(retainMcp ? {} : { __DD_MCP_STRIP__: "true" }),
61
+ };
54
62
  const minSdk = flagValue("min-sdk") ?? "30";
55
63
  // --node-flavor=lite selects build/dist/android-lite/<abi>/libnode.so
56
64
  // (NODEJS_MOBILE_FLAVOR=lite from build-android.mjs: no intl/inspector/
@@ -450,7 +458,7 @@ while (workerEntries.size !== prevCount) {
450
458
  splitting: false,
451
459
  sourcemap: "none",
452
460
  minify: false,
453
- define: retainMcp ? {} : { __DD_MCP_STRIP__: "true" },
461
+ define: modeDefines,
454
462
  outdir: scanOutdir,
455
463
  });
456
464
  if (!scan.success) {
@@ -475,7 +483,7 @@ const build = await Bun.build({
475
483
  splitting: false,
476
484
  sourcemap: "none",
477
485
  minify: modeArg === "prod",
478
- define: retainMcp ? {} : { __DD_MCP_STRIP__: "true" },
486
+ define: modeDefines,
479
487
  external: ["koffi", "bun:ffi", "bun:test", "bun"],
480
488
  outdir: bundleDir,
481
489
  });
package/src/usage.ts CHANGED
@@ -46,7 +46,7 @@ export const COMMANDS: CommandEntry[] = [
46
46
  { name: "game", alias: "g", type: "string", description: "Game to release (games/<game>). For multiple games, use --games. If omitted, infers from the current directory, or defaults to the only game in games/." },
47
47
  { name: "games", type: "string", description: "Comma-separated game names (e.g. sandjongg,to-the-ocean)" },
48
48
  { name: "target", alias: "t", type: "string", default: "all", enum: [...RELEASE_TARGETS], description: "Target platform(s): win, linux, mac, android, or all (android can't mix with desktop targets)" },
49
- { name: "format", type: "string", description: "Package format(s), comma-separated: dir, deb, appimage, flatpak (linux only; default: build.linux.target in package.json, else dir)" },
49
+ { name: "format", type: "string", description: "Package format(s), comma-separated: dir, deb, appimage, flatpak, rpm (linux only; default: build.linux.target in package.json, else dir)" },
50
50
  { name: "runtime", type: "string", enum: ["bun", "node", "deno", "all"], description: "JS runtime embedded in the package: bun (compiled binary), node, deno, or all (default: build.runtime in package.json, \"runtime\" in downdraft.config.json, else bun). node/deno are linux-only today" },
51
51
  { name: "stage", type: "string", default: "release", enum: [...RELEASE_STAGES], description: "Stage: build, package, or release (all compile the native binary)" },
52
52
  { name: "mode", type: "string", default: "prod", enum: [...BUILD_MODES], description: "Build mode" },