From 5b3ef17916cd97b23f840a21beec9516658fd756 Mon Sep 17 00:00:00 2001
From: Adam Boniecki <20281641+abonie@users.noreply.github.com>
Date: Thu, 20 Nov 2025 18:20:16 +0100
Subject: [PATCH 1/4] Update the agent pool for the PR builds (#19102)
---
azure-pipelines-PR.yml | 54 +++++++++++++-----------------------------
1 file changed, 16 insertions(+), 38 deletions(-)
diff --git a/azure-pipelines-PR.yml b/azure-pipelines-PR.yml
index cfa5411ee33..eee3e74008b 100644
--- a/azure-pipelines-PR.yml
+++ b/azure-pipelines-PR.yml
@@ -55,6 +55,8 @@ variables:
value: Release
- name: _PublishUsingPipelines
value: true
+ - name: _WindowsMachineQueueName
+ value: windows.vs2026preview.scout.amd64.open
- name: VisualStudioDropName
value: Products/$(System.TeamProject)/$(Build.Repository.Name)/$(Build.SourceBranchName)/$(Build.BuildNumber)
- name: Codeql.Enabled
@@ -101,7 +103,7 @@ stages:
value: Test
pool:
name: $(DncEngPublicBuildPool)
- demands: ImageOverride -equals $(WindowsMachineQueueName)
+ demands: ImageOverride -equals $(_WindowsMachineQueueName)
timeoutInMinutes: 90
strategy:
maxParallel: 2
@@ -216,12 +218,8 @@ stages:
- job: WindowsLangVersionPreview
pool:
- # The PR build definition sets this variable:
- # WindowsMachineQueueName=Windows.vs2022.amd64.open
- # and there is an alternate build definition that sets this to a queue that is always scouting the
- # next preview of Visual Studio.
name: $(DncEngPublicBuildPool)
- demands: ImageOverride -equals $(WindowsMachineQueueName)
+ demands: ImageOverride -equals $(_WindowsMachineQueueName)
timeoutInMinutes: 120
steps:
- checkout: self
@@ -256,12 +254,8 @@ stages:
- job: WindowsNoRealsig_testCoreclr
pool:
- # The PR build definition sets this variable:
- # WindowsMachineQueueName=Windows.vs2022.amd64.open
- # and there is an alternate build definition that sets this to a queue that is always scouting the
- # next preview of Visual Studio.
name: $(DncEngPublicBuildPool)
- demands: ImageOverride -equals $(WindowsMachineQueueName)
+ demands: ImageOverride -equals $(_WindowsMachineQueueName)
timeoutInMinutes: 120
steps:
- checkout: self
@@ -306,12 +300,8 @@ stages:
- job: WindowsNoRealsig_testDesktop
pool:
- # The PR build definition sets this variable:
- # WindowsMachineQueueName=Windows.vs2022.amd64.open
- # and there is an alternate build definition that sets this to a queue that is always scouting the
- # next preview of Visual Studio.
name: $(DncEngPublicBuildPool)
- demands: ImageOverride -equals $(WindowsMachineQueueName)
+ demands: ImageOverride -equals $(_WindowsMachineQueueName)
timeoutInMinutes: 120
strategy:
parallel: 4
@@ -359,12 +349,8 @@ stages:
- job: WindowsStrictIndentation
pool:
- # The PR build definition sets this variable:
- # WindowsMachineQueueName=Windows.vs2022.amd64.open
- # and there is an alternate build definition that sets this to a queue that is always scouting the
- # next preview of Visual Studio.
name: $(DncEngPublicBuildPool)
- demands: ImageOverride -equals $(WindowsMachineQueueName)
+ demands: ImageOverride -equals $(_WindowsMachineQueueName)
timeoutInMinutes: 120
steps:
- checkout: self
@@ -400,7 +386,7 @@ stages:
- job: WindowsNoStrictIndentation
pool:
name: $(DncEngPublicBuildPool)
- demands: ImageOverride -equals $(WindowsMachineQueueName)
+ demands: ImageOverride -equals $(_WindowsMachineQueueName)
timeoutInMinutes: 120
steps:
- checkout: self
@@ -441,12 +427,8 @@ stages:
- name: __VSNeverShowWhatsNew
value: 1
pool:
- # The PR build definition sets this variable:
- # WindowsMachineQueueName=Windows.vs2022.amd64.open
- # and there is an alternate build definition that sets this to a queue that is always scouting the
- # next preview of Visual Studio.
name: $(DncEngPublicBuildPool)
- demands: ImageOverride -equals $(WindowsMachineQueueName)
+ demands: ImageOverride -equals $(_WindowsMachineQueueName)
timeoutInMinutes: 120
strategy:
matrix:
@@ -546,12 +528,8 @@ stages:
- name: __VSNeverShowWhatsNew
value: 1
pool:
- # The PR build definition sets this variable:
- # WindowsMachineQueueName=Windows.vs2022.amd64.open
- # and there is an alternate build definition that sets this to a queue that is always scouting the
- # next preview of Visual Studio.
name: $(DncEngPublicBuildPool)
- demands: ImageOverride -equals $(WindowsMachineQueueName)
+ demands: ImageOverride -equals $(_WindowsMachineQueueName)
timeoutInMinutes: 120
strategy:
parallel: 4
@@ -622,7 +600,7 @@ stages:
- job: MockOfficial
pool:
name: $(DncEngPublicBuildPool)
- demands: ImageOverride -equals $(WindowsMachineQueueName)
+ demands: ImageOverride -equals $(_WindowsMachineQueueName)
steps:
- checkout: self
clean: true
@@ -721,7 +699,7 @@ stages:
- job: EndToEndBuildTests
pool:
name: $(DncEngPublicBuildPool)
- demands: ImageOverride -equals $(WindowsMachineQueueName)
+ demands: ImageOverride -equals $(_WindowsMachineQueueName)
strategy:
maxParallel: 2
matrix:
@@ -761,7 +739,7 @@ stages:
- job: Plain_Build_Windows
pool:
name: $(DncEngPublicBuildPool)
- demands: ImageOverride -equals $(WindowsMachineQueueName)
+ demands: ImageOverride -equals $(_WindowsMachineQueueName)
variables:
- name: _BuildConfig
value: Debug
@@ -816,7 +794,7 @@ stages:
- job: Benchmarks
pool:
name: $(DncEngPublicBuildPool)
- demands: ImageOverride -equals $(WindowsMachineQueueName)
+ demands: ImageOverride -equals $(_WindowsMachineQueueName)
variables:
- name: _BuildConfig
value: Release
@@ -832,7 +810,7 @@ stages:
- job: Build_And_Test_AOT_Windows
pool:
name: $(DncEngPublicBuildPool)
- demands: ImageOverride -equals $(WindowsMachineQueueName)
+ demands: ImageOverride -equals $(_WindowsMachineQueueName)
strategy:
maxParallel: 2
matrix:
@@ -873,7 +851,7 @@ stages:
- job: ILVerify
pool:
name: $(DncEngPublicBuildPool)
- demands: ImageOverride -equals $(WindowsMachineQueueName)
+ demands: ImageOverride -equals $(_WindowsMachineQueueName)
steps:
- checkout: self
clean: true
From a60664c7f004157818707b6b2f7af135c8b491b6 Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Thu, 6 Nov 2025 15:48:26 +0000
Subject: [PATCH 2/4] Implement test-only Visual Studio installation discovery
infrastructure (#18906)
---
.../FSharp.Test.Utilities.fsproj | 2 +
.../VSInstallDiscovery.fs | 128 ++++++++++++++++++
.../Helpers/AssemblyResolver.fs | 31 +++--
.../tests/Salsa/VisualFSharp.Salsa.fsproj | 2 +-
.../tests/UnitTests/AssemblyResolver.fs | 23 ++--
5 files changed, 163 insertions(+), 23 deletions(-)
create mode 100644 tests/FSharp.Test.Utilities/VSInstallDiscovery.fs
diff --git a/tests/FSharp.Test.Utilities/FSharp.Test.Utilities.fsproj b/tests/FSharp.Test.Utilities/FSharp.Test.Utilities.fsproj
index c1aebde1ed3..7bc4f6e058f 100644
--- a/tests/FSharp.Test.Utilities/FSharp.Test.Utilities.fsproj
+++ b/tests/FSharp.Test.Utilities/FSharp.Test.Utilities.fsproj
@@ -32,6 +32,7 @@
+
@@ -93,6 +94,7 @@
+
diff --git a/tests/FSharp.Test.Utilities/VSInstallDiscovery.fs b/tests/FSharp.Test.Utilities/VSInstallDiscovery.fs
new file mode 100644
index 00000000000..e491ea329bd
--- /dev/null
+++ b/tests/FSharp.Test.Utilities/VSInstallDiscovery.fs
@@ -0,0 +1,128 @@
+// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
+
+namespace FSharp.Test
+
+/// Test-only Visual Studio installation discovery infrastructure.
+/// Provides a centralized, robust, and graceful discovery mechanism for Visual Studio installations
+/// used by integration/editor/unit tests under vsintegration/tests.
+module VSInstallDiscovery =
+
+ open System
+ open System.IO
+ open System.Diagnostics
+
+ /// Result of VS installation discovery
+ type VSInstallResult =
+ | Found of installPath: string * source: string
+ | NotFound of reason: string
+
+ /// Attempts to find a Visual Studio installation using multiple fallback strategies
+ let tryFindVSInstallation () : VSInstallResult =
+
+ /// Check if a path exists and looks like a valid VS installation
+ let validateVSPath path =
+ if String.IsNullOrEmpty(path) then false
+ else
+ try
+ let fullPath = Path.GetFullPath(path)
+ Directory.Exists(fullPath) &&
+ Directory.Exists(Path.Combine(fullPath, "IDE")) &&
+ (File.Exists(Path.Combine(fullPath, "IDE", "devenv.exe")) ||
+ File.Exists(Path.Combine(fullPath, "IDE", "VSIXInstaller.exe")))
+ with
+ | _ -> false
+
+ /// Strategy 1: VSAPPIDDIR (derive parent of Common7/IDE)
+ let tryVSAppIdDir () =
+ let envVar = Environment.GetEnvironmentVariable("VSAPPIDDIR")
+ if not (String.IsNullOrEmpty(envVar)) then
+ try
+ let parentPath = Path.Combine(envVar, "..")
+ if validateVSPath parentPath then
+ Some (Found (Path.GetFullPath(parentPath), "VSAPPIDDIR environment variable"))
+ else None
+ with
+ | _ -> None
+ else None
+
+ /// Strategy 2: Highest version among VS*COMNTOOLS environment variables
+ let tryVSCommonTools () =
+ let vsVersions = [
+ ("VS180COMNTOOLS", 18) // Visual Studio 2026
+ ("VS170COMNTOOLS", 17) // Visual Studio 2022
+ ("VS160COMNTOOLS", 16) // Visual Studio 2019
+ ("VS150COMNTOOLS", 15) // Visual Studio 2017
+ ("VS140COMNTOOLS", 14) // Visual Studio 2015
+ ("VS120COMNTOOLS", 12) // Visual Studio 2013
+ ]
+
+ vsVersions
+ |> List.tryPick (fun (envName, version) ->
+ let envVar = Environment.GetEnvironmentVariable(envName)
+ if not (String.IsNullOrEmpty(envVar)) then
+ try
+ let parentPath = Path.Combine(envVar, "..")
+ if validateVSPath parentPath then
+ Some (Found (Path.GetFullPath(parentPath), $"{envName} environment variable (VS version {version})"))
+ else None
+ with
+ | _ -> None
+ else None)
+
+ /// Strategy 3: vswhere.exe (Visual Studio Installer)
+ let tryVSWhere () =
+ try
+ let programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86)
+ let vswherePath = Path.Combine(programFiles, "Microsoft Visual Studio", "Installer", "vswhere.exe")
+
+ if File.Exists(vswherePath) then
+ let startInfo = ProcessStartInfo(
+ FileName = vswherePath,
+ Arguments = "-latest -products * -requires Microsoft.Component.MSBuild -property installationPath",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true
+ )
+
+ use proc = Process.Start(startInfo)
+ proc.WaitForExit(5000) |> ignore // 5 second timeout
+
+ if proc.ExitCode = 0 then
+ let output = proc.StandardOutput.ReadToEnd().Trim()
+ if validateVSPath output then
+ Some (Found (Path.GetFullPath(output), "vswhere.exe discovery"))
+ else None
+ else None
+ else None
+ with
+ | _ -> None
+
+ // Try each strategy in order of precedence
+ match tryVSAppIdDir () with
+ | Some result -> result
+ | None ->
+ match tryVSCommonTools () with
+ | Some result -> result
+ | None ->
+ match tryVSWhere () with
+ | Some result -> result
+ | None -> NotFound "No Visual Studio installation found using any discovery method"
+
+ /// Gets the VS installation directory, with graceful fallback behavior.
+ /// Returns None if no VS installation can be found, allowing callers to handle gracefully.
+ let tryGetVSInstallDir () : string option =
+ match tryFindVSInstallation () with
+ | Found (path, _) -> Some path
+ | NotFound _ -> None
+
+ /// Gets the VS installation directory with detailed logging.
+ /// Useful for debugging installation discovery issues in tests.
+ let getVSInstallDirWithLogging (logAction: string -> unit) : string option =
+ match tryFindVSInstallation () with
+ | Found (path, source) ->
+ logAction $"Visual Studio installation found at: {path} (via {source})"
+ Some path
+ | NotFound reason ->
+ logAction $"Visual Studio installation not found: {reason}"
+ None
diff --git a/vsintegration/tests/FSharp.Editor.Tests/Helpers/AssemblyResolver.fs b/vsintegration/tests/FSharp.Editor.Tests/Helpers/AssemblyResolver.fs
index f41f10fa138..78a57793517 100644
--- a/vsintegration/tests/FSharp.Editor.Tests/Helpers/AssemblyResolver.fs
+++ b/vsintegration/tests/FSharp.Editor.Tests/Helpers/AssemblyResolver.fs
@@ -8,21 +8,26 @@ open System.Reflection
module AssemblyResolver =
open System.Globalization
+ open FSharp.Test.VSInstallDiscovery
let vsInstallDir =
- // use the environment variable to find the VS installdir
- let vsvar =
- let var = Environment.GetEnvironmentVariable("VS170COMNTOOLS")
-
- if String.IsNullOrEmpty var then
- Environment.GetEnvironmentVariable("VSAPPIDDIR")
- else
- var
-
- if String.IsNullOrEmpty vsvar then
- failwith "VS170COMNTOOLS and VSAPPIDDIR environment variables not found."
-
- Path.Combine(vsvar, "..")
+ // Use centralized VS installation discovery with graceful fallback
+ match tryGetVSInstallDir () with
+ | Some dir -> dir
+ | None ->
+ // Fallback to legacy behavior for backward compatibility
+ let vsvar =
+ let var = Environment.GetEnvironmentVariable("VS170COMNTOOLS")
+
+ if String.IsNullOrEmpty var then
+ Environment.GetEnvironmentVariable("VSAPPIDDIR")
+ else
+ var
+
+ if String.IsNullOrEmpty vsvar then
+ failwith "VS170COMNTOOLS and VSAPPIDDIR environment variables not found."
+
+ Path.Combine(vsvar, "..")
let probingPaths =
[|
diff --git a/vsintegration/tests/Salsa/VisualFSharp.Salsa.fsproj b/vsintegration/tests/Salsa/VisualFSharp.Salsa.fsproj
index 6396eada6a9..f7410214430 100644
--- a/vsintegration/tests/Salsa/VisualFSharp.Salsa.fsproj
+++ b/vsintegration/tests/Salsa/VisualFSharp.Salsa.fsproj
@@ -16,7 +16,7 @@
-
+
CompilerLocation.fs
diff --git a/vsintegration/tests/UnitTests/AssemblyResolver.fs b/vsintegration/tests/UnitTests/AssemblyResolver.fs
index aab95cc46fc..38b5ee45290 100644
--- a/vsintegration/tests/UnitTests/AssemblyResolver.fs
+++ b/vsintegration/tests/UnitTests/AssemblyResolver.fs
@@ -6,17 +6,22 @@ open System.Reflection
module AssemblyResolver =
open System.Globalization
+ open FSharp.Test.VSInstallDiscovery
let vsInstallDir =
- // use the environment variable to find the VS installdir
- let vsvar =
- let var = Environment.GetEnvironmentVariable("VS170COMNTOOLS")
- if String.IsNullOrEmpty var then
- Environment.GetEnvironmentVariable("VSAPPIDDIR")
- else
- var
- if String.IsNullOrEmpty vsvar then failwith "VS170COMNTOOLS and VSAPPIDDIR environment variables not found."
- Path.Combine(vsvar, "..")
+ // Use centralized VS installation discovery with graceful fallback
+ match tryGetVSInstallDir () with
+ | Some dir -> dir
+ | None ->
+ // Fallback to legacy behavior for backward compatibility
+ let vsvar =
+ let var = Environment.GetEnvironmentVariable("VS170COMNTOOLS")
+ if String.IsNullOrEmpty var then
+ Environment.GetEnvironmentVariable("VSAPPIDDIR")
+ else
+ var
+ if String.IsNullOrEmpty vsvar then failwith "VS170COMNTOOLS and VSAPPIDDIR environment variables not found."
+ Path.Combine(vsvar, "..")
let probingPaths = [|
Path.Combine(vsInstallDir, @"IDE\CommonExtensions\Microsoft\Editor")
From 7861ca6766f7cae6c3b11118321eb0e956c2a9b2 Mon Sep 17 00:00:00 2001
From: Adam Boniecki <20281641+abonie@users.noreply.github.com>
Date: Thu, 13 Nov 2025 18:25:08 +0100
Subject: [PATCH 3/4] Quick fix for vs dir discovery (#19086)
---
vsintegration/tests/Salsa/VsMocks.fs | 16 +++++++++++-----
1 file changed, 11 insertions(+), 5 deletions(-)
diff --git a/vsintegration/tests/Salsa/VsMocks.fs b/vsintegration/tests/Salsa/VsMocks.fs
index 5189b7faec5..1ce67c04763 100644
--- a/vsintegration/tests/Salsa/VsMocks.fs
+++ b/vsintegration/tests/Salsa/VsMocks.fs
@@ -1653,12 +1653,18 @@ module internal VsActual =
let vsInstallDir =
// use the environment variable to find the VS installdir
let vsvar =
- let var = Environment.GetEnvironmentVariable("VS170COMNTOOLS")
- if String.IsNullOrEmpty var then
- Environment.GetEnvironmentVariable("VSAPPIDDIR")
+ // Try VS180COMNTOOLS first, then VS170COMNTOOLS, then VSAPPIDDIR
+ // TODO : use tryGetVSInstallDir from test utils instead
+ let var18 = Environment.GetEnvironmentVariable("VS180COMNTOOLS")
+ if String.IsNullOrEmpty var18 then
+ let var17 = Environment.GetEnvironmentVariable("VS170COMNTOOLS")
+ if String.IsNullOrEmpty var17 then
+ Environment.GetEnvironmentVariable("VSAPPIDDIR")
+ else
+ var17
else
- var
- if String.IsNullOrEmpty vsvar then failwith "VS170COMNTOOLS and VSAPPIDDIR environment variables not found."
+ var18
+ if String.IsNullOrEmpty vsvar then failwith "VS180COMNTOOLS, VS170COMNTOOLS and VSAPPIDDIR environment variables not found."
Path.Combine(vsvar, "..")
let CreateEditorCatalog() =
From 78552e7342c738a7a826d823723f60acfdfca64b Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Mon, 1 Dec 2025 12:38:06 +0000
Subject: [PATCH 4/4] Centralize Visual Studio installation discovery and
assembly resolution in test infrastructure (#19085)
---
.../FSharp.Test.Utilities.fsproj | 3 +-
.../VSInstallDiscovery.fs | 52 ++++++++++++++++
.../Helpers/AssemblyResolver.fs | 61 ++-----------------
.../tests/Salsa/VisualFSharp.Salsa.fsproj | 3 +
vsintegration/tests/Salsa/VsMocks.fs | 18 +-----
.../tests/UnitTests/AssemblyResolver.fs | 52 ++--------------
6 files changed, 67 insertions(+), 122 deletions(-)
diff --git a/tests/FSharp.Test.Utilities/FSharp.Test.Utilities.fsproj b/tests/FSharp.Test.Utilities/FSharp.Test.Utilities.fsproj
index 7bc4f6e058f..d899b551e30 100644
--- a/tests/FSharp.Test.Utilities/FSharp.Test.Utilities.fsproj
+++ b/tests/FSharp.Test.Utilities/FSharp.Test.Utilities.fsproj
@@ -102,7 +102,8 @@
- $(NoWarn);NU1510
+ $(NoWarn);NU1510;44
+
diff --git a/tests/FSharp.Test.Utilities/VSInstallDiscovery.fs b/tests/FSharp.Test.Utilities/VSInstallDiscovery.fs
index e491ea329bd..e59eec8eed2 100644
--- a/tests/FSharp.Test.Utilities/VSInstallDiscovery.fs
+++ b/tests/FSharp.Test.Utilities/VSInstallDiscovery.fs
@@ -126,3 +126,55 @@ module VSInstallDiscovery =
| NotFound reason ->
logAction $"Visual Studio installation not found: {reason}"
None
+
+ /// Gets the VS installation directory or fails with a detailed error message.
+ /// This is the recommended method for test scenarios that require VS to be installed.
+ let getVSInstallDirOrFail () : string =
+ match tryFindVSInstallation () with
+ | Found (path, _) -> path
+ | NotFound reason ->
+ failwith $"Visual Studio installation not found: {reason}. Ensure VS is installed or environment variables (VSAPPIDDIR, VS*COMNTOOLS) are set."
+
+/// Assembly resolver for Visual Studio test infrastructure.
+/// Provides centralized assembly resolution for VS integration tests.
+module VSAssemblyResolver =
+ open System
+ open System.IO
+ open System.Reflection
+ open System.Globalization
+
+ /// Adds an assembly resolver that probes Visual Studio installation directories.
+ /// This should be called early in test initialization to ensure VS assemblies can be loaded.
+ let addResolver () =
+ let vsInstallDir = VSInstallDiscovery.getVSInstallDirOrFail ()
+
+ let probingPaths =
+ [|
+ Path.Combine(vsInstallDir, @"IDE\CommonExtensions\Microsoft\Editor")
+ Path.Combine(vsInstallDir, @"IDE\PublicAssemblies")
+ Path.Combine(vsInstallDir, @"IDE\PrivateAssemblies")
+ Path.Combine(vsInstallDir, @"IDE\CommonExtensions\Microsoft\ManagedLanguages\VBCSharp\LanguageServices")
+ Path.Combine(vsInstallDir, @"IDE\Extensions\Microsoft\CodeSense\Framework")
+ Path.Combine(vsInstallDir, @"IDE")
+ |]
+
+ AppDomain.CurrentDomain.add_AssemblyResolve(fun _ args ->
+ let found () =
+ probingPaths
+ |> Seq.tryPick (fun p ->
+ try
+ let name = AssemblyName(args.Name)
+ let codebase = Path.GetFullPath(Path.Combine(p, name.Name) + ".dll")
+ if File.Exists(codebase) then
+ name.CodeBase <- codebase
+ name.CultureInfo <- Unchecked.defaultof
+ name.Version <- Unchecked.defaultof
+ Some name
+ else
+ None
+ with _ ->
+ None)
+
+ match found () with
+ | None -> Unchecked.defaultof
+ | Some name -> Assembly.Load(name))
diff --git a/vsintegration/tests/FSharp.Editor.Tests/Helpers/AssemblyResolver.fs b/vsintegration/tests/FSharp.Editor.Tests/Helpers/AssemblyResolver.fs
index 78a57793517..ef8c01ca908 100644
--- a/vsintegration/tests/FSharp.Editor.Tests/Helpers/AssemblyResolver.fs
+++ b/vsintegration/tests/FSharp.Editor.Tests/Helpers/AssemblyResolver.fs
@@ -2,62 +2,9 @@
namespace FSharp.Editor.Tests.Helpers
-open System
-open System.IO
-open System.Reflection
-
module AssemblyResolver =
- open System.Globalization
- open FSharp.Test.VSInstallDiscovery
-
- let vsInstallDir =
- // Use centralized VS installation discovery with graceful fallback
- match tryGetVSInstallDir () with
- | Some dir -> dir
- | None ->
- // Fallback to legacy behavior for backward compatibility
- let vsvar =
- let var = Environment.GetEnvironmentVariable("VS170COMNTOOLS")
-
- if String.IsNullOrEmpty var then
- Environment.GetEnvironmentVariable("VSAPPIDDIR")
- else
- var
-
- if String.IsNullOrEmpty vsvar then
- failwith "VS170COMNTOOLS and VSAPPIDDIR environment variables not found."
-
- Path.Combine(vsvar, "..")
-
- let probingPaths =
- [|
- Path.Combine(vsInstallDir, @"IDE\CommonExtensions\Microsoft\Editor")
- Path.Combine(vsInstallDir, @"IDE\PublicAssemblies")
- Path.Combine(vsInstallDir, @"IDE\PrivateAssemblies")
- Path.Combine(vsInstallDir, @"IDE\CommonExtensions\Microsoft\ManagedLanguages\VBCSharp\LanguageServices")
- Path.Combine(vsInstallDir, @"IDE\Extensions\Microsoft\CodeSense\Framework")
- Path.Combine(vsInstallDir, @"IDE")
- |]
-
- let addResolver () =
- AppDomain.CurrentDomain.add_AssemblyResolve (fun h args ->
- let found () =
- (probingPaths)
- |> Seq.tryPick (fun p ->
- try
- let name = AssemblyName(args.Name)
- let codebase = Path.GetFullPath(Path.Combine(p, name.Name) + ".dll")
-
- if File.Exists(codebase) then
- name.CodeBase <- codebase
- name.CultureInfo <- Unchecked.defaultof
- name.Version <- Unchecked.defaultof
- Some(name)
- else
- None
- with _ ->
- None)
+ open FSharp.Test.VSAssemblyResolver
- match found () with
- | None -> Unchecked.defaultof
- | Some name -> Assembly.Load(name))
+ /// Adds an assembly resolver that probes Visual Studio installation directories.
+ /// This is a compatibility shim that delegates to the centralized implementation.
+ let addResolver = addResolver
diff --git a/vsintegration/tests/Salsa/VisualFSharp.Salsa.fsproj b/vsintegration/tests/Salsa/VisualFSharp.Salsa.fsproj
index f7410214430..d41b116c21e 100644
--- a/vsintegration/tests/Salsa/VisualFSharp.Salsa.fsproj
+++ b/vsintegration/tests/Salsa/VisualFSharp.Salsa.fsproj
@@ -24,6 +24,9 @@
UnitTests.TestLib.Utils.fs
+
+ VSInstallDiscovery.fs
+
diff --git a/vsintegration/tests/Salsa/VsMocks.fs b/vsintegration/tests/Salsa/VsMocks.fs
index 1ce67c04763..54fe8b906de 100644
--- a/vsintegration/tests/Salsa/VsMocks.fs
+++ b/vsintegration/tests/Salsa/VsMocks.fs
@@ -1642,6 +1642,7 @@ module internal VsActual =
open System.ComponentModel.Composition.Primitives
open Microsoft.VisualStudio.Text
open Microsoft.VisualStudio.Threading
+ open FSharp.Test.VSInstallDiscovery
type TestExportJoinableTaskContext () =
@@ -1650,22 +1651,7 @@ module internal VsActual =
[)>]
member public _.JoinableTaskContext : JoinableTaskContext = jtc
- let vsInstallDir =
- // use the environment variable to find the VS installdir
- let vsvar =
- // Try VS180COMNTOOLS first, then VS170COMNTOOLS, then VSAPPIDDIR
- // TODO : use tryGetVSInstallDir from test utils instead
- let var18 = Environment.GetEnvironmentVariable("VS180COMNTOOLS")
- if String.IsNullOrEmpty var18 then
- let var17 = Environment.GetEnvironmentVariable("VS170COMNTOOLS")
- if String.IsNullOrEmpty var17 then
- Environment.GetEnvironmentVariable("VSAPPIDDIR")
- else
- var17
- else
- var18
- if String.IsNullOrEmpty vsvar then failwith "VS180COMNTOOLS, VS170COMNTOOLS and VSAPPIDDIR environment variables not found."
- Path.Combine(vsvar, "..")
+ let vsInstallDir = getVSInstallDirOrFail ()
let CreateEditorCatalog() =
let thisAssembly = Assembly.GetExecutingAssembly().Location
diff --git a/vsintegration/tests/UnitTests/AssemblyResolver.fs b/vsintegration/tests/UnitTests/AssemblyResolver.fs
index 38b5ee45290..cf36b723e40 100644
--- a/vsintegration/tests/UnitTests/AssemblyResolver.fs
+++ b/vsintegration/tests/UnitTests/AssemblyResolver.fs
@@ -1,52 +1,8 @@
namespace Microsoft.VisualStudio.FSharp
-open System
-open System.IO
-open System.Reflection
-
module AssemblyResolver =
- open System.Globalization
- open FSharp.Test.VSInstallDiscovery
-
- let vsInstallDir =
- // Use centralized VS installation discovery with graceful fallback
- match tryGetVSInstallDir () with
- | Some dir -> dir
- | None ->
- // Fallback to legacy behavior for backward compatibility
- let vsvar =
- let var = Environment.GetEnvironmentVariable("VS170COMNTOOLS")
- if String.IsNullOrEmpty var then
- Environment.GetEnvironmentVariable("VSAPPIDDIR")
- else
- var
- if String.IsNullOrEmpty vsvar then failwith "VS170COMNTOOLS and VSAPPIDDIR environment variables not found."
- Path.Combine(vsvar, "..")
-
- let probingPaths = [|
- Path.Combine(vsInstallDir, @"IDE\CommonExtensions\Microsoft\Editor")
- Path.Combine(vsInstallDir, @"IDE\PublicAssemblies")
- Path.Combine(vsInstallDir, @"IDE\PrivateAssemblies")
- Path.Combine(vsInstallDir, @"IDE\CommonExtensions\Microsoft\ManagedLanguages\VBCSharp\LanguageServices")
- Path.Combine(vsInstallDir, @"IDE\Extensions\Microsoft\CodeSense\Framework")
- Path.Combine(vsInstallDir, @"IDE")
- |]
+ open FSharp.Test.VSAssemblyResolver
- let addResolver () =
- AppDomain.CurrentDomain.add_AssemblyResolve(fun h args ->
- let found () =
- (probingPaths ) |> Seq.tryPick(fun p ->
- try
- let name = AssemblyName(args.Name)
- let codebase = Path.GetFullPath(Path.Combine(p, name.Name) + ".dll")
- if File.Exists(codebase) then
- name.CodeBase <- codebase
- name.CultureInfo <- Unchecked.defaultof
- name.Version <- Unchecked.defaultof
- Some (name)
- else None
- with | _ -> None
- )
- match found() with
- | None -> Unchecked.defaultof
- | Some name -> Assembly.Load(name) )
+ /// Adds an assembly resolver that probes Visual Studio installation directories.
+ /// This is a compatibility shim that delegates to the centralized implementation.
+ let addResolver = addResolver