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 @@ -3,11 +3,69 @@

using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using System;
using System.Reflection;

#nullable disable

namespace AssemblyLoadContextTest
{
/// <summary>
/// Task that validates assembly version roll-forward behavior.
/// Tests that MSBuildLoadContext accepts newer assembly versions when older versions are requested.
/// </summary>
public class ValidateAssemblyVersionRollForward : Task
{
/// <summary>
/// The name of the assembly to check (e.g., "System.Collections.Immutable")
/// </summary>
[Required]
public string AssemblyName { get; set; }

/// <summary>
/// The minimum expected version (e.g., "1.0.0.0")
/// </summary>
[Required]
public string MinimumVersion { get; set; }

public override bool Execute()
{
try
{
// Try to load the assembly by name with minimum version
var minimumVersion = Version.Parse(MinimumVersion);
var assemblyName = new AssemblyName(AssemblyName)
{
Version = minimumVersion
};

// This will trigger MSBuildLoadContext.Load which should accept newer versions
var assembly = Assembly.Load(assemblyName);
var loadedVersion = assembly.GetName().Version;

Log.LogMessage(MessageImportance.High,
$"Requested {AssemblyName} version {minimumVersion}, loaded version {loadedVersion}");

// Verify that we got a version >= minimum
if (loadedVersion < minimumVersion)
{
Log.LogError(
$"Assembly version roll-forward failed: requested {minimumVersion}, but loaded {loadedVersion} which is older");
return false;
}

Log.LogMessage(MessageImportance.High,
$"Assembly version roll-forward succeeded: loaded version {loadedVersion} >= requested {minimumVersion}");
return true;
}
catch (Exception ex)
{
Log.LogErrorFromException(ex, showStackTrace: true);
return false;
}
}
}

public class RegisterObject : Task
{
internal const string CacheKey = "RegressionForMSBuild#5080";
Expand Down
27 changes: 27 additions & 0 deletions src/msbuild/src/Build.UnitTests/BackEnd/TaskBuilder_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,33 @@ public void SameAssemblyFromDifferentRelativePathsSharesAssemblyLoadContext()
logger.AssertLogDoesntContain("MSB4018");
}

#if FEATURE_ASSEMBLYLOADCONTEXT
/// <summary>
/// Regression test for https://github.com/dotnet/msbuild/issues/12370
/// Verifies that MSBuildLoadContext accepts newer assembly versions when older versions are requested (version roll-forward).
/// </summary>
[Fact]
public void MSBuildLoadContext_AcceptsNewerAssemblyVersions()
{
string realTaskPath = Assembly.GetExecutingAssembly().Location;

// Use System.Collections.Immutable as test assembly - it's available in modern .NET runtime
// Request an older version (1.0.0.0) which should roll forward to whatever version is available
string projectContents = @"<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
<UsingTask TaskName=`ValidateAssemblyVersionRollForward` AssemblyFile=`" + realTaskPath + @"` />

<Target Name=`Build`>
<ValidateAssemblyVersionRollForward AssemblyName=`System.Collections.Immutable` MinimumVersion=`1.0.0.0` />
</Target>
</Project>";

MockLogger logger = ObjectModelHelpers.BuildProjectExpectSuccess(projectContents, _testOutput);

// Verify that the task logged success message
logger.AssertLogContains("Assembly version roll-forward succeeded");
}
#endif


#if FEATURE_CODETASKFACTORY
/// <summary>
Expand Down
49 changes: 0 additions & 49 deletions src/msbuild/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -384,54 +384,5 @@ public void TaskEnvironment_GetAbsolutePath_WithInvalidPathChars_ShouldNotThrow(
DisposeTaskEnvironment(taskEnvironment);
}
}

[Theory]
[MemberData(nameof(EnvironmentTypes))]
public void TaskEnvironment_GetAbsolutePath_WithEmptyPath_ReturnsProjectDirectory(string environmentType)
{
var taskEnvironment = CreateTaskEnvironment(environmentType);

// Empty path should absolutize to project directory (Path.Combine behavior)
var absolutePath = taskEnvironment.GetAbsolutePath(string.Empty);

absolutePath.Value.ShouldBe(taskEnvironment.ProjectDirectory.Value);
absolutePath.OriginalValue.ShouldBe(string.Empty);
}

[Theory]
[MemberData(nameof(EnvironmentTypes))]
public void TaskEnvironment_GetAbsolutePath_WithNullPath_WhenWave18_4Disabled_ReturnsNullPath(string environmentType)
{
using TestEnvironment testEnv = TestEnvironment.Create();
ChangeWaves.ResetStateForTests();
testEnv.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_4.ToString());
BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly();

var taskEnvironment = CreateTaskEnvironment(environmentType);

// When Wave18_4 is disabled, null path returns as-is
var absolutePath = taskEnvironment.GetAbsolutePath(null!);

absolutePath.Value.ShouldBeNull();
absolutePath.OriginalValue.ShouldBeNull();

ChangeWaves.ResetStateForTests();
}

[Theory]
[MemberData(nameof(EnvironmentTypes))]
public void TaskEnvironment_GetAbsolutePath_WithNullPath_WhenWave18_4Enabled_Throws(string environmentType)
{
using TestEnvironment testEnv = TestEnvironment.Create();
ChangeWaves.ResetStateForTests();
BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly();

var taskEnvironment = CreateTaskEnvironment(environmentType);

// When Wave18_4 is enabled, null path should throw
Should.Throw<ArgumentNullException>(() => taskEnvironment.GetAbsolutePath(null!));

ChangeWaves.ResetStateForTests();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ public bool IsTraversal
{
if (!_isTraversalProject.HasValue)
{
#if NET471_OR_GREATER
#if FEATURE_MSIOREDIST
if (MemoryExtensions.Equals(Microsoft.IO.Path.GetFileName(ProjectFullPath.AsSpan()), "dirs.proj".AsSpan(), StringComparison.OrdinalIgnoreCase))
#else
if (MemoryExtensions.Equals(Path.GetFileName(ProjectFullPath.AsSpan()), "dirs.proj", StringComparison.OrdinalIgnoreCase))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,7 @@ public AbsolutePath ProjectDirectory
/// <inheritdoc/>
public AbsolutePath GetAbsolutePath(string path)
{
// Opt-out for null path when Wave18_4 is disabled - return null as-is.
if (!ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_4) && path is null)
{
return new AbsolutePath(path!, path!, ignoreRootedCheck: true);
}

return new AbsolutePath(path, basePath: ProjectDirectory);
return new AbsolutePath(path, ProjectDirectory);
}

/// <inheritdoc/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,6 @@ public AbsolutePath ProjectDirectory
/// <inheritdoc/>
public AbsolutePath GetAbsolutePath(string path)
{
// Opt-out for null path when Wave18_4 is disabled - return null as-is.
if (!ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_4) && path is null)
{
return new AbsolutePath(path!, path!, ignoreRootedCheck: true);
}

return new AbsolutePath(path, ProjectDirectory);
}

Expand Down
5 changes: 2 additions & 3 deletions src/msbuild/src/Build/BuildCheck/Checks/DoubleWritesCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@

using System;
using System.Collections.Generic;
#if !FEATURE_MSIOREDIST
using System.IO;
#endif
using System.Linq;
using Microsoft.Build.Shared;
using static Microsoft.Build.Experimental.BuildCheck.TaskInvocationCheckData;

#if FEATURE_MSIOREDIST
using Path = Microsoft.IO.Path;
#else
using System.IO;
#endif

namespace Microsoft.Build.Experimental.BuildCheck.Checks;
Expand Down
5 changes: 2 additions & 3 deletions src/msbuild/src/Build/BuildCheck/Checks/ExecCliBuildCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@

using System;
using System.Collections.Generic;
#if !FEATURE_MSIOREDIST
using System.IO;
#endif
using Microsoft.Build.Shared;

#if FEATURE_MSIOREDIST
using Path = Microsoft.IO.Path;
#else
using System.IO;
#endif

namespace Microsoft.Build.Experimental.BuildCheck.Checks;
Expand Down
9 changes: 5 additions & 4 deletions src/msbuild/src/Build/Evaluation/Expander.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
#if NET
using System.IO;
#else
using Microsoft.IO;
#endif
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
Expand All @@ -34,6 +30,11 @@
using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem;
using TaskItemFactory = Microsoft.Build.Execution.ProjectItemInstance.TaskItem.TaskItemFactory;

#if FEATURE_MSIOREDIST
using Directory = Microsoft.IO.Directory;
using Path = Microsoft.IO.Path;
#endif

#nullable disable

namespace Microsoft.Build.Evaluation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
using System;
using System.Globalization;

#if NETFRAMEWORK
#if FEATURE_MSIOREDIST
using Microsoft.IO;
#endif

Expand Down
3 changes: 2 additions & 1 deletion src/msbuild/src/Build/Instance/ProjectItemInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,8 @@ private void CommonConstructor(
if (itemDefinitions == null || !useItemDefinitionsWithoutModification)
{
// TaskItems don't have an item type. So for their benefit, we have to lookup and add the regular item definition.
inheritedItemDefinitions = (itemDefinitions == null) ? null : new List<ProjectItemDefinitionInstance>(itemDefinitions);
inheritedItemDefinitions = (itemDefinitions == null) ? null : new List<ProjectItemDefinitionInstance>(itemDefinitions.Count + 1);
((List<ProjectItemDefinitionInstance>)inheritedItemDefinitions)?.AddRange(itemDefinitions);

ProjectItemDefinitionInstance itemDefinition;
if (projectToUse.ItemDefinitions.TryGetValue(itemTypeToUse, out itemDefinition))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
using Microsoft.Build.BackEnd.Components.RequestBuilder;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
#if NETFRAMEWORK
using Microsoft.IO;

#if FEATURE_MSIOREDIST
using Path = Microsoft.IO.Path;
#else
using System.IO;
#endif
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
using System.Buffers;
#endif

#if NETFRAMEWORK
using Microsoft.IO;
#if FEATURE_MSIOREDIST
using Path = Microsoft.IO.Path;
#else
using System.IO;
#endif
Expand Down
11 changes: 6 additions & 5 deletions src/msbuild/src/Build/Utilities/Utilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
#if NET
using System.IO;
#else
using Microsoft.IO;
#endif
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
Expand All @@ -22,6 +17,12 @@
using Toolset = Microsoft.Build.Evaluation.Toolset;
using XmlElementWithLocation = Microsoft.Build.Construction.XmlElementWithLocation;

#if FEATURE_MSIOREDIST
using Path = Microsoft.IO.Path;
#else
using System.IO;
#endif

#nullable disable

namespace Microsoft.Build.Internal
Expand Down
26 changes: 25 additions & 1 deletion src/msbuild/src/Framework.UnitTests/AbsolutePath_Tests.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
// 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.IO;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Shouldly;
using Xunit;

namespace Microsoft.Build.UnitTests
{
public class AbsolutePath_Tests
{
private static AbsolutePath GetTestBasePath()
{
string baseDirectory = Path.Combine(Path.GetTempPath(), "abspath_test_base");
return new AbsolutePath(baseDirectory, ignoreRootedCheck: false);
}

private static void ValidatePathAcceptance(string path, bool shouldBeAccepted)
{
if (shouldBeAccepted)
Expand All @@ -36,11 +44,27 @@ public void AbsolutePath_FromAbsolutePath_ShouldPreservePath()
Path.IsPathRooted(absolutePath.Value).ShouldBeTrue();
}

[Theory]
[InlineData(null)]
[InlineData("")]
public void AbsolutePath_NullOrEmpty_ShouldThrow(string? path)
{
Should.Throw<ArgumentException>(() => new AbsolutePath(path!));
}

[Theory]
[InlineData(null)]
[InlineData("")]
public void AbsolutePath_NullOrEmptyWithBasePath_ShouldThrow(string? path)
{
var basePath = GetTestBasePath();
Should.Throw<ArgumentException>(() => new AbsolutePath(path!, basePath));
}

[Theory]
[InlineData("subfolder")]
[InlineData("deep/nested/path")]
[InlineData(".")]
[InlineData("")]
[InlineData("..")]
public void AbsolutePath_FromRelativePath_ShouldResolveAgainstBase(string relativePath)
{
Expand Down
Loading