From a2b4e785bf19a3aeab942968abf0be2fcf0bb587 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Jul 2026 22:35:17 +0000 Subject: [PATCH 1/4] Switch Node.js CI and development builds from node-gyp to Bazel The npm-install source-build fallback (binding.gyp + node-gyp-build) stays in place for tarball installs without a scoped binary package, but the main CI test job, the Makefile target, and source-checkout development now build the addon and prebuild assets with Bazel, the same path the npm release uses. Detailed Changes: - **.github/workflows/nodejs.yml**: - build-and-test now builds prebuilds/ with build-node-prebuild-bazel.sh and runs npm test with PREBUILDS_ONLY=1, instead of relying on the implicit node-gyp build from npm ci and prebuildify. - Move the Windows job to windows-2025-vs2026, the runner already used for native Bazel builds in bazel.yml, and add Bazel/Python/BuildBuddy setup mirroring bazel.yml. - pack-source-install is unchanged; it still covers the shipped node-gyp source-build fallback for tarball installs. - **scripts/install.js**: - Skip the node-gyp-build step in source checkouts and point developers at the Bazel script; tarball installs keep the fallback behavior. - **node/opencc.js**: - Honor PREBUILDS_ONLY (the node-gyp-build convention CI and docs already reference) by skipping the build/Release lookup, so prebuild tests cannot silently pick up a stale local build. - **scripts/build-node-prebuild-bazel.sh**: - Default to the host platform when no target argument is given. - **Makefile, CONTRIBUTING.md, node/PUBLISHING.md**: - Update the make node target and developer instructions to the Bazel build flow. --- .github/workflows/nodejs.yml | 49 ++++++++++++++++++++++------ CONTRIBUTING.md | 11 ++++--- Makefile | 3 +- node/PUBLISHING.md | 5 ++- node/opencc.js | 15 +++++---- scripts/build-node-prebuild-bazel.sh | 16 ++++++--- scripts/install.js | 33 ++++++++++++------- 7 files changed, 93 insertions(+), 39 deletions(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 49f725c7d..aaeff9ee9 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -11,6 +11,8 @@ concurrency: cancel-in-progress: true jobs: + # Build the addon and prebuild assets with Bazel (the same path the npm + # release uses), then run the Node.js tests against the prebuild layout. build-and-test: permissions: contents: read @@ -27,12 +29,19 @@ jobs: node-version: 24 - os: macos-15-intel node-version: 24 - - os: windows-2022 + - os: windows-2025-vs2026 node-version: 24 steps: - uses: actions/checkout@v5 with: fetch-depth: 0 + - name: Best-effort Defender exclusions + if: runner.os == 'Windows' + shell: pwsh + continue-on-error: true + run: | + Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE" ` + -ExclusionProcess "bazel.exe","bazelisk.exe","python.exe","cl.exe","link.exe" - name: Compute version id: version shell: bash @@ -45,17 +54,38 @@ jobs: uses: actions/setup-node@v5 with: node-version: ${{ matrix.node-version }} - - run: npm ci - - run: npm test - - name: Build Node.js prebuild - if: ${{ matrix.node-version == 24 }} - run: npm run prebuild + - uses: bazel-contrib/setup-bazel@0.19.0 + with: + bazelisk-cache: true + repository-cache: true + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + - name: Configure BuildBuddy + if: (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && runner.os != 'Windows' + shell: bash + env: + BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }} + run: | + if [ -n "${BUILDBUDDY_API_KEY}" ]; then + { + echo "build --config=buildbuddy" + echo "build --remote_header=x-buildbuddy-api-key=${BUILDBUDDY_API_KEY}" + echo "build --bes_header=x-buildbuddy-api-key=${BUILDBUDDY_API_KEY}" + } >> .bazelrc.user + echo "BuildBuddy enabled." + else + echo "BUILDBUDDY_API_KEY not set; skipping BuildBuddy config." + fi + - run: npm ci --omit=optional + - name: Build Node.js prebuild with Bazel + shell: bash + run: ./scripts/build-node-prebuild-bazel.sh - name: Verify Node.js prebuild layout - if: ${{ matrix.node-version == 24 }} run: | node -e "const fs=require('fs'); const path=require('path'); const dirs=fs.readdirSync('prebuilds',{withFileTypes:true}).filter((entry)=>entry.isDirectory()).map((entry)=>entry.name); if(!dirs.includes('assets')) throw new Error('missing prebuilds/assets'); const platformDirs=dirs.filter((name)=>name!=='assets'); if(platformDirs.length!==1) throw new Error('expected one platform prebuild directory, found '+platformDirs.join(',')); const addon=path.join('prebuilds', platformDirs[0], 'opencc.node'); if(!fs.existsSync(addon)) throw new Error('missing '+addon); console.log('Verified '+addon+' with shared assets');" - - name: Test Node.js prebuild - if: ${{ matrix.node-version == 24 }} + - name: Test env: PREBUILDS_ONLY: 1 run: npm test @@ -65,7 +95,6 @@ jobs: with: name: opencc-${{ env.VERSION }}-nodejs-${{ matrix.os }}-node-${{ matrix.node-version }} path: | - node/build/** prebuilds/** **/npm-debug.log diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7f9e59c54..d87d7a3ee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -192,17 +192,20 @@ ctest --test-dir build --output-on-failure ### Node.js 測試 -Node.js binding 與 npm CLI 修改請執行: +Node.js binding 與 npm CLI 修改請執行(addon 以 Bazel 構建,源碼倉庫內 +`npm install` 不會再觸發 node-gyp 編譯): ```bash -npm install +npm install --omit=optional +./scripts/build-node-prebuild-bazel.sh npm test ``` -驗證 npm prebuild 佈局時: +`build-node-prebuild-bazel.sh` 會產生 `prebuilds/-/opencc.node` +與共用資源 `prebuilds/assets/`,與 npm 發佈使用的佈局一致。如需確保測試只使用 +prebuild 佈局(忽略本地 `build/Release`): ```bash -npm run prebuild PREBUILDS_ONLY=1 npm test ``` diff --git a/Makefile b/Makefile index c7dac8e7a..b471cb8e0 100644 --- a/Makefile +++ b/Makefile @@ -74,8 +74,7 @@ benchmark: (cd build/perf; ctest --verbose) node: - node-gyp configure - node-gyp build + ./scripts/build-node-prebuild-bazel.sh node-test: node npm test diff --git a/node/PUBLISHING.md b/node/PUBLISHING.md index a5608b072..a67c5cf31 100644 --- a/node/PUBLISHING.md +++ b/node/PUBLISHING.md @@ -50,9 +50,12 @@ Bazel: ```sh npm install --omit=optional --ignore-scripts -./scripts/build-node-prebuild-bazel.sh "$(node -p 'process.platform + "-" + process.arch')" +./scripts/build-node-prebuild-bazel.sh ``` +The script defaults to the host platform; pass an explicit target such as +`win32-x64` for the Bazel + Zig cross-compile path. + This creates: ```text diff --git a/node/opencc.js b/node/opencc.js index 1a797a27b..711de5145 100644 --- a/node/opencc.js +++ b/node/opencc.js @@ -47,12 +47,15 @@ function resolveBindingPath() { // sandbox), as a prebuild under prebuilds/-/, or via an // @opencc/opencc-- scoped package. A direct filesystem lookup // replaces node-gyp-build (OpenCC ships a single N-API build per platform). - const candidates = [ - path.join(packageRoot, 'build', 'Release', 'opencc.node'), - path.join( - packageRoot, 'prebuilds', `${process.platform}-${process.arch}`, 'opencc.node' - ), - ]; + // PREBUILDS_ONLY keeps its node-gyp-build meaning: skip the local + // build/Release directory so tests exercise the shipped prebuild layout. + const candidates = []; + if (!process.env.PREBUILDS_ONLY) { + candidates.push(path.join(packageRoot, 'build', 'Release', 'opencc.node')); + } + candidates.push(path.join( + packageRoot, 'prebuilds', `${process.platform}-${process.arch}`, 'opencc.node' + )); for (const candidate of candidates) { if (fs.existsSync(candidate)) { return candidate; diff --git a/scripts/build-node-prebuild-bazel.sh b/scripts/build-node-prebuild-bazel.sh index ec18421d7..3d796c627 100755 --- a/scripts/build-node-prebuild-bazel.sh +++ b/scripts/build-node-prebuild-bazel.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash # # Build opencc.node with Bazel and copy it to the npm prebuilds directory. -# Usage: ./scripts/build-node-prebuild-bazel.sh +# Usage: ./scripts/build-node-prebuild-bazel.sh [] +# With no argument, builds for the host platform. set -euo pipefail @@ -18,7 +19,7 @@ esac usage() { cat >&2 <<'EOF' -Usage: ./scripts/build-node-prebuild-bazel.sh +Usage: ./scripts/build-node-prebuild-bazel.sh [] Supported targets: darwin-arm64 @@ -26,15 +27,22 @@ Supported targets: linux-arm64 linux-x64 win32-x64 + +With no argument, builds for the host platform. EOF } -if [[ $# -ne 1 ]]; then +if [[ $# -gt 1 ]]; then usage exit 2 fi -target="$1" +if [[ $# -eq 1 ]]; then + target="$1" +else + target="$(node -p "process.platform + '-' + process.arch")" + echo "No target specified; defaulting to host target $target." +fi case "$target" in darwin-arm64|darwin-x64|linux-arm64|linux-x64|win32-x64) ;; diff --git a/scripts/install.js b/scripts/install.js index 9864643c7..0b252ce32 100644 --- a/scripts/install.js +++ b/scripts/install.js @@ -8,18 +8,27 @@ const packageRoot = path.resolve(__dirname, '..'); const scopedPackageName = `@opencc/opencc-${process.platform}-${process.arch}`; const isSourceCheckout = fs.existsSync(path.join(packageRoot, '.git')); -if (!isSourceCheckout) { - try { - const scopedPackage = require(require.resolve(scopedPackageName, { - paths: [packageRoot], - })); - if (scopedPackage && scopedPackage.binaryPath) { - process.exit(0); - } - } catch (error) { - if (!error || error.code !== 'MODULE_NOT_FOUND') { - throw error; - } +// In a source checkout, the native addon is built with Bazel, not node-gyp +// (see CONTRIBUTING.md). Skip the npm-install source build entirely. +if (isSourceCheckout) { + console.log( + 'opencc: source checkout detected; skipping the node-gyp install build.\n' + + 'opencc: run ./scripts/build-node-prebuild-bazel.sh to build the addon ' + + 'with Bazel before `npm test`.' + ); + process.exit(0); +} + +try { + const scopedPackage = require(require.resolve(scopedPackageName, { + paths: [packageRoot], + })); + if (scopedPackage && scopedPackage.binaryPath) { + process.exit(0); + } +} catch (error) { + if (!error || error.code !== 'MODULE_NOT_FOUND') { + throw error; } } From 07094169a5d9d9e4c278eaa31d783f9b9c60de8e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Jul 2026 23:36:53 +0000 Subject: [PATCH 2/4] Honor npm_config_build_from_source in the install script The AppVeyor npm packaging job builds from a source checkout with npm_config_build_from_source=true and expects `npm install` to produce build/Release before `npm test`. Keep the source-checkout skip for regular development, but respect the standard npm --build-from-source escape hatch so that job (and anyone explicitly requesting a node-gyp build) still works. --- CONTRIBUTING.md | 3 ++- scripts/install.js | 14 +++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d87d7a3ee..0a1a86812 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -193,7 +193,8 @@ ctest --test-dir build --output-on-failure ### Node.js 測試 Node.js binding 與 npm CLI 修改請執行(addon 以 Bazel 構建,源碼倉庫內 -`npm install` 不會再觸發 node-gyp 編譯): +`npm install` 不會再觸發 node-gyp 編譯;如需舊的 node-gyp 構建,可用 +`npm install --build-from-source`): ```bash npm install --omit=optional diff --git a/scripts/install.js b/scripts/install.js index 0b252ce32..bb1237158 100644 --- a/scripts/install.js +++ b/scripts/install.js @@ -9,12 +9,20 @@ const scopedPackageName = `@opencc/opencc-${process.platform}-${process.arch}`; const isSourceCheckout = fs.existsSync(path.join(packageRoot, '.git')); // In a source checkout, the native addon is built with Bazel, not node-gyp -// (see CONTRIBUTING.md). Skip the npm-install source build entirely. -if (isSourceCheckout) { +// (see CONTRIBUTING.md), so the npm-install source build is skipped unless +// the standard npm escape hatch is set (`npm install --build-from-source`, +// or the npm_config_build_from_source environment variable, as used by the +// AppVeyor npm packaging job). +const buildFromSource = !['', 'false', '0'].includes( + String(process.env.npm_config_build_from_source || '').toLowerCase() +); +if (isSourceCheckout && !buildFromSource) { console.log( 'opencc: source checkout detected; skipping the node-gyp install build.\n' + 'opencc: run ./scripts/build-node-prebuild-bazel.sh to build the addon ' + - 'with Bazel before `npm test`.' + 'with Bazel before `npm test`,\n' + + 'opencc: or re-run with `npm install --build-from-source` for the ' + + 'node-gyp build.' ); process.exit(0); } From 6822cc2e5a72fab9b8b8df0d5498cbf62ba7a9b6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Jul 2026 23:50:41 +0000 Subject: [PATCH 3/4] Never satisfy a source-checkout install with a scoped binary package The previous commit made the install script consult the @opencc/opencc-- scoped package before the node-gyp build even in source checkouts. On AppVeyor, plain `npm install` pulls the published scoped optionalDependency, so the build-from-source request exited early and `npm test` ran against the released 1.4.1 addon and its bundled dictionaries instead of the checked-out sources, failing the test cases added since that release. Restrict the scoped-package early exit to tarball installs, and let build-from-source force a local compile there too, matching the standard npm flag semantics. --- scripts/install.js | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/scripts/install.js b/scripts/install.js index bb1237158..c7ecf0ed9 100644 --- a/scripts/install.js +++ b/scripts/install.js @@ -27,16 +27,22 @@ if (isSourceCheckout && !buildFromSource) { process.exit(0); } -try { - const scopedPackage = require(require.resolve(scopedPackageName, { - paths: [packageRoot], - })); - if (scopedPackage && scopedPackage.binaryPath) { - process.exit(0); - } -} catch (error) { - if (!error || error.code !== 'MODULE_NOT_FOUND') { - throw error; +// An installed scoped binary package satisfies the install, but never in a +// source checkout (its published addon and dictionaries would shadow the +// checked-out sources) and never when build-from-source explicitly asks for +// a local compile. +if (!isSourceCheckout && !buildFromSource) { + try { + const scopedPackage = require(require.resolve(scopedPackageName, { + paths: [packageRoot], + })); + if (scopedPackage && scopedPackage.binaryPath) { + process.exit(0); + } + } catch (error) { + if (!error || error.code !== 'MODULE_NOT_FOUND') { + throw error; + } } } From e7081c6096abd4b8095526e42875611b3a58600d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 15 Jul 2026 00:20:12 +0000 Subject: [PATCH 4/4] Correct the prebuild-assets error message command `node-gyp prebuild` is not a real command; the npm script is `npm run prebuild`. --- scripts/prepare-node-prebuild-artifacts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/prepare-node-prebuild-artifacts.js b/scripts/prepare-node-prebuild-artifacts.js index cc4376bf6..b39ac23a9 100644 --- a/scripts/prepare-node-prebuild-artifacts.js +++ b/scripts/prepare-node-prebuild-artifacts.js @@ -58,7 +58,7 @@ function prepareArtifacts(root = path.join(__dirname, '..')) { if (jsonCount === 0 || dictionaryCount === 0) { throw new Error( - 'Could not prepare Node.js prebuild assets. Run node-gyp prebuild first, ' + + 'Could not prepare Node.js prebuild assets. Run `npm run prebuild` first, ' + 'or run `bazel build -c opt //data/dictionary:binary_dictionaries` before this script.' ); }