Skip to content

Commit

Permalink
Add support for SDK Resolvers (#2002)
Browse files Browse the repository at this point in the history
* Adds support for MSBuild to discover plug-in based resolvers for the
'SDK' import feature. A resolver assembly can be placed in:
    MSBuild\SdkResolver\(ResolverName)\(ResolverName).dll

MSBuild will create an instance of any class that implements SdkResolver
and attempt to resolve all SDK references with those resolver (sorted by
SdkResolver.Priority).

* Adds support for MSBuild to consume an Sdk reference as element (as an
alternative to a property on the Project element). Both versions of the
syntax will add an implicit Import element at the top and the bottom of
the file.

Related to #1493
  • Loading branch information
AndyGerlicher authored Apr 25, 2017
1 parent 134f1b7 commit 9e22548
Show file tree
Hide file tree
Showing 35 changed files with 1,378 additions and 122 deletions.
1 change: 1 addition & 0 deletions build/NuGetPackages/Microsoft.Build.Framework.nuspec
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<dependency id="System.Collections" version="4.0.11" />
<dependency id="System.Diagnostics.Debug" version="4.0.11" />
<dependency id="System.Globalization" version="4.0.11" />
<dependency id="System.Linq" version="4.1.0" />
<dependency id="System.Runtime" version="4.1.0" />
<dependency id="System.Runtime.InteropServices" version="4.1.0" />
<dependency id="System.Threading" version="4.0.11" />
Expand Down
42 changes: 42 additions & 0 deletions ref/net46/Microsoft.Build.Framework/Microsoft.Build.Framework.cs
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,48 @@ public sealed partial class RunInSTAAttribute : System.Attribute
{
public RunInSTAAttribute() { }
}
public abstract partial class SdkLogger
{
protected SdkLogger() { }
public abstract void LogMessage(string message, Microsoft.Build.Framework.MessageImportance messageImportance=(Microsoft.Build.Framework.MessageImportance)(2));
}
public sealed partial class SdkReference : System.IEquatable<Microsoft.Build.Framework.SdkReference>
{
public SdkReference(string name, string version, string minimumVersion) { }
public string MinimumVersion { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public string Name { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public string Version { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public bool Equals(Microsoft.Build.Framework.SdkReference other) { throw null; }
public override bool Equals(object obj) { throw null; }
public override int GetHashCode() { throw null; }
public override string ToString() { throw null; }
public static bool TryParse(string sdk, out Microsoft.Build.Framework.SdkReference sdkReference) { sdkReference = default(Microsoft.Build.Framework.SdkReference); throw null; }
}
public abstract partial class SdkResolver
{
protected SdkResolver() { }
public abstract string Name { get; }
public abstract int Priority { get; }
public abstract Microsoft.Build.Framework.SdkResult Resolve(Microsoft.Build.Framework.SdkReference sdkReference, Microsoft.Build.Framework.SdkResolverContext resolverContext, Microsoft.Build.Framework.SdkResultFactory factory);
}
public abstract partial class SdkResolverContext
{
protected SdkResolverContext() { }
public virtual Microsoft.Build.Framework.SdkLogger Logger { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]protected set { } }
public virtual string ProjectFilePath { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]protected set { } }
public virtual string SolutionFilePath { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]protected set { } }
}
public abstract partial class SdkResult
{
protected SdkResult() { }
public bool Success { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]protected set { } }
}
public abstract partial class SdkResultFactory
{
protected SdkResultFactory() { }
public abstract Microsoft.Build.Framework.SdkResult IndicateFailure(System.Collections.Generic.IEnumerable<string> errors, System.Collections.Generic.IEnumerable<string> warnings=null);
public abstract Microsoft.Build.Framework.SdkResult IndicateSuccess(string path, string version, System.Collections.Generic.IEnumerable<string> warnings=null);
}
public partial class TargetFinishedEventArgs : Microsoft.Build.Framework.BuildStatusEventArgs
{
protected TargetFinishedEventArgs() { }
Expand Down
9 changes: 9 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,14 @@ 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 MinimumVersion { get { throw null; } set { } }
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
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,48 @@ public sealed partial class RunInSTAAttribute : System.Attribute
{
public RunInSTAAttribute() { }
}
public abstract partial class SdkLogger
{
protected SdkLogger() { }
public abstract void LogMessage(string message, Microsoft.Build.Framework.MessageImportance messageImportance=(Microsoft.Build.Framework.MessageImportance)(2));
}
public sealed partial class SdkReference : System.IEquatable<Microsoft.Build.Framework.SdkReference>
{
public SdkReference(string name, string version, string minimumVersion) { }
public string MinimumVersion { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public string Name { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public string Version { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public bool Equals(Microsoft.Build.Framework.SdkReference other) { throw null; }
public override bool Equals(object obj) { throw null; }
public override int GetHashCode() { throw null; }
public override string ToString() { throw null; }
public static bool TryParse(string sdk, out Microsoft.Build.Framework.SdkReference sdkReference) { sdkReference = default(Microsoft.Build.Framework.SdkReference); throw null; }
}
public abstract partial class SdkResolver
{
protected SdkResolver() { }
public abstract string Name { get; }
public abstract int Priority { get; }
public abstract Microsoft.Build.Framework.SdkResult Resolve(Microsoft.Build.Framework.SdkReference sdkReference, Microsoft.Build.Framework.SdkResolverContext resolverContext, Microsoft.Build.Framework.SdkResultFactory factory);
}
public abstract partial class SdkResolverContext
{
protected SdkResolverContext() { }
public virtual Microsoft.Build.Framework.SdkLogger Logger { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]protected set { } }
public virtual string ProjectFilePath { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]protected set { } }
public virtual string SolutionFilePath { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]protected set { } }
}
public abstract partial class SdkResult
{
protected SdkResult() { }
public bool Success { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]protected set { } }
}
public abstract partial class SdkResultFactory
{
protected SdkResultFactory() { }
public abstract Microsoft.Build.Framework.SdkResult IndicateFailure(System.Collections.Generic.IEnumerable<string> errors, System.Collections.Generic.IEnumerable<string> warnings=null);
public abstract Microsoft.Build.Framework.SdkResult IndicateSuccess(string path, string version, System.Collections.Generic.IEnumerable<string> warnings=null);
}
public partial class TargetFinishedEventArgs : Microsoft.Build.Framework.BuildStatusEventArgs
{
protected TargetFinishedEventArgs() { }
Expand Down
9 changes: 9 additions & 0 deletions ref/netstandard1.3/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,14 @@ 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 MinimumVersion { get { throw null; } set { } }
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
Loading

0 comments on commit 9e22548

Please sign in to comment.