diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 7dc3ded17e6..0bb7691abcf 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -142,6 +142,19 @@ jobs: } "$CLI_BIN" --version + - name: Verify native bindings packaged + working-directory: apps/desktop + run: | + APP_DIR=$(find release -maxdepth 2 -name "*.app" -type d | head -1) + UNPACKED="$APP_DIR/Contents/Resources/app.asar.unpacked/node_modules" + BINDING="$UNPACKED/@duckdb/node-bindings-darwin-${{ matrix.arch }}/duckdb.node" + test -f "$BINDING" || { + echo "::error::DuckDB native binding missing from packaged app — Intel/arm launch will crash. Expected: $BINDING" + ls -la "$UNPACKED/@duckdb" 2>/dev/null || echo " (no @duckdb directory in app.asar.unpacked)" + exit 1 + } + echo "OK: bundled $BINDING ($(du -h "$BINDING" | cut -f1))" + - name: Upload DMG artifact uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: diff --git a/apps/desktop/runtime-dependencies.ts b/apps/desktop/runtime-dependencies.ts index fa36b93e61b..2e2710c7d5d 100644 --- a/apps/desktop/runtime-dependencies.ts +++ b/apps/desktop/runtime-dependencies.ts @@ -79,6 +79,19 @@ const externalizedRuntimeModules: ExternalizedRuntimeModule[] = [ ], asarUnpackGlobs: ["**/node_modules/@libsql/**/*"], }, + { + specifier: "@mastra/duckdb", + materialize: [ + "@mastra/duckdb", + "@duckdb/node-api", + "@duckdb/node-bindings", + ], + packagedCopies: [ + copyWholeModule("@mastra/duckdb"), + copyWholeModule("@duckdb"), + ], + asarUnpackGlobs: ["**/node_modules/@duckdb/**/*"], + }, ]; const packagedSupportModules = [ diff --git a/apps/desktop/scripts/copy-native-modules.ts b/apps/desktop/scripts/copy-native-modules.ts index 3b34f3ecc75..3f859771711 100644 --- a/apps/desktop/scripts/copy-native-modules.ts +++ b/apps/desktop/scripts/copy-native-modules.ts @@ -470,6 +470,50 @@ function copyParcelWatcherPlatformPackages(nodeModulesDir: string): void { } } +function copyDuckdbPlatformPackages(nodeModulesDir: string): void { + const nodeBindingsPath = join(nodeModulesDir, "@duckdb", "node-bindings"); + const nodeBindingsPkgJsonPath = join(nodeBindingsPath, "package.json"); + if (!existsSync(nodeBindingsPkgJsonPath)) return; + + type DuckdbBindingsPackageJson = { + optionalDependencies?: Record; + }; + const nodeBindingsPkg = JSON.parse( + readFileSync(nodeBindingsPkgJsonPath, "utf8"), + ) as DuckdbBindingsPackageJson; + const optionalDeps = nodeBindingsPkg.optionalDependencies ?? {}; + + console.log("\nPreparing duckdb platform package..."); + + // The native binding is a `cpu`/`os`-gated optional dependency, so Bun only + // installs the host's. For the target arch, fetch it from npm when missing. + const targetSuffix = `${TARGET_PLATFORM}-${TARGET_ARCH}`; + const targetEntry = Object.entries(optionalDeps).find(([name]) => + name.endsWith(targetSuffix), + ); + if (!targetEntry) { + console.error( + ` [ERROR] No @duckdb/node-bindings optional dependency matched ${targetSuffix}`, + ); + process.exit(1); + } + + const [targetName, targetVersion] = targetEntry; + const destPath = join(nodeModulesDir, targetName); + if (existsSync(destPath)) { + copyModuleIfSymlink(nodeModulesDir, targetName, true); + return; + } + + copyExactModuleVersion( + nodeModulesDir, + targetName, + targetVersion, + destPath, + true, + ); +} + function prepareNativeModules() { console.log("Preparing external runtime modules for electron-builder..."); console.log( @@ -488,6 +532,7 @@ function prepareNativeModules() { copyAstGrepPlatformPackages(nodeModulesDir); copyParcelWatcherPlatformPackages(nodeModulesDir); copyLibsqlDependencies(nodeModulesDir); + copyDuckdbPlatformPackages(nodeModulesDir); console.log("\nDone!"); } diff --git a/apps/desktop/scripts/validate-native-runtime.ts b/apps/desktop/scripts/validate-native-runtime.ts index 5115b848f7f..044e086b074 100644 --- a/apps/desktop/scripts/validate-native-runtime.ts +++ b/apps/desktop/scripts/validate-native-runtime.ts @@ -504,6 +504,27 @@ function validateParcelWatcherPrepared(): void { ); } +function validateDuckdbPrepared(): void { + const nodeModulesDir = join(projectRoot, "node_modules"); + const targetArch = process.env.TARGET_ARCH || process.arch; + const targetPlatform = process.env.TARGET_PLATFORM || process.platform; + const bindingPackage = `@duckdb/node-bindings-${targetPlatform}-${targetArch}`; + + if (!existsSync(join(nodeModulesDir, bindingPackage, "duckdb.node"))) { + fail( + [ + "Missing platform-specific @duckdb/node-bindings package.", + `Expected: ${bindingPackage}/duckdb.node`, + "Run `bun run copy:native-modules` and ensure optional dependencies are materialized.", + ].join("\n"), + ); + } + + console.log( + `[validate:native-runtime] OK: platform duckdb binding present (${bindingPackage})`, + ); +} + function main(): void { validateWorkspacePackagesBundled(); validateOnlyExpectedExternalRequires(); @@ -511,6 +532,7 @@ function main(): void { validateParcelWatcherNotBundled(); validateNativeModulesPrepared(); validateParcelWatcherPrepared(); + validateDuckdbPrepared(); console.log("[validate:native-runtime] All checks passed"); }