diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7a654ec..c2badc5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -67,7 +67,7 @@ jobs: - name: Setup npm pacakges run: npm install --legacy-peer-deps env: - SETUPSWIFT_SWIFTORG_METADATA: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.swiftorg != '' && format('{"commit":"{0}}"}', github.event.inputs.swiftorg) || github.event_name == 'workflow_dispatch' && github.event.inputs.swiftorg == 'true' || github.event_name == 'schedule' && '{"commit":"HEAD"}' }} + SETUPSWIFT_SWIFTORG_METADATA: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.swiftorg != '' && format('{{"commit":"{0}"}}', github.event.inputs.swiftorg) || github.event_name == 'workflow_dispatch' && github.event.inputs.swiftorg == 'true' || github.event_name == 'schedule' && '{"commit":"HEAD"}' }} - name: Check latest swift.org id: swift-org @@ -159,6 +159,7 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] swift: ['latest'] development: [false, true] + check-link: [false] include: - os: macos-latest swift: '5.0.0' # oldest @@ -172,12 +173,14 @@ jobs: - os: windows-latest swift: '5.9' # 2nd installation approach development: false + check-link: false - os: ubuntu-latest swift: '5.3.0' # oldest development: false - os: windows-latest swift: '5.3' # 1st installation approach development: false + check-link: true - os: ubuntu-22.04 swift: ${{ fromJSON(vars.SETUPSWIFT_CUSTOM_TOOLCHAINS).ubuntu2204 }} # custom toolchain development: true @@ -227,7 +230,7 @@ jobs: run: swift --version | grep ${{ steps.setup-swift.outputs.swift-version }} || exit 1 - name: Check link - if: runner.os == 'Windows' + if: runner.os == 'Windows' && matrix.check-link == 'true' run: which link | grep "Microsoft Visual Studio" || exit 1 - name: Install SDK @@ -412,10 +415,6 @@ jobs: - name: Verify Swift version run: swift --version | grep ${{ steps.setup-swift.outputs.swift-version }} || exit 1 - - name: Check link - if: runner.os == 'Windows' - run: which link | grep "Microsoft Visual Studio" || exit 1 - - name: Verify Swift SDKs if: runner.os != 'Windows' run: swift sdk list | grep ${{ steps.setup-swift.outputs.swift-version }}-RELEASE_static-linux || exit 1 diff --git a/__tests__/installer/windows.test.ts b/__tests__/installer/windows.test.ts index 8706042..ed9473b 100644 --- a/__tests__/installer/windows.test.ts +++ b/__tests__/installer/windows.test.ts @@ -180,7 +180,37 @@ describe('windows toolchain installation verification', () => { jest.spyOn(fs, 'rename').mockResolvedValue() jest.spyOn(core, 'getBooleanInput').mockReturnValue(false) jest.spyOn(exec, 'exec').mockResolvedValue(0) - jest.spyOn(exec, 'getExecOutput').mockResolvedValue({ + const execSpy = jest.spyOn(exec, 'getExecOutput').mockResolvedValue({ + exitCode: 0, + stdout: JSON.stringify([visualStudio]), + stderr: '' + }) + jest.spyOn(cache, 'restoreCache').mockResolvedValue(undefined) + const cacheSpy = jest.spyOn(cache, 'saveCache').mockResolvedValue(1) + jest.spyOn(toolCache, 'downloadTool').mockResolvedValue(download) + jest.spyOn(exec, 'exec').mockResolvedValue(0) + await expect(installer['download']('x86_64')).resolves.toBe( + `${download}.exe` + ) + expect(execSpy).not.toHaveBeenCalled() + expect(cacheSpy).not.toHaveBeenCalled() + }) + + it('tests download without caching with custom Visual Studio components', async () => { + const installer = new WindowsToolchainInstaller(toolchain) + expect(installer['version']).toStrictEqual(parseSemVer('5.8')) + expect(installer['baseUrl'].href).toBe( + 'https://download.swift.org/swift-5.8-release/windows10/swift-5.8-RELEASE' + ) + + const download = path.resolve('tool', 'download', 'path') + process.env.VSWHERE_PATH = path.join('C:', 'Visual Studio') + jest.spyOn(fs, 'access').mockResolvedValue() + jest.spyOn(fs, 'rename').mockResolvedValue() + jest.spyOn(core, 'getInput').mockReturnValue(' ') + jest.spyOn(core, 'getBooleanInput').mockReturnValue(false) + jest.spyOn(exec, 'exec').mockResolvedValue(0) + const execSpy = jest.spyOn(exec, 'getExecOutput').mockResolvedValue({ exitCode: 0, stdout: JSON.stringify([visualStudio]), stderr: '' @@ -192,6 +222,7 @@ describe('windows toolchain installation verification', () => { await expect(installer['download']('x86_64')).resolves.toBe( `${download}.exe` ) + expect(execSpy).toHaveBeenCalled() expect(cacheSpy).not.toHaveBeenCalled() }) @@ -265,6 +296,11 @@ describe('windows toolchain installation verification', () => { expect(addPathSpy.mock.calls).toStrictEqual([['b'], ['c']]) expect(exportVariableSpy.mock.calls).toStrictEqual([['SDKROOT', 'root']]) + jest.spyOn(exec, 'getExecOutput').mockResolvedValueOnce({ + exitCode: 0, + stdout: 'Apple Swift version 5.8', + stderr: '' + }) const setupSpy = jest .spyOn(VisualStudio, 'setup') .mockResolvedValue(visualStudio) @@ -315,6 +351,11 @@ describe('windows toolchain installation verification', () => { expect(addPathSpy.mock.calls).toStrictEqual([['b'], ['c']]) expect(exportVariableSpy.mock.calls).toStrictEqual([['SDKROOT', 'root']]) + jest.spyOn(exec, 'getExecOutput').mockResolvedValueOnce({ + exitCode: 0, + stdout: 'Apple Swift version 5.8', + stderr: '' + }) const setupSpy = jest .spyOn(VisualStudio, 'setup') .mockResolvedValue(visualStudio) @@ -336,11 +377,18 @@ describe('windows toolchain installation verification', () => { .mockResolvedValue() jest.spyOn(fs, 'copyFile').mockResolvedValue() jest.spyOn(exec, 'exec').mockResolvedValue(0) - jest.spyOn(exec, 'getExecOutput').mockResolvedValue({ - exitCode: 0, - stdout: vsEnvs.join(os.EOL), - stderr: '' - }) + jest + .spyOn(exec, 'getExecOutput') + .mockResolvedValueOnce({ + exitCode: 0, + stdout: 'Apple Swift version 5.8', + stderr: '' + }) + .mockResolvedValue({ + exitCode: 0, + stdout: vsEnvs.join(os.EOL), + stderr: '' + }) const toolPath = path.join( installation, 'Developer', @@ -387,11 +435,18 @@ describe('windows toolchain installation verification', () => { }) jest.spyOn(fs, 'copyFile').mockResolvedValue() jest.spyOn(exec, 'exec').mockResolvedValue(0) - jest.spyOn(exec, 'getExecOutput').mockResolvedValue({ - exitCode: 0, - stdout: vsEnvs.join(os.EOL), - stderr: '' - }) + jest + .spyOn(exec, 'getExecOutput') + .mockResolvedValueOnce({ + exitCode: 0, + stdout: 'Apple Swift version 5.8', + stderr: '' + }) + .mockResolvedValue({ + exitCode: 0, + stdout: vsEnvs.join(os.EOL), + stderr: '' + }) const toolPath = path.join( installation, 'Developer', @@ -426,7 +481,9 @@ describe('windows toolchain installation verification', () => { it('tests add to PATH without SDK copying', async () => { const installer = new WindowsToolchainInstaller(toolchain) const installation = path.resolve('tool', 'installed', 'path') - jest.spyOn(VisualStudio, 'setup').mockResolvedValue(visualStudio) + const vsSetupSpy = jest + .spyOn(VisualStudio, 'setup') + .mockResolvedValue(visualStudio) jest .spyOn(fs, 'access') .mockRejectedValueOnce(new Error()) @@ -441,11 +498,168 @@ describe('windows toolchain installation verification', () => { }) jest.spyOn(fs, 'copyFile').mockResolvedValue() jest.spyOn(exec, 'exec').mockResolvedValue(0) - jest.spyOn(exec, 'getExecOutput').mockResolvedValue({ - exitCode: 0, - stdout: vsEnvs.join(os.EOL), - stderr: '' - }) + jest + .spyOn(exec, 'getExecOutput') + .mockResolvedValueOnce({ + exitCode: 0, + stdout: 'Apple Swift version 5.8', + stderr: '' + }) + .mockResolvedValue({ + exitCode: 0, + stdout: vsEnvs.join(os.EOL), + stderr: '' + }) + const toolPath = path.join( + installation, + 'Developer', + 'Toolchains', + 'unknown-Asserts-development.xctoolchain' + ) + const sdkroot = path.join( + installation, + 'Developer', + 'Platforms', + 'Windows.platform', + 'Developer', + 'SDKs', + 'Windows.sdk' + ) + const swiftLibs = path.join(sdkroot, 'usr', 'lib', 'swift') + const swiftPath = path.join(toolPath, 'usr', 'bin') + const swiftDev = path.join(installation, 'Swift-development', 'bin') + const icu67 = path.join(installation, 'icu-67', 'usr', 'bin') + await installer['add'](installation, 'x86_64') + expect(vsSetupSpy).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) + expect(process.env.SWIFTFLAGS).toContain(`-sdk ${sdkroot}`) + expect(process.env.SWIFTFLAGS).toContain(`-I ${swiftLibs}`) + expect(process.env.SWIFTFLAGS).toContain( + `-L ${path.join(swiftLibs, 'windows')}` + ) + }) + + it('tests add to PATH without SDK copying Swift 5.9.1', async () => { + const toolchain = { + name: 'Windows 10', + date: new Date('2023-10-19'), + download: 'swift-5.9-RELEASE-windows10.exe', + download_signature: 'swift-5.9.1-RELEASE-windows10.exe.sig', + dir: 'swift-5.9.1-RELEASE', + platform: 'windows10', + branch: 'swift-5.9.1-release', + windows: true, + preventCaching: false + } + const installer = new WindowsToolchainInstaller(toolchain) + const installation = path.resolve('tool', 'installed', 'path') + const vsSetupSpy = jest.spyOn(VisualStudio, 'setup') + jest + .spyOn(fs, 'access') + .mockRejectedValueOnce(new Error()) + .mockImplementation(async p => { + if ( + typeof p === 'string' && + (p.endsWith('ucrt.modulemap') || p.endsWith('winsdk.modulemap')) + ) { + return Promise.reject(new Error()) + } + return Promise.resolve() + }) + jest.spyOn(fs, 'copyFile').mockResolvedValue() + jest.spyOn(exec, 'exec').mockResolvedValue(0) + jest + .spyOn(exec, 'getExecOutput') + .mockResolvedValueOnce({ + exitCode: 0, + stdout: 'Apple Swift version 5.9.1', + stderr: '' + }) + .mockResolvedValue({ + exitCode: 0, + stdout: vsEnvs.join(os.EOL), + stderr: '' + }) + jest.spyOn(core, 'getBooleanInput').mockReturnValue(false) + const toolPath = path.join( + installation, + 'Developer', + 'Toolchains', + 'unknown-Asserts-development.xctoolchain' + ) + const sdkroot = path.join( + installation, + 'Developer', + 'Platforms', + 'Windows.platform', + 'Developer', + 'SDKs', + 'Windows.sdk' + ) + const swiftLibs = path.join(sdkroot, 'usr', 'lib', 'swift') + const swiftPath = path.join(toolPath, 'usr', 'bin') + const swiftDev = path.join(installation, 'Swift-development', 'bin') + const icu67 = path.join(installation, 'icu-67', 'usr', 'bin') + await installer['add'](installation, 'x86_64') + expect(vsSetupSpy).not.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) + expect(process.env.SWIFTFLAGS).toContain(`-sdk ${sdkroot}`) + expect(process.env.SWIFTFLAGS).toContain(`-I ${swiftLibs}`) + expect(process.env.SWIFTFLAGS).toContain( + `-L ${path.join(swiftLibs, 'windows')}` + ) + }) + + it('tests add to PATH without SDK copying Swift 5.9.1 with Visual Studio linker', async () => { + const toolchain = { + name: 'Windows 10', + date: new Date('2023-10-19'), + download: 'swift-5.9-RELEASE-windows10.exe', + download_signature: 'swift-5.9.1-RELEASE-windows10.exe.sig', + dir: 'swift-5.9.1-RELEASE', + platform: 'windows10', + branch: 'swift-5.9.1-release', + windows: true, + preventCaching: false + } + const installer = new WindowsToolchainInstaller(toolchain) + const installation = path.resolve('tool', 'installed', 'path') + const vsSetupSpy = jest + .spyOn(VisualStudio, 'setup') + .mockResolvedValue(visualStudio) + jest + .spyOn(fs, 'access') + .mockRejectedValueOnce(new Error()) + .mockImplementation(async p => { + if ( + typeof p === 'string' && + (p.endsWith('ucrt.modulemap') || p.endsWith('winsdk.modulemap')) + ) { + return Promise.reject(new Error()) + } + return Promise.resolve() + }) + jest.spyOn(fs, 'copyFile').mockResolvedValue() + jest.spyOn(exec, 'exec').mockResolvedValue(0) + jest + .spyOn(exec, 'getExecOutput') + .mockResolvedValueOnce({ + exitCode: 0, + stdout: 'Apple Swift version 5.9.1', + stderr: '' + }) + .mockResolvedValue({ + exitCode: 0, + stdout: vsEnvs.join(os.EOL), + stderr: '' + }) + jest.spyOn(core, 'getBooleanInput').mockReturnValue(true) const toolPath = path.join( installation, 'Developer', @@ -466,6 +680,7 @@ describe('windows toolchain installation verification', () => { const swiftDev = path.join(installation, 'Swift-development', 'bin') const icu67 = path.join(installation, 'icu-67', 'usr', 'bin') await installer['add'](installation, 'x86_64') + expect(vsSetupSpy).toHaveBeenCalled() expect(process.env.PATH?.includes(swiftPath)).toBeTruthy() expect(process.env.PATH?.includes(swiftDev)).toBeTruthy() expect(process.env.PATH?.includes(icu67)).toBeTruthy() @@ -517,11 +732,18 @@ describe('windows toolchain installation verification', () => { jest.spyOn(toolCache, 'cacheDir').mockResolvedValue(cached) jest.spyOn(cache, 'saveCache').mockResolvedValue(1) jest.spyOn(exec, 'exec').mockResolvedValue(0) - jest.spyOn(exec, 'getExecOutput').mockResolvedValue({ - exitCode: 0, - stdout: vsEnvs.join(os.EOL), - stderr: '' - }) + jest + .spyOn(exec, 'getExecOutput') + .mockResolvedValueOnce({ + exitCode: 0, + stdout: 'Apple Swift version 5.8', + stderr: '' + }) + .mockResolvedValue({ + exitCode: 0, + stdout: vsEnvs.join(os.EOL), + stderr: '' + }) await installer.install('x86_64') expect(setupSpy).toHaveBeenCalled() expect(process.env.PATH?.includes(swiftPath)).toBeTruthy() diff --git a/action.yml b/action.yml index 554dc89..a76fd35 100644 --- a/action.yml +++ b/action.yml @@ -54,6 +54,11 @@ inputs: i.e. Enable this option for installing static SDK: https://www.swift.org/documentation/articles/static-linux-getting-started.html required: false default: 'false' + prefer-visual-studio-linker: + description: >- + Whether to prefer using the Visual Studio linker over the default linker. This is unsafe and not recommended to set. + required: false + default: 'false' sdks: description: >- Semi-colon separated list of Swift SDKs to install along with the main toolchain. diff --git a/dist/index.js b/dist/index.js index 2c06518..6bd7396 100644 --- a/dist/index.js +++ b/dist/index.js @@ -634,9 +634,14 @@ class WindowsToolchainInstaller extends verify_1.VerifyingToolchainInstaller { }; } async download(arch) { - core.debug(`Using VS requirement ${JSON.stringify(this.vsRequirement(arch))}`); + let vsSetupAction = new Promise(resolve => resolve({})); + const vsComponents = core.getInput('visual-studio-components'); + if (vsComponents.length) { + core.debug(`Using VS requirement ${JSON.stringify(this.vsRequirement(arch))}`); + vsSetupAction = utils_1.VisualStudio.setup(await this.vsRequirement(arch)); + } const [, toolchain] = await Promise.all([ - utils_1.VisualStudio.setup(await this.vsRequirement(arch)), + vsSetupAction, super.download(arch) ]); const exeFile = `${toolchain}.exe`; @@ -685,8 +690,12 @@ class WindowsToolchainInstaller extends verify_1.VerifyingToolchainInstaller { core.warning(`Failed VS enviroment after installation ${installLocation}`); return; } - const visualStudio = await utils_1.VisualStudio.setup(await this.vsRequirement(arch)); - await visualStudio.update(sdkroot); + const version = await this.installedSwiftVersion(); + if (semver.lte(semver.coerce(version) ?? version, '5.9.0') || + core.getBooleanInput('prefer-visual-studio-linker')) { + const visualStudio = await utils_1.VisualStudio.setup(await this.vsRequirement(arch)); + await visualStudio.update(sdkroot); + } const swiftFlags = [ '-sdk', sdkroot, diff --git a/src/installer/windows/index.ts b/src/installer/windows/index.ts index 34ab57a..df87a97 100644 --- a/src/installer/windows/index.ts +++ b/src/installer/windows/index.ts @@ -81,13 +81,20 @@ export class WindowsToolchainInstaller extends VerifyingToolchainInstaller resolve({})) + const vsComponents = core.getInput('visual-studio-components') + if (vsComponents.length) { + core.debug( + `Using VS requirement ${JSON.stringify(this.vsRequirement(arch))}` + ) + vsSetupAction = VisualStudio.setup(await this.vsRequirement(arch)) + } + const [, toolchain] = await Promise.all([ - VisualStudio.setup(await this.vsRequirement(arch)), + vsSetupAction, super.download(arch) ]) + const exeFile = `${toolchain}.exe` await fs.rename(toolchain, exeFile) core.debug(`Toolchain installer downloaded at "${exeFile}"`) @@ -138,10 +145,17 @@ export class WindowsToolchainInstaller extends VerifyingToolchainInstaller