diff --git a/package.json b/package.json index 3ee58570..f93547c9 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "build:js": "vite build", "build:vendor-protoc": "node scripts/vendor-protoc.cjs", "build:vendor-plugins": "node scripts/vendor-plugins.cjs", + "build:vendor-node": "node scripts/vendor-node.cjs", "build:plugin-runtime": "npm run --prefix plugin-runtime build", "test": "vitest", "coverage": "vitest run --coverage", diff --git a/plugin-runtime/package.json b/plugin-runtime/package.json index 107b75d6..74056824 100644 --- a/plugin-runtime/package.json +++ b/plugin-runtime/package.json @@ -2,9 +2,9 @@ "name": "@yaak/plugin-runtime", "scripts": { "dev": "nodemon", - "build": "run-p build:* && node scripts/generate-sea.cjs", - "build:main": "esbuild src/index.ts --bundle --platform=node --outfile=build/index.js", - "build:worker": "esbuild src/index.worker.ts --bundle --platform=node --outfile=build/index.worker.js", + "build": "run-p build:*", + "build:main": "esbuild src/index.ts --bundle --platform=node --outfile=build/index.cjs", + "build:worker": "esbuild src/index.worker.ts --bundle --platform=node --outfile=build/index.worker.cjs", "build:proto": "grpc_tools_node_protoc --ts_proto_out=src/gen --ts_proto_opt=outputServices=nice-grpc,outputServices=generic-definitions,useExactTypes=false --proto_path=../proto ../proto/plugins/*.proto" }, "dependencies": { diff --git a/plugin-runtime/scripts/generate-sea.cjs b/plugin-runtime/scripts/generate-sea.cjs deleted file mode 100644 index 8125e801..00000000 --- a/plugin-runtime/scripts/generate-sea.cjs +++ /dev/null @@ -1,102 +0,0 @@ -const path = require('node:path'); -const {execSync} = require('node:child_process'); -const {cpSync, mkdirSync, chmodSync, unlinkSync, rmSync, readdirSync, statSync} = require('node:fs'); -const pluginRuntimeDir = path.join(__dirname, '..'); -const destDir = path.join(__dirname, '..', '..', 'src-tauri', 'vendored', 'plugin-runtime'); -const blobPath = path.join(pluginRuntimeDir, 'yaak-plugins.blob'); - -const DST_BIN_MAP = { - darwin_arm64: 'yaakplugins-aarch64-apple-darwin', - darwin_x64: 'yaakplugins-x86_64-apple-darwin', - linux_x64: 'yaakplugins-x86_64-unknown-linux-gnu', - win32_x64: 'yaakplugins-x86_64-pc-windows-msvc.exe', -}; - -// Build the sea -console.log('Building SEA blob'); -execSync('node --experimental-sea-config sea-config.json', {cwd: pluginRuntimeDir}); - -const tmp = path.join(__dirname, 'tmp', `${Math.random()}`); -mkdirSync(tmp, {recursive: true}); - -let tmpNodePath = process.platform === 'win32' ? path.join(tmp, 'node.exe') : path.join(tmp, 'node'); - -console.log('Copying Node.js binary'); -cpSync(process.execPath, tmpNodePath); - -console.log('Changing Node.js binary permissions'); -chmodSync(tmpNodePath, 0o755); - -console.log('Removing Node.js code signature'); -try { - if (process.platform === 'darwin') execSync(`codesign --remove-signature ${tmpNodePath}`); - else if (process.platform === 'win32') execSync(`"${getSigntoolLocation()}" remove /s ${tmpNodePath}`); - /* Nothing for Linux */ -} catch (err) { - console.log('Failed remove signature', err); - process.exit(1); -} - -try { - console.log('Injecting sea blob into Node.js'); - if (process.platform === 'win32') execSync(`npx postject ${tmpNodePath} NODE_SEA_BLOB ${blobPath} --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2`); - else if (process.platform === 'darwin') execSync(`npx postject ${tmpNodePath} NODE_SEA_BLOB ${blobPath} --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 --macho-segment-name NODE_SEA`); - else if (process.platform === 'linux') execSync(`npx postject ${tmpNodePath} NODE_SEA_BLOB ${blobPath} --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2`); -} catch (err) { - console.log('Failed to inject blob', err.stdout.toString()); - process.exit(1); -} - -unlinkSync(blobPath); - -console.log('Re-signing Node.js'); -try { - if (process.platform === 'darwin') execSync(`codesign --sign - ${tmpNodePath}`); - // NOTE: Don't need to resign, as Tauri will sign the sidecar binaries during release - // else if (process.platform === 'win32') execSync(`"${getSigntoolLocation()}" sign /fd SHA256 ${tmpNodePath}`); - /* Nothing for Linux */ -} catch (err) { - console.log('Failed sign', err); - process.exit(1); -} - -const key = `${process.platform}_${process.env.NODE_ARCH ?? process.arch}`; -const dstPath = path.join(destDir, DST_BIN_MAP[key]); -cpSync(tmpNodePath, dstPath); - -rmSync(tmp, {recursive: true, force: true}); - -console.log(`Copied sea to ${dstPath}`) - - -// https://github.com/skymatic/code-sign-action/blob/a2a8833d4e9202556539b564a2a4af5b6da3e8b2/index.ts -function getSigntoolLocation() { - const windowsKitsFolder = 'C:/Program Files (x86)/Windows Kits/10/bin/'; - const folders = readdirSync(windowsKitsFolder); - let fileName = ''; - let maxVersion = 0; - for (const folder of folders) { - if (!folder.endsWith('.0')) { - continue; - } - const folderVersion = parseInt(folder.replace(/\./g, '')); - if (folderVersion > maxVersion) { - const signtoolFilename = `${windowsKitsFolder}${folder}/x64/signtool.exe`; - try { - const stat = statSync(signtoolFilename); - if (stat.isFile()) { - fileName = signtoolFilename; - maxVersion = folderVersion; - } - } catch { - console.warn('Skipping %s due to error.', signtoolFilename); - } - } - } - if (fileName === '') { - throw new Error('Unable to find signtool.exe in ' + windowsKitsFolder); - } - - console.log(`Signtool location is ${fileName}.`); - return fileName; -} diff --git a/plugin-runtime/sea-config.json b/plugin-runtime/sea-config.json deleted file mode 100644 index 83bb5d19..00000000 --- a/plugin-runtime/sea-config.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "main": "build/index.js", - "disableExperimentalSEAWarning": true, - "output": "yaak-plugins.blob", - "assets": { - "worker": "build/index.worker.js" - } -} diff --git a/plugin-runtime/src/PluginHandle.ts b/plugin-runtime/src/PluginHandle.ts index b80223f5..7addfd8a 100644 --- a/plugin-runtime/src/PluginHandle.ts +++ b/plugin-runtime/src/PluginHandle.ts @@ -1,4 +1,5 @@ import { randomUUID } from 'node:crypto'; +import path from 'node:path'; import { Worker } from 'node:worker_threads'; import { PluginInfo } from './plugins'; @@ -24,10 +25,11 @@ export class PluginHandle { readonly pluginDir: string; readonly #worker: Worker; - constructor({ pluginDir, workerJsPath }: { pluginDir: string; workerJsPath: string }) { + constructor(pluginDir: string) { this.pluginDir = pluginDir; - this.#worker = new Worker(workerJsPath, { + const workerPath = path.join(__dirname, 'index.worker.cjs'); + this.#worker = new Worker(workerPath, { workerData: { pluginDir: this.pluginDir, }, diff --git a/plugin-runtime/src/PluginManager.ts b/plugin-runtime/src/PluginManager.ts index 59bd149b..fc0ff75f 100644 --- a/plugin-runtime/src/PluginManager.ts +++ b/plugin-runtime/src/PluginManager.ts @@ -1,14 +1,9 @@ -import { existsSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import path from 'node:path'; -import { getAsset } from 'node:sea'; import { PluginHandle } from './PluginHandle'; import { loadPlugins, PluginInfo } from './plugins'; export class PluginManager { #handles: PluginHandle[] | null = null; static #instance: PluginManager | null = null; - static #workerPath = path.join(tmpdir(), `index.${Math.random()}.worker.js`); public static instance(): PluginManager { if (PluginManager.#instance == null) { @@ -19,22 +14,10 @@ export class PluginManager { } async plugins(): Promise { - await this.#ensureWorkerForSea(); - this.#handles = this.#handles ?? loadPlugins(PluginManager.#workerPath); + this.#handles = this.#handles ?? loadPlugins(); return this.#handles; } - /** - * Copy worker JS asset to filesystem if we're in single-executable-application (SEA) - * @private - */ - async #ensureWorkerForSea() { - if (existsSync(PluginManager.#workerPath)) return; - - console.log('Writing worker file to', PluginManager.#workerPath); - writeFileSync(PluginManager.#workerPath, getAsset('worker', 'utf8')); - } - async #pluginsWithInfo(): Promise<{ plugin: PluginHandle; info: PluginInfo }[]> { const plugins = await this.plugins(); return Promise.all(plugins.map(async (plugin) => ({ plugin, info: await plugin.getInfo() }))); diff --git a/plugin-runtime/src/plugins.ts b/plugin-runtime/src/plugins.ts index 14e6479e..c844ac72 100644 --- a/plugin-runtime/src/plugins.ts +++ b/plugin-runtime/src/plugins.ts @@ -8,11 +8,11 @@ export interface PluginInfo { capabilities: ('import' | 'export' | 'filter')[]; } -export function loadPlugins(workerJsPath: string): PluginHandle[] { +export function loadPlugins(): PluginHandle[] { const pluginsDir = process.env.PLUGINS_DIR; if (!pluginsDir) throw new Error('PLUGINS_DIR is not set'); console.log('Loading plugins from', pluginsDir); const pluginDirs = fs.readdirSync(pluginsDir).map((p) => path.join(pluginsDir, p)); - return pluginDirs.map((pluginDir) => new PluginHandle({ pluginDir, workerJsPath })); + return pluginDirs.map((pluginDir) => new PluginHandle(pluginDir)); } diff --git a/scripts/vendor-node.cjs b/scripts/vendor-node.cjs new file mode 100644 index 00000000..86a9238a --- /dev/null +++ b/scripts/vendor-node.cjs @@ -0,0 +1,22 @@ +const path = require('node:path'); +const {cpSync} = require('node:fs'); +const destDir = path.join(__dirname, '..', 'src-tauri', 'vendored', 'node'); + +const DST_BIN_MAP = { + darwin_arm64: 'node-aarch64-apple-darwin', + darwin_x64: 'node-x86_64-apple-darwin', + linux_x64: 'node-x86_64-unknown-linux-gnu', + win32_x64: 'node-x86_64-pc-windows-msvc.exe', +}; + +// Build the sea +console.log('Vendoring NodeJS binary'); + +// console.log('Changing Node.js binary permissions'); +// chmodSync(tmpNodePath, 0o755); + +const key = `${process.platform}_${process.env.NODE_ARCH ?? process.arch}`; +const dstPath = path.join(destDir, DST_BIN_MAP[key]); +cpSync(process.execPath, dstPath); + +console.log(`Copied NodeJS to ${dstPath}`) diff --git a/src-tauri/tauri-plugin-plugin-runtime/src/nodejs.rs b/src-tauri/tauri-plugin-plugin-runtime/src/nodejs.rs index ec7741a2..7e36eec7 100644 --- a/src-tauri/tauri-plugin-plugin-runtime/src/nodejs.rs +++ b/src-tauri/tauri-plugin-plugin-runtime/src/nodejs.rs @@ -23,10 +23,16 @@ pub async fn node_start(app: &AppHandle, temp_dir: &PathBuf) -> S let plugins_dir = app .path() .resolve("plugins", BaseDirectory::Resource) - .expect("failed to resolve plugin directory resource"); - let plugins_dir = plugins_dir.to_string_lossy().to_string(); + .expect("failed to resolve plugin directory resource") + .to_string_lossy() + .to_string(); - // Remove UNC prefix for Windows paths + let plugin_runtime_dir = app + .path() + .resolve("plugin-runtime", BaseDirectory::Resource) + .expect("failed to resolve plugin runtime resource"); + + // HACK: Remove UNC prefix for Windows paths let plugins_dir = plugins_dir.replace("\\\\?\\", ""); info!( @@ -37,10 +43,11 @@ pub async fn node_start(app: &AppHandle, temp_dir: &PathBuf) -> S let (mut rx, _child) = app .shell() - .sidecar("yaakplugins") + .sidecar("node") .unwrap() .env("GRPC_PORT_FILE_PATH", port_file_path.clone()) .env("PLUGINS_DIR", plugins_dir) + .args(&[plugin_runtime_dir.join("index.cjs")]) .spawn() .unwrap(); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index a0aadb98..1e2d85de 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -42,7 +42,7 @@ "category": "DeveloperTool", "externalBin": [ "vendored/protoc/protoc", - "vendored/plugin-runtime/yaakplugins" + "vendored/node/node" ], "icon": [ "icons/release/32x32.png", @@ -55,7 +55,8 @@ "resources": { "migrations": "migrations", "vendored/protoc/include": "protoc-include", - "vendored/plugins": "plugins" + "vendored/plugins": "plugins", + "../plugin-runtime/build": "plugin-runtime" }, "shortDescription": "Play with APIs, intuitively", "targets": [