text: const cache = require('@actions/cache');
  const core = require('@actions/core');
  const io = require('@actions/io');
  const exec = require('@actions/exec');
  const tc = require('@actions/tool-cache');
  const path = require('path');
  const fs = require('fs');
  const crypto = require('crypto');
  const assert = require('assert').strict;
  const { hashElement } = require('folder-hash');
  
  const inst_version = '2021-11-30';
  const inst_url = `https://github.com/msys2/msys2-installer/releases/download/${inst_version}/msys2-base-x86_64-${inst_version.replace(/-/g, '')}.sfx.exe`;
  const checksum = '971f247546d1c7f92e711650136004a4f54119ce3131fe558112e24d69d0d352';
  // see https://github.com/msys2/setup-msys2/issues/61
  const INSTALL_CACHE_ENABLED = false;
  const CACHE_FLUSH_COUNTER = 0;
  const inst32_version = '2021-07-05';
  const inst32_url = `https://github.com/msys2/msys2-installer/releases/download/nightly-i686/msys2-base-i686-${inst32_version.replace(/-/g, '')}.sfx.exe`;
  const checksum32 = '29b4c44b3f65bc0b496b8a9753bdebdc7ec61935db0d3dd09d89f1659e763d05';
  
  function changeGroup(str) {
    core.endGroup();
    core.startGroup(str);
  }
  
  function parseInput() {
    let p_release = core.getInput('release') === 'true';
    let p_update = core.getInput('update') === 'true';
    let p_pathtype = core.getInput('path-type');
    let p_msystem = core.getInput('msystem');
    let p_install = core.getInput('install');
    let p_bitness = core.getInput('bitness');
    let p_pacboy = core.getInput('pacboy');
    let p_platformcheckseverity = core.getInput('platform-check-severity');
    let p_location = core.getInput('location');
  
    const msystem_allowed = ['MSYS', 'MINGW32', 'MINGW64', 'UCRT64', 'CLANG32', 'CLANG64'];
    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()
  
    p_install = (p_install === 'false') ? [] : p_install.split(/\s+/);
    p_pacboy = (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`);
    }
  
    return {
      release: p_release,
      update: p_update,
      pathtype: p_pathtype,
      msystem: p_msystem,
      install: p_install,
      bitness: p_bitness,
      pacboy: p_pacboy,
      platformcheckseverity: p_platformcheckseverity,
      location: (p_location == "RUNNER_TEMP") ? process.env['RUNNER_TEMP'] : p_location,
    }
  }
  
  async function downloadInstaller(input) {
    let url = inst_url;
    let version = inst_version
    let chksum = checksum;
    let arch = 'x64';
    if (input.bitness === "32") {
      url = inst32_url;
      version = inst32_version;
      chksum = checksum32;
      arch = 'x86';
    }
    // We use the last field only, so that each version is ensured semver incompatible with the previous one.
    version = `0.0.${version.replace(/-/g, '')}`;
    const inst_path = tc.find('msys2-installer', version, arch);
    const destination = inst_path ? path.join(inst_path, 'base.exe') : await tc.downloadTool(url);
    let computedChecksum = '';
    await exec.exec(`powershell.exe`, [`(Get-FileHash ${destination} -Algorithm SHA256)[0].Hash`], {listeners: {stdout: (data) => { computedChecksum += data.toString(); }}});
    if (computedChecksum.slice(0, -2).toUpperCase() !== chksum.toUpperCase()) {
      throw new Error(`The SHA256 of the installer does not match! expected ${chksum} got ${computedChecksum}`);
    }
    return path.join(inst_path || await tc.cacheFile(destination, 'base.exe', 'msys2-installer', version, arch), 'base.exe');
  }
  
  async function disableKeyRefresh(msysRootDir) {
    const postFile = path.join(msysRootDir, 'etc\\post-install\\07-pacman-key.post');
    await exec.exec(`powershell.exe`, [`((Get-Content -path ${postFile} -Raw) -replace '--refresh-keys', '--version') | Set-Content -Path ${postFile}`]);
  }
  
  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;
  }
  
  async function restoreCache(paths, primaryKey, restoreKeys) {
      const restoreKey = await cache.restoreCache(paths, primaryKey, restoreKeys);
      console.log(`Cache restore for ${primaryKey}, got ${restoreKey}`);
      return restoreKey;
  }
  
  async function hashPath(path) {
    return (await hashElement(path, {encoding: 'hex'}))['hash'].toString();
  }
  
  class PackageCache {
  
    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, input.bitness].toString() + (input.bitness === "32" ? checksum32 : 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 {
  
    constructor(msysRootDir, input) {
      let shasum = crypto.createHash('sha1');
      shasum.update(JSON.stringify(input) + (input.bitness === "32" ? checksum32 : 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;
  
  async function writeWrapper(msysRootDir, pathtype, destDir, name) {
    let wrap = [
      `@echo off`,
      `setlocal`,
      `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);
  }
  
  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);
  }
  
  async function pacman(args, opts, cmd) {
    await runMsys([cmd ? cmd : 'pacman', '--noconfirm'].concat(args), opts);
  }
  
  async function killMsysProcs(input) {
    if (input.bitness === "32" && process.env['PROCESSOR_ARCHITECTURE'] !== "x86") {
      // taskkill doesn't work on 32-bit processes on 64-bit windows
      await exec.exec(path.join(process.env['windir'], "SysWOW64", "WindowsPowerShell", "v1.0", "PowerShell.exe"), ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", "Get-Process | Where-Object { $_.Modules.ModuleName -contains 'msys-2.0.dll' } | Stop-Process -Force"]);
    } else {
      await exec.exec('taskkill', ['/F', '/FI', 'MODULES eq msys-2.0.dll']);
    }
  }
  
  
  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:', `msys${input.bitness}`);
      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, `msys${input.bitness}`);
        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(input);
  
          changeGroup('Extracting MSYS2...');
          await exec.exec(inst_dest, ['-y'], {cwd: dest});
  
          changeGroup('Disable Key Refresh...');
          await disableKeyRefresh(msysRootDir);
          core.endGroup();
        }
      }
  
      const pathDir = path.join(tmp_dir, 'setup-msys2');
      await io.mkdirP(pathDir);
      writeWrapper(msysRootDir, input.pathtype, pathDir, 'msys2.cmd');
      core.addPath(pathDir);
  
      const packageCache = new PackageCache(msysRootDir, input);
  
      if (!cachedInstall) {
        core.startGroup('Restoring package cache...');
        await packageCache.restore();
        core.endGroup();
  
        core.startGroup('Starting MSYS2 for the first time...');
        await runMsys(['uname', '-a']);
        core.endGroup();
      }
  
      if (input.update) {
        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']);
        changeGroup('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});
        changeGroup('Killing remaining tasks...');
        await killMsysProcs(input);
        changeGroup('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), {env: Object.assign({'MSYSTEM': input.msystem}, process.env)}, 'pacboy');
        core.endGroup();
      }
  
      if (input.bitness === "32") {
        core.startGroup('Killing remaining tasks...');
        await killMsysProcs(input);
        changeGroup('autorebase.bat...');
        await exec.exec(path.join(msysRootDir, "autorebase.bat"));
        core.endGroup();
      }
  
      core.exportVariable('MSYSTEM', input.msystem);
  
      if (!cachedInstall) {
        core.startGroup('Saving package cache...');
        await packageCache.prune();
        await packageCache.save();
        await packageCache.clear();
        core.endGroup();
      }
  
      if (instCache !== null) {
        core.startGroup('Saving environment...');
        await packageCache.clear();
        await instCache.save();
        core.endGroup();
      }
    }
    catch (error) {
      core.setFailed(error.message);
    }
  }
  
  run()
  