diff --git a/MSBuild.slnx b/MSBuild.slnx
index d807edb08b4..674708cf91d 100644
--- a/MSBuild.slnx
+++ b/MSBuild.slnx
@@ -74,6 +74,9 @@
+
+
+
diff --git a/src/BuildCheck.UnitTests/EndToEndTests.cs b/src/BuildCheck.UnitTests/EndToEndTests.cs
index 646dc27984f..542a1a2aefb 100644
--- a/src/BuildCheck.UnitTests/EndToEndTests.cs
+++ b/src/BuildCheck.UnitTests/EndToEndTests.cs
@@ -9,6 +9,7 @@
using System.Text.RegularExpressions;
using System.Xml;
using Microsoft.Build.Experimental.BuildCheck;
+using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.UnitTests;
using Microsoft.Build.UnitTests.Shared;
@@ -160,7 +161,7 @@ private EmbedResourceTestOutput RunEmbeddedResourceTest(string resourceXmlToAdd,
const string templateToReplace = "###EmbeddedResourceToAdd";
TransientTestFolder workFolder = _env.CreateFolder(createFolder: true);
- CopyFilesRecursively(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path);
+ FileUtilities.CopyDirectory(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path);
ReplaceStringInFile(Path.Combine(workFolder.Path, referencedProjectName, $"{referencedProjectName}.csproj"),
templateToReplace, resourceXmlToAdd);
File.Copy(
@@ -196,21 +197,6 @@ void ReplaceStringInFile(string filePath, string original, string replacement)
}
}
- private static void CopyFilesRecursively(string sourcePath, string targetPath)
- {
- // First Create all directories
- foreach (string dirPath in Directory.GetDirectories(sourcePath, "*", SearchOption.AllDirectories))
- {
- Directory.CreateDirectory(dirPath.Replace(sourcePath, targetPath));
- }
-
- // Then copy all the files & Replaces any files with the same name
- foreach (string newPath in Directory.GetFiles(sourcePath, "*", SearchOption.AllDirectories))
- {
- File.Copy(newPath, newPath.Replace(sourcePath, targetPath), true);
- }
- }
-
private static int GetWarningsCount(string output)
{
Regex regex = new Regex(@"(\d+) Warning\(s\)");
@@ -270,7 +256,7 @@ public void CopyToOutputTest(bool skipUnchangedDuringCopy)
const string entryProjectName = "EntryProject";
TransientTestFolder workFolder = _env.CreateFolder(createFolder: true);
- CopyFilesRecursively(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path);
+ FileUtilities.CopyDirectory(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path);
_env.SetCurrentDirectory(Path.Combine(workFolder.Path, entryProjectName));
@@ -382,7 +368,7 @@ public void TFMConfusionCheckTest(string tfmString, string cliSuffix, bool shoul
const string templateToReplace = "###TFM";
TransientTestFolder workFolder = _env.CreateFolder(createFolder: true);
- CopyFilesRecursively(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path);
+ FileUtilities.CopyDirectory(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path);
ReplaceStringInFile(Path.Combine(workFolder.Path, $"{projectName}.csproj"),
templateToReplace, tfmString);
diff --git a/src/Framework/Properties/AssemblyInfo.cs b/src/Framework/Properties/AssemblyInfo.cs
index 7473cdc18aa..b05a7c23e88 100644
--- a/src/Framework/Properties/AssemblyInfo.cs
+++ b/src/Framework/Properties/AssemblyInfo.cs
@@ -52,6 +52,7 @@
[assembly: InternalsVisibleTo("Microsoft.Build.Engine.OM.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")]
[assembly: InternalsVisibleTo("Microsoft.Build.Utilities.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")]
[assembly: InternalsVisibleTo("Microsoft.Build.CommandLine.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")]
+[assembly: InternalsVisibleTo("Microsoft.Build.EndToEnd.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")]
[assembly: InternalsVisibleTo("Microsoft.Build.Tasks.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")]
[assembly: InternalsVisibleTo("Microsoft.Build.UnitTests.Shared, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")]
diff --git a/src/MSBuild.EndToEnd.Tests/Microsoft.Build.EndToEnd.Tests.csproj b/src/MSBuild.EndToEnd.Tests/Microsoft.Build.EndToEnd.Tests.csproj
new file mode 100644
index 00000000000..ba5daca9122
--- /dev/null
+++ b/src/MSBuild.EndToEnd.Tests/Microsoft.Build.EndToEnd.Tests.csproj
@@ -0,0 +1,31 @@
+
+
+
+ $(RuntimeOutputTargetFrameworks)
+ $(RuntimeOutputPlatformTarget)
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs b/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs
new file mode 100644
index 00000000000..53fde409436
--- /dev/null
+++ b/src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs
@@ -0,0 +1,190 @@
+// 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 System.Linq;
+using Microsoft.Build.Framework;
+using Microsoft.Build.UnitTests;
+using Microsoft.Build.UnitTests.Shared;
+using Shouldly;
+using Xunit;
+
+namespace Microsoft.Build.EndToEndTests
+{
+ ///
+ /// Tests for multithreaded MSBuild execution scenarios using test assets.
+ ///
+ public class MultithreadedExecution_Tests : IClassFixture, IDisposable
+ {
+ private readonly ITestOutputHelper _output;
+ private readonly TestEnvironment _env;
+ private readonly string _testAssetDir;
+
+ private readonly int _timeoutInMilliseconds = 180_000;
+
+ // Common parameters for all multithreaded tests:
+ // /nodereuse:false - Prevents MSBuild server processes from persisting between tests,
+ // ensuring proper test isolation and avoiding potential timeouts
+ // /v:minimal - Reduces log verbosity for cleaner test output and better performance
+ private const string CommonMSBuildArgs = "/nodereuse:false /v:minimal";
+
+ public MultithreadedExecution_Tests(ITestOutputHelper output, TestSolutionAssetsFixture testAssetFixture)
+ {
+ _output = output;
+ _env = TestEnvironment.Create(output);
+ _testAssetDir = testAssetFixture.TestAssetDir;
+ }
+
+ public void Dispose()
+ {
+ _env.Dispose();
+ }
+
+ ///
+ /// Prepares an isolated copy of test assets in a temporary directory for each test run.
+ /// This ensures fresh builds and proper test isolation.
+ ///
+ /// Test asset
+ /// TestSolutionAsset for the copied asset in a temporary folder.
+ private TestSolutionAsset PrepareIsolatedTestAssets(TestSolutionAsset testAsset)
+ {
+ string sourceAssetDir = Path.Combine(_testAssetDir, testAsset.SolutionFolder);
+
+ // Ensure source test asset exists
+ Directory.Exists(sourceAssetDir).ShouldBeTrue($"Test asset not found: {sourceAssetDir}.");
+
+ // Create isolated copy of entire test asset directory structure
+ TransientTestFolder workFolder = _env.CreateFolder(createFolder: true);
+
+ FileUtilities.CopyDirectory(sourceAssetDir, workFolder.Path);
+
+ // Return TestSolutionAsset with temp folder and project file
+ return new TestSolutionAsset(workFolder.Path, testAsset.ProjectRelativePath);
+ }
+
+ ///
+ /// Helper method to resolve TestSolutionAsset instances by name.
+ /// This is the easiest way to work around the limitation that [InlineData] cannot pass complex objects like TestSolutionAsset directly.
+ ///
+ private static TestSolutionAsset GetTestAssetByName(string testAssetName)
+ {
+ return testAssetName switch
+ {
+ nameof(TestSolutionAssetsFixture.SingleProject) => TestSolutionAssetsFixture.SingleProject,
+ nameof(TestSolutionAssetsFixture.ProjectWithDependencies) => TestSolutionAssetsFixture.ProjectWithDependencies,
+ nameof(TestSolutionAssetsFixture.NonSdkSingleProject) => TestSolutionAssetsFixture.NonSdkSingleProject,
+ nameof(TestSolutionAssetsFixture.NonSdkProjectWithDependencies) => TestSolutionAssetsFixture.NonSdkProjectWithDependencies,
+ _ => throw new ArgumentException($"Unknown test asset name: {testAssetName}", nameof(testAssetName))
+ };
+ }
+
+ ///
+ /// Builds a test asset with the given MSBuild args and verifies success.
+ ///
+ private void BuildAndVerify(string testAssetName, string multithreadingArgs)
+ {
+ // Resolve TestSolutionAsset from name
+ TestSolutionAsset testAsset = GetTestAssetByName(testAssetName);
+ // Prepare isolated copy of test assets to ensure fresh builds
+ TestSolutionAsset isolatedAsset = PrepareIsolatedTestAssets(testAsset);
+
+ string output = RunnerUtilities.ExecBootstrapedMSBuild(
+ $"\"{isolatedAsset.ProjectPath}\" {multithreadingArgs} {CommonMSBuildArgs}",
+ out bool success,
+ timeoutMilliseconds: _timeoutInMilliseconds);
+
+ success.ShouldBeTrue($"Build failed with args '{multithreadingArgs}' for {testAsset.SolutionFolder}. Output:\n{output}");
+
+ _output.WriteLine($"Built {testAsset.SolutionFolder} with arguments {multithreadingArgs}.");
+ }
+
+ ///
+ /// Tests building projects with various multithreading flags.
+ ///
+ [Theory]
+ [InlineData(nameof(TestSolutionAssetsFixture.SingleProject), "/m:1 /mt")]
+ [InlineData(nameof(TestSolutionAssetsFixture.SingleProject), "/m:8 /mt")]
+ [InlineData(nameof(TestSolutionAssetsFixture.SingleProject), "/mt")]
+ [InlineData(nameof(TestSolutionAssetsFixture.ProjectWithDependencies), "/m:1 /mt")]
+ [InlineData(nameof(TestSolutionAssetsFixture.ProjectWithDependencies), "/m:2 /mt")]
+ [InlineData(nameof(TestSolutionAssetsFixture.ProjectWithDependencies), "/m:8 /mt")]
+ public void MultithreadedBuild_Success(string testAssetName, string multithreadingArgs)
+ {
+ BuildAndVerify(testAssetName, multithreadingArgs);
+ }
+
+ ///
+ /// Builds a test asset with binary logging, then replays the binlog and verifies both succeed.
+ ///
+ private void BuildWithBinlogAndVerifyReplay(string testAssetName, string multithreadingArgs)
+ {
+ // Resolve TestSolutionAsset from name
+ TestSolutionAsset testAsset = GetTestAssetByName(testAssetName);
+
+ // Prepare isolated copy of test assets to ensure fresh builds
+ TestSolutionAsset isolatedAsset = PrepareIsolatedTestAssets(testAsset);
+
+ string binlogPath = Path.Combine(isolatedAsset.SolutionFolder, "build.binlog");
+
+ // Build with binary logging
+ string output = RunnerUtilities.ExecBootstrapedMSBuild(
+ $"\"{isolatedAsset.ProjectPath}\" {multithreadingArgs} /bl:\"{binlogPath}\" {CommonMSBuildArgs}",
+ out bool success,
+ timeoutMilliseconds: _timeoutInMilliseconds);
+
+ success.ShouldBeTrue($"Build failed with args '{multithreadingArgs}' for {testAsset.SolutionFolder}. Output:\n{output}.");
+
+ // Verify binary log was created and has content
+ File.Exists(binlogPath).ShouldBeTrue("Binary log file was not created.");
+ new FileInfo(binlogPath).Length.ShouldBeGreaterThan(0, "Binary log file was created but is empty.");
+
+ // Test binlog replay
+ string replayOutput = RunnerUtilities.ExecBootstrapedMSBuild(
+ $"\"{binlogPath}\" {CommonMSBuildArgs}",
+ out bool replaySuccess,
+ timeoutMilliseconds: _timeoutInMilliseconds);
+
+ replaySuccess.ShouldBeTrue($"Binlog replay failed. Output:\n{replayOutput}");
+
+ _output.WriteLine($"Built and replayed {testAsset.SolutionFolder} with arguments {multithreadingArgs}.");
+ }
+
+ ///
+ /// Tests binary logging with multithreaded builds and verifies replay functionality.
+ ///
+ [Theory]
+ [InlineData(nameof(TestSolutionAssetsFixture.SingleProject), "/m:8 /mt")]
+ [InlineData(nameof(TestSolutionAssetsFixture.ProjectWithDependencies), "/m:8 /mt")]
+ public void MultithreadedBuild_BinaryLogging(string testAssetName, string multithreadingArgs)
+ {
+ BuildWithBinlogAndVerifyReplay(testAssetName, multithreadingArgs);
+ }
+
+ ///
+ /// Tests building non-SDK-style projects with multithreading flags.
+ ///
+ [WindowsOnlyTheory]
+ [InlineData(nameof(TestSolutionAssetsFixture.NonSdkSingleProject), "/m:1 /mt")]
+ [InlineData(nameof(TestSolutionAssetsFixture.NonSdkSingleProject), "/m:8 /mt")]
+ [InlineData(nameof(TestSolutionAssetsFixture.NonSdkSingleProject), "/mt")]
+ [InlineData(nameof(TestSolutionAssetsFixture.NonSdkProjectWithDependencies), "/m:1 /mt")]
+ [InlineData(nameof(TestSolutionAssetsFixture.NonSdkProjectWithDependencies), "/m:2 /mt")]
+ [InlineData(nameof(TestSolutionAssetsFixture.NonSdkProjectWithDependencies), "/m:8 /mt")]
+ public void MultithreadedBuild_NonSdkStyle_Success(string testAssetName, string multithreadingArgs)
+ {
+ BuildAndVerify(testAssetName, multithreadingArgs);
+ }
+
+ ///
+ /// Tests binary logging with non-SDK-style multithreaded builds and verifies replay functionality.
+ ///
+ [WindowsOnlyTheory]
+ [InlineData(nameof(TestSolutionAssetsFixture.NonSdkSingleProject), "/m:8 /mt")]
+ [InlineData(nameof(TestSolutionAssetsFixture.NonSdkProjectWithDependencies), "/m:8 /mt")]
+ public void MultithreadedBuild_NonSdkStyle_BinaryLogging(string testAssetName, string multithreadingArgs)
+ {
+ BuildWithBinlogAndVerifyReplay(testAssetName, multithreadingArgs);
+ }
+ }
+}
diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/ConsoleApp/ConsoleApp.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/ConsoleApp/ConsoleApp.csproj
new file mode 100644
index 00000000000..56476a6a79c
--- /dev/null
+++ b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/ConsoleApp/ConsoleApp.csproj
@@ -0,0 +1,16 @@
+
+
+
+
+ Exe
+ v4.7.2
+
+
+
+
+
+
+
+
+
+
diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/ConsoleApp/Program.cs b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/ConsoleApp/Program.cs
new file mode 100644
index 00000000000..c106ee47fe5
--- /dev/null
+++ b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/ConsoleApp/Program.cs
@@ -0,0 +1,15 @@
+using System;
+using Library1;
+using Library2;
+
+namespace NonSdkConsoleApp
+{
+ class Program
+ {
+ static void Main(string[] args)
+ {
+ var c1 = new Class1();
+ var c2 = new Class2();
+ }
+ }
+}
diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/Library1/Class1.cs b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/Library1/Class1.cs
new file mode 100644
index 00000000000..13c5247c524
--- /dev/null
+++ b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/Library1/Class1.cs
@@ -0,0 +1,6 @@
+namespace Library1
+{
+ public class Class1
+ {
+ }
+}
diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/Library1/Library1.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/Library1/Library1.csproj
new file mode 100644
index 00000000000..9b68e2f1f6c
--- /dev/null
+++ b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/Library1/Library1.csproj
@@ -0,0 +1,11 @@
+
+
+
+ Library
+ v4.7.2
+
+
+
+
+
+
diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/Library2/Class2.cs b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/Library2/Class2.cs
new file mode 100644
index 00000000000..72f3dba396e
--- /dev/null
+++ b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/Library2/Class2.cs
@@ -0,0 +1,6 @@
+namespace Library2
+{
+ public class Class2
+ {
+ }
+}
diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/Library2/Library2.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/Library2/Library2.csproj
new file mode 100644
index 00000000000..d51282c86c1
--- /dev/null
+++ b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkProjectWithDependencies/Library2/Library2.csproj
@@ -0,0 +1,11 @@
+
+
+
+ Library
+ v4.7.2
+
+
+
+
+
+
diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkSingleProject/NonSdkSingleProject.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkSingleProject/NonSdkSingleProject.csproj
new file mode 100644
index 00000000000..d6a90b59173
--- /dev/null
+++ b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkSingleProject/NonSdkSingleProject.csproj
@@ -0,0 +1,12 @@
+
+
+
+
+ Exe
+ v4.7.2
+
+
+
+
+
+
diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkSingleProject/Program.cs b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkSingleProject/Program.cs
new file mode 100644
index 00000000000..5ab00dec12b
--- /dev/null
+++ b/src/MSBuild.EndToEnd.Tests/TestAssets/NonSdkSingleProject/Program.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace NonSdkSingleProject
+{
+ class Program
+ {
+ static void Main(string[] args)
+ {
+ }
+ }
+}
diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/ConsoleApp/ConsoleApp.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/ConsoleApp/ConsoleApp.csproj
new file mode 100644
index 00000000000..e9c22e44cdd
--- /dev/null
+++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/ConsoleApp/ConsoleApp.csproj
@@ -0,0 +1,11 @@
+
+
+ Exe
+ net10.0
+ latest
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/ConsoleApp/Program.cs b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/ConsoleApp/Program.cs
new file mode 100644
index 00000000000..26d73d8bcaf
--- /dev/null
+++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/ConsoleApp/Program.cs
@@ -0,0 +1,9 @@
+namespace ConsoleApp
+{
+ class Program
+ {
+ static void Main(string[] args)
+ {
+ }
+ }
+}
diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library1/Class1.cs b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library1/Class1.cs
new file mode 100644
index 00000000000..c8224d76a9a
--- /dev/null
+++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library1/Class1.cs
@@ -0,0 +1,7 @@
+namespace Library1
+{
+ public class Class1
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library1/Library1.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library1/Library1.csproj
new file mode 100644
index 00000000000..77cb2792044
--- /dev/null
+++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library1/Library1.csproj
@@ -0,0 +1,11 @@
+
+
+ net10.0
+ latest
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library2/Class2.cs b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library2/Class2.cs
new file mode 100644
index 00000000000..7de56b98ddb
--- /dev/null
+++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library2/Class2.cs
@@ -0,0 +1,7 @@
+namespace Library2
+{
+ public class Class2
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library2/Library2.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library2/Library2.csproj
new file mode 100644
index 00000000000..77cb2792044
--- /dev/null
+++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library2/Library2.csproj
@@ -0,0 +1,11 @@
+
+
+ net10.0
+ latest
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library3/Class3.cs b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library3/Class3.cs
new file mode 100644
index 00000000000..50a703c482b
--- /dev/null
+++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library3/Class3.cs
@@ -0,0 +1,7 @@
+namespace Library3
+{
+ public class Class3
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library3/Library3.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library3/Library3.csproj
new file mode 100644
index 00000000000..53502388eb3
--- /dev/null
+++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library3/Library3.csproj
@@ -0,0 +1,6 @@
+
+
+ net10.0
+ latest
+
+
\ No newline at end of file
diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library4/Class4.cs b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library4/Class4.cs
new file mode 100644
index 00000000000..61e843a9623
--- /dev/null
+++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library4/Class4.cs
@@ -0,0 +1,7 @@
+namespace Library4
+{
+ public class Class4
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library4/Library4.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library4/Library4.csproj
new file mode 100644
index 00000000000..53502388eb3
--- /dev/null
+++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/Library4/Library4.csproj
@@ -0,0 +1,6 @@
+
+
+ net10.0
+ latest
+
+
\ No newline at end of file
diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/global.json b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/global.json
new file mode 100644
index 00000000000..611555aea5f
--- /dev/null
+++ b/src/MSBuild.EndToEnd.Tests/TestAssets/ProjectWithDependencies/global.json
@@ -0,0 +1,9 @@
+{
+ "sdk": {
+ // This global.json is needed to prevent builds running in tests using the bootstrap layout from walking
+ // up the repo tree and resolving our sdk.paths, instead of the bootstrap layout's SDK.
+ // See https://github.com/dotnet/runtime/issues/118488 for details.
+ "allowPrerelease": true,
+ "rollForward": "latestMajor"
+ }
+}
diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/SingleProject/Program.cs b/src/MSBuild.EndToEnd.Tests/TestAssets/SingleProject/Program.cs
new file mode 100644
index 00000000000..6d3de979e33
--- /dev/null
+++ b/src/MSBuild.EndToEnd.Tests/TestAssets/SingleProject/Program.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace ConsoleApp
+{
+ class Program
+ {
+ static void Main(string[] args)
+ {
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/SingleProject/SingleProject.csproj b/src/MSBuild.EndToEnd.Tests/TestAssets/SingleProject/SingleProject.csproj
new file mode 100644
index 00000000000..5c0a78df5ac
--- /dev/null
+++ b/src/MSBuild.EndToEnd.Tests/TestAssets/SingleProject/SingleProject.csproj
@@ -0,0 +1,10 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+
+
+
\ No newline at end of file
diff --git a/src/MSBuild.EndToEnd.Tests/TestAssets/SingleProject/global.json b/src/MSBuild.EndToEnd.Tests/TestAssets/SingleProject/global.json
new file mode 100644
index 00000000000..611555aea5f
--- /dev/null
+++ b/src/MSBuild.EndToEnd.Tests/TestAssets/SingleProject/global.json
@@ -0,0 +1,9 @@
+{
+ "sdk": {
+ // This global.json is needed to prevent builds running in tests using the bootstrap layout from walking
+ // up the repo tree and resolving our sdk.paths, instead of the bootstrap layout's SDK.
+ // See https://github.com/dotnet/runtime/issues/118488 for details.
+ "allowPrerelease": true,
+ "rollForward": "latestMajor"
+ }
+}
diff --git a/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs b/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs
new file mode 100644
index 00000000000..c5bd822538d
--- /dev/null
+++ b/src/MSBuild.EndToEnd.Tests/TestAssetsFixture.cs
@@ -0,0 +1,53 @@
+// 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.UnitTests.Shared;
+using Shouldly;
+
+namespace Microsoft.Build.EndToEndTests
+{
+ ///
+ /// Fixture for test solution assets that handles expensive initialization like NuGet restore.
+ /// Restore runs through bootstrap MSBuild, so failures here can surface real regressions
+ /// in the same code paths that the tests exercise.
+ ///
+ public class TestSolutionAssetsFixture
+ {
+ internal string TestAssetDir { get; }
+
+ // Test solution asset definitions
+ internal static readonly TestSolutionAsset SingleProject = new("SingleProject", "SingleProject.csproj");
+ internal static readonly TestSolutionAsset ProjectWithDependencies = new("ProjectWithDependencies", "ConsoleApp/ConsoleApp.csproj");
+ internal static readonly TestSolutionAsset NonSdkSingleProject = new("NonSdkSingleProject", "NonSdkSingleProject.csproj");
+ internal static readonly TestSolutionAsset NonSdkProjectWithDependencies = new("NonSdkProjectWithDependencies", "ConsoleApp/ConsoleApp.csproj");
+
+ // Non-SDK projects do not require NuGet restore.
+ private static readonly TestSolutionAsset[] AssetsToRestore =
+ [
+ SingleProject,
+ ProjectWithDependencies
+ ];
+
+
+ public TestSolutionAssetsFixture()
+ {
+ TestAssetDir = Path.Combine(Path.GetDirectoryName(typeof(TestSolutionAssetsFixture).Assembly.Location) ?? AppContext.BaseDirectory, "TestAssets");
+ RestoreTestAssets();
+ }
+
+ private void RestoreTestAssets()
+ {
+ foreach (var asset in AssetsToRestore)
+ {
+ string projectPath = Path.Combine(TestAssetDir, asset.ProjectPath);
+
+ File.Exists(projectPath).ShouldBeTrue($"Test asset project not found: {projectPath}");
+
+ string output = RunnerUtilities.ExecBootstrapedMSBuild($"\"{projectPath}\" /t:Restore /v:minimal", out bool success, timeoutMilliseconds: 120_000);
+ success.ShouldBeTrue($"Failed to restore test asset {asset.SolutionFolder}\\{asset.ProjectRelativePath}. Output:\n{output}");
+ }
+ }
+ }
+}
diff --git a/src/MSBuild.EndToEnd.Tests/TestSolutionAsset.cs b/src/MSBuild.EndToEnd.Tests/TestSolutionAsset.cs
new file mode 100644
index 00000000000..f7909df7544
--- /dev/null
+++ b/src/MSBuild.EndToEnd.Tests/TestSolutionAsset.cs
@@ -0,0 +1,31 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.IO;
+
+namespace Microsoft.Build.EndToEndTests
+{
+ ///
+ /// Represents a test solution asset.
+ ///
+ internal readonly struct TestSolutionAsset
+ {
+ // Solution folder containing the test asset
+ internal string SolutionFolder { get; }
+
+ // Path to main (entry) project file relative to the solution folder
+ internal string ProjectRelativePath { get; }
+
+ internal TestSolutionAsset(string solutionFolder, string projectFile)
+ {
+ SolutionFolder = solutionFolder;
+ ProjectRelativePath = projectFile;
+ }
+
+ ///
+ /// Gets the path to the project file. This is relative when used as a test asset definition,
+ /// or absolute when used as an isolated test instance (after PrepareIsolatedTestAssets).
+ ///
+ internal string ProjectPath => Path.Combine(SolutionFolder, ProjectRelativePath);
+ }
+}