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..0a1a86812 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -192,17 +192,21 @@ 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 編譯;如需舊的 node-gyp 構建,可用 +`npm install --build-from-source`): ```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..c7ecf0ed9 100644 --- a/scripts/install.js +++ b/scripts/install.js @@ -8,7 +8,30 @@ const packageRoot = path.resolve(__dirname, '..'); const scopedPackageName = `@opencc/opencc-${process.platform}-${process.arch}`; const isSourceCheckout = fs.existsSync(path.join(packageRoot, '.git')); -if (!isSourceCheckout) { +// In a source checkout, the native addon is built with Bazel, not node-gyp +// (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`,\n' + + 'opencc: or re-run with `npm install --build-from-source` for the ' + + 'node-gyp build.' + ); + process.exit(0); +} + +// 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], 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.' ); }