@lakutata/builder 3.0.0-beta.0
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/LICENSE +23 -0
- package/README.md +27 -0
- package/dist/esm/index.js +13 -0
- package/dist/esm/lib/Archive.js +21 -0
- package/dist/esm/lib/Bundle.js +162 -0
- package/dist/esm/lib/Executable.js +221 -0
- package/dist/esm/lib/Icons.js +372 -0
- package/dist/esm/lib/Installer.js +158 -0
- package/dist/esm/lib/Native.js +533 -0
- package/dist/esm/lib/NativeRuntime.js +177 -0
- package/dist/esm/lib/Platform.js +9 -0
- package/dist/esm/lib/Processes.js +42 -0
- package/dist/esm/lib/Scripts.js +161 -0
- package/dist/esm/lib/Tools.js +109 -0
- package/dist/esm/lib/electron/Electron.js +366 -0
- package/dist/esm/lib/linux/Linux.js +361 -0
- package/dist/esm/lib/mac/Mac.js +270 -0
- package/dist/esm/lib/win/Windows.js +525 -0
- package/dist/types/index.d.ts +13 -0
- package/dist/types/lib/Archive.d.ts +14 -0
- package/dist/types/lib/Bundle.d.ts +96 -0
- package/dist/types/lib/Executable.d.ts +50 -0
- package/dist/types/lib/Icons.d.ts +70 -0
- package/dist/types/lib/Installer.d.ts +242 -0
- package/dist/types/lib/Native.d.ts +196 -0
- package/dist/types/lib/NativeRuntime.d.ts +75 -0
- package/dist/types/lib/Platform.d.ts +4 -0
- package/dist/types/lib/Processes.d.ts +21 -0
- package/dist/types/lib/Scripts.d.ts +77 -0
- package/dist/types/lib/Tools.d.ts +32 -0
- package/dist/types/lib/electron/Electron.d.ts +13 -0
- package/dist/types/lib/linux/Linux.d.ts +29 -0
- package/dist/types/lib/mac/Mac.d.ts +19 -0
- package/dist/types/lib/win/Windows.d.ts +20 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023-present Lakutata
|
|
4
|
+
|
|
5
|
+
All rights reserved
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# @lakutata/builder
|
|
2
|
+
|
|
3
|
+
The builder of the applications of the lakutata framework, used by the `bundle` and `executable` commands of
|
|
4
|
+
`@lakutata/cli`, and usable from the scripts of a project:
|
|
5
|
+
|
|
6
|
+
```js
|
|
7
|
+
import {buildExecutable, buildInstaller, bundleProject} from '@lakutata/builder'
|
|
8
|
+
|
|
9
|
+
//dist/<name>.cjs, run by node, its native addons and its resources next to it
|
|
10
|
+
await bundleProject({project: process.cwd()})
|
|
11
|
+
//dist/<name>, which runs without Node.js: Node.js (by default) or Bun embedded, one file or a directory
|
|
12
|
+
await buildExecutable({project: process.cwd(), runtime: 'node', layout: 'directory'})
|
|
13
|
+
//dist/installer: the installers of the platform (deb, rpm, apk, archlinux, tar.gz on Linux; pkg, tar.gz on macOS;
|
|
14
|
+
//nsis, msi, zip on Windows), its service registered
|
|
15
|
+
await buildInstaller({project: process.cwd()})
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The application is compiled first (`tsc`, which emits the metadata of the decorators). The bundle is one CommonJS file
|
|
19
|
+
(esbuild); the native addons, which cannot be bundled, are gathered with what they load at run time into a native tree;
|
|
20
|
+
the resources of the application (the `lakutata.resources` of its `package.json`) are found at run time by
|
|
21
|
+
`resourcesPath()` of lakutata. The installers are configured by the `lakutata.installer` of the `package.json`. See the
|
|
22
|
+
documentation of `lakutata package bundle`, `lakutata package exe` and `lakutata package installer` in `@lakutata/cli`.
|
|
23
|
+
|
|
24
|
+
The installers of Linux are tested in containers (Docker): `npm run test:installer` makes them on Debian and Alpine,
|
|
25
|
+
installs, upgrades and removes them on Debian and Fedora (systemd) and Alpine (OpenRC). The ones of macOS are installed
|
|
26
|
+
in a virtual machine of macOS (`npm run test:installer:mac`, tart), the ones of Windows on Windows as an administrator
|
|
27
|
+
(`LAKUTATA_WINDOWS_INSTALL=1 npm run test:installer:windows`).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The builder of the applications of lakutata: their bundles (one CommonJS file run by node) and their executables (the
|
|
3
|
+
* single executable applications of Node.js, or Bun), with their native addons and their resources
|
|
4
|
+
*/
|
|
5
|
+
export { bundleProject, programName, resourceFiles } from './lib/Bundle.js';
|
|
6
|
+
export { buildExecutable } from './lib/Executable.js';
|
|
7
|
+
export { NativeCollector, packNativeTree, writeNativeTree } from './lib/Native.js';
|
|
8
|
+
export { buildInstaller } from './lib/Installer.js';
|
|
9
|
+
export { LINUX_TARGETS } from './lib/linux/Linux.js';
|
|
10
|
+
export { INSTALLER_HOOKS, NATIVE_HOOKS } from './lib/Scripts.js';
|
|
11
|
+
export { detectLibc } from './lib/Platform.js';
|
|
12
|
+
export { WINDOWS_TARGETS } from './lib/win/Windows.js';
|
|
13
|
+
export { MAC_TARGETS } from './lib/mac/Mac.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { cp, rm, stat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { runCommand } from './Processes.js';
|
|
4
|
+
/**
|
|
5
|
+
* The tar.gz archive of the directory of the executable (its native addons and its resources next to it), named after
|
|
6
|
+
* the package, its version, its platform and its architecture
|
|
7
|
+
* @param plan
|
|
8
|
+
* @param arch
|
|
9
|
+
* @param work
|
|
10
|
+
* @param outdir
|
|
11
|
+
*/
|
|
12
|
+
export async function archiveDirectory(plan, arch, work, outdir) {
|
|
13
|
+
const base = `${plan.name}-${plan.version}-${process.platform}-${arch}`;
|
|
14
|
+
await rm(path.join(work, base), { recursive: true, force: true });
|
|
15
|
+
await cp(plan.directory, path.join(work, base), { recursive: true, verbatimSymlinks: true });
|
|
16
|
+
const file = path.join(outdir, `${base}.tar.gz`);
|
|
17
|
+
const archived = await runCommand('tar', ['-czf', file, base], work);
|
|
18
|
+
if (archived.code !== 0)
|
|
19
|
+
throw new Error(`The tar.gz archive cannot be made: ${archived.output.trim()}`);
|
|
20
|
+
return { target: 'tar.gz', file: file, size: (await stat(file)).size };
|
|
21
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { chmod, cp, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { hashNativeTree, NativeCollector, writeNativeTree } from './Native.js';
|
|
5
|
+
import { EMBEDDED_RESOURCES, nativeBanner, packagedBanner } from './NativeRuntime.js';
|
|
6
|
+
/**
|
|
7
|
+
* The name of the program of a package: its first bin, or the name of the package without its scope
|
|
8
|
+
* @param manifest
|
|
9
|
+
*/
|
|
10
|
+
export function programName(manifest) {
|
|
11
|
+
if (manifest.bin && typeof manifest.bin === 'object') {
|
|
12
|
+
const [name] = Object.keys(manifest.bin);
|
|
13
|
+
if (name)
|
|
14
|
+
return name;
|
|
15
|
+
}
|
|
16
|
+
return String(manifest.name ?? 'app').replace(/^@[^/]+\//, '');
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The ESM sources use import.meta.url, which the CommonJS bundle gives from its file
|
|
20
|
+
*/
|
|
21
|
+
const IMPORT_META_URL = '__lakutataImportMetaUrl';
|
|
22
|
+
/**
|
|
23
|
+
* The first code of a bundle embedded in an executable of Bun, which bundles it again into a function without the
|
|
24
|
+
* arguments of the CommonJS modules: its file is the executable (as in the executables of Node.js), its require loads
|
|
25
|
+
* the modules of node and the ones of a node_modules next to the executable (the ones left out of the bundle)
|
|
26
|
+
*/
|
|
27
|
+
const BUN_PRELUDE = 'var __filename=process.execPath,__dirname=process.getBuiltinModule(\'node:path\').dirname(__filename),require=typeof require===\'function\'?require:process.getBuiltinModule(\'node:module\').createRequire(__filename);';
|
|
28
|
+
/**
|
|
29
|
+
* The files of the resources of an application, declared by the "lakutata.resources" of its package.json: their paths in
|
|
30
|
+
* the resources ("/" separated) and their sources
|
|
31
|
+
* @param project
|
|
32
|
+
* @param manifest
|
|
33
|
+
*/
|
|
34
|
+
export function resourceFiles(project, manifest) {
|
|
35
|
+
const declarations = manifest.lakutata?.resources;
|
|
36
|
+
if (declarations === undefined)
|
|
37
|
+
return { declared: [], files: [] };
|
|
38
|
+
const valid = (declaration) => typeof declaration?.from === 'string' && typeof declaration?.to === 'string' &&
|
|
39
|
+
!!declaration.to && !path.isAbsolute(declaration.to) && !declaration.to.split(/[\\/]/).includes('..');
|
|
40
|
+
if (!Array.isArray(declarations) || !declarations.every(valid))
|
|
41
|
+
throw new Error('The resources of the application (lakutata.resources of its package.json) must be [{"from": "<file or directory of the project>", "to": "<its path in the resources>"}]');
|
|
42
|
+
const files = [];
|
|
43
|
+
for (const { from, to } of declarations) {
|
|
44
|
+
const source = path.resolve(project, from);
|
|
45
|
+
if (!existsSync(source))
|
|
46
|
+
throw new Error(`The resource ${from} does not exist: build it first`);
|
|
47
|
+
const target = to.split(/[\\/]/).filter(Boolean).join('/');
|
|
48
|
+
const walk = (file, at) => {
|
|
49
|
+
const stats = statSync(file);
|
|
50
|
+
if (stats.isDirectory())
|
|
51
|
+
for (const entry of readdirSync(file))
|
|
52
|
+
walk(path.join(file, entry), `${at}/${entry}`);
|
|
53
|
+
else if (stats.isFile())
|
|
54
|
+
files.push({ path: at, source: file, mode: stats.mode & 0o777 });
|
|
55
|
+
};
|
|
56
|
+
walk(source, target);
|
|
57
|
+
}
|
|
58
|
+
return { declared: declarations.map(({ to }) => to), files: files };
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The code with a code before it, after its hashbang
|
|
62
|
+
* @param code
|
|
63
|
+
* @param first
|
|
64
|
+
*/
|
|
65
|
+
function prepend(code, first) {
|
|
66
|
+
const hashbang = /^#![^\n]*\n/.exec(code);
|
|
67
|
+
return hashbang ? `${hashbang[0]}${first}${code.slice(hashbang[0].length)}` : `${first}${code}`;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Bundle the compiled application and its dependencies into one CommonJS file, run by node and embedded in the executables.
|
|
71
|
+
* The sources are compiled by tsc first: the metadata of the decorators (the injections, the DTOs) are emitted by it.
|
|
72
|
+
* The native addons cannot be bundled: their packages (and what they load) are gathered into a native tree, which the
|
|
73
|
+
* require of the bundle loads them from
|
|
74
|
+
* @param options
|
|
75
|
+
*/
|
|
76
|
+
export async function bundleProject(options) {
|
|
77
|
+
const project = path.resolve(options.project);
|
|
78
|
+
const manifestFile = path.join(project, 'package.json');
|
|
79
|
+
if (!existsSync(manifestFile))
|
|
80
|
+
throw new Error(`${project} has no package.json`);
|
|
81
|
+
const manifest = JSON.parse(await readFile(manifestFile, 'utf8'));
|
|
82
|
+
const name = programName(manifest);
|
|
83
|
+
const entry = path.resolve(project, options.entry ?? manifest.main ?? 'build/App.js');
|
|
84
|
+
if (!existsSync(entry))
|
|
85
|
+
throw new Error(`The entry ${path.relative(process.cwd(), entry) || entry} does not exist: build the project first`);
|
|
86
|
+
const outfile = path.resolve(project, options.outfile ?? path.join('dist', `${name}.cjs`));
|
|
87
|
+
const collector = new NativeCollector(project, options.runtime);
|
|
88
|
+
//esbuild is loaded by the commands which need it
|
|
89
|
+
const { build } = await import('esbuild');
|
|
90
|
+
const result = await build({
|
|
91
|
+
entryPoints: [entry],
|
|
92
|
+
outfile: outfile,
|
|
93
|
+
write: false,
|
|
94
|
+
bundle: true,
|
|
95
|
+
platform: 'node',
|
|
96
|
+
target: `node${process.versions.node.split('.')[0]}`,
|
|
97
|
+
format: 'cjs',
|
|
98
|
+
minify: options.minify ?? true,
|
|
99
|
+
//The names of the classes are the ones of the objects of the container and of the errors
|
|
100
|
+
keepNames: true,
|
|
101
|
+
external: options.external ?? [],
|
|
102
|
+
//The dynamic imports of the modules left out are requires (the require of the bundle loads the native ones)
|
|
103
|
+
supported: { 'dynamic-import': false },
|
|
104
|
+
banner: { js: `var ${IMPORT_META_URL}=require('node:url').pathToFileURL(__filename).href;` },
|
|
105
|
+
define: {
|
|
106
|
+
'import.meta.url': IMPORT_META_URL,
|
|
107
|
+
//Bun replaces process.env.NODE_ENV with the mode of its build: the one of the run is read
|
|
108
|
+
...options.runtime === 'bun' ? { 'process.env.NODE_ENV': 'globalThis.process.env.NODE_ENV' } : {}
|
|
109
|
+
},
|
|
110
|
+
plugins: [collector.plugin()],
|
|
111
|
+
logLevel: 'silent'
|
|
112
|
+
}).catch((error) => {
|
|
113
|
+
const messages = (error.errors ?? []).map(({ text, location }) => location ? `${location.file}:${location.line}: ${text}` : text);
|
|
114
|
+
//The bundles are CommonJS, which the executables of Node.js run
|
|
115
|
+
const hint = messages.some((message) => message.includes('Top-level await')) ? '\nThe bundles are CommonJS modules, without top-level await: await in an async function' : '';
|
|
116
|
+
throw new Error(`The bundle failed:\n ${messages.length ? messages.join('\n ') : error.message}${hint}`);
|
|
117
|
+
});
|
|
118
|
+
let code = result.outputFiles[0].text;
|
|
119
|
+
const embedded = options.native === 'embedded';
|
|
120
|
+
let native = await collector.collect();
|
|
121
|
+
const resources = resourceFiles(project, manifest);
|
|
122
|
+
const base = path.basename(outfile, path.extname(outfile));
|
|
123
|
+
const nativeDirectory = path.resolve(path.dirname(outfile), options.directories?.native ?? `${base}.native`);
|
|
124
|
+
const resourcesDirectory = path.resolve(path.dirname(outfile), options.directories?.resources ?? `${base}.resources`);
|
|
125
|
+
await rm(nativeDirectory, { recursive: true, force: true });
|
|
126
|
+
await rm(resourcesDirectory, { recursive: true, force: true });
|
|
127
|
+
//The resources of an executable are extracted with its native tree
|
|
128
|
+
if (embedded && resources.files.length) {
|
|
129
|
+
const files = [...native?.files ?? [], ...resources.files.map((file) => ({ ...file, path: `${EMBEDDED_RESOURCES}/${file.path}` }))];
|
|
130
|
+
native = { modules: native?.modules ?? [], files: files, links: native?.links ?? [], hash: hashNativeTree(files, native?.links ?? []) };
|
|
131
|
+
}
|
|
132
|
+
if (native) {
|
|
133
|
+
//The require loading the native packages is the first code, after the hashbang
|
|
134
|
+
code = prepend(code, nativeBanner({ name: name, hash: native.hash, directory: path.basename(nativeDirectory), modules: native.modules, embedded: embedded }));
|
|
135
|
+
if (!embedded)
|
|
136
|
+
await writeNativeTree(native, nativeDirectory);
|
|
137
|
+
}
|
|
138
|
+
if (!embedded) {
|
|
139
|
+
for (const file of resources.files) {
|
|
140
|
+
const target = path.join(resourcesDirectory, ...file.path.split('/'));
|
|
141
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
142
|
+
await cp(file.source, target);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
code = prepend(code, packagedBanner(path.basename(resourcesDirectory)));
|
|
146
|
+
//Before the require of the native tree, which loads the modules of node by it
|
|
147
|
+
if (options.runtime === 'bun')
|
|
148
|
+
code = prepend(code, BUN_PRELUDE);
|
|
149
|
+
await mkdir(path.dirname(outfile), { recursive: true });
|
|
150
|
+
await writeFile(outfile, code);
|
|
151
|
+
await chmod(outfile, 0o755);
|
|
152
|
+
return {
|
|
153
|
+
name: name,
|
|
154
|
+
entry: entry,
|
|
155
|
+
outfile: outfile,
|
|
156
|
+
size: (await stat(outfile)).size,
|
|
157
|
+
native: native,
|
|
158
|
+
nativeDirectory: native && !embedded ? nativeDirectory : undefined,
|
|
159
|
+
resources: resources.declared,
|
|
160
|
+
resourcesDirectory: resources.files.length && !embedded ? resourcesDirectory : undefined
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { chmod, copyFile, mkdir, mkdtemp, readFile, rm, stat, 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 { detectLibc } from './Platform.js';
|
|
7
|
+
import { runCommand } from './Processes.js';
|
|
8
|
+
import { bundleProject, programName } from './Bundle.js';
|
|
9
|
+
import { applicationIcon, icoFile, setExecutableIcon } from './Icons.js';
|
|
10
|
+
import { packNativeTree } from './Native.js';
|
|
11
|
+
import { BUN_NATIVE_ASSETS, NATIVE_DATA_ASSET, NATIVE_INDEX_ASSET } from './NativeRuntime.js';
|
|
12
|
+
/**
|
|
13
|
+
* The fuse of the Node.js binaries telling that a single executable application is injected
|
|
14
|
+
*/
|
|
15
|
+
const SEA_FUSE = 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2';
|
|
16
|
+
/**
|
|
17
|
+
* Run a command which must succeed
|
|
18
|
+
* @param command
|
|
19
|
+
* @param args
|
|
20
|
+
* @param cwd
|
|
21
|
+
*/
|
|
22
|
+
async function must(command, args, cwd) {
|
|
23
|
+
const result = await runCommand(command, args, cwd);
|
|
24
|
+
if (result.code !== 0)
|
|
25
|
+
throw new Error(`${command} ${args.join(' ')} failed: ${result.output.trim()}`);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Run a function without the output of the console: postject prints the warnings of LIEF, which it binds when it is called
|
|
29
|
+
* ("Can't find string offset for section name", the signature of node.exe changed by the injection)
|
|
30
|
+
* @param run
|
|
31
|
+
*/
|
|
32
|
+
async function quiet(run) {
|
|
33
|
+
const { log, warn } = console;
|
|
34
|
+
console.log = console.warn = () => undefined;
|
|
35
|
+
try {
|
|
36
|
+
return await run();
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
console.log = log;
|
|
40
|
+
console.warn = warn;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The bun of the project (the bun package of npm, in its node_modules or in the ones of its workspace), else the one of
|
|
45
|
+
* the system
|
|
46
|
+
* @param project
|
|
47
|
+
* @return the command, and the version of Bun
|
|
48
|
+
*/
|
|
49
|
+
async function findBun(project) {
|
|
50
|
+
const candidates = [];
|
|
51
|
+
try {
|
|
52
|
+
const root = path.dirname(createRequire(path.join(project, 'package.json')).resolve('bun/package.json'));
|
|
53
|
+
candidates.push(path.join(root, 'bin', 'bun.exe'));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
//Not installed in the project
|
|
57
|
+
}
|
|
58
|
+
candidates.push('bun');
|
|
59
|
+
for (const command of candidates) {
|
|
60
|
+
if (command !== 'bun' && !existsSync(command))
|
|
61
|
+
continue;
|
|
62
|
+
const result = await runCommand(command, ['--version'], project).catch(() => undefined);
|
|
63
|
+
if (result?.code === 0)
|
|
64
|
+
return { command: command, version: result.output.trim() };
|
|
65
|
+
}
|
|
66
|
+
throw new Error('Bun is not installed: install it in the project (npm install --save-dev bun), or on the system (https://bun.sh)');
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Write the icon of the application (ico) into the work directory
|
|
70
|
+
* @param work
|
|
71
|
+
* @param icon
|
|
72
|
+
*/
|
|
73
|
+
async function writeIcon(work, icon) {
|
|
74
|
+
const file = path.join(work, 'icon.ico');
|
|
75
|
+
await writeFile(file, icon);
|
|
76
|
+
return file;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Embed a bundle in the Node.js binary running the tool (the single executable applications of Node.js)
|
|
80
|
+
* @param bundle
|
|
81
|
+
* @param work
|
|
82
|
+
* @param file
|
|
83
|
+
* @param warnings
|
|
84
|
+
* @param icon the icon of the application (ico, Windows)
|
|
85
|
+
*/
|
|
86
|
+
async function embedInNode(bundle, work, file, warnings, icon) {
|
|
87
|
+
const assets = {};
|
|
88
|
+
if (bundle.native) {
|
|
89
|
+
//The files of the native packages, extracted by the executable at its first run
|
|
90
|
+
const { index, data } = packNativeTree(bundle.native);
|
|
91
|
+
assets[NATIVE_DATA_ASSET] = path.join(work, 'native.gz');
|
|
92
|
+
assets[NATIVE_INDEX_ASSET] = path.join(work, 'native.json');
|
|
93
|
+
await writeFile(assets[NATIVE_DATA_ASSET], data);
|
|
94
|
+
await writeFile(assets[NATIVE_INDEX_ASSET], JSON.stringify(index));
|
|
95
|
+
}
|
|
96
|
+
//The preparation blob of the bundle
|
|
97
|
+
const blob = path.join(work, 'main.blob');
|
|
98
|
+
await writeFile(path.join(work, 'sea-config.json'), JSON.stringify({
|
|
99
|
+
main: bundle.outfile,
|
|
100
|
+
output: blob,
|
|
101
|
+
disableExperimentalSEAWarning: true,
|
|
102
|
+
//The code cache of V8, made by the Node.js embedded: the bundle is not compiled again at start
|
|
103
|
+
useCodeCache: true,
|
|
104
|
+
useSnapshot: false,
|
|
105
|
+
assets: assets
|
|
106
|
+
}));
|
|
107
|
+
await must(process.execPath, ['--experimental-sea-config', 'sea-config.json'], work);
|
|
108
|
+
const { inject } = await import('postject');
|
|
109
|
+
await copyFile(process.execPath, file);
|
|
110
|
+
//The icon of the application replaces the one of node.exe, before the bundle is embedded
|
|
111
|
+
if (icon)
|
|
112
|
+
await setExecutableIcon(file, icon);
|
|
113
|
+
//The signature of macOS is removed before the binary is changed, then made again (ad hoc)
|
|
114
|
+
if (process.platform === 'darwin')
|
|
115
|
+
await must('codesign', ['--remove-signature', file], work);
|
|
116
|
+
await quiet(() => inject(file, 'NODE_SEA_BLOB', readFileSync(blob), { sentinelFuse: SEA_FUSE, machoSegmentName: process.platform === 'darwin' ? 'NODE_SEA' : undefined }));
|
|
117
|
+
if (process.platform === 'darwin')
|
|
118
|
+
await must('codesign', ['--sign', '-', '--force', file], work);
|
|
119
|
+
//The Authenticode signature of node.exe does not match the executable anymore
|
|
120
|
+
if (process.platform === 'win32')
|
|
121
|
+
warnings.push(`${path.basename(file)} has the signature of Node.js, which is not valid anymore: sign it (signtool) before distributing it`);
|
|
122
|
+
return `Node.js ${process.version}`;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Compile a bundle into an executable of Bun, in bytecode: the files of the native tree are embedded beside it, the
|
|
126
|
+
* executable gives their paths to the require of the bundle (BUN_NATIVE_ASSETS). As the executables of Node.js, it loads neither the .env
|
|
127
|
+
* nor the bunfig.toml of the directory it runs in; it reads the package.json of the packages it loads from the disk (the
|
|
128
|
+
* native tree, the modules left out)
|
|
129
|
+
* @param bun
|
|
130
|
+
* @param bundle
|
|
131
|
+
* @param work
|
|
132
|
+
* @param file
|
|
133
|
+
* @param icon the icon of the application (ico, Windows)
|
|
134
|
+
*/
|
|
135
|
+
async function compileWithBun(bun, bundle, work, file, icon) {
|
|
136
|
+
let entry = bundle.outfile;
|
|
137
|
+
if (bundle.native) {
|
|
138
|
+
const { index, data } = packNativeTree(bundle.native);
|
|
139
|
+
await writeFile(path.join(work, 'native.gz'), data);
|
|
140
|
+
await writeFile(path.join(work, 'native.json'), JSON.stringify(index));
|
|
141
|
+
entry = path.join(work, 'main.mjs');
|
|
142
|
+
await writeFile(entry, [
|
|
143
|
+
`import data from './native.gz' with {type: 'file'}`,
|
|
144
|
+
`import index from './native.json' with {type: 'file'}`,
|
|
145
|
+
`globalThis[Symbol.for(${JSON.stringify(BUN_NATIVE_ASSETS)})] = {data, index}`,
|
|
146
|
+
//Not imported: the bytecode of Bun is the one of CommonJS modules, without top-level await
|
|
147
|
+
`require(${JSON.stringify(`./${path.basename(bundle.outfile)}`)})`
|
|
148
|
+
].join('\n'));
|
|
149
|
+
}
|
|
150
|
+
await must(bun.command, [
|
|
151
|
+
'build', entry, '--compile', '--outfile', file,
|
|
152
|
+
//Compiled to the bytecode of JavaScriptCore: it starts without parsing the bundle
|
|
153
|
+
'--bytecode',
|
|
154
|
+
'--compile-autoload-package-json', '--no-compile-autoload-dotenv', '--no-compile-autoload-bunfig',
|
|
155
|
+
//The icon of the application (Windows)
|
|
156
|
+
...icon ? [`--windows-icon=${await writeIcon(work, icon)}`] : []
|
|
157
|
+
], work);
|
|
158
|
+
return `Bun ${bun.version}`;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Make the executable of the application for the platform it is made on, which runs without Node.js installed: its bundle
|
|
162
|
+
* embedded in the Node.js binary running the tool (the single executable applications of Node.js), or compiled by Bun.
|
|
163
|
+
* The executables of the other platforms are made on them (their native addons are built there), as the applications are
|
|
164
|
+
* built there
|
|
165
|
+
* @param options
|
|
166
|
+
*/
|
|
167
|
+
export async function buildExecutable(options) {
|
|
168
|
+
const project = path.resolve(options.project);
|
|
169
|
+
const outdir = path.resolve(project, options.outdir ?? 'dist');
|
|
170
|
+
//Bun is found before the application is bundled
|
|
171
|
+
const bun = options.runtime === 'bun' ? await findBun(project) : undefined;
|
|
172
|
+
const work = await mkdtemp(path.join(os.tmpdir(), 'lakutata-executable-'));
|
|
173
|
+
try {
|
|
174
|
+
const directory = options.layout === 'directory';
|
|
175
|
+
//The directory of the executable: dist/<name>, its native tree and its resources written there (the links of
|
|
176
|
+
//the tree are the ones of their directory), the one of the executable alone otherwise
|
|
177
|
+
const manifest = JSON.parse(await readFile(path.join(project, 'package.json'), 'utf8'));
|
|
178
|
+
const target = directory ? path.join(outdir, programName(manifest)) : outdir;
|
|
179
|
+
if (directory)
|
|
180
|
+
await rm(target, { recursive: true, force: true });
|
|
181
|
+
await mkdir(target, { recursive: true });
|
|
182
|
+
const bundle = await bundleProject({
|
|
183
|
+
...options,
|
|
184
|
+
outfile: path.join(work, 'main.cjs'),
|
|
185
|
+
native: directory ? 'directory' : 'embedded',
|
|
186
|
+
directories: { native: path.join(target, 'native'), resources: path.join(target, 'resources') }
|
|
187
|
+
});
|
|
188
|
+
//The executables of Node.js do not load the native addons on musl (postject rewrites their symbols): the ones of Bun do
|
|
189
|
+
if (!bun && detectLibc() === 'musl' && bundle.native?.files.some((file) => file.path.endsWith('.node')))
|
|
190
|
+
throw new Error('The executables of Node.js do not load native addons on musl (Alpine): make the executable with Bun (--runtime bun, or "runtime": "bun" of lakutata.installer)');
|
|
191
|
+
const file = path.join(target, process.platform === 'win32' ? `${bundle.name}.exe` : bundle.name);
|
|
192
|
+
await rm(file, { force: true });
|
|
193
|
+
//The native tree and the resources of the layout directory are not embedded
|
|
194
|
+
const embedded = directory ? { ...bundle, native: undefined } : bundle;
|
|
195
|
+
//Made and signed aside: codesign takes a directory holding "resources" for a bundle of macOS (its file system
|
|
196
|
+
//ignores the case), whose signature it writes beside the executable (_CodeSignature)
|
|
197
|
+
const made = path.join(work, 'executable', path.basename(file));
|
|
198
|
+
await mkdir(path.dirname(made), { recursive: true });
|
|
199
|
+
const warnings = [];
|
|
200
|
+
//The icon of the application: the one of the executables of Windows (the ones of macOS and Linux have none)
|
|
201
|
+
const application = applicationIcon(project, manifest);
|
|
202
|
+
const icon = application && process.platform === 'win32' ? icoFile(application.image) : undefined;
|
|
203
|
+
const runtime = bun ? await compileWithBun(bun, embedded, work, made, icon) : await embedInNode(embedded, work, made, warnings, icon);
|
|
204
|
+
await copyFile(made, file);
|
|
205
|
+
await chmod(file, 0o755);
|
|
206
|
+
return {
|
|
207
|
+
name: bundle.name,
|
|
208
|
+
file: file,
|
|
209
|
+
platform: `${process.platform === 'win32' ? 'win' : process.platform}-${process.arch}`,
|
|
210
|
+
size: (await stat(file)).size,
|
|
211
|
+
native: bundle.native?.modules ?? [],
|
|
212
|
+
resources: bundle.resources,
|
|
213
|
+
directory: directory ? target : undefined,
|
|
214
|
+
runtime: runtime,
|
|
215
|
+
warnings: warnings
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
await rm(work, { recursive: true, force: true });
|
|
220
|
+
}
|
|
221
|
+
}
|