-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
Copy pathbase-distribution.ts
295 lines (248 loc) · 8.38 KB
/
base-distribution.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
import {v4 as uuidv4} from 'uuid';
import * as tc from '@actions/tool-cache';
import * as hc from '@actions/http-client';
import * as core from '@actions/core';
import * as io from '@actions/io';
import semver from 'semver';
import * as assert from 'assert';
import * as path from 'path';
import os from 'os';
import fs from 'fs';
import {NodeInputs, INodeVersion, INodeVersionInfo} from './base-models';
export default abstract class BaseDistribution {
protected httpClient: hc.HttpClient;
protected osPlat = os.platform();
constructor(protected nodeInfo: NodeInputs) {
this.httpClient = new hc.HttpClient('setup-node', [], {
allowRetries: true,
maxRetries: 3
});
}
protected abstract getDistributionUrl(): string;
public async setupNodeJs() {
let nodeJsVersions: INodeVersion[] | undefined;
if (this.nodeInfo.checkLatest) {
const evaluatedVersion = await this.findVersionInDist(nodeJsVersions);
this.nodeInfo.versionSpec = evaluatedVersion;
}
let toolPath = this.findVersionInHostedToolCacheDirectory();
if (toolPath) {
core.info(`Found in cache @ ${toolPath}`);
} else {
const evaluatedVersion = await this.findVersionInDist(nodeJsVersions);
const toolName = this.getNodejsDistInfo(evaluatedVersion);
toolPath = await this.downloadNodejs(toolName);
}
if (this.osPlat != 'win32') {
toolPath = path.join(toolPath, 'bin');
}
core.addPath(toolPath);
}
protected async findVersionInDist(nodeJsVersions?: INodeVersion[]) {
if (!nodeJsVersions) {
nodeJsVersions = await this.getNodeJsVersions();
}
const versions = this.filterVersions(nodeJsVersions);
const evaluatedVersion = this.evaluateVersions(versions);
if (!evaluatedVersion) {
throw new Error(
`Unable to find Node version '${this.nodeInfo.versionSpec}' for platform ${this.osPlat} and architecture ${this.nodeInfo.arch}.`
);
}
return evaluatedVersion;
}
protected evaluateVersions(versions: string[]): string {
let version = '';
const {range, options} = this.validRange(this.nodeInfo.versionSpec);
core.debug(`evaluating ${versions.length} versions`);
for (const potential of versions) {
const satisfied: boolean = semver.satisfies(potential, range, options);
if (satisfied) {
version = potential;
break;
}
}
if (version) {
core.debug(`matched: ${version}`);
} else {
core.debug('match not found');
}
return version;
}
protected findVersionInHostedToolCacheDirectory() {
return tc.find(
'node',
this.nodeInfo.versionSpec,
this.translateArchToDistUrl(this.nodeInfo.arch)
);
}
protected async getNodeJsVersions(): Promise<INodeVersion[]> {
const initialUrl = this.getDistributionUrl();
const dataUrl = `${initialUrl}/index.json`;
const response = await this.httpClient.getJson<INodeVersion[]>(dataUrl);
return response.result || [];
}
protected getNodejsDistInfo(version: string) {
const osArch: string = this.translateArchToDistUrl(this.nodeInfo.arch);
version = semver.clean(version) || '';
const fileName: string =
this.osPlat == 'win32'
? `node-v${version}-win-${osArch}`
: `node-v${version}-${this.osPlat}-${osArch}`;
const urlFileName: string =
this.osPlat == 'win32' ? `${fileName}.7z` : `${fileName}.tar.gz`;
const initialUrl = this.getDistributionUrl();
const url = `${initialUrl}/v${version}/${urlFileName}`;
return <INodeVersionInfo>{
downloadUrl: url,
resolvedVersion: version,
arch: osArch,
fileName: fileName
};
}
protected async downloadNodejs(info: INodeVersionInfo) {
let downloadPath = '';
core.info(
`Acquiring ${info.resolvedVersion} - ${info.arch} from ${info.downloadUrl}`
);
try {
downloadPath = await tc.downloadTool(info.downloadUrl);
} catch (err) {
if (
err instanceof tc.HTTPError &&
err.httpStatusCode == 404 &&
this.osPlat == 'win32'
) {
return await this.acquireWindowsNodeFromFallbackLocation(
info.resolvedVersion,
info.arch
);
}
throw err;
}
const toolPath = await this.extractArchive(downloadPath, info);
core.info('Done');
return toolPath;
}
protected validRange(versionSpec: string) {
let options: semver.RangeOptions | undefined;
const c = semver.clean(versionSpec) || '';
const valid = semver.valid(c) ?? versionSpec;
return {range: valid, options};
}
protected async acquireWindowsNodeFromFallbackLocation(
version: string,
arch: string = os.arch()
): Promise<string> {
const initialUrl = this.getDistributionUrl();
const osArch: string = this.translateArchToDistUrl(arch);
// Create temporary folder to download to
const tempDownloadFolder = `temp_${uuidv4()}`;
const tempDirectory = process.env['RUNNER_TEMP'] || '';
assert.ok(tempDirectory, 'Expected RUNNER_TEMP to be defined');
const tempDir: string = path.join(tempDirectory, tempDownloadFolder);
await io.mkdirP(tempDir);
let exeUrl: string;
let libUrl: string;
try {
exeUrl = `${initialUrl}/v${version}/win-${osArch}/node.exe`;
libUrl = `${initialUrl}/v${version}/win-${osArch}/node.lib`;
core.info(`Downloading only node binary from ${exeUrl}`);
const exePath = await tc.downloadTool(exeUrl);
await io.cp(exePath, path.join(tempDir, 'node.exe'));
const libPath = await tc.downloadTool(libUrl);
await io.cp(libPath, path.join(tempDir, 'node.lib'));
} catch (err) {
if (err instanceof tc.HTTPError && err.httpStatusCode == 404) {
exeUrl = `${initialUrl}/v${version}/node.exe`;
libUrl = `${initialUrl}/v${version}/node.lib`;
const exePath = await tc.downloadTool(exeUrl);
await io.cp(exePath, path.join(tempDir, 'node.exe'));
const libPath = await tc.downloadTool(libUrl);
await io.cp(libPath, path.join(tempDir, 'node.lib'));
} else {
throw err;
}
}
const toolPath = await tc.cacheDir(tempDir, 'node', version, arch);
return toolPath;
}
protected async extractArchive(
downloadPath: string,
info: INodeVersionInfo | null
) {
//
// Extract
//
core.info('Extracting ...');
let extPath: string;
info = info || ({} as INodeVersionInfo); // satisfy compiler, never null when reaches here
if (this.osPlat == 'win32') {
const _7zPath = path.join(__dirname, '../..', 'externals', '7zr.exe');
extPath = await tc.extract7z(downloadPath, undefined, _7zPath);
// 7z extracts to folder matching file name
const nestedPath = path.join(
extPath,
path.basename(info.fileName, '.7z')
);
if (fs.existsSync(nestedPath)) {
extPath = nestedPath;
}
} else {
extPath = await tc.extractTar(downloadPath, undefined, [
'xz',
'--strip',
'1'
]);
}
//
// Install into the local tool cache - node extracts with a root folder that matches the fileName downloaded
//
core.info('Adding to the cache ...');
const toolPath = await tc.cacheDir(
extPath,
'node',
info.resolvedVersion,
info.arch
);
return toolPath;
}
protected getDistFileName(): string {
const osArch: string = this.translateArchToDistUrl(this.nodeInfo.arch);
// node offers a json list of versions
let dataFileName: string;
switch (this.osPlat) {
case 'linux':
dataFileName = `linux-${osArch}`;
break;
case 'darwin':
dataFileName = `osx-${osArch}-tar`;
break;
case 'win32':
dataFileName = `win-${osArch}-exe`;
break;
default:
throw new Error(`Unexpected OS '${this.osPlat}'`);
}
return dataFileName;
}
protected filterVersions(nodeJsVersions: INodeVersion[]) {
const versions: string[] = [];
const dataFileName = this.getDistFileName();
nodeJsVersions.forEach((nodeVersion: INodeVersion) => {
// ensure this version supports your os and platform
if (nodeVersion.files.indexOf(dataFileName) >= 0) {
versions.push(nodeVersion.version);
}
});
return versions.sort(semver.rcompare);
}
protected translateArchToDistUrl(arch: string): string {
switch (arch) {
case 'arm':
return 'armv7l';
default:
return arch;
}
}
}