Skip to content

Commit c849248

Browse files
authored
[Opt-in] Parallelize Targets when building a solution (#7512)
Fixes #5072 (comment) Context When building a SLN, a metaproj is used to represent the build behavior. When there are multiple targets (ex clean;build), the current behavior is to run all of first Target in the projects, then run second Target. To improve the parallelism, the solution can pass both target to the project. Each project can start the second target without waiting for all of the first Target to finish. When the feature is enabled via environment variable, MSBuildSolutionBatchTargets, Solution Generator will create a "SlnProjectResolveProjectReference" target to build all the project/targets. All targets will depend on this new target. Add support for "SkipNonexistentProjects" as a metadata in MSBuild task. This allow the removal of it as a parameter during solution generation. Testing Added unit tests.
1 parent a1d9d69 commit c849248

File tree

7 files changed

+427
-51
lines changed

7 files changed

+427
-51
lines changed

src/Build.UnitTests/BackEnd/MSBuild_Tests.cs

+50
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,56 @@ public void SkipNonexistentProjectsBuildingInParallel()
314314
Assert.DoesNotContain(error, logger.FullLog);
315315
}
316316

317+
/// <summary>
318+
/// Verifies that nonexistent projects are skipped when requested when building in parallel.
319+
/// DDB # 125831
320+
/// </summary>
321+
[Fact]
322+
public void SkipNonexistentProjectsAsMetadataBuildingInParallel()
323+
{
324+
ObjectModelHelpers.DeleteTempProjectDirectory();
325+
ObjectModelHelpers.CreateFileInTempProjectDirectory(
326+
"SkipNonexistentProjectsMain.csproj",
327+
@"<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
328+
<Target Name=`t` >
329+
<ItemGroup>
330+
<ProjectReference Include=`this_project_does_not_exist_warn.csproj` >
331+
<SkipNonexistentProjects>true</SkipNonexistentProjects>
332+
</ProjectReference>
333+
<ProjectReference Include=`this_project_does_not_exist_error.csproj` >
334+
</ProjectReference>
335+
<ProjectReference Include=`foo.csproj` >
336+
<SkipNonexistentProjects>false</SkipNonexistentProjects>
337+
</ProjectReference>
338+
</ItemGroup>
339+
<MSBuild Projects=`@(ProjectReference)` BuildInParallel=`true` />
340+
</Target>
341+
</Project>
342+
");
343+
344+
ObjectModelHelpers.CreateFileInTempProjectDirectory(
345+
"foo.csproj",
346+
@"<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
347+
<Target Name=`t` >
348+
<Message Text=`Hello from foo.csproj`/>
349+
</Target>
350+
</Project>
351+
");
352+
353+
MockLogger logger = new MockLogger(_testOutput);
354+
ObjectModelHelpers.BuildTempProjectFileExpectFailure(@"SkipNonexistentProjectsMain.csproj", logger);
355+
356+
logger.AssertLogContains("Hello from foo.csproj");
357+
string message = String.Format(AssemblyResources.GetString("MSBuild.ProjectFileNotFoundMessage"), "this_project_does_not_exist_warn.csproj");
358+
string error = String.Format(AssemblyResources.GetString("MSBuild.ProjectFileNotFound"), "this_project_does_not_exist_warn.csproj");
359+
string error2 = String.Format(AssemblyResources.GetString("MSBuild.ProjectFileNotFound"), "this_project_does_not_exist_error.csproj");
360+
Assert.Equal(0, logger.WarningCount);
361+
Assert.Equal(1, logger.ErrorCount);
362+
Assert.Contains(message, logger.FullLog); // for the missing project
363+
Assert.Contains(error2, logger.FullLog);
364+
Assert.DoesNotContain(error, logger.FullLog);
365+
}
366+
317367
[Fact]
318368
public void LogErrorWhenBuildingVCProj()
319369
{

src/Build.UnitTests/Construction/SolutionProjectGenerator_Tests.cs

+155
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,161 @@ public void BuildProjectAsTarget()
123123
}
124124
}
125125

126+
/// <summary>
127+
/// Build Solution with Multiple Targets (ex. Clean;Build;Custom).
128+
/// </summary>
129+
[Fact]
130+
public void BuildProjectWithMultipleTargets()
131+
{
132+
using (TestEnvironment testEnvironment = TestEnvironment.Create())
133+
{
134+
TransientTestFolder folder = testEnvironment.CreateFolder(createFolder: true);
135+
TransientTestFolder classLibFolder = testEnvironment.CreateFolder(Path.Combine(folder.Path, "classlib"), createFolder: true);
136+
TransientTestFile classLibrary = testEnvironment.CreateFile(classLibFolder, "classlib.csproj",
137+
@"<Project>
138+
<Target Name=""Build"">
139+
<Message Text=""classlib.Build""/>
140+
</Target>
141+
<Target Name=""Clean"">
142+
<Message Text=""classlib.Clean""/>
143+
</Target>
144+
<Target Name=""Custom"">
145+
<Message Text=""classlib.Custom""/>
146+
</Target>
147+
</Project>
148+
");
149+
150+
TransientTestFolder simpleProjectFolder = testEnvironment.CreateFolder(Path.Combine(folder.Path, "simpleProject"), createFolder: true);
151+
TransientTestFile simpleProject = testEnvironment.CreateFile(simpleProjectFolder, "simpleProject.csproj",
152+
@"<Project>
153+
<Target Name=""Build"">
154+
<Message Text=""simpleProject.Build""/>
155+
</Target>
156+
<Target Name=""Clean"">
157+
<Message Text=""simpleProject.Clean""/>
158+
</Target>
159+
<Target Name=""Custom"">
160+
<Message Text=""simpleProject.Custom""/>
161+
</Target>
162+
</Project>
163+
");
164+
165+
TransientTestFile solutionFile = testEnvironment.CreateFile(folder, "testFolder.sln",
166+
@"
167+
Microsoft Visual Studio Solution File, Format Version 12.00
168+
# Visual Studio Version 16
169+
VisualStudioVersion = 16.6.30114.105
170+
MinimumVisualStudioVersion = 10.0.40219.1
171+
Project(""{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"") = ""simpleProject"", ""simpleProject\simpleProject.csproj"", ""{AA52A05F-A9C0-4C89-9933-BF976A304C91}""
172+
EndProject
173+
Project(""{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"") = ""classlib"", ""classlib\classlib.csproj"", ""{80B8E6B8-E46D-4456-91B1-848FD35C4AB9}""
174+
EndProject
175+
Global
176+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
177+
Debug|x86 = Debug|x86
178+
EndGlobalSection
179+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
180+
{AA52A05F-A9C0-4C89-9933-BF976A304C91}.Debug|x86.ActiveCfg = Debug|x86
181+
{AA52A05F-A9C0-4C89-9933-BF976A304C91}.Debug|x86.Build.0 = Debug|x86
182+
{80B8E6B8-E46D-4456-91B1-848FD35C4AB9}.Debug|x86.ActiveCfg = Debug|x86
183+
{80B8E6B8-E46D-4456-91B1-848FD35C4AB9}.Debug|x86.Build.0 = Debug|x86
184+
EndGlobalSection
185+
EndGlobal
186+
");
187+
188+
string output = RunnerUtilities.ExecMSBuild(solutionFile.Path + " /t:Clean;Build;Custom", out bool success);
189+
success.ShouldBeTrue();
190+
output.ShouldContain("classlib.Build");
191+
output.ShouldContain("classlib.Clean");
192+
output.ShouldContain("classlib.Custom");
193+
output.ShouldContain("simpleProject.Build");
194+
output.ShouldContain("simpleProject.Clean");
195+
output.ShouldContain("simpleProject.Custom");
196+
}
197+
}
198+
199+
200+
/// <summary>
201+
/// Build Solution with Multiple Targets (ex. Clean;Build;Custom).
202+
/// </summary>
203+
[Fact]
204+
public void BuildProjectWithMultipleTargetsInParallel()
205+
{
206+
using (TestEnvironment testEnvironment = TestEnvironment.Create())
207+
{
208+
TransientTestFolder folder = testEnvironment.CreateFolder(createFolder: true);
209+
TransientTestFolder classLibFolder = testEnvironment.CreateFolder(Path.Combine(folder.Path, "classlib"), createFolder: true);
210+
TransientTestFile classLibrary = testEnvironment.CreateFile(classLibFolder, "classlib.csproj",
211+
@"<Project>
212+
<Target Name=""Build"">
213+
<Message Text=""classlib.Build""/>
214+
</Target>
215+
<Target Name=""Clean"">
216+
<Message Text=""classlib.Clean""/>
217+
</Target>
218+
<Target Name=""Custom"">
219+
<Message Text=""classlib.Custom""/>
220+
</Target>
221+
</Project>
222+
");
223+
224+
TransientTestFolder simpleProjectFolder = testEnvironment.CreateFolder(Path.Combine(folder.Path, "simpleProject"), createFolder: true);
225+
TransientTestFile simpleProject = testEnvironment.CreateFile(simpleProjectFolder, "simpleProject.csproj",
226+
@"<Project>
227+
<Target Name=""Build"">
228+
<Message Text=""simpleProject.Build""/>
229+
</Target>
230+
<Target Name=""Clean"">
231+
<Message Text=""simpleProject.Clean""/>
232+
</Target>
233+
<Target Name=""Custom"">
234+
<Message Text=""simpleProject.Custom""/>
235+
</Target>
236+
</Project>
237+
");
238+
239+
TransientTestFile solutionFile = testEnvironment.CreateFile(folder, "testFolder.sln",
240+
@"
241+
Microsoft Visual Studio Solution File, Format Version 12.00
242+
# Visual Studio Version 16
243+
VisualStudioVersion = 16.6.30114.105
244+
MinimumVisualStudioVersion = 10.0.40219.1
245+
Project(""{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"") = ""simpleProject"", ""simpleProject\simpleProject.csproj"", ""{AA52A05F-A9C0-4C89-9933-BF976A304C91}""
246+
EndProject
247+
Project(""{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"") = ""classlib"", ""classlib\classlib.csproj"", ""{80B8E6B8-E46D-4456-91B1-848FD35C4AB9}""
248+
EndProject
249+
Global
250+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
251+
Debug|x86 = Debug|x86
252+
EndGlobalSection
253+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
254+
{AA52A05F-A9C0-4C89-9933-BF976A304C91}.Debug|x86.ActiveCfg = Debug|x86
255+
{AA52A05F-A9C0-4C89-9933-BF976A304C91}.Debug|x86.Build.0 = Debug|x86
256+
{80B8E6B8-E46D-4456-91B1-848FD35C4AB9}.Debug|x86.ActiveCfg = Debug|x86
257+
{80B8E6B8-E46D-4456-91B1-848FD35C4AB9}.Debug|x86.Build.0 = Debug|x86
258+
EndGlobalSection
259+
EndGlobal
260+
");
261+
262+
try
263+
{
264+
Environment.SetEnvironmentVariable("MSBuildSolutionBatchTargets", "1");
265+
var output = RunnerUtilities.ExecMSBuild(solutionFile.Path + " /m /t:Clean;Build;Custom", out bool success);
266+
success.ShouldBeTrue();
267+
output.ShouldContain("classlib.Build");
268+
output.ShouldContain("classlib.Clean");
269+
output.ShouldContain("classlib.Custom");
270+
output.ShouldContain("simpleProject.Build");
271+
output.ShouldContain("simpleProject.Clean");
272+
output.ShouldContain("simpleProject.Custom");
273+
}
274+
finally
275+
{
276+
Environment.SetEnvironmentVariable("MSBuildSolutionBatchTargets", "");
277+
}
278+
}
279+
}
280+
126281
/// <summary>
127282
/// Verify the AddNewErrorWarningMessageElement method
128283
/// </summary>

src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/MSBuild.cs

+55-18
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,13 @@ internal class MSBuild : ITask
2525
/// <summary>
2626
/// Enum describing the behavior when a project doesn't exist on disk.
2727
/// </summary>
28-
private enum SkipNonexistentProjectsBehavior
28+
private enum SkipNonExistentProjectsBehavior
2929
{
30+
/// <summary>
31+
/// Default when unset by user.
32+
/// </summary>
33+
Undefined,
34+
3035
/// <summary>
3136
/// Skip the project if there is no file on disk.
3237
/// </summary>
@@ -49,7 +54,7 @@ private enum SkipNonexistentProjectsBehavior
4954
private readonly List<ITaskItem> _targetOutputs = new List<ITaskItem>();
5055

5156
// Whether to skip project files that don't exist on disk. By default we error for such projects.
52-
private SkipNonexistentProjectsBehavior _skipNonexistentProjects = SkipNonexistentProjectsBehavior.Error;
57+
private SkipNonExistentProjectsBehavior _skipNonExistentProjects = SkipNonExistentProjectsBehavior.Undefined;
5358

5459
private TaskLoggingHelper _logHelper;
5560

@@ -162,19 +167,22 @@ public string SkipNonexistentProjects
162167
{
163168
get
164169
{
165-
switch (_skipNonexistentProjects)
170+
switch (_skipNonExistentProjects)
166171
{
167-
case SkipNonexistentProjectsBehavior.Build:
172+
case SkipNonExistentProjectsBehavior.Undefined:
173+
return "Undefined";
174+
175+
case SkipNonExistentProjectsBehavior.Build:
168176
return "Build";
169177

170-
case SkipNonexistentProjectsBehavior.Error:
178+
case SkipNonExistentProjectsBehavior.Error:
171179
return "False";
172180

173-
case SkipNonexistentProjectsBehavior.Skip:
181+
case SkipNonExistentProjectsBehavior.Skip:
174182
return "True";
175183

176184
default:
177-
ErrorUtilities.ThrowInternalError("Unexpected case {0}", _skipNonexistentProjects);
185+
ErrorUtilities.ThrowInternalError("Unexpected case {0}", _skipNonExistentProjects);
178186
break;
179187
}
180188

@@ -184,15 +192,9 @@ public string SkipNonexistentProjects
184192

185193
set
186194
{
187-
if (String.Equals("Build", value, StringComparison.OrdinalIgnoreCase))
188-
{
189-
_skipNonexistentProjects = SkipNonexistentProjectsBehavior.Build;
190-
}
191-
else
195+
if (TryParseSkipNonExistentProjects(value, out SkipNonExistentProjectsBehavior behavior))
192196
{
193-
ErrorUtilities.VerifyThrowArgument(ConversionUtilities.CanConvertStringToBool(value), "MSBuild.InvalidSkipNonexistentProjectValue");
194-
bool originalSkipValue = ConversionUtilities.ConvertStringToBool(value);
195-
_skipNonexistentProjects = originalSkipValue ? SkipNonexistentProjectsBehavior.Skip : SkipNonexistentProjectsBehavior.Error;
197+
_skipNonExistentProjects = behavior;
196198
}
197199
}
198200
}
@@ -324,7 +326,21 @@ public async Task<bool> ExecuteInternal()
324326
break;
325327
}
326328

327-
if (FileSystems.Default.FileExists(projectPath) || (_skipNonexistentProjects == SkipNonexistentProjectsBehavior.Build))
329+
// Try to get the behavior from metadata if it is undefined.
330+
var skipNonExistProjects = _skipNonExistentProjects;
331+
if (_skipNonExistentProjects == SkipNonExistentProjectsBehavior.Undefined)
332+
{
333+
if (TryParseSkipNonExistentProjects(project.GetMetadata("SkipNonexistentProjects"), out SkipNonExistentProjectsBehavior behavior))
334+
{
335+
skipNonExistProjects = behavior;
336+
}
337+
else
338+
{
339+
skipNonExistProjects = SkipNonExistentProjectsBehavior.Error;
340+
}
341+
}
342+
343+
if (FileSystems.Default.FileExists(projectPath) || (skipNonExistProjects == SkipNonExistentProjectsBehavior.Build))
328344
{
329345
if (FileUtilities.IsVCProjFilename(projectPath))
330346
{
@@ -365,13 +381,13 @@ public async Task<bool> ExecuteInternal()
365381
}
366382
else
367383
{
368-
if (_skipNonexistentProjects == SkipNonexistentProjectsBehavior.Skip)
384+
if (skipNonExistProjects == SkipNonExistentProjectsBehavior.Skip)
369385
{
370386
Log.LogMessageFromResources(MessageImportance.High, "MSBuild.ProjectFileNotFoundMessage", project.ItemSpec);
371387
}
372388
else
373389
{
374-
ErrorUtilities.VerifyThrow(_skipNonexistentProjects == SkipNonexistentProjectsBehavior.Error, "skipNonexistentProjects has unexpected value {0}", _skipNonexistentProjects);
390+
ErrorUtilities.VerifyThrow(skipNonExistProjects == SkipNonExistentProjectsBehavior.Error, "skipNonexistentProjects has unexpected value {0}", skipNonExistProjects);
375391
Log.LogErrorWithCodeFromResources("MSBuild.ProjectFileNotFound", project.ItemSpec);
376392
success = false;
377393
}
@@ -714,6 +730,27 @@ internal static async Task<bool> ExecuteTargets(
714730
return success;
715731
}
716732

733+
private bool TryParseSkipNonExistentProjects(string value, out SkipNonExistentProjectsBehavior behavior)
734+
{
735+
if (string.IsNullOrEmpty(value))
736+
{
737+
behavior = SkipNonExistentProjectsBehavior.Error;
738+
return false;
739+
}
740+
else if (String.Equals("Build", value, StringComparison.OrdinalIgnoreCase))
741+
{
742+
behavior = SkipNonExistentProjectsBehavior.Build;
743+
}
744+
else
745+
{
746+
ErrorUtilities.VerifyThrowArgument(ConversionUtilities.CanConvertStringToBool(value), "MSBuild.InvalidSkipNonexistentProjectValue");
747+
bool originalSkipValue = ConversionUtilities.ConvertStringToBool(value);
748+
behavior = originalSkipValue ? SkipNonExistentProjectsBehavior.Skip : SkipNonExistentProjectsBehavior.Error;
749+
}
750+
751+
return true;
752+
}
753+
717754
#endregion
718755
}
719756
}

0 commit comments

Comments
 (0)