-
Notifications
You must be signed in to change notification settings - Fork 359
Expand file tree
/
Copy pathpackage.ts
More file actions
276 lines (227 loc) · 9.82 KB
/
package.ts
File metadata and controls
276 lines (227 loc) · 9.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
/**
* This script builds the distributable packages. It assumes that we have _just_
* built the JavaScript parts.
*/
'use strict';
import childProcess from 'child_process';
import fs from 'fs';
import * as path from 'path';
import { flipFuses, FuseV1Options, FuseVersion } from '@electron/fuses';
import { executeAppBuilder, log } from 'builder-util';
import {
AfterPackContext, Arch, build, CliOptions, Configuration, LinuxTargetSpecificOptions,
} from 'electron-builder';
import _ from 'lodash';
import plist from 'plist';
import yaml from 'yaml';
import buildUtils from './lib/build-utils';
import buildInstaller, { buildCustomAction } from './lib/installer-win32';
import { ReadWrite } from '@pkg/utils/typeUtils';
class Builder {
async replaceInFile(srcFile: string, pattern: string | RegExp, replacement: string, dstFile?: string) {
dstFile = dstFile || srcFile;
await fs.promises.stat(srcFile);
const data = await fs.promises.readFile(srcFile, 'utf8');
await fs.promises.writeFile(dstFile, data.replace(pattern, replacement));
}
protected get electronBinary() {
const platformPath = {
darwin: [`mac-${ buildUtils.arch }`, 'Rancher Desktop.app/Contents/MacOS/Rancher Desktop'],
win32: ['win-unpacked', 'Rancher Desktop.exe'],
}[process.platform as string];
if (!platformPath) {
throw new Error('Failed to find platform-specific Electron binary');
}
return path.join(buildUtils.distDir, ...platformPath);
}
/**
* Flip the Electron fuses so that the app can't be used as a node runtime.
* @see https://www.electronjs.org/docs/latest/tutorial/fuses
*/
protected async flipFuses(context: AfterPackContext) {
const extension = {
darwin: '.app',
win32: '.exe',
}[context.electronPlatformName] ?? '';
const exeName = `${ context.packager.appInfo.productFilename }${ extension }`;
const exePath = path.join(context.appOutDir, exeName);
const resetAdHocDarwinSignature = context.arch === Arch.arm64;
const integrityEnabled = context.electronPlatformName === 'darwin';
await flipFuses(
exePath,
{
version: FuseVersion.V1,
resetAdHocDarwinSignature,
[FuseV1Options.RunAsNode]: false,
[FuseV1Options.EnableCookieEncryption]: false,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: integrityEnabled,
[FuseV1Options.OnlyLoadAppFromAsar]: true,
},
);
}
/**
* Manually write out the Linux .desktop application shortcut definition; this
* is needed as by default this only happens for snap/fpm/etc., but not zip
* files.
*/
protected async writeLinuxDesktopFile(context: AfterPackContext) {
const { LinuxPackager } = await import('app-builder-lib/out/linuxPackager');
const { LinuxTargetHelper } = await import('app-builder-lib/out/targets/LinuxTargetHelper');
const config = context.packager.config.linux;
if (!(context.packager instanceof LinuxPackager) || !config) {
return;
}
const options: LinuxTargetSpecificOptions = {
...context.packager.platformSpecificBuildOptions,
compression: undefined,
};
const helper = new LinuxTargetHelper(context.packager);
const leaf = `${ context.packager.executableName }.desktop`;
const destination = path.join(context.appOutDir, `resources/resources/linux/${ leaf }`);
await helper.writeDesktopEntry(options, context.packager.executableName, destination);
}
/**
* Edit the application's `Info.plist` file to remove the UsageDescription
* keys; there is no reason for the application to get any of those permissions.
*/
protected async removeMacUsageDescriptions(context: AfterPackContext) {
const { MacPackager } = await import('app-builder-lib/out/macPackager');
const { packager } = context;
const config = packager.config.mac;
if (!(packager instanceof MacPackager) || !config) {
return;
}
const { productFilename } = packager.appInfo;
const plistPath = path.join(context.appOutDir, `${ productFilename }.app`, 'Contents', 'Info.plist');
const plistContents = await fs.promises.readFile(plistPath, 'utf-8');
const plistData = plist.parse(plistContents);
if (typeof plistData !== 'object' || !('CFBundleName' in plistData)) {
return;
}
const plistCopy: Record<string, plist.PlistValue> = structuredClone(plistData);
for (const key in plistData) {
if (/^NS.*UsageDescription$/.test(key)) {
delete plistCopy[key];
}
}
await fs.promises.writeFile(plistPath, plist.build(plistCopy), 'utf-8');
}
protected async afterPack(context: AfterPackContext) {
await this.flipFuses(context);
await this.writeLinuxDesktopFile(context);
await this.removeMacUsageDescriptions(context);
}
async package(): Promise<CliOptions> {
log.info('Packaging...');
// Build the electron builder configuration to include the version data
const config: ReadWrite<Configuration> = yaml.parse(await fs.promises.readFile('packaging/electron-builder.yml', 'utf-8'));
const configPath = path.join(buildUtils.distDir, 'electron-builder.yaml');
const fullBuildVersion = childProcess.execFileSync('git', ['describe', '--tags']).toString().trim();
const finalBuildVersion = fullBuildVersion.replace(/^v/, '');
const distDir = path.join(process.cwd(), 'dist');
const electronPlatform = ({
darwin: 'mac',
win32: 'win',
linux: 'linux',
} as const)[process.platform as string];
if (!electronPlatform) {
throw new Error(`Packaging for ${ process.platform } is not supported`);
}
switch (electronPlatform) {
case 'linux':
await this.createLinuxResources(finalBuildVersion);
break;
case 'win':
await this.createWindowsResources(distDir);
break;
}
// When there are files (e.g., extraFiles or extraResources) specified at both
// the top-level and platform-specific levels, we need to combine them
// and place the combined list at the top level. This approach enables us to have
// platform-specific exclusions, since the two lists are initially processed
// separately and then merged together afterward.
for (const key of ['files', 'extraFiles', 'extraResources'] as const) {
const section = config[electronPlatform];
const items = config[key];
const overrideItems = section?.[key];
if (!section || !Array.isArray(items) || !Array.isArray(overrideItems)) {
continue;
}
config[key] = items.concat(overrideItems);
delete section[key];
}
_.set(config, 'extraMetadata.version', finalBuildVersion);
await fs.promises.writeFile(configPath, yaml.stringify(config), 'utf-8');
config.afterPack = this.afterPack.bind(this);
const options: CliOptions = {
config,
publish: 'never',
arm64: buildUtils.arch === 'arm64',
x64: buildUtils.arch === 'x64',
};
if (electronPlatform) {
if (process.argv.includes('--zip')) {
options[electronPlatform] = ['zip'];
} else {
const rawTarget = config[electronPlatform]?.target ?? [];
const target = Array.isArray(rawTarget) ? rawTarget : [rawTarget];
options[electronPlatform] = target.map(t => typeof t === 'string' ? t : t.target);
}
}
await build(options);
return options;
}
async buildInstaller(config: CliOptions) {
const appDir = path.join(buildUtils.distDir, 'win-unpacked');
const { version } = (config.config as any).extraMetadata;
const installerPath = path.join(buildUtils.distDir, `Rancher.Desktop.Setup.${ version }.msi`);
if (config.win && !process.argv.includes('--zip')) {
// Only build installer if we're not asked not to.
await buildInstaller(buildUtils.distDir, appDir, installerPath);
}
}
protected async createLinuxResources(finalBuildVersion: string) {
const appData = 'packaging/linux/rancher-desktop.appdata.xml';
const release = `<release version="${ finalBuildVersion }" date="${ new Date().toISOString() }"/>`;
await this.replaceInFile(appData, /<release.*\/>/g, release, appData.replace('packaging', 'resources'));
}
protected async createWindowsResources(workDir: string) {
// Create stub executable with the correct icon (for the installer)
const imageFile = path.join(process.cwd(), 'resources', 'icons', 'logo-square-512.png');
const iconArgs = ['icon', '--format', 'ico', '--out', workDir, '--input', imageFile];
const iconResult = await this.executeAppBuilderAsJson(iconArgs);
const iconFile = iconResult.icons[0].file;
const executable = path.join(process.cwd(), 'resources', 'win32', 'bin', 'rdctl.exe');
const rceditArgs = [executable, '--set-icon', iconFile];
await executeAppBuilder(['rcedit', '--args', JSON.stringify(rceditArgs)], undefined, undefined, 3);
// Create the custom action for the installer
log.info('building Windows Installer custom action...');
const customActionFile = await buildCustomAction();
// Wait for the virus scanner to be done with the new DLL file
for (let i = 0; i < 30; i++) {
try {
await fs.promises.readFile(customActionFile);
break;
} catch {
await buildUtils.sleep(5_000);
}
}
}
protected async executeAppBuilderAsJson(...args: Parameters<typeof executeAppBuilder>) {
const result = JSON.parse(await executeAppBuilder(...args));
if (result.error) {
throw new Error(result.error);
}
return result;
}
async run() {
const options = await this.package();
await this.buildInstaller(options);
}
}
(new Builder()).run().catch((e) => {
console.error(e);
process.exit(1);
});