-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Expose import tree as MSBuildImportedProject items #13681
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
80ba8d1
b77f46e
4b5d3f7
e5985a5
ad9566a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,241 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.Collections.Generic; | ||
| using System.IO; | ||
| using System.Linq; | ||
| using Microsoft.Build.Definition; | ||
| using Microsoft.Build.Evaluation; | ||
| using Microsoft.Build.Execution; | ||
| using Microsoft.Build.Unittest; | ||
| using Microsoft.Build.UnitTests; | ||
| using Shouldly; | ||
| using Xunit; | ||
|
|
||
| namespace Microsoft.Build.UnitTests.OM.Instance | ||
| { | ||
| /// <summary> | ||
| /// Tests for the MSBuildImportedProject items synthesized from the import closure. | ||
| /// </summary> | ||
| public class ProjectInstance_ImportedProjectItems_Tests | ||
| { | ||
| private readonly ITestOutputHelper _output; | ||
|
|
||
| public ProjectInstance_ImportedProjectItems_Tests(ITestOutputHelper output) | ||
| { | ||
| _output = output; | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ImportedProjectItemsNotCreatedWithoutOptIn() | ||
| { | ||
| using TestEnvironment env = TestEnvironment.Create(_output); | ||
|
|
||
| string importContent = "<Project />"; | ||
| var importFile = env.CreateFile("import.targets", importContent); | ||
|
|
||
| string projectContent = $""" | ||
| <Project> | ||
| <Import Project="{importFile.Path}" /> | ||
| </Project> | ||
| """; | ||
| var projectFile = env.CreateFile("test.proj", projectContent); | ||
|
|
||
| using var collection = new ProjectCollection(); | ||
| var project = new Project(projectFile.Path, globalProperties: null, toolsVersion: null, collection); | ||
| ProjectInstance instance = project.CreateProjectInstance(); | ||
|
|
||
| instance.GetItems("MSBuildImportedProject").Count.ShouldBe(0); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ImportedProjectItemsCreatedWhenPropertyIsSet() | ||
| { | ||
| using TestEnvironment env = TestEnvironment.Create(_output); | ||
|
|
||
| string importContent = "<Project />"; | ||
| var importFile = env.CreateFile("import.targets", importContent); | ||
|
|
||
| string projectContent = $""" | ||
| <Project> | ||
| <PropertyGroup> | ||
| <MSBuildProvideImportedProjects>true</MSBuildProvideImportedProjects> | ||
| </PropertyGroup> | ||
| <Import Project="{importFile.Path}" /> | ||
| </Project> | ||
| """; | ||
| var projectFile = env.CreateFile("test.proj", projectContent); | ||
|
|
||
| using var collection = new ProjectCollection(); | ||
| var project = new Project(projectFile.Path, globalProperties: null, toolsVersion: null, collection); | ||
| ProjectInstance instance = project.CreateProjectInstance(); | ||
|
|
||
| var items = instance.GetItems("MSBuildImportedProject").ToList(); | ||
| items.Count.ShouldBe(1); | ||
| items[0].EvaluatedInclude.ShouldBe(importFile.Path); | ||
| items[0].GetMetadataValue("ImportingProjectPath").ShouldBe(projectFile.Path); | ||
| items[0].GetMetadataValue("Sdk").ShouldBeEmpty(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ImportedProjectItemsHaveCorrectImportingPath() | ||
| { | ||
| using TestEnvironment env = TestEnvironment.Create(_output); | ||
|
|
||
| string import2Content = "<Project />"; | ||
| var import2File = env.CreateFile("import2.targets", import2Content); | ||
|
|
||
| string import1Content = $""" | ||
| <Project> | ||
| <Import Project="{import2File.Path}" /> | ||
| </Project> | ||
| """; | ||
| var import1File = env.CreateFile("import1.targets", import1Content); | ||
|
|
||
| string projectContent = $""" | ||
| <Project> | ||
| <PropertyGroup> | ||
| <MSBuildProvideImportedProjects>true</MSBuildProvideImportedProjects> | ||
| </PropertyGroup> | ||
| <Import Project="{import1File.Path}" /> | ||
| </Project> | ||
| """; | ||
| var projectFile = env.CreateFile("test.proj", projectContent); | ||
|
|
||
| using var collection = new ProjectCollection(); | ||
| var project = new Project(projectFile.Path, globalProperties: null, toolsVersion: null, collection); | ||
| ProjectInstance instance = project.CreateProjectInstance(); | ||
|
|
||
| var items = instance.GetItems("MSBuildImportedProject").ToList(); | ||
| items.Count.ShouldBe(2); | ||
|
|
||
| // project -> import1 | ||
| var item1 = items.First(i => i.EvaluatedInclude == import1File.Path); | ||
| item1.GetMetadataValue("ImportingProjectPath").ShouldBe(projectFile.Path); | ||
|
|
||
| // import1 -> import2 | ||
| var item2 = items.First(i => i.EvaluatedInclude == import2File.Path); | ||
| item2.GetMetadataValue("ImportingProjectPath").ShouldBe(import1File.Path); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ImportedProjectItemsExcludeRootProject() | ||
| { | ||
| using TestEnvironment env = TestEnvironment.Create(_output); | ||
|
|
||
| string projectContent = """ | ||
| <Project> | ||
| <PropertyGroup> | ||
| <MSBuildProvideImportedProjects>true</MSBuildProvideImportedProjects> | ||
| </PropertyGroup> | ||
| </Project> | ||
| """; | ||
| var projectFile = env.CreateFile("test.proj", projectContent); | ||
|
|
||
| using var collection = new ProjectCollection(); | ||
| var project = new Project(projectFile.Path, globalProperties: null, toolsVersion: null, collection); | ||
| ProjectInstance instance = project.CreateProjectInstance(); | ||
|
|
||
| instance.GetItems("MSBuildImportedProject").Count.ShouldBe(0); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ImportedProjectItemsAvailableToTargets() | ||
| { | ||
| using TestEnvironment env = TestEnvironment.Create(_output); | ||
|
|
||
| string importContent = "<Project />"; | ||
| var importFile = env.CreateFile("import.targets", importContent); | ||
|
|
||
| string projectContent = $""" | ||
| <Project> | ||
| <PropertyGroup> | ||
| <MSBuildProvideImportedProjects>true</MSBuildProvideImportedProjects> | ||
| </PropertyGroup> | ||
| <Import Project="{importFile.Path}" /> | ||
| <Target Name="ShowImports"> | ||
| <Message Text="Import: %(MSBuildImportedProject.Identity) from %(MSBuildImportedProject.ImportingProjectPath)" Importance="High" /> | ||
| </Target> | ||
| </Project> | ||
| """; | ||
| var projectFile = env.CreateFile("test.proj", projectContent); | ||
|
|
||
| using var collection = new ProjectCollection(); | ||
| var project = new Project(projectFile.Path, globalProperties: null, toolsVersion: null, collection); | ||
| ProjectInstance instance = project.CreateProjectInstance(); | ||
|
|
||
| var mockLogger = new MockLogger(_output); | ||
| instance.Build(["ShowImports"], [mockLogger]).ShouldBeTrue(); | ||
| mockLogger.AssertLogContains($"Import: {importFile.Path} from {projectFile.Path}"); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ImportedProjectItemsHaveSdkMetadataForSdkImports() | ||
| { | ||
| using TestEnvironment env = TestEnvironment.Create(_output); | ||
|
|
||
| string testSdkDirectory = env.CreateFolder().Path; | ||
| File.WriteAllText(Path.Combine(testSdkDirectory, "Sdk.props"), "<Project />"); | ||
| File.WriteAllText(Path.Combine(testSdkDirectory, "Sdk.targets"), "<Project />"); | ||
|
|
||
| var projectOptions = SdkUtilities.CreateProjectOptionsWithResolver( | ||
| new SdkUtilities.FileBasedMockSdkResolver(new Dictionary<string, string> | ||
| { | ||
| { "MyTestSdk", testSdkDirectory }, | ||
| })); | ||
|
|
||
| string projectContent = """ | ||
| <Project Sdk="MyTestSdk"> | ||
| <PropertyGroup> | ||
| <MSBuildProvideImportedProjects>true</MSBuildProvideImportedProjects> | ||
| </PropertyGroup> | ||
| </Project> | ||
| """; | ||
|
|
||
| using ProjectRootElementFromString projectRootElementFromString = new(projectContent); | ||
| Project project = Project.FromProjectRootElement( | ||
| projectRootElementFromString.Project, | ||
| projectOptions); | ||
| ProjectInstance instance = project.CreateProjectInstance(); | ||
|
|
||
| var items = instance.GetItems("MSBuildImportedProject").ToList(); | ||
| items.Count.ShouldBe(2); // Sdk.props and Sdk.targets | ||
|
|
||
| // Both should have Sdk metadata set to "MyTestSdk" | ||
| foreach (var item in items) | ||
| { | ||
| item.GetMetadataValue("Sdk").ShouldBe("MyTestSdk"); | ||
| } | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ImportedProjectItemsCreatedWhenSetViaGlobalProperty() | ||
| { | ||
| using TestEnvironment env = TestEnvironment.Create(_output); | ||
|
|
||
| string importContent = "<Project />"; | ||
| var importFile = env.CreateFile("import.targets", importContent); | ||
|
|
||
| string projectContent = $""" | ||
| <Project> | ||
| <Import Project="{importFile.Path}" /> | ||
| </Project> | ||
| """; | ||
| var projectFile = env.CreateFile("test.proj", projectContent); | ||
|
|
||
| var globalProperties = new Dictionary<string, string> | ||
| { | ||
| { "MSBuildProvideImportedProjects", "true" }, | ||
| }; | ||
|
|
||
| using var collection = new ProjectCollection(); | ||
| var project = new Project(projectFile.Path, globalProperties, toolsVersion: null, collection); | ||
| ProjectInstance instance = project.CreateProjectInstance(); | ||
|
|
||
| var items = instance.GetItems("MSBuildImportedProject").ToList(); | ||
| items.Count.ShouldBe(1); | ||
| items[0].EvaluatedInclude.ShouldBe(importFile.Path); | ||
| items[0].GetMetadataValue("ImportingProjectPath").ShouldBe(projectFile.Path); | ||
| } | ||
| } | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not convinced this is the right place for the code. From the conversation with @rainersigwald, my understanding was that we'd place this in the evaluation stage: at the start of the items pass, after the property evaluation pass. This change runs after evaluation, during |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3376,6 +3376,42 @@ private void CreateImportsSnapshot(IList<ResolvedImport> importClosure, IList<Re | |
|
|
||
| _importPathsIncludingDuplicates = importPathsIncludingDuplicates; | ||
| ImportPathsIncludingDuplicates = importPathsIncludingDuplicates.AsReadOnly(); | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would prefer to make this a separate helper method instead, if we would keep this code here. |
||
| // Optionally synthesize MSBuildImportedProject items from the import closure, | ||
| // making the import tree available to targets and tasks as regular items. | ||
| if (string.Equals( | ||
| _properties?.GetProperty(Constants.MSBuildProvideImportedProjectsPropertyName)?.EvaluatedValue, | ||
| "true", | ||
| StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
|
drewnoakes marked this conversation as resolved.
|
||
| List<KeyValuePair<string, string>> metadata = null; | ||
|
|
||
| foreach (ResolvedImport import in importClosure) | ||
| { | ||
| if (import.ImportingElement is null) | ||
| { | ||
| // Skip the outer project itself, which is not an import. | ||
| continue; | ||
| } | ||
|
|
||
| metadata ??= []; | ||
| metadata.Clear(); | ||
|
|
||
| metadata.Add(new("ImportingProjectPath", import.ImportingElement.ContainingProject.EscapedFullPath)); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's make the constants for item and metadata names as well. |
||
|
|
||
| if (import.SdkResult?.SdkReference?.Name is { } sdkName) | ||
| { | ||
| metadata.Add(new("Sdk", sdkName)); | ||
| } | ||
|
|
||
| _items.Add(new ProjectItemInstance( | ||
| project: this, | ||
| itemType: "MSBuildImportedProject", | ||
| includeEscaped: import.ImportedProject.EscapedFullPath, | ||
| directMetadata: metadata, | ||
| definingFileEscaped: EscapingUtilities.Escape(FullPath))); | ||
| } | ||
|
drewnoakes marked this conversation as resolved.
|
||
| } | ||
|
drewnoakes marked this conversation as resolved.
|
||
| } | ||
|
|
||
| /// <summary> | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.