@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.
@@ -0,0 +1,372 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { deflateSync, inflateSync } from 'node:zlib';
5
+ /**
6
+ * The signature of the PNG files
7
+ */
8
+ const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
9
+ /**
10
+ * The table of the CRC-32 of the chunks of PNG
11
+ */
12
+ const CRC_TABLE = (() => {
13
+ const table = new Uint32Array(256);
14
+ for (let n = 0; n < 256; n++) {
15
+ let c = n;
16
+ for (let k = 0; k < 8; k++)
17
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
18
+ table[n] = c >>> 0;
19
+ }
20
+ return table;
21
+ })();
22
+ /**
23
+ * The CRC-32 of a chunk of PNG (its type and its data)
24
+ * @param data
25
+ */
26
+ function crc32(data) {
27
+ let crc = 0xffffffff;
28
+ for (const byte of data)
29
+ crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
30
+ return (crc ^ 0xffffffff) >>> 0;
31
+ }
32
+ /**
33
+ * The predictor of Paeth (the filter 4 of PNG)
34
+ * @param a the byte on the left
35
+ * @param b the byte above
36
+ * @param c the byte above on the left
37
+ */
38
+ function paeth(a, b, c) {
39
+ const p = a + b - c;
40
+ const pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c);
41
+ return pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
42
+ }
43
+ /**
44
+ * Decode a PNG file (not interlaced): its pixels in RGBA, whatever its colors (gray, RGB, palette, with alpha) and its
45
+ * depth (1 to 16 bits)
46
+ * @param data
47
+ * @param file its name, in the errors
48
+ */
49
+ export function decodePng(data, file = 'the image') {
50
+ if (data.length < 8 || !data.subarray(0, 8).equals(PNG_SIGNATURE))
51
+ throw new Error(`${file} is not a PNG image`);
52
+ let offset = 8;
53
+ let width = 0, height = 0, depth = 0, type = 0, interlace = 0;
54
+ let palette;
55
+ let transparency;
56
+ const compressed = [];
57
+ while (offset + 8 <= data.length) {
58
+ const length = data.readUInt32BE(offset);
59
+ const name = data.toString('latin1', offset + 4, offset + 8);
60
+ const chunk = data.subarray(offset + 8, offset + 8 + length);
61
+ offset += 12 + length;
62
+ if (name === 'IHDR') {
63
+ width = chunk.readUInt32BE(0);
64
+ height = chunk.readUInt32BE(4);
65
+ depth = chunk[8];
66
+ type = chunk[9];
67
+ interlace = chunk[12];
68
+ }
69
+ else if (name === 'PLTE')
70
+ palette = chunk;
71
+ else if (name === 'tRNS')
72
+ transparency = chunk;
73
+ else if (name === 'IDAT')
74
+ compressed.push(chunk);
75
+ else if (name === 'IEND')
76
+ break;
77
+ }
78
+ if (!width || !height)
79
+ throw new Error(`${file} has no header (IHDR)`);
80
+ if (interlace)
81
+ throw new Error(`${file} is interlaced: save it without interlacing`);
82
+ const channels = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 }[type];
83
+ if (!channels || ![1, 2, 4, 8, 16].includes(depth))
84
+ throw new Error(`${file} has colors this tool does not read (type ${type}, ${depth} bits)`);
85
+ if (type === 3 && !palette)
86
+ throw new Error(`${file} has no palette (PLTE)`);
87
+ const raw = inflateSync(Buffer.concat(compressed));
88
+ const bitsPerPixel = channels * depth;
89
+ const stride = Math.ceil(width * bitsPerPixel / 8);
90
+ //The distance of the byte on the left: one pixel, at least one byte
91
+ const left = Math.max(1, bitsPerPixel >> 3);
92
+ const rows = Buffer.alloc(stride * height);
93
+ for (let y = 0; y < height; y++) {
94
+ const filter = raw[y * (stride + 1)];
95
+ const source = raw.subarray(y * (stride + 1) + 1, (y + 1) * (stride + 1));
96
+ const row = rows.subarray(y * stride, (y + 1) * stride);
97
+ const above = y ? rows.subarray((y - 1) * stride, y * stride) : undefined;
98
+ for (let x = 0; x < stride; x++) {
99
+ const a = x >= left ? row[x - left] : 0;
100
+ const b = above ? above[x] : 0;
101
+ const c = above && x >= left ? above[x - left] : 0;
102
+ const predicted = filter === 0 ? 0 : filter === 1 ? a : filter === 2 ? b : filter === 3 ? (a + b) >> 1 : filter === 4 ? paeth(a, b, c) : -1;
103
+ if (predicted < 0)
104
+ throw new Error(`${file} has an unknown filter (${filter})`);
105
+ row[x] = (source[x] + predicted) & 0xff;
106
+ }
107
+ }
108
+ //A sample of a pixel, as it is written, and scaled to 8 bits
109
+ const value = (row, index) => {
110
+ if (depth === 8)
111
+ return row[index];
112
+ if (depth === 16)
113
+ return row.readUInt16BE(index * 2);
114
+ return row[(index * depth) >> 3] >> (8 - depth - ((index * depth) & 7)) & ((1 << depth) - 1);
115
+ };
116
+ const sample = (row, index) => {
117
+ if (depth === 8 || type === 3)
118
+ return value(row, index);
119
+ if (depth === 16)
120
+ return row[index * 2];
121
+ return Math.round(value(row, index) * 255 / ((1 << depth) - 1));
122
+ };
123
+ //The color made transparent by tRNS, of the gray and RGB images
124
+ const key = (index) => transparency && transparency.length >= (index + 1) * 2 ? transparency.readUInt16BE(index * 2) : -1;
125
+ const pixels = Buffer.alloc(width * height * 4);
126
+ for (let y = 0; y < height; y++) {
127
+ const row = rows.subarray(y * stride, (y + 1) * stride);
128
+ for (let x = 0; x < width; x++) {
129
+ const target = (y * width + x) * 4;
130
+ const raw16 = (channel) => value(row, x * channels + channel);
131
+ if (type === 3) {
132
+ const index = sample(row, x);
133
+ palette.copy(pixels, target, index * 3, index * 3 + 3);
134
+ pixels[target + 3] = transparency && index < transparency.length ? transparency[index] : 255;
135
+ }
136
+ else if (type === 0 || type === 4) {
137
+ const gray = sample(row, x * channels);
138
+ pixels[target] = pixels[target + 1] = pixels[target + 2] = gray;
139
+ pixels[target + 3] = type === 4 ? sample(row, x * channels + 1) : raw16(0) === key(0) ? 0 : 255;
140
+ }
141
+ else {
142
+ for (let channel = 0; channel < 3; channel++)
143
+ pixels[target + channel] = sample(row, x * channels + channel);
144
+ pixels[target + 3] = type === 6 ? sample(row, x * channels + 3) : raw16(0) === key(0) && raw16(1) === key(1) && raw16(2) === key(2) ? 0 : 255;
145
+ }
146
+ }
147
+ }
148
+ return { width: width, height: height, pixels: pixels };
149
+ }
150
+ /**
151
+ * Encode an image into a PNG file (RGBA, 8 bits)
152
+ * @param image
153
+ */
154
+ export function encodePng(image) {
155
+ const chunk = (name, data) => {
156
+ const typed = Buffer.concat([Buffer.from(name, 'latin1'), data]);
157
+ const length = Buffer.alloc(4);
158
+ length.writeUInt32BE(data.length);
159
+ const crc = Buffer.alloc(4);
160
+ crc.writeUInt32BE(crc32(typed));
161
+ return Buffer.concat([length, typed, crc]);
162
+ };
163
+ const header = Buffer.alloc(13);
164
+ header.writeUInt32BE(image.width, 0);
165
+ header.writeUInt32BE(image.height, 4);
166
+ header[8] = 8;
167
+ header[9] = 6;
168
+ const stride = image.width * 4;
169
+ const raw = Buffer.alloc((stride + 1) * image.height);
170
+ for (let y = 0; y < image.height; y++) {
171
+ //The filter Sub (1): the icons compress better
172
+ raw[y * (stride + 1)] = 1;
173
+ for (let x = 0; x < stride; x++) {
174
+ const value = image.pixels[y * stride + x];
175
+ raw[y * (stride + 1) + 1 + x] = (value - (x >= 4 ? image.pixels[y * stride + x - 4] : 0)) & 0xff;
176
+ }
177
+ }
178
+ return Buffer.concat([PNG_SIGNATURE, chunk('IHDR', header), chunk('IDAT', deflateSync(raw, { level: 9 })), chunk('IEND', Buffer.alloc(0))]);
179
+ }
180
+ /**
181
+ * Resize an image (a square into a square): each pixel the average of the ones it covers, weighted by their alpha
182
+ * (premultiplied, the transparent edges keep their colors)
183
+ * @param image
184
+ * @param size
185
+ */
186
+ export function resize(image, size) {
187
+ if (image.width === size && image.height === size)
188
+ return image;
189
+ //Separable: the columns, then the rows
190
+ const pass = (source, width, height, target, horizontal) => {
191
+ const length = horizontal ? width : height;
192
+ const scale = length / target;
193
+ const output = new Float64Array((horizontal ? target * height : width * target) * 4);
194
+ for (let i = 0; i < target; i++) {
195
+ const start = i * scale, end = (i + 1) * scale;
196
+ //The weights of the source pixels the target pixel covers
197
+ const weights = [];
198
+ for (let j = Math.floor(start); j < Math.min(length, Math.ceil(end)); j++) {
199
+ const weight = Math.min(end, j + 1) - Math.max(start, j);
200
+ if (weight > 0)
201
+ weights.push([j, weight]);
202
+ }
203
+ const total = weights.reduce((sum, [, weight]) => sum + weight, 0);
204
+ for (let k = 0; k < (horizontal ? height : width); k++) {
205
+ for (let channel = 0; channel < 4; channel++) {
206
+ let sum = 0;
207
+ for (const [j, weight] of weights)
208
+ sum += source[((horizontal ? k * width + j : j * width + k) * 4) + channel] * weight;
209
+ output[((horizontal ? k * target + i : i * width + k) * 4) + channel] = sum / total;
210
+ }
211
+ }
212
+ }
213
+ return output;
214
+ };
215
+ const premultiplied = new Float64Array(image.width * image.height * 4);
216
+ for (let i = 0; i < image.width * image.height; i++) {
217
+ const alpha = image.pixels[i * 4 + 3] / 255;
218
+ for (let channel = 0; channel < 3; channel++)
219
+ premultiplied[i * 4 + channel] = image.pixels[i * 4 + channel] * alpha;
220
+ premultiplied[i * 4 + 3] = image.pixels[i * 4 + 3];
221
+ }
222
+ const resized = pass(pass(premultiplied, image.width, image.height, size, true), size, image.height, size, false);
223
+ const pixels = Buffer.alloc(size * size * 4);
224
+ for (let i = 0; i < size * size; i++) {
225
+ const alpha = resized[i * 4 + 3];
226
+ for (let channel = 0; channel < 3; channel++)
227
+ pixels[i * 4 + channel] = alpha > 0 ? Math.min(255, Math.round(resized[i * 4 + channel] * 255 / alpha)) : 0;
228
+ pixels[i * 4 + 3] = Math.min(255, Math.round(alpha));
229
+ }
230
+ return { width: size, height: size, pixels: pixels };
231
+ }
232
+ /**
233
+ * The sizes of the icons of Windows (ico)
234
+ */
235
+ export const ICO_SIZES = [16, 20, 24, 32, 40, 48, 64, 128, 256];
236
+ /**
237
+ * The icon of Windows (ico) of an image, in its sizes: the bitmaps of the ones smaller than 256 (the format every tool
238
+ * reads: the resources of the executables, NSIS, Windows Installer), the PNG of 256
239
+ * @param image
240
+ */
241
+ export function icoFile(image) {
242
+ const images = ICO_SIZES.map((size) => {
243
+ const resized = resize(image, size);
244
+ if (size >= 256)
245
+ return encodePng(resized);
246
+ //BITMAPINFOHEADER, its height the one of the colors and of the mask; the rows from the bottom, BGRA, then the mask
247
+ const header = Buffer.alloc(40);
248
+ header.writeUInt32LE(40, 0);
249
+ header.writeInt32LE(size, 4);
250
+ header.writeInt32LE(size * 2, 8);
251
+ header.writeUInt16LE(1, 12);
252
+ header.writeUInt16LE(32, 14);
253
+ const colors = Buffer.alloc(size * size * 4);
254
+ const maskStride = Math.ceil(size / 32) * 4;
255
+ const mask = Buffer.alloc(maskStride * size);
256
+ for (let y = 0; y < size; y++) {
257
+ for (let x = 0; x < size; x++) {
258
+ const from = (y * size + x) * 4;
259
+ const to = ((size - 1 - y) * size + x) * 4;
260
+ colors[to] = resized.pixels[from + 2];
261
+ colors[to + 1] = resized.pixels[from + 1];
262
+ colors[to + 2] = resized.pixels[from];
263
+ colors[to + 3] = resized.pixels[from + 3];
264
+ if (!resized.pixels[from + 3])
265
+ mask[(size - 1 - y) * maskStride + (x >> 3)] |= 0x80 >> (x & 7);
266
+ }
267
+ }
268
+ return Buffer.concat([header, colors, mask]);
269
+ });
270
+ const directory = Buffer.alloc(6 + 16 * images.length);
271
+ directory.writeUInt16LE(1, 2);
272
+ directory.writeUInt16LE(images.length, 4);
273
+ let offset = directory.length;
274
+ images.forEach((data, index) => {
275
+ const entry = 6 + index * 16;
276
+ const size = ICO_SIZES[index];
277
+ directory[entry] = size >= 256 ? 0 : size;
278
+ directory[entry + 1] = size >= 256 ? 0 : size;
279
+ directory.writeUInt16LE(1, entry + 4);
280
+ directory.writeUInt16LE(32, entry + 6);
281
+ directory.writeUInt32LE(data.length, entry + 8);
282
+ directory.writeUInt32LE(offset, entry + 12);
283
+ offset += data.length;
284
+ });
285
+ return Buffer.concat([directory, ...images]);
286
+ }
287
+ /**
288
+ * The types of the icons of macOS (icns) of PNG, by their sizes in pixels (the Retina ones are the doubles of the others)
289
+ */
290
+ const ICNS_TYPES = [['icp4', 16], ['icp5', 32], ['ic11', 32], ['icp6', 64], ['ic12', 64], ['ic07', 128], ['ic08', 256], ['ic13', 256], ['ic09', 512], ['ic14', 512], ['ic10', 1024]];
291
+ /**
292
+ * The icon of macOS (icns) of an image: its PNG in the sizes of macOS, but the ones larger than it
293
+ * @param image
294
+ */
295
+ export function icnsFile(image) {
296
+ const encoded = new Map();
297
+ const entries = [];
298
+ for (const [type, size] of ICNS_TYPES) {
299
+ if (size > image.width)
300
+ continue;
301
+ if (!encoded.has(size))
302
+ encoded.set(size, encodePng(resize(image, size)));
303
+ const data = encoded.get(size);
304
+ const header = Buffer.alloc(8);
305
+ header.write(type, 0, 'latin1');
306
+ header.writeUInt32BE(data.length + 8, 4);
307
+ entries.push(header, data);
308
+ }
309
+ const header = Buffer.alloc(8);
310
+ header.write('icns', 0, 'latin1');
311
+ header.writeUInt32BE(8 + entries.reduce((sum, entry) => sum + entry.length, 0), 4);
312
+ return Buffer.concat([header, ...entries]);
313
+ }
314
+ /**
315
+ * The sizes of the icons of Linux (the hicolor theme)
316
+ */
317
+ export const LINUX_SIZES = [16, 24, 32, 48, 64, 128, 256, 512];
318
+ /**
319
+ * Write the icons of Linux of an image into a directory: its PNG in the sizes of the hicolor theme (<size>x<size>.png),
320
+ * but the ones larger than it
321
+ * @param image
322
+ * @param directory
323
+ */
324
+ export async function writeLinuxIcons(image, directory) {
325
+ await mkdir(directory, { recursive: true });
326
+ const files = [];
327
+ for (const size of LINUX_SIZES.filter((size) => size <= image.width)) {
328
+ const file = path.join(directory, `${size}x${size}.png`);
329
+ await writeFile(file, encodePng(resize(image, size)));
330
+ files.push(file);
331
+ }
332
+ return files;
333
+ }
334
+ /**
335
+ * Give an executable of Windows the icon (ico), as its icon groups (the one of node.exe), its signature removed (it does
336
+ * not match it anymore)
337
+ * @param file
338
+ * @param ico
339
+ */
340
+ export async function setExecutableIcon(file, ico) {
341
+ const { NtExecutable, NtExecutableResource, Data, Resource } = await import('resedit');
342
+ const executable = NtExecutable.from(await readFile(file), { ignoreCert: true });
343
+ const resource = NtExecutableResource.from(executable);
344
+ const icons = Data.IconFile.from(ico).icons.map((item) => item.data);
345
+ const groups = Resource.IconGroupEntry.fromEntries(resource.entries);
346
+ for (const { id, lang } of groups.length ? groups : [{ id: 1, lang: 1033 }])
347
+ Resource.IconGroupEntry.replaceIconsForResource(resource.entries, id, lang, icons);
348
+ resource.outputResource(executable);
349
+ await writeFile(file, Buffer.from(executable.generate()));
350
+ }
351
+ /**
352
+ * The icon of the application (lakutata.icon of its package.json): a square PNG image, 256 pixels at least (1024 for
353
+ * the Retina screens of macOS), undefined without icon
354
+ * @param project
355
+ * @param manifest
356
+ */
357
+ export function applicationIcon(project, manifest) {
358
+ const declared = manifest.lakutata?.icon;
359
+ if (declared === undefined)
360
+ return undefined;
361
+ if (typeof declared !== 'string' || !declared)
362
+ throw new Error('The icon of the application (lakutata.icon of its package.json) must be a PNG image of the project');
363
+ const file = path.resolve(project, declared);
364
+ if (!existsSync(file))
365
+ throw new Error(`The icon of the application ${declared} (lakutata.icon) does not exist`);
366
+ const image = decodePng(readFileSync(file), declared);
367
+ if (image.width !== image.height)
368
+ throw new Error(`The icon of the application ${declared} must be square (it is ${image.width}x${image.height})`);
369
+ if (image.width < 256)
370
+ throw new Error(`The icon of the application ${declared} must be 256 pixels at least (it is ${image.width}): its icons are made from it`);
371
+ return { file: file, image: image };
372
+ }
@@ -0,0 +1,158 @@
1
+ import { readFile, mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { programName } from './Bundle.js';
5
+ import { buildElectronInstaller } from './electron/Electron.js';
6
+ import { buildExecutable } from './Executable.js';
7
+ import { applicationIcon, icoFile } from './Icons.js';
8
+ import { detectLibc } from './Platform.js';
9
+ import { resolveScripts } from './Scripts.js';
10
+ import { buildLinuxPackages, LINUX_TARGETS } from './linux/Linux.js';
11
+ import { buildMacPackages, MAC_TARGETS } from './mac/Mac.js';
12
+ import { buildWindowsPackages, WINDOWS_TARGETS } from './win/Windows.js';
13
+ /**
14
+ * The maintainer of the package.json: its author
15
+ * @param author
16
+ */
17
+ function packageAuthor(author) {
18
+ if (typeof author === 'string' && author.trim())
19
+ return author.trim();
20
+ if (author && typeof author === 'object' && typeof author.name === 'string') {
21
+ const { name, email } = author;
22
+ return email ? `${name} <${email}>` : name;
23
+ }
24
+ return undefined;
25
+ }
26
+ /**
27
+ * The configuration of the installers of a project, checked
28
+ * @param manifest
29
+ */
30
+ function installerConfig(manifest) {
31
+ const config = manifest.lakutata?.installer ?? {};
32
+ if (!config || typeof config !== 'object' || Array.isArray(config))
33
+ throw new Error('The installers of the application (lakutata.installer of its package.json) must be an object');
34
+ return config;
35
+ }
36
+ /**
37
+ * Make the installers of the application for the platform it is made on (the installers of the other platforms are
38
+ * made on them, as its executables): its executable in a directory, with its native addons and its resources, installed
39
+ * in /opt/<name> (Linux) or /usr/local/lib/<name> (macOS), its program in the PATH, its service registered (systemd,
40
+ * OpenRC, launchd)
41
+ * @param options
42
+ */
43
+ export async function buildInstaller(options) {
44
+ const project = path.resolve(options.project);
45
+ const manifest = JSON.parse(await readFile(path.join(project, 'package.json'), 'utf8'));
46
+ const config = installerConfig(manifest);
47
+ if (config.runtime === 'electron')
48
+ return buildElectronInstaller(project, manifest, config, options);
49
+ const mac = process.platform === 'darwin';
50
+ const windows = process.platform === 'win32';
51
+ if (process.platform !== 'linux' && !mac && !windows)
52
+ throw new Error(`The installers of ${process.platform} are not supported: the ones of Linux are made on Linux, the ones of macOS on macOS, the ones of Windows on Windows`);
53
+ const libc = detectLibc();
54
+ const targets = options.targets?.length ? options.targets
55
+ : mac ? config.mac?.targets ?? ['pkg', 'tar.gz']
56
+ : windows ? config.win?.targets ?? ['nsis', 'msi', 'zip']
57
+ : config.linux?.targets ?? (libc === 'glibc' ? ['deb', 'rpm', 'tar.gz'] : ['apk', 'tar.gz']);
58
+ for (const target of targets) {
59
+ if (mac) {
60
+ if (!MAC_TARGETS.includes(target))
61
+ throw new Error(`The installer ${target} is not one of macOS (${MAC_TARGETS.join(', ')})`);
62
+ continue;
63
+ }
64
+ if (windows) {
65
+ if (!WINDOWS_TARGETS.includes(target))
66
+ throw new Error(`The installer ${target} is not one of Windows (${WINDOWS_TARGETS.join(', ')})`);
67
+ continue;
68
+ }
69
+ if (!LINUX_TARGETS.includes(target))
70
+ throw new Error(`The installer ${target} is not one of Linux (${LINUX_TARGETS.join(', ')})`);
71
+ //The executable links the C library of the system it is made on
72
+ if (target === 'apk' && libc !== 'musl')
73
+ throw new Error('The apk packages are made on Alpine (musl): the executables made on the systems of the glibc do not run there');
74
+ if (['deb', 'rpm', 'archlinux'].includes(target) && libc !== 'glibc')
75
+ throw new Error(`The ${target} packages are made on a system of the glibc: the executables made on Alpine (musl) do not run there`);
76
+ }
77
+ const program = programName(manifest);
78
+ const name = config.name ?? program.toLowerCase();
79
+ if (!/^[a-z0-9][a-z0-9+.-]*$/.test(name))
80
+ throw new Error(`The name of the package "${name}" must be lower case letters, digits, "+", "-" and "." (lakutata.installer.name)`);
81
+ const version = String(manifest.version ?? '');
82
+ if (!/^\d+\.\d+\.\d+/.test(version))
83
+ throw new Error(`The version of the package "${version}" must be a version (1.2.3) of the package.json`);
84
+ const warnings = [];
85
+ let maintainer = config.maintainer ?? packageAuthor(manifest.author);
86
+ if (!maintainer) {
87
+ maintainer = name;
88
+ warnings.push(`The packages have no maintainer: give it (the author of the package.json, or lakutata.installer.maintainer)`);
89
+ }
90
+ const appId = config.appId ?? name;
91
+ if (!/^[A-Za-z0-9][A-Za-z0-9.-]*$/.test(appId))
92
+ throw new Error(`The identifier of the application "${appId}" must be letters, digits, "." and "-" (lakutata.installer.appId)`);
93
+ const service = config.service === true ? {} : config.service || undefined;
94
+ if (service?.name !== undefined && !/^[A-Za-z0-9_.@-]+$/.test(service.name))
95
+ throw new Error(`The name of the service "${service.name}" must be letters, digits, "_", ".", "@" and "-"`);
96
+ const platform = mac ? 'mac' : windows ? 'win' : 'linux';
97
+ const scripts = resolveScripts(project, platform, config[platform], targets, warnings);
98
+ const application = applicationIcon(project, manifest);
99
+ const work = await mkdtemp(path.join(os.tmpdir(), 'lakutata-installer-'));
100
+ try {
101
+ //The icon of the installers of Windows (the executable has it too)
102
+ let icon;
103
+ if (windows && application) {
104
+ icon = path.join(work, 'icon.ico');
105
+ await writeFile(icon, icoFile(application.image));
106
+ }
107
+ const executable = await buildExecutable({
108
+ project: project,
109
+ entry: options.entry,
110
+ minify: options.minify,
111
+ external: options.external,
112
+ runtime: config.runtime,
113
+ layout: 'directory',
114
+ outdir: path.join(work, 'stage')
115
+ });
116
+ warnings.push(...executable.warnings);
117
+ const plan = {
118
+ name: name,
119
+ appId: appId,
120
+ productName: config.productName ?? (typeof manifest.productName === 'string' ? manifest.productName : name),
121
+ program: program,
122
+ version: version,
123
+ description: config.description ?? manifest.description ?? name,
124
+ maintainer: maintainer,
125
+ homepage: config.homepage ?? (typeof manifest.homepage === 'string' ? manifest.homepage : undefined),
126
+ license: config.license ?? (typeof manifest.license === 'string' ? manifest.license : undefined),
127
+ vendor: config.vendor,
128
+ addToPath: config.addToPath ?? true,
129
+ depends: config.linux?.depends ?? [],
130
+ recommends: config.linux?.recommends ?? [],
131
+ section: config.linux?.section,
132
+ priority: config.linux?.priority,
133
+ service: service && {
134
+ name: service.name ?? name,
135
+ description: service.description ?? config.description ?? manifest.description ?? name,
136
+ args: service.args ?? [],
137
+ env: { MODE: 'production', ...service.env },
138
+ //The users of the services of macOS are hidden by their names (_www), the ones of Windows are virtual
139
+ user: service.user ?? (mac ? `_${name}` : windows ? `NT SERVICE\\${service.name ?? name}` : name),
140
+ restart: service.restart ?? 'on-failure',
141
+ autoStart: service.autoStart ?? true
142
+ },
143
+ directory: executable.directory,
144
+ prefix: mac ? `/usr/local/lib/${name}` : windows ? `C:\\Program Files\\${name}` : `/opt/${name}`,
145
+ scripts: scripts,
146
+ icon: icon
147
+ };
148
+ const outdir = path.resolve(project, options.outdir ?? path.join('dist', 'installer'));
149
+ await mkdir(outdir, { recursive: true });
150
+ const files = mac ? await buildMacPackages(plan, targets, path.join(work, 'mac'), outdir)
151
+ : windows ? await buildWindowsPackages(plan, targets, path.join(work, 'win'), outdir)
152
+ : await buildLinuxPackages(plan, targets, path.join(work, 'linux'), outdir);
153
+ return { name: name, version: version, files: files, executable: executable, warnings: warnings };
154
+ }
155
+ finally {
156
+ await rm(work, { recursive: true, force: true });
157
+ }
158
+ }