Skip to content

Commit

Permalink
Add ability to skip projects dynamically in Traversal projects and Vi…
Browse files Browse the repository at this point in the history
…sual Studio solution files (#439)
  • Loading branch information
jeffkl authored Apr 26, 2023
1 parent cef0a8d commit ed27e69
Show file tree
Hide file tree
Showing 12 changed files with 636 additions and 310 deletions.
77 changes: 77 additions & 0 deletions src/Traversal.UnitTests/CustomProjectCreatorTemplates.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.Build.Evaluation;
using Microsoft.Build.Utilities.ProjectCreation;
using System;
using System.Collections.Generic;
using System.IO;

namespace Microsoft.Build.Traversal.UnitTests
Expand All @@ -13,6 +14,82 @@ public static class CustomProjectCreatorTemplates
{
private static readonly string ThisAssemblyDirectory = Path.GetDirectoryName(typeof(CustomProjectCreatorTemplates).Assembly.Location);

public static ProjectCreator DirectoryBuildProps(
this ProjectCreatorTemplates templates,
string directory = null,
ProjectCollection projectCollection = null)
{
return ProjectCreator.Create(
path: Path.Combine(directory, "Directory.Build.props"),
projectCollection: projectCollection,
projectFileOptions: NewProjectFileOptions.None)
.Save();
}

public static ProjectCreator SolutionMetaproj(
this ProjectCreatorTemplates templates,
string directory,
params ProjectCreator[] projectReferences)
{
FileInfo directorySolutionPropsPath = new FileInfo(Path.Combine(directory, "Directory.Solution.props"));
FileInfo directorySolutionTargetsPath = new FileInfo(Path.Combine(directory, "Directory.Solution.targets"));

ProjectCreator.Create(
path: directorySolutionPropsPath.FullName,
projectFileOptions: NewProjectFileOptions.None)
.Import(Path.Combine(ThisAssemblyDirectory, "Sdk", "Sdk.props"))
.Save();

ProjectCreator.Create(
path: directorySolutionTargetsPath.FullName,
projectFileOptions: NewProjectFileOptions.None)
.Import(Path.Combine(ThisAssemblyDirectory, "Sdk", "Sdk.targets"))
.Save();

return ProjectCreator.Create(
path: Path.Combine(directory, "Solution.metaproj"),
projectFileOptions: NewProjectFileOptions.None)
.Property("_DirectorySolutionPropsFile", directorySolutionPropsPath.Name)
.Property("_DirectorySolutionPropsBasePath", directorySolutionPropsPath.DirectoryName)
.Property("DirectorySolutionPropsPath", directorySolutionPropsPath.FullName)
.Property("Configuration", "Debug")
.Property("Platform", "Any CPU")
.Property("SolutionDir", directory)
.Property("SolutionExt", ".sln")
.Property("SolutionFileName", "Solution.sln")
.Property("SolutionName", "Solution")
.Property("SolutionPath", Path.Combine(directory, "Solution.sln"))
.Property("CurrentSolutionConfigurationContents", string.Empty)
.Property("_DirectorySolutionTargetsFile", directorySolutionTargetsPath.Name)
.Property("_DirectorySolutionTargetsBasePath", directorySolutionTargetsPath.DirectoryName)
.Property("DirectorySolutionTargetsPath", directorySolutionTargetsPath.FullName)
.Import(directorySolutionPropsPath.FullName)
.ForEach(projectReferences, (item, projectCreator) =>
{
projectCreator.ItemInclude(
"ProjectReference",
item.FullPath,
metadata: new Dictionary<string, string>
{
["AdditionalProperties"] = "Configuration=Debug; Platform=AnyCPU",
["Platform"] = "AnyCPU",
["Configuration"] = "Debug",
["ToolsVersion"] = string.Empty,
["SkipNonexistentProjects"] = bool.FalseString,
});
})
.Target("Build", outputs: "@(CollectedBuildOutput)")
.Task("MSBuild", parameters: new Dictionary<string, string>
{
["BuildInParallel"] = bool.TrueString,
["Projects"] = "@(ProjectReference)",
["Properties"] = "BuildingSolutionFile=true; CurrentSolutionConfigurationContents=$(CurrentSolutionConfigurationContents); SolutionDir=$(SolutionDir); SolutionExt=$(SolutionExt); SolutionFileName=$(SolutionFileName); SolutionName=$(SolutionName); SolutionPath=$(SolutionPath)",
})
.TaskOutputItem("TargetOutputs", "CollectedBuildOutput")
.Import(directorySolutionTargetsPath.FullName)
.Save();
}

public static ProjectCreator ProjectWithBuildOutput(
this ProjectCreatorTemplates templates,
string target,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
<ProjectReference Include="..\Traversal\Microsoft.Build.Traversal.csproj" ReferenceOutputAssembly="false" />
</ItemGroup>
<ItemGroup>
<None Include="..\Traversal\Sdk\Sdk.props" Link="Sdk\Sdk.props" CopyToOutputDirectory="PreserveNewest" />
<None Include="..\Traversal\Sdk\Sdk.targets" Link="Sdk\Sdk.targets" CopyToOutputDirectory="PreserveNewest" />
<None Include="..\Traversal\Sdk\**" Link="Sdk\$(RelativeDir)%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
58 changes: 58 additions & 0 deletions src/Traversal.UnitTests/SolutionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// Licensed under the MIT license.

using Microsoft.Build.Execution;
using Microsoft.Build.UnitTests.Common;
using Microsoft.Build.Utilities.ProjectCreation;
using Shouldly;
using System.Collections.Generic;
using System.IO;
using Xunit;

namespace Microsoft.Build.Traversal.UnitTests
{
public class SolutionTests : MSBuildSdkTestBase
{
[Fact]
public void SolutionsCanSkipProjects()
{
ProjectCreator projectA = ProjectCreator.Templates
.ProjectWithBuildOutput("Build")
.Target("ShouldSkipProject", returns: "@(ProjectToSkip)")
.ItemInclude("ProjectToSkip", "$(MSBuildProjectFullPath)", condition: "false", metadata: new Dictionary<string, string> { ["Message"] = "Project A is not skipped!" })
.Save(Path.Combine(TestRootPath, "ProjectA", "ProjectA.csproj"));

ProjectCreator projectB = ProjectCreator.Templates
.ProjectWithBuildOutput("Build")
.Target("ShouldSkipProject", returns: "@(ProjectToSkip)")
.ItemInclude("ProjectToSkip", "$(MSBuildProjectFullPath)", condition: "true", metadata: new Dictionary<string, string> { ["Message"] = "Project B is skipped!" })
.Save(Path.Combine(TestRootPath, "ProjectB", "ProjectB.csproj"));

ProjectCreator.Templates.SolutionMetaproj(
TestRootPath,
new[] { projectA, projectB })
.TryBuild("Build", out bool result, out BuildOutput buildOutput, out IDictionary<string, TargetResult> targetOutputs);

result.ShouldBeTrue();

buildOutput.Messages.High.ShouldHaveSingleItem()
.ShouldContain("Project B is skipped!");

targetOutputs.TryGetValue("Build", out TargetResult buildTargetResult).ShouldBeTrue();

buildTargetResult.Items.ShouldHaveSingleItem()
.ItemSpec.ShouldBe(Path.Combine("bin", "ProjectA.dll"));
}

[Fact]
public void IsUsingMicrosoftTraversalSdkSet()
{
ProjectCreator.Templates
.SolutionMetaproj(TestRootPath)
.TryGetPropertyValue("UsingMicrosoftTraversalSdk", out string usingMicrosoftTraversalSdk);

usingMicrosoftTraversalSdk.ShouldBe("true", StringCompareShould.IgnoreCase);
}
}
}
30 changes: 30 additions & 0 deletions src/Traversal.UnitTests/TraversalTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,36 @@ ProjectCreator GetSkeletonCSProjWithMessageTasksPrintingWellKnownMetadata(string
}
}

[Fact]
public void TraversalsCanSkipProjects()
{
ProjectCreator projectA = ProjectCreator.Templates
.ProjectWithBuildOutput("Build")
.Target("ShouldSkipProject", returns: "@(ProjectToSkip)")
.ItemInclude("ProjectToSkip", "$(MSBuildProjectFullPath)", condition: "false", metadata: new Dictionary<string, string> { ["Message"] = "Project A is not skipped!" })
.Save(Path.Combine(TestRootPath, "ProjectA", "ProjectA.csproj"));

ProjectCreator projectB = ProjectCreator.Templates
.ProjectWithBuildOutput("Build")
.Target("ShouldSkipProject", returns: "@(ProjectToSkip)")
.ItemInclude("ProjectToSkip", "$(MSBuildProjectFullPath)", condition: "true", metadata: new Dictionary<string, string> { ["Message"] = "Project B is skipped!" })
.Save(Path.Combine(TestRootPath, "ProjectB", "ProjectB.csproj"));

ProjectCreator.Templates
.TraversalProject(new string[] { projectA, projectB }, path: GetTempFile("dirs.proj"))
.TryBuild("Build", out bool result, out BuildOutput buildOutput, out IDictionary<string, TargetResult> targetOutputs);

result.ShouldBeTrue();

buildOutput.Messages.High.ShouldHaveSingleItem()
.ShouldContain("Project B is skipped!");

targetOutputs.TryGetValue("Build", out TargetResult buildTargetResult).ShouldBeTrue();

buildTargetResult.Items.ShouldHaveSingleItem()
.ItemSpec.ShouldBe(Path.Combine("bin", "ProjectA.dll"));
}

[Theory]
[InlineData("Build")]
[InlineData("Clean")]
Expand Down
89 changes: 85 additions & 4 deletions src/Traversal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,88 @@ A traversal project can also reference other traversal projects. This is useful
</ItemGroup>
</Project>
```
## Dynamically Skip Projects in a Traversal Project
By default, every project included in a Traversal project is built. There are two ways to dynamically skip projects. One is to manually
add conditions to the `<ProjectReference />` items:

```xml
<Project Sdk="Microsoft.Build.Traversal">
<ItemGroup>
<ProjectReference Include="src\Common\Common.csproj" />
<ProjectReference Include="src\App\App.csproj" />
<ProjectReference Include="src\WebApplication\WebApplication.csproj" Condition="'$(DoNotBuildWebApp)' == 'true'" />
</ItemGroup>
</Project>
```

This allows you to pass MSBuild global properties to skip a particular project:

```
msbuild /Property:DoNotBuildWebApp=true
```

Another method is to add a `ShouldSkipProject` target to your `Directory.Build.targets`. Use the target below as a template:

```xml
<Target Name="ShouldSkipProject" Returns="@(ProjectToSkip)">
<ItemGroup>
<!-- Add the current project to the ProjectToSkip item with a message if DoNotBuildWebApp is true and the current project is WebApplication.csproj -->
<ProjectToSkip Include="$(MSBuildProjectFullPath)"
Condition="'$(DoNotBuildWebApp)' == 'true' And '$(MSBuildProjectFile)' == 'WebApplication.csproj'"
Message="Web applications are excluded because 'DoNotBuildWebApp' is set to 'true'." />
</ItemGroup>
</Target>
```

This results in a message being logged that a particular project is skipped:

```
ValidateSolutionConfiguration:
Building solution configuration "Debug|Any CPU".
SkipProjects:
Skipping project "D:\MySource\src\WebApplication\WebApplication.csproj". Web applications are excluded because 'DoNotBuildWebApp' is set to 'true'.
```

## Dynamically Skip Projects in a Visual Studio Solution File
By default, every project included in a Visual Studio solution file is built. Visual Studio solution files are essentially traversal files
and can be extended with Microsoft.Build.Traversal. To do this, create a file named `Directory.Solution.props` and `Directory.Solution.targets`
in the same folder of any solution with the following contents:

Directory.Solution.props:
```xml
<Project>
<Import Project="Microsoft.Build.Traversal" Project="Sdk.props" />
</Project>
```

Directory.Solution.targets:
```xml
<Project>
<Import Project="Microsoft.Build.Traversal" Project="Sdk.targets" />
</Project>
```

Finally, add a `ShouldSkipProject` target to your `Directory.Build.targets`. Use the target below as a template:

```xml
<Target Name="ShouldSkipProject" Returns="@(ProjectToSkip)">
<ItemGroup Condition="'$(MSBuildRuntimeType)' == 'Core'">
<!-- Skip building Visual Studio Extension (VSIX) projects if the user is building with dotnet build since its only supported to build those projects with MSBuild.exe -->
<ProjectToSkip Include="$(MSBuildProjectFullPath)"
Message="Visual Studio Extension (VSIX) projects cannot be built with dotnet.exe and require you to use msbuild.exe or Visual Studio."
Condition="'$(VsSDKVersion)' != ''" />
</ItemGroup>
</Target>
```

This example will skip building VSIX projects when a user builds with `dotnet build` since they need to use `MSBuild.exe` to build those projects.

```
ValidateSolutionConfiguration:
Building solution configuration "Debug|Any CPU".
SkipProjects:
Skipping project "D:\MySource\src\MyVSExtension\MyVSExtension.csproj". Visual Studio Extension (VSIX) projects cannot be built with dotnet.exe and require you to use msbuild.exe or Visual Studio.
```

## Extensibility

Expand All @@ -58,7 +139,7 @@ Setting the following properties control how Traversal works.

Add to the list of custom files to import after Traversal targets. This can be useful if you want to extend or override an existing target for you specific needs.
```xml
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project>
<PropertyGroup>
<CustomAfterTraversalTargets>$(CustomAfterTraversalTargets);My.After.Traversal.targets</CustomAfterTraversalTargets>
</PropertyGroup>
Expand All @@ -77,7 +158,7 @@ The following properties control global properties passed to different parts of

Set some properties during build.
```xml
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project>
<PropertyGroup>
<TraversalGlobalProperties>Property1=true;EnableSomething=true</TraversalGlobalProperties>
</PropertyGroup>
Expand All @@ -102,7 +183,7 @@ The following properties control the invocation of the to traversed projects.
Change the `TestInParallel` setting for the Test target.

```xml
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project>
<PropertyGroup>
<TestInParallel>true</TestInParallel>
</PropertyGroup>
Expand All @@ -122,7 +203,7 @@ The following attributes can be set to false to exclude ProjectReferences for a
Add the `Test` attribute to the `ProjectReference` to exclude it when invoking the Test target.

```xml
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project>
<ItemGroup>
<ProjectReference Include="ProjectA.csproj" Test="false" />
</ItemGroup>
Expand Down
43 changes: 7 additions & 36 deletions src/Traversal/Sdk/Sdk.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,53 +4,24 @@
Licensed under the MIT license.
-->
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project InitialTargets="SkipProjects" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

<PropertyGroup>
<UsingMicrosoftTraversalSdk>true</UsingMicrosoftTraversalSdk>

<!-- Don't automatically reference assembly packages since NoTargets don't need reference assemblies -->
<AutomaticallyUseReferenceAssemblyPackages Condition="'$(AutomaticallyUseReferenceAssemblyPackages)' == ''">false</AutomaticallyUseReferenceAssemblyPackages>
</PropertyGroup>

<Import Project="$(CustomBeforeTraversalProps)" Condition=" '$(CustomBeforeTraversalProps)' != '' And Exists('$(CustomBeforeTraversalProps)') " />

<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />

<PropertyGroup>
<MSBuildAllProjects Condition="'$(MSBuildToolsVersion)' != 'Current'">$(MSBuildAllProjects);$(MsBuildThisFileFullPath)</MSBuildAllProjects>

<!--
A list of project names that are considered traversal projects. Add to this list if you name your projects something other than "dirs.proj"
-->
<TraversalProjectNames Condition=" '$(TraversalProjectNames)' == '' ">dirs.proj</TraversalProjectNames>

<IsTraversal Condition=" '$(IsTraversal)' == '' And $(TraversalProjectNames.IndexOf($(MSBuildProjectFile), System.StringComparison.OrdinalIgnoreCase)) >= 0 ">true</IsTraversal>

<!--
NuGet should always restore Traversal projects with "PackageReference" style restore. Setting this property will cause the right thing to happen even if there aren't any PackageReference items in the project.
-->
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>

<!-- Targeting packs shouldn't be referenced as traversal projects don't compile. -->
<DisableImplicitFrameworkReferences Condition="'$(DisableImplicitFrameworkReferences)' == ''">true</DisableImplicitFrameworkReferences>
</PropertyGroup>

<ItemDefinitionGroup Condition=" '$(TraversalDoNotReferenceOutputAssemblies)' != 'false' ">
<ProjectReference>
<!--
Setting ReferenceOutputAssembly skips adding the outputs of the referenced project to an item
-->
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<!--
Setting SkipGetTargetFrameworkProperties skips target framework cross-project validation in NuGet
-->
<SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties>
</ProjectReference>
</ItemDefinitionGroup>
<Import Project="$(CustomBeforeTraversalProps)" Condition=" '$(CustomBeforeTraversalProps)' != '' And Exists('$(CustomBeforeTraversalProps)') " />

<Target Name="ShouldSkipProject" Returns="@(ProjectToSkip)" />

<!-- For CPS/VS support. Importing in .props allows any subsequent targets to redefine this if needed -->
<Target Name="CompileDesignTime" />
<!-- When building a solution file, import Solution.props, otherwise Traversal.props -->
<Import Project="Solution.props" Condition="'$(DirectorySolutionPropsPath)' != ''" />
<Import Project="Traversal.props" Condition="'$(DirectorySolutionPropsPath)' == ''" />

<Import Project="$(CustomAfterTraversalProps)" Condition=" '$(CustomAfterTraversalProps)' != '' And Exists('$(CustomAfterTraversalProps)') " />
</Project>
Loading

0 comments on commit ed27e69

Please sign in to comment.