diff --git a/__tests__/installer/windows.test.ts b/__tests__/installer/windows.test.ts index e2a1c93..a249a71 100644 --- a/__tests__/installer/windows.test.ts +++ b/__tests__/installer/windows.test.ts @@ -17,7 +17,7 @@ describe('windows toolchain installation verification', () => { download: 'swift-5.8-RELEASE-windows10.exe', download_signature: 'swift-5.8-RELEASE-windows10.exe.sig', dir: 'swift-5.8-RELEASE', - platform: 'ubuntu2204', + platform: 'windows10', branch: 'swift-5.8-release', windows: true } @@ -48,7 +48,7 @@ describe('windows toolchain installation verification', () => { const installer = new WindowsToolchainInstaller(toolchain) expect(installer['version']).toStrictEqual(parseSemVer('5.8')) expect(installer['baseUrl']).toBe( - 'https://download.swift.org/swift-5.8-release/ubuntu2204/swift-5.8-RELEASE' + 'https://download.swift.org/swift-5.8-release/windows10/swift-5.8-RELEASE' ) const download = path.resolve('tool', 'download', 'path') @@ -67,7 +67,6 @@ describe('windows toolchain installation verification', () => { jest.spyOn(toolCache, 'downloadTool').mockResolvedValue(download) jest.spyOn(exec, 'exec').mockResolvedValue(0) await expect(installer['download']()).resolves.toBe(`${download}.exe`) - expect(installer['visualStudio']).toStrictEqual(visualStudio) expect(cacheSpy).toHaveBeenCalled() }) @@ -75,7 +74,7 @@ describe('windows toolchain installation verification', () => { const installer = new WindowsToolchainInstaller(toolchain) expect(installer['version']).toStrictEqual(parseSemVer('5.8')) expect(installer['baseUrl']).toBe( - 'https://download.swift.org/swift-5.8-release/ubuntu2204/swift-5.8-RELEASE' + 'https://download.swift.org/swift-5.8-release/windows10/swift-5.8-RELEASE' ) const download = path.resolve('tool', 'download', 'path') @@ -94,7 +93,6 @@ describe('windows toolchain installation verification', () => { jest.spyOn(toolCache, 'downloadTool').mockResolvedValue(download) jest.spyOn(exec, 'exec').mockResolvedValue(0) await expect(installer['download']()).resolves.toBe(`${download}.exe`) - expect(installer['visualStudio']).toStrictEqual(visualStudio) expect(cacheSpy).not.toHaveBeenCalled() }) @@ -131,8 +129,8 @@ describe('windows toolchain installation verification', () => { it('tests add to PATH', async () => { const installer = new WindowsToolchainInstaller(toolchain) - installer['visualStudio'] = visualStudio const installation = path.resolve('tool', 'installed', 'path') + jest.spyOn(vs, 'setupVisualStudioTools').mockResolvedValue(visualStudio) jest.spyOn(fs, 'access').mockRejectedValue(new Error()) jest.spyOn(fs, 'copyFile').mockResolvedValue() jest.spyOn(exec, 'exec').mockResolvedValue(0) @@ -166,6 +164,47 @@ describe('windows toolchain installation verification', () => { expect(process.env.SDKROOT).toBe(sdkroot) }) + it('tests installation with cache', async () => { + const installer = new WindowsToolchainInstaller(toolchain) + const cached = path.resolve('tool', 'cached', 'path') + const toolPath = path.join( + cached, + 'Developer', + 'Toolchains', + 'unknown-Asserts-development.xctoolchain' + ) + const sdkroot = path.join( + cached, + 'Developer', + 'Platforms', + 'Windows.platform', + 'Developer', + 'SDKs', + 'Windows.sdk' + ) + const swiftPath = path.join(toolPath, 'usr', 'bin') + const swiftDev = path.join(cached, 'Swift-development', 'bin') + const icu67 = path.join(cached, 'icu-67', 'usr', 'bin') + const setupSpy = jest + .spyOn(vs, 'setupVisualStudioTools') + .mockResolvedValue(visualStudio) + jest.spyOn(fs, 'access').mockRejectedValue(new Error()) + jest.spyOn(fs, 'copyFile').mockResolvedValue() + jest.spyOn(toolCache, 'find').mockReturnValue(cached) + jest.spyOn(exec, 'exec').mockResolvedValue(0) + jest.spyOn(exec, 'getExecOutput').mockResolvedValue({ + exitCode: 0, + stdout: vsEnvs.join(os.EOL), + stderr: '' + }) + await installer.install() + expect(setupSpy).toHaveBeenCalled() + expect(process.env.PATH?.includes(swiftPath)).toBeTruthy() + expect(process.env.PATH?.includes(swiftDev)).toBeTruthy() + expect(process.env.PATH?.includes(icu67)).toBeTruthy() + expect(process.env.SDKROOT).toBe(sdkroot) + }) + it('tests installed swift version detection', async () => { const installer = new WindowsToolchainInstaller(toolchain) jest.spyOn(exec, 'getExecOutput').mockResolvedValue({ diff --git a/__tests__/utils/visual_studio.test.ts b/__tests__/utils/visual_studio.test.ts index e946f41..c099b40 100644 --- a/__tests__/utils/visual_studio.test.ts +++ b/__tests__/utils/visual_studio.test.ts @@ -58,36 +58,36 @@ describe('visual studio setup validation', () => { ) }) - it('tests visual studio setup successfully', async () => { + it('tests visual studio setup fails when invalid path', async () => { fsAccessMock() process.env.VSWHERE_PATH = path.join('C:', 'Visual Studio') - jest.spyOn(exec, 'exec').mockResolvedValue(0) + jest.spyOn(exec, 'exec').mockResolvedValue(-1) jest.spyOn(exec, 'getExecOutput').mockResolvedValue({ exitCode: 0, - stdout: JSON.stringify([visualStudio]), + stdout: JSON.stringify([{...visualStudio, installationPath: ''}]), stderr: '' }) await expect( vs.setupVisualStudioTools({version: '16', components: ['Component']}) - ).resolves.toMatchObject(visualStudio) + ).rejects.toMatchObject( + new Error( + `Unable to find any Visual Studio installation for version: 16.` + ) + ) }) - it('tests visual studio setup fails when invalid path', async () => { + it('tests visual studio setup successfully', async () => { fsAccessMock() process.env.VSWHERE_PATH = path.join('C:', 'Visual Studio') - jest.spyOn(exec, 'exec').mockResolvedValue(-1) + jest.spyOn(exec, 'exec').mockResolvedValue(0) jest.spyOn(exec, 'getExecOutput').mockResolvedValue({ exitCode: 0, - stdout: JSON.stringify([{...visualStudio, installationPath: ''}]), + stdout: JSON.stringify([visualStudio]), stderr: '' }) await expect( vs.setupVisualStudioTools({version: '16', components: ['Component']}) - ).rejects.toMatchObject( - new Error( - `Unable to find any Visual Studio installation for version: 16.` - ) - ) + ).resolves.toMatchObject(visualStudio) }) }) diff --git a/dist/index.js b/dist/index.js index fc55b4d..573b1a3 100644 --- a/dist/index.js +++ b/dist/index.js @@ -469,26 +469,28 @@ const exec_1 = __nccwpck_require__(71514); const verify_1 = __nccwpck_require__(38780); const utils_1 = __nccwpck_require__(11606); class WindowsToolchainInstaller extends verify_1.VerifyingToolchainInstaller { + constructor() { + super(...arguments); + this.vsRequirement = { + version: '16', + components: [ + 'Microsoft.VisualStudio.Component.VC.ATL', + 'Microsoft.VisualStudio.Component.VC.CMake.Project', + 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64', + 'Microsoft.VisualStudio.Component.Windows10SDK' + ] + }; + } download() { const _super = Object.create(null, { download: { get: () => super.download } }); return __awaiter(this, void 0, void 0, function* () { - const vsRequirement = { - version: '16', - components: [ - 'Microsoft.VisualStudio.Component.VC.ATL', - 'Microsoft.VisualStudio.Component.VC.CMake.Project', - 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64', - 'Microsoft.VisualStudio.Component.Windows10SDK' - ] - }; - core.debug(`Using Visual Studio requirement ${vsRequirement}`); - const [visualStudio, toolchain] = yield Promise.all([ - (0, utils_1.setupVisualStudioTools)(vsRequirement), + core.debug(`Using Visual Studio requirement ${this.vsRequirement}`); + const [, toolchain] = yield Promise.all([ + (0, utils_1.setupVisualStudioTools)(this.vsRequirement), _super.download.call(this) ]); - this.visualStudio = visualStudio; const exeFile = `${toolchain}.exe`; yield fs_1.promises.rename(toolchain, exeFile); core.debug(`Toolchain installer downloaded at "${exeFile}"`); @@ -498,10 +500,7 @@ class WindowsToolchainInstaller extends verify_1.VerifyingToolchainInstaller { unpack(exe) { return __awaiter(this, void 0, void 0, function* () { core.debug(`Installing toolchain from "${exe}"`); - const code = yield (0, exec_1.exec)(`"${exe}"`, ['-q']); - if (code !== 0) { - throw new Error(`Swift installer failed with exit code: "${code}"`); - } + yield (0, exec_1.exec)(`"${exe}"`, ['-q']); const installation = yield Installation.detect(); return installation.location; }); @@ -525,10 +524,8 @@ class WindowsToolchainInstaller extends verify_1.VerifyingToolchainInstaller { core.addPath(envPath); } core.debug(`Swift installed at "${swiftPath}"`); - if (!this.visualStudio) { - throw new Error('No supported Visual Studio installation in installer'); - } - yield (0, utils_1.setupSupportFiles)(this.visualStudio, installation.sdkroot); + const visualStudio = yield (0, utils_1.setupVisualStudioTools)(this.vsRequirement); + yield (0, utils_1.setupSupportFiles)(visualStudio, installation.sdkroot); const swiftFlags = `-sdk %SDKROOT% -I %SDKROOT%/usr/lib/swift -L %SDKROOT%/usr/lib/swift/windows`; core.exportVariable('SWIFTFLAGS', swiftFlags); }); @@ -1759,12 +1756,7 @@ function refreshKeys() { function refreshKeysFromServer(server) { return __awaiter(this, void 0, void 0, function* () { try { - const code = yield (0, exec_1.exec)('gpg', [ - '--keyserver', - server, - '--refresh-keys', - 'Swift' - ]); + const code = yield (0, exec_1.exec)('gpg', ['--keyserver', server, '--refresh-keys', 'Swift'], { ignoreReturnCode: true }); return code === 0; } catch (error) { @@ -1908,9 +1900,13 @@ function setupSupportFiles(visualStudio, sdkroot) { }); } exports.setupSupportFiles = setupSupportFiles; +let visualStudio; /// set up required visual studio tools for swift on windows function setupVisualStudioTools(requirement) { return __awaiter(this, void 0, void 0, function* () { + if (visualStudio) { + return visualStudio; + } /// https://github.com/microsoft/vswhere/wiki/Find-MSBuild /// get visual studio properties const vswhereExe = yield getVsWherePath(); @@ -1942,6 +1938,7 @@ function setupVisualStudioTools(requirement) { '--force', '--quiet' ]); + visualStudio = vs; return vs; }); } diff --git a/src/installer/windows.ts b/src/installer/windows.ts deleted file mode 100644 index c7692d0..0000000 --- a/src/installer/windows.ts +++ /dev/null @@ -1,162 +0,0 @@ -import * as path from 'path' -import {promises as fs} from 'fs' -import * as core from '@actions/core' -import {exec} from '@actions/exec' -import {VerifyingToolchainInstaller} from './verify' -import {WindowsToolchainSnapshot} from '../snapshot' -import { - VisualStudioRequirement, - VisualStudio, - setupVisualStudioTools, - setupSupportFiles -} from '../utils' - -export class WindowsToolchainInstaller extends VerifyingToolchainInstaller { - private visualStudio?: VisualStudio - - protected async download() { - const vsRequirement: VisualStudioRequirement = { - version: '16', - components: [ - 'Microsoft.VisualStudio.Component.VC.ATL', - 'Microsoft.VisualStudio.Component.VC.CMake.Project', - 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64', - 'Microsoft.VisualStudio.Component.Windows10SDK' - ] - } - core.debug(`Using Visual Studio requirement ${vsRequirement}`) - const [visualStudio, toolchain] = await Promise.all([ - setupVisualStudioTools(vsRequirement), - super.download() - ]) - this.visualStudio = visualStudio - const exeFile = `${toolchain}.exe` - await fs.rename(toolchain, exeFile) - core.debug(`Toolchain installer downloaded at "${exeFile}"`) - return exeFile - } - - protected async unpack(exe: string) { - core.debug(`Installing toolchain from "${exe}"`) - const code = await exec(`"${exe}"`, ['-q']) - if (code !== 0) { - throw new Error(`Swift installer failed with exit code: "${code}"`) - } - const installation = await Installation.detect() - return installation.location - } - - protected async add(installLocation: string) { - const installation = await Installation.get(installLocation) - core.exportVariable('SDKROOT', installation.sdkroot) - if (installation.devdir) { - core.exportVariable('DEVELOPER_DIR', installation.devdir) - } - const location = installation.location - const swiftPath = path.join(installation.toolchain, 'usr', 'bin') - const swiftDev = path.join(location, 'Swift-development', 'bin') - const icu67 = path.join(location, 'icu-67', 'usr', 'bin') - const tools = path.join(location, 'Tools') - const runtimePath = path.join(installation.runtime, 'usr', 'bin') - const requirePaths = [swiftPath, swiftDev, icu67, tools, runtimePath] - - for (const envPath of requirePaths) { - core.debug(`Adding "${envPath}" to PATH`) - core.addPath(envPath) - } - core.debug(`Swift installed at "${swiftPath}"`) - if (!this.visualStudio) { - throw new Error('No supported Visual Studio installation in installer') - } - await setupSupportFiles(this.visualStudio, installation.sdkroot) - const swiftFlags = `-sdk %SDKROOT% -I %SDKROOT%/usr/lib/swift -L %SDKROOT%/usr/lib/swift/windows` - core.exportVariable('SWIFTFLAGS', swiftFlags) - } -} - -class Installation { - readonly location: string - readonly toolchain: string - readonly sdkroot: string - readonly runtime: string - readonly devdir?: string - - private constructor( - location: string, - toolchain: string, - sdkroot: string, - runtime: string, - devdir?: string - ) { - this.location = location - this.toolchain = toolchain - this.sdkroot = sdkroot - this.runtime = runtime - this.devdir = devdir - } - - static async get(location: string, fallback?: string) { - let toolchain: string - let sdkroot: string - let runtime: string - let devdir: string | undefined - - 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') - await fs.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) - } - - static async detect() { - const systemDrive = process.env.SystemDrive ?? 'C:' - const defaultPath = path.join(systemDrive, 'Library') - const devPath = path.join(systemDrive, 'Program Files', 'Swift') - const installation = await Installation.get(devPath, defaultPath) - if (path.relative(devPath, installation.location)) { - const runtimeRoot = path.join(installation.location, 'Swift') - try { - await fs.access(devPath) - await fs.cp(devPath, runtimeRoot, {recursive: true}) - } catch (error) { - core.debug(`Runtime check failed with "${error}"`) - } - } - 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 - } -} diff --git a/src/installer/windows/index.ts b/src/installer/windows/index.ts new file mode 100644 index 0000000..e2beb35 --- /dev/null +++ b/src/installer/windows/index.ts @@ -0,0 +1,64 @@ +import * as path from 'path' +import {promises as fs} from 'fs' +import * as core from '@actions/core' +import {exec} from '@actions/exec' +import {VerifyingToolchainInstaller} from '../verify' +import {WindowsToolchainSnapshot} from '../../snapshot' +import {setupVisualStudioTools, setupSupportFiles} from '../../utils' +import {Installation} from './installation' + +export class WindowsToolchainInstaller extends VerifyingToolchainInstaller { + private readonly vsRequirement = { + version: '16', + components: [ + 'Microsoft.VisualStudio.Component.VC.ATL', + 'Microsoft.VisualStudio.Component.VC.CMake.Project', + 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64', + 'Microsoft.VisualStudio.Component.Windows10SDK' + ] + } + + protected async download() { + core.debug(`Using Visual Studio requirement ${this.vsRequirement}`) + const [, toolchain] = await Promise.all([ + setupVisualStudioTools(this.vsRequirement), + super.download() + ]) + const exeFile = `${toolchain}.exe` + await fs.rename(toolchain, exeFile) + core.debug(`Toolchain installer downloaded at "${exeFile}"`) + return exeFile + } + + protected async unpack(exe: string) { + core.debug(`Installing toolchain from "${exe}"`) + await exec(`"${exe}"`, ['-q']) + const installation = await Installation.detect() + return installation.location + } + + protected async add(installLocation: string) { + const installation = await Installation.get(installLocation) + core.exportVariable('SDKROOT', installation.sdkroot) + if (installation.devdir) { + core.exportVariable('DEVELOPER_DIR', installation.devdir) + } + const location = installation.location + const swiftPath = path.join(installation.toolchain, 'usr', 'bin') + const swiftDev = path.join(location, 'Swift-development', 'bin') + const icu67 = path.join(location, 'icu-67', 'usr', 'bin') + const tools = path.join(location, 'Tools') + const runtimePath = path.join(installation.runtime, 'usr', 'bin') + const requirePaths = [swiftPath, swiftDev, icu67, tools, runtimePath] + + for (const envPath of requirePaths) { + core.debug(`Adding "${envPath}" to PATH`) + core.addPath(envPath) + } + core.debug(`Swift installed at "${swiftPath}"`) + const visualStudio = await setupVisualStudioTools(this.vsRequirement) + await setupSupportFiles(visualStudio, installation.sdkroot) + const swiftFlags = `-sdk %SDKROOT% -I %SDKROOT%/usr/lib/swift -L %SDKROOT%/usr/lib/swift/windows` + core.exportVariable('SWIFTFLAGS', swiftFlags) + } +} diff --git a/src/installer/windows/installation.ts b/src/installer/windows/installation.ts new file mode 100644 index 0000000..55061d1 --- /dev/null +++ b/src/installer/windows/installation.ts @@ -0,0 +1,90 @@ +import * as path from 'path' +import {promises as fs} from 'fs' +import * as core from '@actions/core' + +export class Installation { + readonly location: string + readonly toolchain: string + readonly sdkroot: string + readonly runtime: string + readonly devdir?: string + + private constructor( + location: string, + toolchain: string, + sdkroot: string, + runtime: string, + devdir?: string + ) { + this.location = location + this.toolchain = toolchain + this.sdkroot = sdkroot + this.runtime = runtime + this.devdir = devdir + } + + static async get(location: string, fallback?: string) { + let toolchain: string + let sdkroot: string + let runtime: string + let devdir: string | undefined + + 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') + await fs.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) + } + + static async detect() { + const systemDrive = process.env.SystemDrive ?? 'C:' + const defaultPath = path.join(systemDrive, 'Library') + const devPath = path.join(systemDrive, 'Program Files', 'Swift') + const installation = await Installation.get(devPath, defaultPath) + if (path.relative(devPath, installation.location)) { + const runtimeRoot = path.join(installation.location, 'Swift') + try { + await fs.access(devPath) + await fs.cp(devPath, runtimeRoot, {recursive: true}) + } catch (error) { + core.debug(`Runtime check failed with "${error}"`) + } + } + 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 + } +} diff --git a/src/utils/gpg.ts b/src/utils/gpg.ts index e9e866e..9a7fb77 100644 --- a/src/utils/gpg.ts +++ b/src/utils/gpg.ts @@ -59,12 +59,11 @@ async function refreshKeys() { async function refreshKeysFromServer(server: string) { try { - const code = await exec('gpg', [ - '--keyserver', - server, - '--refresh-keys', - 'Swift' - ]) + const code = await exec( + 'gpg', + ['--keyserver', server, '--refresh-keys', 'Swift'], + {ignoreReturnCode: true} + ) return code === 0 } catch (error) { core.warning( diff --git a/src/utils/visual_studio.ts b/src/utils/visual_studio.ts index 0397e75..95e625e 100644 --- a/src/utils/visual_studio.ts +++ b/src/utils/visual_studio.ts @@ -97,10 +97,15 @@ export async function setupSupportFiles( } } +let visualStudio: VisualStudio | undefined + /// set up required visual studio tools for swift on windows export async function setupVisualStudioTools( requirement: VisualStudioRequirement ) { + if (visualStudio) { + return visualStudio + } /// https://github.com/microsoft/vswhere/wiki/Find-MSBuild /// get visual studio properties const vswhereExe = await getVsWherePath() @@ -140,6 +145,7 @@ export async function setupVisualStudioTools( '--force', '--quiet' ]) + visualStudio = vs return vs }