Skip to content

Commit 05b9999

Browse files
authored
Implement test-only Visual Studio installation discovery infrastructure (#18906)
1 parent 8ac5131 commit 05b9999

File tree

5 files changed

+163
-23
lines changed

5 files changed

+163
-23
lines changed

tests/FSharp.Test.Utilities/FSharp.Test.Utilities.fsproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
<Compile Include="TestFramework.fs" />
3333
<Compile Include="ILChecker.fs" />
3434
<Compile Include="Utilities.fs" />
35+
<Compile Include="VSInstallDiscovery.fs" />
3536
<Compile Include="CompilerAssert.fs" />
3637
<Compile Include="ProjectGeneration.fs" />
3738
<Compile Include="Assert.fs" />
@@ -93,6 +94,7 @@
9394
<InternalsVisibleTo Include="FSharp.Tests.FSharpSuite" />
9495
<InternalsVisibleTo Include="LanguageServiceProfiling" />
9596
<InternalsVisibleTo Include="FSharp.Compiler.Benchmarks" />
97+
<InternalsVisibleTo Include="FSharp.Editor.Tests" />
9698
</ItemGroup>
9799

98100
<ItemGroup Condition="'$(TargetFramework)' != '$(FSharpNetCoreProductTargetFramework)'">
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
2+
3+
namespace FSharp.Test
4+
5+
/// Test-only Visual Studio installation discovery infrastructure.
6+
/// Provides a centralized, robust, and graceful discovery mechanism for Visual Studio installations
7+
/// used by integration/editor/unit tests under vsintegration/tests.
8+
module VSInstallDiscovery =
9+
10+
open System
11+
open System.IO
12+
open System.Diagnostics
13+
14+
/// Result of VS installation discovery
15+
type VSInstallResult =
16+
| Found of installPath: string * source: string
17+
| NotFound of reason: string
18+
19+
/// Attempts to find a Visual Studio installation using multiple fallback strategies
20+
let tryFindVSInstallation () : VSInstallResult =
21+
22+
/// Check if a path exists and looks like a valid VS installation
23+
let validateVSPath path =
24+
if String.IsNullOrEmpty(path) then false
25+
else
26+
try
27+
let fullPath = Path.GetFullPath(path)
28+
Directory.Exists(fullPath) &&
29+
Directory.Exists(Path.Combine(fullPath, "IDE")) &&
30+
(File.Exists(Path.Combine(fullPath, "IDE", "devenv.exe")) ||
31+
File.Exists(Path.Combine(fullPath, "IDE", "VSIXInstaller.exe")))
32+
with
33+
| _ -> false
34+
35+
/// Strategy 1: VSAPPIDDIR (derive parent of Common7/IDE)
36+
let tryVSAppIdDir () =
37+
let envVar = Environment.GetEnvironmentVariable("VSAPPIDDIR")
38+
if not (String.IsNullOrEmpty(envVar)) then
39+
try
40+
let parentPath = Path.Combine(envVar, "..")
41+
if validateVSPath parentPath then
42+
Some (Found (Path.GetFullPath(parentPath), "VSAPPIDDIR environment variable"))
43+
else None
44+
with
45+
| _ -> None
46+
else None
47+
48+
/// Strategy 2: Highest version among VS*COMNTOOLS environment variables
49+
let tryVSCommonTools () =
50+
let vsVersions = [
51+
("VS180COMNTOOLS", 18) // Visual Studio 2026
52+
("VS170COMNTOOLS", 17) // Visual Studio 2022
53+
("VS160COMNTOOLS", 16) // Visual Studio 2019
54+
("VS150COMNTOOLS", 15) // Visual Studio 2017
55+
("VS140COMNTOOLS", 14) // Visual Studio 2015
56+
("VS120COMNTOOLS", 12) // Visual Studio 2013
57+
]
58+
59+
vsVersions
60+
|> List.tryPick (fun (envName, version) ->
61+
let envVar = Environment.GetEnvironmentVariable(envName)
62+
if not (String.IsNullOrEmpty(envVar)) then
63+
try
64+
let parentPath = Path.Combine(envVar, "..")
65+
if validateVSPath parentPath then
66+
Some (Found (Path.GetFullPath(parentPath), $"{envName} environment variable (VS version {version})"))
67+
else None
68+
with
69+
| _ -> None
70+
else None)
71+
72+
/// Strategy 3: vswhere.exe (Visual Studio Installer)
73+
let tryVSWhere () =
74+
try
75+
let programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86)
76+
let vswherePath = Path.Combine(programFiles, "Microsoft Visual Studio", "Installer", "vswhere.exe")
77+
78+
if File.Exists(vswherePath) then
79+
let startInfo = ProcessStartInfo(
80+
FileName = vswherePath,
81+
Arguments = "-latest -products * -requires Microsoft.Component.MSBuild -property installationPath",
82+
UseShellExecute = false,
83+
RedirectStandardOutput = true,
84+
RedirectStandardError = true,
85+
CreateNoWindow = true
86+
)
87+
88+
use proc = Process.Start(startInfo)
89+
proc.WaitForExit(5000) |> ignore // 5 second timeout
90+
91+
if proc.ExitCode = 0 then
92+
let output = proc.StandardOutput.ReadToEnd().Trim()
93+
if validateVSPath output then
94+
Some (Found (Path.GetFullPath(output), "vswhere.exe discovery"))
95+
else None
96+
else None
97+
else None
98+
with
99+
| _ -> None
100+
101+
// Try each strategy in order of precedence
102+
match tryVSAppIdDir () with
103+
| Some result -> result
104+
| None ->
105+
match tryVSCommonTools () with
106+
| Some result -> result
107+
| None ->
108+
match tryVSWhere () with
109+
| Some result -> result
110+
| None -> NotFound "No Visual Studio installation found using any discovery method"
111+
112+
/// Gets the VS installation directory, with graceful fallback behavior.
113+
/// Returns None if no VS installation can be found, allowing callers to handle gracefully.
114+
let tryGetVSInstallDir () : string option =
115+
match tryFindVSInstallation () with
116+
| Found (path, _) -> Some path
117+
| NotFound _ -> None
118+
119+
/// Gets the VS installation directory with detailed logging.
120+
/// Useful for debugging installation discovery issues in tests.
121+
let getVSInstallDirWithLogging (logAction: string -> unit) : string option =
122+
match tryFindVSInstallation () with
123+
| Found (path, source) ->
124+
logAction $"Visual Studio installation found at: {path} (via {source})"
125+
Some path
126+
| NotFound reason ->
127+
logAction $"Visual Studio installation not found: {reason}"
128+
None

vsintegration/tests/FSharp.Editor.Tests/Helpers/AssemblyResolver.fs

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,26 @@ open System.Reflection
88

99
module AssemblyResolver =
1010
open System.Globalization
11+
open FSharp.Test.VSInstallDiscovery
1112

1213
let vsInstallDir =
13-
// use the environment variable to find the VS installdir
14-
let vsvar =
15-
let var = Environment.GetEnvironmentVariable("VS170COMNTOOLS")
16-
17-
if String.IsNullOrEmpty var then
18-
Environment.GetEnvironmentVariable("VSAPPIDDIR")
19-
else
20-
var
21-
22-
if String.IsNullOrEmpty vsvar then
23-
failwith "VS170COMNTOOLS and VSAPPIDDIR environment variables not found."
24-
25-
Path.Combine(vsvar, "..")
14+
// Use centralized VS installation discovery with graceful fallback
15+
match tryGetVSInstallDir () with
16+
| Some dir -> dir
17+
| None ->
18+
// Fallback to legacy behavior for backward compatibility
19+
let vsvar =
20+
let var = Environment.GetEnvironmentVariable("VS170COMNTOOLS")
21+
22+
if String.IsNullOrEmpty var then
23+
Environment.GetEnvironmentVariable("VSAPPIDDIR")
24+
else
25+
var
26+
27+
if String.IsNullOrEmpty vsvar then
28+
failwith "VS170COMNTOOLS and VSAPPIDDIR environment variables not found."
29+
30+
Path.Combine(vsvar, "..")
2631

2732
let probingPaths =
2833
[|

vsintegration/tests/Salsa/VisualFSharp.Salsa.fsproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
</ItemGroup>
1717

1818
<ItemGroup>
19-
<Compile Include="$(FSharpSourcesRoot)\Compiler\Utilities\NullnessShims.fs" />
19+
<Compile Include="$(FSharpSourcesRoot)\Compiler\Utilities\NullnessShims.fs"/>
2020
<EmbeddedText Include="$(FSharpSourcesRoot)\Compiler\Facilities\UtilsStrings.txt" />
2121
<Compile Include="$(FSharpSourcesRoot)\Compiler\Facilities\CompilerLocation.fs">
2222
<Link>CompilerLocation.fs</Link>

vsintegration/tests/UnitTests/AssemblyResolver.fs

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,22 @@ open System.Reflection
66

77
module AssemblyResolver =
88
open System.Globalization
9+
open FSharp.Test.VSInstallDiscovery
910

1011
let vsInstallDir =
11-
// use the environment variable to find the VS installdir
12-
let vsvar =
13-
let var = Environment.GetEnvironmentVariable("VS170COMNTOOLS")
14-
if String.IsNullOrEmpty var then
15-
Environment.GetEnvironmentVariable("VSAPPIDDIR")
16-
else
17-
var
18-
if String.IsNullOrEmpty vsvar then failwith "VS170COMNTOOLS and VSAPPIDDIR environment variables not found."
19-
Path.Combine(vsvar, "..")
12+
// Use centralized VS installation discovery with graceful fallback
13+
match tryGetVSInstallDir () with
14+
| Some dir -> dir
15+
| None ->
16+
// Fallback to legacy behavior for backward compatibility
17+
let vsvar =
18+
let var = Environment.GetEnvironmentVariable("VS170COMNTOOLS")
19+
if String.IsNullOrEmpty var then
20+
Environment.GetEnvironmentVariable("VSAPPIDDIR")
21+
else
22+
var
23+
if String.IsNullOrEmpty vsvar then failwith "VS170COMNTOOLS and VSAPPIDDIR environment variables not found."
24+
Path.Combine(vsvar, "..")
2025

2126
let probingPaths = [|
2227
Path.Combine(vsInstallDir, @"IDE\CommonExtensions\Microsoft\Editor")

0 commit comments

Comments
 (0)