@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
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The global symbol of the packed applications giving the directory of their resources (resourcesPath of lakutata)
|
|
3
|
+
*/
|
|
4
|
+
export const PACKAGED = 'lakutata.packaged';
|
|
5
|
+
/**
|
|
6
|
+
* The directory of the resources of the embedded trees
|
|
7
|
+
*/
|
|
8
|
+
export const EMBEDDED_RESOURCES = '.lakutata/resources';
|
|
9
|
+
/**
|
|
10
|
+
* The asset of the executables holding the files of the native tree (gzipped), and the one of its index
|
|
11
|
+
*/
|
|
12
|
+
export const NATIVE_DATA_ASSET = 'lakutata-native';
|
|
13
|
+
export const NATIVE_INDEX_ASSET = 'lakutata-native.json';
|
|
14
|
+
/**
|
|
15
|
+
* The global symbol of the executables of Bun giving the files of the native tree they embed: {data, index}, their paths
|
|
16
|
+
* in the executable
|
|
17
|
+
*/
|
|
18
|
+
export const BUN_NATIVE_ASSETS = 'lakutata.native';
|
|
19
|
+
/**
|
|
20
|
+
* The require of a bundle having native addons, which loads the native packages from the native tree: next to the bundle,
|
|
21
|
+
* or, in an executable, extracted from its assets once into the cache directory of the program (a directory named after
|
|
22
|
+
* the hash of the tree: the versions of the program do not share it). The assets are the ones of a single executable
|
|
23
|
+
* application of Node.js, or the files embedded in an executable of Bun. The other modules are loaded by the require of node.
|
|
24
|
+
* Its source is written at the top of the bundle: it uses nothing but its arguments and the modules of node
|
|
25
|
+
* @param nodeRequire
|
|
26
|
+
* @param config
|
|
27
|
+
*/
|
|
28
|
+
export function lakutataNativeRequire(nodeRequire, config) {
|
|
29
|
+
const path = nodeRequire('node:path');
|
|
30
|
+
const fs = nodeRequire('node:fs');
|
|
31
|
+
const os = nodeRequire('node:os');
|
|
32
|
+
const { createRequire } = nodeRequire('node:module');
|
|
33
|
+
let sea;
|
|
34
|
+
try {
|
|
35
|
+
sea = nodeRequire('node:sea');
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
sea = undefined;
|
|
39
|
+
}
|
|
40
|
+
//The files of the tree embedded in an executable of Bun (BUN_NATIVE_ASSETS)
|
|
41
|
+
const bun = globalThis[Symbol.for('lakutata.native')];
|
|
42
|
+
const asset = (name) => bun ? fs.readFileSync(bun[name]) : Buffer.from(sea.getAsset(name === 'data' ? 'lakutata-native' : 'lakutata-native.json'));
|
|
43
|
+
const modules = new Set(config.modules);
|
|
44
|
+
let root;
|
|
45
|
+
let treeRequire;
|
|
46
|
+
//The cache directory of the program: LAKUTATA_NATIVE_DIR, else the one of the platform
|
|
47
|
+
const cacheDirectory = () => {
|
|
48
|
+
if (process.env.LAKUTATA_NATIVE_DIR)
|
|
49
|
+
return process.env.LAKUTATA_NATIVE_DIR;
|
|
50
|
+
const home = os.homedir();
|
|
51
|
+
if (process.platform === 'darwin')
|
|
52
|
+
return path.join(home, 'Library', 'Caches', config.name);
|
|
53
|
+
if (process.platform === 'win32')
|
|
54
|
+
return path.join(process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'), config.name, 'Cache');
|
|
55
|
+
return path.join(process.env.XDG_CACHE_HOME || path.join(home, '.cache'), config.name);
|
|
56
|
+
};
|
|
57
|
+
//Extract the tree into a directory named after its hash: written aside, then renamed (the processes starting together
|
|
58
|
+
//do not see a tree partly written)
|
|
59
|
+
const extract = (base) => {
|
|
60
|
+
const directory = path.join(base, 'native', config.hash);
|
|
61
|
+
if (fs.existsSync(path.join(directory, '.complete')))
|
|
62
|
+
return directory;
|
|
63
|
+
const index = JSON.parse(asset('index').toString('utf8'));
|
|
64
|
+
const data = nodeRequire('node:zlib').gunzipSync(asset('data'));
|
|
65
|
+
const temporary = `${directory}.${process.pid}.${Date.now()}`;
|
|
66
|
+
const local = (root, file) => path.join(root, ...file.split('/'));
|
|
67
|
+
try {
|
|
68
|
+
for (const file of index.files) {
|
|
69
|
+
const target = local(temporary, file.path);
|
|
70
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
71
|
+
fs.writeFileSync(target, data.subarray(file.offset, file.offset + file.size), { mode: file.mode });
|
|
72
|
+
}
|
|
73
|
+
for (const link of index.links) {
|
|
74
|
+
const at = local(temporary, link.path);
|
|
75
|
+
fs.mkdirSync(path.dirname(at), { recursive: true });
|
|
76
|
+
//A junction of Windows points to its absolute path, the one of the tree once renamed
|
|
77
|
+
if (process.platform === 'win32')
|
|
78
|
+
fs.symlinkSync(local(directory, link.target), at, 'junction');
|
|
79
|
+
else
|
|
80
|
+
fs.symlinkSync(path.relative(path.dirname(at), local(temporary, link.target)), at, 'dir');
|
|
81
|
+
}
|
|
82
|
+
fs.writeFileSync(path.join(temporary, '.complete'), '');
|
|
83
|
+
try {
|
|
84
|
+
fs.renameSync(temporary, directory);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
//Another process extracted it
|
|
88
|
+
if (!fs.existsSync(path.join(directory, '.complete')))
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
fs.rmSync(temporary, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
return directory;
|
|
96
|
+
};
|
|
97
|
+
const locate = () => {
|
|
98
|
+
if (root)
|
|
99
|
+
return root;
|
|
100
|
+
if (config.embedded) {
|
|
101
|
+
try {
|
|
102
|
+
root = extract(cacheDirectory());
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
//The cache directory cannot be written: the temporary directory
|
|
106
|
+
if (process.env.LAKUTATA_NATIVE_DIR)
|
|
107
|
+
throw error;
|
|
108
|
+
root = extract(path.join(os.tmpdir(), config.name));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
//Next to the bundle, or in the resources of a packaged Electron application (outside of its archive)
|
|
113
|
+
const resources = process.resourcesPath;
|
|
114
|
+
const local = path.join(__dirname, config.directory);
|
|
115
|
+
root = fs.existsSync(local) || !resources ? local : path.join(resources, config.directory);
|
|
116
|
+
}
|
|
117
|
+
return root;
|
|
118
|
+
};
|
|
119
|
+
const fromTree = (id) => {
|
|
120
|
+
const parts = id.split('/');
|
|
121
|
+
return modules.has(id.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0]);
|
|
122
|
+
};
|
|
123
|
+
const tree = () => treeRequire ??= createRequire(path.join(locate(), 'index.js'));
|
|
124
|
+
const require = ((id) => fromTree(id) ? tree()(id) : nodeRequire(id));
|
|
125
|
+
require.resolve = ((id, options) => fromTree(id) ? tree().resolve(id, options) : nodeRequire.resolve(id, options));
|
|
126
|
+
require.cache = nodeRequire.cache;
|
|
127
|
+
require.main = nodeRequire.main;
|
|
128
|
+
require.extensions = nodeRequire.extensions;
|
|
129
|
+
//The addons of the application itself
|
|
130
|
+
require.lakutataNative = (key) => {
|
|
131
|
+
const addon = { exports: {} };
|
|
132
|
+
process.dlopen(addon, path.join(locate(), ...key.split('/')));
|
|
133
|
+
return addon.exports;
|
|
134
|
+
};
|
|
135
|
+
//The resources of an executable are extracted with the tree (PACKAGED, EMBEDDED_RESOURCES)
|
|
136
|
+
if (config.embedded)
|
|
137
|
+
globalThis[Symbol.for('lakutata.packaged')] = { resources: () => path.join(locate(), '.lakutata', 'resources') };
|
|
138
|
+
return require;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Load the environment file of the application into its environment, before it starts: the file given by
|
|
142
|
+
* LAKUTATA_ENV_FILE (the services of Windows, whose wrapper has none), its lines NAME=value or NAME="value" (the
|
|
143
|
+
* quoted values with their \\ escapes, as the installers write them), its comments (#). Its source is written at the top
|
|
144
|
+
* of the bundle: it uses nothing but its argument and the modules of node
|
|
145
|
+
* @param nodeRequire
|
|
146
|
+
*/
|
|
147
|
+
export function lakutataEnvironmentFile(nodeRequire) {
|
|
148
|
+
const file = process.env.LAKUTATA_ENV_FILE;
|
|
149
|
+
if (!file)
|
|
150
|
+
return;
|
|
151
|
+
const fs = nodeRequire('node:fs');
|
|
152
|
+
if (!fs.existsSync(file))
|
|
153
|
+
return;
|
|
154
|
+
for (const line of fs.readFileSync(file, 'utf8').split(/\r?\n/)) {
|
|
155
|
+
const matched = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/.exec(line);
|
|
156
|
+
if (!matched || line.trimStart().startsWith('#'))
|
|
157
|
+
continue;
|
|
158
|
+
const raw = matched[2];
|
|
159
|
+
process.env[matched[1]] = raw.startsWith('"') && raw.endsWith('"') && raw.length > 1 ? raw.slice(1, -1).replace(/\\(.)/g, '$1') : raw;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The source given to the top of every bundle: it is packed (isPackaged of lakutata), its resources are in a directory
|
|
164
|
+
* next to it, or in the resources of a packaged Electron application (outside of its archive); its environment file is
|
|
165
|
+
* loaded (LAKUTATA_ENV_FILE)
|
|
166
|
+
* @param directory the directory of the resources, relative to the bundle
|
|
167
|
+
*/
|
|
168
|
+
export function packagedBanner(directory) {
|
|
169
|
+
return `globalThis[Symbol.for(${JSON.stringify(PACKAGED)})]={resources:function(){var p=require('node:path'),l=p.join(__dirname,${JSON.stringify(directory)}),r=process.resourcesPath;return require('node:fs').existsSync(l)||!r?l:p.join(r,${JSON.stringify(directory)})}};(${lakutataEnvironmentFile.toString()})(require);\n`;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The source given to the top of a bundle: its require loads the native packages from the tree
|
|
173
|
+
* @param config
|
|
174
|
+
*/
|
|
175
|
+
export function nativeBanner(config) {
|
|
176
|
+
return `var require = (${lakutataNativeRequire.toString()})(require, ${JSON.stringify(config)});\n`;
|
|
177
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The C library of Linux: the executables made on it run on the systems of the same library (undefined out of Linux)
|
|
3
|
+
*/
|
|
4
|
+
export function detectLibc() {
|
|
5
|
+
if (process.platform !== 'linux')
|
|
6
|
+
return undefined;
|
|
7
|
+
const report = process.report?.getReport();
|
|
8
|
+
return report?.header?.glibcVersionRuntime ? 'glibc' : 'musl';
|
|
9
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* An argument of a command run by cmd.exe (Windows): quoted as the programs read their arguments
|
|
5
|
+
* @param arg
|
|
6
|
+
*/
|
|
7
|
+
function quoted(arg) {
|
|
8
|
+
if (/^[\w\-.:/\\=@,+]+$/.test(arg))
|
|
9
|
+
return arg;
|
|
10
|
+
return `"${arg.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1')}"`;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Spawn a command: on Windows, the commands found in the PATH by their names (the package managers, the commands of npm:
|
|
14
|
+
* npm.cmd, bun.cmd) are scripts, run by cmd.exe with their arguments quoted; the executables given by their paths (the
|
|
15
|
+
* ones of spaces: C:\Program Files\nodejs\node.exe) are run as they are
|
|
16
|
+
* @param command
|
|
17
|
+
* @param args
|
|
18
|
+
* @param options
|
|
19
|
+
*/
|
|
20
|
+
export function spawnCommand(command, args, options) {
|
|
21
|
+
if (process.platform === 'win32' && !path.isAbsolute(command))
|
|
22
|
+
return spawn([command, ...args].map(quoted).join(' '), { ...options, shell: true });
|
|
23
|
+
return spawn(command, args, options);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Run a command, its output is given back
|
|
27
|
+
* @param command
|
|
28
|
+
* @param args
|
|
29
|
+
* @param cwd
|
|
30
|
+
* @return the exit code and the output
|
|
31
|
+
*/
|
|
32
|
+
export function runCommand(command, args, cwd) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const child = spawnCommand(command, args, { cwd: cwd });
|
|
35
|
+
let output = '';
|
|
36
|
+
//Piped (the default stdio)
|
|
37
|
+
child.stdout.on('data', (chunk) => output += chunk.toString());
|
|
38
|
+
child.stderr.on('data', (chunk) => output += chunk.toString());
|
|
39
|
+
child.once('error', reject);
|
|
40
|
+
child.once('close', (code) => resolve({ code: code ?? 1, output: output }));
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* The moments of the installers the scripts of the applications are run at, the same on every system: before and after
|
|
6
|
+
* the installation (and the upgrades), before and after the removal (not the one of an upgrade)
|
|
7
|
+
*/
|
|
8
|
+
export const INSTALLER_HOOKS = ['preinstall', 'postinstall', 'preremove', 'postremove'];
|
|
9
|
+
/**
|
|
10
|
+
* The scripts of the packages of Linux, as nFPM runs them: the ones of each package, given its own arguments (the ones
|
|
11
|
+
* of dpkg, of rpm, of apk, of pacman); the upgrades of apk and pacman have their own ones
|
|
12
|
+
*/
|
|
13
|
+
export const NATIVE_HOOKS = ['preinstall', 'postinstall', 'preremove', 'postremove', 'preupgrade', 'postupgrade'];
|
|
14
|
+
/**
|
|
15
|
+
* The installers which run no script (archives, copied by their users)
|
|
16
|
+
*/
|
|
17
|
+
const ARCHIVES = ['tar.gz', 'zip'];
|
|
18
|
+
/**
|
|
19
|
+
* The moments the installers of lakutata run the scripts at: all of them, but the archives
|
|
20
|
+
* @param target
|
|
21
|
+
*/
|
|
22
|
+
function lakutataHooks(target) {
|
|
23
|
+
return ARCHIVES.includes(target) ? [] : INSTALLER_HOOKS;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A script of the project, read
|
|
27
|
+
* @param project
|
|
28
|
+
* @param file
|
|
29
|
+
* @param where its place in the configuration
|
|
30
|
+
*/
|
|
31
|
+
function read(project, file, where) {
|
|
32
|
+
if (typeof file !== 'string' || !file)
|
|
33
|
+
throw new Error(`The script ${where} must be a file of the project`);
|
|
34
|
+
const absolute = path.resolve(project, file);
|
|
35
|
+
if (!existsSync(absolute) || !statSync(absolute).isFile())
|
|
36
|
+
throw new Error(`The script ${where} (${file}) does not exist`);
|
|
37
|
+
//Without the mark of UTF-8, which the scripts are given with when they need it
|
|
38
|
+
return { file: file, content: readFileSync(absolute, 'utf8').replace(/^\ufeff/, '') };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The scripts of a moment, checked
|
|
42
|
+
* @param project
|
|
43
|
+
* @param scripts
|
|
44
|
+
* @param where
|
|
45
|
+
* @param windows
|
|
46
|
+
*/
|
|
47
|
+
function installerHooks(project, scripts, where, windows) {
|
|
48
|
+
if (scripts === undefined)
|
|
49
|
+
return {};
|
|
50
|
+
if (!scripts || typeof scripts !== 'object' || Array.isArray(scripts))
|
|
51
|
+
throw new Error(`The scripts ${where} must be {"preinstall": "<file>", "postinstall": "<file>", "preremove": "<file>", "postremove": "<file>"}`);
|
|
52
|
+
const hooks = {};
|
|
53
|
+
for (const [hook, file] of Object.entries(scripts)) {
|
|
54
|
+
if (!INSTALLER_HOOKS.includes(hook))
|
|
55
|
+
throw new Error(`The script ${where}.${hook} is not one of ${INSTALLER_HOOKS.join(', ')}`);
|
|
56
|
+
if (windows && typeof file === 'string' && !/\.ps1$/i.test(file))
|
|
57
|
+
throw new Error(`The script ${where}.${hook} (${file}) must be a script of PowerShell (.ps1)`);
|
|
58
|
+
hooks[hook] = read(project, file, `${where}.${hook}`);
|
|
59
|
+
}
|
|
60
|
+
return hooks;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The scripts of the installers made on a platform: the ones of the platform (lakutata.installer.<platform>.scripts),
|
|
64
|
+
* replaced by the ones of each installer (lakutata.installer.<platform>.<installer>.scripts), the native ones of the
|
|
65
|
+
* packages of Linux (nativeScripts), the file of NSIS included by its installer (win.nsis.include). The ones an
|
|
66
|
+
* installer does not run are told (warnings)
|
|
67
|
+
* @param project
|
|
68
|
+
* @param platform its name in the configuration: linux, mac or win
|
|
69
|
+
* @param config its configuration
|
|
70
|
+
* @param targets the installers made
|
|
71
|
+
* @param warnings
|
|
72
|
+
* @param hooks the moments each installer runs the scripts at (the installers of lakutata by default)
|
|
73
|
+
* @param native whether the packages of Linux run native scripts (the ones of lakutata)
|
|
74
|
+
*/
|
|
75
|
+
export function resolveScripts(project, platform, config, targets, warnings, hooks = lakutataHooks, native = true) {
|
|
76
|
+
const windows = platform === 'win';
|
|
77
|
+
const where = `lakutata.installer.${platform}`;
|
|
78
|
+
const common = installerHooks(project, config?.scripts, `${where}.scripts`, windows);
|
|
79
|
+
const resolved = {};
|
|
80
|
+
const unrun = [];
|
|
81
|
+
for (const target of targets) {
|
|
82
|
+
const options = config?.[target];
|
|
83
|
+
if (options !== undefined && (!options || typeof options !== 'object' || Array.isArray(options)))
|
|
84
|
+
throw new Error(`${where}.${target} must be an object`);
|
|
85
|
+
const own = (options ?? {});
|
|
86
|
+
const scripts = { hooks: { ...common, ...installerHooks(project, own.scripts, `${where}.${target}.scripts`, windows) }, native: {} };
|
|
87
|
+
if (own.nativeScripts !== undefined) {
|
|
88
|
+
if (!native || platform !== 'linux' || ARCHIVES.includes(target))
|
|
89
|
+
throw new Error(`${where}.${target}.nativeScripts: the native scripts are the ones of the packages of Linux lakutata makes (deb, rpm, apk, archlinux), not the ones of Electron`);
|
|
90
|
+
if (!own.nativeScripts || typeof own.nativeScripts !== 'object' || Array.isArray(own.nativeScripts))
|
|
91
|
+
throw new Error(`${where}.${target}.nativeScripts must be {"<script>": "<file>"}`);
|
|
92
|
+
//The upgrades of deb and rpm run their scripts of the installation and of the removal
|
|
93
|
+
const names = target === 'apk' || target === 'archlinux' ? NATIVE_HOOKS : INSTALLER_HOOKS;
|
|
94
|
+
for (const [hook, file] of Object.entries(own.nativeScripts)) {
|
|
95
|
+
if (!names.includes(hook))
|
|
96
|
+
throw new Error(`${where}.${target}.nativeScripts.${hook} is not a script of ${target} (${names.join(', ')})`);
|
|
97
|
+
scripts.native[hook] = read(project, file, `${where}.${target}.nativeScripts.${hook}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (own.include !== undefined) {
|
|
101
|
+
if (!windows || target !== 'nsis')
|
|
102
|
+
throw new Error(`${where}.${target}.include: the file of NSIS is included by the installer of NSIS (win.nsis.include)`);
|
|
103
|
+
if (typeof own.include === 'string' && !/\.nsh$/i.test(own.include))
|
|
104
|
+
throw new Error(`${where}.nsis.include (${own.include}) must be a file of NSIS (.nsh)`);
|
|
105
|
+
scripts.include = read(project, own.include, `${where}.nsis.include`);
|
|
106
|
+
}
|
|
107
|
+
//The ones the installer does not run
|
|
108
|
+
const run = hooks(target);
|
|
109
|
+
const skipped = INSTALLER_HOOKS.filter((hook) => !!scripts.hooks[hook] && !run.includes(hook));
|
|
110
|
+
for (const hook of skipped)
|
|
111
|
+
delete scripts.hooks[hook];
|
|
112
|
+
if (skipped.length)
|
|
113
|
+
unrun.push(`${target} (${skipped.join(', ')})`);
|
|
114
|
+
resolved[target] = scripts;
|
|
115
|
+
}
|
|
116
|
+
if (unrun.length)
|
|
117
|
+
warnings.push(`Some scripts of the application are not run by their installers: ${unrun.join(', ')}`);
|
|
118
|
+
return resolved;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The lines of sh running a script embedded in them (the scripts run before the files of the application are installed,
|
|
122
|
+
* or after they are removed): written to a temporary file, run by the interpreter of its first line (a temporary file
|
|
123
|
+
* system may not run its files), its failure the one of the script running it
|
|
124
|
+
* @param script
|
|
125
|
+
* @param prefix the directory of the application (LAKUTATA_INSTALL_DIR)
|
|
126
|
+
* @param args the arguments given to it (sh)
|
|
127
|
+
*/
|
|
128
|
+
export function embeddedShell(script, prefix, args) {
|
|
129
|
+
const first = script.content.split('\n', 1)[0];
|
|
130
|
+
const interpreter = first.startsWith('#!') ? first.slice(2).trim() : '/bin/sh';
|
|
131
|
+
//Not a line of the script
|
|
132
|
+
const tag = `LAKUTATA_SCRIPT_${createHash('sha1').update(script.content).digest('hex').slice(0, 12).toUpperCase()}`;
|
|
133
|
+
const content = script.content.replace(/\r\n/g, '\n');
|
|
134
|
+
return [
|
|
135
|
+
`# ${script.file}`,
|
|
136
|
+
'lakutata_script="$(mktemp)"',
|
|
137
|
+
`cat > "$lakutata_script" <<'${tag}'`,
|
|
138
|
+
...content.endsWith('\n') ? content.slice(0, -1).split('\n') : content.split('\n'),
|
|
139
|
+
tag,
|
|
140
|
+
`LAKUTATA_INSTALL_DIR='${prefix.replace(/'/g, `'\\''`)}' ${interpreter} "$lakutata_script" ${args} || { lakutata_status=$?; rm -f "$lakutata_script"; exit $lakutata_status; }`,
|
|
141
|
+
'rm -f "$lakutata_script"'
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The lines of sh running a script encoded in them (base64): the scripts of the packages of electron-builder are
|
|
146
|
+
* templates, which replace their ${...}
|
|
147
|
+
* @param script
|
|
148
|
+
* @param prefix the directory of the application (LAKUTATA_INSTALL_DIR)
|
|
149
|
+
* @param args the arguments given to it (sh)
|
|
150
|
+
*/
|
|
151
|
+
export function encodedShell(script, prefix, args) {
|
|
152
|
+
const first = script.content.split('\n', 1)[0];
|
|
153
|
+
const interpreter = first.startsWith('#!') ? first.slice(2).trim() : '/bin/sh';
|
|
154
|
+
return [
|
|
155
|
+
`# ${script.file}`,
|
|
156
|
+
'lakutata_script="$(mktemp)"',
|
|
157
|
+
`printf '%s' '${Buffer.from(script.content.replace(/\r\n/g, '\n'), 'utf8').toString('base64')}' | base64 -d > "$lakutata_script"`,
|
|
158
|
+
`LAKUTATA_INSTALL_DIR='${prefix.replace(/'/g, `'\\''`)}' ${interpreter} "$lakutata_script" ${args} || { lakutata_status=$?; rm -f "$lakutata_script"; exit $lakutata_status; }`,
|
|
159
|
+
'rm -f "$lakutata_script"'
|
|
160
|
+
];
|
|
161
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { chmod, mkdir, mkdtemp, rename, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { runCommand } from './Processes.js';
|
|
7
|
+
/**
|
|
8
|
+
* The tools of Windows: the same files on x64 and arm64 (x86 and .NET, emulated or AnyCPU)
|
|
9
|
+
* @param archive
|
|
10
|
+
* @param sha256
|
|
11
|
+
*/
|
|
12
|
+
function windows(archive, sha256) {
|
|
13
|
+
return { 'win32-x64': { archive: archive, sha256: sha256 }, 'win32-arm64': { archive: archive, sha256: sha256 } };
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The tools of the builder
|
|
17
|
+
*/
|
|
18
|
+
const TOOLS = {
|
|
19
|
+
//The packages of Linux (deb, rpm, apk, archlinux), made on Linux: https://nfpm.goreleaser.com
|
|
20
|
+
nfpm: {
|
|
21
|
+
version: '2.47.0',
|
|
22
|
+
executable: 'nfpm',
|
|
23
|
+
url: (archive) => `https://github.com/goreleaser/nfpm/releases/download/v2.47.0/${archive}`,
|
|
24
|
+
archives: {
|
|
25
|
+
'linux-x64': { archive: 'nfpm_2.47.0_Linux_x86_64.tar.gz', sha256: '0660ca602b2d2d2ae4781a06c692b3eeb9d437ffea05b831d76e41f4a3188783' },
|
|
26
|
+
'linux-arm64': { archive: 'nfpm_2.47.0_Linux_arm64.tar.gz', sha256: '1c0f5f2999b9a974bfb04fdb0cc3306096de530ac5dbb25d739cc5f5219c919c' }
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
//The wrapper of the services of Windows (.NET Framework, AnyCPU): https://github.com/winsw/winsw
|
|
30
|
+
winsw: {
|
|
31
|
+
version: '2.12.0',
|
|
32
|
+
executable: 'WinSW.NET461.exe',
|
|
33
|
+
url: (archive) => `https://github.com/winsw/winsw/releases/download/v2.12.0/${archive}`,
|
|
34
|
+
archives: windows('WinSW.NET461.exe', 'b5066b7bbdfba1293e5d15cda3caaea88fbeab35bd5b38c41c913d492aadfc4f')
|
|
35
|
+
},
|
|
36
|
+
//The installers of Windows (.exe): https://nsis.sourceforge.io
|
|
37
|
+
nsis: {
|
|
38
|
+
version: '3.13',
|
|
39
|
+
executable: 'nsis-3.13/makensis.exe',
|
|
40
|
+
url: (archive) => `https://sourceforge.net/projects/nsis/files/NSIS%203/3.13/${archive}/download`,
|
|
41
|
+
archives: windows('nsis-3.13.zip', 'ba63dffc4410ee89193e1cb5a41989991bd77c61068da17e3156d136b7b0b3d8')
|
|
42
|
+
},
|
|
43
|
+
//The packages of Windows Installer (.msi), candle and light: https://github.com/wixtoolset/wix3
|
|
44
|
+
wix: {
|
|
45
|
+
version: '3.14.1',
|
|
46
|
+
executable: 'candle.exe',
|
|
47
|
+
url: (archive) => `https://github.com/wixtoolset/wix3/releases/download/wix3141rtm/${archive}`,
|
|
48
|
+
archives: windows('wix314-binaries.zip', '6ac824e1642d6f7277d0ed7ea09411a508f6116ba6fae0aa5f2c7daa2ff43d31')
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* The directory of the cache of lakutata (the one of its tool): LAKUTATA_CACHE_DIR, else ~/.cache/lakutata
|
|
53
|
+
*/
|
|
54
|
+
export function cacheDirectory() {
|
|
55
|
+
return process.env.LAKUTATA_CACHE_DIR ?? path.join(process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), '.cache'), 'lakutata');
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* A tool of the builder: the one given by its environment variable (LAKUTATA_NFPM, LAKUTATA_WINSW, LAKUTATA_NSIS:
|
|
59
|
+
* makensis.exe, LAKUTATA_WIX: candle.exe, next to light.exe), else the version of the builder, downloaded once into the
|
|
60
|
+
* cache and checked with its sha256
|
|
61
|
+
* @param name
|
|
62
|
+
*/
|
|
63
|
+
export async function ensureTool(name) {
|
|
64
|
+
const given = process.env[`LAKUTATA_${name.toUpperCase()}`];
|
|
65
|
+
if (given)
|
|
66
|
+
return given;
|
|
67
|
+
const tool = TOOLS[name];
|
|
68
|
+
const platform = `${process.platform}-${process.arch}`;
|
|
69
|
+
const release = tool.archives[platform];
|
|
70
|
+
if (!release)
|
|
71
|
+
throw new Error(`${name} ${tool.version} has no release for ${platform}: give its executable (LAKUTATA_${name.toUpperCase()})`);
|
|
72
|
+
const directory = path.join(cacheDirectory(), 'tools', `${name}-${tool.version}-${platform}`);
|
|
73
|
+
const executable = path.join(directory, ...tool.executable.split('/'));
|
|
74
|
+
if (existsSync(executable))
|
|
75
|
+
return executable;
|
|
76
|
+
//SourceForge answers the browsers with its page, the tools with the file
|
|
77
|
+
const response = await fetch(tool.url(release.archive), { headers: { 'user-agent': 'Wget' } }).catch((error) => {
|
|
78
|
+
throw new Error(`${name} cannot be downloaded (${error.message}): give its executable (LAKUTATA_${name.toUpperCase()})`);
|
|
79
|
+
});
|
|
80
|
+
if (!response.ok)
|
|
81
|
+
throw new Error(`${name} cannot be downloaded (${tool.url(release.archive)}: ${response.status}): give its executable (LAKUTATA_${name.toUpperCase()})`);
|
|
82
|
+
const archive = Buffer.from(await response.arrayBuffer());
|
|
83
|
+
const sha256 = createHash('sha256').update(archive).digest('hex');
|
|
84
|
+
if (sha256 !== release.sha256)
|
|
85
|
+
throw new Error(`The archive of ${name} ${tool.version} downloaded is not the one of its release (sha256 ${sha256})`);
|
|
86
|
+
//Extracted aside, then renamed: the builds running together do not see it partly written
|
|
87
|
+
await mkdir(path.dirname(directory), { recursive: true });
|
|
88
|
+
const temporary = await mkdtemp(`${directory}.`);
|
|
89
|
+
try {
|
|
90
|
+
await writeFile(path.join(temporary, release.archive), archive);
|
|
91
|
+
//An archive (tar.gz, zip: the tar of Windows reads them), or the executable itself
|
|
92
|
+
if (/\.(tar\.gz|zip)$/.test(release.archive)) {
|
|
93
|
+
const extracted = await runCommand('tar', ['-xf', release.archive], temporary);
|
|
94
|
+
if (extracted.code !== 0)
|
|
95
|
+
throw new Error(`The archive of ${name} cannot be extracted: ${extracted.output.trim()}`);
|
|
96
|
+
await rm(path.join(temporary, release.archive));
|
|
97
|
+
}
|
|
98
|
+
await chmod(path.join(temporary, ...tool.executable.split('/')), 0o755);
|
|
99
|
+
await rename(temporary, directory).catch((error) => {
|
|
100
|
+
//Extracted by another build
|
|
101
|
+
if (!existsSync(executable))
|
|
102
|
+
throw error;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
await rm(temporary, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
return executable;
|
|
109
|
+
}
|