Skip to content

Commit bdee25b

Browse files
committed
Add support for RootOutputPath
1 parent 5a69289 commit bdee25b

File tree

5 files changed

+272
-7
lines changed

5 files changed

+272
-7
lines changed

src/Tasks/Microsoft.NET.Build.Tasks/sdk/Sdk.props

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ Copyright (c) .NET Foundation. All rights reserved.
5757
<ImportDirectoryBuildProps>false</ImportDirectoryBuildProps>
5858
</PropertyGroup>
5959

60+
<!-- Setting RootOutputPath automatically opts in to standard output paths -->
61+
<PropertyGroup Condition="'$(RootOutputPath)' != ''">
62+
<UseStandardOutputPaths Condition="'$(UseStandardOutputPaths)' == ''">true</UseStandardOutputPaths>
63+
<BaseStandardOutputPath>$(RootOutputPath)</BaseStandardOutputPath>
64+
</PropertyGroup>
65+
6066
<!-- Set up base standard output folders if UseStandardOutputPaths is set -->
6167
<PropertyGroup Condition="'$(UseStandardOutputPaths)' == 'true'">
6268
<UseStandardIntermediateOutput Condition="'$(UseStandardIntermediateOutput)' == ''">true</UseStandardIntermediateOutput>
@@ -66,6 +72,11 @@ Copyright (c) .NET Foundation. All rights reserved.
6672

6773
<BaseOutputPath Condition="'$(BaseOutputPath)' == ''">$(BaseStandardOutputPath)build\</BaseOutputPath>
6874
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)' == ''">$(BaseStandardOutputPath)obj\</BaseIntermediateOutputPath>
75+
76+
<!-- If RootOutputPath is set, then projects share the BaseStandardOutputPath, and the project name should be appended to the output and
77+
intermediate output paths -->
78+
<BaseOutputPath Condition="'$(RootOutputPath)' != ''">$(BaseOutputPath)$(MSBuildProjectName)\</BaseOutputPath>
79+
<BaseIntermediateOutputPath Condition="'$(RootOutputPath)' != ''">$(BaseIntermediateOutputPath)$(MSBuildProjectName)\</BaseIntermediateOutputPath>
6980
</PropertyGroup>
7081

7182
<PropertyGroup Condition="'$(MSBuildProjectFullPath)' == '$(ProjectToOverrideProjectExtensionsPath)'">

src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.DefaultOutputPaths.targets

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,21 @@ Copyright (c) .NET Foundation. All rights reserved.
3333
<UseStandardOutputPaths>true</UseStandardOutputPaths>
3434
</PropertyGroup>
3535

36+
<!-- Handle RootOutputPath and if it wasn't set in Sdk.props -->
37+
<PropertyGroup Condition="'$(RootOutputPath)' != ''">
38+
<UseStandardOutputPaths Condition="'$(UseStandardOutputPaths)' == ''">true</UseStandardOutputPaths>
39+
<BaseStandardOutputPath Condition="'$(BaseStandardOutputPath)' == ''">$(RootOutputPath)</BaseStandardOutputPath>
40+
</PropertyGroup>
41+
3642
<!-- Set BaseOutputPath for standard output format if it wasn't set in Sdk.props -->
37-
<PropertyGroup Condition="'$(UseStandardOutputPaths)' == 'true'">
43+
<PropertyGroup Condition="'$(UseStandardOutputPaths)' == 'true' And '$(BaseOutputPath)' == ''">
3844
<BaseStandardOutputPath Condition="'$(BaseStandardOutputPath)' == ''">bin\</BaseStandardOutputPath>
3945
<BaseStandardOutputPath>$([MSBuild]::EnsureTrailingSlash($(BaseStandardOutputPath)))</BaseStandardOutputPath>
4046

41-
<BaseOutputPath Condition="'$(BaseOutputPath)' == ''">$(BaseStandardOutputPath)build\</BaseOutputPath>
47+
<BaseOutputPath>$(BaseStandardOutputPath)build\</BaseOutputPath>
48+
49+
<!-- If RootOutputPath is set, then projects share the BaseStandardOutputPath, and the project name should be appended to the output path -->
50+
<BaseOutputPath Condition="'$(RootOutputPath)' != ''">$(BaseOutputPath)$(MSBuildProjectName)\</BaseOutputPath>
4251
</PropertyGroup>
4352

4453
<PropertyGroup Condition="'$(UseStandardOutputPaths)' != 'true'">
@@ -115,7 +124,9 @@ Copyright (c) .NET Foundation. All rights reserved.
115124

116125
<!-- Publish path -->
117126
<PublishDirName Condition="'$(PublishDirName)' == ''">publish</PublishDirName>
118-
<PublishDir Condition="'$(PublishDir)' == ''">$(BaseStandardOutputPath)$(PublishDirName)\$(_ArtifactPivots)\</PublishDir>
127+
128+
<PublishDir Condition="'$(PublishDir)' == '' And '$(RootOutputPath)' == ''">$(BaseStandardOutputPath)$(PublishDirName)\$(_ArtifactPivots)\</PublishDir>
129+
<PublishDir Condition="'$(PublishDir)' == '' And '$(RootOutputPath)' != ''">$(BaseStandardOutputPath)$(PublishDirName)\$(MSBuildProjectName)\$(_ArtifactPivots)\</PublishDir>
119130

120131
</PropertyGroup>
121132

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
//
4+
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Diagnostics.Contracts;
8+
using System.IO;
9+
using System.Linq;
10+
using System.Runtime.CompilerServices;
11+
using System.Text;
12+
using System.Threading.Tasks;
13+
using FluentAssertions;
14+
using Microsoft.NET.TestFramework;
15+
using Microsoft.NET.TestFramework.Assertions;
16+
using Microsoft.NET.TestFramework.Commands;
17+
using Microsoft.NET.TestFramework.ProjectConstruction;
18+
using Xunit;
19+
using Xunit.Abstractions;
20+
using Xunit.Sdk;
21+
22+
23+
namespace Microsoft.NET.Build.Tests
24+
{
25+
public class RootOutputPathTests : SdkTest
26+
{
27+
public RootOutputPathTests(ITestOutputHelper log) : base(log)
28+
{
29+
}
30+
31+
(List<TestProject> testProjects, TestAsset testAsset) GetTestProjects(bool setRootOutputInProject, [CallerMemberName] string callingMethod = "")
32+
{
33+
var testProject1 = new TestProject()
34+
{
35+
Name = "App1",
36+
IsExe = true
37+
};
38+
39+
var testProject2 = new TestProject()
40+
{
41+
Name = "App2",
42+
IsExe = true
43+
};
44+
45+
var testLibraryProject = new TestProject()
46+
{
47+
Name = "Library",
48+
};
49+
50+
testProject1.ReferencedProjects.Add(testLibraryProject);
51+
testProject2.ReferencedProjects.Add(testLibraryProject);
52+
53+
List<TestProject> testProjects = new() { testProject1, testProject2, testLibraryProject };
54+
55+
if (setRootOutputInProject)
56+
{
57+
foreach (var testProject in testProjects)
58+
{
59+
testProject.AdditionalProperties["RootOutputPath"] = "..\\artifacts";
60+
}
61+
}
62+
63+
var testAsset = _testAssetsManager.CreateTestProjects(testProjects, callingMethod: callingMethod, identifier: setRootOutputInProject.ToString());
64+
65+
if (!setRootOutputInProject)
66+
{
67+
File.WriteAllText(Path.Combine(testAsset.Path, "Directory.Build.props"),
68+
"""
69+
<Project>
70+
<PropertyGroup>
71+
<RootOutputPath>$(MSBuildThisFileDirectory)\artifacts</RootOutputPath>
72+
</PropertyGroup>
73+
</Project>
74+
""");
75+
}
76+
77+
return (testProjects, testAsset);
78+
}
79+
80+
[Theory]
81+
[InlineData(true)]
82+
[InlineData(false)]
83+
public void ItUsesRootOutputPathForBuild(bool setRootOutputInProject)
84+
{
85+
var (testProjects, testAsset) = GetTestProjects(setRootOutputInProject);
86+
87+
new DotnetCommand(Log, "build")
88+
.WithWorkingDirectory(testAsset.Path)
89+
.Execute()
90+
.Should()
91+
.Pass();
92+
93+
ValidateIntermediatePaths(testAsset, testProjects, setRootOutputInProject);
94+
95+
foreach (var testProject in testProjects)
96+
{
97+
new FileInfo(Path.Combine(testAsset.TestRoot, "artifacts", "build", testProject.Name, "debug", testProject.Name + ".dll"))
98+
.Should()
99+
.Exist();
100+
}
101+
}
102+
103+
[Theory]
104+
[InlineData(true)]
105+
[InlineData(false)]
106+
public void ItUsesRootOutputPathForPublish(bool setRootOutputInProject)
107+
{
108+
var (testProjects, testAsset) = GetTestProjects(setRootOutputInProject);
109+
110+
new DotnetCommand(Log, "publish")
111+
.WithWorkingDirectory(testAsset.Path)
112+
.Execute()
113+
.Should()
114+
.Pass();
115+
116+
ValidateIntermediatePaths(testAsset, testProjects, setRootOutputInProject);
117+
118+
foreach (var testProject in testProjects)
119+
{
120+
new FileInfo(Path.Combine(testAsset.TestRoot, "artifacts", "build", testProject.Name, "debug", testProject.Name + ".dll"))
121+
.Should()
122+
.Exist();
123+
124+
new FileInfo(Path.Combine(testAsset.TestRoot, "artifacts", "publish", testProject.Name, "debug", testProject.Name + ".dll"))
125+
.Should()
126+
.Exist();
127+
}
128+
}
129+
130+
[Theory]
131+
[InlineData(true)]
132+
[InlineData(false)]
133+
public void ItUsesRootOutputPathForPack(bool setRootOutputInProject)
134+
{
135+
var (testProjects, testAsset) = GetTestProjects(setRootOutputInProject);
136+
137+
new DotnetCommand(Log, "pack")
138+
.WithWorkingDirectory(testAsset.Path)
139+
.Execute()
140+
.Should()
141+
.Pass();
142+
143+
ValidateIntermediatePaths(testAsset, testProjects, setRootOutputInProject);
144+
145+
foreach (var testProject in testProjects)
146+
{
147+
new FileInfo(Path.Combine(testAsset.TestRoot, "artifacts", "build", testProject.Name, "debug", testProject.Name + ".dll"))
148+
.Should()
149+
.Exist();
150+
151+
new FileInfo(Path.Combine(testAsset.TestRoot, "artifacts", "package", "debug", testProject.Name + ".1.0.0.nupkg"))
152+
.Should()
153+
.Exist();
154+
}
155+
}
156+
157+
void ValidateIntermediatePaths(TestAsset testAsset, IEnumerable<TestProject> testProjects, bool setRootOutputInProject)
158+
{
159+
foreach (var testProject in testProjects)
160+
{
161+
if (setRootOutputInProject)
162+
{
163+
new DirectoryInfo(Path.Combine(testAsset.TestRoot, testProject.Name))
164+
.Should()
165+
.HaveDirectory("obj");
166+
167+
new DirectoryInfo(Path.Combine(testAsset.TestRoot, testProject.Name, "bin"))
168+
.Should()
169+
.NotExist();
170+
171+
new DirectoryInfo(Path.Combine(testAsset.TestRoot, "artifacts", "obj"))
172+
.Should()
173+
.NotExist();
174+
}
175+
else
176+
{
177+
new DirectoryInfo(Path.Combine(testAsset.TestRoot, testProject.Name))
178+
.Should()
179+
.NotHaveSubDirectories();
180+
181+
new DirectoryInfo(Path.Combine(testAsset.TestRoot, "artifacts", "obj", testProject.Name, "debug"))
182+
.Should()
183+
.Exist();
184+
};
185+
}
186+
}
187+
}
188+
}

src/Tests/Microsoft.NET.TestFramework/ProjectConstruction/TestProject.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public TestProject([CallerMemberName] string name = null)
3838
public string ProjectSdk { get; set; }
3939

4040
// Applies to SDK Projects
41-
public string TargetFrameworks { get; set; }
41+
public string TargetFrameworks { get; set; } = ToolsetInfo.CurrentTargetFramework;
4242

4343
public string RuntimeFrameworkVersion { get; set; }
4444

src/Tests/Microsoft.NET.TestFramework/TestAssetsManager.cs

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
// Copyright (c) .NET Foundation and contributors. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

4+
using FluentAssertions;
5+
using Microsoft.NET.TestFramework.Assertions;
6+
using Microsoft.NET.TestFramework.Commands;
47
using Microsoft.NET.TestFramework.ProjectConstruction;
58
using System;
69
using System.Collections.Generic;
710
using System.IO;
11+
using System.Linq;
812
using System.Runtime.CompilerServices;
913
using System.Security.Cryptography;
1014
using System.Text;
@@ -74,12 +78,63 @@ public TestAsset CreateTestProject(
7478
GetTestDestinationDirectoryPath(testProject.Name, callingMethod, identifier);
7579
TestDestinationDirectories.Add(testDestinationDirectory);
7680

77-
var testAsset = new TestAsset(testDestinationDirectory, TestContext.Current.SdkVersion, Log);
81+
var testAsset = CreateTestProjectsInDirectory(new List<TestProject>() { testProject }, testDestinationDirectory, targetExtension);
7882
testAsset.TestProject = testProject;
7983

80-
Stack<TestProject> projectStack = new Stack<TestProject>();
81-
projectStack.Push(testProject);
84+
return testAsset;
85+
}
86+
87+
/// <summary>
88+
/// Creates a list of test projects and adds them to a solution
89+
/// </summary>
90+
/// <param name="testProjects">The in-memory test projects to write to disk</param>
91+
/// <param name="callingMethod">Defaults to the name of the caller function (presumably the test).
92+
/// Used to prevent file collisions on tests which share the same test project.</param>
93+
/// <param name="identifier">Use this for theories.
94+
/// Pass in the unique theory parameters that can indentify that theory from others.
95+
/// The Identifier is used to distinguish between theory child tests. Generally it should be created using a combination of all of the theory parameter values.
96+
/// This is distinct from the test project name and is used to prevent file collisions between theory tests that use the same test project.</param>
97+
/// <param name="targetExtension">The extension type of the desired test project, e.g. .csproj, or .fsproj.</param>
98+
/// <returns>A new TestAsset directory with the solution and test projects in it.</returns>
99+
public TestAsset CreateTestProjects(
100+
IEnumerable<TestProject> testProjects,
101+
[CallerMemberName] string callingMethod = "",
102+
string identifier = "",
103+
string targetExtension = ".csproj")
104+
{
105+
var testDestinationDirectory =
106+
GetTestDestinationDirectoryPath(callingMethod, callingMethod, identifier);
107+
TestDestinationDirectories.Add(testDestinationDirectory);
108+
109+
var testAsset = CreateTestProjectsInDirectory(testProjects, testDestinationDirectory, targetExtension);
110+
111+
new DotnetNewCommand(Log, "sln")
112+
.WithVirtualHive()
113+
.WithWorkingDirectory(testDestinationDirectory)
114+
.Execute()
115+
.Should()
116+
.Pass();
117+
118+
foreach (var testProject in testProjects)
119+
{
120+
new DotnetCommand(Log, "sln", "add", testProject.Name)
121+
.WithWorkingDirectory(testDestinationDirectory)
122+
.Execute()
123+
.Should()
124+
.Pass();
125+
}
126+
127+
return testAsset;
128+
}
129+
130+
private TestAsset CreateTestProjectsInDirectory(
131+
IEnumerable<TestProject> testProjects,
132+
string testDestinationDirectory,
133+
string targetExtension = ".csproj")
134+
{
135+
var testAsset = new TestAsset(testDestinationDirectory, TestContext.Current.SdkVersion, Log);
82136

137+
Stack<TestProject> projectStack = new Stack<TestProject>(testProjects);
83138
HashSet<TestProject> createdProjects = new HashSet<TestProject>();
84139

85140
while (projectStack.Count > 0)

0 commit comments

Comments
 (0)