yapin.js-client 0.1.0 → 0.1.4
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 +1 -1
- package/docs/README.md +1 -0
- package/docs/about-page.md +19 -2
- package/docs/configuration.md +36 -2
- package/docs/getting-started.md +1 -1
- package/docs/installer.md +39 -12
- package/docs/ui.md +20 -3
- package/docs/welcome.md +63 -0
- package/package.json +1 -1
- package/src/build/build.js +34 -22
- package/src/build/nsis.js +763 -0
- package/src/config/defaults.js +30 -3
- package/src/config/load.js +9 -1
- package/src/desktop/preload.cjs +3 -1
- package/src/desktop/shim.cjs +60 -11
- package/src/installer/main.js +5 -0
- package/src/installer/nsis.js +75 -0
- package/src/installer/operations.js +23 -18
- package/src/installer/page.js +29 -0
- package/src/installer/processes.js +74 -19
- package/src/runtime/about.js +126 -36
- package/src/runtime/builtin-pages.js +2 -2
- package/src/runtime/client.js +21 -1
- package/src/runtime/onboarding.js +196 -0
- package/src/runtime/ui.js +52 -9
- package/src/shared/constants.js +1 -1
- package/src/shared/icons.js +52 -0
- package/types/index.d.ts +38 -1
- package/types/runtime.d.ts +26 -2
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join, sep } from 'node:path';
|
|
5
|
+
import { inflateSync } from 'node:zlib';
|
|
6
|
+
import { defaultStrings } from '../installer/page.js';
|
|
7
|
+
import { fileVersion, pngToIco } from './windows.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The Windows setup wizard: an NSIS (https://nsis.sourceforge.io) installer with the Modern UI pages
|
|
11
|
+
* people know from other Windows installers — Welcome, License, a page to choose what to do, the
|
|
12
|
+
* progress page with "Show details", and Finish with "Open Yapin’".
|
|
13
|
+
*
|
|
14
|
+
* NSIS shows the pages; the work is done by installer.mjs, run with the desktop app's own Node (see
|
|
15
|
+
* src/installer/nsis.js for how the two talk). Building needs makensis, from the NSIS installer on
|
|
16
|
+
* Windows, or `apt install nsis` / `brew install makensis` elsewhere.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** makensis: the configured path, MAKENSIS, the usual install folders, or the PATH. Null if none works. */
|
|
20
|
+
export function findMakensis({ configured = null, env = process.env, platform = process.platform } = {}) {
|
|
21
|
+
const candidates = [];
|
|
22
|
+
if (typeof configured === 'string') candidates.push(configured);
|
|
23
|
+
if (env.MAKENSIS) candidates.push(env.MAKENSIS);
|
|
24
|
+
if (platform === 'win32') {
|
|
25
|
+
for (const base of [env['ProgramFiles(x86)'], env.ProgramFiles, env.ProgramW6432]) if (base) candidates.push(join(base, 'NSIS', 'makensis.exe'));
|
|
26
|
+
}
|
|
27
|
+
candidates.push('makensis');
|
|
28
|
+
for (const candidate of candidates) {
|
|
29
|
+
if (candidate !== 'makensis' && !existsSync(candidate)) continue;
|
|
30
|
+
try {
|
|
31
|
+
execFileSync(candidate, ['-VERSION'], { stdio: 'pipe', windowsHide: true });
|
|
32
|
+
return candidate;
|
|
33
|
+
} catch {
|
|
34
|
+
// not this one
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A double-quoted NSIS string. NSIS strings are limited to 1024 characters, so long text is cut. */
|
|
41
|
+
export function nsisString(text, max = 1000) {
|
|
42
|
+
let value = String(text ?? '');
|
|
43
|
+
if (value.length > max) value = `${value.slice(0, max - 1).trimEnd()}…`;
|
|
44
|
+
return `"${value.replace(/\$/g, '$$$$').replace(/"/g, '$$\\"').replace(/\t/g, '$$\\t').replace(/\r?\n/g, '$$\\r$$\\n')}"`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** An NSIS string mixing text with variables: nsisVars('Yapper ', ['$R3']) keeps $R3 a variable. */
|
|
48
|
+
export function nsisVars(...parts) {
|
|
49
|
+
return `"${parts.map((part) => (Array.isArray(part) ? part.join('') : nsisString(part).slice(1, -1))).join('')}"`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Marketing text without its markdown: **bold**, *italic*, `code` and [links](…) become plain words. */
|
|
53
|
+
export function plainText(text) {
|
|
54
|
+
return String(text ?? '')
|
|
55
|
+
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
|
|
56
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
57
|
+
.replace(/(^|[^*])\*([^*]+)\*/g, '$1$2')
|
|
58
|
+
.replace(/`([^`]+)`/g, '$1');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Decodes an 8-bit, non-interlaced PNG to RGBA. Returns null for anything else. */
|
|
62
|
+
export function decodePng(png) {
|
|
63
|
+
if (!Buffer.isBuffer(png) || png.length < 33 || png.readUInt32BE(0) !== 0x89504e47) return null;
|
|
64
|
+
let offset = 8;
|
|
65
|
+
let header = null;
|
|
66
|
+
let palette = null;
|
|
67
|
+
let transparency = null;
|
|
68
|
+
const data = [];
|
|
69
|
+
while (offset + 8 <= png.length) {
|
|
70
|
+
const length = png.readUInt32BE(offset);
|
|
71
|
+
const type = png.toString('ascii', offset + 4, offset + 8);
|
|
72
|
+
const body = png.subarray(offset + 8, offset + 8 + length);
|
|
73
|
+
if (type === 'IHDR') header = { width: body.readUInt32BE(0), height: body.readUInt32BE(4), depth: body[8], color: body[9], interlace: body[12] };
|
|
74
|
+
else if (type === 'PLTE') palette = body;
|
|
75
|
+
else if (type === 'tRNS') transparency = body;
|
|
76
|
+
else if (type === 'IDAT') data.push(body);
|
|
77
|
+
else if (type === 'IEND') break;
|
|
78
|
+
offset += 12 + length;
|
|
79
|
+
}
|
|
80
|
+
if (!header || header.depth !== 8 || header.interlace !== 0) return null;
|
|
81
|
+
const channels = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 }[header.color];
|
|
82
|
+
if (!channels || (header.color === 3 && !palette)) return null;
|
|
83
|
+
const { width, height } = header;
|
|
84
|
+
let raw;
|
|
85
|
+
try {
|
|
86
|
+
raw = inflateSync(Buffer.concat(data));
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const stride = width * channels;
|
|
91
|
+
if (raw.length < (stride + 1) * height) return null;
|
|
92
|
+
const pixels = Buffer.alloc(stride * height);
|
|
93
|
+
for (let y = 0; y < height; y++) {
|
|
94
|
+
const filter = raw[y * (stride + 1)];
|
|
95
|
+
const line = raw.subarray(y * (stride + 1) + 1, (y + 1) * (stride + 1));
|
|
96
|
+
for (let x = 0; x < stride; x++) {
|
|
97
|
+
const left = x >= channels ? pixels[y * stride + x - channels] : 0;
|
|
98
|
+
const up = y > 0 ? pixels[(y - 1) * stride + x] : 0;
|
|
99
|
+
const corner = x >= channels && y > 0 ? pixels[(y - 1) * stride + x - channels] : 0;
|
|
100
|
+
let value = line[x];
|
|
101
|
+
if (filter === 1) value += left;
|
|
102
|
+
else if (filter === 2) value += up;
|
|
103
|
+
else if (filter === 3) value += (left + up) >> 1;
|
|
104
|
+
else if (filter === 4) {
|
|
105
|
+
const p = left + up - corner;
|
|
106
|
+
const pa = Math.abs(p - left);
|
|
107
|
+
const pb = Math.abs(p - up);
|
|
108
|
+
const pc = Math.abs(p - corner);
|
|
109
|
+
value += pa <= pb && pa <= pc ? left : pb <= pc ? up : corner;
|
|
110
|
+
}
|
|
111
|
+
pixels[y * stride + x] = value & 0xff;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const rgba = Buffer.alloc(width * height * 4);
|
|
115
|
+
for (let i = 0; i < width * height; i++) {
|
|
116
|
+
const s = i * channels;
|
|
117
|
+
let r, g, b, a;
|
|
118
|
+
if (header.color === 0) [r, g, b, a] = [pixels[s], pixels[s], pixels[s], 255];
|
|
119
|
+
else if (header.color === 4) [r, g, b, a] = [pixels[s], pixels[s], pixels[s], pixels[s + 1]];
|
|
120
|
+
else if (header.color === 2) [r, g, b, a] = [pixels[s], pixels[s + 1], pixels[s + 2], 255];
|
|
121
|
+
else if (header.color === 6) [r, g, b, a] = [pixels[s], pixels[s + 1], pixels[s + 2], pixels[s + 3]];
|
|
122
|
+
else {
|
|
123
|
+
const index = pixels[s];
|
|
124
|
+
[r, g, b] = [palette[index * 3], palette[index * 3 + 1], palette[index * 3 + 2]];
|
|
125
|
+
a = transparency && index < transparency.length ? transparency[index] : 255;
|
|
126
|
+
}
|
|
127
|
+
rgba.set([r, g, b, a], i * 4);
|
|
128
|
+
}
|
|
129
|
+
return { width, height, rgba };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** A width×height RGB canvas, filled by `paint(x, y)` → [r, g, b]. */
|
|
133
|
+
function canvas(width, height, paint) {
|
|
134
|
+
const rgb = Buffer.alloc(width * height * 3);
|
|
135
|
+
for (let y = 0; y < height; y++) for (let x = 0; x < width; x++) rgb.set(paint(x, y), (y * width + x) * 3);
|
|
136
|
+
return { width, height, rgb };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Draws `image` (RGBA) scaled to fit a size×size box at (left, top), averaging the pixels it covers. */
|
|
140
|
+
function drawImage(target, image, left, top, size) {
|
|
141
|
+
const scale = Math.min(size / image.width, size / image.height);
|
|
142
|
+
const w = Math.max(1, Math.round(image.width * scale));
|
|
143
|
+
const h = Math.max(1, Math.round(image.height * scale));
|
|
144
|
+
left += Math.floor((size - w) / 2);
|
|
145
|
+
top += Math.floor((size - h) / 2);
|
|
146
|
+
for (let y = 0; y < h; y++) {
|
|
147
|
+
for (let x = 0; x < w; x++) {
|
|
148
|
+
const x0 = Math.floor(x / scale);
|
|
149
|
+
const y0 = Math.floor(y / scale);
|
|
150
|
+
const x1 = Math.min(image.width, Math.max(x0 + 1, Math.floor((x + 1) / scale)));
|
|
151
|
+
const y1 = Math.min(image.height, Math.max(y0 + 1, Math.floor((y + 1) / scale)));
|
|
152
|
+
let r = 0, g = 0, b = 0, a = 0, n = 0;
|
|
153
|
+
for (let sy = y0; sy < y1; sy++) {
|
|
154
|
+
for (let sx = x0; sx < x1; sx++) {
|
|
155
|
+
const i = (sy * image.width + sx) * 4;
|
|
156
|
+
const alpha = image.rgba[i + 3];
|
|
157
|
+
r += image.rgba[i] * alpha;
|
|
158
|
+
g += image.rgba[i + 1] * alpha;
|
|
159
|
+
b += image.rgba[i + 2] * alpha;
|
|
160
|
+
a += alpha;
|
|
161
|
+
n++;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const tx = left + x;
|
|
165
|
+
const ty = top + y;
|
|
166
|
+
if (!a || tx < 0 || ty < 0 || tx >= target.width || ty >= target.height) continue;
|
|
167
|
+
const coverage = a / n / 255;
|
|
168
|
+
const o = (ty * target.width + tx) * 3;
|
|
169
|
+
target.rgb[o] = Math.round(target.rgb[o] * (1 - coverage) + (r / a) * coverage);
|
|
170
|
+
target.rgb[o + 1] = Math.round(target.rgb[o + 1] * (1 - coverage) + (g / a) * coverage);
|
|
171
|
+
target.rgb[o + 2] = Math.round(target.rgb[o + 2] * (1 - coverage) + (b / a) * coverage);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** A 24-bit Windows bitmap, the format NSIS wants for its pictures. */
|
|
177
|
+
export function toBmp({ width, height, rgb }) {
|
|
178
|
+
const stride = Math.ceil((width * 3) / 4) * 4;
|
|
179
|
+
const file = Buffer.alloc(54 + stride * height);
|
|
180
|
+
file.write('BM', 0, 'ascii');
|
|
181
|
+
file.writeUInt32LE(file.length, 2);
|
|
182
|
+
file.writeUInt32LE(54, 10);
|
|
183
|
+
file.writeUInt32LE(40, 14);
|
|
184
|
+
file.writeInt32LE(width, 18);
|
|
185
|
+
file.writeInt32LE(height, 22);
|
|
186
|
+
file.writeUInt16LE(1, 26);
|
|
187
|
+
file.writeUInt16LE(24, 28);
|
|
188
|
+
file.writeUInt32LE(stride * height, 34);
|
|
189
|
+
for (let y = 0; y < height; y++) {
|
|
190
|
+
const row = 54 + (height - 1 - y) * stride;
|
|
191
|
+
for (let x = 0; x < width; x++) {
|
|
192
|
+
const i = (y * width + x) * 3;
|
|
193
|
+
file[row + x * 3] = rgb[i + 2];
|
|
194
|
+
file[row + x * 3 + 1] = rgb[i + 1];
|
|
195
|
+
file[row + x * 3 + 2] = rgb[i];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return file;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function hexColor(value, fallback = [124, 92, 255]) {
|
|
202
|
+
const match = /^#([0-9a-f]{3,8})$/i.exec(String(value ?? ''));
|
|
203
|
+
if (!match) return fallback;
|
|
204
|
+
const hex = match[1].length < 6 ? [...match[1].slice(0, 3)].map((c) => c + c).join('') : match[1].slice(0, 6);
|
|
205
|
+
return [0, 2, 4].map((i) => Number.parseInt(hex.slice(i, i + 2), 16));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** The picture down the left of the Welcome and Finish pages (164×314): the accent colour, darkening, with the logo. */
|
|
209
|
+
export function sidebarBitmap({ accent, logo }) {
|
|
210
|
+
const [r, g, b] = hexColor(accent);
|
|
211
|
+
const image = canvas(164, 314, (x, y) => {
|
|
212
|
+
const t = y / 313;
|
|
213
|
+
const shade = 1 - 0.55 * t - 0.1 * (x / 163);
|
|
214
|
+
return [r * shade, g * shade, b * shade].map((c) => Math.max(0, Math.min(255, Math.round(c))));
|
|
215
|
+
});
|
|
216
|
+
if (logo) drawImage(image, logo, 34, 36, 96);
|
|
217
|
+
return toBmp(image);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** The picture in the top right of the other pages (150×57): the logo on white. */
|
|
221
|
+
export function headerBitmap({ logo }) {
|
|
222
|
+
const image = canvas(150, 57, () => [255, 255, 255]);
|
|
223
|
+
if (logo) drawImage(image, logo, 150 - 44 - 8, 6, 44);
|
|
224
|
+
return toBmp(image);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** The logo to draw, from a PNG data URI or file. Null if there's none, or it isn't a PNG we can read. */
|
|
228
|
+
export function readLogo(source) {
|
|
229
|
+
if (!source || typeof source !== 'string') return null;
|
|
230
|
+
if (/^data:image\/png;base64,/i.test(source)) return decodePng(Buffer.from(source.slice(source.indexOf(',') + 1), 'base64'));
|
|
231
|
+
if (/\.png$/i.test(source) && existsSync(source)) return decodePng(readFileSync(source));
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Every text the wizard shows, with {name}, {version} and {product} filled in. */
|
|
236
|
+
export function wizardStrings(installer) {
|
|
237
|
+
const strings = { ...defaultStrings, ...(installer.marketing?.strings ?? {}) };
|
|
238
|
+
const vars = { name: installer.app.name, version: installer.app.version, product: installer.target?.productName ?? 'Yapper' };
|
|
239
|
+
return Object.fromEntries(Object.entries(strings).map(([key, text]) => [key, String(text).replace(/\{(name|version|product)\}/g, (_, k) => vars[k])]));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** The Welcome page's text: the intro, then the marketing description (or tagline), the notice, and "Click Next". */
|
|
243
|
+
export function welcomeText(installer, strings = wizardStrings(installer)) {
|
|
244
|
+
const m = installer.marketing ?? {};
|
|
245
|
+
const app = installer.app;
|
|
246
|
+
let about = plainText(m.description ?? m.tagline ?? app.description ?? '').trim();
|
|
247
|
+
if (about.length > 360) about = `${about.slice(0, 359).trimEnd()}…`;
|
|
248
|
+
const notice = m.notice === false ? '' : plainText(m.notice ?? `${app.name} is an unofficial modification of the Yapin’ desktop app. It isn’t made, supported or endorsed by Yapin’.`);
|
|
249
|
+
return [strings.setupWelcomeText, about, notice, strings.setupContinue].filter(Boolean).join('\n\n');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* The .nsi script. `files` holds the paths of what the build wrote for it (license, bitmaps, icon);
|
|
254
|
+
* `source` is the installer folder that gets packed in.
|
|
255
|
+
*/
|
|
256
|
+
export function nsisScript({ installer, source, out, files, company = '', description = '', exeLocations = [] }) {
|
|
257
|
+
const s = wizardStrings(installer);
|
|
258
|
+
const q = (text) => nsisString(text);
|
|
259
|
+
const app = installer.app;
|
|
260
|
+
const m = installer.marketing ?? {};
|
|
261
|
+
const closeApp = installer.closeApp ?? 'ask';
|
|
262
|
+
const terms = m.terms?.text ? { title: m.terms.title ?? null, requireAccept: m.terms.requireAccept !== false } : null;
|
|
263
|
+
const product = installer.target?.productName ?? 'Yapper';
|
|
264
|
+
const ini = '$PLUGINSDIR\\status.ini';
|
|
265
|
+
const result = '$PLUGINSDIR\\result.ini';
|
|
266
|
+
|
|
267
|
+
return `; Generated by yapin.js-client: the Windows setup wizard for ${app.name.replace(/[\r\n]/g, ' ')}.
|
|
268
|
+
Unicode true
|
|
269
|
+
ManifestDPIAware true
|
|
270
|
+
SetCompressor /SOLID lzma
|
|
271
|
+
RequestExecutionLevel user
|
|
272
|
+
|
|
273
|
+
!include "MUI2.nsh"
|
|
274
|
+
!include "nsDialogs.nsh"
|
|
275
|
+
!include "LogicLib.nsh"
|
|
276
|
+
!include "WinMessages.nsh"
|
|
277
|
+
!include "FileFunc.nsh"
|
|
278
|
+
|
|
279
|
+
Name ${q(app.name)}
|
|
280
|
+
OutFile ${q(out)}
|
|
281
|
+
BrandingText ${q(`${app.name} ${app.version}`)}
|
|
282
|
+
VIProductVersion "${fileVersion(app.version)}"
|
|
283
|
+
VIAddVersionKey "ProductName" ${q(app.name)}
|
|
284
|
+
VIAddVersionKey "ProductVersion" ${q(app.version)}
|
|
285
|
+
VIAddVersionKey "FileVersion" ${q(app.version)}
|
|
286
|
+
VIAddVersionKey "FileDescription" ${q(description || `${app.name} Setup`)}
|
|
287
|
+
${company ? `VIAddVersionKey "CompanyName" ${q(company)}\nVIAddVersionKey "LegalCopyright" ${q(`© ${new Date().getFullYear()} ${company}`)}` : ''}
|
|
288
|
+
|
|
289
|
+
${files.icon ? `!define MUI_ICON ${q(files.icon)}` : ''}
|
|
290
|
+
!define MUI_WELCOMEFINISHPAGE_BITMAP ${q(files.sidebar)}
|
|
291
|
+
!define MUI_HEADERIMAGE
|
|
292
|
+
!define MUI_HEADERIMAGE_RIGHT
|
|
293
|
+
!define MUI_HEADERIMAGE_BITMAP ${q(files.header)}
|
|
294
|
+
!define MUI_ABORTWARNING
|
|
295
|
+
|
|
296
|
+
Var Runtime
|
|
297
|
+
Var Electron
|
|
298
|
+
Var Count
|
|
299
|
+
Var Running
|
|
300
|
+
Var HasData
|
|
301
|
+
Var Index
|
|
302
|
+
Var Action
|
|
303
|
+
Var Purge
|
|
304
|
+
Var MainLabel
|
|
305
|
+
Var ProgressTitle
|
|
306
|
+
Var ProgressText
|
|
307
|
+
Var DoneTitle
|
|
308
|
+
Var DoneText
|
|
309
|
+
Var FinishText
|
|
310
|
+
Var Dialog
|
|
311
|
+
Var List
|
|
312
|
+
Var Where
|
|
313
|
+
Var State
|
|
314
|
+
Var RadioMain
|
|
315
|
+
Var RadioUninstall
|
|
316
|
+
Var PurgeBox
|
|
317
|
+
Var Notice
|
|
318
|
+
|
|
319
|
+
!define MUI_WELCOMEPAGE_TITLE ${q(s.setupWelcomeTitle)}
|
|
320
|
+
!define MUI_WELCOMEPAGE_TEXT ${q(welcomeText(installer, s))}
|
|
321
|
+
!insertmacro MUI_PAGE_WELCOME
|
|
322
|
+
${
|
|
323
|
+
terms
|
|
324
|
+
? `${terms.title ? `!define MUI_PAGE_HEADER_TEXT ${q(terms.title)}\n` : ''}${
|
|
325
|
+
terms.requireAccept ? `!define MUI_LICENSEPAGE_CHECKBOX\n!define MUI_LICENSEPAGE_CHECKBOX_TEXT ${q(s.accept)}` : '!define MUI_LICENSEPAGE_BUTTON "$(^NextBtn)"\n!define MUI_LICENSEPAGE_TEXT_BOTTOM " "'
|
|
326
|
+
}
|
|
327
|
+
!insertmacro MUI_PAGE_LICENSE ${q(files.license)}`
|
|
328
|
+
: ''
|
|
329
|
+
}
|
|
330
|
+
Page custom ChoosePage ChooseLeave
|
|
331
|
+
!define MUI_PAGE_HEADER_TEXT "$ProgressTitle"
|
|
332
|
+
!define MUI_PAGE_HEADER_SUBTEXT "$ProgressText"
|
|
333
|
+
!define MUI_INSTFILESPAGE_FINISHHEADER_TEXT "$DoneTitle"
|
|
334
|
+
!define MUI_INSTFILESPAGE_FINISHHEADER_SUBTEXT "$DoneText"
|
|
335
|
+
!define MUI_INSTFILESPAGE_ABORTHEADER_TEXT ${q(s.setupFailedTitle)}
|
|
336
|
+
!define MUI_INSTFILESPAGE_ABORTHEADER_SUBTEXT ${q(s.setupFailedText)}
|
|
337
|
+
!insertmacro MUI_PAGE_INSTFILES
|
|
338
|
+
!define MUI_FINISHPAGE_TITLE ${q(s.setupFinishTitle)}
|
|
339
|
+
!define MUI_FINISHPAGE_TEXT "$FinishText"
|
|
340
|
+
!define MUI_FINISHPAGE_RUN
|
|
341
|
+
!define MUI_FINISHPAGE_RUN_TEXT ${q(s.launch)}
|
|
342
|
+
!define MUI_FINISHPAGE_RUN_FUNCTION LaunchApp
|
|
343
|
+
${installer.launchAfter === false ? '!define MUI_FINISHPAGE_RUN_NOTCHECKED' : ''}
|
|
344
|
+
!insertmacro MUI_PAGE_FINISH
|
|
345
|
+
!insertmacro MUI_LANGUAGE "English"
|
|
346
|
+
|
|
347
|
+
; Runs installer.mjs with the arguments in $R9: with its output in the log when $R8 is "log". Exit code in $R0.
|
|
348
|
+
Function RunInstaller
|
|
349
|
+
\${If} $Electron == 1
|
|
350
|
+
System::Call 'kernel32::SetEnvironmentVariable(t "ELECTRON_RUN_AS_NODE", t "1")'
|
|
351
|
+
\${Else}
|
|
352
|
+
System::Call 'kernel32::SetEnvironmentVariable(t "ELECTRON_RUN_AS_NODE", p 0)'
|
|
353
|
+
\${EndIf}
|
|
354
|
+
SetDetailsPrint none
|
|
355
|
+
SetOutPath "$PLUGINSDIR\\app"
|
|
356
|
+
SetDetailsPrint lastused
|
|
357
|
+
\${If} $R8 == "log"
|
|
358
|
+
nsExec::ExecToLog '"$Runtime" "$PLUGINSDIR\\app\\installer.mjs" $R9'
|
|
359
|
+
\${Else}
|
|
360
|
+
nsExec::Exec '"$Runtime" "$PLUGINSDIR\\app\\installer.mjs" $R9'
|
|
361
|
+
\${EndIf}
|
|
362
|
+
Pop $R0
|
|
363
|
+
System::Call 'kernel32::SetEnvironmentVariable(t "ELECTRON_RUN_AS_NODE", p 0)'
|
|
364
|
+
SetDetailsPrint none
|
|
365
|
+
SetOutPath "$TEMP"
|
|
366
|
+
SetDetailsPrint lastused
|
|
367
|
+
FunctionEnd
|
|
368
|
+
|
|
369
|
+
; The desktop app's own Node (Electron run as Node), or node.exe.
|
|
370
|
+
Function FindRuntime
|
|
371
|
+
StrCpy $Electron 1
|
|
372
|
+
${exeLocations
|
|
373
|
+
.map(
|
|
374
|
+
(exe) => ` ExpandEnvStrings $R0 ${q(exe)}
|
|
375
|
+
\${If} \${FileExists} "$R0"
|
|
376
|
+
StrCpy $Runtime $R0
|
|
377
|
+
Return
|
|
378
|
+
\${EndIf}`,
|
|
379
|
+
)
|
|
380
|
+
.join('\n')}
|
|
381
|
+
StrCpy $Electron 0
|
|
382
|
+
SearchPath $Runtime "node.exe"
|
|
383
|
+
FunctionEnd
|
|
384
|
+
|
|
385
|
+
Function GetStatus
|
|
386
|
+
Delete "${ini}"
|
|
387
|
+
StrCpy $R8 "quiet"
|
|
388
|
+
StrCpy $R9 'status --nsis "${ini}"'
|
|
389
|
+
Call RunInstaller
|
|
390
|
+
ReadINIStr $Count "${ini}" status count
|
|
391
|
+
\${If} $Count == ""
|
|
392
|
+
MessageBox MB_ICONSTOP|MB_OK ${q(s.setupFailed)} /SD IDOK
|
|
393
|
+
Quit
|
|
394
|
+
\${EndIf}
|
|
395
|
+
ReadINIStr $Running "${ini}" status running
|
|
396
|
+
ReadINIStr $HasData "${ini}" status hasData
|
|
397
|
+
FunctionEnd
|
|
398
|
+
|
|
399
|
+
; What Next does for install $Index: install, repair (also reinstalling the same version), or "" when nothing can be done.
|
|
400
|
+
Function DefaultAction
|
|
401
|
+
ReadINIStr $R0 "${ini}" "install$Index" state
|
|
402
|
+
ReadINIStr $R1 "${ini}" "install$Index" installedVersion
|
|
403
|
+
ReadINIStr $R2 "${ini}" "install$Index" canInstall
|
|
404
|
+
ReadINIStr $R3 "${ini}" "install$Index" ours
|
|
405
|
+
StrCpy $Action ""
|
|
406
|
+
\${If} $R2 == 1
|
|
407
|
+
StrCpy $Action "install"
|
|
408
|
+
StrCpy $MainLabel ${q(s.install)}
|
|
409
|
+
\${If} $R0 == "needs-repair"
|
|
410
|
+
StrCpy $Action "repair"
|
|
411
|
+
StrCpy $MainLabel ${q(s.repair)}
|
|
412
|
+
\${ElseIf} $R0 == "installed"
|
|
413
|
+
StrCpy $MainLabel ${q(s.update)}
|
|
414
|
+
\${If} $R1 == ${q(app.version)}
|
|
415
|
+
StrCpy $Action "repair"
|
|
416
|
+
StrCpy $MainLabel ${q(s.repair)}
|
|
417
|
+
\${EndIf}
|
|
418
|
+
\${EndIf}
|
|
419
|
+
\${EndIf}
|
|
420
|
+
FunctionEnd
|
|
421
|
+
|
|
422
|
+
Function .onInit
|
|
423
|
+
InitPluginsDir
|
|
424
|
+
SetOutPath "$PLUGINSDIR\\app"
|
|
425
|
+
File /r /x "*.sh" /x "*.command" ${q(`${source}${sep}*`)}
|
|
426
|
+
SetOutPath "$TEMP"
|
|
427
|
+
Call FindRuntime
|
|
428
|
+
\${If} $Runtime == ""
|
|
429
|
+
MessageBox MB_ICONSTOP|MB_OK ${q(s.notFound)} /SD IDOK
|
|
430
|
+
Abort
|
|
431
|
+
\${EndIf}
|
|
432
|
+
Call GetStatus
|
|
433
|
+
StrCpy $Index 0
|
|
434
|
+
StrCpy $Purge 0
|
|
435
|
+
Call DefaultAction
|
|
436
|
+
; Silent mode (/S) does what Next would, or uninstalls with /UNINSTALL (and deletes data with /PURGE).
|
|
437
|
+
\${GetParameters} $R0
|
|
438
|
+
ClearErrors
|
|
439
|
+
\${GetOptions} $R0 "/UNINSTALL" $R1
|
|
440
|
+
\${IfNot} \${Errors}
|
|
441
|
+
StrCpy $Action "uninstall"
|
|
442
|
+
\${EndIf}
|
|
443
|
+
ClearErrors
|
|
444
|
+
\${GetOptions} $R0 "/PURGE" $R1
|
|
445
|
+
\${IfNot} \${Errors}
|
|
446
|
+
StrCpy $Purge 1
|
|
447
|
+
\${EndIf}
|
|
448
|
+
FunctionEnd
|
|
449
|
+
|
|
450
|
+
Function ChoosePage
|
|
451
|
+
!insertmacro MUI_HEADER_TEXT ${q(s.setupChooseTitle)} ${q(s.setupChooseSubtitle)}
|
|
452
|
+
nsDialogs::Create 1018
|
|
453
|
+
Pop $Dialog
|
|
454
|
+
\${If} $Dialog == error
|
|
455
|
+
Abort
|
|
456
|
+
\${EndIf}
|
|
457
|
+
\${If} $Count == 0
|
|
458
|
+
\${NSD_CreateLabel} 0 0 100% 40u ${q(s.notFound)}
|
|
459
|
+
Pop $R0
|
|
460
|
+
GetDlgItem $R0 $HWNDPARENT 1
|
|
461
|
+
EnableWindow $R0 0
|
|
462
|
+
nsDialogs::Show
|
|
463
|
+
Return
|
|
464
|
+
\${EndIf}
|
|
465
|
+
\${If} $Count > 1
|
|
466
|
+
\${NSD_CreateLabel} 0 0 100% 10u ${q(s.installInto)}
|
|
467
|
+
Pop $R0
|
|
468
|
+
\${NSD_CreateDropList} 0 11u 100% 80u ""
|
|
469
|
+
Pop $List
|
|
470
|
+
StrCpy $R1 0
|
|
471
|
+
\${DoWhile} $R1 < $Count
|
|
472
|
+
ReadINIStr $R2 "${ini}" "install$R1" location
|
|
473
|
+
ReadINIStr $R3 "${ini}" "install$R1" appVersion
|
|
474
|
+
\${NSD_CB_AddString} $List ${nsisVars(`${product} `, ['$R3'], ' (', ['$R2'], ')')}
|
|
475
|
+
IntOp $R1 $R1 + 1
|
|
476
|
+
\${Loop}
|
|
477
|
+
\${NSD_CB_SetSelectionIndex} $List $Index
|
|
478
|
+
\${NSD_OnChange} $List OnPickInstall
|
|
479
|
+
\${Else}
|
|
480
|
+
ReadINIStr $R3 "${ini}" "install0" appVersion
|
|
481
|
+
\${NSD_CreateLabel} 0 0 100% 10u ${nsisVars(`${product} `, ['$R3'])}
|
|
482
|
+
Pop $R0
|
|
483
|
+
CreateFont $R1 "$(^Font)" "$(^FontSize)" 700
|
|
484
|
+
SendMessage $R0 \${WM_SETFONT} $R1 0
|
|
485
|
+
\${NSD_CreateLabel} 0 11u 100% 12u ""
|
|
486
|
+
Pop $Where
|
|
487
|
+
\${NSD_AddStyle} $Where 0x8000 ; SS_PATHELLIPSIS: long paths lose their middle, not their end
|
|
488
|
+
\${EndIf}
|
|
489
|
+
\${NSD_CreateLabel} 0 28u 100% 10u ""
|
|
490
|
+
Pop $State
|
|
491
|
+
\${NSD_CreateRadioButton} 0 46u 100% 12u ""
|
|
492
|
+
Pop $RadioMain
|
|
493
|
+
\${NSD_OnClick} $RadioMain OnPickAction
|
|
494
|
+
\${NSD_CreateRadioButton} 0 60u 100% 12u ${q(s.setupUninstall)}
|
|
495
|
+
Pop $RadioUninstall
|
|
496
|
+
\${NSD_OnClick} $RadioUninstall OnPickAction
|
|
497
|
+
\${NSD_CreateCheckBox} 12u 74u -12u 12u ${q(s.purge)}
|
|
498
|
+
Pop $PurgeBox
|
|
499
|
+
\${NSD_CreateLabel} 0 96u 100% 40u ""
|
|
500
|
+
Pop $Notice
|
|
501
|
+
Call UpdateChoice
|
|
502
|
+
nsDialogs::Show
|
|
503
|
+
FunctionEnd
|
|
504
|
+
|
|
505
|
+
Function OnPickInstall
|
|
506
|
+
Pop $R0
|
|
507
|
+
\${NSD_CB_GetSelectionIndex} $List $Index
|
|
508
|
+
Call DefaultAction
|
|
509
|
+
Call UpdateChoice
|
|
510
|
+
FunctionEnd
|
|
511
|
+
|
|
512
|
+
Function OnPickAction
|
|
513
|
+
Pop $R0
|
|
514
|
+
Call UpdateButtons
|
|
515
|
+
FunctionEnd
|
|
516
|
+
|
|
517
|
+
; Shows install $Index: where it is, what's there, and the choices that make sense.
|
|
518
|
+
Function UpdateChoice
|
|
519
|
+
ReadINIStr $R0 "${ini}" "install$Index" state
|
|
520
|
+
ReadINIStr $R1 "${ini}" "install$Index" ours
|
|
521
|
+
ReadINIStr $R2 "${ini}" "install$Index" installedName
|
|
522
|
+
ReadINIStr $R3 "${ini}" "install$Index" installedVersion
|
|
523
|
+
ReadINIStr $R4 "${ini}" "install$Index" blocked
|
|
524
|
+
ReadINIStr $R5 "${ini}" "install$Index" location
|
|
525
|
+
\${If} $Count == 1
|
|
526
|
+
\${NSD_SetText} $Where "$R5"
|
|
527
|
+
\${EndIf}
|
|
528
|
+
|
|
529
|
+
\${If} $R4 != ""
|
|
530
|
+
\${NSD_SetText} $State ${q(s.setupBlockedState)}
|
|
531
|
+
\${ElseIf} $R0 == "installed"
|
|
532
|
+
\${NSD_SetText} $State ${nsisVars(`${s.setupInstalledState} `, ['$R2'], ' ', ['$R3'])}
|
|
533
|
+
\${ElseIf} $R0 == "needs-repair"
|
|
534
|
+
\${NSD_SetText} $State ${q(s.setupNeedsRepairState)}
|
|
535
|
+
\${Else}
|
|
536
|
+
\${NSD_SetText} $State ${q(s.setupNotInstalled)}
|
|
537
|
+
\${EndIf}
|
|
538
|
+
|
|
539
|
+
; Uninstalling is offered for this client app (installed, or broken by a desktop update).
|
|
540
|
+
StrCpy $R6 0
|
|
541
|
+
\${If} $R0 == "needs-repair"
|
|
542
|
+
StrCpy $R6 1
|
|
543
|
+
\${ElseIf} $R0 == "installed"
|
|
544
|
+
\${AndIf} $R1 == 1
|
|
545
|
+
StrCpy $R6 1
|
|
546
|
+
\${EndIf}
|
|
547
|
+
|
|
548
|
+
\${If} $Action == ""
|
|
549
|
+
\${OrIf} $R6 == 0
|
|
550
|
+
ShowWindow $RadioMain \${SW_HIDE}
|
|
551
|
+
\${Else}
|
|
552
|
+
ShowWindow $RadioMain \${SW_SHOW}
|
|
553
|
+
\${EndIf}
|
|
554
|
+
\${If} $Action == "repair"
|
|
555
|
+
\${AndIf} $R0 == "needs-repair"
|
|
556
|
+
\${NSD_SetText} $RadioMain ${q(s.setupRepair)}
|
|
557
|
+
\${ElseIf} $Action == "repair"
|
|
558
|
+
\${NSD_SetText} $RadioMain ${q(s.setupReinstall)}
|
|
559
|
+
\${ElseIf} $R0 == "installed"
|
|
560
|
+
\${NSD_SetText} $RadioMain ${q(s.setupUpdate)}
|
|
561
|
+
\${Else}
|
|
562
|
+
\${NSD_SetText} $RadioMain ${q(s.setupInstall)}
|
|
563
|
+
\${EndIf}
|
|
564
|
+
\${If} $R6 == 1
|
|
565
|
+
ShowWindow $RadioUninstall \${SW_SHOW}
|
|
566
|
+
\${Else}
|
|
567
|
+
ShowWindow $RadioUninstall \${SW_HIDE}
|
|
568
|
+
\${EndIf}
|
|
569
|
+
\${If} $R6 == 1
|
|
570
|
+
\${AndIf} $HasData == 1
|
|
571
|
+
ShowWindow $PurgeBox \${SW_SHOW}
|
|
572
|
+
\${Else}
|
|
573
|
+
ShowWindow $PurgeBox \${SW_HIDE}
|
|
574
|
+
\${EndIf}
|
|
575
|
+
\${If} $Action == ""
|
|
576
|
+
\${AndIf} $R6 == 1
|
|
577
|
+
\${NSD_Uncheck} $RadioMain
|
|
578
|
+
\${NSD_Check} $RadioUninstall
|
|
579
|
+
\${Else}
|
|
580
|
+
\${NSD_Check} $RadioMain
|
|
581
|
+
\${NSD_Uncheck} $RadioUninstall
|
|
582
|
+
\${EndIf}
|
|
583
|
+
|
|
584
|
+
\${If} $R4 != ""
|
|
585
|
+
\${NSD_SetText} $Notice "$R4"
|
|
586
|
+
\${ElseIf} $R0 == "needs-repair"
|
|
587
|
+
\${NSD_SetText} $Notice ${q(s.needsRepair)}
|
|
588
|
+
\${ElseIf} $Running == 1
|
|
589
|
+
\${NSD_SetText} $Notice ${q(closeApp === 'never' ? s.setupStillRunning : s.appRunning)}
|
|
590
|
+
\${Else}
|
|
591
|
+
\${NSD_SetText} $Notice ""
|
|
592
|
+
\${EndIf}
|
|
593
|
+
Call UpdateButtons
|
|
594
|
+
FunctionEnd
|
|
595
|
+
|
|
596
|
+
; Next says what it will do, and the "delete my data" box only applies to uninstalling.
|
|
597
|
+
Function UpdateButtons
|
|
598
|
+
GetDlgItem $R0 $HWNDPARENT 1
|
|
599
|
+
\${NSD_GetState} $RadioUninstall $R1
|
|
600
|
+
\${If} $R1 == \${BST_CHECKED}
|
|
601
|
+
EnableWindow $PurgeBox 1
|
|
602
|
+
SendMessage $R0 \${WM_SETTEXT} 0 ${q(`STR:${s.uninstall}`)}
|
|
603
|
+
EnableWindow $R0 1
|
|
604
|
+
\${Else}
|
|
605
|
+
EnableWindow $PurgeBox 0
|
|
606
|
+
\${NSD_Uncheck} $PurgeBox
|
|
607
|
+
SendMessage $R0 \${WM_SETTEXT} 0 "STR:$MainLabel"
|
|
608
|
+
\${If} $Action == ""
|
|
609
|
+
EnableWindow $R0 0
|
|
610
|
+
\${Else}
|
|
611
|
+
EnableWindow $R0 1
|
|
612
|
+
\${EndIf}
|
|
613
|
+
\${EndIf}
|
|
614
|
+
FunctionEnd
|
|
615
|
+
|
|
616
|
+
Function ChooseLeave
|
|
617
|
+
\${NSD_GetState} $RadioUninstall $R0
|
|
618
|
+
\${If} $R0 == \${BST_CHECKED}
|
|
619
|
+
StrCpy $Action "uninstall"
|
|
620
|
+
\${NSD_GetState} $PurgeBox $R1
|
|
621
|
+
\${If} $R1 == \${BST_CHECKED}
|
|
622
|
+
StrCpy $Purge 1
|
|
623
|
+
\${Else}
|
|
624
|
+
StrCpy $Purge 0
|
|
625
|
+
\${EndIf}
|
|
626
|
+
\${Else}
|
|
627
|
+
Call DefaultAction
|
|
628
|
+
StrCpy $Purge 0
|
|
629
|
+
\${EndIf}
|
|
630
|
+
\${If} $Action == ""
|
|
631
|
+
Abort
|
|
632
|
+
\${EndIf}
|
|
633
|
+
${
|
|
634
|
+
closeApp === 'never'
|
|
635
|
+
? ` \${If} $Running == 1
|
|
636
|
+
Call GetStatus
|
|
637
|
+
\${If} $Running == 1
|
|
638
|
+
MessageBox MB_ICONEXCLAMATION|MB_OK ${q(s.setupStillRunning)}
|
|
639
|
+
Abort
|
|
640
|
+
\${EndIf}
|
|
641
|
+
\${EndIf}`
|
|
642
|
+
: closeApp === 'ask'
|
|
643
|
+
? ` \${If} $Running == 1
|
|
644
|
+
MessageBox MB_ICONQUESTION|MB_OKCANCEL ${q(s.appRunning)} IDOK +2
|
|
645
|
+
Abort
|
|
646
|
+
\${EndIf}`
|
|
647
|
+
: ''
|
|
648
|
+
}
|
|
649
|
+
Call SetProgressText
|
|
650
|
+
FunctionEnd
|
|
651
|
+
|
|
652
|
+
Function SetProgressText
|
|
653
|
+
StrCpy $DoneTitle ${q(s.setupDoneTitle)}
|
|
654
|
+
StrCpy $DoneText ${q(s.setupDoneText)}
|
|
655
|
+
\${If} $Action == "uninstall"
|
|
656
|
+
StrCpy $ProgressTitle ${q(s.setupUninstallingTitle)}
|
|
657
|
+
StrCpy $ProgressText ${q(s.setupUninstallingText)}
|
|
658
|
+
\${ElseIf} $Action == "repair"
|
|
659
|
+
StrCpy $ProgressTitle ${q(s.setupRepairingTitle)}
|
|
660
|
+
StrCpy $ProgressText ${q(s.setupRepairingText)}
|
|
661
|
+
\${Else}
|
|
662
|
+
StrCpy $ProgressTitle ${q(s.setupInstallingTitle)}
|
|
663
|
+
StrCpy $ProgressText ${q(s.setupInstallingText)}
|
|
664
|
+
\${EndIf}
|
|
665
|
+
FunctionEnd
|
|
666
|
+
|
|
667
|
+
Section "-${app.id}"
|
|
668
|
+
\${If} $Action == ""
|
|
669
|
+
DetailPrint ${q(s.setupBlockedState)}
|
|
670
|
+
Abort
|
|
671
|
+
\${EndIf}
|
|
672
|
+
Call SetProgressText
|
|
673
|
+
ReadINIStr $R7 "${ini}" "install$Index" state
|
|
674
|
+
IntOp $R1 $Index + 1
|
|
675
|
+
StrCpy $R9 '$Action --nsis "${result}" --index $R1${closeApp === 'never' ? '' : ' --close'}'
|
|
676
|
+
\${If} $Purge == 1
|
|
677
|
+
StrCpy $R9 "$R9 --purge"
|
|
678
|
+
\${EndIf}
|
|
679
|
+
Delete "${result}"
|
|
680
|
+
StrCpy $R8 "log"
|
|
681
|
+
Call RunInstaller
|
|
682
|
+
ReadINIStr $R1 "${result}" result ok
|
|
683
|
+
\${If} $R1 != 1
|
|
684
|
+
ReadINIStr $R2 "${result}" result error
|
|
685
|
+
ReadINIStr $R3 "${result}" result hint
|
|
686
|
+
\${If} $R2 == ""
|
|
687
|
+
StrCpy $R2 ${q(s.setupFailed)}
|
|
688
|
+
\${EndIf}
|
|
689
|
+
DetailPrint "$R2"
|
|
690
|
+
\${If} $R3 != ""
|
|
691
|
+
DetailPrint "$R3"
|
|
692
|
+
\${EndIf}
|
|
693
|
+
SetDetailsView show
|
|
694
|
+
MessageBox MB_ICONSTOP|MB_OK "$R2$\\r$\\n$\\r$\\n$R3" /SD IDOK
|
|
695
|
+
Abort
|
|
696
|
+
\${EndIf}
|
|
697
|
+
ReadINIStr $R4 "${result}" result previous
|
|
698
|
+
\${If} $Action == "uninstall"
|
|
699
|
+
StrCpy $FinishText ${q(s.uninstalled)}
|
|
700
|
+
\${ElseIf} $Action == "repair"
|
|
701
|
+
\${AndIf} $R7 == "needs-repair"
|
|
702
|
+
StrCpy $FinishText ${q(s.repaired)}
|
|
703
|
+
\${ElseIf} $R4 == "installed"
|
|
704
|
+
\${AndIf} $Action == "install"
|
|
705
|
+
StrCpy $FinishText ${q(s.updated)}
|
|
706
|
+
\${Else}
|
|
707
|
+
StrCpy $FinishText ${q(s.installed)}
|
|
708
|
+
\${EndIf}
|
|
709
|
+
StrCpy $R5 ${q(s.setupFinishText)}
|
|
710
|
+
StrCpy $FinishText "$FinishText$\\r$\\n$\\r$\\n$R5"
|
|
711
|
+
SectionEnd
|
|
712
|
+
|
|
713
|
+
; Starts the desktop app from its own folder, so this installer's temporary folder can be removed.
|
|
714
|
+
Function LaunchApp
|
|
715
|
+
ReadINIStr $R0 "${ini}" "install$Index" executable
|
|
716
|
+
ReadINIStr $R1 "${ini}" "install$Index" folder
|
|
717
|
+
\${If} \${FileExists} "$R0"
|
|
718
|
+
SetOutPath "$R1"
|
|
719
|
+
Exec '"$R0"'
|
|
720
|
+
SetOutPath "$TEMP"
|
|
721
|
+
\${EndIf}
|
|
722
|
+
FunctionEnd
|
|
723
|
+
`;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/** Text NSIS shows on the license page (its title is the page's heading): UTF-16 with a byte order mark, so any language works. */
|
|
727
|
+
function licenseFile(installer) {
|
|
728
|
+
const text = String(installer.marketing.terms.text).replace(/\r?\n/g, '\r\n');
|
|
729
|
+
return Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(text, 'utf16le')]);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Builds the setup wizard into `out` with makensis. `installer` is installer.json's content and
|
|
734
|
+
* `source` the installer folder. Pictures: `sidebar` (164×314) and `header` (150×57) .bmp files
|
|
735
|
+
* replace the generated ones; `icon` is a PNG (at most 256 px) or .ico.
|
|
736
|
+
*/
|
|
737
|
+
export function buildNsisExe({ makensis, installer, source, out, icon = null, sidebar = null, header = null, company = '', description = '', exeLocations = [] }) {
|
|
738
|
+
const work = mkdtempSync(join(tmpdir(), 'yc-nsis-'));
|
|
739
|
+
try {
|
|
740
|
+
const logo = readLogo(installer.marketing?.logo);
|
|
741
|
+
const files = { sidebar: sidebar ?? join(work, 'sidebar.bmp'), header: header ?? join(work, 'header.bmp'), icon: null, license: null };
|
|
742
|
+
if (!sidebar) writeFileSync(files.sidebar, sidebarBitmap({ accent: installer.marketing?.accent, logo }));
|
|
743
|
+
if (!header) writeFileSync(files.header, headerBitmap({ logo }));
|
|
744
|
+
if (icon) {
|
|
745
|
+
files.icon = join(work, 'icon.ico');
|
|
746
|
+
writeFileSync(files.icon, /\.ico$/i.test(icon) ? readFileSync(icon) : pngToIco(readFileSync(icon)));
|
|
747
|
+
}
|
|
748
|
+
if (installer.marketing?.terms?.text) {
|
|
749
|
+
files.license = join(work, 'license.txt');
|
|
750
|
+
writeFileSync(files.license, licenseFile(installer));
|
|
751
|
+
}
|
|
752
|
+
const script = join(work, 'setup.nsi');
|
|
753
|
+
writeFileSync(script, `${nsisScript({ installer, source, out, files, company, description, exeLocations })}`);
|
|
754
|
+
try {
|
|
755
|
+
execFileSync(makensis, ['-V2', '-INPUTCHARSET', 'UTF8', script], { stdio: 'pipe', windowsHide: true });
|
|
756
|
+
} catch (error) {
|
|
757
|
+
throw new Error(`makensis failed: ${String(error.stdout ?? '').trim() || String(error.stderr ?? '').trim() || error.message}`);
|
|
758
|
+
}
|
|
759
|
+
return out;
|
|
760
|
+
} finally {
|
|
761
|
+
rmSync(work, { recursive: true, force: true });
|
|
762
|
+
}
|
|
763
|
+
}
|