diff --git a/.config/tsaoptions.json b/.config/tsaoptions.json
new file mode 100644
index 00000000..219f02d7
--- /dev/null
+++ b/.config/tsaoptions.json
@@ -0,0 +1,11 @@
+{
+ "instanceUrl": "https://devdiv.visualstudio.com/",
+ "template": "TFSDEVDIV",
+ "projectName": "DEVDIV",
+ "areaPath": "DevDiv\\NET Tools Prague\\MSBuild",
+ "iterationPath": "DevDiv",
+ "notificationAliases": [ "msbtm@microsoft.com" ],
+ "repositoryName": "MSBuildLocator",
+ "codebaseName": "MSBuildLocator",
+ "serviceTreeId": "d0ebbe59-0779-4466-8280-a0ff9cab5550"
+}
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
index 9c74879f..a2a87c55 100644
--- a/.github/workflows/pull-request.yml
+++ b/.github/workflows/pull-request.yml
@@ -22,22 +22,23 @@ jobs:
with:
dotnet-version: |
6.0.x
+ 8.0.x
- name: Restore
- run: dotnet restore -bl:restore.binlog
+ run: dotnet restore -bl:LocatorRestore.binlog
- name: Build
run: dotnet build --no-restore -bl:MSBuildLocator.binlog
- name: Test
run: dotnet test --no-build --verbosity normal
- name: Pack
- run: dotnet pack --no-build
+ run: dotnet pack --no-build --configuration Debug -bl:LocatorPack.binlog
- name: Upload Packages
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v4
with:
name: package
path: '**/*.*nupkg'
if-no-files-found: error
- name: Upload logs
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v4
if: ${{ always() }}
with:
name: logs
diff --git a/Directory.Build.props b/Directory.Build.props
index ee0885fa..a58f4b5f 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -29,7 +29,7 @@
-
+
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 85c64f2b..6e1123d9 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -1,3 +1,7 @@
+trigger:
+ - main
+ - release/*
+
variables:
- name: BuildPlatform
value: ''
@@ -22,6 +26,10 @@ extends:
sdl:
sourceAnalysisPool:
name: VSEngSS-MicroBuild2022-1ES
+ policheck:
+ enabled: true
+ tsa:
+ enabled: true
stages:
- stage: stage
jobs:
diff --git a/branding/MSBuild-NuGet-Icon.png b/branding/MSBuild-NuGet-Icon.png
index d51ddc13..ddb55a0e 100644
Binary files a/branding/MSBuild-NuGet-Icon.png and b/branding/MSBuild-NuGet-Icon.png differ
diff --git a/src/MSBuildLocator.Tests/Microsoft.Build.Locator.Tests.csproj b/src/MSBuildLocator.Tests/Microsoft.Build.Locator.Tests.csproj
index 963b3685..e7949c93 100644
--- a/src/MSBuildLocator.Tests/Microsoft.Build.Locator.Tests.csproj
+++ b/src/MSBuildLocator.Tests/Microsoft.Build.Locator.Tests.csproj
@@ -8,10 +8,10 @@
-
-
-
-
+
+
+
+
diff --git a/src/MSBuildLocator/DotNetSdkLocationHelper.cs b/src/MSBuildLocator/DotNetSdkLocationHelper.cs
index f5398ff6..58cb54d9 100644
--- a/src/MSBuildLocator/DotNetSdkLocationHelper.cs
+++ b/src/MSBuildLocator/DotNetSdkLocationHelper.cs
@@ -56,8 +56,8 @@ internal static class DotNetSdkLocationHelper
// in the .NET 5 SDK rely on the .NET 5.0 runtime. Assuming the runtime that shipped with a particular SDK has the same version,
// this ensures that we don't choose an SDK that doesn't work with the runtime of the chosen application. This is not guaranteed
// to always work but should work for now.
- if (!allowQueryAllRuntimeVersions &&
- (major > Environment.Version.Major ||
+ if (!allowQueryAllRuntimeVersions &&
+ (major > Environment.Version.Major ||
(major == Environment.Version.Major && minor > Environment.Version.Minor)))
{
return null;
@@ -70,46 +70,104 @@ internal static class DotNetSdkLocationHelper
discoveryType: DiscoveryType.DotNetSdk);
}
- public static IEnumerable GetInstances(string workingDirectory, bool allowQueryAllRuntimes)
- {
- foreach (var basePath in GetDotNetBasePaths(workingDirectory))
+ public static IEnumerable GetInstances(string workingDirectory, bool allowQueryAllRuntimes, bool allowAllDotnetLocations)
+ {
+ string? bestSdkPath;
+ string[] allAvailableSdks;
+ try
+ {
+ AddUnmanagedDllResolver();
+
+ bestSdkPath = GetSdkFromGlobalSettings(workingDirectory);
+ allAvailableSdks = GetAllAvailableSDKs(allowAllDotnetLocations).ToArray();
+ }
+ finally
+ {
+ RemoveUnmanagedDllResolver();
+ }
+
+ Dictionary versionInstanceMap = new();
+ foreach (var basePath in allAvailableSdks)
{
var dotnetSdk = GetInstance(basePath, allowQueryAllRuntimes);
if (dotnetSdk != null)
{
- yield return dotnetSdk;
+ // We want to return the best SDK first
+ if (dotnetSdk.VisualStudioRootPath == bestSdkPath)
+ {
+ // We will add a null entry to the map to ensure we do not add the same SDK from a different location.
+ versionInstanceMap[dotnetSdk.Version] = null;
+ yield return dotnetSdk;
+ }
+
+ // Only add an SDK once, even if it's installed in multiple locations.
+ versionInstanceMap.TryAdd(dotnetSdk.Version, dotnetSdk);
}
}
- }
- private static IEnumerable GetDotNetBasePaths(string workingDirectory)
- {
- try
+ // We want to return the newest SDKs first. Using OfType will remove the null entry added if we found the best SDK.
+ var instances = versionInstanceMap.Values.OfType().OrderByDescending(i => i.Version);
+ foreach (var instance in instances)
{
- AddUnmanagedDllResolver();
+ yield return instance;
+ }
- string? bestSDK = GetSdkFromGlobalSettings(workingDirectory);
- if (!string.IsNullOrEmpty(bestSDK))
+ // Returns the list of all available SDKs ordered by ascending version.
+ static IEnumerable GetAllAvailableSDKs(bool allowAllDotnetLocations)
+ {
+ bool foundSdks = false;
+ string[]? resolvedPaths = null;
+ foreach (string dotnetPath in s_dotnetPathCandidates.Value)
{
- yield return bestSDK;
- }
+ int rc = NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, result: (key, value) => resolvedPaths = value);
- string[] dotnetPaths = GetAllAvailableSDKs();
- // We want to return the newest SDKs first, however, so iterate over the list in reverse order.
- // If basePath is disqualified because it was later
- // than the runtime version, this ensures that RegisterDefaults will return the latest valid
- // SDK instead of the earliest installed.
- for (int i = dotnetPaths.Length - 1; i >= 0; i--)
- {
- if (dotnetPaths[i] != bestSDK)
+ if (rc == 0 && resolvedPaths != null)
{
- yield return dotnetPaths[i];
+ foundSdks = true;
+
+ foreach (string path in resolvedPaths)
+ {
+ yield return path;
+ }
+
+ if (resolvedPaths.Length > 0 && !allowAllDotnetLocations)
+ {
+ break;
+ }
}
}
+
+ // Errors are automatically printed to stderr. We should not continue to try to output anything if we failed.
+ if (!foundSdks)
+ {
+ throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_get_available_sdks)));
+ }
}
- finally
+
+ // Determines the directory location of the SDK accounting for global.json and multi-level lookup policy.
+ static string? GetSdkFromGlobalSettings(string workingDirectory)
{
- RemoveUnmanagedDllResolver();
+ string? resolvedSdk = null;
+ foreach (string dotnetPath in s_dotnetPathCandidates.Value)
+ {
+ int rc = NativeMethods.hostfxr_resolve_sdk2(exe_dir: dotnetPath, working_dir: workingDirectory, flags: 0, result: (key, value) =>
+ {
+ if (key == NativeMethods.hostfxr_resolve_sdk2_result_key_t.resolved_sdk_dir)
+ {
+ resolvedSdk = value;
+ }
+ });
+
+ if (rc == 0)
+ {
+ SetEnvironmentVariableIfEmpty("DOTNET_HOST_PATH", Path.Combine(dotnetPath, ExeName));
+ return resolvedSdk;
+ }
+ }
+
+ return string.IsNullOrEmpty(resolvedSdk)
+ ? throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_resolve_sdk2)))
+ : resolvedSdk;
}
}
@@ -158,7 +216,7 @@ private static IntPtr HostFxrResolver(Assembly assembly, string libraryName)
};
var orderedVersions = fileEnumerable.Where(v => v != null).Select(v => v!).OrderByDescending(f => f).ToList();
-
+
foreach (SemanticVersion hostFxrVersion in orderedVersions)
{
string hostFxrAssembly = Path.Combine(hostFxrRoot, hostFxrVersion.OriginalValue, hostFxrLibName);
@@ -178,35 +236,6 @@ private static IntPtr HostFxrResolver(Assembly assembly, string libraryName)
}
private static string SdkResolutionExceptionMessage(string methodName) => $"Failed to find all versions of .NET Core MSBuild. Call to {methodName}. There may be more details in stderr.";
-
- ///
- /// Determines the directory location of the SDK accounting for
- /// global.json and multi-level lookup policy.
- ///
- private static string? GetSdkFromGlobalSettings(string workingDirectory)
- {
- string? resolvedSdk = null;
- foreach (string dotnetPath in s_dotnetPathCandidates.Value)
- {
- int rc = NativeMethods.hostfxr_resolve_sdk2(exe_dir: dotnetPath, working_dir: workingDirectory, flags: 0, result: (key, value) =>
- {
- if (key == NativeMethods.hostfxr_resolve_sdk2_result_key_t.resolved_sdk_dir)
- {
- resolvedSdk = value;
- }
- });
-
- if (rc == 0)
- {
- SetEnvironmentVariableIfEmpty("DOTNET_HOST_PATH", Path.Combine(dotnetPath, ExeName));
- return resolvedSdk;
- }
- }
-
- return string.IsNullOrEmpty(resolvedSdk)
- ? throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_resolve_sdk2)))
- : resolvedSdk;
- }
private static IList ResolveDotnetPathCandidates()
{
@@ -256,7 +285,7 @@ void AddIfValid(string? path)
// 32-bit architecture has (x86) suffix
string envVarName = (IntPtr.Size == 4) ? "DOTNET_ROOT(x86)" : "DOTNET_ROOT";
var dotnetPath = FindDotnetPathFromEnvVariable(envVarName);
-
+
return dotnetPath;
}
@@ -285,26 +314,6 @@ void AddIfValid(string? path)
return dotnetPath;
}
- ///
- /// Returns the list of all available SDKs ordered by ascending version.
- ///
- private static string[] GetAllAvailableSDKs()
- {
- string[]? resolvedPaths = null;
- foreach (string dotnetPath in s_dotnetPathCandidates.Value)
- {
- int rc = NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, result: (key, value) => resolvedPaths = value);
-
- if (rc == 0 && resolvedPaths != null && resolvedPaths.Length > 0)
- {
- break;
- }
- }
-
- // Errors are automatically printed to stderr. We should not continue to try to output anything if we failed.
- return resolvedPaths ?? throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_get_available_sdks)));
- }
-
///
/// This native method call determines the actual location of path, including
/// resolving symbolic links.
@@ -321,7 +330,7 @@ private static string[] GetAllAvailableSDKs()
private static string? FindDotnetPathFromEnvVariable(string environmentVariable)
{
string? dotnetPath = Environment.GetEnvironmentVariable(environmentVariable);
-
+
return string.IsNullOrEmpty(dotnetPath) ? null : ValidatePath(dotnetPath);
}
diff --git a/src/MSBuildLocator/MSBuildLocator.cs b/src/MSBuildLocator/MSBuildLocator.cs
index f501f49d..9d766763 100644
--- a/src/MSBuildLocator/MSBuildLocator.cs
+++ b/src/MSBuildLocator/MSBuildLocator.cs
@@ -52,6 +52,14 @@ public static class MSBuildLocator
///
+ /// Allow discovery of .NET SDK versions from all discovered dotnet install locations.
+ ///
+ ///
+ /// Defaults to . Set this to only if you do not mind behaving differently than the dotnet muxer.
+ ///
/// Gets a value indicating whether an instance of MSBuild can be registered.
///
@@ -200,7 +208,7 @@ private static void RegisterMSBuildPathsInternally(string[] msbuildSearchPaths)
{
if (string.IsNullOrWhiteSpace(msbuildSearchPaths[i]))
{
- nullOrWhiteSpaceExceptions.Add(new ArgumentException($"Value at position {i+1} may not be null or whitespace", nameof(msbuildSearchPaths)));
+ nullOrWhiteSpaceExceptions.Add(new ArgumentException($"Value at position {i + 1} may not be null or whitespace", nameof(msbuildSearchPaths)));
}
}
if (nullOrWhiteSpaceExceptions.Count > 0)
@@ -266,7 +274,7 @@ private static void RegisterMSBuildPathsInternally(string[] msbuildSearchPaths)
AppDomain.CurrentDomain.AssemblyResolve += s_registeredHandler;
#else
- s_registeredHandler = (_, assemblyName) =>
+ s_registeredHandler = (_, assemblyName) =>
{
return TryLoadAssembly(assemblyName);
};
@@ -377,7 +385,8 @@ private static IEnumerable GetInstances(VisualStudioInstan
#if NETCOREAPP
// AllowAllRuntimeVersions was added to VisualStudioInstanceQueryOptions for fulfilling Roslyn's needs. One of the properties will be removed in v2.0.
bool allowAllRuntimeVersions = AllowQueryAllRuntimeVersions || options.AllowAllRuntimeVersions;
- foreach (var dotnetSdk in DotNetSdkLocationHelper.GetInstances(options.WorkingDirectory, allowAllRuntimeVersions))
+ bool allowAllDotnetLocations = AllowQueryAllDotnetLocations || options.AllowAllDotnetLocations;
+ foreach (var dotnetSdk in DotNetSdkLocationHelper.GetInstances(options.WorkingDirectory, allowAllRuntimeVersions, allowAllDotnetLocations))
yield return dotnetSdk;
#endif
}
@@ -404,7 +413,7 @@ private static VisualStudioInstance GetDevConsoleInstance()
Version.TryParse(versionString, out version);
}
- if(version != null)
+ if (version != null)
{
return new VisualStudioInstance("DEVCONSOLE", path, version, DiscoveryType.DeveloperConsole);
}
diff --git a/src/MSBuildLocator/Microsoft.Build.Locator.csproj b/src/MSBuildLocator/Microsoft.Build.Locator.csproj
index ba90a98a..2df37509 100644
--- a/src/MSBuildLocator/Microsoft.Build.Locator.csproj
+++ b/src/MSBuildLocator/Microsoft.Build.Locator.csproj
@@ -25,7 +25,7 @@
-
+
diff --git a/src/MSBuildLocator/VisualStudioInstanceQueryOptions.cs b/src/MSBuildLocator/VisualStudioInstanceQueryOptions.cs
index c3e16604..116abb2e 100644
--- a/src/MSBuildLocator/VisualStudioInstanceQueryOptions.cs
+++ b/src/MSBuildLocator/VisualStudioInstanceQueryOptions.cs
@@ -37,6 +37,14 @@ public class VisualStudioInstanceQueryOptions
/// Defaults to . Set this to only if your application has special logic to handle loading an incompatible SDK, such as launching a new process with the target SDK's runtime.
///
+ /// Allow discovery of .NET SDK versions from all discovered dotnet install locations.
+ ///
+ ///
+ /// Defaults to . Set this to only if you do not mind behaving differently than a command-line dotnet invocation.
+ ///
diff --git a/version.json b/version.json
index 1d011499..99db79e8 100644
--- a/version.json
+++ b/version.json
@@ -1,5 +1,5 @@
{
- "version": "1.7",
+ "version": "1.8",
"assemblyVersion": "1.0.0.0",
"publicReleaseRefSpec": [
"^refs/heads/release/.*"