diff --git a/src/Traversal.UnitTests/CustomProjectCreatorTemplates.cs b/src/Traversal.UnitTests/CustomProjectCreatorTemplates.cs index 161d9c40..cebb1b1d 100644 --- a/src/Traversal.UnitTests/CustomProjectCreatorTemplates.cs +++ b/src/Traversal.UnitTests/CustomProjectCreatorTemplates.cs @@ -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 @@ -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 + { + ["AdditionalProperties"] = "Configuration=Debug; Platform=AnyCPU", + ["Platform"] = "AnyCPU", + ["Configuration"] = "Debug", + ["ToolsVersion"] = string.Empty, + ["SkipNonexistentProjects"] = bool.FalseString, + }); + }) + .Target("Build", outputs: "@(CollectedBuildOutput)") + .Task("MSBuild", parameters: new Dictionary + { + ["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, diff --git a/src/Traversal.UnitTests/Microsoft.Build.Traversal.UnitTests.csproj b/src/Traversal.UnitTests/Microsoft.Build.Traversal.UnitTests.csproj index 88945164..61268d15 100644 --- a/src/Traversal.UnitTests/Microsoft.Build.Traversal.UnitTests.csproj +++ b/src/Traversal.UnitTests/Microsoft.Build.Traversal.UnitTests.csproj @@ -15,7 +15,6 @@ - - + diff --git a/src/Traversal.UnitTests/SolutionTests.cs b/src/Traversal.UnitTests/SolutionTests.cs new file mode 100644 index 00000000..4faf81c1 --- /dev/null +++ b/src/Traversal.UnitTests/SolutionTests.cs @@ -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 { ["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 { ["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 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); + } + } +} \ No newline at end of file diff --git a/src/Traversal.UnitTests/TraversalTests.cs b/src/Traversal.UnitTests/TraversalTests.cs index 6e75ad2e..4d4463cf 100644 --- a/src/Traversal.UnitTests/TraversalTests.cs +++ b/src/Traversal.UnitTests/TraversalTests.cs @@ -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 { ["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 { ["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 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")] diff --git a/src/Traversal/README.md b/src/Traversal/README.md index c9cc53f9..0576d2ad 100644 --- a/src/Traversal/README.md +++ b/src/Traversal/README.md @@ -39,7 +39,88 @@ A traversal project can also reference other traversal projects. This is useful ``` +## 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 `` items: +```xml + + + + + + + +``` + +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 + + + + + + +``` + +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 + + + +``` + +Directory.Solution.targets: +```xml + + + +``` + +Finally, add a `ShouldSkipProject` target to your `Directory.Build.targets`. Use the target below as a template: + +```xml + + + + + + +``` + +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 @@ -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 - + $(CustomAfterTraversalTargets);My.After.Traversal.targets @@ -77,7 +158,7 @@ The following properties control global properties passed to different parts of Set some properties during build. ```xml - + Property1=true;EnableSomething=true @@ -102,7 +183,7 @@ The following properties control the invocation of the to traversed projects. Change the `TestInParallel` setting for the Test target. ```xml - + true @@ -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 - + diff --git a/src/Traversal/Sdk/Sdk.props b/src/Traversal/Sdk/Sdk.props index c90904d6..1e44ff54 100644 --- a/src/Traversal/Sdk/Sdk.props +++ b/src/Traversal/Sdk/Sdk.props @@ -4,53 +4,24 @@ Licensed under the MIT license. --> - + true false - - - - - - $(MSBuildAllProjects);$(MsBuildThisFileFullPath) - - - dirs.proj - - true - - - PackageReference - - - true - - - - false - - true - - + + + - - + + + diff --git a/src/Traversal/Sdk/Sdk.targets b/src/Traversal/Sdk/Sdk.targets index 40b4a936..c39af4f0 100644 --- a/src/Traversal/Sdk/Sdk.targets +++ b/src/Traversal/Sdk/Sdk.targets @@ -4,277 +4,27 @@ Licensed under the MIT license. --> - + - - - bin\Debug\ - bin\$(Configuration)\ - bin\$(Configuration)\$(Platform)\ - - - - - - - false - - - net45 - - - - - - - - - - - - - - - - - - - - - - - - - - $(MSBuildAllProjects);$(MsBuildThisFileFullPath) - - - BuildOnlySettings; - PrepareForBuild; - PreBuildEvent; - ResolveReferences; - PostBuildEvent - - - - - - BeforeResolveReferences; - AfterResolveReferences - - - - Build - - - - Build - - - - BeforeClean; - UnmanagedUnregistration; - CoreClean; - PrepareProjectReferences; - CleanPublishFolder; - AfterClean - - - - ResolveReferences; - - - - Build; - - - - - - - - - - - - - - - - - - - - - - - - - - true - false - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + <_NonExistentProjectToSkip Include="@(ProjectToSkip)" Condition="!Exists('%(ProjectToSkip.Identity)')"/> + + + + + - - - - - - - - - - - - - - diff --git a/src/Traversal/Sdk/Solution.props b/src/Traversal/Sdk/Solution.props new file mode 100644 index 00000000..bdc11766 --- /dev/null +++ b/src/Traversal/Sdk/Solution.props @@ -0,0 +1,9 @@ + + + + + diff --git a/src/Traversal/Sdk/Solution.targets b/src/Traversal/Sdk/Solution.targets new file mode 100644 index 00000000..caf1d529 --- /dev/null +++ b/src/Traversal/Sdk/Solution.targets @@ -0,0 +1,20 @@ + + + + + + + + + diff --git a/src/Traversal/Sdk/Traversal.props b/src/Traversal/Sdk/Traversal.props new file mode 100644 index 00000000..9f77db4c --- /dev/null +++ b/src/Traversal/Sdk/Traversal.props @@ -0,0 +1,42 @@ + + + + + + + + dirs.proj + + true + + + PackageReference + + + true + + + + + + false + + true + + + + + + \ No newline at end of file diff --git a/src/Traversal/Sdk/Traversal.targets b/src/Traversal/Sdk/Traversal.targets new file mode 100644 index 00000000..30582484 --- /dev/null +++ b/src/Traversal/Sdk/Traversal.targets @@ -0,0 +1,289 @@ + + + + + + bin\Debug\ + bin\$(Configuration)\ + bin\$(Configuration)\$(Platform)\ + + + + + + + false + + + net45 + + + + + + + + + + + + + + + + + + + + + + + + + + $(MSBuildAllProjects);$(MsBuildThisFileFullPath) + + + BuildOnlySettings; + PrepareForBuild; + PreBuildEvent; + ResolveReferences; + PostBuildEvent + + + + + + BeforeResolveReferences; + AfterResolveReferences + + + + Build + + + + Build + + + + BeforeClean; + UnmanagedUnregistration; + CoreClean; + PrepareProjectReferences; + CleanPublishFolder; + AfterClean + + + + ResolveReferences; + + + + Build; + + + + + + + + + + + + + + + + + + + + + + + + + + true + false + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Traversal/version.json b/src/Traversal/version.json index 129c5886..bd071699 100644 --- a/src/Traversal/version.json +++ b/src/Traversal/version.json @@ -1,4 +1,4 @@ { "inherit": true, - "version": "3.4" + "version": "4.0" }