From SEA to regular NodeJS

This commit is contained in:
Gregory Schier
2024-07-21 22:14:17 -07:00
parent 6a5f61e84b
commit 3cd7c1ef2e
10 changed files with 47 additions and 141 deletions

View File

@@ -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": {

View File

@@ -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;
}

View File

@@ -1,8 +0,0 @@
{
"main": "build/index.js",
"disableExperimentalSEAWarning": true,
"output": "yaak-plugins.blob",
"assets": {
"worker": "build/index.worker.js"
}
}

View File

@@ -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,
},

View File

@@ -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<PluginHandle[]> {
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() })));

View File

@@ -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));
}