Skip to content

Commit

Permalink
Release DOTNET_ROOT handling changes and packages warning presence co…
Browse files Browse the repository at this point in the history
…ndition
  • Loading branch information
YuliiaKovalova committed Sep 20, 2023
2 parents 4507992 + 65af323 commit e0281df
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 68 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ That additional build logic is distributed with Visual Studio, with Visual Studi

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.

## How Locator searches for .NET SDK?

MSBuild.Locator searches for the locally installed SDK based on the following priority:

1. DOTNET_ROOT
2. Current process path if MSBuild.Locator is called from dotnet.exe
3. DOTNET_HOST_PATH
4. DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR
5. PATH

Note that probing stops when the first dotnet executable is found among the listed variables.

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).

## Documentation

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).
Expand Down
2 changes: 2 additions & 0 deletions samples/BuilderApp/BuilderApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,6 @@
Not necessary if you use the package! -->
<Import Project="..\..\src\MSBuildLocator\build\Microsoft.Build.Locator.props"/>

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

</Project>
4 changes: 2 additions & 2 deletions src/MSBuildLocator.Tests/Microsoft.Build.Locator.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="Shouldly" Version="4.2.1" />
<PackageReference Include="xunit" Version="2.5.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0" />
<PackageReference Include="xunit" Version="2.5.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.1" />
</ItemGroup>

<ItemGroup>
Expand Down
141 changes: 77 additions & 64 deletions src/MSBuildLocator/DotNetSdkLocationHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ internal static class DotNetSdkLocationHelper
private static readonly Regex VersionRegex = new Regex(@"^(\d+)\.(\d+)\.(\d+)", RegexOptions.Multiline);
private static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
private static readonly string ExeName = IsWindows ? "dotnet.exe" : "dotnet";
private static readonly Lazy<string> DotnetPath = new(() => ResolveDotnetPath());
private static readonly Lazy<IList<string>> s_dotnetPathCandidates = new(() => ResolveDotnetPathCandidates());

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

string hostFxrRoot = Path.Combine(DotnetPath.Value, "host", "fxr");
if (Directory.Exists(hostFxrRoot))
// Get the dotnet path candidates
foreach (string dotnetPath in s_dotnetPathCandidates.Value)
{
var fileEnumerable = new FileSystemEnumerable<SemanticVersion?>(
directory: hostFxrRoot,
transform: static (ref FileSystemEntry entry) => SemanticVersionParser.TryParse(entry.FileName.ToString(), out var version) ? version : null)
hostFxrRoot = Path.Combine(dotnetPath, "host", "fxr");
if (Directory.Exists(hostFxrRoot))
{
ShouldIncludePredicate = static (ref FileSystemEntry entry) => entry.IsDirectory
};
var fileEnumerable = new FileSystemEnumerable<SemanticVersion?>(
directory: hostFxrRoot,
transform: static (ref FileSystemEntry entry) => SemanticVersionParser.TryParse(entry.FileName.ToString(), out var version) ? version : null)
{
ShouldIncludePredicate = static (ref FileSystemEntry entry) => entry.IsDirectory
};

// Load hostfxr from the highest version, because it should be backward-compatible
if (fileEnumerable.Max() is SemanticVersion hostFxrVersion)
{
string hostFxrAssembly = Path.Combine(hostFxrRoot, hostFxrVersion.OriginalValue, hostFxrLibName);
return NativeLibrary.Load(hostFxrAssembly);
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);
if (NativeLibrary.TryLoad(hostFxrAssembly, out IntPtr handle))
{
return handle;
}
}
}
}

Expand All @@ -176,68 +185,69 @@ private static IntPtr HostFxrResolver(Assembly assembly, string libraryName)
private static string? GetSdkFromGlobalSettings(string workingDirectory)
{
string? resolvedSdk = null;
int rc = NativeMethods.hostfxr_resolve_sdk2(exe_dir: DotnetPath.Value, working_dir: workingDirectory, flags: 0, result: (key, value) =>
foreach (string dotnetPath in s_dotnetPathCandidates.Value)
{
if (key == NativeMethods.hostfxr_resolve_sdk2_result_key_t.resolved_sdk_dir)
int rc = NativeMethods.hostfxr_resolve_sdk2(exe_dir: dotnetPath, working_dir: workingDirectory, flags: 0, result: (key, value) =>
{
resolvedSdk = value;
}
});
if (key == NativeMethods.hostfxr_resolve_sdk2_result_key_t.resolved_sdk_dir)
{
resolvedSdk = value;
}
});

if (rc != 0)
{
throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_resolve_sdk2)));
if (rc == 0)
{
SetEnvironmentVariableIfEmpty("DOTNET_HOST_PATH", Path.Combine(dotnetPath, ExeName));
return resolvedSdk;
}
}

return resolvedSdk;
return string.IsNullOrEmpty(resolvedSdk)
? throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_resolve_sdk2)))
: resolvedSdk;
}

private static string ResolveDotnetPath()
private static IList<string> ResolveDotnetPathCandidates()
{
string? dotnetPath = GetDotnetPathFromROOT();
var pathCandidates = new List<string>();
AddIfValid(GetDotnetPathFromROOT());

string? dotnetExePath = GetCurrentProcessPath();
bool isRunFromDotnetExecutable = !string.IsNullOrEmpty(dotnetExePath)
&& Path.GetFileName(dotnetExePath).Equals(ExeName, StringComparison.InvariantCultureIgnoreCase);

if (isRunFromDotnetExecutable)
{
AddIfValid(Path.GetDirectoryName(dotnetExePath));
}

if (string.IsNullOrEmpty(dotnetPath))
string? hostPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH");
if (!string.IsNullOrEmpty(hostPath) && File.Exists(hostPath))
{
string? dotnetExePath = GetCurrentProcessPath();
var isRunFromDotnetExecutable = !string.IsNullOrEmpty(dotnetExePath)
&& Path.GetFileName(dotnetExePath).Equals(ExeName, StringComparison.OrdinalIgnoreCase);

if (isRunFromDotnetExecutable)
if (!IsWindows)
{
dotnetPath = Path.GetDirectoryName(dotnetExePath);
hostPath = realpath(hostPath) ?? hostPath;
}
else
{
// DOTNET_HOST_PATH is pointing to the file, DOTNET_ROOT is the path of the folder
string? hostPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH");
if (!string.IsNullOrEmpty(hostPath) && File.Exists(hostPath))
{
if (!IsWindows)
{
hostPath = realpath(hostPath) ?? hostPath;
}

dotnetPath = Path.GetDirectoryName(hostPath);
if (dotnetPath is not null)
{
// don't overwrite DOTNET_HOST_PATH, if we use it.
return dotnetPath;
}
}

dotnetPath = FindDotnetPathFromEnvVariable("DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR")
?? GetDotnetPathFromPATH();
}
AddIfValid(Path.GetDirectoryName(hostPath));
}

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

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

return dotnetPath;
void AddIfValid(string? path)
{
if (!string.IsNullOrEmpty(path))
{
pathCandidates.Add(path);
}
}
}

private static string? GetDotnetPathFromROOT()
Expand Down Expand Up @@ -280,15 +290,18 @@ private static string ResolveDotnetPath()
private static string[] GetAllAvailableSDKs()
{
string[]? resolvedPaths = null;
int rc = NativeMethods.hostfxr_get_available_sdks(exe_dir: DotnetPath.Value, result: (key, value) => resolvedPaths = value);

// Errors are automatically printed to stderr. We should not continue to try to output anything if we failed.
if (rc != 0)
foreach (string dotnetPath in s_dotnetPathCandidates.Value)
{
throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_get_available_sdks)));
int rc = NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, result: (key, value) => resolvedPaths = value);

if (rc == 0 && resolvedPaths != null && resolvedPaths.Length > 0)
{
break;
}
}

return resolvedPaths ?? Array.Empty<string>();
// 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)));
}

/// <summary>
Expand Down
3 changes: 3 additions & 0 deletions src/MSBuildLocator/Microsoft.Build.Locator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@

<Title>MSBuild Locator</Title>
<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>

<EnablePackageValidation>true</EnablePackageValidation>
<PackageValidationBaselineVersion>1.6.1</PackageValidationBaselineVersion>
</PropertyGroup>

<PropertyGroup Condition="'$(TargetFramework)'=='net46'">
Expand Down
3 changes: 1 addition & 2 deletions src/MSBuildLocator/build/Microsoft.Build.Locator.targets
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
<ItemGroup>
<MSBuildPackagesWithoutPrivateAssets
Include="@(PackageReference)"
Condition="
'%(PackageReference.ExcludeAssets)' != 'runtime' and
Condition="!$([MSBuild]::ValueOrDefault('%(PackageReference.ExcludeAssets)', '').ToLower().Contains('runtime')) and
(
'%(PackageReference.Identity)' == 'Microsoft.Build' or
'%(PackageReference.Identity)' == 'Microsoft.Build.Framework' or
Expand Down

0 comments on commit e0281df

Please sign in to comment.