Skip to content

Commit

Permalink
Add support to specify SDK by element.
Browse files Browse the repository at this point in the history
Adds syntax for <Sdk> element. This is identical to using the <Project Sdk="" /> attribute and results in an implicit import at the top and bottom of the project.
  • Loading branch information
AndyGerlicher committed Apr 23, 2017
1 parent dfcf598 commit b9aa944
Show file tree
Hide file tree
Showing 11 changed files with 245 additions and 45 deletions.
8 changes: 8 additions & 0 deletions ref/net46/Microsoft.Build/Microsoft.Build.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand All @@ -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<bool> 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
{
Expand Down
18 changes: 8 additions & 10 deletions ref/netstandard1.3/Microsoft.Build/Microsoft.Build.cs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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; }
Expand All @@ -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<bool> 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
{
Expand Down
126 changes: 94 additions & 32 deletions src/Build.OM.UnitTests/Construction/ProjectSdkImplicitImport_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,49 +36,71 @@ public ProjectSdkImplicitImport_Tests()
Directory.CreateDirectory(_testSdkDirectory);
}

[Fact]
public void SdkImportsAreInLogicalProject()
[Theory]
[InlineData(@"
<Project Sdk=""{0}"">
<PropertyGroup>
<UsedToTestIfImplicitImportsAreInTheCorrectLocation>null</UsedToTestIfImplicitImportsAreInTheCorrectLocation>
</PropertyGroup>
</Project>
")]
[InlineData(@"
<Project>
<Sdk Name=""{0}"" />
<PropertyGroup>
<UsedToTestIfImplicitImportsAreInTheCorrectLocation>null</UsedToTestIfImplicitImportsAreInTheCorrectLocation>
</PropertyGroup>
</Project>
")]
public void SdkImportsAreInLogicalProject(string projectFormatString)
{
File.WriteAllText(_sdkPropsPath, "<Project><PropertyGroup><InitialImportProperty>Hello</InitialImportProperty></PropertyGroup></Project>");
File.WriteAllText(_sdkTargetsPath, "<Project><PropertyGroup><FinalImportProperty>World</FinalImportProperty></PropertyGroup></Project>");

using (new Helpers.TemporaryEnvironment("MSBuildSDKsPath", _testSdkRoot))
{
string content = $@"
<Project Sdk=""{SdkName}"">
<PropertyGroup>
<UsedToTestIfImplicitImportsAreInTheCorrectLocation>null</UsedToTestIfImplicitImportsAreInTheCorrectLocation>
</PropertyGroup>
</Project>";
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<ProjectElement> children = project.GetLogicalProject().ToList();

Assert.Equal(6, children.Count);

// <Sdk> style will have an extra ProjectElment.
var expected = projectFormatString.Contains("Sdk=") ? 6 : 7;
Assert.Equal(expected, children.Count);
}
}

[Fact]
public void SdkImportsAreInImportList()
[Theory]
[InlineData(@"
<Project Sdk=""{0}"">
<PropertyGroup>
<UsedToTestIfImplicitImportsAreInTheCorrectLocation>null</UsedToTestIfImplicitImportsAreInTheCorrectLocation>
</PropertyGroup>
</Project>
")]
[InlineData(@"
<Project>
<Sdk Name=""{0}"" />
<PropertyGroup>
<UsedToTestIfImplicitImportsAreInTheCorrectLocation>null</UsedToTestIfImplicitImportsAreInTheCorrectLocation>
</PropertyGroup>
</Project>
")]
public void SdkImportsAreInImportList(string projectFormatString)
{
File.WriteAllText(_sdkPropsPath, "<Project><PropertyGroup><InitialImportProperty>Hello</InitialImportProperty></PropertyGroup></Project>");
File.WriteAllText(_sdkTargetsPath, "<Project><PropertyGroup><FinalImportProperty>World</FinalImportProperty></PropertyGroup></Project>");

using (new Helpers.TemporaryEnvironment("MSBuildSDKsPath", _testSdkRoot))
{
string content = $@"
<Project Sdk=""{SdkName}"">
<PropertyGroup>
<UsedToTestIfImplicitImportsAreInTheCorrectLocation>null</UsedToTestIfImplicitImportsAreInTheCorrectLocation>
</PropertyGroup>
</Project>";
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);
Expand All @@ -103,8 +125,17 @@ public void SdkImportsAreInImportList()
/// <summary>
/// Verifies that when a user specifies more than one SDK that everything works as expected
/// </summary>
[Fact]
public void SdkSupportsMultiple()
[Theory]
[InlineData(@"
<Project Sdk=""{0};{1};{2}"">
</Project >")]
[InlineData(@"
<Project>
<Sdk Name=""{0}"" />
<Sdk Name=""{1}"" />
<Sdk Name=""{2}"" />
</Project>")]
public void SdkSupportsMultiple(string projectFormatString)
{
IList<string> sdkNames = new List<string>
{
Expand All @@ -123,10 +154,7 @@ public void SdkSupportsMultiple()

using (new Helpers.TemporaryEnvironment("MSBuildSDKsPath", _testSdkRoot))
{
string content = $@"
<Project Sdk=""{String.Join("; ", sdkNames)}"">
</Project>";
string content = string.Format(projectFormatString, sdkNames[0], sdkNames[1], sdkNames[2]);

ProjectRootElement projectRootElement = ProjectRootElement.Create(XmlReader.Create(new StringReader(content)));

Expand All @@ -144,8 +172,13 @@ public void SdkSupportsMultiple()
}
}

[Fact]
public void ProjectWithSdkImportsIsCloneable()
[Theory]
[InlineData(@"<Project Sdk=""{0}"" ToolsVersion=""15.0"">
")]
[InlineData(@"<Project ToolsVersion=""15.0"">
<Sdk Name=""{0}"" />
")]
public void ProjectWithSdkImportsIsCloneable(string projectFileFirstLineFormat)
{
File.WriteAllText(_sdkPropsPath, "<Project />");
File.WriteAllText(_sdkTargetsPath, "<Project />");
Expand All @@ -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 = $@"<Project Sdk=""{SdkName}"" ToolsVersion=""15.0"">
string content = $@"{string.Format(projectFileFirstLineFormat, SdkName)}
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp1.0</TargetFramework>
Expand All @@ -177,8 +210,13 @@ public void ProjectWithSdkImportsIsCloneable()
}
}

[Fact]
public void ProjectWithSdkImportsIsRemoveable()
[Theory]
[InlineData(@"<Project Sdk=""{0}"" ToolsVersion=""15.0"">
")]
[InlineData(@"<Project ToolsVersion=""15.0"">
<Sdk Name=""{0}"" />
")]
public void ProjectWithSdkImportsIsRemoveable(string projectFileFirstLineFormat)
{
File.WriteAllText(_sdkPropsPath, "<Project />");
File.WriteAllText(_sdkTargetsPath, "<Project />");
Expand All @@ -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 = $@"<Project Sdk=""{SdkName}"" ToolsVersion=""15.0"">
string content = $@"{string.Format(projectFileFirstLineFormat, SdkName)}
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp1.0</TargetFramework>
Expand Down Expand Up @@ -260,6 +298,30 @@ public void ProjectWithEmptySdkName()
}
}

/// <summary>
/// Verifies that an empty SDK attribute works and nothing is imported.
/// </summary>
[Fact]
public void ProjectWithEmptySdkNameElementThrows()
{
using (new Helpers.TemporaryEnvironment("MSBuildSDKsPath", _testSdkRoot))
{
string content = @"
<Project>
<Sdk Name="""" />
<PropertyGroup>
<UsedToTestIfImplicitImportsAreInTheCorrectLocation>null</UsedToTestIfImplicitImportsAreInTheCorrectLocation>
</PropertyGroup>
</Project>";

var e =
Assert.Throws<InvalidProjectFileException>(() => new Project(
ProjectRootElement.Create(XmlReader.Create(new StringReader(content)))));

Assert.Equal("MSB4238", e.ErrorCode);
}
}

/// <summary>
/// Verifies that an error occurs when one or more SDK names are empty.
/// </summary>
Expand Down
18 changes: 18 additions & 0 deletions src/Build/Construction/ProjectRootElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1547,6 +1547,14 @@ public ProjectWhenElement CreateWhenElement(string condition)
return ProjectWhenElement.CreateDisconnected(condition, this);
}

/// <summary>
/// Creates a project SDK element attached to this project.
/// </summary>
public ProjectSdkElement CreateProjectSdkElement(string sdkName, string sdkVersion)
{
return ProjectSdkElement.CreateDisconnected(sdkName, sdkVersion, this);
}

/// <summary>
/// Save the project to the file system, if dirty.
/// Uses the Encoding returned by the Encoding property.
Expand Down Expand Up @@ -1902,6 +1910,16 @@ internal List<ProjectImportElement> GetImplicitImportNodes(ProjectRootElement cu
nodes.Add(ProjectImportElement.CreateImplicit("Sdk.targets", currentProjectOrImport, ImplicitImportLocation.Bottom, referencedSdk));
}

foreach (var sdkNode in Children.OfType<ProjectSdkElement>())
{
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;
}

Expand Down
86 changes: 86 additions & 0 deletions src/Build/Construction/ProjectSdkElement.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using Microsoft.Build.Internal;
using Microsoft.Build.Shared;

namespace Microsoft.Build.Construction
{
/// <summary>
/// ProjectSdkElement represents the Sdk element within the MSBuild project.
/// </summary>
public class ProjectSdkElement : ProjectElementContainer
{
/// <summary>
/// Initialize a parented ProjectSdkElement
/// </summary>
internal ProjectSdkElement(XmlElementWithLocation xmlElement, ProjectRootElement parent,
ProjectRootElement containingProject)
: base(xmlElement, parent, containingProject)
{
ErrorUtilities.VerifyThrowArgumentNull(parent, "parent");
}

/// <summary>
/// Initialize an non-parented ProjectSdkElement
/// </summary>
private ProjectSdkElement(XmlElementWithLocation xmlElement, ProjectRootElement containingProject)
: base(xmlElement, null, containingProject)
{ }

/// <summary>
/// Gets or sets the name of the SDK.
/// </summary>
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);
}
}

/// <summary>
/// Gets or sets the version of the SDK.
/// </summary>
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);
}
}

/// <inheritdoc />
internal override void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent,
ProjectElement previousSibling, ProjectElement nextSibling)
{
ErrorUtilities.VerifyThrowInvalidOperation(parent is ProjectRootElement, "OM_CannotAcceptParent");
}

/// <inheritdoc />
protected override ProjectElement CreateNewInstance(ProjectRootElement owner)
{
return owner.CreateProjectSdkElement(Name, Version);
}

/// <summary>
/// Creates a non-parented ProjectSdkElement, wrapping an non-parented XmlElement.
/// Caller should then ensure the element is added to a parent
/// </summary>
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;
}
}
}
Loading

0 comments on commit b9aa944

Please sign in to comment.