Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
<Link>TestData\GlobbingTestData.cs</Link>
</Compile>
<Compile Include="..\Shared\ProcessExtensions.cs" />
<Compile Include="..\UnitTests.Shared\EnvironmentProvider.cs" />
<Compile Include="..\UnitTests.Shared\RunnerUtilities.cs" />
<None Include="..\Shared\UnitTests\App.config">
<Link>App.config</Link>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
<Compile Include="..\Shared\UnitTests\ResourceUtilities_Tests.cs">
<ExcludeFromStyleCop>true</ExcludeFromStyleCop>
</Compile>
<Compile Include="..\UnitTests.Shared\EnvironmentProvider.cs" />
<Compile Include="..\UnitTests.Shared\RunnerUtilities.cs">
<ExcludeFromStyleCop>true</ExcludeFromStyleCop>
</Compile>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
<Compile Include="..\Shared\UnitTests\MockEngine.cs" />
<Compile Include="..\Shared\UnitTests\MockLogger.cs" />
<Compile Include="..\Shared\UnitTests\ObjectModelHelpers.cs" />
<Compile Include="..\UnitTests.Shared\EnvironmentProvider.cs" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: just see there is Shared\UnitTests and UnitTests.Shared: what is the difference here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch!

For historical reasons, there is a bunch of code in MSBuild that is compiled into multiple assemblies (sometimes with slightly different ifdefs). That includes test assemblies. However, the reasons for doing this in non-test assemblies (they aren't great reasons but they are reasons) don't really apply for test, so we introduced a shared assembly for test stuff to build that sort of thing once--but didn't move everything.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it looks like shortly after introducing that assembly, we dropped all references to it (in ~2017).

It's still a good idea.

<Compile Include="..\UnitTests.Shared\RunnerUtilities.cs" />

<EmbeddedResource Include="..\MSBuild\MSBuild\Microsoft.Build.Core.xsd">
Expand Down
1 change: 1 addition & 0 deletions src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
<Link>TestEnvironment.cs</Link>
</Compile>
<Compile Include="..\Shared\ProcessExtensions.cs" />
<Compile Include="..\UnitTests.Shared\EnvironmentProvider.cs" />
<Compile Include="..\UnitTests.Shared\RunnerUtilities.cs" />

<EmbeddedResource Include="SampleResx" />
Expand Down
138 changes: 138 additions & 0 deletions src/UnitTests.Shared/EnvironmentProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
//

using System;
using System.Collections.Generic;
#if !NET6_0_OR_GREATER
using System.Diagnostics;
#endif
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;

namespace Microsoft.Build.UnitTests.Shared
{
public class EnvironmentProvider
{
private static class Constants
{
public const string DotNet = "dotnet";
public const string Path = "PATH";
public const string DotnetMsbuildSdkResolverCliDir = "DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR";
public static readonly bool RunningOnWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
public static readonly string ExeSuffix = RunningOnWindows ? ".exe" : string.Empty;
}

private IEnumerable<string>? _searchPaths;

private readonly Func<string, string?> _getEnvironmentVariable;
private readonly Func<string?> _getCurrentProcessPath;

public EnvironmentProvider(Func<string, string?> getEnvironmentVariable)
: this(getEnvironmentVariable, GetCurrentProcessPath)
{ }

public EnvironmentProvider(Func<string, string?> getEnvironmentVariable, Func<string?> getCurrentProcessPath)
{
_getEnvironmentVariable = getEnvironmentVariable;
_getCurrentProcessPath = getCurrentProcessPath;
}

private IEnumerable<string> SearchPaths
{
get
{
if (_searchPaths == null)
{
var searchPaths = new List<string>();

searchPaths.AddRange(
(_getEnvironmentVariable(Constants.Path) ?? string.Empty)
.Split(new char[] { Path.PathSeparator }, options: StringSplitOptions.RemoveEmptyEntries)
.Select(p => p.Trim('"')));

_searchPaths = searchPaths;
}

return _searchPaths;
}
}

public string? GetCommandPath(string commandName)
{
var commandNameWithExtension = commandName + Constants.ExeSuffix;
var commandPath = SearchPaths
.Where(p => !Path.GetInvalidPathChars().Any(p.Contains))
.Select(p => Path.Combine(p, commandNameWithExtension))
.FirstOrDefault(File.Exists);

return commandPath;
}

public string? GetDotnetExePath()
{
string? environmentOverride = _getEnvironmentVariable(Constants.DotnetMsbuildSdkResolverCliDir);
if (!string.IsNullOrEmpty(environmentOverride))
{
return Path.Combine(environmentOverride, Constants.DotNet + Constants.ExeSuffix);
}

string? dotnetExe = _getCurrentProcessPath();

if (string.IsNullOrEmpty(dotnetExe) || !Path.GetFileNameWithoutExtension(dotnetExe)
.Equals(Constants.DotNet, StringComparison.InvariantCultureIgnoreCase))
{
string? dotnetExeFromPath = GetCommandPath(Constants.DotNet);
#if NET
if (dotnetExeFromPath != null && !Constants.RunningOnWindows)
{
// on Linux the 'dotnet' command from PATH is a symlink so we need to
// resolve it to get the actual path to the binary
FileInfo fi = new FileInfo(dotnetExeFromPath);
while (fi.LinkTarget != null)
{
dotnetExeFromPath = fi.LinkTarget;
fi = new FileInfo(dotnetExeFromPath);
}
}
#endif
if (!string.IsNullOrWhiteSpace(dotnetExeFromPath))
{
dotnetExe = dotnetExeFromPath;
}
}

return dotnetExe;
}

public static string? GetDotnetExePath(Func<string, string?>? getEnvironmentVariable = null)
{
if (getEnvironmentVariable == null)
{
getEnvironmentVariable = Environment.GetEnvironmentVariable;
}
var environmentProvider = new EnvironmentProvider(getEnvironmentVariable);
return environmentProvider.GetDotnetExePath();
}

public static string? GetDotnetExePath(Func<string, string?> getEnvironmentVariable, Func<string?> getCurrentProcessPath)
{
getEnvironmentVariable ??= Environment.GetEnvironmentVariable;
getCurrentProcessPath ??= GetCurrentProcessPath;
var environmentProvider = new EnvironmentProvider(getEnvironmentVariable, getCurrentProcessPath);
return environmentProvider.GetDotnetExePath();
}

private static string? GetCurrentProcessPath()
{
string? currentProcessPath;
#if NET6_0_OR_GREATER
currentProcessPath = Environment.ProcessPath;
#else
currentProcessPath = Process.GetCurrentProcess().MainModule.FileName;
#endif
return currentProcessPath;
}
}
}
19 changes: 4 additions & 15 deletions src/UnitTests.Shared/RunnerUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ namespace Microsoft.Build.UnitTests.Shared
public static class RunnerUtilities
{
public static string PathToCurrentlyRunningMsBuildExe => BuildEnvironmentHelper.Instance.CurrentMSBuildExePath;
#if !FEATURE_RUN_EXE_IN_TESTS
private static readonly string s_dotnetExePath = EnvironmentProvider.GetDotnetExePath();
#endif

/// <summary>
/// Invoke the currently running msbuild and return the stdout, stderr, and process exit status.
Expand All @@ -29,7 +32,7 @@ public static string ExecMSBuild(string pathToMsBuildExe, string msbuildParamete
#if FEATURE_RUN_EXE_IN_TESTS
var pathToExecutable = pathToMsBuildExe;
#else
var pathToExecutable = ResolveRuntimeExecutableName();
var pathToExecutable = s_dotnetExePath;
msbuildParameters = FileUtilities.EnsureDoubleQuotes(pathToMsBuildExe) + " " + msbuildParameters;
#endif

Expand All @@ -52,20 +55,6 @@ private static void AdjustForShellExecution(ref string pathToExecutable, ref str
}
}

#if !FEATURE_RUN_EXE_IN_TESTS
/// <summary>
/// Resolve the platform specific path to the runtime executable that msbuild.exe needs to be run in (unix-mono, {unix, windows}-corerun).
/// </summary>
private static string ResolveRuntimeExecutableName()
{
// Run the child process with the same host as the currently-running process.
using (Process currentProcess = Process.GetCurrentProcess())
{
return currentProcess.MainModule.FileName;
}
}
#endif

/// <summary>
/// Run the process and get stdout and stderr
/// </summary>
Expand Down