diff --git a/.gitignore b/.gitignore index 657faa349..d0661f683 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,6 @@ config.local.json # 个人/本机配置(local 作用域),不进版本库 profiles/cocos.config.json + +# performance profiling artifacts +perf/ diff --git a/.vscodeignore b/.vscodeignore index 0c9ad8a1f..daeb31f42 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -72,7 +72,7 @@ docs/dev/** # =========================================== # 工作流和脚本 # =========================================== -workflow/!(postinstall|utils|electron-rebuild).js +workflow/!(postinstall|utils|electron-rebuild|node-gyp-rebuild).js # =========================================== # 发布和构建产物 diff --git a/docs/dev/native-modules-injection.md b/docs/dev/native-modules-injection.md new file mode 100644 index 000000000..bf492e87f --- /dev/null +++ b/docs/dev/native-modules-injection.md @@ -0,0 +1,65 @@ +# Native Module .node Injection (gl / sharp ABI Adaptation for Host Electron) + +## Background + +cocos-cli depends on two native modules: + +- `gl` (headless-gl): loads `build/Release/webgl.node` via `bindings('webgl')` +- `sharp`: loads its native binding via `require('../build/Release/sharp-.node')` + +These `.node` binaries are tightly coupled to the ABI of the Node/Electron runtime that +loads them. When a cocos-cli process is started by a host Electron runtime (e.g. pink / +Cocos Creator editor), the `.node` binaries bundled with cocos-cli may not match the +host's Electron version, causing load failures. + +## Mechanism + +Via patch-package patches (`patches/gl+9.0.0-rc.10.patch`, `patches/sharp+0.32.6.patch`), +the gl / sharp native binding loaders read environment variables. When an env var is +set, the `.node` file at the specified path is loaded; otherwise the bundled default +binary is used. **When unset, behavior is identical to before.** + +## Env Var Contract (host side) + +| Env var | Meaning | Example value | +|---|---|---| +| `COCOS_CLI_GL_NODE` | Path to the gl (webgl) native binding `.node` | `/path/to/electron/webgl.node` | +| `COCOS_CLI_SHARP_NODE` | Path to the sharp native binding `.node` | `/path/to/electron/sharp-darwin-arm64v8.node` | + +> The path should be absolute. When unset or empty, cocos-cli falls back to its bundled `.node`. + +### pink CocosMainService usage example + +Pass the env vars when spawning the cocos-cli process: + +```ts +const child = spawn(cocosCliEntry, args, { + env: { + ...process.env, + COCOS_CLI_GL_NODE: path.join(pinkNativeDir, 'webgl.node'), + COCOS_CLI_SHARP_NODE: path.join(pinkNativeDir, 'sharp-darwin-arm64v8.node'), + }, +}); +``` + +## Child Process Propagation + +cocos-cli's internal child processes (effect compilation, builder workers, scene process, +script/engine compilation, etc.) are launched via `spawn`/`fork` without an explicit `env` +override, so they **automatically inherit** the main process env vars. The host only needs +to set the env vars on the main process to cover the whole chain. + +## Patch Maintenance + +- Patch files: `patches/gl+9.0.0-rc.10.patch`, `patches/sharp+0.32.6.patch` +- Apply/rebuild: `npm run rebuild` (runs patch-package + @electron/rebuild) +- Apply manually: `npx patch-package` +- Regenerate patches (after modifying node_modules): + + ```sh + npx patch-package gl sharp --exclude 'build/|node-addon-api' + ``` + + > Note: passing multiple `--exclude` values merges them into a single regex (with a + > literal comma), so use an alternation inside one regex (e.g. `'build/|node-addon-api'`); + > paths are relative to the package root (no leading `/`), so `build/` matches `build/...`. diff --git a/package-lock.json b/package-lock.json index 6e4304554..6c266c5e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -76,7 +76,7 @@ "rollup-plugin-terser": "^7.0.2", "rotating-file-stream": "^3.2.7", "semver": "^7.7.2", - "sharp": "^0.32.6", + "sharp": "0.32.6", "socket.io": "^4.8.1", "socket.io-client": "^4.8.1", "strip-ansi": "^6.0.1", diff --git a/package.json b/package.json index 0be8fa16b..0b30dfc72 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,8 @@ "start:preview": "node ./dist/cli.js preview --scene-editor", "start:mcp-inspector": "npx @modelcontextprotocol/inspector", "cli": "node ./dist/cli.js", - "rebuild": "node workflow/electron-rebuild.js" + "rebuild": "node workflow/electron-rebuild.js", + "rebuild:node-gyp": "node workflow/node-gyp-rebuild.js" }, "author": "COCOS", "files": [ @@ -175,7 +176,7 @@ "rollup-plugin-terser": "^7.0.2", "rotating-file-stream": "^3.2.7", "semver": "^7.7.2", - "sharp": "^0.32.6", + "sharp": "0.32.6", "socket.io": "^4.8.1", "socket.io-client": "^4.8.1", "strip-ansi": "^6.0.1", @@ -193,4 +194,4 @@ "zod-to-ts": "1.1.4", "xml2js": "^0.6.0" } -} \ No newline at end of file +} diff --git a/patches/gl+9.0.0-rc.10.patch b/patches/gl+9.0.0-rc.10.patch index 611f3ce96..76e10cdd4 100644 --- a/patches/gl+9.0.0-rc.10.patch +++ b/patches/gl+9.0.0-rc.10.patch @@ -1,3 +1,17 @@ +diff --git a/node_modules/gl/src/javascript/native-gl.js b/node_modules/gl/src/javascript/native-gl.js +index 996ee1d..8c41e57 100644 +--- a/node_modules/gl/src/javascript/native-gl.js ++++ b/node_modules/gl/src/javascript/native-gl.js +@@ -1,3 +1,8 @@ +-const NativeWebGL = require('bindings')('webgl') ++// Allow overriding the native .node binding via env var (used when the host electron ABI differs from the default node ABI) ++const nativeWebglPath = process.env.COCOS_CLI_GL_NODE; ++if (nativeWebglPath) { ++ console.log(`[cocos-cli] gl: loading native binding from COCOS_CLI_GL_NODE=${nativeWebglPath}`); ++} ++const NativeWebGL = nativeWebglPath ? require(nativeWebglPath) : require('bindings')('webgl') + const { WebGLRenderingContext: NativeWebGLRenderingContext } = NativeWebGL + process.on('exit', NativeWebGL.cleanup) diff --git a/node_modules/gl/src/native/webgl.cc b/node_modules/gl/src/native/webgl.cc index 0e03023..bd406a7 100644 --- a/node_modules/gl/src/native/webgl.cc diff --git a/patches/sharp+0.32.6.patch b/patches/sharp+0.32.6.patch new file mode 100644 index 000000000..decd9a387 --- /dev/null +++ b/patches/sharp+0.32.6.patch @@ -0,0 +1,17 @@ +diff --git a/node_modules/sharp/lib/sharp.js b/node_modules/sharp/lib/sharp.js +index a41e83d..f1f48b1 100644 +--- a/node_modules/sharp/lib/sharp.js ++++ b/node_modules/sharp/lib/sharp.js +@@ -7,6 +7,11 @@ const platformAndArch = require('./platform')(); + /* istanbul ignore next */ + try { +- module.exports = require(`../build/Release/sharp-${platformAndArch}.node`); ++ // Allow overriding the native .node binding via env var (used when the host electron ABI differs from the default node ABI) ++ const nativeBindingPath = process.env.COCOS_CLI_SHARP_NODE || `../build/Release/sharp-${platformAndArch}.node`; ++ if (process.env.COCOS_CLI_SHARP_NODE) { ++ console.log(`[cocos-cli] sharp: loading native binding from COCOS_CLI_SHARP_NODE=${process.env.COCOS_CLI_SHARP_NODE}`); ++ } ++ module.exports = require(nativeBindingPath); + } catch (err) { + // Bail early if bindings aren't available + const help = ['', 'Something went wrong installing the "sharp" module', '', err.message, '', 'Possible solutions:']; diff --git a/workflow/electron-rebuild.js b/workflow/electron-rebuild.js index 660f09698..5464fa404 100644 --- a/workflow/electron-rebuild.js +++ b/workflow/electron-rebuild.js @@ -15,9 +15,9 @@ function run(cmd) { } try { - run('npx --yes patch-package'); + run('npx --yes patch-package --error-on-fail'); run(`npx @electron/rebuild --force --version ${electronVersion}`); } catch (err) { console.error('\n[rebuild] failed'); process.exit(1); -} \ No newline at end of file +} diff --git a/workflow/node-gyp-rebuild.js b/workflow/node-gyp-rebuild.js new file mode 100644 index 000000000..767e3c76d --- /dev/null +++ b/workflow/node-gyp-rebuild.js @@ -0,0 +1,14 @@ +const { execSync } = require('child_process'); + +function run(cmd) { + console.log(`\n> ${cmd}`); + execSync(cmd, { stdio: 'inherit' }); +} + +try { + run('npx --yes patch-package --error-on-fail'); + run('npm rebuild'); +} catch (err) { + console.error('\n[rebuild] failed'); + process.exit(1); +} diff --git a/workflow/release.js b/workflow/release.js index fb88d75f0..1808e9645 100644 --- a/workflow/release.js +++ b/workflow/release.js @@ -474,6 +474,8 @@ function createReleasePipeline(config) { const rebuild = async () => { if (config.type === 'electron') { await runCommand('npm', ['run', 'rebuild'], { cwd: extensionDir() }); + } else { + await runCommand('npm', ['run', 'rebuild:node-gyp'], { cwd: extensionDir() }); } };