Skip to content

Commit e0281df

Browse files
Release DOTNET_ROOT handling changes and packages warning presence condition
2 parents 4507992 + 65af323 commit e0281df

File tree

6 files changed

+99
-68
lines changed

6 files changed

+99
-68
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,20 @@ That additional build logic is distributed with Visual Studio, with Visual Studi
88

99
Loading MSBuild from Visual Studio also ensures that your application gets the same view of projects as `MSBuild.exe`, `dotnet build`, or Visual Studio, including bug fixes, feature additions, and performance improvements that may come from a newer MSBuild release.
1010

11+
## How Locator searches for .NET SDK?
12+
13+
MSBuild.Locator searches for the locally installed SDK based on the following priority:
14+
15+
1. DOTNET_ROOT
16+
2. Current process path if MSBuild.Locator is called from dotnet.exe
17+
3. DOTNET_HOST_PATH
18+
4. DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR
19+
5. PATH
20+
21+
Note that probing stops when the first dotnet executable is found among the listed variables.
22+
23+
Documentation describing the definition of these variables can be found here: [.NET Environment Variables](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-environment-variables).
24+
1125
## Documentation
1226

1327
Documentation is located on the official Microsoft documentation site: [Use Microsoft.Build.Locator](https://docs.microsoft.com/visualstudio/msbuild/updating-an-existing-application#use-microsoftbuildlocator).

samples/BuilderApp/BuilderApp.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,6 @@
2828
Not necessary if you use the package! -->
2929
<Import Project="..\..\src\MSBuildLocator\build\Microsoft.Build.Locator.props"/>
3030

31+
<Import Project="..\..\src\MSBuildLocator\build\Microsoft.Build.Locator.targets"/>
32+
3133
</Project>

src/MSBuildLocator.Tests/Microsoft.Build.Locator.Tests.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
<ItemGroup>
1111
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
1212
<PackageReference Include="Shouldly" Version="4.2.1" />
13-
<PackageReference Include="xunit" Version="2.5.0" />
14-
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0" />
13+
<PackageReference Include="xunit" Version="2.5.1" />
14+
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.1" />
1515
</ItemGroup>
1616

1717
<ItemGroup>

src/MSBuildLocator/DotNetSdkLocationHelper.cs

Lines changed: 77 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ internal static class DotNetSdkLocationHelper
2222
private static readonly Regex VersionRegex = new Regex(@"^(\d+)\.(\d+)\.(\d+)", RegexOptions.Multiline);
2323
private static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
2424
private static readonly string ExeName = IsWindows ? "dotnet.exe" : "dotnet";
25-
private static readonly Lazy<string> DotnetPath = new(() => ResolveDotnetPath());
25+
private static readonly Lazy<IList<string>> s_dotnetPathCandidates = new(() => ResolveDotnetPathCandidates());
2626

2727
public static VisualStudioInstance? GetInstance(string dotNetSdkPath)
2828
{
@@ -141,22 +141,31 @@ private static IntPtr HostFxrResolver(Assembly assembly, string libraryName)
141141
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
142142
"hostfxr.dll" :
143143
RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "libhostfxr.dylib" : "libhostfxr.so";
144+
string hostFxrRoot = string.Empty;
144145

145-
string hostFxrRoot = Path.Combine(DotnetPath.Value, "host", "fxr");
146-
if (Directory.Exists(hostFxrRoot))
146+
// Get the dotnet path candidates
147+
foreach (string dotnetPath in s_dotnetPathCandidates.Value)
147148
{
148-
var fileEnumerable = new FileSystemEnumerable<SemanticVersion?>(
149-
directory: hostFxrRoot,
150-
transform: static (ref FileSystemEntry entry) => SemanticVersionParser.TryParse(entry.FileName.ToString(), out var version) ? version : null)
149+
hostFxrRoot = Path.Combine(dotnetPath, "host", "fxr");
150+
if (Directory.Exists(hostFxrRoot))
151151
{
152-
ShouldIncludePredicate = static (ref FileSystemEntry entry) => entry.IsDirectory
153-
};
152+
var fileEnumerable = new FileSystemEnumerable<SemanticVersion?>(
153+
directory: hostFxrRoot,
154+
transform: static (ref FileSystemEntry entry) => SemanticVersionParser.TryParse(entry.FileName.ToString(), out var version) ? version : null)
155+
{
156+
ShouldIncludePredicate = static (ref FileSystemEntry entry) => entry.IsDirectory
157+
};
154158

155-
// Load hostfxr from the highest version, because it should be backward-compatible
156-
if (fileEnumerable.Max() is SemanticVersion hostFxrVersion)
157-
{
158-
string hostFxrAssembly = Path.Combine(hostFxrRoot, hostFxrVersion.OriginalValue, hostFxrLibName);
159-
return NativeLibrary.Load(hostFxrAssembly);
159+
var orderedVersions = fileEnumerable.Where(v => v != null).Select(v => v!).OrderByDescending(f => f).ToList();
160+
161+
foreach (SemanticVersion hostFxrVersion in orderedVersions)
162+
{
163+
string hostFxrAssembly = Path.Combine(hostFxrRoot, hostFxrVersion.OriginalValue, hostFxrLibName);
164+
if (NativeLibrary.TryLoad(hostFxrAssembly, out IntPtr handle))
165+
{
166+
return handle;
167+
}
168+
}
160169
}
161170
}
162171

@@ -176,68 +185,69 @@ private static IntPtr HostFxrResolver(Assembly assembly, string libraryName)
176185
private static string? GetSdkFromGlobalSettings(string workingDirectory)
177186
{
178187
string? resolvedSdk = null;
179-
int rc = NativeMethods.hostfxr_resolve_sdk2(exe_dir: DotnetPath.Value, working_dir: workingDirectory, flags: 0, result: (key, value) =>
188+
foreach (string dotnetPath in s_dotnetPathCandidates.Value)
180189
{
181-
if (key == NativeMethods.hostfxr_resolve_sdk2_result_key_t.resolved_sdk_dir)
190+
int rc = NativeMethods.hostfxr_resolve_sdk2(exe_dir: dotnetPath, working_dir: workingDirectory, flags: 0, result: (key, value) =>
182191
{
183-
resolvedSdk = value;
184-
}
185-
});
192+
if (key == NativeMethods.hostfxr_resolve_sdk2_result_key_t.resolved_sdk_dir)
193+
{
194+
resolvedSdk = value;
195+
}
196+
});
186197

187-
if (rc != 0)
188-
{
189-
throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_resolve_sdk2)));
198+
if (rc == 0)
199+
{
200+
SetEnvironmentVariableIfEmpty("DOTNET_HOST_PATH", Path.Combine(dotnetPath, ExeName));
201+
return resolvedSdk;
202+
}
190203
}
191204

192-
return resolvedSdk;
205+
return string.IsNullOrEmpty(resolvedSdk)
206+
? throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_resolve_sdk2)))
207+
: resolvedSdk;
193208
}
194209

195-
private static string ResolveDotnetPath()
210+
private static IList<string> ResolveDotnetPathCandidates()
196211
{
197-
string? dotnetPath = GetDotnetPathFromROOT();
212+
var pathCandidates = new List<string>();
213+
AddIfValid(GetDotnetPathFromROOT());
214+
215+
string? dotnetExePath = GetCurrentProcessPath();
216+
bool isRunFromDotnetExecutable = !string.IsNullOrEmpty(dotnetExePath)
217+
&& Path.GetFileName(dotnetExePath).Equals(ExeName, StringComparison.InvariantCultureIgnoreCase);
218+
219+
if (isRunFromDotnetExecutable)
220+
{
221+
AddIfValid(Path.GetDirectoryName(dotnetExePath));
222+
}
198223

199-
if (string.IsNullOrEmpty(dotnetPath))
224+
string? hostPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH");
225+
if (!string.IsNullOrEmpty(hostPath) && File.Exists(hostPath))
200226
{
201-
string? dotnetExePath = GetCurrentProcessPath();
202-
var isRunFromDotnetExecutable = !string.IsNullOrEmpty(dotnetExePath)
203-
&& Path.GetFileName(dotnetExePath).Equals(ExeName, StringComparison.OrdinalIgnoreCase);
204-
205-
if (isRunFromDotnetExecutable)
227+
if (!IsWindows)
206228
{
207-
dotnetPath = Path.GetDirectoryName(dotnetExePath);
229+
hostPath = realpath(hostPath) ?? hostPath;
208230
}
209-
else
210-
{
211-
// DOTNET_HOST_PATH is pointing to the file, DOTNET_ROOT is the path of the folder
212-
string? hostPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH");
213-
if (!string.IsNullOrEmpty(hostPath) && File.Exists(hostPath))
214-
{
215-
if (!IsWindows)
216-
{
217-
hostPath = realpath(hostPath) ?? hostPath;
218-
}
219-
220-
dotnetPath = Path.GetDirectoryName(hostPath);
221-
if (dotnetPath is not null)
222-
{
223-
// don't overwrite DOTNET_HOST_PATH, if we use it.
224-
return dotnetPath;
225-
}
226-
}
227231

228-
dotnetPath = FindDotnetPathFromEnvVariable("DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR")
229-
?? GetDotnetPathFromPATH();
230-
}
232+
AddIfValid(Path.GetDirectoryName(hostPath));
231233
}
232234

233-
if (string.IsNullOrEmpty(dotnetPath))
234-
{
235-
throw new InvalidOperationException("Could not find the dotnet executable. Is it set on the DOTNET_ROOT?");
236-
}
235+
AddIfValid(FindDotnetPathFromEnvVariable("DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR"));
236+
AddIfValid(GetDotnetPathFromPATH());
237237

238-
SetEnvironmentVariableIfEmpty("DOTNET_HOST_PATH", Path.Combine(dotnetPath, ExeName));
238+
return pathCandidates.Count == 0
239+
? throw new InvalidOperationException("Path to dotnet executable is not set. " +
240+
"The probed variables are: DOTNET_ROOT, DOTNET_HOST_PATH, DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR and PATH. " +
241+
"Make sure, that at least one of the listed variables points to the existing dotnet executable.")
242+
: pathCandidates;
239243

240-
return dotnetPath;
244+
void AddIfValid(string? path)
245+
{
246+
if (!string.IsNullOrEmpty(path))
247+
{
248+
pathCandidates.Add(path);
249+
}
250+
}
241251
}
242252

243253
private static string? GetDotnetPathFromROOT()
@@ -280,15 +290,18 @@ private static string ResolveDotnetPath()
280290
private static string[] GetAllAvailableSDKs()
281291
{
282292
string[]? resolvedPaths = null;
283-
int rc = NativeMethods.hostfxr_get_available_sdks(exe_dir: DotnetPath.Value, result: (key, value) => resolvedPaths = value);
284-
285-
// Errors are automatically printed to stderr. We should not continue to try to output anything if we failed.
286-
if (rc != 0)
293+
foreach (string dotnetPath in s_dotnetPathCandidates.Value)
287294
{
288-
throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_get_available_sdks)));
295+
int rc = NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, result: (key, value) => resolvedPaths = value);
296+
297+
if (rc == 0 && resolvedPaths != null && resolvedPaths.Length > 0)
298+
{
299+
break;
300+
}
289301
}
290302

291-
return resolvedPaths ?? Array.Empty<string>();
303+
// Errors are automatically printed to stderr. We should not continue to try to output anything if we failed.
304+
return resolvedPaths ?? throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_get_available_sdks)));
292305
}
293306

294307
/// <summary>

src/MSBuildLocator/Microsoft.Build.Locator.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414

1515
<Title>MSBuild Locator</Title>
1616
<Description>Package that assists in locating and using a copy of MSBuild installed as part of Visual Studio 2017 or higher or .NET Core SDK 2.1 or higher.</Description>
17+
18+
<EnablePackageValidation>true</EnablePackageValidation>
19+
<PackageValidationBaselineVersion>1.6.1</PackageValidationBaselineVersion>
1720
</PropertyGroup>
1821

1922
<PropertyGroup Condition="'$(TargetFramework)'=='net46'">

src/MSBuildLocator/build/Microsoft.Build.Locator.targets

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
<ItemGroup>
44
<MSBuildPackagesWithoutPrivateAssets
55
Include="@(PackageReference)"
6-
Condition="
7-
'%(PackageReference.ExcludeAssets)' != 'runtime' and
6+
Condition="!$([MSBuild]::ValueOrDefault('%(PackageReference.ExcludeAssets)', '').ToLower().Contains('runtime')) and
87
(
98
'%(PackageReference.Identity)' == 'Microsoft.Build' or
109
'%(PackageReference.Identity)' == 'Microsoft.Build.Framework' or

0 commit comments

Comments
 (0)