@lakutata/builder 3.1.1 → 3.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/README.zh-CN.md +5 -1
- package/dist/esm/index.js +3 -0
- package/dist/esm/lib/Archive.js +3 -2
- package/dist/esm/lib/Bundle.js +12 -5
- package/dist/esm/lib/Directories.js +18 -0
- package/dist/esm/lib/Executable.js +27 -6
- package/dist/esm/lib/Files.js +44 -5
- package/dist/esm/lib/Installer.js +26 -12
- package/dist/esm/lib/Native.js +58 -4
- package/dist/esm/lib/NativeRuntime.js +2 -0
- package/dist/esm/lib/linux/Linux.js +57 -8
- package/dist/esm/lib/mac/Mac.js +39 -11
- package/dist/esm/lib/nwjs/Addons.js +307 -0
- package/dist/esm/lib/nwjs/Package.js +149 -0
- package/dist/esm/lib/nwjs/Runtime.js +214 -0
- package/dist/esm/lib/nwjs/Stage.js +92 -0
- package/dist/esm/lib/win/Windows.js +63 -41
- package/dist/types/index.d.ts +4 -0
- package/dist/types/lib/Bundle.d.ts +5 -0
- package/dist/types/lib/Directories.d.ts +8 -0
- package/dist/types/lib/Executable.d.ts +7 -0
- package/dist/types/lib/Files.d.ts +11 -1
- package/dist/types/lib/Installer.d.ts +33 -4
- package/dist/types/lib/Native.d.ts +23 -1
- package/dist/types/lib/mac/Mac.d.ts +1 -1
- package/dist/types/lib/nwjs/Addons.d.ts +51 -0
- package/dist/types/lib/nwjs/Package.d.ts +52 -0
- package/dist/types/lib/nwjs/Runtime.d.ts +116 -0
- package/dist/types/lib/nwjs/Stage.d.ts +104 -0
- package/dist/types/lib/win/Windows.d.ts +28 -0
- package/package.json +2 -2
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { copyFileSync, lstatSync, mkdirSync, readdirSync, readlinkSync } from 'node:fs';
|
|
2
|
-
import { stat, mkdir, writeFile, rm } from 'node:fs/promises';
|
|
2
|
+
import { stat, mkdir, readFile, writeFile, rm } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { archiveDirectory } from '../Archive.js';
|
|
5
|
+
import { decodePng, writeLinuxIcons } from '../Icons.js';
|
|
5
6
|
import { runCommand } from '../Processes.js';
|
|
6
7
|
import { ensureTool } from '../Tools.js';
|
|
7
8
|
import { embeddedShell, INSTALLER_HOOKS, NATIVE_HOOKS } from '../Scripts.js';
|
|
@@ -18,11 +19,47 @@ const RUNTIME_DEPENDS = {
|
|
|
18
19
|
apk: ['libstdc++', 'libgcc'],
|
|
19
20
|
archlinux: ['gcc-libs']
|
|
20
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* The libraries of the system a desktop application of Chromium (NW.js) loads, by package: the deb ones of Electron, and
|
|
24
|
+
* the atomic operations the Node.js of NW.js loads (libnode.so needs libatomic.so.1, which only the compilers install)
|
|
25
|
+
*/
|
|
26
|
+
const DESKTOP_DEPENDS = {
|
|
27
|
+
deb: ['libgtk-3-0', 'libnotify4', 'libnss3', 'libxss1', 'libxtst6', 'xdg-utils', 'libatspi2.0-0', 'libuuid1', 'libsecret-1-0', 'libasound2', 'libgbm1', 'libatomic1'],
|
|
28
|
+
rpm: ['gtk3', 'libnotify', 'nss', 'libXScrnSaver', 'libXtst', 'xdg-utils', 'at-spi2-core', 'libuuid', 'libsecret', 'alsa-lib', 'mesa-libgbm', 'libatomic'],
|
|
29
|
+
apk: [],
|
|
30
|
+
archlinux: []
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* The desktop entry of a desktop application, which the menus of the desktops list
|
|
34
|
+
* @param plan
|
|
35
|
+
* @param executable
|
|
36
|
+
*/
|
|
37
|
+
function desktopEntry(plan, executable) {
|
|
38
|
+
return [
|
|
39
|
+
'[Desktop Entry]',
|
|
40
|
+
'Type=Application',
|
|
41
|
+
`Name=${plan.productName}`,
|
|
42
|
+
`Comment=${plan.description.replace(/\s+/g, ' ')}`,
|
|
43
|
+
`Exec="${plan.prefix}/${executable}" %U`,
|
|
44
|
+
`Icon=${plan.name}`,
|
|
45
|
+
'Terminal=false',
|
|
46
|
+
'Categories=Utility;',
|
|
47
|
+
''
|
|
48
|
+
].join('\n');
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The lines refreshing the menus and the icons of the desktops after the installation and the removal of a desktop
|
|
52
|
+
* application, where their tools are installed
|
|
53
|
+
*/
|
|
54
|
+
const DESKTOP_REFRESH = [
|
|
55
|
+
'command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database -q /usr/share/applications || true',
|
|
56
|
+
'command -v gtk-update-icon-cache >/dev/null 2>&1 && gtk-update-icon-cache -q -t -f /usr/share/icons/hicolor || true'
|
|
57
|
+
];
|
|
21
58
|
/**
|
|
22
59
|
* The architecture of the packages (the one of Go, which nFPM translates for each package)
|
|
23
60
|
*/
|
|
24
|
-
function packageArch() {
|
|
25
|
-
return { x64: 'amd64', arm64: 'arm64', ia32: '386', arm: 'arm7' }[
|
|
61
|
+
function packageArch(plan) {
|
|
62
|
+
return { x64: 'amd64', arm64: 'arm64', ia32: '386', arm: 'arm7' }[plan.arch] ?? plan.arch;
|
|
26
63
|
}
|
|
27
64
|
/**
|
|
28
65
|
* The characters of the globs, which nFPM reads in the sources of the contents (the chunks of Turbopack are named
|
|
@@ -78,7 +115,7 @@ function packageFile(plan, target) {
|
|
|
78
115
|
const arch = { x64: 'x86_64', arm64: 'aarch64' }[process.arch] ?? process.arch;
|
|
79
116
|
switch (target) {
|
|
80
117
|
case 'deb':
|
|
81
|
-
return `${plan.name}_${plan.version}_${packageArch()}.deb`;
|
|
118
|
+
return `${plan.name}_${plan.version}_${packageArch(plan)}.deb`;
|
|
82
119
|
case 'rpm':
|
|
83
120
|
return `${plan.name}-${plan.version}-1.${arch}.rpm`;
|
|
84
121
|
case 'apk':
|
|
@@ -276,7 +313,8 @@ function packageScripts(plan, target) {
|
|
|
276
313
|
//Not indented: the lines of the script are the ones of its file
|
|
277
314
|
...application ? [scriptAction(target, hook), 'if [ -n "$lakutata_action" ]; then', ...embeddedShell(application, plan.prefix, '"$lakutata_action"'), 'fi'] : [],
|
|
278
315
|
...native ? embeddedShell(native, plan.prefix, '"$@"') : [],
|
|
279
|
-
...service.after[hook] ?? []
|
|
316
|
+
...service.after[hook] ?? [],
|
|
317
|
+
...plan.desktop && (hook === 'postinstall' || hook === 'postremove' || hook === 'postupgrade') ? DESKTOP_REFRESH : []
|
|
280
318
|
];
|
|
281
319
|
if (lines.length)
|
|
282
320
|
scripts[hook] = ['#!/bin/sh', 'set -e', ...lines, ''].join('\n');
|
|
@@ -297,7 +335,7 @@ export async function buildLinuxPackages(plan, targets, work, outdir) {
|
|
|
297
335
|
const service = plan.service;
|
|
298
336
|
for (const target of targets) {
|
|
299
337
|
if (target === 'tar.gz') {
|
|
300
|
-
files.push(await archiveDirectory(plan, packageArch(), work, outdir));
|
|
338
|
+
files.push(await archiveDirectory(plan, packageArch(plan), work, outdir));
|
|
301
339
|
continue;
|
|
302
340
|
}
|
|
303
341
|
const directory = path.join(work, target);
|
|
@@ -305,6 +343,17 @@ export async function buildLinuxPackages(plan, targets, work, outdir) {
|
|
|
305
343
|
const contents = packageContents(plan.directory, plan.prefix, directory);
|
|
306
344
|
if (plan.addToPath)
|
|
307
345
|
contents.push({ src: `${plan.prefix}/${plan.program}`, dst: `/usr/bin/${plan.program}`, type: 'symlink' });
|
|
346
|
+
if (plan.desktop) {
|
|
347
|
+
//Its entry in the menus of the desktops, its icons in the theme hicolor
|
|
348
|
+
await writeFile(path.join(directory, 'desktop'), desktopEntry(plan, plan.desktop.executable));
|
|
349
|
+
contents.push({ src: path.join(directory, 'desktop'), dst: `/usr/share/applications/${plan.name}.desktop`, file_info: { mode: 0o644 } });
|
|
350
|
+
if (plan.desktop.icon) {
|
|
351
|
+
for (const icon of await writeLinuxIcons(decodePng(await readFile(plan.desktop.icon), plan.desktop.icon), path.join(directory, 'icons'))) {
|
|
352
|
+
const size = path.basename(icon, '.png');
|
|
353
|
+
contents.push({ src: icon, dst: `/usr/share/icons/hicolor/${size}/apps/${plan.name}.png`, file_info: { mode: 0o644 } });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
308
357
|
if (service) {
|
|
309
358
|
await writeFile(path.join(directory, 'environment'), environmentFile(service));
|
|
310
359
|
contents.push({ src: path.join(directory, 'environment'), dst: `/etc/${plan.name}/${plan.name}.env`, type: 'config|noreplace', file_info: { mode: 0o640 } });
|
|
@@ -331,7 +380,7 @@ export async function buildLinuxPackages(plan, targets, work, outdir) {
|
|
|
331
380
|
}
|
|
332
381
|
const config = {
|
|
333
382
|
name: plan.name,
|
|
334
|
-
arch: packageArch(),
|
|
383
|
+
arch: packageArch(plan),
|
|
335
384
|
platform: 'linux',
|
|
336
385
|
version: plan.version,
|
|
337
386
|
maintainer: plan.maintainer,
|
|
@@ -341,7 +390,7 @@ export async function buildLinuxPackages(plan, targets, work, outdir) {
|
|
|
341
390
|
...plan.license ? { license: plan.license } : {},
|
|
342
391
|
...plan.section ? { section: plan.section } : {},
|
|
343
392
|
...plan.priority ? { priority: plan.priority } : {},
|
|
344
|
-
depends: [...new Set([...RUNTIME_DEPENDS[target], ...plan.depends])],
|
|
393
|
+
depends: [...new Set([...RUNTIME_DEPENDS[target], ...plan.desktop ? DESKTOP_DEPENDS[target] : [], ...plan.depends])],
|
|
345
394
|
...plan.recommends.length ? { recommends: plan.recommends } : {},
|
|
346
395
|
contents: contents,
|
|
347
396
|
scripts: common,
|
package/dist/esm/lib/mac/Mac.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import { chmod,
|
|
1
|
+
import { chmod, mkdir, readFile, rm, stat, symlink, writeFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { archiveDirectory } from '../Archive.js';
|
|
4
|
+
import { copyDirectory } from '../Directories.js';
|
|
4
5
|
import { runCommand } from '../Processes.js';
|
|
5
6
|
import { embeddedShell } from '../Scripts.js';
|
|
6
7
|
/**
|
|
7
8
|
* The installers of macOS: the package of its installer, and the archive of the directory of the executable
|
|
8
9
|
*/
|
|
9
|
-
export const MAC_TARGETS = ['pkg', 'tar.gz'];
|
|
10
|
+
export const MAC_TARGETS = ['pkg', 'dmg', 'tar.gz'];
|
|
10
11
|
/**
|
|
11
12
|
* The architecture of the packages
|
|
12
13
|
*/
|
|
13
|
-
function packageArch() {
|
|
14
|
-
return
|
|
14
|
+
function packageArch(plan) {
|
|
15
|
+
return plan.arch;
|
|
15
16
|
}
|
|
16
17
|
/**
|
|
17
18
|
* A text of a property list
|
|
@@ -103,7 +104,7 @@ function preInstall(plan) {
|
|
|
103
104
|
return [
|
|
104
105
|
'#!/bin/sh',
|
|
105
106
|
'set -e',
|
|
106
|
-
`if [ -e ${doubleQuoted(`${plan.prefix}/${plan.program}`)} ]; then lakutata_action=upgrade; else lakutata_action=install; fi`,
|
|
107
|
+
`if [ -e ${doubleQuoted(plan.desktop ? plan.prefix : `${plan.prefix}/${plan.program}`)} ]; then lakutata_action=upgrade; else lakutata_action=install; fi`,
|
|
107
108
|
`if [ -n "$INSTALLER_TEMP" ]; then echo "$lakutata_action" > ${ACTION_FILE}; fi`,
|
|
108
109
|
...hooks.preinstall ? embeddedShell(hooks.preinstall, plan.prefix, '"$lakutata_action"') : [],
|
|
109
110
|
''
|
|
@@ -223,6 +224,26 @@ function uninstall(plan) {
|
|
|
223
224
|
''
|
|
224
225
|
].join('\n');
|
|
225
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* The disk image of a desktop application (dmg): its bundle and a link to /Applications, where its user drags it
|
|
229
|
+
* @param plan
|
|
230
|
+
* @param work
|
|
231
|
+
* @param outdir
|
|
232
|
+
*/
|
|
233
|
+
async function diskImage(plan, work, outdir) {
|
|
234
|
+
if (!plan.desktop)
|
|
235
|
+
throw new Error('The disk images (dmg) are made for the desktop applications (NW.js): the other applications are installed by pkg');
|
|
236
|
+
const volume = path.join(work, 'dmg');
|
|
237
|
+
await rm(volume, { recursive: true, force: true });
|
|
238
|
+
await copyDirectory(path.join(plan.directory, plan.desktop.executable), path.join(volume, plan.desktop.executable));
|
|
239
|
+
await symlink('/Applications', path.join(volume, 'Applications'));
|
|
240
|
+
const file = path.join(outdir, `${plan.name}-${plan.version}-${packageArch(plan)}.dmg`);
|
|
241
|
+
await rm(file, { force: true });
|
|
242
|
+
const created = await runCommand('hdiutil', ['create', '-volname', plan.productName, '-srcfolder', volume, '-ov', '-format', 'UDZO', file], work);
|
|
243
|
+
if (created.code !== 0)
|
|
244
|
+
throw new Error(`The disk image cannot be made: ${created.output.trim()}`);
|
|
245
|
+
return { target: 'dmg', file: file, size: (await stat(file)).size };
|
|
246
|
+
}
|
|
226
247
|
/**
|
|
227
248
|
* Make the installers of macOS of an application: the package installed by its installer (pkgbuild, productbuild), the
|
|
228
249
|
* tar.gz archive of the directory of the executable
|
|
@@ -236,7 +257,11 @@ export async function buildMacPackages(plan, targets, work, outdir) {
|
|
|
236
257
|
const files = [];
|
|
237
258
|
for (const target of targets) {
|
|
238
259
|
if (target === 'tar.gz') {
|
|
239
|
-
files.push(await archiveDirectory(plan, packageArch(), work, outdir));
|
|
260
|
+
files.push(await archiveDirectory(plan, packageArch(plan), work, outdir));
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (target === 'dmg') {
|
|
264
|
+
files.push(await diskImage(plan, work, outdir));
|
|
240
265
|
continue;
|
|
241
266
|
}
|
|
242
267
|
//The files installed, from the root of the system
|
|
@@ -246,9 +271,11 @@ export async function buildMacPackages(plan, targets, work, outdir) {
|
|
|
246
271
|
await rm(scripts, { recursive: true, force: true });
|
|
247
272
|
const prefix = path.join(root, ...plan.prefix.split('/'));
|
|
248
273
|
await mkdir(path.dirname(prefix), { recursive: true });
|
|
249
|
-
|
|
250
|
-
await
|
|
251
|
-
if (plan.
|
|
274
|
+
//A desktop application: its bundle into /Applications (removed by its user, as the other applications)
|
|
275
|
+
await copyDirectory(plan.desktop ? path.join(plan.directory, plan.desktop.executable) : plan.directory, prefix);
|
|
276
|
+
if (!plan.desktop)
|
|
277
|
+
await writeFile(path.join(prefix, 'uninstall'), uninstall(plan), { mode: 0o755 });
|
|
278
|
+
if (plan.addToPath && !plan.desktop) {
|
|
252
279
|
await mkdir(path.join(root, 'usr', 'local', 'bin'), { recursive: true });
|
|
253
280
|
await symlink(path.posix.relative('/usr/local/bin', `${plan.prefix}/${plan.program}`), path.join(root, 'usr', 'local', 'bin', plan.program));
|
|
254
281
|
}
|
|
@@ -270,14 +297,15 @@ export async function buildMacPackages(plan, targets, work, outdir) {
|
|
|
270
297
|
const built = await runCommand('pkgbuild', ['--root', root, '--identifier', plan.appId, '--version', plan.version, '--install-location', '/', '--scripts', scripts, '--ownership', 'recommended', component], work);
|
|
271
298
|
if (built.code !== 0)
|
|
272
299
|
throw new Error(`The pkg package cannot be made: ${built.output.trim()}`);
|
|
273
|
-
const file = path.join(outdir, `${plan.name}-${plan.version}-${packageArch()}.pkg`);
|
|
300
|
+
const file = path.join(outdir, `${plan.name}-${plan.version}-${packageArch(plan)}.pkg`);
|
|
274
301
|
await rm(file, { force: true });
|
|
275
302
|
//Installed on the Macs of the architecture of the program only (productbuild gives both)
|
|
276
303
|
const distribution = path.join(work, 'distribution.xml');
|
|
277
304
|
const synthesized = await runCommand('productbuild', ['--synthesize', '--package', component, distribution], work);
|
|
278
305
|
if (synthesized.code !== 0)
|
|
279
306
|
throw new Error(`The pkg package cannot be made: ${synthesized.output.trim()}`);
|
|
280
|
-
|
|
307
|
+
//A desktop application of Intel runs on the Macs of Apple silicon too (Rosetta)
|
|
308
|
+
const architectures = `hostArchitectures="${packageArch(plan) === 'arm64' ? 'arm64' : plan.desktop ? 'x86_64,arm64' : 'x86_64'}"`;
|
|
281
309
|
const options = await readFile(distribution, 'utf8');
|
|
282
310
|
await writeFile(distribution, /hostArchitectures="[^"]*"/.test(options) ? options.replace(/hostArchitectures="[^"]*"/, architectures) : options.replace('<options ', `<options ${architectures} `));
|
|
283
311
|
const product = await runCommand('productbuild', ['--distribution', distribution, '--package-path', work, '--identifier', plan.appId, '--version', plan.version, file], work);
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { cp, mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { runCommand } from '../Processes.js';
|
|
7
|
+
import { cacheDirectory } from '../Tools.js';
|
|
8
|
+
import { compareVersions } from './Runtime.js';
|
|
9
|
+
/**
|
|
10
|
+
* The name of the Node.js the addons of Windows import their functions from, as node-gyp links them (node.lib), and
|
|
11
|
+
* the library NW.js holds them in
|
|
12
|
+
*/
|
|
13
|
+
const HOST = 'node.exe';
|
|
14
|
+
const NWJS_NODE = 'node.dll';
|
|
15
|
+
/**
|
|
16
|
+
* The offset in the file of an address relative to the image, undefined outside the sections
|
|
17
|
+
* @param sections
|
|
18
|
+
* @param address
|
|
19
|
+
*/
|
|
20
|
+
function offsetOf(sections, address) {
|
|
21
|
+
const section = sections.find((item) => address >= item.address && address < item.address + item.size);
|
|
22
|
+
return section && address - section.address + section.offset;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The string ending with a zero at an offset of a file
|
|
26
|
+
* @param file
|
|
27
|
+
* @param offset
|
|
28
|
+
*/
|
|
29
|
+
function stringAt(file, offset) {
|
|
30
|
+
const end = file.indexOf(0, offset);
|
|
31
|
+
return file.toString('latin1', offset, end < 0 ? file.length : end);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Make an addon of Windows load in NW.js: the libraries it imports (imported or delay-loaded) named node.exe are named
|
|
35
|
+
* node.dll, the library of NW.js holding the functions of Node.js (Node-API). The delay-load hook of node-gyp gives an
|
|
36
|
+
* addon the executable when it loads node.exe, which in NW.js does not export them: the addon fails to load ("A dynamic
|
|
37
|
+
* link library (DLL) initialization routine failed"), or blocks NW.js. Named node.dll, the library is the one NW.js
|
|
38
|
+
* loaded. The addon is changed in place (its other bytes are kept); it then loads in NW.js only, not in Node.js.
|
|
39
|
+
* @param file the content of the addon (a .node file of Windows)
|
|
40
|
+
* @returns the names changed: 0 when it imports no node.exe (an addon built for NW.js, or loading Node.js otherwise)
|
|
41
|
+
* @internal
|
|
42
|
+
*/
|
|
43
|
+
export function nameNwjsNode(file) {
|
|
44
|
+
if (file.length < 64 || file.readUInt16LE(0) !== 0x5a4d)
|
|
45
|
+
throw new Error('Not a portable executable (no MZ header)');
|
|
46
|
+
const header = file.readUInt32LE(0x3c);
|
|
47
|
+
if (file.readUInt32LE(header) !== 0x4550)
|
|
48
|
+
throw new Error('Not a portable executable (no PE signature)');
|
|
49
|
+
const coff = header + 4;
|
|
50
|
+
const sectionCount = file.readUInt16LE(coff + 2);
|
|
51
|
+
const optionalSize = file.readUInt16LE(coff + 16);
|
|
52
|
+
const optional = coff + 20;
|
|
53
|
+
const magic = file.readUInt16LE(optional);
|
|
54
|
+
if (magic !== 0x10b && magic !== 0x20b)
|
|
55
|
+
throw new Error(`Not a portable executable (optional header ${magic.toString(16)})`);
|
|
56
|
+
//The data directories: after 96 bytes (PE32), 112 bytes (PE32+); the image base, for the old delay-load tables (addresses)
|
|
57
|
+
const directories = optional + (magic === 0x10b ? 96 : 112);
|
|
58
|
+
const imageBase = magic === 0x10b ? file.readUInt32LE(optional + 28) : Number(file.readBigUInt64LE(optional + 24));
|
|
59
|
+
const sections = [];
|
|
60
|
+
for (let index = 0, table = optional + optionalSize; index < sectionCount; index++, table += 40) {
|
|
61
|
+
sections.push({ address: file.readUInt32LE(table + 12), size: Math.max(file.readUInt32LE(table + 8), file.readUInt32LE(table + 16)), offset: file.readUInt32LE(table + 20) });
|
|
62
|
+
}
|
|
63
|
+
let changed = 0;
|
|
64
|
+
const rename = (address) => {
|
|
65
|
+
const offset = offsetOf(sections, address);
|
|
66
|
+
if (offset === undefined || stringAt(file, offset).toLowerCase() !== HOST)
|
|
67
|
+
return;
|
|
68
|
+
file.write(NWJS_NODE, offset, 'latin1');
|
|
69
|
+
changed++;
|
|
70
|
+
};
|
|
71
|
+
//The imports (directory 1): descriptors of 20 bytes, the name at 12, ending with an empty one
|
|
72
|
+
const imports = offsetOf(sections, file.readUInt32LE(directories + 8));
|
|
73
|
+
if (imports !== undefined && file.readUInt32LE(directories + 12)) {
|
|
74
|
+
for (let descriptor = imports; descriptor + 20 <= file.length && file.readUInt32LE(descriptor + 12); descriptor += 20)
|
|
75
|
+
rename(file.readUInt32LE(descriptor + 12));
|
|
76
|
+
}
|
|
77
|
+
//The delay-loaded imports (directory 13): descriptors of 32 bytes, the name at 4 (an address before the RVA attribute)
|
|
78
|
+
const delayed = offsetOf(sections, file.readUInt32LE(directories + 13 * 8));
|
|
79
|
+
if (delayed !== undefined && file.readUInt32LE(directories + 13 * 8 + 4)) {
|
|
80
|
+
for (let descriptor = delayed; descriptor + 32 <= file.length && file.readUInt32LE(descriptor + 4); descriptor += 32) {
|
|
81
|
+
const name = file.readUInt32LE(descriptor + 4);
|
|
82
|
+
rename(file.readUInt32LE(descriptor) & 1 ? name : name - imageBase);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return changed;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The architectures of a native binary (Mach-O, possibly universal; ELF; PE) as node names them: x64, ia32, arm64, arm
|
|
89
|
+
* @param file
|
|
90
|
+
* @internal
|
|
91
|
+
*/
|
|
92
|
+
export function binaryArchitectures(file) {
|
|
93
|
+
const names = { 0x01000007: 'x64', 0x0100000c: 'arm64', 7: 'ia32', 12: 'arm' };
|
|
94
|
+
if (file.length >= 8 && file.readUInt32BE(0) === 0xcafebabe) {
|
|
95
|
+
const count = file.readUInt32BE(4);
|
|
96
|
+
return Array.from({ length: count }, (_, index) => names[file.readUInt32BE(8 + index * 20)] ?? 'unknown');
|
|
97
|
+
}
|
|
98
|
+
if (file.length >= 8 && (file.readUInt32LE(0) === 0xfeedfacf || file.readUInt32LE(0) === 0xfeedface))
|
|
99
|
+
return [names[file.readUInt32LE(4)] ?? 'unknown'];
|
|
100
|
+
if (file.length >= 20 && file.readUInt32BE(0) === 0x7f454c46)
|
|
101
|
+
return [{ 62: 'x64', 183: 'arm64', 3: 'ia32', 40: 'arm' }[file.readUInt16LE(18)] ?? 'unknown'];
|
|
102
|
+
if (file.length >= 64 && file.readUInt16LE(0) === 0x5a4d) {
|
|
103
|
+
const machine = file.readUInt16LE(file.readUInt32LE(0x3c) + 4);
|
|
104
|
+
return [{ 0x8664: 'x64', 0x14c: 'ia32', 0xaa64: 'arm64' }[machine] ?? 'unknown'];
|
|
105
|
+
}
|
|
106
|
+
return ['unknown'];
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Whether an addon uses the C++ API of V8 (NAN, v8.h) rather than Node-API: the names of the functions of V8 it imports
|
|
110
|
+
* (mangled by the Itanium ABI or by MSVC). Built for the V8 of Node.js, it does not run in the one of NW.js (Chromium)
|
|
111
|
+
* @param file
|
|
112
|
+
* @internal
|
|
113
|
+
*/
|
|
114
|
+
export function usesV8Api(file) {
|
|
115
|
+
return file.includes('_ZN2v8') || file.includes('@v8@@');
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The first release of NW.js whose headers node-gyp builds the addons with (--target, --dist-url)
|
|
119
|
+
*/
|
|
120
|
+
const NODE_GYP_HEADERS = '0.111.3';
|
|
121
|
+
/**
|
|
122
|
+
* The Node.js whose headers the addons of Node-API of 32-bit Windows are built with when the one of NW.js is later:
|
|
123
|
+
* Node.js 23 and later publish no library of 32-bit Windows (node.lib), and Node-API runs the addons built for an older
|
|
124
|
+
* Node.js
|
|
125
|
+
*/
|
|
126
|
+
const WINDOWS_32_NODE = '22.22.0';
|
|
127
|
+
/**
|
|
128
|
+
* node-gyp: the one of the project, of the builder, else the one of npm
|
|
129
|
+
* @param project
|
|
130
|
+
*/
|
|
131
|
+
function nodeGyp(project) {
|
|
132
|
+
for (const base of [path.join(project, 'package.json'), import.meta.url]) {
|
|
133
|
+
try {
|
|
134
|
+
return createRequire(base).resolve('node-gyp/bin/node-gyp.js');
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
//Not installed there
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
//The node-gyp of npm, next to the node running the build
|
|
141
|
+
const prefix = process.platform === 'win32' ? path.dirname(process.execPath) : path.join(path.dirname(process.execPath), '..', 'lib');
|
|
142
|
+
const bundled = path.join(prefix, 'node_modules', 'npm', 'node_modules', 'node-gyp', 'bin', 'node-gyp.js');
|
|
143
|
+
if (existsSync(bundled))
|
|
144
|
+
return bundled;
|
|
145
|
+
throw new Error('node-gyp builds the addons for NW.js: install it in the project (npm install --save-dev node-gyp)');
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The headers of a release of NW.js for node-gyp, in a directory of their own (--devdir) of the cache: downloaded once,
|
|
149
|
+
* their common.gypi without the linker of Chromium (lld, which the compilers of macOS and of the Linux systems do not
|
|
150
|
+
* have: the linker of the system links the addons as well)
|
|
151
|
+
* @param project
|
|
152
|
+
* @param release
|
|
153
|
+
* @returns the directory given to node-gyp (--devdir)
|
|
154
|
+
*/
|
|
155
|
+
async function nwjsHeaders(project, release) {
|
|
156
|
+
const devdir = path.join(cacheDirectory(), 'nwjs', 'node-gyp');
|
|
157
|
+
const common = path.join(devdir, release.version, 'common.gypi');
|
|
158
|
+
if (!existsSync(common)) {
|
|
159
|
+
const installed = await runCommand(process.execPath, [nodeGyp(project), 'install', `--target=${release.version}`, `--dist-url=${mirror()}`, `--devdir=${devdir}`], project);
|
|
160
|
+
if (installed.code !== 0 || !existsSync(common))
|
|
161
|
+
throw new Error(`The headers of NW.js ${release.version} cannot be downloaded: ${installed.output.trim().split('\n').slice(-3).join(' ')}`);
|
|
162
|
+
}
|
|
163
|
+
const content = await readFile(common, 'utf8');
|
|
164
|
+
const linked = content.replace(/,\s*'-fuse-ld=lld'/g, '').replace(/-fuse-ld=lld\s*/g, '');
|
|
165
|
+
if (linked !== content)
|
|
166
|
+
await writeFile(common, linked);
|
|
167
|
+
return devdir;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Where the releases of NW.js are downloaded from (LAKUTATA_NWJS_MIRROR, else dl.nwjs.io)
|
|
171
|
+
*/
|
|
172
|
+
function mirror() {
|
|
173
|
+
return (process.env.LAKUTATA_NWJS_MIRROR ?? 'https://dl.nwjs.io').replace(/\/+$/, '');
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The directory of the package of a file (the one holding its package.json)
|
|
177
|
+
* @param file
|
|
178
|
+
*/
|
|
179
|
+
function packageOf(file) {
|
|
180
|
+
for (let directory = path.dirname(file); directory !== path.dirname(directory); directory = path.dirname(directory)) {
|
|
181
|
+
if (existsSync(path.join(directory, 'package.json')))
|
|
182
|
+
return directory;
|
|
183
|
+
}
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* The .node files under a directory
|
|
188
|
+
* @param directory
|
|
189
|
+
*/
|
|
190
|
+
async function nodeFiles(directory) {
|
|
191
|
+
if (!existsSync(directory))
|
|
192
|
+
return [];
|
|
193
|
+
const found = [];
|
|
194
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
195
|
+
const file = path.join(directory, entry.name);
|
|
196
|
+
if (entry.isDirectory())
|
|
197
|
+
found.push(...await nodeFiles(file));
|
|
198
|
+
else if (entry.isFile() && entry.name.endsWith('.node'))
|
|
199
|
+
found.push(file);
|
|
200
|
+
}
|
|
201
|
+
return found;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* The headers node-gyp builds the addons of a release of NW.js with: the ones of NW.js for the addons using V8, else the
|
|
205
|
+
* ones of its Node.js (the Node-API it has; a Node.js 22 on 32-bit Windows when its own one is later)
|
|
206
|
+
* @param project
|
|
207
|
+
* @param release
|
|
208
|
+
* @param platform
|
|
209
|
+
* @param v8
|
|
210
|
+
*/
|
|
211
|
+
async function headersOf(project, release, platform, v8) {
|
|
212
|
+
if (!v8)
|
|
213
|
+
return [`--target=${platform === 'win-ia32' && compareVersions(release.node, '23.0.0') >= 0 ? WINDOWS_32_NODE : release.node}`];
|
|
214
|
+
if (platform === 'win-ia32') {
|
|
215
|
+
//The library of 32-bit Windows (at the root of the release, as node-gyp looks for it): the releases since 0.111.3 have none
|
|
216
|
+
const library = await fetch(`${mirror()}/v${release.version}/node.lib`, { method: 'HEAD' });
|
|
217
|
+
if (!library.ok)
|
|
218
|
+
throw new Error(`NW.js ${release.version} publishes no library of 32-bit Windows (node.lib) to build the addons using V8 with: they run in its x64 build, or use a package of Node-API`);
|
|
219
|
+
}
|
|
220
|
+
return [`--target=${release.version}`, `--dist-url=${mirror()}`, `--devdir=${await nwjsHeaders(project, release)}`];
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Build a package of addons again from its sources (a copy of it), its addons written into build/Release of its copy
|
|
224
|
+
* in the native tree
|
|
225
|
+
* @param project
|
|
226
|
+
* @param source the directory of the package, in the node_modules of the project
|
|
227
|
+
* @param target the directory of the package in the native tree
|
|
228
|
+
* @param arch
|
|
229
|
+
* @param headers the arguments giving node-gyp its headers (headersOf)
|
|
230
|
+
*/
|
|
231
|
+
async function rebuild(project, source, target, arch, headers) {
|
|
232
|
+
if (!existsSync(path.join(source, 'binding.gyp')))
|
|
233
|
+
throw new Error(`${path.basename(source)} has no sources to build its addons for NW.js (binding.gyp): its addons are not for ${arch}${headers.length > 1 ? ' or use V8' : ''}`);
|
|
234
|
+
const work = await mkdtemp(path.join(os.tmpdir(), 'lakutata-nwjs-addon-'));
|
|
235
|
+
try {
|
|
236
|
+
const copy = path.join(work, path.basename(source));
|
|
237
|
+
await cp(source, copy, { recursive: true, filter: (file) => path.relative(source, file) !== 'build' });
|
|
238
|
+
//The modules of its build (node-addon-api) found where the package is
|
|
239
|
+
const modules = (createRequire(path.join(source, 'package.json')).resolve.paths('x') ?? []);
|
|
240
|
+
const args = [nodeGyp(project), 'rebuild', `--arch=${arch}`, ...headers];
|
|
241
|
+
const previous = process.env.NODE_PATH;
|
|
242
|
+
process.env.NODE_PATH = [...modules, previous].filter(Boolean).join(path.delimiter);
|
|
243
|
+
try {
|
|
244
|
+
const built = await runCommand(process.execPath, args, copy);
|
|
245
|
+
if (built.code !== 0)
|
|
246
|
+
throw new Error(`The addons of ${path.basename(source)} cannot be built for NW.js (node-gyp ${args.slice(1).join(' ')}): ${built.output.trim().split('\n').slice(-5).join(' ')}`);
|
|
247
|
+
}
|
|
248
|
+
finally {
|
|
249
|
+
if (previous === undefined)
|
|
250
|
+
delete process.env.NODE_PATH;
|
|
251
|
+
else
|
|
252
|
+
process.env.NODE_PATH = previous;
|
|
253
|
+
}
|
|
254
|
+
await mkdir(path.join(target, 'build', 'Release'), { recursive: true });
|
|
255
|
+
for (const file of await nodeFiles(path.join(copy, 'build', 'Release')))
|
|
256
|
+
await cp(file, path.join(target, 'build', 'Release', path.basename(file)));
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
await rm(work, { recursive: true, force: true });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Make the addons of the native tree of an application of NW.js load in its runtime: the packages whose addons are not
|
|
264
|
+
* for the architecture of the runtime (built with the headers of its Node.js), or use V8 (NW.js 0.111.3 and later: the
|
|
265
|
+
* headers of NW.js), built again from their sources; on Windows, the node.exe they import named node.dll
|
|
266
|
+
* @param project
|
|
267
|
+
* @param directory the native tree
|
|
268
|
+
* @param files the files of the tree (their sources)
|
|
269
|
+
* @param release
|
|
270
|
+
* @param platform the build of NW.js (win-x64, osx-arm64…)
|
|
271
|
+
* @internal
|
|
272
|
+
*/
|
|
273
|
+
export async function prepareNwjsAddons(project, directory, files, release, platform) {
|
|
274
|
+
const arch = platform.slice(platform.indexOf('-') + 1);
|
|
275
|
+
const result = { renamed: [], rebuilt: [] };
|
|
276
|
+
const done = new Set();
|
|
277
|
+
for (const file of files.filter((item) => item.path.endsWith('.node'))) {
|
|
278
|
+
const target = path.join(directory, ...file.path.split('/'));
|
|
279
|
+
const content = await readFile(target);
|
|
280
|
+
const v8 = usesV8Api(content);
|
|
281
|
+
if (v8 && compareVersions(release.version, NODE_GYP_HEADERS) < 0)
|
|
282
|
+
throw new Error(`${file.path} uses the API of V8 (not Node-API): it runs in NW.js ${NODE_GYP_HEADERS} or later, built with its headers; NW.js ${release.version} needs a package of Node-API instead`);
|
|
283
|
+
if (!v8 && binaryArchitectures(content).includes(arch))
|
|
284
|
+
continue;
|
|
285
|
+
const source = packageOf(file.source);
|
|
286
|
+
if (!source)
|
|
287
|
+
throw new Error(`${file.path} is not for ${arch} and is not in a package: build it for NW.js ${release.version} (${platform})`);
|
|
288
|
+
const relative = path.relative(source, file.source).split(path.sep).join('/');
|
|
289
|
+
const packageTarget = path.join(directory, ...file.path.slice(0, file.path.length - relative.length).split('/'));
|
|
290
|
+
if (done.has(packageTarget))
|
|
291
|
+
continue;
|
|
292
|
+
done.add(packageTarget);
|
|
293
|
+
await rebuild(project, source, packageTarget, arch, await headersOf(project, release, platform, v8));
|
|
294
|
+
result.rebuilt.push(path.basename(source));
|
|
295
|
+
}
|
|
296
|
+
if (platform.startsWith('win-')) {
|
|
297
|
+
for (const file of await nodeFiles(directory)) {
|
|
298
|
+
const content = await readFile(file);
|
|
299
|
+
//The addons of Windows only (portable executables)
|
|
300
|
+
if (content.length > 64 && content.readUInt16LE(0) === 0x5a4d && nameNwjsNode(content)) {
|
|
301
|
+
await writeFile(file, content);
|
|
302
|
+
result.renamed.push(path.relative(directory, file).split(path.sep).join('/'));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return result;
|
|
307
|
+
}
|