-
Notifications
You must be signed in to change notification settings - Fork 175
/
module-rebuilder.ts
321 lines (268 loc) · 10.5 KB
/
module-rebuilder.ts
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
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
import * as debug from 'debug';
import * as detectLibc from 'detect-libc';
import * as fs from 'fs-extra';
import * as NodeGyp from 'node-gyp';
import * as path from 'path';
import { cacheModuleState } from './cache';
import { promisify } from 'util';
import { readPackageJson } from './read-package-json';
import { Rebuilder } from './rebuild';
import { spawn } from '@malept/cross-spawn-promise';
import { ELECTRON_GYP_DIR } from './constants';
import { getClangEnvironmentVars } from './clang-fetcher';
const d = debug('electron-rebuild');
const locateBinary = async (basePath: string, suffix: string): Promise<string | null> => {
let parentPath = basePath;
let testPath: string | undefined;
while (testPath !== parentPath) {
testPath = parentPath;
const checkPath = path.resolve(testPath, suffix);
if (await fs.pathExists(checkPath)) {
return checkPath;
}
parentPath = path.resolve(testPath, '..');
}
return null;
};
async function locatePrebuild(modulePath: string): Promise<string | null> {
return await locateBinary(modulePath, 'node_modules/prebuild-install/bin.js');
}
type PackageJSONValue = string | Record<string, unknown>;
export enum BuildType {
Debug = 'Debug',
Release = 'Release',
}
export class ModuleRebuilder {
private modulePath: string;
private packageJSON: Record<string, PackageJSONValue | undefined>;
private rebuilder: Rebuilder;
constructor(rebuilder: Rebuilder, modulePath: string) {
this.modulePath = modulePath;
this.rebuilder = rebuilder;
}
get buildType(): BuildType {
return this.rebuilder.debug ? BuildType.Debug : BuildType.Release;
}
get metaPath(): string {
return path.resolve(this.modulePath, 'build', this.buildType, '.forge-meta');
}
get metaData(): string {
return `${this.rebuilder.arch}--${this.rebuilder.ABI}`;
}
get moduleName(): string {
return path.basename(this.modulePath);
}
async alreadyBuiltByRebuild(): Promise<boolean> {
if (await fs.pathExists(this.metaPath)) {
const meta = await fs.readFile(this.metaPath, 'utf8');
return meta === this.metaData;
}
return false;
}
async buildNodeGypArgs(prefixedArgs: string[]): Promise<string[]> {
const args = [
'node',
'node-gyp',
'rebuild',
...prefixedArgs,
`--runtime=electron`,
`--target=${this.rebuilder.electronVersion}`,
`--arch=${this.rebuilder.arch}`,
`--dist-url=${this.rebuilder.headerURL}`,
'--build-from-source'
];
if (process.env.DEBUG) {
args.push('--verbose');
}
if (this.rebuilder.debug) {
args.push('--debug');
}
args.push(...(await this.buildNodeGypArgsFromBinaryField()));
if (this.rebuilder.msvsVersion) {
args.push(`--msvs_version=${this.rebuilder.msvsVersion}`);
}
return args;
}
async buildNodeGypArgsFromBinaryField(): Promise<string[]> {
const binary = await this.packageJSONFieldWithDefault('binary', {}) as Record<string, string>;
const flags = await Promise.all(Object.entries(binary).map(async ([binaryKey, binaryValue]) => {
if (binaryKey === 'napi_versions') {
return;
}
let value = binaryValue
if (binaryKey === 'module_path') {
value = path.resolve(this.modulePath, value);
}
value = value.replace('{configuration}', this.buildType)
.replace('{node_abi}', `electron-v${this.rebuilder.electronVersion.split('.').slice(0, 2).join('.')}`)
.replace('{platform}', process.platform)
.replace('{arch}', this.rebuilder.arch)
.replace('{version}', await this.packageJSONField('version') as string)
.replace('{libc}', detectLibc.family || 'unknown');
for (const [replaceKey, replaceValue] of Object.entries(binary)) {
value = value.replace(`{${replaceKey}}`, replaceValue);
}
return `--${binaryKey}=${value}`;
}))
return flags.filter(value => value) as string[];
}
async cacheModuleState(cacheKey: string): Promise<void> {
if (this.rebuilder.useCache) {
await cacheModuleState(this.modulePath, this.rebuilder.cachePath, cacheKey);
}
}
async isPrebuildNativeModule(): Promise<boolean> {
const dependencies = await this.packageJSONFieldWithDefault('dependencies', {});
return !!dependencies['prebuild-install']
}
async packageJSONFieldWithDefault(key: string, defaultValue: PackageJSONValue): Promise<PackageJSONValue> {
const result = await this.packageJSONField(key);
return result === undefined ? defaultValue : result;
}
async packageJSONField(key: string): Promise<PackageJSONValue | undefined> {
this.packageJSON ||= await readPackageJson(this.modulePath);
return this.packageJSON[key];
}
/**
* Whether a prebuild-based native module exists.
*/
async prebuildNativeModuleExists(): Promise<boolean> {
return fs.pathExists(path.resolve(this.modulePath, 'prebuilds', `${process.platform}-${this.rebuilder.arch}`, `electron-${this.rebuilder.ABI}.node`))
}
private restoreEnv(env: Record<string, string | undefined>): void {
const gotKeys = new Set<string>(Object.keys(process.env));
const expectedKeys = new Set<string>(Object.keys(env));
for (const key of Object.keys(process.env)) {
if (!expectedKeys.has(key)) {
delete process.env[key];
} else if (env[key] !== process.env[key]) {
process.env[key] = env[key];
}
}
for (const key of Object.keys(env)) {
if (!gotKeys.has(key)) {
process.env[key] = env[key];
}
}
}
async rebuildNodeGypModule(cacheKey: string): Promise<void> {
if (this.modulePath.includes(' ')) {
console.error('Attempting to build a module with a space in the path');
console.error('See https://github.com/nodejs/node-gyp/issues/65#issuecomment-368820565 for reasons why this may not work');
// FIXME: Re-enable the throw when more research has been done
// throw new Error(`node-gyp does not support building modules with spaces in their path, tried to build: ${modulePath}`);
}
let env: Record<string, string | undefined>;
const extraNodeGypArgs: string[] = [];
if (this.rebuilder.useElectronClang) {
env = { ...process.env };
const { env: clangEnv, args: clangArgs } = await getClangEnvironmentVars(this.rebuilder.electronVersion, this.rebuilder.arch);
Object.assign(process.env, clangEnv);
extraNodeGypArgs.push(...clangArgs);
}
const nodeGypArgs = await this.buildNodeGypArgs(extraNodeGypArgs);
d('rebuilding', this.moduleName, 'with args', nodeGypArgs);
const nodeGyp = NodeGyp();
nodeGyp.parseArgv(nodeGypArgs);
nodeGyp.devDir = ELECTRON_GYP_DIR;
let command = nodeGyp.todo.shift();
const originalWorkingDir = process.cwd();
try {
process.chdir(this.modulePath);
while (command) {
if (command.name === 'configure') {
command.args = command.args.filter((arg: string) => !extraNodeGypArgs.includes(arg));
} else if (command.name === 'build' && process.platform === 'win32') {
// This is disgusting but it prevents node-gyp from destroying our MSBuild arguments
command.args.map = (fn: (arg: string) => string) => {
return Array.prototype.map.call(command.args, (arg: string) => {
if (arg.startsWith('/p:')) return arg;
return fn(arg);
});
}
}
await promisify(nodeGyp.commands[command.name])(command.args);
command = nodeGyp.todo.shift();
}
} catch (err) {
let errorMessage = `node-gyp failed to rebuild '${this.modulePath}'.\n`;
errorMessage += `Error: ${err.message || err}\n\n`;
throw new Error(errorMessage);
} finally {
process.chdir(originalWorkingDir);
}
d('built:', this.moduleName);
await this.writeMetadata();
await this.replaceExistingNativeModule();
await this.cacheModuleState(cacheKey);
if (this.rebuilder.useElectronClang) {
this.restoreEnv(env!);
}
}
async rebuildPrebuildModule(cacheKey: string): Promise<boolean> {
if (!(await this.isPrebuildNativeModule())) {
return false;
}
d(`assuming is prebuild powered: ${this.moduleName}`);
const prebuildInstallPath = await locatePrebuild(this.modulePath);
if (prebuildInstallPath) {
d(`triggering prebuild download step: ${this.moduleName}`);
let success = false;
try {
await this.runPrebuildInstall(prebuildInstallPath);
success = true;
} catch (err) {
d('failed to use prebuild-install:', err);
}
if (success) {
d('built:', this.moduleName);
await this.writeMetadata();
await this.cacheModuleState(cacheKey);
return true;
}
} else {
d(`could not find prebuild-install relative to: ${this.modulePath}`);
}
return false;
}
async replaceExistingNativeModule(): Promise<void> {
const buildLocation = path.resolve(this.modulePath, 'build', this.buildType);
d('searching for .node file', buildLocation);
const buildLocationFiles = await fs.readdir(buildLocation);
d('testing files', buildLocationFiles);
const nodeFile = buildLocationFiles.find((file) => file !== '.node' && file.endsWith('.node'));
const nodePath = nodeFile ? path.resolve(buildLocation, nodeFile) : undefined;
if (nodePath && await fs.pathExists(nodePath)) {
d('found .node file', nodePath);
if (!this.rebuilder.disablePreGypCopy) {
const abiPath = path.resolve(this.modulePath, `bin/${process.platform}-${this.rebuilder.arch}-${this.rebuilder.ABI}`);
d('copying to prebuilt place:', abiPath);
await fs.ensureDir(abiPath);
await fs.copy(nodePath, path.resolve(abiPath, `${this.moduleName}.node`));
}
}
}
async runPrebuildInstall(prebuildInstallPath: string): Promise<void> {
const shimExt = process.env.ELECTRON_REBUILD_TESTS ? 'ts' : 'js';
const executable = process.env.ELECTRON_REBUILD_TESTS ? path.resolve(__dirname, '..', 'node_modules', '.bin', 'ts-node') : process.execPath;
await spawn(
executable,
[
path.resolve(__dirname, `prebuild-shim.${shimExt}`),
prebuildInstallPath,
`--arch=${this.rebuilder.arch}`,
`--platform=${process.platform}`,
'--runtime=electron',
`--target=${this.rebuilder.electronVersion}`,
`--tag-prefix=${this.rebuilder.prebuildTagPrefix}`
],
{
cwd: this.modulePath,
}
);
}
async writeMetadata(): Promise<void> {
await fs.ensureDir(path.dirname(this.metaPath));
await fs.writeFile(this.metaPath, this.metaData);
}
}