Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release DOTNET_ROOT handling changes and packages warning presence condition #247

Merged
merged 8 commits into from
Sep 20, 2023
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