Skip to content

Commit 480816a

Browse files
authored
refactor: updated Visual Studio setup process for required components installation
1 parent 4463355 commit 480816a

File tree

5 files changed

+178
-35
lines changed

5 files changed

+178
-35
lines changed

__tests__/utils/visual_studio/setup.test.ts

+60-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import * as path from 'path'
22
import {promises as fs} from 'fs'
33
import os from 'os'
4+
import crypto from 'crypto'
45
import * as exec from '@actions/exec'
5-
import {VisualStudio} from '../../../src/utils/visual_studio'
6+
import {
7+
VisualStudio,
8+
VisualStudioConfig
9+
} from '../../../src/utils/visual_studio'
610

711
describe('visual studio setup validation', () => {
812
const env = process.env
@@ -21,11 +25,13 @@ describe('visual studio setup validation', () => {
2125

2226
beforeEach(() => {
2327
process.env = {...env}
28+
VisualStudio.shared = undefined
2429
})
2530

2631
afterEach(() => {
2732
jest.restoreAllMocks()
2833
process.env = env
34+
VisualStudio.shared = undefined
2935
})
3036

3137
it('tests visual studio setup fails when invalid path', async () => {
@@ -58,6 +64,59 @@ describe('visual studio setup validation', () => {
5864
await expect(
5965
VisualStudio.setup({version: '16', components: visualStudio.components})
6066
).resolves.toMatchObject(visualStudio)
67+
expect(VisualStudio.shared).toStrictEqual(visualStudio)
68+
})
69+
70+
it('tests visual studio duplicate setup', async () => {
71+
VisualStudio.shared = visualStudio
72+
const fsAccessSpy = jest.spyOn(fs, 'access')
73+
const execSpy = jest.spyOn(exec, 'exec')
74+
const getExecOutputSpy = jest.spyOn(exec, 'getExecOutput')
75+
await expect(
76+
VisualStudio.setup({version: '16', components: visualStudio.components})
77+
).resolves.toMatchObject(visualStudio)
78+
expect(VisualStudio.shared).toBe(visualStudio)
79+
for (const spy of [fsAccessSpy, execSpy, getExecOutputSpy]) {
80+
expect(spy).not.toHaveBeenCalled()
81+
}
82+
})
83+
84+
it('tests visual studio setup successfully skipping components installation', async () => {
85+
process.env.VSWHERE_PATH = path.join('C:', 'Visual Studio')
86+
const ucrtVersion = '1'
87+
const ucrtSdkDir = path.join('C:', 'UniversalCRTSdkDir')
88+
const vcToolsInstallDir = path.join('C:', 'VCToolsInstallDir')
89+
const tmpDir = process.env.RUNNER_TEMP || os.tmpdir()
90+
const configId = '792a1d5c-ef88-45da-858c-baf3e6e0d048'
91+
const configFileName = `swift-setup-installation-${configId}.vsconfig`
92+
const vsConfig: VisualStudioConfig = {
93+
version: visualStudio.installationVersion,
94+
components: visualStudio.components
95+
}
96+
97+
jest.spyOn(fs, 'access').mockResolvedValue()
98+
jest.spyOn(exec, 'exec').mockResolvedValue(0)
99+
jest.spyOn(crypto, 'randomUUID').mockReturnValue(configId)
100+
jest
101+
.spyOn(exec, 'getExecOutput')
102+
.mockResolvedValueOnce({
103+
exitCode: 0,
104+
stdout: JSON.stringify([visualStudio]),
105+
stderr: ''
106+
})
107+
.mockResolvedValueOnce({
108+
exitCode: 0,
109+
stdout: `UniversalCRTSdkDir=${ucrtSdkDir}\nUCRTVersion=${ucrtVersion}\nVCToolsInstallDir=${vcToolsInstallDir}`,
110+
stderr: ''
111+
})
112+
const readFileSpy = jest.spyOn(fs, 'readFile')
113+
readFileSpy.mockResolvedValue(JSON.stringify(vsConfig))
114+
115+
await expect(
116+
VisualStudio.setup({version: '16', components: visualStudio.components})
117+
).resolves.toMatchObject(visualStudio)
118+
expect(readFileSpy.mock.calls[0][0]).toBe(path.join(tmpDir, configFileName))
119+
expect(VisualStudio.shared).toStrictEqual(visualStudio)
61120
})
62121

63122
it('tests visual studio environment setup', async () => {

dist/index.js

+34-16
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/installer/windows/installation/index.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {env, fallback} from './fallback'
77
declare module './base' {
88
// eslint-disable-next-line no-shadow, @typescript-eslint/no-namespace
99
export namespace Installation {
10+
let lastInstallation: Installation | CustomInstallation
1011
export function get(
1112
install?: string
1213
): Promise<Installation | CustomInstallation | undefined>
@@ -20,9 +21,9 @@ declare module './base' {
2021
}
2122
}
2223

23-
Installation.get = async (install?: string) => {
24+
Installation.get = async function (install?: string) {
2425
if (!(install?.length ?? 1)) {
25-
return lastInstallation
26+
return this.lastInstallation
2627
}
2728

2829
const approaches = [
@@ -47,14 +48,13 @@ Installation.get = async (install?: string) => {
4748
return undefined
4849
}
4950

50-
let lastInstallation: Installation | CustomInstallation
51-
Installation.install = async (exe: string) => {
51+
Installation.install = async function (exe: string) {
5252
core.debug(`Installing toolchain from "${exe}"`)
5353
const oldEnv = await env()
5454
await exec(`"${exe}"`, ['-q'])
5555
const newEnv = await env()
56-
lastInstallation = await Installation.detect(oldEnv, newEnv)
57-
return lastInstallation
56+
this.lastInstallation = await Installation.detect(oldEnv, newEnv)
57+
return this.lastInstallation
5858
}
5959

6060
Installation.detect = async (

src/utils/visual_studio/base.ts

+46
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,49 @@ export interface VisualStudioEnv {
9292
readonly VCToolsInstallDir?: string
9393
readonly [name: string]: string | undefined
9494
}
95+
96+
/**
97+
* Represents the structure of a Visual Studio .vsconfig file
98+
*/
99+
export interface VisualStudioConfig {
100+
/**
101+
* The version of the .vsconfig file format
102+
*/
103+
version: string
104+
105+
/**
106+
* List of workloads and components to be installed
107+
*/
108+
components: string[]
109+
110+
/**
111+
* Optional installation channel URI
112+
* Example: https://aka.ms/vs/17/release/channel
113+
*/
114+
installChannelUri?: string
115+
116+
/**
117+
* Runtime components to be installed (e.g., .NET Core, ASP.NET Core runtimes)
118+
*/
119+
runtimeComponents?: string[]
120+
121+
/**
122+
* Installation-related properties
123+
*/
124+
properties?: {
125+
/**
126+
* A custom name for the configuration
127+
*/
128+
nickname?: string
129+
130+
/**
131+
* Channel ID to use for installation, e.g., VisualStudio.17.Release
132+
*/
133+
channelId?: string
134+
135+
/**
136+
* Installation path for Visual Studio
137+
*/
138+
installPath?: string
139+
}
140+
}

src/utils/visual_studio/setup.ts

+32-12
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
1+
import * as os from 'os'
2+
import * as path from 'path'
3+
import {promises as fs} from 'fs'
4+
import {randomUUID} from 'crypto'
15
import * as core from '@actions/core'
26
import {exec, getExecOutput} from '@actions/exec'
3-
import {VisualStudio, VisualStudioRequirement} from './base'
7+
import {VisualStudio, VisualStudioRequirement, VisualStudioConfig} from './base'
48
import {VSWhere} from './vswhere'
59

610
declare module './base' {
711
// eslint-disable-next-line no-shadow, @typescript-eslint/no-namespace
812
export namespace VisualStudio {
13+
let shared: VisualStudio | undefined
914
function setup(requirement: VisualStudioRequirement): Promise<VisualStudio>
1015
}
1116
}
1217

13-
let shared: VisualStudio
14-
1518
/// set up required visual studio tools for swift on windows
1619
VisualStudio.setup = async function (requirement: VisualStudioRequirement) {
17-
if (shared) {
18-
return shared
20+
if (this.shared) {
21+
return this.shared
1922
}
2023
/// https://github.com/microsoft/vswhere/wiki/Find-MSBuild
2124
/// get visual studio properties
@@ -46,16 +49,33 @@ VisualStudio.setup = async function (requirement: VisualStudioRequirement) {
4649
}
4750

4851
const vsEnv = await vs.env()
49-
const comps = requirement.components
52+
let comps = requirement.components
5053
if (
5154
vsEnv.UCRTVersion &&
5255
vsEnv.UniversalCRTSdkDir &&
53-
vsEnv.VCToolsInstallDir &&
54-
comps.length < 3
56+
vsEnv.VCToolsInstallDir
5557
) {
56-
core.debug('VS components already setup, skipping installation')
57-
shared = vs
58-
return vs
58+
const tmpDir = process.env.RUNNER_TEMP || os.tmpdir()
59+
const configFileName = `swift-setup-installation-${randomUUID()}.vsconfig`
60+
const configPath = path.join(tmpDir, configFileName)
61+
core.debug(`Exporting VS installation config to "${configPath}"`)
62+
await exec(`"${vs.properties.setupEngineFilePath}"`, [
63+
'export',
64+
'--path',
65+
tmpDir,
66+
'--config',
67+
configFileName
68+
])
69+
70+
const configContent = await fs.readFile(configPath, 'utf-8')
71+
const vsConfig: VisualStudioConfig = JSON.parse(configContent)
72+
const installedComponents = new Set(vsConfig.components)
73+
comps = comps.filter(comp => !installedComponents.has(comp))
74+
if (comps.length == 0) {
75+
core.debug('VS components already setup, skipping installation')
76+
this.shared = vs
77+
return vs
78+
}
5979
}
6080

6181
/// https://docs.microsoft.com/en-us/visualstudio/install/use-command-line-parameters-to-install-visual-studio?view=vs-2022
@@ -70,6 +90,6 @@ VisualStudio.setup = async function (requirement: VisualStudioRequirement) {
7090
'--force',
7191
'--quiet'
7292
])
73-
shared = vs
93+
this.shared = vs
7494
return vs
7595
}

0 commit comments

Comments
 (0)