diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 1480c1d7..fc75b5a6 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -123,6 +123,12 @@ jobs: ghc: "9.2.5" cabal: "3.6" + # Test ghcup on windows (PR #206) which choco does not have 9.4.5 + - os: windows-latest + plan: + ghc: "9.4.5" + cabal: "3.8" + steps: - uses: actions/checkout@v3 diff --git a/setup/dist/index.js b/setup/dist/index.js index b7e9f80e..93834f7f 100644 --- a/setup/dist/index.js +++ b/setup/dist/index.js @@ -13357,44 +13357,62 @@ async function isInstalled(tool, version, os) { const toolPath = tc.find(tool, version); if (toolPath) return success(tool, version, toolPath, os); - const ghcupPath = `${process_1.default.env.HOME}/.ghcup${tool === 'ghc' ? `/ghc/${version}` : ''}/bin`; + // Path where ghcup installs binaries + const ghcupPath = os === 'win32' ? 'C:/ghcup/bin' : `${process_1.default.env.HOME}/.ghcup/bin`; + // Path where apt installs binaries of a tool const v = aptVersion(tool, version); const aptPath = `/opt/${tool}/${v}/bin`; + // Path where choco installs binaries of a tool const chocoPath = await getChocoPath(tool, version, (0, opts_1.releaseRevision)(version, tool, os)); const locations = { stack: [], cabal: { - win32: [chocoPath], - linux: [aptPath], - darwin: [] + win32: [chocoPath, ghcupPath], + linux: [aptPath, ghcupPath], + darwin: [ghcupPath] }[os], ghc: { - win32: [chocoPath], + win32: [chocoPath, ghcupPath], linux: [aptPath, ghcupPath], darwin: [ghcupPath] }[os] }; + core.debug(`isInstalled ${tool} ${version} ${locations[tool]}`); + const f = await exec(await ghcupBin(os), ['whereis', tool, version]); + core.info(`\n`); + core.debug(`isInstalled whereis ${f}`); for (const p of locations[tool]) { + core.info(`Attempting to access tool ${tool} at location ${p}`); const installedPath = await fs_1.promises .access(p) .then(() => p) .catch(() => undefined); + if (installedPath == undefined) { + core.info(`Failed to access tool ${tool} at location ${p}`); + } + else { + core.info(`Succeeded accessing tool ${tool} at location ${p}`); + } if (installedPath) { // Make sure that the correct ghc is used, even if ghcup has set a // default prior to this action being ran. - if (tool === 'ghc' && installedPath === ghcupPath) - await exec(await ghcupBin(os), ['set', tool, version]); - return success(tool, version, installedPath, os); - } - } - if (tool === 'cabal' && os !== 'win32') { - const installedPath = await fs_1.promises - .access(`${ghcupPath}/cabal-${version}`) - .then(() => ghcupPath) - .catch(() => undefined); - if (installedPath) { - await exec(await ghcupBin(os), ['set', tool, version]); - return success(tool, version, installedPath, os); + core.debug(`isInstalled installedPath: ${installedPath}`); + if (installedPath === ghcupPath) { + // If the result of this `ghcup set` is non-zero, the version we want + // is probably not actually installed + const ghcupSetResult = await exec(await ghcupBin(os), [ + 'set', + tool, + version + ]); + if (ghcupSetResult == 0) + return success(tool, version, installedPath, os); + } + else { + // Install methods apt and choco have precise install paths, + // so if the install path is present, the tool should be present, too. + return success(tool, version, installedPath, os); + } } } return false; @@ -13431,6 +13449,9 @@ async function installTool(tool, version, os) { break; case 'win32': await choco(tool, version); + if (await isInstalled(tool, version, os)) + return; + await ghcup(tool, version, os); break; case 'darwin': await ghcup(tool, version, os); @@ -13529,6 +13550,10 @@ async function choco(tool, version) { core.addPath(chocoPath); } async function ghcupBin(os) { + core.debug(`ghcupBin : ${os}`); + if (os === 'win32') { + return 'ghcup'; + } const cachedBin = tc.find('ghcup', opts_1.ghcup_version); if (cachedBin) return (0, path_1.join)(cachedBin, 'ghcup'); @@ -13667,6 +13692,34 @@ const rv = __importStar(__nccwpck_require__(8738)); exports.release_revisions = rv; exports.supported_versions = sv; exports.ghcup_version = sv.ghcup[0]; // Known to be an array of length 1 +/** + * Reads the example `actions.yml` file and selects the `inputs` key. The result + * will be a key-value map of the following shape: + * ``` + * { + * 'ghc-version': { + * required: false, + * description: '...', + * default: 'latest' + * }, + * 'cabal-version': { + * required: false, + * description: '...', + * default: 'latest' + * }, + * 'stack-version': { + * required: false, + * description: '...', + * default: 'latest' + * }, + * 'enable-stack': { + * required: false, + * default: 'latest' + * }, + * ... + * } + * ``` + */ exports.yamlInputs = (0, js_yaml_1.load)((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '..', 'action.yml'), 'utf8') // The action.yml file structure is statically known. // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -13854,6 +13907,9 @@ async function run(inputs) { core.info('Preparing to setup a Haskell environment'); const os = process.platform; const opts = (0, opts_1.getOpts)((0, opts_1.getDefaults)(os), os, inputs); + core.debug(`run: inputs = ${JSON.stringify(inputs)}`); + core.debug(`run: os = ${JSON.stringify(os)}`); + core.debug(`run: opts = ${JSON.stringify(opts)}`); if (opts.ghcup.releaseChannel) { await core.group(`Preparing ghcup environment`, async () => (0, installer_1.addGhcupReleaseChannel)(opts.ghcup.releaseChannel, os)); } @@ -13883,8 +13939,15 @@ async function run(inputs) { : `${process.env.HOME}/.cabal/store`; fs.appendFileSync(configFile, `store-dir: ${storeDir}${os_1.EOL}`); core.setOutput('cabal-store', storeDir); - // Issue #130: for non-choco installs, add ~/.cabal/bin to PATH - if (process.platform !== 'win32') { + if (process.platform === 'win32') { + // Some Windows version cannot symlink, so we need to switch to 'install-method: copy'. + // Choco does this for us, but not GHCup: https://github.com/haskell/ghcup-hs/issues/808 + // However, here we do not know whether we installed with choco or not, so do it always: + fs.appendFileSync(configFile, `install-method: copy${os_1.EOL}`); + fs.appendFileSync(configFile, `overwrite-policy: always${os_1.EOL}`); + } + else { + // Issue #130: for non-choco installs, add ~/.cabal/bin to PATH const installdir = `${process.env.HOME}/.cabal/bin`; core.info(`Adding ${installdir} to PATH`); core.addPath(installdir); diff --git a/setup/lib/installer.js b/setup/lib/installer.js index d8877327..3a3ccbde 100644 --- a/setup/lib/installer.js +++ b/setup/lib/installer.js @@ -76,44 +76,62 @@ async function isInstalled(tool, version, os) { const toolPath = tc.find(tool, version); if (toolPath) return success(tool, version, toolPath, os); - const ghcupPath = `${process_1.default.env.HOME}/.ghcup${tool === 'ghc' ? `/ghc/${version}` : ''}/bin`; + // Path where ghcup installs binaries + const ghcupPath = os === 'win32' ? 'C:/ghcup/bin' : `${process_1.default.env.HOME}/.ghcup/bin`; + // Path where apt installs binaries of a tool const v = aptVersion(tool, version); const aptPath = `/opt/${tool}/${v}/bin`; + // Path where choco installs binaries of a tool const chocoPath = await getChocoPath(tool, version, (0, opts_1.releaseRevision)(version, tool, os)); const locations = { stack: [], cabal: { - win32: [chocoPath], - linux: [aptPath], - darwin: [] + win32: [chocoPath, ghcupPath], + linux: [aptPath, ghcupPath], + darwin: [ghcupPath] }[os], ghc: { - win32: [chocoPath], + win32: [chocoPath, ghcupPath], linux: [aptPath, ghcupPath], darwin: [ghcupPath] }[os] }; + core.debug(`isInstalled ${tool} ${version} ${locations[tool]}`); + const f = await exec(await ghcupBin(os), ['whereis', tool, version]); + core.info(`\n`); + core.debug(`isInstalled whereis ${f}`); for (const p of locations[tool]) { + core.info(`Attempting to access tool ${tool} at location ${p}`); const installedPath = await fs_1.promises .access(p) .then(() => p) .catch(() => undefined); + if (installedPath == undefined) { + core.info(`Failed to access tool ${tool} at location ${p}`); + } + else { + core.info(`Succeeded accessing tool ${tool} at location ${p}`); + } if (installedPath) { // Make sure that the correct ghc is used, even if ghcup has set a // default prior to this action being ran. - if (tool === 'ghc' && installedPath === ghcupPath) - await exec(await ghcupBin(os), ['set', tool, version]); - return success(tool, version, installedPath, os); - } - } - if (tool === 'cabal' && os !== 'win32') { - const installedPath = await fs_1.promises - .access(`${ghcupPath}/cabal-${version}`) - .then(() => ghcupPath) - .catch(() => undefined); - if (installedPath) { - await exec(await ghcupBin(os), ['set', tool, version]); - return success(tool, version, installedPath, os); + core.debug(`isInstalled installedPath: ${installedPath}`); + if (installedPath === ghcupPath) { + // If the result of this `ghcup set` is non-zero, the version we want + // is probably not actually installed + const ghcupSetResult = await exec(await ghcupBin(os), [ + 'set', + tool, + version + ]); + if (ghcupSetResult == 0) + return success(tool, version, installedPath, os); + } + else { + // Install methods apt and choco have precise install paths, + // so if the install path is present, the tool should be present, too. + return success(tool, version, installedPath, os); + } } } return false; @@ -150,6 +168,9 @@ async function installTool(tool, version, os) { break; case 'win32': await choco(tool, version); + if (await isInstalled(tool, version, os)) + return; + await ghcup(tool, version, os); break; case 'darwin': await ghcup(tool, version, os); @@ -248,6 +269,10 @@ async function choco(tool, version) { core.addPath(chocoPath); } async function ghcupBin(os) { + core.debug(`ghcupBin : ${os}`); + if (os === 'win32') { + return 'ghcup'; + } const cachedBin = tc.find('ghcup', opts_1.ghcup_version); if (cachedBin) return (0, path_1.join)(cachedBin, 'ghcup'); diff --git a/setup/lib/opts.d.ts b/setup/lib/opts.d.ts index 9fa53c54..5b5a3603 100644 --- a/setup/lib/opts.d.ts +++ b/setup/lib/opts.d.ts @@ -41,6 +41,34 @@ export type Defaults = Record & { }; }; }; +/** + * Reads the example `actions.yml` file and selects the `inputs` key. The result + * will be a key-value map of the following shape: + * ``` + * { + * 'ghc-version': { + * required: false, + * description: '...', + * default: 'latest' + * }, + * 'cabal-version': { + * required: false, + * description: '...', + * default: 'latest' + * }, + * 'stack-version': { + * required: false, + * description: '...', + * default: 'latest' + * }, + * 'enable-stack': { + * required: false, + * default: 'latest' + * }, + * ... + * } + * ``` + */ export declare const yamlInputs: Record; diff --git a/setup/lib/opts.js b/setup/lib/opts.js index 5a388737..4ec91ee4 100644 --- a/setup/lib/opts.js +++ b/setup/lib/opts.js @@ -33,6 +33,34 @@ const rv = __importStar(require("./release-revisions.json")); exports.release_revisions = rv; exports.supported_versions = sv; exports.ghcup_version = sv.ghcup[0]; // Known to be an array of length 1 +/** + * Reads the example `actions.yml` file and selects the `inputs` key. The result + * will be a key-value map of the following shape: + * ``` + * { + * 'ghc-version': { + * required: false, + * description: '...', + * default: 'latest' + * }, + * 'cabal-version': { + * required: false, + * description: '...', + * default: 'latest' + * }, + * 'stack-version': { + * required: false, + * description: '...', + * default: 'latest' + * }, + * 'enable-stack': { + * required: false, + * default: 'latest' + * }, + * ... + * } + * ``` + */ exports.yamlInputs = (0, js_yaml_1.load)((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '..', 'action.yml'), 'utf8') // The action.yml file structure is statically known. // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/setup/lib/setup-haskell.js b/setup/lib/setup-haskell.js index 8f36baa3..42859074 100644 --- a/setup/lib/setup-haskell.js +++ b/setup/lib/setup-haskell.js @@ -49,6 +49,9 @@ async function run(inputs) { core.info('Preparing to setup a Haskell environment'); const os = process.platform; const opts = (0, opts_1.getOpts)((0, opts_1.getDefaults)(os), os, inputs); + core.debug(`run: inputs = ${JSON.stringify(inputs)}`); + core.debug(`run: os = ${JSON.stringify(os)}`); + core.debug(`run: opts = ${JSON.stringify(opts)}`); if (opts.ghcup.releaseChannel) { await core.group(`Preparing ghcup environment`, async () => (0, installer_1.addGhcupReleaseChannel)(opts.ghcup.releaseChannel, os)); } @@ -78,8 +81,15 @@ async function run(inputs) { : `${process.env.HOME}/.cabal/store`; fs.appendFileSync(configFile, `store-dir: ${storeDir}${os_1.EOL}`); core.setOutput('cabal-store', storeDir); - // Issue #130: for non-choco installs, add ~/.cabal/bin to PATH - if (process.platform !== 'win32') { + if (process.platform === 'win32') { + // Some Windows version cannot symlink, so we need to switch to 'install-method: copy'. + // Choco does this for us, but not GHCup: https://github.com/haskell/ghcup-hs/issues/808 + // However, here we do not know whether we installed with choco or not, so do it always: + fs.appendFileSync(configFile, `install-method: copy${os_1.EOL}`); + fs.appendFileSync(configFile, `overwrite-policy: always${os_1.EOL}`); + } + else { + // Issue #130: for non-choco installs, add ~/.cabal/bin to PATH const installdir = `${process.env.HOME}/.cabal/bin`; core.info(`Adding ${installdir} to PATH`); core.addPath(installdir); diff --git a/setup/src/installer.ts b/setup/src/installer.ts index b3b68961..87284e7c 100644 --- a/setup/src/installer.ts +++ b/setup/src/installer.ts @@ -72,12 +72,15 @@ async function isInstalled( const toolPath = tc.find(tool, version); if (toolPath) return success(tool, version, toolPath, os); - const ghcupPath = `${process.env.HOME}/.ghcup${ - tool === 'ghc' ? `/ghc/${version}` : '' - }/bin`; + // Path where ghcup installs binaries + const ghcupPath = + os === 'win32' ? 'C:/ghcup/bin' : `${process.env.HOME}/.ghcup/bin`; + + // Path where apt installs binaries of a tool const v = aptVersion(tool, version); const aptPath = `/opt/${tool}/${v}/bin`; + // Path where choco installs binaries of a tool const chocoPath = await getChocoPath( tool, version, @@ -87,45 +90,55 @@ async function isInstalled( const locations = { stack: [], // Always installed into the tool cache cabal: { - win32: [chocoPath], - linux: [aptPath], - darwin: [] + win32: [chocoPath, ghcupPath], + linux: [aptPath, ghcupPath], + darwin: [ghcupPath] }[os], ghc: { - win32: [chocoPath], + win32: [chocoPath, ghcupPath], linux: [aptPath, ghcupPath], darwin: [ghcupPath] }[os] }; + core.debug(`isInstalled ${tool} ${version} ${locations[tool]}`); + const f = await exec(await ghcupBin(os), ['whereis', tool, version]); + core.info(`\n`); + core.debug(`isInstalled whereis ${f}`); for (const p of locations[tool]) { + core.info(`Attempting to access tool ${tool} at location ${p}`); const installedPath = await afs .access(p) .then(() => p) .catch(() => undefined); - if (installedPath) { - // Make sure that the correct ghc is used, even if ghcup has set a - // default prior to this action being ran. - if (tool === 'ghc' && installedPath === ghcupPath) - await exec(await ghcupBin(os), ['set', tool, version]); - - return success(tool, version, installedPath, os); + if (installedPath == undefined) { + core.info(`Failed to access tool ${tool} at location ${p}`); + } else { + core.info(`Succeeded accessing tool ${tool} at location ${p}`); } - } - - if (tool === 'cabal' && os !== 'win32') { - const installedPath = await afs - .access(`${ghcupPath}/cabal-${version}`) - .then(() => ghcupPath) - .catch(() => undefined); if (installedPath) { - await exec(await ghcupBin(os), ['set', tool, version]); - return success(tool, version, installedPath, os); + // Make sure that the correct ghc is used, even if ghcup has set a + // default prior to this action being ran. + core.debug(`isInstalled installedPath: ${installedPath}`); + if (installedPath === ghcupPath) { + // If the result of this `ghcup set` is non-zero, the version we want + // is probably not actually installed + const ghcupSetResult = await exec(await ghcupBin(os), [ + 'set', + tool, + version + ]); + if (ghcupSetResult == 0) + return success(tool, version, installedPath, os); + } else { + // Install methods apt and choco have precise install paths, + // so if the install path is present, the tool should be present, too. + return success(tool, version, installedPath, os); + } } } - return false; } @@ -164,6 +177,8 @@ export async function installTool( break; case 'win32': await choco(tool, version); + if (await isInstalled(tool, version, os)) return; + await ghcup(tool, version, os); break; case 'darwin': await ghcup(tool, version, os); @@ -288,6 +303,10 @@ async function choco(tool: Tool, version: string): Promise { } async function ghcupBin(os: OS): Promise { + core.debug(`ghcupBin : ${os}`); + if (os === 'win32') { + return 'ghcup'; + } const cachedBin = tc.find('ghcup', ghcup_version); if (cachedBin) return join(cachedBin, 'ghcup'); diff --git a/setup/src/opts.ts b/setup/src/opts.ts index 5be74ed4..568aa1b3 100644 --- a/setup/src/opts.ts +++ b/setup/src/opts.ts @@ -35,6 +35,34 @@ export type Defaults = Record & { general: {matcher: {enable: boolean}}; }; +/** + * Reads the example `actions.yml` file and selects the `inputs` key. The result + * will be a key-value map of the following shape: + * ``` + * { + * 'ghc-version': { + * required: false, + * description: '...', + * default: 'latest' + * }, + * 'cabal-version': { + * required: false, + * description: '...', + * default: 'latest' + * }, + * 'stack-version': { + * required: false, + * description: '...', + * default: 'latest' + * }, + * 'enable-stack': { + * required: false, + * default: 'latest' + * }, + * ... + * } + * ``` + */ export const yamlInputs: Record = ( load( readFileSync(join(__dirname, '..', 'action.yml'), 'utf8') diff --git a/setup/src/setup-haskell.ts b/setup/src/setup-haskell.ts index 942a4ec8..a0502e49 100644 --- a/setup/src/setup-haskell.ts +++ b/setup/src/setup-haskell.ts @@ -26,6 +26,9 @@ export default async function run( core.info('Preparing to setup a Haskell environment'); const os = process.platform as OS; const opts = getOpts(getDefaults(os), os, inputs); + core.debug(`run: inputs = ${JSON.stringify(inputs)}`); + core.debug(`run: os = ${JSON.stringify(os)}`); + core.debug(`run: opts = ${JSON.stringify(opts)}`); if (opts.ghcup.releaseChannel) { await core.group(`Preparing ghcup environment`, async () => @@ -72,9 +75,14 @@ export default async function run( : `${process.env.HOME}/.cabal/store`; fs.appendFileSync(configFile, `store-dir: ${storeDir}${EOL}`); core.setOutput('cabal-store', storeDir); - - // Issue #130: for non-choco installs, add ~/.cabal/bin to PATH - if (process.platform !== 'win32') { + if (process.platform === 'win32') { + // Some Windows version cannot symlink, so we need to switch to 'install-method: copy'. + // Choco does this for us, but not GHCup: https://github.com/haskell/ghcup-hs/issues/808 + // However, here we do not know whether we installed with choco or not, so do it always: + fs.appendFileSync(configFile, `install-method: copy${EOL}`); + fs.appendFileSync(configFile, `overwrite-policy: always${EOL}`); + } else { + // Issue #130: for non-choco installs, add ~/.cabal/bin to PATH const installdir = `${process.env.HOME}/.cabal/bin`; core.info(`Adding ${installdir} to PATH`); core.addPath(installdir);