Skip to content

Commit 4826b64

Browse files
authored
perf: improve Windows installation performance for Swift 5.9.1 and later (#434)
- Introduced `prefer-visual-studio-linker` flag for choosing Visual Studio linker
1 parent d916d5b commit 4826b64

File tree

5 files changed

+289
-40
lines changed

5 files changed

+289
-40
lines changed

.github/workflows/main.yml

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ jobs:
6767
- name: Setup npm pacakges
6868
run: npm install --legacy-peer-deps
6969
env:
70-
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"}' }}
70+
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"}' }}
7171

7272
- name: Check latest swift.org
7373
id: swift-org
@@ -159,6 +159,7 @@ jobs:
159159
os: [ubuntu-latest, macos-latest, windows-latest]
160160
swift: ['latest']
161161
development: [false, true]
162+
check-link: [false]
162163
include:
163164
- os: macos-latest
164165
swift: '5.0.0' # oldest
@@ -172,12 +173,14 @@ jobs:
172173
- os: windows-latest
173174
swift: '5.9' # 2nd installation approach
174175
development: false
176+
check-link: false
175177
- os: ubuntu-latest
176178
swift: '5.3.0' # oldest
177179
development: false
178180
- os: windows-latest
179181
swift: '5.3' # 1st installation approach
180182
development: false
183+
check-link: true
181184
- os: ubuntu-22.04
182185
swift: ${{ fromJSON(vars.SETUPSWIFT_CUSTOM_TOOLCHAINS).ubuntu2204 }} # custom toolchain
183186
development: true
@@ -227,7 +230,7 @@ jobs:
227230
run: swift --version | grep ${{ steps.setup-swift.outputs.swift-version }} || exit 1
228231

229232
- name: Check link
230-
if: runner.os == 'Windows'
233+
if: runner.os == 'Windows' && matrix.check-link == 'true'
231234
run: which link | grep "Microsoft Visual Studio" || exit 1
232235

233236
- name: Install SDK
@@ -412,10 +415,6 @@ jobs:
412415
- name: Verify Swift version
413416
run: swift --version | grep ${{ steps.setup-swift.outputs.swift-version }} || exit 1
414417

415-
- name: Check link
416-
if: runner.os == 'Windows'
417-
run: which link | grep "Microsoft Visual Studio" || exit 1
418-
419418
- name: Verify Swift SDKs
420419
if: runner.os != 'Windows'
421420
run: swift sdk list | grep ${{ steps.setup-swift.outputs.swift-version }}-RELEASE_static-linux || exit 1

__tests__/installer/windows.test.ts

Lines changed: 244 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,37 @@ describe('windows toolchain installation verification', () => {
180180
jest.spyOn(fs, 'rename').mockResolvedValue()
181181
jest.spyOn(core, 'getBooleanInput').mockReturnValue(false)
182182
jest.spyOn(exec, 'exec').mockResolvedValue(0)
183-
jest.spyOn(exec, 'getExecOutput').mockResolvedValue({
183+
const execSpy = jest.spyOn(exec, 'getExecOutput').mockResolvedValue({
184+
exitCode: 0,
185+
stdout: JSON.stringify([visualStudio]),
186+
stderr: ''
187+
})
188+
jest.spyOn(cache, 'restoreCache').mockResolvedValue(undefined)
189+
const cacheSpy = jest.spyOn(cache, 'saveCache').mockResolvedValue(1)
190+
jest.spyOn(toolCache, 'downloadTool').mockResolvedValue(download)
191+
jest.spyOn(exec, 'exec').mockResolvedValue(0)
192+
await expect(installer['download']('x86_64')).resolves.toBe(
193+
`${download}.exe`
194+
)
195+
expect(execSpy).not.toHaveBeenCalled()
196+
expect(cacheSpy).not.toHaveBeenCalled()
197+
})
198+
199+
it('tests download without caching with custom Visual Studio components', async () => {
200+
const installer = new WindowsToolchainInstaller(toolchain)
201+
expect(installer['version']).toStrictEqual(parseSemVer('5.8'))
202+
expect(installer['baseUrl'].href).toBe(
203+
'https://download.swift.org/swift-5.8-release/windows10/swift-5.8-RELEASE'
204+
)
205+
206+
const download = path.resolve('tool', 'download', 'path')
207+
process.env.VSWHERE_PATH = path.join('C:', 'Visual Studio')
208+
jest.spyOn(fs, 'access').mockResolvedValue()
209+
jest.spyOn(fs, 'rename').mockResolvedValue()
210+
jest.spyOn(core, 'getInput').mockReturnValue(' ')
211+
jest.spyOn(core, 'getBooleanInput').mockReturnValue(false)
212+
jest.spyOn(exec, 'exec').mockResolvedValue(0)
213+
const execSpy = jest.spyOn(exec, 'getExecOutput').mockResolvedValue({
184214
exitCode: 0,
185215
stdout: JSON.stringify([visualStudio]),
186216
stderr: ''
@@ -192,6 +222,7 @@ describe('windows toolchain installation verification', () => {
192222
await expect(installer['download']('x86_64')).resolves.toBe(
193223
`${download}.exe`
194224
)
225+
expect(execSpy).toHaveBeenCalled()
195226
expect(cacheSpy).not.toHaveBeenCalled()
196227
})
197228

@@ -265,6 +296,11 @@ describe('windows toolchain installation verification', () => {
265296
expect(addPathSpy.mock.calls).toStrictEqual([['b'], ['c']])
266297
expect(exportVariableSpy.mock.calls).toStrictEqual([['SDKROOT', 'root']])
267298

299+
jest.spyOn(exec, 'getExecOutput').mockResolvedValueOnce({
300+
exitCode: 0,
301+
stdout: 'Apple Swift version 5.8',
302+
stderr: ''
303+
})
268304
const setupSpy = jest
269305
.spyOn(VisualStudio, 'setup')
270306
.mockResolvedValue(visualStudio)
@@ -315,6 +351,11 @@ describe('windows toolchain installation verification', () => {
315351
expect(addPathSpy.mock.calls).toStrictEqual([['b'], ['c']])
316352
expect(exportVariableSpy.mock.calls).toStrictEqual([['SDKROOT', 'root']])
317353

354+
jest.spyOn(exec, 'getExecOutput').mockResolvedValueOnce({
355+
exitCode: 0,
356+
stdout: 'Apple Swift version 5.8',
357+
stderr: ''
358+
})
318359
const setupSpy = jest
319360
.spyOn(VisualStudio, 'setup')
320361
.mockResolvedValue(visualStudio)
@@ -336,11 +377,18 @@ describe('windows toolchain installation verification', () => {
336377
.mockResolvedValue()
337378
jest.spyOn(fs, 'copyFile').mockResolvedValue()
338379
jest.spyOn(exec, 'exec').mockResolvedValue(0)
339-
jest.spyOn(exec, 'getExecOutput').mockResolvedValue({
340-
exitCode: 0,
341-
stdout: vsEnvs.join(os.EOL),
342-
stderr: ''
343-
})
380+
jest
381+
.spyOn(exec, 'getExecOutput')
382+
.mockResolvedValueOnce({
383+
exitCode: 0,
384+
stdout: 'Apple Swift version 5.8',
385+
stderr: ''
386+
})
387+
.mockResolvedValue({
388+
exitCode: 0,
389+
stdout: vsEnvs.join(os.EOL),
390+
stderr: ''
391+
})
344392
const toolPath = path.join(
345393
installation,
346394
'Developer',
@@ -387,11 +435,18 @@ describe('windows toolchain installation verification', () => {
387435
})
388436
jest.spyOn(fs, 'copyFile').mockResolvedValue()
389437
jest.spyOn(exec, 'exec').mockResolvedValue(0)
390-
jest.spyOn(exec, 'getExecOutput').mockResolvedValue({
391-
exitCode: 0,
392-
stdout: vsEnvs.join(os.EOL),
393-
stderr: ''
394-
})
438+
jest
439+
.spyOn(exec, 'getExecOutput')
440+
.mockResolvedValueOnce({
441+
exitCode: 0,
442+
stdout: 'Apple Swift version 5.8',
443+
stderr: ''
444+
})
445+
.mockResolvedValue({
446+
exitCode: 0,
447+
stdout: vsEnvs.join(os.EOL),
448+
stderr: ''
449+
})
395450
const toolPath = path.join(
396451
installation,
397452
'Developer',
@@ -426,7 +481,9 @@ describe('windows toolchain installation verification', () => {
426481
it('tests add to PATH without SDK copying', async () => {
427482
const installer = new WindowsToolchainInstaller(toolchain)
428483
const installation = path.resolve('tool', 'installed', 'path')
429-
jest.spyOn(VisualStudio, 'setup').mockResolvedValue(visualStudio)
484+
const vsSetupSpy = jest
485+
.spyOn(VisualStudio, 'setup')
486+
.mockResolvedValue(visualStudio)
430487
jest
431488
.spyOn(fs, 'access')
432489
.mockRejectedValueOnce(new Error())
@@ -441,11 +498,168 @@ describe('windows toolchain installation verification', () => {
441498
})
442499
jest.spyOn(fs, 'copyFile').mockResolvedValue()
443500
jest.spyOn(exec, 'exec').mockResolvedValue(0)
444-
jest.spyOn(exec, 'getExecOutput').mockResolvedValue({
445-
exitCode: 0,
446-
stdout: vsEnvs.join(os.EOL),
447-
stderr: ''
448-
})
501+
jest
502+
.spyOn(exec, 'getExecOutput')
503+
.mockResolvedValueOnce({
504+
exitCode: 0,
505+
stdout: 'Apple Swift version 5.8',
506+
stderr: ''
507+
})
508+
.mockResolvedValue({
509+
exitCode: 0,
510+
stdout: vsEnvs.join(os.EOL),
511+
stderr: ''
512+
})
513+
const toolPath = path.join(
514+
installation,
515+
'Developer',
516+
'Toolchains',
517+
'unknown-Asserts-development.xctoolchain'
518+
)
519+
const sdkroot = path.join(
520+
installation,
521+
'Developer',
522+
'Platforms',
523+
'Windows.platform',
524+
'Developer',
525+
'SDKs',
526+
'Windows.sdk'
527+
)
528+
const swiftLibs = path.join(sdkroot, 'usr', 'lib', 'swift')
529+
const swiftPath = path.join(toolPath, 'usr', 'bin')
530+
const swiftDev = path.join(installation, 'Swift-development', 'bin')
531+
const icu67 = path.join(installation, 'icu-67', 'usr', 'bin')
532+
await installer['add'](installation, 'x86_64')
533+
expect(vsSetupSpy).toHaveBeenCalled()
534+
expect(process.env.PATH?.includes(swiftPath)).toBeTruthy()
535+
expect(process.env.PATH?.includes(swiftDev)).toBeTruthy()
536+
expect(process.env.PATH?.includes(icu67)).toBeTruthy()
537+
expect(process.env.SDKROOT).toBe(sdkroot)
538+
expect(process.env.SWIFTFLAGS).toContain(`-sdk ${sdkroot}`)
539+
expect(process.env.SWIFTFLAGS).toContain(`-I ${swiftLibs}`)
540+
expect(process.env.SWIFTFLAGS).toContain(
541+
`-L ${path.join(swiftLibs, 'windows')}`
542+
)
543+
})
544+
545+
it('tests add to PATH without SDK copying Swift 5.9.1', async () => {
546+
const toolchain = {
547+
name: 'Windows 10',
548+
date: new Date('2023-10-19'),
549+
download: 'swift-5.9-RELEASE-windows10.exe',
550+
download_signature: 'swift-5.9.1-RELEASE-windows10.exe.sig',
551+
dir: 'swift-5.9.1-RELEASE',
552+
platform: 'windows10',
553+
branch: 'swift-5.9.1-release',
554+
windows: true,
555+
preventCaching: false
556+
}
557+
const installer = new WindowsToolchainInstaller(toolchain)
558+
const installation = path.resolve('tool', 'installed', 'path')
559+
const vsSetupSpy = jest.spyOn(VisualStudio, 'setup')
560+
jest
561+
.spyOn(fs, 'access')
562+
.mockRejectedValueOnce(new Error())
563+
.mockImplementation(async p => {
564+
if (
565+
typeof p === 'string' &&
566+
(p.endsWith('ucrt.modulemap') || p.endsWith('winsdk.modulemap'))
567+
) {
568+
return Promise.reject(new Error())
569+
}
570+
return Promise.resolve()
571+
})
572+
jest.spyOn(fs, 'copyFile').mockResolvedValue()
573+
jest.spyOn(exec, 'exec').mockResolvedValue(0)
574+
jest
575+
.spyOn(exec, 'getExecOutput')
576+
.mockResolvedValueOnce({
577+
exitCode: 0,
578+
stdout: 'Apple Swift version 5.9.1',
579+
stderr: ''
580+
})
581+
.mockResolvedValue({
582+
exitCode: 0,
583+
stdout: vsEnvs.join(os.EOL),
584+
stderr: ''
585+
})
586+
jest.spyOn(core, 'getBooleanInput').mockReturnValue(false)
587+
const toolPath = path.join(
588+
installation,
589+
'Developer',
590+
'Toolchains',
591+
'unknown-Asserts-development.xctoolchain'
592+
)
593+
const sdkroot = path.join(
594+
installation,
595+
'Developer',
596+
'Platforms',
597+
'Windows.platform',
598+
'Developer',
599+
'SDKs',
600+
'Windows.sdk'
601+
)
602+
const swiftLibs = path.join(sdkroot, 'usr', 'lib', 'swift')
603+
const swiftPath = path.join(toolPath, 'usr', 'bin')
604+
const swiftDev = path.join(installation, 'Swift-development', 'bin')
605+
const icu67 = path.join(installation, 'icu-67', 'usr', 'bin')
606+
await installer['add'](installation, 'x86_64')
607+
expect(vsSetupSpy).not.toHaveBeenCalled()
608+
expect(process.env.PATH?.includes(swiftPath)).toBeTruthy()
609+
expect(process.env.PATH?.includes(swiftDev)).toBeTruthy()
610+
expect(process.env.PATH?.includes(icu67)).toBeTruthy()
611+
expect(process.env.SDKROOT).toBe(sdkroot)
612+
expect(process.env.SWIFTFLAGS).toContain(`-sdk ${sdkroot}`)
613+
expect(process.env.SWIFTFLAGS).toContain(`-I ${swiftLibs}`)
614+
expect(process.env.SWIFTFLAGS).toContain(
615+
`-L ${path.join(swiftLibs, 'windows')}`
616+
)
617+
})
618+
619+
it('tests add to PATH without SDK copying Swift 5.9.1 with Visual Studio linker', async () => {
620+
const toolchain = {
621+
name: 'Windows 10',
622+
date: new Date('2023-10-19'),
623+
download: 'swift-5.9-RELEASE-windows10.exe',
624+
download_signature: 'swift-5.9.1-RELEASE-windows10.exe.sig',
625+
dir: 'swift-5.9.1-RELEASE',
626+
platform: 'windows10',
627+
branch: 'swift-5.9.1-release',
628+
windows: true,
629+
preventCaching: false
630+
}
631+
const installer = new WindowsToolchainInstaller(toolchain)
632+
const installation = path.resolve('tool', 'installed', 'path')
633+
const vsSetupSpy = jest
634+
.spyOn(VisualStudio, 'setup')
635+
.mockResolvedValue(visualStudio)
636+
jest
637+
.spyOn(fs, 'access')
638+
.mockRejectedValueOnce(new Error())
639+
.mockImplementation(async p => {
640+
if (
641+
typeof p === 'string' &&
642+
(p.endsWith('ucrt.modulemap') || p.endsWith('winsdk.modulemap'))
643+
) {
644+
return Promise.reject(new Error())
645+
}
646+
return Promise.resolve()
647+
})
648+
jest.spyOn(fs, 'copyFile').mockResolvedValue()
649+
jest.spyOn(exec, 'exec').mockResolvedValue(0)
650+
jest
651+
.spyOn(exec, 'getExecOutput')
652+
.mockResolvedValueOnce({
653+
exitCode: 0,
654+
stdout: 'Apple Swift version 5.9.1',
655+
stderr: ''
656+
})
657+
.mockResolvedValue({
658+
exitCode: 0,
659+
stdout: vsEnvs.join(os.EOL),
660+
stderr: ''
661+
})
662+
jest.spyOn(core, 'getBooleanInput').mockReturnValue(true)
449663
const toolPath = path.join(
450664
installation,
451665
'Developer',
@@ -466,6 +680,7 @@ describe('windows toolchain installation verification', () => {
466680
const swiftDev = path.join(installation, 'Swift-development', 'bin')
467681
const icu67 = path.join(installation, 'icu-67', 'usr', 'bin')
468682
await installer['add'](installation, 'x86_64')
683+
expect(vsSetupSpy).toHaveBeenCalled()
469684
expect(process.env.PATH?.includes(swiftPath)).toBeTruthy()
470685
expect(process.env.PATH?.includes(swiftDev)).toBeTruthy()
471686
expect(process.env.PATH?.includes(icu67)).toBeTruthy()
@@ -517,11 +732,18 @@ describe('windows toolchain installation verification', () => {
517732
jest.spyOn(toolCache, 'cacheDir').mockResolvedValue(cached)
518733
jest.spyOn(cache, 'saveCache').mockResolvedValue(1)
519734
jest.spyOn(exec, 'exec').mockResolvedValue(0)
520-
jest.spyOn(exec, 'getExecOutput').mockResolvedValue({
521-
exitCode: 0,
522-
stdout: vsEnvs.join(os.EOL),
523-
stderr: ''
524-
})
735+
jest
736+
.spyOn(exec, 'getExecOutput')
737+
.mockResolvedValueOnce({
738+
exitCode: 0,
739+
stdout: 'Apple Swift version 5.8',
740+
stderr: ''
741+
})
742+
.mockResolvedValue({
743+
exitCode: 0,
744+
stdout: vsEnvs.join(os.EOL),
745+
stderr: ''
746+
})
525747
await installer.install('x86_64')
526748
expect(setupSpy).toHaveBeenCalled()
527749
expect(process.env.PATH?.includes(swiftPath)).toBeTruthy()

action.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ inputs:
5454
i.e. Enable this option for installing static SDK: https://www.swift.org/documentation/articles/static-linux-getting-started.html
5555
required: false
5656
default: 'false'
57+
prefer-visual-studio-linker:
58+
description: >-
59+
Whether to prefer using the Visual Studio linker over the default linker. This is unsafe and not recommended to set.
60+
required: false
61+
default: 'false'
5762
sdks:
5863
description: >-
5964
Semi-colon separated list of Swift SDKs to install along with the main toolchain.

0 commit comments

Comments
 (0)