diff --git a/.github/workflows/build-native.yml b/.github/workflows/build-native.yml new file mode 100644 index 000000000..5da69b0c0 --- /dev/null +++ b/.github/workflows/build-native.yml @@ -0,0 +1,134 @@ +name: Build Native Binaries + +on: + push: + tags: + - "v*" + workflow_dispatch: + inputs: + publish: + description: "Publish platform packages to npm" + required: false + default: "false" + type: choice + options: + - "false" + - "true" + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - os: macos-14 + target: aarch64-apple-darwin + platform: darwin-arm64 + - os: macos-13 + target: x86_64-apple-darwin + platform: darwin-x64 + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + platform: linux-x64-gnu + - os: ubuntu-latest + target: aarch64-unknown-linux-gnu + platform: linux-arm64-gnu + cross: true + - os: windows-latest + target: x86_64-pc-windows-msvc + platform: win32-x64-msvc + + runs-on: ${{ matrix.os }} + name: Build ${{ matrix.platform }} + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Install cross-compilation tools (Linux ARM64) + if: matrix.cross + run: | + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu + + - name: Build native addon + working-directory: native/crates/engine + env: + RUSTFLAGS: "" + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: ${{ matrix.cross && 'aarch64-linux-gnu-gcc' || '' }} + run: cargo build --release --target ${{ matrix.target }} + + - name: Prepare artifact (Unix) + if: runner.os != 'Windows' + run: | + mkdir -p artifacts + cp native/target/${{ matrix.target }}/release/libgsd_engine.dylib artifacts/gsd_engine.node 2>/dev/null || \ + cp native/target/${{ matrix.target }}/release/libgsd_engine.so artifacts/gsd_engine.node 2>/dev/null || \ + echo "No library found" + ls -la artifacts/ + + - name: Prepare artifact (Windows) + if: runner.os == 'Windows' + run: | + mkdir artifacts + copy native\target\${{ matrix.target }}\release\gsd_engine.dll artifacts\gsd_engine.node + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: native-${{ matrix.platform }} + path: artifacts/gsd_engine.node + if-no-files-found: error + + publish: + needs: build + if: startsWith(github.ref, 'refs/tags/v') || github.event.inputs.publish == 'true' + runs-on: ubuntu-latest + name: Publish platform packages + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "22" + registry-url: "https://registry.npmjs.org" + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Copy binaries to platform packages + run: | + for platform in darwin-arm64 darwin-x64 linux-x64-gnu linux-arm64-gnu win32-x64-msvc; do + cp "artifacts/native-${platform}/gsd_engine.node" "native/npm/${platform}/gsd_engine.node" + echo "Copied binary for ${platform}" + ls -la "native/npm/${platform}/" + done + + - name: Sync platform package versions + run: node native/scripts/sync-platform-versions.js + + - name: Publish platform packages + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + for platform in darwin-arm64 darwin-x64 linux-x64-gnu linux-arm64-gnu win32-x64-msvc; do + echo "Publishing @gsd/engine-${platform}..." + cd "native/npm/${platform}" + npm publish --access public || echo "Failed to publish ${platform} (may already exist)" + cd - + done + + - name: Wait for npm registry propagation + run: sleep 30 + + - name: Publish main package + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm publish diff --git a/native/npm/darwin-arm64/package.json b/native/npm/darwin-arm64/package.json new file mode 100644 index 000000000..58dd68565 --- /dev/null +++ b/native/npm/darwin-arm64/package.json @@ -0,0 +1,20 @@ +{ + "name": "@gsd/engine-darwin-arm64", + "version": "2.10.2", + "description": "GSD native engine binary for macOS ARM64", + "os": [ + "darwin" + ], + "cpu": [ + "arm64" + ], + "main": "gsd_engine.node", + "files": [ + "gsd_engine.node" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/gsd-build/gsd-2.git" + } +} diff --git a/native/npm/darwin-x64/package.json b/native/npm/darwin-x64/package.json new file mode 100644 index 000000000..45048a9d1 --- /dev/null +++ b/native/npm/darwin-x64/package.json @@ -0,0 +1,20 @@ +{ + "name": "@gsd/engine-darwin-x64", + "version": "2.10.2", + "description": "GSD native engine binary for macOS Intel", + "os": [ + "darwin" + ], + "cpu": [ + "x64" + ], + "main": "gsd_engine.node", + "files": [ + "gsd_engine.node" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/gsd-build/gsd-2.git" + } +} diff --git a/native/npm/linux-arm64-gnu/package.json b/native/npm/linux-arm64-gnu/package.json new file mode 100644 index 000000000..3ec130297 --- /dev/null +++ b/native/npm/linux-arm64-gnu/package.json @@ -0,0 +1,20 @@ +{ + "name": "@gsd/engine-linux-arm64-gnu", + "version": "2.10.2", + "description": "GSD native engine binary for Linux ARM64 (glibc)", + "os": [ + "linux" + ], + "cpu": [ + "arm64" + ], + "main": "gsd_engine.node", + "files": [ + "gsd_engine.node" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/gsd-build/gsd-2.git" + } +} diff --git a/native/npm/linux-x64-gnu/package.json b/native/npm/linux-x64-gnu/package.json new file mode 100644 index 000000000..f2e59bfd2 --- /dev/null +++ b/native/npm/linux-x64-gnu/package.json @@ -0,0 +1,20 @@ +{ + "name": "@gsd/engine-linux-x64-gnu", + "version": "2.10.2", + "description": "GSD native engine binary for Linux x64 (glibc)", + "os": [ + "linux" + ], + "cpu": [ + "x64" + ], + "main": "gsd_engine.node", + "files": [ + "gsd_engine.node" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/gsd-build/gsd-2.git" + } +} diff --git a/native/npm/win32-x64-msvc/package.json b/native/npm/win32-x64-msvc/package.json new file mode 100644 index 000000000..087333771 --- /dev/null +++ b/native/npm/win32-x64-msvc/package.json @@ -0,0 +1,20 @@ +{ + "name": "@gsd/engine-win32-x64-msvc", + "version": "2.10.2", + "description": "GSD native engine binary for Windows x64 (MSVC)", + "os": [ + "win32" + ], + "cpu": [ + "x64" + ], + "main": "gsd_engine.node", + "files": [ + "gsd_engine.node" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/gsd-build/gsd-2.git" + } +} diff --git a/native/scripts/sync-platform-versions.cjs b/native/scripts/sync-platform-versions.cjs new file mode 100644 index 000000000..59b0d8834 --- /dev/null +++ b/native/scripts/sync-platform-versions.cjs @@ -0,0 +1,65 @@ +#!/usr/bin/env node + +/** + * Synchronize platform package versions with the root package version. + * + * Reads version from root package.json, writes it to all platform + * package.json files and updates optionalDependencies in root package.json. + */ + +const fs = require("fs"); +const path = require("path"); + +const rootDir = path.resolve(__dirname, "..", ".."); +const npmDir = path.resolve(__dirname, "..", "npm"); + +const rootPkgPath = path.join(rootDir, "package.json"); +const rootPkg = JSON.parse(fs.readFileSync(rootPkgPath, "utf-8")); +const version = rootPkg.version; + +console.log(`[sync-platform-versions] Syncing to version ${version}`); + +const platformPackages = [ + "darwin-arm64", + "darwin-x64", + "linux-x64-gnu", + "linux-arm64-gnu", + "win32-x64-msvc", +]; + +// Update each platform package.json +for (const platform of platformPackages) { + const pkgPath = path.join(npmDir, platform, "package.json"); + if (!fs.existsSync(pkgPath)) { + console.warn(` Skipping ${platform}: ${pkgPath} not found`); + continue; + } + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")); + if (pkg.version !== version) { + console.log(` ${platform}: ${pkg.version} -> ${version}`); + pkg.version = version; + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n"); + } else { + console.log(` ${platform}: already ${version}`); + } +} + +// Update optionalDependencies in root package.json +let rootChanged = false; +const optDeps = rootPkg.optionalDependencies || {}; +for (const platform of platformPackages) { + const depName = `@gsd/engine-${platform}`; + if (optDeps[depName] && optDeps[depName] !== version) { + console.log(` root optionalDependencies ${depName}: ${optDeps[depName]} -> ${version}`); + optDeps[depName] = version; + rootChanged = true; + } +} + +if (rootChanged) { + rootPkg.optionalDependencies = optDeps; + fs.writeFileSync(rootPkgPath, JSON.stringify(rootPkg, null, 2) + "\n"); + console.log(" Updated root package.json optionalDependencies"); +} + +console.log("[sync-platform-versions] Done."); diff --git a/package.json b/package.json index 6744c04c0..0df1383c7 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,8 @@ "pi:install-global": "node scripts/install-pi-global.js", "pi:uninstall-global": "node scripts/uninstall-pi-global.js", "sync-pkg-version": "node scripts/sync-pkg-version.cjs", - "prepublishOnly": "npm run sync-pkg-version && npm run build" + "sync-platform-versions": "node native/scripts/sync-platform-versions.cjs", + "prepublishOnly": "npm run sync-pkg-version && npm run sync-platform-versions && npm run build" }, "dependencies": { "@clack/prompts": "^1.1.0", @@ -82,6 +83,11 @@ "typescript": "^5.4.0" }, "optionalDependencies": { + "@gsd/engine-darwin-arm64": "2.10.2", + "@gsd/engine-darwin-x64": "2.10.2", + "@gsd/engine-linux-x64-gnu": "2.10.2", + "@gsd/engine-linux-arm64-gnu": "2.10.2", + "@gsd/engine-win32-x64-msvc": "2.10.2", "fsevents": "~2.3.3" }, "overrides": { diff --git a/packages/native/src/native.ts b/packages/native/src/native.ts index 91ade924c..322dae98b 100644 --- a/packages/native/src/native.ts +++ b/packages/native/src/native.ts @@ -2,7 +2,10 @@ * Native addon loader. * * Locates and loads the compiled Rust N-API addon (`.node` file). - * Tries platform-tagged release builds first, then falls back to dev builds. + * Resolution order: + * 1. @gsd/engine-{platform} npm optional dependency (production install) + * 2. native/addon/gsd_engine.{platform}.node (local release build) + * 3. native/addon/gsd_engine.dev.node (local debug build) */ import { createRequire } from "node:module"; @@ -15,31 +18,49 @@ const require = createRequire(import.meta.url); const addonDir = path.resolve(__dirname, "..", "..", "..", "native", "addon"); const platformTag = `${process.platform}-${process.arch}`; -const candidates = [ - path.join(addonDir, `gsd_engine.${platformTag}.node`), - path.join(addonDir, "gsd_engine.dev.node"), -]; +/** Map Node.js platform/arch to the npm package suffix */ +const platformPackageMap: Record = { + "darwin-arm64": "darwin-arm64", + "darwin-x64": "darwin-x64", + "linux-x64": "linux-x64-gnu", + "linux-arm64": "linux-arm64-gnu", + "win32-x64": "win32-x64-msvc", +}; function loadNative(): Record { const errors: string[] = []; - for (const candidate of candidates) { + // 1. Try the platform-specific npm optional dependency + const packageSuffix = platformPackageMap[platformTag]; + if (packageSuffix) { try { - return require(candidate) as Record; + return require(`@gsd/engine-${packageSuffix}`) as Record; } catch (err) { const message = err instanceof Error ? err.message : String(err); - errors.push(`${candidate}: ${message}`); + errors.push(`@gsd/engine-${packageSuffix}: ${message}`); } } + // 2. Try local release build (native/addon/gsd_engine.{platform}.node) + const releasePath = path.join(addonDir, `gsd_engine.${platformTag}.node`); + try { + return require(releasePath) as Record; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + errors.push(`${releasePath}: ${message}`); + } + + // 3. Try local dev build (native/addon/gsd_engine.dev.node) + const devPath = path.join(addonDir, "gsd_engine.dev.node"); + try { + return require(devPath) as Record; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + errors.push(`${devPath}: ${message}`); + } + const details = errors.map((e) => ` - ${e}`).join("\n"); - const supportedPlatforms = [ - "darwin-arm64", - "darwin-x64", - "linux-x64", - "linux-arm64", - "win32-x64", - ]; + const supportedPlatforms = Object.keys(platformPackageMap); throw new Error( `Failed to load gsd_engine native addon for ${platformTag}.\n\n` + `Tried:\n${details}\n\n` +