-
Notifications
You must be signed in to change notification settings - Fork 40
/
main.js
463 lines (402 loc) · 15.2 KB
/
main.js
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
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
import cache from '@actions/cache';
import core from '@actions/core';
import io from '@actions/io';
import exec from '@actions/exec';
import tc from '@actions/tool-cache';
import path from 'node:path';
import fs from 'node:fs';
import crypto from 'node:crypto';
import assert from 'node:assert/strict';
import process from 'node:process';
import { hashElement } from 'folder-hash';
// XXX: hack to make ncc copy those files to dist
// eslint-disable-next-line
function dummy() {
return [__dirname + '/action.yml', __dirname + '/README.md'];
}
const INSTALLER_VERSION = '2024-12-08';
const INSTALLER_URL = `https://github.com/msys2/msys2-installer/releases/download/${INSTALLER_VERSION}/msys2-base-x86_64-${INSTALLER_VERSION.replace(/-/g, '')}.sfx.exe`;
const INSTALLER_CHECKSUM = '66502c086115131b8a3304d834471b5ac9981efb5c3c329952b1f27a8ca6b5b4';
// see https://github.com/msys2/setup-msys2/issues/61
const INSTALL_CACHE_ENABLED = false;
const CACHE_FLUSH_COUNTER = 0;
class Input {
constructor() {
/** @type {boolean} */
this.release;
/** @type {boolean} */
this.update;
/** @type {string} */
this.pathtype;
/** @type {string} */
this.msystem;
/** @type {string[]} */
this.install;
/** @type {string[]} */
this.pacboy;
/** @type {string} */
this.platformcheckseverity;
/** @type {string} */
this.location;
/** @type {boolean} */
this.cache;
}
}
/**
* @returns {Input}
*/
function parseInput() {
let p_release = core.getBooleanInput('release');
let p_update = core.getBooleanInput('update');
let p_pathtype = core.getInput('path-type');
let p_msystem = core.getInput('msystem');
let p_install = core.getInput('install');
let p_pacboy = core.getInput('pacboy');
let p_platformcheckseverity = core.getInput('platform-check-severity');
let p_location = core.getInput('location');
let p_cache = core.getBooleanInput('cache');
const msystem_allowed = ['MSYS', 'MINGW32', 'MINGW64', 'UCRT64', 'CLANG64', 'CLANGARM64'];
if (!msystem_allowed.includes(p_msystem.toUpperCase())) {
throw new Error(`'msystem' needs to be one of ${ msystem_allowed.join(', ') }, got ${p_msystem}`);
}
p_msystem = p_msystem.toUpperCase()
let p_install_list = (p_install === 'false') ? [] : p_install.split(/\s+/);
let p_pacboy_list = (p_pacboy === 'false') ? [] : p_pacboy.split(/\s+/);
const platformcheckseverity_allowed = ['fatal', 'warn'];
if (!platformcheckseverity_allowed.includes(p_platformcheckseverity)) {
throw new Error(`'platform-check-severity' needs to be one of ${ platformcheckseverity_allowed.join(', ') }, got ${p_platformcheckseverity}`);
}
if ( process.platform === 'win32' && (p_location === 'C:\\' || p_location === 'C:') ) {
throw new Error(`'location' cannot be 'C:' because that contains the built-in MSYS2 installation
in GitHub Actions environments. See option 'release', in order to use that installation:
https://github.com/msys2/setup-msys2#release`);
}
let input = new Input();
input.release = p_release;
input.update = p_update;
input.pathtype = p_pathtype;
input.msystem = p_msystem;
input.install = p_install_list;
input.pacboy = p_pacboy_list;
input.platformcheckseverity = p_platformcheckseverity;
input.location = (p_location == "RUNNER_TEMP") ? process.env['RUNNER_TEMP'] : p_location;
input.cache = p_cache;
return input;
}
/**
* @param {string} filePath
* @returns {Promise<string>}
*/
async function computeChecksum(filePath) {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('sha256');
const stream = fs.createReadStream(filePath);
stream.on('data', data => {
hash.update(data);
});
stream.on('end', () => {
const fileHash = hash.digest('hex');
resolve(fileHash);
});
stream.on('error', error => {
reject(error);
});
});
}
/**
* @returns {Promise<string>}
*/
async function downloadInstaller() {
// We use the last field only, so that each version is ensured semver incompatible with the previous one.
const version = `0.0.${INSTALLER_VERSION.replace(/-/g, '')}`
const inst_path = tc.find('msys2-installer', version, 'x64');
const destination = inst_path ? path.join(inst_path, 'base.exe') : await tc.downloadTool(INSTALLER_URL);
let computedChecksum = await computeChecksum(destination);
if (computedChecksum.toUpperCase() !== INSTALLER_CHECKSUM.toUpperCase()) {
throw new Error(`The SHA256 of the installer does not match! expected ${INSTALLER_CHECKSUM} got ${computedChecksum}`);
}
return path.join(inst_path || await tc.cacheFile(destination, 'base.exe', 'msys2-installer', version, 'x64'), 'base.exe');
}
/**
* @param {string} msysRootDir
* @returns {Promise<void>}
*/
async function disableKeyRefresh(msysRootDir) {
const postFile = path.join(msysRootDir, 'etc\\post-install\\07-pacman-key.post');
const content = await fs.promises.readFile(postFile, 'utf8');
const newContent = content.replace('--refresh-keys', '--version');
await fs.promises.writeFile(postFile, newContent, 'utf8');
}
/**
* @param {string[]} paths
* @param {string} restoreKey
* @param {string} saveKey
* @returns {Promise<number|undefined>}
*/
async function saveCacheMaybe(paths, restoreKey, saveKey) {
if (restoreKey === saveKey) {
console.log(`Cache unchanged, skipping save for ${saveKey}`);
return;
}
let cacheId;
try {
cacheId = await cache.saveCache(paths, saveKey);
} catch (error) {
// In case we try to save a cache for a key that already exists we'll get an error.
// This usually happens because something created the same cache while we were running.
// Since the cache is already there now this is fine with us.
console.log(error.message);
}
if (cacheId !== undefined) {
console.log(`Cache saved as ID ${cacheId} using key ${saveKey}`);
}
return cacheId;
}
/**
* @param {string[]} paths
* @param {string} primaryKey
* @param {string[]} restoreKeys
* @returns {Promise<string|undefined>}
*/
async function restoreCache(paths, primaryKey, restoreKeys) {
let restoreKey;
try {
restoreKey = await cache.restoreCache(paths, primaryKey, restoreKeys);
console.log(`Cache restore for ${primaryKey}, got ${restoreKey}`);
} catch (error) {
core.warning(`Restore cache failed: ${error.message}`);
} finally {
console.log(`Cache restore for ${primaryKey}, got ${restoreKey}`);
}
return restoreKey;
}
/**
* @param {string} path
* @returns {Promise<string>}
*/
async function hashPath(path) {
return (await hashElement(path, {encoding: 'hex'}))['hash'].toString();
}
class PackageCache {
/**
* @param {string} msysRootDir
* @param {Input} input
*/
constructor(msysRootDir, input) {
// We include "update" in the fallback key so that a job run with update=false never fetches
// a cache created with update=true. Because this would mean a newer version than needed is in the cache
// which would never be used but also would win during cache prunging because it is newer.
this.fallbackCacheKey = 'msys2-pkgs-upd:' + input.update.toString();
// We want a cache key that is ideally always the same for the same kind of job.
// So that mingw32 and ming64 jobs, and jobs with different install packages have different caches.
let shasum = crypto.createHash('sha1');
shasum.update([CACHE_FLUSH_COUNTER, input.release, input.update, input.pathtype, input.msystem, input.install].toString() + INSTALLER_CHECKSUM);
this.jobCacheKey = this.fallbackCacheKey + '-conf:' + shasum.digest('hex').slice(0, 8);
this.restoreKey = undefined;
this.pkgCachePath = path.join(msysRootDir, 'var', 'cache', 'pacman', 'pkg');
}
async restore() {
// We ideally want a cache matching our configuration, but every cache is OK since we prune it later anyway
this.restoreKey = await restoreCache([this.pkgCachePath], this.jobCacheKey, [this.jobCacheKey, this.fallbackCacheKey]);
return (this.restoreKey !== undefined)
}
async save() {
const saveKey = this.jobCacheKey + '-files:' + await hashPath(this.pkgCachePath);
const cacheId = await saveCacheMaybe([this.pkgCachePath], this.restoreKey, saveKey);
return (cacheId !== undefined);
}
async prune() {
// Remove all uninstalled packages
await runMsys(['paccache', '-r', '-f', '-u', '-k0']);
// Keep the newest for all other packages
await runMsys(['paccache', '-r', '-f', '-k1']);
}
async clear() {
// Remove all cached packages
await pacman(['-Scc']);
}
}
class InstallCache {
/**
* @param {string} msysRootDir
* @param {Input} input
*/
constructor(msysRootDir, input) {
let shasum = crypto.createHash('sha1');
shasum.update(JSON.stringify(input) + INSTALLER_CHECKSUM);
this.jobCacheKey = 'msys2-inst-conf:' + shasum.digest('hex');
this.msysRootDir = msysRootDir
}
async restore() {
// We only want a cache which matches our configuration
this.restoreKey = await restoreCache([this.msysRootDir], this.jobCacheKey, [this.jobCacheKey]);
return (this.restoreKey !== undefined)
}
async save() {
// In cases any of the installed packages have changed we get something new here
const pacmanStateDir = path.join(this.msysRootDir, 'var', 'lib', 'pacman', 'local');
const saveKey = this.jobCacheKey + '-state:' + await hashPath(pacmanStateDir);
const cacheId = await saveCacheMaybe([this.msysRootDir], this.restoreKey, saveKey);
return (cacheId !== undefined);
}
}
let cmd = null;
/**
* @param {string} msysRootDir
* @param {string} msystem
* @param {string} pathtype
* @param {string} destDir
* @param {string} name
*/
async function writeWrapper(msysRootDir, msystem, pathtype, destDir, name) {
let wrap = [
`@echo off`,
`setlocal`,
`IF NOT DEFINED MSYSTEM set MSYSTEM=` + msystem,
`IF NOT DEFINED MSYS2_PATH_TYPE set MSYS2_PATH_TYPE=` + pathtype,
`set CHERE_INVOKING=1`,
msysRootDir + `\\usr\\bin\\bash.exe -leo pipefail %*`
].join('\r\n');
cmd = path.join(destDir, name);
fs.writeFileSync(cmd, wrap);
}
/**
* @param {string[]} args
* @param {object} opts
*/
async function runMsys(args, opts) {
assert.ok(cmd);
const quotedArgs = args.map((arg) => {return `'${arg.replace(/'/g, `'\\''`)}'`}); // fix confused vim syntax highlighting with: `
await exec.exec('cmd', ['/D', '/S', '/C', cmd].concat(['-c', quotedArgs.join(' ')]), opts);
}
/**
* @param {string[]} args
* @param {object} opts
* @param {string} [cmd]
*/
async function pacman(args, opts, cmd) {
await runMsys([cmd ? cmd : 'pacman', '--noconfirm'].concat(args), opts);
}
/**
* @returns {Promise<void>}
*/
async function run() {
try {
const input = parseInput();
if (process.platform !== 'win32') {
const msg = "MSYS2 does not work on non-windows platforms; please check the 'runs-on' field of the job"
if (input.platformcheckseverity === 'fatal') {
core.setFailed(msg);
} else {
console.log(msg);
}
return;
}
const tmp_dir = process.env['RUNNER_TEMP'];
if (!tmp_dir) {
core.setFailed('environment variable RUNNER_TEMP is undefined');
return;
}
let cachedInstall = false;
let instCache = null;
let msysRootDir = path.join('C:', 'msys64');
if (input.release) {
// Use upstream package instead of the default installation in the virtual environment.
let dest = (input.location) ? input.location : tmp_dir;
msysRootDir = path.join(dest, 'msys64');
await io.mkdirP(msysRootDir);
if (INSTALL_CACHE_ENABLED) {
instCache = new InstallCache(msysRootDir, input);
core.startGroup('Restoring environment...');
cachedInstall = await instCache.restore();
core.endGroup();
}
if (!cachedInstall) {
core.startGroup('Downloading MSYS2...');
let inst_dest = await downloadInstaller();
core.endGroup();
core.startGroup('Extracting MSYS2...');
await exec.exec(inst_dest, ['-y'], {cwd: dest});
core.endGroup();
core.startGroup('Disable Key Refresh...');
await disableKeyRefresh(msysRootDir);
core.endGroup();
}
}
const pathDir = path.join(tmp_dir, 'setup-msys2');
await io.mkdirP(pathDir);
writeWrapper(msysRootDir, input.msystem, input.pathtype, pathDir, 'msys2.cmd');
core.addPath(pathDir);
core.setOutput('msys2-location', msysRootDir);
// XXX: ideally this should be removed, we don't want to pollute the user's environment
core.exportVariable('MSYSTEM', input.msystem);
const packageCache = input.cache ? new PackageCache(msysRootDir, input) : null;
if (!cachedInstall) {
if (packageCache !== null) {
core.startGroup('Restoring package cache...');
await packageCache.restore();
core.endGroup();
}
core.startGroup('Starting MSYS2 for the first time...');
await runMsys(['uname', '-a']);
core.endGroup();
}
core.startGroup('Disable CheckSpace...');
// Reduce time required to install packages by disabling pacman's disk space checking
await runMsys(['sed', '-i', 's/^CheckSpace/#CheckSpace/g', '/etc/pacman.conf']);
core.endGroup();
if (input.update) {
core.startGroup('Updating packages...');
await pacman(['-Syuu', '--overwrite', '*'], {ignoreReturnCode: true});
// We have changed /etc/pacman.conf above which means on a pacman upgrade
// pacman.conf will be installed as pacman.conf.pacnew
await runMsys(['mv', '-f', '/etc/pacman.conf.pacnew', '/etc/pacman.conf'], {ignoreReturnCode: true, silent: true});
core.endGroup();
core.startGroup('Killing remaining tasks...');
await exec.exec('taskkill', ['/F', '/FI', 'MODULES eq msys-2.0.dll']);
core.endGroup();
core.startGroup('Final system upgrade...');
await pacman(['-Syuu', '--overwrite', '*'], {});
core.endGroup();
}
if (input.install.length) {
core.startGroup('Installing additional packages through pacman...');
await pacman(['-S', '--needed', '--overwrite', '*'].concat(
(input.pacboy.length) ? input.install.concat(['pactoys']) : input.install
), {});
core.endGroup();
} else {
if (input.pacboy.length) {
core.startGroup('Installing pacboy...');
await pacman(['-S', '--needed', '--overwrite', '*', 'pactoys'], {});
core.endGroup();
}
}
if (input.pacboy.length) {
core.startGroup('Installing additional packages through pacboy...');
await pacman(['-S', '--needed'].concat(input.pacboy), {}, 'pacboy');
core.endGroup();
}
if (!cachedInstall && packageCache !== null) {
core.startGroup('Saving package cache...');
await packageCache.prune();
await packageCache.save();
await packageCache.clear();
core.endGroup();
}
if (instCache !== null) {
core.startGroup('Saving environment...');
if (packageCache !== null) {
await packageCache.clear();
}
await instCache.save();
core.endGroup();
}
}
catch (error) {
core.setFailed(error.message);
}
}
await run();
// https://github.com/actions/toolkit/issues/1578#issuecomment-1879770064
process.exit();