diff --git a/ref/net46/Microsoft.Build/Microsoft.Build.cs b/ref/net46/Microsoft.Build/Microsoft.Build.cs index 56053b99559..a930c4a87a8 100644 --- a/ref/net46/Microsoft.Build/Microsoft.Build.cs +++ b/ref/net46/Microsoft.Build/Microsoft.Build.cs @@ -308,6 +308,7 @@ internal ProjectRootElement() { } public Microsoft.Build.Construction.ProjectOtherwiseElement CreateOtherwiseElement() { throw null; } public Microsoft.Build.Construction.ProjectOutputElement CreateOutputElement(string taskParameter, string itemType, string propertyName) { throw null; } public Microsoft.Build.Construction.ProjectExtensionsElement CreateProjectExtensionsElement() { throw null; } + public Microsoft.Build.Construction.ProjectSdkElement CreateProjectSdkElement(string sdkName, string sdkVersion) { throw null; } public Microsoft.Build.Construction.ProjectPropertyElement CreatePropertyElement(string name) { throw null; } public Microsoft.Build.Construction.ProjectPropertyGroupElement CreatePropertyGroupElement() { throw null; } public Microsoft.Build.Construction.ProjectTargetElement CreateTargetElement(string name) { throw null; } @@ -334,6 +335,13 @@ public void Save(System.Text.Encoding saveEncoding) { } public static Microsoft.Build.Construction.ProjectRootElement TryOpen(string path, Microsoft.Build.Evaluation.ProjectCollection projectCollection) { throw null; } public static Microsoft.Build.Construction.ProjectRootElement TryOpen(string path, Microsoft.Build.Evaluation.ProjectCollection projectCollection, System.Nullable preserveFormatting) { throw null; } } + public partial class ProjectSdkElement : Microsoft.Build.Construction.ProjectElementContainer + { + internal ProjectSdkElement() { } + public string Name { get { throw null; } set { } } + public string Version { get { throw null; } set { } } + protected override Microsoft.Build.Construction.ProjectElement CreateNewInstance(Microsoft.Build.Construction.ProjectRootElement owner) { throw null; } + } [System.Diagnostics.DebuggerDisplayAttribute("Name={Name} #Children={Count} Condition={Condition}")] public partial class ProjectTargetElement : Microsoft.Build.Construction.ProjectElementContainer { diff --git a/ref/netstandard1.3/Microsoft.Build/Microsoft.Build.cs b/ref/netstandard1.3/Microsoft.Build/Microsoft.Build.cs index 635902e6d9c..bdcd804cac0 100644 --- a/ref/netstandard1.3/Microsoft.Build/Microsoft.Build.cs +++ b/ref/netstandard1.3/Microsoft.Build/Microsoft.Build.cs @@ -1,13 +1,3 @@ -namespace Microsoft.Build.BackEnd -{ - public partial class DefaultSdkResolver : Microsoft.Build.Framework.SdkResolver - { - public DefaultSdkResolver() { } - public override string Name { get { throw null; } } - public override int Priority { get { throw null; } } - public override Microsoft.Build.Framework.SdkResult Resolve(Microsoft.Build.Framework.ReferencedSdk sdk, Microsoft.Build.Framework.SdkResolverContext context, Microsoft.Build.Framework.SdkResultFactory factory) { throw null; } - } -} namespace Microsoft.Build.Construction { public abstract partial class ElementLocation @@ -318,6 +308,7 @@ internal ProjectRootElement() { } public Microsoft.Build.Construction.ProjectOtherwiseElement CreateOtherwiseElement() { throw null; } public Microsoft.Build.Construction.ProjectOutputElement CreateOutputElement(string taskParameter, string itemType, string propertyName) { throw null; } public Microsoft.Build.Construction.ProjectExtensionsElement CreateProjectExtensionsElement() { throw null; } + public Microsoft.Build.Construction.ProjectSdkElement CreateProjectSdkElement(string sdkName, string sdkVersion) { throw null; } public Microsoft.Build.Construction.ProjectPropertyElement CreatePropertyElement(string name) { throw null; } public Microsoft.Build.Construction.ProjectPropertyGroupElement CreatePropertyGroupElement() { throw null; } public Microsoft.Build.Construction.ProjectTargetElement CreateTargetElement(string name) { throw null; } @@ -344,6 +335,13 @@ public void Save(System.Text.Encoding saveEncoding) { } public static Microsoft.Build.Construction.ProjectRootElement TryOpen(string path, Microsoft.Build.Evaluation.ProjectCollection projectCollection) { throw null; } public static Microsoft.Build.Construction.ProjectRootElement TryOpen(string path, Microsoft.Build.Evaluation.ProjectCollection projectCollection, System.Nullable preserveFormatting) { throw null; } } + public partial class ProjectSdkElement : Microsoft.Build.Construction.ProjectElementContainer + { + internal ProjectSdkElement() { } + public string Name { get { throw null; } set { } } + public string Version { get { throw null; } set { } } + protected override Microsoft.Build.Construction.ProjectElement CreateNewInstance(Microsoft.Build.Construction.ProjectRootElement owner) { throw null; } + } [System.Diagnostics.DebuggerDisplayAttribute("Name={Name} #Children={Count} Condition={Condition}")] public partial class ProjectTargetElement : Microsoft.Build.Construction.ProjectElementContainer { diff --git a/src/Build.OM.UnitTests/Construction/ProjectSdkImplicitImport_Tests.cs b/src/Build.OM.UnitTests/Construction/ProjectSdkImplicitImport_Tests.cs index 34f2ef3f8df..c99a43537cf 100644 --- a/src/Build.OM.UnitTests/Construction/ProjectSdkImplicitImport_Tests.cs +++ b/src/Build.OM.UnitTests/Construction/ProjectSdkImplicitImport_Tests.cs @@ -36,49 +36,71 @@ public ProjectSdkImplicitImport_Tests() Directory.CreateDirectory(_testSdkDirectory); } - [Fact] - public void SdkImportsAreInLogicalProject() + [Theory] + [InlineData(@" + + + null + + +")] + [InlineData(@" + + + + null + + +")] + public void SdkImportsAreInLogicalProject(string projectFormatString) { File.WriteAllText(_sdkPropsPath, "Hello"); File.WriteAllText(_sdkTargetsPath, "World"); using (new Helpers.TemporaryEnvironment("MSBuildSDKsPath", _testSdkRoot)) { - string content = $@" - - - null - - "; + string content = string.Format(projectFormatString, SdkName); ProjectRootElement projectRootElement = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); - Project project = new Project(projectRootElement); + var project = new Project(projectRootElement); IList children = project.GetLogicalProject().ToList(); - - Assert.Equal(6, children.Count); + + // style will have an extra ProjectElment. + var expected = projectFormatString.Contains("Sdk=") ? 6 : 7; + Assert.Equal(expected, children.Count); } } - [Fact] - public void SdkImportsAreInImportList() + [Theory] + [InlineData(@" + + + null + + +")] + [InlineData(@" + + + + null + + +")] + public void SdkImportsAreInImportList(string projectFormatString) { File.WriteAllText(_sdkPropsPath, "Hello"); File.WriteAllText(_sdkTargetsPath, "World"); using (new Helpers.TemporaryEnvironment("MSBuildSDKsPath", _testSdkRoot)) { - string content = $@" - - - null - - "; + string content = string.Format(projectFormatString, SdkName); ProjectRootElement projectRootElement = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); - Project project = new Project(projectRootElement); + var project = new Project(projectRootElement); // The XML representation of the project should indicate there are no imports Assert.Equal(0, projectRootElement.Imports.Count); @@ -103,8 +125,17 @@ public void SdkImportsAreInImportList() /// /// Verifies that when a user specifies more than one SDK that everything works as expected /// - [Fact] - public void SdkSupportsMultiple() + [Theory] + [InlineData(@" + +")] + [InlineData(@" + + + + +")] + public void SdkSupportsMultiple(string projectFormatString) { IList sdkNames = new List { @@ -123,10 +154,7 @@ public void SdkSupportsMultiple() using (new Helpers.TemporaryEnvironment("MSBuildSDKsPath", _testSdkRoot)) { - string content = $@" - - - "; + string content = string.Format(projectFormatString, sdkNames[0], sdkNames[1], sdkNames[2]); ProjectRootElement projectRootElement = ProjectRootElement.Create(XmlReader.Create(new StringReader(content))); @@ -144,8 +172,13 @@ public void SdkSupportsMultiple() } } - [Fact] - public void ProjectWithSdkImportsIsCloneable() + [Theory] + [InlineData(@" +")] + [InlineData(@" + +")] + public void ProjectWithSdkImportsIsCloneable(string projectFileFirstLineFormat) { File.WriteAllText(_sdkPropsPath, ""); File.WriteAllText(_sdkTargetsPath, ""); @@ -154,7 +187,7 @@ public void ProjectWithSdkImportsIsCloneable() { // Based on the new-console-project CLI template (but not matching exactly // should not be a deal-breaker). - string content = $@" + string content = $@"{string.Format(projectFileFirstLineFormat, SdkName)} Exe netcoreapp1.0 @@ -177,8 +210,13 @@ public void ProjectWithSdkImportsIsCloneable() } } - [Fact] - public void ProjectWithSdkImportsIsRemoveable() + [Theory] + [InlineData(@" +")] + [InlineData(@" + +")] + public void ProjectWithSdkImportsIsRemoveable(string projectFileFirstLineFormat) { File.WriteAllText(_sdkPropsPath, ""); File.WriteAllText(_sdkTargetsPath, ""); @@ -187,7 +225,7 @@ public void ProjectWithSdkImportsIsRemoveable() { // Based on the new-console-project CLI template (but not matching exactly // should not be a deal-breaker). - string content = $@" + string content = $@"{string.Format(projectFileFirstLineFormat, SdkName)} Exe netcoreapp1.0 @@ -260,6 +298,30 @@ public void ProjectWithEmptySdkName() } } + /// + /// Verifies that an empty SDK attribute works and nothing is imported. + /// + [Fact] + public void ProjectWithEmptySdkNameElementThrows() + { + using (new Helpers.TemporaryEnvironment("MSBuildSDKsPath", _testSdkRoot)) + { + string content = @" + + + + null + + "; + + var e = + Assert.Throws(() => new Project( + ProjectRootElement.Create(XmlReader.Create(new StringReader(content))))); + + Assert.Equal("MSB4238", e.ErrorCode); + } + } + /// /// Verifies that an error occurs when one or more SDK names are empty. /// diff --git a/src/Build/Construction/ProjectRootElement.cs b/src/Build/Construction/ProjectRootElement.cs index ce5cf06db9f..163a150ea99 100644 --- a/src/Build/Construction/ProjectRootElement.cs +++ b/src/Build/Construction/ProjectRootElement.cs @@ -1547,6 +1547,14 @@ public ProjectWhenElement CreateWhenElement(string condition) return ProjectWhenElement.CreateDisconnected(condition, this); } + /// + /// Creates a project SDK element attached to this project. + /// + public ProjectSdkElement CreateProjectSdkElement(string sdkName, string sdkVersion) + { + return ProjectSdkElement.CreateDisconnected(sdkName, sdkVersion, this); + } + /// /// Save the project to the file system, if dirty. /// Uses the Encoding returned by the Encoding property. @@ -1902,6 +1910,16 @@ internal List GetImplicitImportNodes(ProjectRootElement cu nodes.Add(ProjectImportElement.CreateImplicit("Sdk.targets", currentProjectOrImport, ImplicitImportLocation.Bottom, referencedSdk)); } + foreach (var sdkNode in Children.OfType()) + { + var referencedSdk = new ReferencedSdk( + sdkNode.XmlElement.GetAttribute("Name"), + sdkNode.XmlElement.GetAttribute("Version")); + + nodes.Add(ProjectImportElement.CreateImplicit("Sdk.props", currentProjectOrImport, ImplicitImportLocation.Top, referencedSdk)); + nodes.Add(ProjectImportElement.CreateImplicit("Sdk.targets", currentProjectOrImport, ImplicitImportLocation.Bottom, referencedSdk)); + } + return nodes; } diff --git a/src/Build/Construction/ProjectSdkElement.cs b/src/Build/Construction/ProjectSdkElement.cs new file mode 100644 index 00000000000..882d4039d5e --- /dev/null +++ b/src/Build/Construction/ProjectSdkElement.cs @@ -0,0 +1,86 @@ +using Microsoft.Build.Internal; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Construction +{ + /// + /// ProjectSdkElement represents the Sdk element within the MSBuild project. + /// + public class ProjectSdkElement : ProjectElementContainer + { + /// + /// Initialize a parented ProjectSdkElement + /// + internal ProjectSdkElement(XmlElementWithLocation xmlElement, ProjectRootElement parent, + ProjectRootElement containingProject) + : base(xmlElement, parent, containingProject) + { + ErrorUtilities.VerifyThrowArgumentNull(parent, "parent"); + } + + /// + /// Initialize an non-parented ProjectSdkElement + /// + private ProjectSdkElement(XmlElementWithLocation xmlElement, ProjectRootElement containingProject) + : base(xmlElement, null, containingProject) + { } + + /// + /// Gets or sets the name of the SDK. + /// + public string Name + { + get { return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.sdkName); } + set + { + ErrorUtilities.VerifyThrowArgumentLength(value, XMakeAttributes.sdkName); + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.sdkName, value); + MarkDirty($"Set SDK Version to {value}", XMakeAttributes.sdkName); + } + } + + /// + /// Gets or sets the version of the SDK. + /// + public string Version + { + get { return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.sdkVersion); } + set + { + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.sdkVersion, value); + MarkDirty($"Set SDK Version to {value}", XMakeAttributes.sdkVersion); + } + } + + /// + internal override void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, + ProjectElement previousSibling, ProjectElement nextSibling) + { + ErrorUtilities.VerifyThrowInvalidOperation(parent is ProjectRootElement, "OM_CannotAcceptParent"); + } + + /// + protected override ProjectElement CreateNewInstance(ProjectRootElement owner) + { + return owner.CreateProjectSdkElement(Name, Version); + } + + /// + /// Creates a non-parented ProjectSdkElement, wrapping an non-parented XmlElement. + /// Caller should then ensure the element is added to a parent + /// + internal static ProjectSdkElement CreateDisconnected(string sdkName, string sdkVersion, + ProjectRootElement containingProject) + { + var element = containingProject.CreateElement(XMakeElements.sdk); + + var sdkElement = new ProjectSdkElement(element, containingProject) + { + Name = sdkName, + Version = sdkVersion + }; + + return sdkElement; + } + } +} diff --git a/src/Build/Evaluation/Evaluator.cs b/src/Build/Evaluation/Evaluator.cs index 4cabaa81556..7ffd5085370 100644 --- a/src/Build/Evaluation/Evaluator.cs +++ b/src/Build/Evaluation/Evaluator.cs @@ -998,7 +998,7 @@ element is ProjectOtherwiseElement #endif // Get all the implicit imports (e.g. , but not ) - var implicitImports = currentProjectOrImport.CreateImplicitImportNodes(currentProjectOrImport); + var implicitImports = currentProjectOrImport.GetImplicitImportNodes(currentProjectOrImport); // Evaluate the "top" implicit imports as if they were the first entry in the file. foreach (var import in implicitImports.Where(i => i.ImplicitImportLocation == ImplicitImportLocation.Top)) @@ -1158,6 +1158,11 @@ child is ProjectItemElement || continue; } + if (element is ProjectSdkElement) + { + continue; // This case is handled by implicit imports. + } + ErrorUtilities.ThrowInternalError("Unexpected child type"); } diff --git a/src/Build/Evaluation/ProjectParser.cs b/src/Build/Evaluation/ProjectParser.cs index bcf8cae92de..9f81b243933 100644 --- a/src/Build/Evaluation/ProjectParser.cs +++ b/src/Build/Evaluation/ProjectParser.cs @@ -237,7 +237,9 @@ private void ParseProjectRootElementChildren(XmlElementWithLocation element) case XMakeElements.projectExtensions: child = ParseProjectExtensionsElement(childElement); break; - + case XMakeElements.sdk: + child = ParseProjectSdkElement(childElement); + break; // Obsolete case XMakeElements.error: case XMakeElements.warning: @@ -946,5 +948,18 @@ private ProjectExtensionsElement ParseProjectExtensionsElement(XmlElementWithLoc // All children inside ProjectExtensions are ignored, since they are only part of its value return new ProjectExtensionsElement(element, _project, _project); } + + /// + /// Parse a ProjectExtensionsElement + /// + private ProjectSdkElement ParseProjectSdkElement(XmlElementWithLocation element) + { + if (string.IsNullOrEmpty(element.GetAttribute(XMakeAttributes.sdkName))) + { + ProjectErrorUtilities.ThrowInvalidProject(element.Location, "InvalidSdkElementName", element.Name); + } + + return new ProjectSdkElement(element, _project, _project); + } } } diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index 934a5309104..455e4289091 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -238,6 +238,7 @@ true + diff --git a/src/Build/Resources/Strings.resx b/src/Build/Resources/Strings.resx index 8fd80e4239a..d9875c15e9f 100644 --- a/src/Build/Resources/Strings.resx +++ b/src/Build/Resources/Strings.resx @@ -1198,6 +1198,10 @@ MSB4237: An SDK resolver was found but could not be loaded. Error: {0}. {StrBegin="MSB4237: "} + + MSB4238: The name "{0}" is not a valid SDK name. + {StrBegin="MSB4238: "} + MSB4189: <{1}> is not a valid child of the <{0}> element. {StrBegin="MSB4189: "} @@ -1605,7 +1609,7 @@ Utilization: {0} Average Utilization: {1:###.0} MSB4128 is being used in FileLogger.cs (can't be added here yet as strings are currently frozen) MSB4129 is used by Shared\XmlUtilities.cs (can't be added here yet as strings are currently frozen) - Next message code should be MSB4238. + Next message code should be MSB4239. Some unused codes which can also be reused (because their messages were deleted, and UE hasn't indexed the codes yet): diff --git a/src/Shared/XMakeAttributes.cs b/src/Shared/XMakeAttributes.cs index b34759d7653..f4b7e0f933e 100644 --- a/src/Shared/XMakeAttributes.cs +++ b/src/Shared/XMakeAttributes.cs @@ -44,6 +44,8 @@ internal static class XMakeAttributes internal const string itemName = "ItemName"; internal const string propertyName = "PropertyName"; internal const string sdk = "Sdk"; + internal const string sdkName = "Name"; + internal const string sdkVersion = "Version"; internal const string toolsVersion = "ToolsVersion"; internal const string runtime = "Runtime"; internal const string msbuildRuntime = "MSBuildRuntime"; diff --git a/src/Shared/XMakeElements.cs b/src/Shared/XMakeElements.cs index d059744d7a7..d5d047d9d73 100644 --- a/src/Shared/XMakeElements.cs +++ b/src/Shared/XMakeElements.cs @@ -34,6 +34,7 @@ internal static class XMakeElements internal const string usingTaskParameterGroup = "ParameterGroup"; internal const string usingTaskParameter = "Parameter"; internal const string usingTaskBody = "Task"; + internal const string sdk = "Sdk"; internal static readonly char[] illegalTargetNameCharacters = new char[] { '$', '@', '(', ')', '%', '*', '?', '.' };