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
172 changes: 172 additions & 0 deletions src/Tasks/Common/AbsolutePath.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

// This is a polyfill for the AbsolutePath struct from MSBuild.
// See: https://github.com/dotnet/msbuild/blob/main/src/Framework/PathHelpers/AbsolutePath.cs

#if NETFRAMEWORK

#nullable enable

using System;
using System.IO;
using System.Runtime.InteropServices;

namespace Microsoft.Build.Framework
{
/// <summary>
/// Represents an absolute file system path.
/// </summary>
public readonly struct AbsolutePath : IEquatable<AbsolutePath>
{
private static readonly bool s_isFileSystemCaseSensitive = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
&& !RuntimeInformation.IsOSPlatform(OSPlatform.OSX);

private static readonly StringComparer s_pathComparer = s_isFileSystemCaseSensitive
? StringComparer.Ordinal
: StringComparer.OrdinalIgnoreCase;

/// <summary>
/// The normalized string representation of this path.
/// </summary>
public string Value { get; }

/// <summary>
/// The original string used to create this path.
/// </summary>
public string OriginalValue { get; }

/// <summary>
/// Initializes a new instance of the <see cref="AbsolutePath"/> struct.
/// </summary>
public AbsolutePath(string path)
{
ValidatePath(path);
Value = path;
OriginalValue = path;
}

/// <summary>
/// Initializes a new instance of the <see cref="AbsolutePath"/> struct.
/// </summary>
internal AbsolutePath(string path, bool ignoreRootedCheck)
: this(path, path, ignoreRootedCheck)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="AbsolutePath"/> struct.
/// </summary>
internal AbsolutePath(string path, string original, bool ignoreRootedCheck)
{
if (!ignoreRootedCheck)
{
ValidatePath(path);
}
Value = path;
OriginalValue = original;
}

private static void ValidatePath(string path)
{
if (string.IsNullOrEmpty(path))
{
throw new ArgumentException("Path must not be null or empty.", nameof(path));
}

if (!IsPathFullyQualified(path))
{
throw new ArgumentException("Path must be rooted.", nameof(path));
}
}

private static bool IsPathFullyQualified(string path)
{
if (!Path.IsPathRooted(path))
{
return false;
}

if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// On non-Windows, a rooted path is fully qualified.
return true;
}

// Windows: drive-rooted paths like "C:\foo" are fully qualified.
if (path.Length >= 3 && path[1] == ':')
{
char separator = path[2];
return separator == Path.DirectorySeparatorChar || separator == Path.AltDirectorySeparatorChar;
}

// UNC/extended paths like "\\server\share" or "\\?\C:\foo" are fully qualified.
if (path.Length >= 2 && IsDirectorySeparator(path[0]) && IsDirectorySeparator(path[1]))
{
return true;
}

// Rooted with single leading separator (e.g., "\foo") is drive-relative on Windows.
return false;
}

private static bool IsDirectorySeparator(char c)
{
return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar;
}

/// <summary>
/// Initializes a new instance by combining an absolute base path with a relative path.
/// </summary>
public AbsolutePath(string path, AbsolutePath basePath)
{
if (string.IsNullOrEmpty(path))
{
throw new ArgumentException("Path must not be null or empty.", nameof(path));
}

Value = Path.Combine(basePath.Value, path);
OriginalValue = path;
}

/// <summary>
/// Implicitly converts an AbsolutePath to a string.
/// </summary>
public static implicit operator string(AbsolutePath path) => path.Value;

/// <summary>
/// Returns the canonical form of this path.
/// </summary>
internal AbsolutePath GetCanonicalForm()
{
if (string.IsNullOrEmpty(Value))
{
return this;
}

bool hasRelativeSegment = Value.Contains("/.") || Value.Contains("\\.");
bool needsSeparatorNormalization = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
&& Value.IndexOf(Path.AltDirectorySeparatorChar) >= 0;

if (!hasRelativeSegment && !needsSeparatorNormalization)
{
return this;
}

return new AbsolutePath(Path.GetFullPath(Value), OriginalValue, ignoreRootedCheck: true);
}

public static bool operator ==(AbsolutePath left, AbsolutePath right) => left.Equals(right);
public static bool operator !=(AbsolutePath left, AbsolutePath right) => !left.Equals(right);

public override bool Equals(object? obj) => obj is AbsolutePath other && Equals(other);

public bool Equals(AbsolutePath other) => s_pathComparer.Equals(Value, other.Value);

public override int GetHashCode() => Value is null ? 0 : s_pathComparer.GetHashCode(Value);

public override string ToString() => Value;
}
}

#endif
33 changes: 33 additions & 0 deletions src/Tasks/Common/IMultiThreadableTask.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

// This is a polyfill for the IMultiThreadableTask interface from MSBuild.
// MSBuild detects this interface by its namespace and name only, ignoring the defining assembly.
// This allows us to use the interface before the MSBuild version containing it is available.
// See: https://github.com/dotnet/msbuild/blob/main/src/Framework/IMultiThreadableTask.cs

// This polyfill is only needed for .NET Framework builds since the newer MSBuild packages
// for .NET Core already include the interface. When targeting .NET Core, we use the
// interface from Microsoft.Build.Framework directly.

#if NETFRAMEWORK

#nullable enable

namespace Microsoft.Build.Framework
{
/// <summary>
/// Interface for tasks that can execute in a thread-safe manner within MSBuild's multithreaded execution model.
/// Tasks that implement this interface declare their capability to run in multiple threads within one process.
/// </summary>
internal interface IMultiThreadableTask : ITask
{
/// <summary>
/// Gets or sets the task execution environment, which provides access to project current directory
/// and environment variables in a thread-safe manner.
/// </summary>
TaskEnvironment TaskEnvironment { get; set; }
}
}

#endif
59 changes: 59 additions & 0 deletions src/Tasks/Common/ITaskEnvironmentDriver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

// This is a polyfill for the ITaskEnvironmentDriver interface from MSBuild.
// See: https://github.com/dotnet/msbuild/blob/main/src/Framework/ITaskEnvironmentDriver.cs

#if NETFRAMEWORK

#nullable enable

using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace Microsoft.Build.Framework
{
/// <summary>
/// Internal interface for managing task execution environment, including environment variables and working directory.
/// </summary>
internal interface ITaskEnvironmentDriver : IDisposable
{
/// <summary>
/// Gets or sets the current working directory for the task environment.
/// </summary>
AbsolutePath ProjectDirectory { get; set; }

/// <summary>
/// Gets an absolute path from the specified path, resolving relative paths against the current project directory.
/// </summary>
AbsolutePath GetAbsolutePath(string path);

/// <summary>
/// Gets the value of the specified environment variable.
/// </summary>
string? GetEnvironmentVariable(string name);

/// <summary>
/// Gets all environment variables for this task environment.
/// </summary>
IReadOnlyDictionary<string, string> GetEnvironmentVariables();

/// <summary>
/// Sets an environment variable to the specified value.
/// </summary>
void SetEnvironmentVariable(string name, string? value);

/// <summary>
/// Sets the environment to match the specified collection of variables.
/// </summary>
void SetEnvironment(IDictionary<string, string> newEnvironment);

/// <summary>
/// Gets a ProcessStartInfo configured with the current environment and working directory.
/// </summary>
ProcessStartInfo GetProcessStartInfo();
}
}

#endif
125 changes: 125 additions & 0 deletions src/Tasks/Common/ProcessTaskEnvironmentDriver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

// This is a polyfill for the MultiProcessTaskEnvironmentDriver from MSBuild.
// Adapted for use in the SDK tasks project where NativeMethodsShared is not available.
// See: https://github.com/dotnet/msbuild/blob/main/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs

#if NETFRAMEWORK

#nullable enable

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;

namespace Microsoft.Build.Framework
{
/// <summary>
/// Default implementation of <see cref="ITaskEnvironmentDriver"/> that directly interacts with the file system
/// and environment variables. Used for multi-process mode and as a test helper.
/// </summary>
internal sealed class ProcessTaskEnvironmentDriver : ITaskEnvironmentDriver
{
private AbsolutePath _projectDirectory;
private readonly Dictionary<string, string> _environmentVariables;

/// <summary>
/// Initializes a new instance with the specified project directory.
/// </summary>
public ProcessTaskEnvironmentDriver(string projectDirectory)
{
_projectDirectory = new AbsolutePath(projectDirectory);

// Seed from the current process environment
_environmentVariables = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (DictionaryEntry entry in Environment.GetEnvironmentVariables())
{
if (entry.Key is string key && entry.Value is string value)
{
_environmentVariables[key] = value;
}
}
}

/// <inheritdoc/>
public AbsolutePath ProjectDirectory
{
get => _projectDirectory;
set => _projectDirectory = value;
}

/// <inheritdoc/>
public AbsolutePath GetAbsolutePath(string path)
{
if (Path.IsPathRooted(path))
{
return new AbsolutePath(path);
}

return new AbsolutePath(path, _projectDirectory);
}

/// <inheritdoc/>
public string? GetEnvironmentVariable(string name)
{
return _environmentVariables.TryGetValue(name, out var value) ? value : null;
}

/// <inheritdoc/>
public IReadOnlyDictionary<string, string> GetEnvironmentVariables()
{
return new Dictionary<string, string>(_environmentVariables, StringComparer.OrdinalIgnoreCase);
}

/// <inheritdoc/>
public void SetEnvironmentVariable(string name, string? value)
{
if (value == null)
{
_environmentVariables.Remove(name);
}
else
{
_environmentVariables[name] = value;
}
}

/// <inheritdoc/>
public void SetEnvironment(IDictionary<string, string> newEnvironment)
{
_environmentVariables.Clear();
foreach (var kvp in newEnvironment)
{
_environmentVariables[kvp.Key] = kvp.Value;
}
}

/// <inheritdoc/>
public ProcessStartInfo GetProcessStartInfo()
{
var startInfo = new ProcessStartInfo
{
WorkingDirectory = _projectDirectory.Value,
};

// Populate environment from the scoped environment dictionary
foreach (var kvp in _environmentVariables)
{
startInfo.EnvironmentVariables[kvp.Key] = kvp.Value;
}

return startInfo;
}

/// <inheritdoc/>
public void Dispose()
{
// No resources to clean up in this implementation.
}
}
}

#endif
Loading
Loading