Skip to content
Open
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
1 change: 1 addition & 0 deletions documentation/wiki/ChangeWaves.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Change wave checks around features will be removed in the release that accompani
## Current Rotation of Change Waves

### 18.8
- Use the Unix logical current directory from `PWD` when resolving relative project paths, so builds under symlinked directories produce logical project full paths and related output paths. Set `MSBUILDDISABLEFEATURESFROMVERSION=18.8` to opt out.
- [RAR task: across multiple input properties, resolve relative paths against the project directory (not the process current directory)](https://github.com/dotnet/msbuild/pull/13319)
- [Console, parallel console, and terminal loggers print the paths of log files written by registered loggers (e.g. file logger and binary logger) as part of the end-of-build summary.](https://github.com/dotnet/msbuild/pull/13577)

Expand Down
43 changes: 43 additions & 0 deletions src/Framework/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1325,6 +1325,49 @@ internal static void RestoreConsoleMode(uint? originalConsoleMode, bool useStand
[DllImport("libc", SetLastError = true)]
internal static extern int symlink(string oldpath, string newpath);

[DllImport("libc", EntryPoint = "realpath", SetLastError = true)]
private static extern IntPtr realpath_native(string path, IntPtr resolved);

[DllImport("libc", EntryPoint = "free")]
private static extern void libc_free(IntPtr ptr);

/// <summary>
/// Resolves <paramref name="path"/> to its canonical form via POSIX <c>realpath(3)</c>, following
/// symlinks. Returns <c>null</c> on Windows, on null/empty input, or when the call fails.
/// Unlike <see cref="System.IO.Path.GetFullPath(string)"/>, this resolves symlinks against the real
/// filesystem without mutating process-global state, so it is safe to call from any thread.
/// </summary>
internal static string RealPath(string path)
{
if (IsWindows || string.IsNullOrEmpty(path))
{
return null;
}

IntPtr ptr = IntPtr.Zero;
try
{
ptr = realpath_native(path, IntPtr.Zero);
if (ptr == IntPtr.Zero)
{
return null;
}
#if NET
return Marshal.PtrToStringUTF8(ptr);
#else
return Marshal.PtrToStringAnsi(ptr);
#endif
}
finally
{
if (ptr != IntPtr.Zero)
{
// realpath() with NULL second arg returns a malloc()'d buffer; caller must free().
libc_free(ptr);
}
}
}

#if FEATURE_WINDOWSINTEROP
[SupportedOSPlatform("windows6.1")]
internal static unsafe bool SetThreadErrorMode(int newMode, out int oldMode)
Expand Down
70 changes: 70 additions & 0 deletions src/MSBuild.UnitTests/XMake_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1362,6 +1362,34 @@ public void ResponseFileInProjectDirectoryFoundImplicitly()
output.ShouldContain("[A=1]");
}

[UnixOnlyFact]
public void ResponseFileInLogicalProjectDirectoryFoundImplicitly()
{
string root = _env.CreateFolder().Path;
string realDirectory = Path.Combine(root, "repo", "project");
string logicalParentDirectory = Path.Combine(root, "links");
string linkDirectory = Path.Combine(logicalParentDirectory, "project");
Directory.CreateDirectory(realDirectory);
Directory.CreateDirectory(logicalParentDirectory);

string errorMessage = null;
NativeMethodsShared.MakeSymbolicLink(linkDirectory, realDirectory, ref errorMessage).ShouldBeTrue(errorMessage);

string content = ObjectModelHelpers.CleanupFileContents("<Project ToolsVersion='msbuilddefaulttoolsversion' xmlns='msbuildnamespace'><Target Name='t'><Warning Text='[A=$(A)]'/></Target></Project>");
File.WriteAllText(Path.Combine(realDirectory, "my.proj"), content);
File.WriteAllText(Path.Combine(root, "repo", "Directory.Build.rsp"), "/p:A=physical");
File.WriteAllText(Path.Combine(logicalParentDirectory, "Directory.Build.rsp"), "/p:A=logical");

_env.SetCurrentDirectory(linkDirectory);
_env.SetEnvironmentVariable("PWD", linkDirectory);

string output = RunnerUtilities.ExecMSBuild("my.proj", out var successfulExit, _output);
successfulExit.ShouldBeTrue();

output.ShouldContain("[A=logical]");
output.ShouldNotContain("[A=physical]");
}

[Fact]
public void ResponseFileSwitchesAppearInCommandLine()
{
Expand Down Expand Up @@ -1915,6 +1943,48 @@ public void TestProcessProjectSwitchReplicateBuildingDFLKG()
MSBuildApp.ProcessProjectSwitch(Array.Empty<string>(), extensionsToIgnore, projectHelper.GetFiles).ShouldBe("test.proj"); // "Expected test.proj to be only project found"
}

[UnixOnlyFact]
public void ResolveProjectPathAgainstLogicalCurrentDirectoryPreservesSymlinkFromPwd()
{
string root = _env.CreateFolder().Path;
string realDirectory = Path.Combine(root, "real");
string linkDirectory = Path.Combine(root, "link");
Directory.CreateDirectory(realDirectory);
Comment thread
AlesProkop marked this conversation as resolved.

string errorMessage = null;
NativeMethodsShared.MakeSymbolicLink(linkDirectory, realDirectory, ref errorMessage).ShouldBeTrue(errorMessage);

_env.SetCurrentDirectory(linkDirectory);
_env.SetEnvironmentVariable("PWD", linkDirectory);

string relativeProjectPath = Path.Combine("MyApp", "MyApp.csproj");

MSBuildApp.ResolveProjectPathAgainstLogicalCurrentDirectory(relativeProjectPath)
.ShouldBe(Path.Combine(linkDirectory, relativeProjectPath));
}

[UnixOnlyFact]
public void ResolveProjectPathAgainstLogicalCurrentDirectoryCanBeDisabledByChangeWave()
{
string root = _env.CreateFolder().Path;
string realDirectory = Path.Combine(root, "real");
string linkDirectory = Path.Combine(root, "link");
Directory.CreateDirectory(realDirectory);

string errorMessage = null;
NativeMethodsShared.MakeSymbolicLink(linkDirectory, realDirectory, ref errorMessage).ShouldBeTrue(errorMessage);

_env.SetCurrentDirectory(linkDirectory);
_env.SetEnvironmentVariable("PWD", linkDirectory);
_env.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_8.ToString());
ChangeWaves.ResetStateForTests();

string relativeProjectPath = Path.Combine("MyApp", "MyApp.csproj");

MSBuildApp.ResolveProjectPathAgainstLogicalCurrentDirectory(relativeProjectPath)
.ShouldBe(relativeProjectPath);
}

/// <summary>
/// Test the case where we remove all of the project extensions that exist in the directory
/// </summary>
Expand Down
56 changes: 54 additions & 2 deletions src/MSBuild/XMake.cs
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,11 @@ private static bool CanRunServerBasedOnCommandLineSwitches(string[] commandLine)
{
commandLineSwitches = CombineSwitchesRespectingPriority(switchesFromAutoResponseFile, switchesNotFromAutoResponseFile, fullCommandLine);
}
string projectFile = ProcessProjectSwitch(commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.Project], commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.IgnoreProjectExtensions], Directory.GetFiles);
string projectFile = ResolveProjectPathAgainstLogicalCurrentDirectory(
ProcessProjectSwitch(
commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.Project],
commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.IgnoreProjectExtensions],
Directory.GetFiles));
Comment thread
AlesProkop marked this conversation as resolved.
if (commandLineSwitches[CommandLineSwitches.ParameterlessSwitch.Help] ||
commandLineSwitches.IsParameterizedSwitchSet(CommandLineSwitches.ParameterizedSwitch.NodeMode) ||
commandLineSwitches[CommandLineSwitches.ParameterlessSwitch.Version] ||
Expand Down Expand Up @@ -2225,7 +2229,11 @@ private static bool ProcessCommandLineSwitches(
commandLine);
}

projectFile = ProcessProjectSwitch(commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.Project], commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.IgnoreProjectExtensions], Directory.GetFiles);
projectFile = ResolveProjectPathAgainstLogicalCurrentDirectory(
ProcessProjectSwitch(
commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.Project],
Comment thread
AlesProkop marked this conversation as resolved.
commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.IgnoreProjectExtensions],
Directory.GetFiles));

// figure out which targets we are building
targets = ProcessTargetSwitch(commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.Target]);
Expand Down Expand Up @@ -3239,6 +3247,50 @@ internal static string ProcessProjectSwitch(
return projectFile;
}

/// <summary>
/// On Unix, rebases a relative project path onto <c>$PWD</c> when <c>$PWD</c> physically resolves
/// to the same directory as <see cref="Directory.GetCurrentDirectory"/>. This makes
/// <c>$(MSBuildProjectFullPath)</c> (and dependent output paths) stable whether the user reached
/// the project through a symlinked working directory or via the kernel-resolved absolute path.
/// </summary>
/// <remarks>
/// No-op on Windows, when <paramref name="projectFile"/> is absolute or contains <c>..</c>,
/// when <c>PWD</c> is unset/relative/stale, or when the change wave is disabled via
/// <c>MSBuildDisableFeaturesFromVersion=18.8</c>. Uses <see cref="NativeMethodsShared.RealPath"/>
/// (POSIX <c>realpath(3)</c>) so no process-global state is mutated; safe under MSBuild Server,
/// multithreaded MSBuild, and hosted scenarios.
/// </remarks>
internal static string ResolveProjectPathAgainstLogicalCurrentDirectory(string projectFile)
{
if (!ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_8)
|| NativeMethodsShared.IsWindows
|| string.IsNullOrEmpty(projectFile)
|| Path.IsPathRooted(projectFile)
|| projectFile.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar).Contains(".."))
{
// Bail on ".." segments: lexical normalization can escape the shared physical prefix,
// so the logical and physical resolutions can end up pointing at different files.
return projectFile;
}

string logicalCurrentDirectory = Environment.GetEnvironmentVariable("PWD");
if (string.IsNullOrEmpty(logicalCurrentDirectory) || !Path.IsPathRooted(logicalCurrentDirectory))
{
return projectFile;
}

// Confirm $PWD physically resolves to the same directory as getcwd(); otherwise PWD is stale.
string physicalPwd = NativeMethodsShared.RealPath(logicalCurrentDirectory);
string physicalCwd = NativeMethodsShared.RealPath(Directory.GetCurrentDirectory());
if (physicalPwd == null || physicalCwd == null
|| !string.Equals(physicalPwd, physicalCwd, StringComparison.Ordinal))
{
return projectFile;
}

return Path.GetFullPath(Path.Combine(logicalCurrentDirectory, projectFile));
}

private static void ValidateExtensions(string[] projectExtensionsToIgnore)
{
if (projectExtensionsToIgnore?.Length > 0)
Expand Down
Loading