From 2d7bdefde607b1f7c201cbc7989f3cae9b764e69 Mon Sep 17 00:00:00 2001 From: Soumya Ranjan Mahunt Date: Wed, 3 Apr 2024 17:53:54 +0000 Subject: [PATCH] feat: added fallback windows installation without caching support --- .eslintrc.json | 2 +- .github/utils/update_metadata.js | 2 +- .github/workflows/main.yml | 6 +- README.md | 6 +- __tests__/installer/windows.test.ts | 32 +- dist/index.js | 320 ++++++++++++++---- src/installer/base.ts | 3 +- src/installer/windows/index.ts | 30 +- src/installer/windows/installation.ts | 78 ----- .../windows/installation/approach.ts | 36 ++ src/installer/windows/installation/base.ts | 9 + .../windows/installation/fallback.ts | 68 ++++ src/installer/windows/installation/index.ts | 64 ++++ src/platform/index.ts | 2 +- src/snapshot/release.ts | 2 +- src/utils/visual_studio/setup.ts | 2 +- src/utils/visual_studio/vswhere.ts | 1 + src/version/index.ts | 2 +- 18 files changed, 476 insertions(+), 189 deletions(-) delete mode 100644 src/installer/windows/installation.ts create mode 100644 src/installer/windows/installation/approach.ts create mode 100644 src/installer/windows/installation/base.ts create mode 100644 src/installer/windows/installation/fallback.ts create mode 100644 src/installer/windows/installation/index.ts diff --git a/.eslintrc.json b/.eslintrc.json index d93bd9e5..6e1d00a6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,6 +1,6 @@ { "plugins": ["jest", "@typescript-eslint"], - "extends": ["plugin:github/recommended"], + "extends": ["plugin:github/recommended","plugin:@typescript-eslint/recommended"], "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": 9, diff --git a/.github/utils/update_metadata.js b/.github/utils/update_metadata.js index f4a8262c..28a27307 100644 --- a/.github/utils/update_metadata.js +++ b/.github/utils/update_metadata.js @@ -111,7 +111,7 @@ exports.fetch = async () => { let checkoutData; if (process.env.SETUPSWIFT_SWIFTORG_METADATA) { checkoutData = JSON.parse(process.env.SETUPSWIFT_SWIFTORG_METADATA); - } + } if (!checkoutData || !checkoutData.commit) { checkoutData = await this.currentData(); } diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c7001095..c80b4a50 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -145,7 +145,6 @@ jobs: if: needs.ci.outputs.run == 'true' needs: ci runs-on: ${{ matrix.os }} - continue-on-error: ${{ matrix.os == 'windows-latest' && matrix.swift == 'latest' }} concurrency: group: integration-test-${{ github.ref }}-${{ matrix.os }}-${{ matrix.swift }}-${{ matrix.development }} cancel-in-progress: true @@ -172,7 +171,6 @@ jobs: - os: ubuntu-22.04 swift: ${{ fromJSON(vars.SETUPSWIFT_CUSTOM_TOOLCHAINS).ubuntu2204 }} development: true - noverify: true steps: - name: Checkout repository @@ -205,11 +203,10 @@ jobs: cache-snapshot: ${{ !matrix.development }} - name: Verify Swift version in macos - if: runner.os == 'macOS' && matrix.noverify != 'true' + if: runner.os == 'macOS' run: xcrun --toolchain ${{ env.TOOLCHAINS || '""' }} swift --version | grep ${{ steps.setup-swift.outputs.swift-version }} || exit 1 - name: Verify Swift version - if: matrix.noverify != 'true' run: swift --version | grep ${{ steps.setup-swift.outputs.swift-version }} || exit 1 dry-run: @@ -263,7 +260,6 @@ jobs: if: needs.ci.outputs.run == 'true' needs: ci runs-on: ${{ matrix.os }} - continue-on-error: ${{ matrix.os == 'windows-latest' }} concurrency: group: e2e-test-${{ github.ref }}-${{ matrix.os }} cancel-in-progress: true diff --git a/README.md b/README.md index 3c9e5e21..9b5a3eca 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,13 @@ [GitHub Action](https://github.com/features/actions) that will setup [Swift](https://swift.org) environment with specified version. This action supports the following functionalities: -- Works on Linux, macOS and Windows(Swift 5.10 and after not supported on Windows). +- Works on Linux, macOS and Windows. - Supports [installing latest major/minor/patch](#specifying-version). - Provides snapshots as soon as published in `swift.org`. - Verifies toolchain snapshots before installation (`gpg` for Linux and Windows, `pkgutil` for macOS) . - Allows development snapshots by enabling `development` flag and optional version. - Prefers existing Xcode installations. -- Caches installed setup in tool cache. +- Caches installed setup in tool cache and actions cache(Swift 5.10 and after does not support caching on Windows). - Allows fetching snapshot metadata without installation (can be used to setup docker images). ## Latest supported toolchains @@ -105,7 +105,7 @@ In other words specifying... i.e. for `macOS`: https://github.com/swiftwasm/swift/releases/download/swift-wasm-5.10-SNAPSHOT-2024-03-30-a/swift-wasm-5.10-SNAPSHOT-2024-03-30-a-macos_x86_64.pkg for `Linux`: https://github.com/swiftwasm/swift/releases/download/swift-wasm-5.10-SNAPSHOT-2024-03-30-a/swift-wasm-5.10-SNAPSHOT-2024-03-30-a-ubuntu22.04_x86_64.tar.gz - > [!IMPORTANT] + > [!IMPORTANT] > When using custom toolchains, please ensure that the toolchain can be installed and used on the GitHub runner, this action won't be able to validate this for custom toolchains. diff --git a/__tests__/installer/windows.test.ts b/__tests__/installer/windows.test.ts index a8c19e35..e948072a 100644 --- a/__tests__/installer/windows.test.ts +++ b/__tests__/installer/windows.test.ts @@ -126,6 +126,36 @@ describe('windows toolchain installation verification', () => { await expect(installer['unpack'](exe)).resolves.toBe(toolPath) }) + it('tests unpack for failed path matching', async () => { + const installer = new WindowsToolchainInstaller(toolchain) + const exe = path.resolve('tool', 'downloaded', 'toolchain.exe') + process.env.SystemDrive = 'C:' + jest.spyOn(exec, 'exec').mockResolvedValue(0) + jest + .spyOn(exec, 'getExecOutput') + .mockResolvedValueOnce({exitCode: 0, stdout: '{}', stderr: ''}) + .mockResolvedValueOnce({exitCode: 0, stdout: '{"PATH":"a"}', stderr: ''}) + .mockResolvedValueOnce({exitCode: 0, stdout: '{}', stderr: ''}) + .mockResolvedValue({ + exitCode: 0, + stdout: `{"SDKROOT":"root","PATH":"a${path.delimiter}b${path.delimiter}c"}`, + stderr: '' + }) + jest + .spyOn(fs, 'access') + .mockRejectedValueOnce(new Error()) + .mockRejectedValueOnce(new Error()) + .mockResolvedValue() + jest.spyOn(fs, 'cp').mockRejectedValue(new Error()) + const addPathSpy = jest.spyOn(core, 'addPath') + const exportVariableSpy = jest.spyOn(core, 'exportVariable') + await expect(installer['unpack'](exe)).resolves.toBe('') + expect(addPathSpy).toHaveBeenCalledTimes(2) + expect(exportVariableSpy).toHaveBeenCalledTimes(1) + expect(addPathSpy.mock.calls).toStrictEqual([['b'], ['c']]) + expect(exportVariableSpy.mock.calls).toStrictEqual([['SDKROOT', 'root']]) + }) + it('tests add to PATH', async () => { const installer = new WindowsToolchainInstaller(toolchain) const installation = path.resolve('tool', 'installed', 'path') @@ -188,7 +218,7 @@ describe('windows toolchain installation verification', () => { const setupSpy = jest .spyOn(VisualStudio, 'setup') .mockResolvedValue(visualStudio) - jest.spyOn(fs, 'access').mockImplementation(p => { + jest.spyOn(fs, 'access').mockImplementation(async p => { if ( typeof p === 'string' && (p.startsWith(path.join(cached, 'Developer')) || diff --git a/dist/index.js b/dist/index.js index 75513c37..d94878ce 100644 --- a/dist/index.js +++ b/dist/index.js @@ -140,11 +140,12 @@ class ToolchainInstaller { core.debug(`Found tool at "${tool}" in tool cache`); cacheHit = true; } - if (version) { + if (tool && version) { tool = yield toolCache.cacheDir(tool, key, version, arch); core.debug(`Added to tool cache at "${tool}"`); } - if (core.getBooleanInput('cache-snapshot') && + if (tool && + core.getBooleanInput('cache-snapshot') && !cacheHit && !this.data.preventCaching) { yield fs_1.promises.cp(tool, restore, { recursive: true }); @@ -510,11 +511,10 @@ const os = __importStar(__nccwpck_require__(2037)); const path = __importStar(__nccwpck_require__(1017)); const fs_1 = __nccwpck_require__(7147); const core = __importStar(__nccwpck_require__(2186)); -const exec_1 = __nccwpck_require__(1514); const semver = __importStar(__nccwpck_require__(1383)); const verify_1 = __nccwpck_require__(8780); const utils_1 = __nccwpck_require__(1606); -const installation_1 = __nccwpck_require__(539); +const installation_1 = __nccwpck_require__(3554); class WindowsToolchainInstaller extends verify_1.VerifyingToolchainInstaller { get vsRequirement() { const reccommended = '10.0.17763'; @@ -550,33 +550,17 @@ class WindowsToolchainInstaller extends verify_1.VerifyingToolchainInstaller { } unpack(exe) { return __awaiter(this, void 0, void 0, function* () { - function env() { - return __awaiter(this, void 0, void 0, function* () { - for (const target of ['Machine', 'User']) { - const { stdout } = yield (0, exec_1.getExecOutput)('powershell', [ - '-NoProfile', - '-Command', - `& {[Environment]::GetEnvironmentVariables('${target}') | ConvertTo-Json}` - ], { failOnStdErr: true }); - core.debug(`${target} variables: "${stdout}"`); - } - }); - } - core.debug(`Installing toolchain from "${exe}"`); - core.startGroup('Environment variables before installation'); - yield env(); - core.endGroup(); - yield (0, exec_1.exec)(`"${exe}"`, ['-q']); - core.startGroup('Environment variables after installation'); - yield env(); - core.endGroup(); - const installation = yield installation_1.Installation.detect(); - return installation.location; + var _a; + const installation = yield installation_1.Installation.install(exe); + return (_a = installation === null || installation === void 0 ? void 0 : installation.location) !== null && _a !== void 0 ? _a : ''; }); } add(installLocation) { return __awaiter(this, void 0, void 0, function* () { const installation = yield installation_1.Installation.get(installLocation); + if (!installation) { + return; + } core.exportVariable('SDKROOT', installation.sdkroot); if (installation.devdir) { core.exportVariable('DEVELOPER_DIR', installation.devdir); @@ -611,7 +595,7 @@ exports.WindowsToolchainInstaller = WindowsToolchainInstaller; /***/ }), -/***/ 539: +/***/ 1124: /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { "use strict"; @@ -649,10 +633,60 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.Installation = void 0; +exports.secondDirectoryLayout = exports.firstDirectoryLayout = void 0; const path = __importStar(__nccwpck_require__(1017)); const fs_1 = __nccwpck_require__(7147); const core = __importStar(__nccwpck_require__(2186)); +const base_1 = __nccwpck_require__(106); +function firstDirectoryLayout(root) { + return __awaiter(this, void 0, void 0, function* () { + var _a; + core.debug('Trying first installation approach'); + const systemDrive = (_a = process.env.SystemDrive) !== null && _a !== void 0 ? _a : 'C:'; + const location = root !== null && root !== void 0 ? root : path.join(systemDrive, 'Library'); + const devdir = path.join(location, 'Developer'); + const toolchainName = 'unknown-Asserts-development.xctoolchain'; + const toolchain = path.join(devdir, 'Toolchains', toolchainName); + yield fs_1.promises.access(toolchain); + const winsdk = path.join('Developer', 'SDKs', 'Windows.sdk'); + const sdkroot = path.join(devdir, 'Platforms', 'Windows.platform', winsdk); + const runtime = path.join(location, 'Swift', 'runtime-development'); + core.debug('First installation approach succeeded'); + return new base_1.Installation(location, toolchain, sdkroot, runtime, devdir); + }); +} +exports.firstDirectoryLayout = firstDirectoryLayout; +function secondDirectoryLayout(root) { + return __awaiter(this, void 0, void 0, function* () { + var _a; + core.debug('Trying secong installation approach'); + const systemDrive = (_a = root !== null && root !== void 0 ? root : process.env.SystemDrive) !== null && _a !== void 0 ? _a : 'C:'; + const location = path.join(systemDrive, 'Program Files', 'Swift'); + const toolchainName = '0.0.0+Asserts'; + const toolchain = path.join(location, 'Toolchains', toolchainName); + yield fs_1.promises.access(toolchain); + const winsdk = path.join('Developer', 'SDKs', 'Windows.sdk'); + const sdkroot = path.join(location, 'Platforms', 'Windows.platform', winsdk); + const runtime = path.join(location, 'Runtimes', '0.0.0'); + const runtimeRoot = path.join(location, 'Swift'); + const devPath = path.join(systemDrive, 'Program Files', 'Swift'); + yield fs_1.promises.cp(devPath, runtimeRoot, { recursive: true }); + core.debug('Second installation approach succeeded'); + return new base_1.Installation(location, toolchain, sdkroot, runtime); + }); +} +exports.secondDirectoryLayout = secondDirectoryLayout; + + +/***/ }), + +/***/ 106: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.Installation = void 0; class Installation { constructor(location, toolchain, sdkroot, runtime, devdir) { this.location = location; @@ -661,59 +695,204 @@ class Installation { this.runtime = runtime; this.devdir = devdir; } - static get(location, fallback) { - return __awaiter(this, void 0, void 0, function* () { - let toolchain; - let sdkroot; - let runtime; - let devdir; - try { - core.debug(`Checking for development snapshot installation`); - toolchain = path.join(location, 'Toolchains', '0.0.0+Asserts'); - sdkroot = path.join(location, 'Platforms', 'Windows.platform', 'Developer', 'SDKs', 'Windows.sdk'); - runtime = path.join(location, 'Runtimes', '0.0.0'); - yield fs_1.promises.access(toolchain); - } - catch (error) { - core.debug(`Switching to default installation due to "${error}"`); - if (fallback) { - location = fallback; - } - devdir = path.join(location, 'Developer'); - toolchain = path.join(devdir, 'Toolchains', 'unknown-Asserts-development.xctoolchain'); - sdkroot = path.join(devdir, 'Platforms', 'Windows.platform', 'Developer', 'SDKs', 'Windows.sdk'); - runtime = path.join(location, 'Swift', 'runtime-development'); - } - return new Installation(location, toolchain, sdkroot, runtime, devdir); - }); +} +exports.Installation = Installation; + + +/***/ }), + +/***/ 6848: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; } - static detect() { - return __awaiter(this, void 0, void 0, function* () { - var _a; - const systemDrive = (_a = process.env.SystemDrive) !== null && _a !== void 0 ? _a : 'C:'; - const defaultPath = path.join(systemDrive, 'Library'); - const devPath = path.join(systemDrive, 'Program Files', 'Swift'); - const installation = yield Installation.get(devPath, defaultPath); - if (path.relative(devPath, installation.location)) { - const runtimeRoot = path.join(installation.location, 'Swift'); - try { - yield fs_1.promises.access(devPath); - yield fs_1.promises.cp(devPath, runtimeRoot, { recursive: true }); + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.fallback = exports.env = void 0; +const path = __importStar(__nccwpck_require__(1017)); +const core = __importStar(__nccwpck_require__(2186)); +const exec_1 = __nccwpck_require__(1514); +function comapareEnvironment(oldJSON, newJSON) { + const difference = {}; + let newPaths = []; + let before; + let after; + try { + before = JSON.parse(oldJSON); + after = JSON.parse(newJSON); + for (const [key, value] of Object.entries(after)) { + const old = before[key]; + if (before[key] !== value) { + if (key.toUpperCase() === 'PATH' && old) { + newPaths = value + .replace(old, '') + .split(path.delimiter) + .filter(item => item); } - catch (error) { - core.debug(`Runtime check failed with "${error}"`); + else { + difference[key] = value; } } + } + } + catch (error) { + core.error(`Environment variables serialization error "${error}"`); + } + return { newPaths, variables: difference }; +} +function env() { + return __awaiter(this, void 0, void 0, function* () { + return yield ['Machine', 'User'].reduce((previous, current) => __awaiter(this, void 0, void 0, function* () { + const modified = yield previous; + const command = `[Environment]::GetEnvironmentVariables('${current}') | ConvertTo-Json`; + const args = ['-NoProfile', '-Command', `& {${command}}`]; + const options = { failOnStdErr: true }; + const { stdout } = yield (0, exec_1.getExecOutput)('powershell', args, options); + modified[current] = stdout; + return modified; + }), Promise.resolve({})); + }); +} +exports.env = env; +function fallback(oldEnv, newEnv) { + return __awaiter(this, void 0, void 0, function* () { + core.debug('Procceding with fallback installation approach'); + const data = Object.entries(newEnv).reduce((previous, current) => { + return Object.assign(Object.assign({}, previous), comapareEnvironment(oldEnv[current[0]], current[1])); + }, {}); + core.debug(`Setting up environment with "${JSON.stringify(data)}"`); + for (const newPath of data.newPaths) { + core.addPath(newPath); + } + for (const pair of Object.entries(data.variables)) { + core.exportVariable(pair[0], pair[1]); + } + }); +} +exports.fallback = fallback; + + +/***/ }), + +/***/ 3554: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +const core = __importStar(__nccwpck_require__(2186)); +const exec_1 = __nccwpck_require__(1514); +const base_1 = __nccwpck_require__(106); +const approach_1 = __nccwpck_require__(1124); +const fallback_1 = __nccwpck_require__(6848); +base_1.Installation.get = (install) => __awaiter(void 0, void 0, void 0, function* () { + var _a; + if (!((_a = install === null || install === void 0 ? void 0 : install.length) !== null && _a !== void 0 ? _a : 1)) { + return undefined; + } + const approaches = [ + () => __awaiter(void 0, void 0, void 0, function* () { return (0, approach_1.secondDirectoryLayout)(install); }), + () => __awaiter(void 0, void 0, void 0, function* () { return (0, approach_1.firstDirectoryLayout)(install); }) + ]; + let counter = 0; + for (const approach of approaches) { + counter += 1; + try { + const installation = yield approach(); core.debug(`Installation location at "${installation.location}"`); core.debug(`Toolchain installed at "${installation.toolchain}"`); core.debug(`SDK installed at "${installation.sdkroot}"`); core.debug(`Runtime installed at "${installation.runtime}"`); core.debug(`Development directory at "${installation.devdir}"`); return installation; - }); + } + catch (error) { + core.debug(`Failed ${counter} time(s)`); + } } -} -exports.Installation = Installation; + return undefined; +}); +base_1.Installation.install = (exe) => __awaiter(void 0, void 0, void 0, function* () { + core.debug(`Installing toolchain from "${exe}"`); + const oldEnv = yield (0, fallback_1.env)(); + yield (0, exec_1.exec)(`"${exe}"`, ['-q']); + const newEnv = yield (0, fallback_1.env)(); + return base_1.Installation.detect(oldEnv, newEnv); +}); +base_1.Installation.detect = (oldEnv, newEnv) => __awaiter(void 0, void 0, void 0, function* () { + const installation = yield base_1.Installation.get(); + if (!installation) { + (0, fallback_1.fallback)(oldEnv, newEnv); + } + return installation; +}); +__exportStar(__nccwpck_require__(106), exports); /***/ }), @@ -2266,6 +2445,7 @@ const path = __importStar(__nccwpck_require__(1017)); const fs_1 = __nccwpck_require__(7147); const io = __importStar(__nccwpck_require__(7436)); const core = __importStar(__nccwpck_require__(2186)); +// eslint-disable-next-line @typescript-eslint/no-namespace var VSWhere; (function (VSWhere) { /// Get vswhere and vs_installer paths diff --git a/src/installer/base.ts b/src/installer/base.ts index 098c3cdf..972c47ec 100644 --- a/src/installer/base.ts +++ b/src/installer/base.ts @@ -69,11 +69,12 @@ export abstract class ToolchainInstaller { cacheHit = true } - if (version) { + if (tool && version) { tool = await toolCache.cacheDir(tool, key, version, arch) core.debug(`Added to tool cache at "${tool}"`) } if ( + tool && core.getBooleanInput('cache-snapshot') && !cacheHit && !this.data.preventCaching diff --git a/src/installer/windows/index.ts b/src/installer/windows/index.ts index 226ef451..9f052723 100644 --- a/src/installer/windows/index.ts +++ b/src/installer/windows/index.ts @@ -2,7 +2,6 @@ import * as os from 'os' import * as path from 'path' import {promises as fs} from 'fs' import * as core from '@actions/core' -import {exec, getExecOutput} from '@actions/exec' import * as semver from 'semver' import {VerifyingToolchainInstaller} from '../verify' import {WindowsToolchainSnapshot} from '../../snapshot' @@ -40,34 +39,15 @@ export class WindowsToolchainInstaller extends VerifyingToolchainInstaller = {} + let newPaths: string[] = [] + let before: Record + let after: Record + try { + before = JSON.parse(oldJSON) + after = JSON.parse(newJSON) + for (const [key, value] of Object.entries(after)) { + const old = before[key] + if (before[key] !== value) { + if (key.toUpperCase() === 'PATH' && old) { + newPaths = value + .replace(old, '') + .split(path.delimiter) + .filter(item => item) + } else { + difference[key] = value + } + } + } + } catch (error) { + core.error(`Environment variables serialization error "${error}"`) + } + return {newPaths, variables: difference} +} + +export async function env() { + return await ['Machine', 'User'].reduce( + async (previous, current) => { + const modified = await previous + const command = `[Environment]::GetEnvironmentVariables('${current}') | ConvertTo-Json` + const args = ['-NoProfile', '-Command', `& {${command}}`] + const options = {failOnStdErr: true} + const {stdout} = await getExecOutput('powershell', args, options) + modified[current] = stdout + return modified + }, + Promise.resolve({} as Record) + ) +} + +export async function fallback( + oldEnv: Record, + newEnv: Record +) { + core.debug('Procceding with fallback installation approach') + const data = Object.entries(newEnv).reduce( + (previous, current) => { + return { + ...previous, + ...comapareEnvironment(oldEnv[current[0]], current[1]) + } + }, + {} as {newPaths: string[]; variables: Record} + ) + core.debug(`Setting up environment with "${JSON.stringify(data)}"`) + for (const newPath of data.newPaths) { + core.addPath(newPath) + } + for (const pair of Object.entries(data.variables)) { + core.exportVariable(pair[0], pair[1]) + } +} diff --git a/src/installer/windows/installation/index.ts b/src/installer/windows/installation/index.ts new file mode 100644 index 00000000..5df88108 --- /dev/null +++ b/src/installer/windows/installation/index.ts @@ -0,0 +1,64 @@ +import * as core from '@actions/core' +import {exec} from '@actions/exec' +import {Installation} from './base' +import {firstDirectoryLayout, secondDirectoryLayout} from './approach' +import {env, fallback} from './fallback' + +declare module './base' { + // eslint-disable-next-line no-shadow, @typescript-eslint/no-namespace + export namespace Installation { + export function get(install?: string): Promise + export function install(exe: string): Promise + export function detect( + oldEnv: Record, + newEnv: Record + ): Promise + } +} + +Installation.get = async (install?: string) => { + if (!(install?.length ?? 1)) { + return undefined + } + const approaches = [ + async () => secondDirectoryLayout(install), + async () => firstDirectoryLayout(install) + ] + let counter = 0 + for (const approach of approaches) { + counter += 1 + try { + const installation = await approach() + core.debug(`Installation location at "${installation.location}"`) + core.debug(`Toolchain installed at "${installation.toolchain}"`) + core.debug(`SDK installed at "${installation.sdkroot}"`) + core.debug(`Runtime installed at "${installation.runtime}"`) + core.debug(`Development directory at "${installation.devdir}"`) + return installation + } catch (error) { + core.debug(`Failed ${counter} time(s)`) + } + } + return undefined +} + +Installation.install = async (exe: string) => { + core.debug(`Installing toolchain from "${exe}"`) + const oldEnv = await env() + await exec(`"${exe}"`, ['-q']) + const newEnv = await env() + return Installation.detect(oldEnv, newEnv) +} + +Installation.detect = async ( + oldEnv: Record, + newEnv: Record +) => { + const installation = await Installation.get() + if (!installation) { + fallback(oldEnv, newEnv) + } + return installation +} + +export * from './base' diff --git a/src/platform/index.ts b/src/platform/index.ts index 1fce1a5f..2da7da95 100644 --- a/src/platform/index.ts +++ b/src/platform/index.ts @@ -10,7 +10,7 @@ import {ToolchainSnapshot} from '../snapshot' import {ToolchainInstaller} from '../installer' declare module './base' { - // eslint-disable-next-line no-shadow + // eslint-disable-next-line no-shadow, @typescript-eslint/no-namespace export namespace Platform { export function currentPlatform(): Promise< Platform> diff --git a/src/snapshot/release.ts b/src/snapshot/release.ts index ec183685..26bb0c08 100644 --- a/src/snapshot/release.ts +++ b/src/snapshot/release.ts @@ -8,7 +8,7 @@ export interface SwiftRelease { readonly platforms: SwiftRelease.Platform[] } -// eslint-disable-next-line no-redeclare +// eslint-disable-next-line @typescript-eslint/no-namespace export namespace SwiftRelease { export interface Platform { readonly name: string diff --git a/src/utils/visual_studio/setup.ts b/src/utils/visual_studio/setup.ts index f5a04452..2aefab70 100644 --- a/src/utils/visual_studio/setup.ts +++ b/src/utils/visual_studio/setup.ts @@ -4,7 +4,7 @@ import {VisualStudio, VisualStudioRequirement} from './base' import {VSWhere} from './vswhere' declare module './base' { - // eslint-disable-next-line no-shadow + // eslint-disable-next-line no-shadow, @typescript-eslint/no-namespace export namespace VisualStudio { function setup(requirement: VisualStudioRequirement): Promise } diff --git a/src/utils/visual_studio/vswhere.ts b/src/utils/visual_studio/vswhere.ts index 66046d67..878a0960 100644 --- a/src/utils/visual_studio/vswhere.ts +++ b/src/utils/visual_studio/vswhere.ts @@ -3,6 +3,7 @@ import {promises as fs} from 'fs' import * as io from '@actions/io' import * as core from '@actions/core' +// eslint-disable-next-line @typescript-eslint/no-namespace export namespace VSWhere { /// Get vswhere and vs_installer paths /// Borrowed from setup-msbuild action: https://github.com/microsoft/setup-msbuild diff --git a/src/version/index.ts b/src/version/index.ts index a928e96e..2080d41a 100644 --- a/src/version/index.ts +++ b/src/version/index.ts @@ -8,7 +8,7 @@ import {ToolchainSnapshotName, DEVELOPMENT_SNAPSHOT} from './name' import {ToolchainSnapshotLocation} from './location' declare module './base' { - // eslint-disable-next-line no-shadow + // eslint-disable-next-line no-shadow, @typescript-eslint/no-namespace export namespace ToolchainVersion { function create(requested: string, dev: boolean): ToolchainVersion }