-
Notifications
You must be signed in to change notification settings - Fork 970
fix(desktop): bundle DuckDB native binding into x64 macOS build #4694
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<string, string>; | ||
| }; | ||
| 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, | ||
| ); | ||
| } | ||
|
Comment on lines
+473
to
+515
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/desktop/scripts/copy-native-modules.ts
Line: 473-515
Comment:
**`targetVersion` from `optionalDependencies` is passed verbatim to `fetchNpmPackage`**
`copyExactModuleVersion` → `fetchNpmPackage` constructs the tarball URL as `https://registry.npmjs.org/${pkg}/-/${bare}-${version}.tgz`. If `optionalDependencies` ever specifies a semver range instead of an exact version, the URL is malformed and the fetch silently falls through to `process.exit(1)`. The same pattern exists in `copyLibsqlDependencies`, so this is not unique to this PR, but the risk is worth noting given the cross-arch fetch path is the sole fallback for the x64 binding in CI.
How can I resolve this? If you propose a fix, please make it concise. |
||
|
|
||
| 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!"); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -504,13 +504,35 @@ 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})`, | ||
| ); | ||
| } | ||
|
Comment on lines
+507
to
+526
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
All other similar validators ( Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/desktop/scripts/validate-native-runtime.ts
Line: 507-526
Comment:
**`validateDuckdbPrepared` lacks a guard for when `@duckdb/node-bindings` is not installed**
All other similar validators (`validateParcelWatcherPrepared`, the ast-grep section of `validateNativeModulesPrepared`) return early or emit a warning when no candidates apply to the current platform. `validateDuckdbPrepared` unconditionally fails if `duckdb.node` is not present. In practice this is shielded by `validateNativeModulesPrepared` running first (since `@duckdb/node-bindings` is in `requiredMaterializedNodeModules`), but there is no analogous guard here if DuckDB is ever made optional per-platform. The failure message would then say "Missing platform-specific @duckdb/node-bindings package" rather than a clear skip.
How can I resolve this? If you propose a fix, please make it concise. |
||
|
|
||
| function main(): void { | ||
| validateWorkspacePackagesBundled(); | ||
| validateOnlyExpectedExternalRequires(); | ||
| validateLibsqlNotBundled(); | ||
| validateParcelWatcherNotBundled(); | ||
| validateNativeModulesPrepared(); | ||
| validateParcelWatcherPrepared(); | ||
| validateDuckdbPrepared(); | ||
| console.log("[validate:native-runtime] All checks passed"); | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
asarUnpackGlobsis broader than necessary — unpacks the pure-JS@duckdb/node-apipackageThe glob
"**/node_modules/@duckdb/**/*"asar-unpacks the entire@duckdbscope, including@duckdb/node-apiwhich is a pure-JS module that does not need filesystem access outside the asar. Existing patterns in this file are scoped to the packages that actually require native access (e.g.,@ast-grep/napi*,@parcel/watcher*). A more targeted glob like"**/node_modules/@duckdb/node-bindings*/**/*"reduces the unpacked asar size and more precisely documents what actually needs native filesystem access.Prompt To Fix With AI