diff --git a/build/NuGetPackages/Microsoft.Build.Framework.nuspec b/build/NuGetPackages/Microsoft.Build.Framework.nuspec index fc2e3388ce9..0a6f9c887a8 100644 --- a/build/NuGetPackages/Microsoft.Build.Framework.nuspec +++ b/build/NuGetPackages/Microsoft.Build.Framework.nuspec @@ -18,6 +18,7 @@ + diff --git a/ref/net46/Microsoft.Build.Framework/Microsoft.Build.Framework.cs b/ref/net46/Microsoft.Build.Framework/Microsoft.Build.Framework.cs index 2d769c07130..060d8a3a5c1 100644 --- a/ref/net46/Microsoft.Build.Framework/Microsoft.Build.Framework.cs +++ b/ref/net46/Microsoft.Build.Framework/Microsoft.Build.Framework.cs @@ -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 + { + 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 errors, System.Collections.Generic.IEnumerable warnings=null); + public abstract Microsoft.Build.Framework.SdkResult IndicateSuccess(string path, string version, System.Collections.Generic.IEnumerable warnings=null); + } public partial class TargetFinishedEventArgs : Microsoft.Build.Framework.BuildStatusEventArgs { protected TargetFinishedEventArgs() { } diff --git a/ref/net46/Microsoft.Build/Microsoft.Build.cs b/ref/net46/Microsoft.Build/Microsoft.Build.cs index ebf51402783..94653e78f93 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,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 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 { diff --git a/ref/netstandard1.3/Microsoft.Build.Framework/Microsoft.Build.Framework.cs b/ref/netstandard1.3/Microsoft.Build.Framework/Microsoft.Build.Framework.cs index a7851bba7b1..4d4f15a65bb 100644 --- a/ref/netstandard1.3/Microsoft.Build.Framework/Microsoft.Build.Framework.cs +++ b/ref/netstandard1.3/Microsoft.Build.Framework/Microsoft.Build.Framework.cs @@ -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 + { + 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 errors, System.Collections.Generic.IEnumerable warnings=null); + public abstract Microsoft.Build.Framework.SdkResult IndicateSuccess(string path, string version, System.Collections.Generic.IEnumerable warnings=null); + } public partial class TargetFinishedEventArgs : Microsoft.Build.Framework.BuildStatusEventArgs { protected TargetFinishedEventArgs() { } diff --git a/ref/netstandard1.3/Microsoft.Build/Microsoft.Build.cs b/ref/netstandard1.3/Microsoft.Build/Microsoft.Build.cs index fd839729c91..39dd75d477a 100644 --- a/ref/netstandard1.3/Microsoft.Build/Microsoft.Build.cs +++ b/ref/netstandard1.3/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,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 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 { 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.UnitTests/BackEnd/MockLoggingService.cs b/src/Build.UnitTests/BackEnd/MockLoggingService.cs index bc18fb59f5d..b335cc5c4a7 100644 --- a/src/Build.UnitTests/BackEnd/MockLoggingService.cs +++ b/src/Build.UnitTests/BackEnd/MockLoggingService.cs @@ -24,6 +24,13 @@ namespace Microsoft.Build.UnitTests.BackEnd /// internal class MockLoggingService : ILoggingService { + private Action _writer; + + public MockLoggingService(Action writter = null) + { + _writer = writter ?? Console.WriteLine; + } + #region ILoggingService Members /// @@ -242,10 +249,10 @@ public void InitializeNodeLoggers(ICollection loggerDescripti /// The args for the message public void LogComment(BuildEventContext buildEventContext, MessageImportance importance, string messageResourceName, params object[] messageArgs) { - Console.WriteLine(messageResourceName); + _writer(messageResourceName); foreach (object o in messageArgs) { - Console.WriteLine((string)o); + _writer((string)o); } } @@ -257,7 +264,7 @@ public void LogComment(BuildEventContext buildEventContext, MessageImportance im /// The message public void LogCommentFromText(BuildEventContext buildEventContext, MessageImportance importance, string message) { - Console.WriteLine(message); + _writer(message); } /// @@ -277,10 +284,10 @@ public void LogBuildEvent(BuildEventArgs buildEvent) /// The message args public void LogError(BuildEventContext buildEventContext, BuildEventFileInfo file, string messageResourceName, params object[] messageArgs) { - Console.WriteLine(messageResourceName); + _writer(messageResourceName); foreach (object o in messageArgs) { - Console.WriteLine((string)o); + _writer((string)o); } } @@ -294,10 +301,10 @@ public void LogError(BuildEventContext buildEventContext, BuildEventFileInfo fil /// The message args public void LogError(BuildEventContext buildEventContext, string subcategoryResourceName, BuildEventFileInfo file, string messageResourceName, params object[] messageArgs) { - Console.WriteLine(messageResourceName); + _writer(messageResourceName); foreach (object o in messageArgs) { - Console.WriteLine((string)o); + _writer((string)o); } } @@ -312,7 +319,7 @@ public void LogError(BuildEventContext buildEventContext, string subcategoryReso /// The message public void LogErrorFromText(BuildEventContext buildEventContext, string subcategoryResourceName, string errorCode, string helpKeyword, BuildEventFileInfo file, string message) { - Console.WriteLine(message); + _writer(message); } /// @@ -332,6 +339,7 @@ public void LogInvalidProjectFileError(BuildEventContext buildEventContext, Inva /// The file public void LogFatalBuildError(BuildEventContext buildEventContext, Exception exception, BuildEventFileInfo file) { + _writer(exception.Message); } /// @@ -378,10 +386,10 @@ public void LogTaskWarningFromException(BuildEventContext buildEventContext, Exc /// The message args public void LogWarning(BuildEventContext buildEventContext, string subcategoryResourceName, BuildEventFileInfo file, string messageResourceName, params object[] messageArgs) { - Console.WriteLine(messageResourceName); + _writer(messageResourceName); foreach (object o in messageArgs) { - Console.WriteLine((string)o); + _writer((string)o); } } @@ -396,7 +404,7 @@ public void LogWarning(BuildEventContext buildEventContext, string subcategoryRe /// The message public void LogWarningFromText(BuildEventContext buildEventContext, string subcategoryResourceName, string warningCode, string helpKeyword, BuildEventFileInfo file, string message) { - Console.WriteLine(message); + _writer(message); } /// diff --git a/src/Build.UnitTests/BackEnd/SdkResolution_Tests.cs b/src/Build.UnitTests/BackEnd/SdkResolution_Tests.cs new file mode 100644 index 00000000000..6b2ff1bfc58 --- /dev/null +++ b/src/Build.UnitTests/BackEnd/SdkResolution_Tests.cs @@ -0,0 +1,153 @@ +using System; +using Microsoft.Build.BackEnd; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Construction; +using Microsoft.Build.Framework; +using Microsoft.Build.UnitTests; +using Microsoft.Build.UnitTests.BackEnd; +using System.Collections.Generic; +using System.Text; +using Xunit; + +namespace Microsoft.Build.Engine.UnitTests.BackEnd +{ + public class SdkResolution_Tests + { + [Fact] + public void AssertFirstResolverCanResolve() + { + var log = new StringBuilder(); + var sdk = new SdkReference("1sdkName", "referencedVersion", "minimumVersion"); + var logger = new MockLoggingService(message => log.AppendLine(message)); + var bec = new BuildEventContext(0, 0, 0, 0, 0); + + SdkResolution resolution = new SdkResolution(new MockLoaderStrategy()); + var result = resolution.GetSdkPath(sdk, logger, bec, new MockElementLocation("file"), "sln"); + + Assert.Equal("resolverpath1", result); + Assert.Equal("MockSdkResolver1 running", log.ToString().Trim()); + } + + [Fact] + public void AssertFirstResolverErrorsSupressedWhenResolved() + { + // 2sdkName will cause MockSdkResolver1 to fail with an error reason. The error will not + // be logged because MockSdkResolver2 will succeed. + var log = new StringBuilder(); + var sdk = new SdkReference("2sdkName", "referencedVersion", "minimumVersion"); + var logger = new MockLoggingService(message => log.AppendLine(message)); + var bec = new BuildEventContext(0, 0, 0, 0, 0); + + SdkResolution resolution = new SdkResolution(new MockLoaderStrategy()); + var result = resolution.GetSdkPath(sdk, logger, bec, new MockElementLocation("file"), "sln"); + + var logResult = log.ToString(); + Assert.Equal("resolverpath2", result); + + // Both resolvers should run, and no ERROR string. + Assert.Contains("MockSdkResolver1 running", logResult); + Assert.Contains("MockSdkResolver2 running", logResult); + + // Resolver2 gives a warning on success or failure. + Assert.Contains("WARNING2", logResult); + Assert.DoesNotContain("ERROR", logResult); + } + + [Fact] + public void AssertAllResolverErrorsLoggedWhenSdkNotResolved() + { + var log = new StringBuilder(); + var sdk = new SdkReference("notfound", "referencedVersion", "minimumVersion"); + var logger = new MockLoggingService(message => log.AppendLine(message)); + var bec = new BuildEventContext(0, 0, 0, 0, 0); + + SdkResolution resolution = new SdkResolution(new MockLoaderStrategy()); + var result = resolution.GetSdkPath(sdk, logger, bec, new MockElementLocation("file"), "sln"); + + var logResult = log.ToString(); + Assert.Null(result); + Assert.Contains("MockSdkResolver1 running", logResult); + Assert.Contains("MockSdkResolver2 running", logResult); + Assert.Contains("ERROR1", logResult); + Assert.Contains("ERROR2", logResult); + Assert.Contains("WARNING2", logResult); + } + + [Fact] + public void AssertErrorLoggedWhenResolverThrows() + { + var log = new StringBuilder(); + var sdk = new SdkReference("1sdkName", "referencedVersion", "minimumVersion"); + var logger = new MockLoggingService(message => log.AppendLine(message)); + var bec = new BuildEventContext(0, 0, 0, 0, 0); + + SdkResolution resolution = new SdkResolution(new MockLoaderStrategy(true)); + var result = resolution.GetSdkPath(sdk, logger, bec, new MockElementLocation("file"), "sln"); + + Assert.Equal("resolverpath1", result); + Assert.Contains("EXMESSAGE", log.ToString()); + } + + private class MockLoaderStrategy : SdkResolverLoader + { + private readonly bool _includeErrorResolver; + + public MockLoaderStrategy(bool includeErrorResolver = false) + { + _includeErrorResolver = includeErrorResolver; + } + + internal override IList LoadResolvers(ILoggingService logger, BuildEventContext bec, ElementLocation location) + { + return _includeErrorResolver + ? new List {new MockSdkResolverThrows(),new MockSdkResolver1(),new MockSdkResolver2()} + : new List {new MockSdkResolver1(), new MockSdkResolver2()}; + } + } + + private class MockSdkResolver1 : SdkResolver + { + public override string Name => "MockSdkResolver1"; + public override int Priority => 1; + + public override SdkResult Resolve(SdkReference sdk, SdkResolverContext resolverContext, SdkResultFactory factory) + { + resolverContext.Logger.LogMessage("MockSdkResolver1 running", MessageImportance.Normal); + + if (sdk.Name.StartsWith("1")) + return factory.IndicateSuccess("resolverpath1", "version1"); + + return factory.IndicateFailure(new[] {"ERROR1"}); + } + } + + private class MockSdkResolver2 : SdkResolver + { + public override string Name => "MockSdkResolver2"; + public override int Priority => 2; + + public override SdkResult Resolve(SdkReference sdk, SdkResolverContext resolverContext, SdkResultFactory factory) + { + resolverContext.Logger.LogMessage("MockSdkResolver2 running", MessageImportance.Normal); + + if (sdk.Name.StartsWith("2")) + return factory.IndicateSuccess("resolverpath2", "version2", new[] {"WARNING2"}); + + return factory.IndicateFailure(new[] { "ERROR2" }, new[] { "WARNING2" }); + } + } + + private class MockSdkResolverThrows : SdkResolver + { + public override string Name => "MockSdkResolverThrows"; + public override int Priority => 0; + + public override SdkResult Resolve(SdkReference sdk, SdkResolverContext resolverContext, SdkResultFactory factory) + { + resolverContext.Logger.LogMessage("MockSdkResolverThrows running", MessageImportance.Normal); + + throw new ArithmeticException("EXMESSAGE"); + } + } + } +} diff --git a/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs b/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs new file mode 100644 index 00000000000..37c00f69eaa --- /dev/null +++ b/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.UnitTests; +using Microsoft.Build.UnitTests.BackEnd; +using Xunit; + +namespace Microsoft.Build.Engine.UnitTests.BackEnd +{ + public class SdkResolverLoader_Tests + { + [Fact] + public void AssertDefaultLoaderReturnsDefaultResolver() + { + var loader = new SdkResolverLoader(); + var log = new StringBuilder(); + var logger = new MockLoggingService(message => log.AppendLine(message)); + var bec = new BuildEventContext(0, 0, 0, 0, 0); + + var resolvers = loader.LoadResolvers(logger, bec, new MockElementLocation("file")); + + Assert.Equal(1, resolvers.Count); + Assert.Equal(typeof(DefaultSdkResolver), resolvers[0].GetType()); + } + + [Fact] + public void VerifySdkResolverLoaderFileDiscoveryPattern() + { + var root = FileUtilities.GetTemporaryDirectory(); + try + { + // Valid pattern is root\(Name)\(Name).dll. No other files should be considered. + var d1 = Directory.CreateDirectory(Path.Combine(root, "Resolver1")); + var d2 = Directory.CreateDirectory(Path.Combine(root, "NoResolver")); + + // Valid. + var f1 = Path.Combine(d1.FullName, "Resolver1.dll"); + + // Invalid, won't be considered. + var f2 = Path.Combine(d1.FullName, "Dependency.dll"); + var f3 = Path.Combine(d2.FullName, "InvalidName.dll"); + var f4 = Path.Combine(d2.FullName, "NoResolver.txt"); + + File.WriteAllText(f1, string.Empty); + File.WriteAllText(f2, string.Empty); + File.WriteAllText(f3, string.Empty); + File.WriteAllText(f4, string.Empty); + + var strategy = new SdkResolverLoader(); + var files = strategy.FindPotentialSdkResolvers(root); + + Assert.Equal(1, files.Count); + Assert.Equal(f1, files[0]); + } + finally + { + FileUtilities.DeleteDirectoryNoThrow(root, true); + } + } + } +} diff --git a/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj b/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj index 6e9a2ca0e94..c75f37f08f3 100644 --- a/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj +++ b/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj @@ -103,6 +103,8 @@ + + diff --git a/src/Build/BackEnd/Components/SdkResolution/DefaultSdkResolver.cs b/src/Build/BackEnd/Components/SdkResolution/DefaultSdkResolver.cs new file mode 100644 index 00000000000..2f27c45bbf3 --- /dev/null +++ b/src/Build/BackEnd/Components/SdkResolution/DefaultSdkResolver.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.IO; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Default SDK resolver for compatibility with VS2017 RTM. + /// + /// Default Sdk folder will to: + /// 1) MSBuildSDKsPath environment variable if defined + /// 2) When in Visual Studio, (VSRoot)\MSBuild\Sdks\ + /// 3) Outside of Visual Studio (MSBuild Root)\Sdks\ + /// + /// + internal class DefaultSdkResolver : SdkResolver + { + public override string Name => "DefaultSdkResolver"; + + public override int Priority => 10000; + + public override SdkResult Resolve(SdkReference sdk, SdkResolverContext context, SdkResultFactory factory) + { + var sdkPath = Path.Combine(BuildEnvironmentHelper.Instance.MSBuildSDKsPath, sdk.Name, "Sdk"); + + // Note: On failure MSBuild will log a generic message, no need to indicate a failure reason here. + return FileUtilities.DirectoryExistsNoThrow(sdkPath) + ? factory.IndicateSuccess(sdkPath, string.Empty) + : factory.IndicateFailure(null); + } + } +} diff --git a/src/Build/BackEnd/Components/SdkResolution/SdkResolution.cs b/src/Build/BackEnd/Components/SdkResolution/SdkResolution.cs new file mode 100644 index 00000000000..1c7e25bdc09 --- /dev/null +++ b/src/Build/BackEnd/Components/SdkResolution/SdkResolution.cs @@ -0,0 +1,198 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Construction; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Component responsible for resolving an SDK to a file path. Loads and coordinates + /// with plug-ins. + /// + internal class SdkResolution + { + private readonly object _lockObject = new object(); + private readonly SdkResolverLoader _sdkResolverLoader; + private IList _resolvers; + + /// + /// Create an instance with a specified resolver assembly loading strategy. Used + /// for testing purposes. + /// + /// Resolver loading strategy. + internal SdkResolution(SdkResolverLoader sdkResolverLoader) + { + _sdkResolverLoader = sdkResolverLoader; + } + + internal static SdkResolution Instance { get; } = new SdkResolution(new SdkResolverLoader()); + + /// + /// Get path on disk to the referenced SDK. + /// + /// SDK referenced by the Project. + /// Logging service. + /// Build event context for logging. + /// Location of the element within the project which referenced the SDK. + /// Path to the solution if known. + /// Path to the root of the referenced SDK. + internal string GetSdkPath(SdkReference sdk, ILoggingService logger, BuildEventContext buildEventContext, + ElementLocation sdkReferenceLocation, string solutionPath) + { + ErrorUtilities.VerifyThrowInternalNull(sdk, nameof(sdk)); + ErrorUtilities.VerifyThrowInternalNull(logger, nameof(logger)); + ErrorUtilities.VerifyThrowInternalNull(buildEventContext, nameof(buildEventContext)); + ErrorUtilities.VerifyThrowInternalNull(sdkReferenceLocation, nameof(sdkReferenceLocation)); + + if (_resolvers == null) Initialize(logger, buildEventContext, sdkReferenceLocation); + + var results = new List(); + + try + { + var buildEngineLogger = new SdkLoggerImpl(logger, buildEventContext); + foreach (var sdkResolver in _resolvers) + { + var context = new SdkResolverContextImpl(buildEngineLogger, sdkReferenceLocation.File, solutionPath); + var resultFactory = new SdkResultFactoryImpl(sdk); + try + { + var result = (SdkResultImpl)sdkResolver.Resolve(sdk, context, resultFactory); + if (result != null && result.Success) + { + LogWarnings(logger, buildEventContext, sdkReferenceLocation, result); + return result.Path; + } + + results.Add(result); + } + catch (Exception e) + { + logger.LogFatalBuildError(buildEventContext, e, new BuildEventFileInfo(sdkReferenceLocation)); + } + } + } + catch (Exception e) + { + logger.LogFatalBuildError(buildEventContext, e, new BuildEventFileInfo(sdkReferenceLocation)); + throw; + } + + foreach (var result in results) + { + LogWarnings(logger, buildEventContext, sdkReferenceLocation, result); + + if (result.Errors != null) + { + foreach (var error in result.Errors) + { + logger.LogErrorFromText(buildEventContext, subcategoryResourceName: null, errorCode: null, + helpKeyword: null, file: new BuildEventFileInfo(sdkReferenceLocation), message: error); + } + } + } + + return null; + } + + private void Initialize(ILoggingService logger, BuildEventContext buildEventContext, ElementLocation location) + { + lock (_lockObject) + { + if (_resolvers != null) return; + _resolvers = _sdkResolverLoader.LoadResolvers(logger, buildEventContext, location); + } + } + + private static void LogWarnings(ILoggingService logger, BuildEventContext bec, ElementLocation location, + SdkResultImpl result) + { + if (result.Warnings == null) return; + + foreach (var warning in result.Warnings) + logger.LogWarningFromText(bec, null, null, null, new BuildEventFileInfo(location), warning); + } + + private class SdkLoggerImpl : SdkLogger + { + private readonly BuildEventContext _buildEventContext; + private readonly ILoggingService _loggingService; + + public SdkLoggerImpl(ILoggingService loggingService, BuildEventContext buildEventContext) + { + _loggingService = loggingService; + _buildEventContext = buildEventContext; + } + + public override void LogMessage(string message, MessageImportance messageImportance = MessageImportance.Low) + { + _loggingService.LogCommentFromText(_buildEventContext, messageImportance, message); + } + } + + private class SdkResultImpl : SdkResult + { + public SdkResultImpl(SdkReference sdkReference, IEnumerable errors, IEnumerable warnings) + { + Success = false; + Sdk = sdkReference; + Errors = errors; + Warnings = warnings; + } + + public SdkResultImpl(SdkReference sdkReference, string path, string version, IEnumerable warnings) + { + Success = true; + Sdk = sdkReference; + Path = path; + Version = version; + Warnings = warnings; + } + + public SdkReference Sdk { get; } + + public string Path { get; } + + public string Version { get; } + + public IEnumerable Errors { get; } + + public IEnumerable Warnings { get; } + } + + private class SdkResultFactoryImpl : SdkResultFactory + { + private readonly SdkReference _sdkReference; + + internal SdkResultFactoryImpl(SdkReference sdkReference) + { + _sdkReference = sdkReference; + } + + public override SdkResult IndicateSuccess(string path, string version, IEnumerable warnings = null) + { + return new SdkResultImpl(_sdkReference, path, version, warnings); + } + + public override SdkResult IndicateFailure(IEnumerable errors, IEnumerable warnings = null) + { + return new SdkResultImpl(_sdkReference, errors, warnings); + } + } + + private sealed class SdkResolverContextImpl : SdkResolverContext + { + public SdkResolverContextImpl(SdkLogger logger, string projectFilePath, string solutionPath) + { + Logger = logger; + ProjectFilePath = projectFilePath; + SolutionFilePath = solutionPath; + } + } + } +} diff --git a/src/Build/BackEnd/Components/SdkResolution/SdkResolverLoader.cs b/src/Build/BackEnd/Components/SdkResolution/SdkResolverLoader.cs new file mode 100644 index 00000000000..8d2623e5165 --- /dev/null +++ b/src/Build/BackEnd/Components/SdkResolution/SdkResolverLoader.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Construction; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd +{ + internal class SdkResolverLoader + { + internal virtual IList LoadResolvers(ILoggingService logger, BuildEventContext buildEventContext, + ElementLocation location) + { + // Always add the default resolver + var resolvers = new List {new DefaultSdkResolver()}; + var potentialResolvers = FindPotentialSdkResolvers( + Path.Combine(BuildEnvironmentHelper.Instance.MSBuildToolsDirectory32, "SdkResolvers")); + + if (potentialResolvers.Count == 0) return resolvers; + +#if !FEATURE_ASSEMBLY_LOADFROM + var loader = new CoreClrAssemblyLoader(); +#endif + + foreach (var potentialResolver in potentialResolvers) + try + { +#if FEATURE_ASSEMBLY_LOADFROM + var assembly = Assembly.LoadFrom(potentialResolver); +#else + loader.AddDependencyLocation(Path.GetDirectoryName(potentialResolver)); + Assembly assembly = loader.LoadFromPath(potentialResolver); +#endif + + resolvers.AddRange(assembly.ExportedTypes + .Select(type => new {type, info = type.GetTypeInfo()}) + .Where(t => t.info.IsClass && t.info.IsPublic && typeof(SdkResolver).IsAssignableFrom(t.type)) + .Select(t => (SdkResolver) Activator.CreateInstance(t.type))); + } + catch (Exception e) + { + logger.LogWarning(buildEventContext, string.Empty, new BuildEventFileInfo(location), + "CouldNotLoadSdkResolver", e.Message); + } + + return resolvers.OrderBy(t => t.Priority).ToList(); + } + + /// + /// Find all files that are to be considered SDK Resolvers. Pattern will match + /// Root\SdkResolver\(ResolverName)\(ResolverName).dll. + /// + /// + /// + internal virtual IList FindPotentialSdkResolvers(string rootFolder) + { + if (string.IsNullOrEmpty(rootFolder) || !FileUtilities.DirectoryExistsNoThrow(rootFolder)) + return new List(); + + return new DirectoryInfo(rootFolder).GetDirectories() + .Select(subfolder => Path.Combine(subfolder.FullName, $"{subfolder.Name}.dll")) + .Where(FileUtilities.FileExistsNoThrow) + .ToList(); + } + } +} diff --git a/src/Build/Construction/ProjectImportElement.cs b/src/Build/Construction/ProjectImportElement.cs index 2095b44ccf3..aa9ffd01570 100644 --- a/src/Build/Construction/ProjectImportElement.cs +++ b/src/Build/Construction/ProjectImportElement.cs @@ -6,7 +6,7 @@ //----------------------------------------------------------------------- using System.Diagnostics; - +using Microsoft.Build.Framework; using Microsoft.Build.Shared; using ProjectXmlUtilities = Microsoft.Build.Internal.ProjectXmlUtilities; @@ -31,7 +31,7 @@ internal ProjectImportElement(XmlElementWithLocation xmlElement, ProjectElementC /// /// Initialize an unparented ProjectImportElement /// - private ProjectImportElement(XmlElementWithLocation xmlElement, ProjectRootElement containingProject) + internal ProjectImportElement(XmlElementWithLocation xmlElement, ProjectRootElement containingProject) : base(xmlElement, null, containingProject) { } @@ -91,7 +91,12 @@ public string Sdk /// added because of the attribute and the location where the project was /// imported. /// - public ImplicitImportLocation ImplicitImportLocation { get; internal set; } = ImplicitImportLocation.None; + public ImplicitImportLocation ImplicitImportLocation { get; internal set; } + + /// + /// if applicable to this import element. + /// + internal SdkReference ParsedSdkReference { get; set; } /// /// Creates an unparented ProjectImportElement, wrapping an unparented XmlElement. @@ -101,27 +106,23 @@ public string Sdk internal static ProjectImportElement CreateDisconnected(string project, ProjectRootElement containingProject) { XmlElementWithLocation element = containingProject.CreateElement(XMakeElements.import); - - ProjectImportElement import = new ProjectImportElement(element, containingProject); - - import.Project = project; - - return import; + return new ProjectImportElement(element, containingProject) {Project = project}; } /// /// Creates an implicit ProjectImportElement as if it was in the project. /// /// - internal static ProjectImportElement CreateImplicit(string project, ProjectRootElement containingProject, ImplicitImportLocation implicitImportLocation, string sdkName) + internal static ProjectImportElement CreateImplicit(string project, ProjectRootElement containingProject, ImplicitImportLocation implicitImportLocation, SdkReference sdkReference) { - ProjectImportElement import = CreateDisconnected(project, containingProject); - - import.ImplicitImportLocation = implicitImportLocation; - - import.Sdk = sdkName; - - return import; + XmlElementWithLocation element = containingProject.CreateElement(XMakeElements.import); + return new ProjectImportElement(element, containingProject) + { + Project = project, + Sdk = sdkReference.ToString(), + ImplicitImportLocation = implicitImportLocation, + ParsedSdkReference = sdkReference + }; } /// diff --git a/src/Build/Construction/ProjectRootElement.cs b/src/Build/Construction/ProjectRootElement.cs index de68cbecb54..cb2dbd2e48e 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. @@ -1886,6 +1894,56 @@ internal void MarkAsExplicitlyLoaded() IsExplicitlyLoaded = true; } + /// + /// Creates and returns a list of nodes which are implicitly + /// referenced by the Project. + /// + /// Current project + /// An containing details of the SDKs referenced by the project. + internal List GetImplicitImportNodes(ProjectRootElement currentProjectOrImport) + { + var nodes = new List(); + + foreach (var referencedSdk in ParseSdks(Sdk, SdkLocation)) + { + nodes.Add(ProjectImportElement.CreateImplicit("Sdk.props", currentProjectOrImport, ImplicitImportLocation.Top, referencedSdk)); + nodes.Add(ProjectImportElement.CreateImplicit("Sdk.targets", currentProjectOrImport, ImplicitImportLocation.Bottom, referencedSdk)); + } + + foreach (var sdkNode in Children.OfType()) + { + var referencedSdk = new SdkReference( + sdkNode.XmlElement.GetAttribute("Name"), + sdkNode.XmlElement.GetAttribute("Version"), + sdkNode.XmlElement.GetAttribute("MinimumVersion")); + + nodes.Add(ProjectImportElement.CreateImplicit("Sdk.props", currentProjectOrImport, ImplicitImportLocation.Top, referencedSdk)); + nodes.Add(ProjectImportElement.CreateImplicit("Sdk.targets", currentProjectOrImport, ImplicitImportLocation.Bottom, referencedSdk)); + } + + return nodes; + } + + private static IEnumerable ParseSdks(string sdks, IElementLocation sdkLocation) + { + if (String.IsNullOrWhiteSpace(sdks)) + { + yield break; + } + + foreach (string sdk in sdks.Split(';').Select(i => i.Trim())) + { + SdkReference sdkReference; + + if (!SdkReference.TryParse(sdk, out sdkReference)) + { + ProjectErrorUtilities.ThrowInvalidProject(sdkLocation, "InvalidSdkFormat", sdks); + } + + yield return sdkReference; + } + } + /// /// Returns a new instance of ProjectRootElement that is affiliated with the same ProjectRootElementCache. /// diff --git a/src/Build/Construction/ProjectSdkElement.cs b/src/Build/Construction/ProjectSdkElement.cs new file mode 100644 index 00000000000..f67f18026f2 --- /dev/null +++ b/src/Build/Construction/ProjectSdkElement.cs @@ -0,0 +1,99 @@ +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 Name 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); + } + } + + /// + /// Gets or sets the minimum version of the SDK required to build the project. + /// + public string MinimumVersion + { + get { return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.sdkMinimumVersion); } + set + { + ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.sdkMinimumVersion, value); + MarkDirty($"Set SDK MinimumVersion to {value}", XMakeAttributes.sdkMinimumVersion); + } + } + + /// + 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/Construction/Solution/SolutionProjectGenerator.cs b/src/Build/Construction/Solution/SolutionProjectGenerator.cs index 1613aa14af4..3940c3f70d9 100644 --- a/src/Build/Construction/Solution/SolutionProjectGenerator.cs +++ b/src/Build/Construction/Solution/SolutionProjectGenerator.cs @@ -40,6 +40,11 @@ internal class SolutionProjectGenerator { #region Private Fields + /// + /// Name of the property used to store the path to the solution being built. + /// + internal const string SolutionPathPropertyName = "SolutionPath"; + /// /// The path node to add in when the output directory for a website is overridden. /// @@ -88,7 +93,7 @@ internal class SolutionProjectGenerator new Tuple("SolutionExt", null), new Tuple("SolutionFileName", null), new Tuple("SolutionName", null), - new Tuple("SolutionPath", null) + new Tuple(SolutionPathPropertyName, null) }; /// @@ -2194,7 +2199,7 @@ private void AddGlobalProperties(ProjectRootElement traversalProject) globalProperties.AddProperty("SolutionFileName", EscapingUtilities.Escape(Path.GetFileName(_solutionFile.FullPath))); globalProperties.AddProperty("SolutionName", EscapingUtilities.Escape(Path.GetFileNameWithoutExtension(_solutionFile.FullPath))); - globalProperties.AddProperty("SolutionPath", EscapingUtilities.Escape(Path.Combine(_solutionFile.SolutionFileDirectory, Path.GetFileName(_solutionFile.FullPath)))); + globalProperties.AddProperty(SolutionPathPropertyName, EscapingUtilities.Escape(Path.Combine(_solutionFile.SolutionFileDirectory, Path.GetFileName(_solutionFile.FullPath)))); // Add other global properties ProjectPropertyGroupElement frameworkVersionProperties = traversalProject.CreatePropertyGroupElement(); diff --git a/src/Build/Definition/Project.cs b/src/Build/Definition/Project.cs index 0d3f0faf38b..8628d781047 100644 --- a/src/Build/Definition/Project.cs +++ b/src/Build/Definition/Project.cs @@ -2492,7 +2492,7 @@ private void ReevaluateIfNecessary(ILoggingService loggingServiceForEvaluation, private void Reevaluate(ILoggingService loggingServiceForEvaluation, ProjectLoadSettings loadSettings) { - Evaluator.Evaluate(_data, _xml, loadSettings, ProjectCollection.MaxNodeCount, ProjectCollection.EnvironmentProperties, loggingServiceForEvaluation, new ProjectItemFactory(this), _projectCollection as IToolsetProvider, _projectCollection.ProjectRootElementCache, s_buildEventContext, null /* no project instance for debugging */); + Evaluator.Evaluate(_data, _xml, loadSettings, ProjectCollection.MaxNodeCount, ProjectCollection.EnvironmentProperties, loggingServiceForEvaluation, new ProjectItemFactory(this), _projectCollection, _projectCollection.ProjectRootElementCache, s_buildEventContext, null /* no project instance for debugging */, _projectCollection.SdkResolution); // We have to do this after evaluation, because evaluation might have changed // the imports being pulled in. diff --git a/src/Build/Definition/ProjectCollection.cs b/src/Build/Definition/ProjectCollection.cs index 1f19b57e258..f735e078d19 100644 --- a/src/Build/Definition/ProjectCollection.cs +++ b/src/Build/Definition/ProjectCollection.cs @@ -199,6 +199,8 @@ public class ProjectCollection : IToolsetProvider, IBuildComponent, IDisposable /// private ProjectRootElementCache _projectRootElementCache; + internal SdkResolution SdkResolution { get; } + /// /// Hook up last minute dumping of any exceptions bringing down the process /// @@ -281,6 +283,8 @@ public ProjectCollection(IDictionary globalProperties, IEnumerab RegisterLoggers(loggers); RegisterForwardingLoggers(remoteLoggers); + SdkResolution = BackEnd.SdkResolution.Instance; + if (globalProperties != null) { _globalProperties = new PropertyDictionary(globalProperties.Count); diff --git a/src/Build/Evaluation/Evaluator.cs b/src/Build/Evaluation/Evaluator.cs index 67a48530d6f..e07683abdb1 100644 --- a/src/Build/Evaluation/Evaluator.cs +++ b/src/Build/Evaluation/Evaluator.cs @@ -177,6 +177,8 @@ internal class Evaluator /// private readonly ProjectInstance _projectInstanceIfAnyForDebuggerOnly; + private readonly SdkResolution _sdkResolution; + /// /// The environment properties with which evaluation should take place. /// @@ -247,7 +249,7 @@ internal class Evaluator /// /// Private constructor called by the static Evaluate method. /// - private Evaluator(IEvaluatorData data, ProjectRootElement projectRootElement, ProjectLoadSettings loadSettings, int maxNodeCount, PropertyDictionary environmentProperties, ILoggingService loggingService, IItemFactory itemFactory, IToolsetProvider toolsetProvider, ProjectRootElementCache projectRootElementCache, BuildEventContext buildEventContext, ProjectInstance projectInstanceIfAnyForDebuggerOnly) + private Evaluator(IEvaluatorData data, ProjectRootElement projectRootElement, ProjectLoadSettings loadSettings, int maxNodeCount, PropertyDictionary environmentProperties, ILoggingService loggingService, IItemFactory itemFactory, IToolsetProvider toolsetProvider, ProjectRootElementCache projectRootElementCache, BuildEventContext buildEventContext, ProjectInstance projectInstanceIfAnyForDebuggerOnly, SdkResolution sdkResolution) { ErrorUtilities.VerifyThrowInternalNull(data, "data"); ErrorUtilities.VerifyThrowInternalNull(projectRootElementCache, "projectRootElementCache"); @@ -276,6 +278,7 @@ private Evaluator(IEvaluatorData data, ProjectRootElement projectRoo _projectRootElementCache = projectRootElementCache; _buildEventContext = buildEventContext; _projectInstanceIfAnyForDebuggerOnly = projectInstanceIfAnyForDebuggerOnly; + _sdkResolution = sdkResolution; } /// @@ -375,7 +378,7 @@ internal static bool DebugEvaluation /// newing one up, yet the whole class need not be static. /// The optional ProjectInstance is only exposed when doing debugging. It is not used by the evaluator. /// - internal static IDictionary Evaluate(IEvaluatorData data, ProjectRootElement root, ProjectLoadSettings loadSettings, int maxNodeCount, PropertyDictionary environmentProperties, ILoggingService loggingService, IItemFactory itemFactory, IToolsetProvider toolsetProvider, ProjectRootElementCache projectRootElementCache, BuildEventContext buildEventContext, ProjectInstance projectInstanceIfAnyForDebuggerOnly) + internal static IDictionary Evaluate(IEvaluatorData data, ProjectRootElement root, ProjectLoadSettings loadSettings, int maxNodeCount, PropertyDictionary environmentProperties, ILoggingService loggingService, IItemFactory itemFactory, IToolsetProvider toolsetProvider, ProjectRootElementCache projectRootElementCache, BuildEventContext buildEventContext, ProjectInstance projectInstanceIfAnyForDebuggerOnly, SdkResolution sdkResolution) { #if (!STANDALONEBUILD) using (new CodeMarkerStartEnd(CodeMarkerEvent.perfMSBuildProjectEvaluateBegin, CodeMarkerEvent.perfMSBuildProjectEvaluateEnd)) @@ -388,7 +391,7 @@ internal static IDictionary Evaluate(IEvaluatorData string beginProjectEvaluate = String.Format(CultureInfo.CurrentCulture, "Evaluate Project {0} - Begin", projectFile); DataCollection.CommentMarkProfile(8812, beginProjectEvaluate); #endif - Evaluator evaluator = new Evaluator(data, root, loadSettings, maxNodeCount, environmentProperties, loggingService, itemFactory, toolsetProvider, projectRootElementCache, buildEventContext, projectInstanceIfAnyForDebuggerOnly); + Evaluator evaluator = new Evaluator(data, root, loadSettings, maxNodeCount, environmentProperties, loggingService, itemFactory, toolsetProvider, projectRootElementCache, buildEventContext, projectInstanceIfAnyForDebuggerOnly, sdkResolution); IDictionary projectLevelLocalsForBuild = evaluator.Evaluate(); return projectLevelLocalsForBuild; #if MSBUILDENABLEVSPROFILING @@ -993,35 +996,11 @@ element is ProjectOtherwiseElement DebuggerManager.BakeStates(Path.GetFileNameWithoutExtension(currentProjectOrImport.FullPath)); } #endif - IList implicitImports = new List(); - - if (!String.IsNullOrWhiteSpace(currentProjectOrImport.Sdk)) - { - // SDK imports are added implicitly where they are evaluated at the top and bottom as if they are in the XML - // - foreach (string sdk in currentProjectOrImport.Sdk.Split(';').Select(i => i.Trim())) - { - if (String.IsNullOrWhiteSpace(sdk)) - { - ProjectErrorUtilities.ThrowInvalidProject(currentProjectOrImport.SdkLocation, "InvalidSdkFormat", currentProjectOrImport.Sdk); - } - - int slashIndex = sdk.LastIndexOf("/", StringComparison.Ordinal); - string sdkName = slashIndex > 0 ? sdk.Substring(0, slashIndex) : sdk; - // TODO: do something other than just ignore the version - - if (sdkName.Contains("/")) - { - ProjectErrorUtilities.ThrowInvalidProject(currentProjectOrImport.SdkLocation, "InvalidSdkFormat", currentProjectOrImport.Sdk); - } - - implicitImports.Add(ProjectImportElement.CreateImplicit("Sdk.props", currentProjectOrImport, ImplicitImportLocation.Top, sdkName)); - - implicitImports.Add(ProjectImportElement.CreateImplicit("Sdk.targets", currentProjectOrImport, ImplicitImportLocation.Bottom, sdkName)); - } - } + // Get all the implicit imports (e.g. , but not ) + 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)) { EvaluateImportElement(currentProjectOrImport.DirectoryPath, import); @@ -1179,9 +1158,15 @@ child is ProjectItemElement || continue; } + if (element is ProjectSdkElement) + { + continue; // This case is handled by implicit imports. + } + ErrorUtilities.ThrowInternalError("Unexpected child type"); } + // Evaluate the "bottom" implicit imports as if they were the last entry in the file. foreach (var import in implicitImports.Where(i => i.ImplicitImportLocation == ImplicitImportLocation.Bottom)) { EvaluateImportElement(currentProjectOrImport.DirectoryPath, import); @@ -2220,7 +2205,7 @@ private List ExpandAndLoadImports(string directoryOfImportin if (fallbackSearchPathMatch.Equals(ProjectImportPathMatch.None)) { List projects; - ExpandAndLoadImportsFromUnescapedImportExpressionConditioned(directoryOfImportingFile, importElement, importElement.Project, out projects); + ExpandAndLoadImportsFromUnescapedImportExpressionConditioned(directoryOfImportingFile, importElement, out projects); return projects; } @@ -2312,14 +2297,7 @@ private List ExpandAndLoadImports(string directoryOfImportin continue; } - string project = importElement.Project; - if (!String.IsNullOrWhiteSpace(importElement.Sdk)) - { - project = Path.Combine(BuildEnvironmentHelper.Instance.MSBuildSDKsPath, importElement.Sdk, "Sdk", project); - } - - - var newExpandedImportPath = project.Replace(extensionPropertyRefAsString, extensionPathExpanded); + var newExpandedImportPath = importElement.Project.Replace(extensionPropertyRefAsString, extensionPathExpanded); _loggingService.LogComment(_buildEventContext, MessageImportance.Low, "TryingExtensionsPath", newExpandedImportPath, extensionPathExpanded); List projects; @@ -2377,22 +2355,38 @@ private List ExpandAndLoadImports(string directoryOfImportin /// Caches the parsed import into the provided collection, so future /// requests can be satisfied without re-parsing it. /// - private LoadImportsResult ExpandAndLoadImportsFromUnescapedImportExpressionConditioned(string directoryOfImportingFile, ProjectImportElement importElement, string unescapedExpression, - out List projects, bool throwOnFileNotExistsError = true) + private void ExpandAndLoadImportsFromUnescapedImportExpressionConditioned(string directoryOfImportingFile, + ProjectImportElement importElement, out List projects, + bool throwOnFileNotExistsError = true) { - if (!EvaluateConditionCollectingConditionedProperties(importElement, ExpanderOptions.ExpandProperties, ParserOptions.AllowProperties, _projectRootElementCache)) + if (!EvaluateConditionCollectingConditionedProperties(importElement, ExpanderOptions.ExpandProperties, + ParserOptions.AllowProperties, _projectRootElementCache)) { projects = new List(); - return LoadImportsResult.ConditionWasFalse; + return; } string project = importElement.Project; - if (!String.IsNullOrWhiteSpace(importElement.Sdk)) + + if (importElement.ParsedSdkReference != null) { - project = Path.Combine(BuildEnvironmentHelper.Instance.MSBuildSDKsPath, importElement.Sdk, "Sdk", project); + // Try to get the solution path when available. + var solutionPath = _data.GetProperty(SolutionProjectGenerator.SolutionPathPropertyName)?.EvaluatedValue; + + // Combine SDK path with the "project" relative path + var sdkRootPath = _sdkResolution.GetSdkPath(importElement.ParsedSdkReference, _loggingService, + _buildEventContext, importElement.Location, solutionPath); + + if (string.IsNullOrEmpty(sdkRootPath)) + { + ProjectErrorUtilities.ThrowInvalidProject(importElement.SdkLocation, "CouldNotResolveSdk", importElement.ParsedSdkReference.ToString()); + } + + project = Path.Combine(sdkRootPath, project); } - return ExpandAndLoadImportsFromUnescapedImportExpression(directoryOfImportingFile, importElement, project, throwOnFileNotExistsError, out projects); + ExpandAndLoadImportsFromUnescapedImportExpression(directoryOfImportingFile, importElement, project, + throwOnFileNotExistsError, out projects); } /// 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/Instance/ProjectInstance.cs b/src/Build/Instance/ProjectInstance.cs index ef1127856af..d95128e1130 100644 --- a/src/Build/Instance/ProjectInstance.cs +++ b/src/Build/Instance/ProjectInstance.cs @@ -248,7 +248,7 @@ public ProjectInstance(string projectFile, IDictionary globalPro BuildEventContext buildEventContext = new BuildEventContext(buildParameters.NodeId, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTaskId); ProjectRootElement xml = ProjectRootElement.OpenProjectOrSolution(projectFile, globalProperties, toolsVersion, projectCollection.LoggingService, buildParameters.ProjectRootElementCache, buildEventContext, true /*Explicitly Loaded*/); - Initialize(xml, globalProperties, toolsVersion, subToolsetVersion, 0 /* no solution version provided */, buildParameters, projectCollection.LoggingService, buildEventContext); + Initialize(xml, globalProperties, toolsVersion, subToolsetVersion, 0 /* no solution version provided */, buildParameters, projectCollection.LoggingService, buildEventContext, projectCollection.SdkResolution); } /// @@ -298,7 +298,7 @@ public ProjectInstance(ProjectRootElement xml, IDictionary globa public ProjectInstance(ProjectRootElement xml, IDictionary globalProperties, string toolsVersion, string subToolsetVersion, ProjectCollection projectCollection) { BuildEventContext buildEventContext = new BuildEventContext(0, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTaskId); - Initialize(xml, globalProperties, toolsVersion, subToolsetVersion, 0 /* no solution version specified */, new BuildParameters(projectCollection), projectCollection.LoggingService, buildEventContext); + Initialize(xml, globalProperties, toolsVersion, subToolsetVersion, 0 /* no solution version specified */, new BuildParameters(projectCollection), projectCollection.LoggingService, buildEventContext, projectCollection.SdkResolution); } /// @@ -319,7 +319,7 @@ internal ProjectInstance(string projectFile, ProjectInstance projectToInheritFro _actualTargets = new RetrievableEntryHashSet(OrdinalIgnoreCaseKeyedComparer.Instance); _targets = new ObjectModel.ReadOnlyDictionary(_actualTargets); _environmentVariableProperties = projectToInheritFrom._environmentVariableProperties; - _itemDefinitions = new RetrievableEntryHashSet((IEnumerable)projectToInheritFrom._itemDefinitions, MSBuildNameIgnoreCaseComparer.Default); + _itemDefinitions = new RetrievableEntryHashSet(projectToInheritFrom._itemDefinitions, MSBuildNameIgnoreCaseComparer.Default); _hostServices = projectToInheritFrom._hostServices; this.ProjectRootElementCache = projectToInheritFrom.ProjectRootElementCache; _explicitToolsVersionSpecified = projectToInheritFrom._explicitToolsVersionSpecified; @@ -331,7 +331,7 @@ internal ProjectInstance(string projectFile, ProjectInstance projectToInheritFro this.EvaluatedItemElements = new List(); - IEvaluatorData thisAsIEvaluatorData = (IEvaluatorData)this; + IEvaluatorData thisAsIEvaluatorData = this; thisAsIEvaluatorData.AfterTargets = new Dictionary>(); thisAsIEvaluatorData.BeforeTargets = new Dictionary>(); @@ -359,7 +359,7 @@ internal ProjectInstance(string projectFile, ProjectInstance projectToInheritFro internal ProjectInstance(ProjectRootElement xml, IDictionary globalProperties, string toolsVersion, int visualStudioVersionFromSolution, ProjectCollection projectCollection) { BuildEventContext buildEventContext = new BuildEventContext(0, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTaskId); - Initialize(xml, globalProperties, toolsVersion, null, visualStudioVersionFromSolution, new BuildParameters(projectCollection), projectCollection.LoggingService, buildEventContext); + Initialize(xml, globalProperties, toolsVersion, null, visualStudioVersionFromSolution, new BuildParameters(projectCollection), projectCollection.LoggingService, buildEventContext, projectCollection.SdkResolution); } /// @@ -375,7 +375,7 @@ internal ProjectInstance(string projectFile, IDictionary globalP ProjectRootElement xml = ProjectRootElement.OpenProjectOrSolution(projectFile, globalProperties, toolsVersion, loggingService, buildParameters.ProjectRootElementCache, buildEventContext, false /*Not explicitly loaded*/); - Initialize(xml, globalProperties, toolsVersion, null, 0 /* no solution version specified */, buildParameters, loggingService, buildEventContext); + Initialize(xml, globalProperties, toolsVersion, null, 0 /* no solution version specified */, buildParameters, loggingService, buildEventContext, ProjectCollection.GlobalProjectCollection.SdkResolution); } /// @@ -388,7 +388,7 @@ internal ProjectInstance(ProjectRootElement xml, IDictionary glo ErrorUtilities.VerifyThrowArgumentNull(xml, "xml"); ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(toolsVersion, "toolsVersion"); ErrorUtilities.VerifyThrowArgumentNull(buildParameters, "buildParameters"); - Initialize(xml, globalProperties, toolsVersion, null, 0 /* no solution version specified */, buildParameters, loggingService, buildEventContext); + Initialize(xml, globalProperties, toolsVersion, null, 0 /* no solution version specified */, buildParameters, loggingService, buildEventContext, ProjectCollection.GlobalProjectCollection.SdkResolution); } /// @@ -2186,7 +2186,7 @@ private static IDictionary CreateCloneDictionary(IDictio /// Tools version may be null. /// Does not set mutability. /// - private void Initialize(ProjectRootElement xml, IDictionary globalProperties, string explicitToolsVersion, string explicitSubToolsetVersion, int visualStudioVersionFromSolution, BuildParameters buildParameters, ILoggingService loggingService, BuildEventContext buildEventContext) + private void Initialize(ProjectRootElement xml, IDictionary globalProperties, string explicitToolsVersion, string explicitSubToolsetVersion, int visualStudioVersionFromSolution, BuildParameters buildParameters, ILoggingService loggingService, BuildEventContext buildEventContext, SdkResolution sdkResolution) { ErrorUtilities.VerifyThrowArgumentNull(xml, "xml"); ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(explicitToolsVersion, "toolsVersion"); @@ -2275,7 +2275,7 @@ private void Initialize(ProjectRootElement xml, IDictionary glob Trace.WriteLine(String.Format(CultureInfo.InvariantCulture, "MSBUILD: Creating a ProjectInstance from an unevaluated state [{0}]", FullPath)); } - _initialGlobalsForDebugging = Evaluator.Evaluate(this, xml, ProjectLoadSettings.Default, buildParameters.MaxNodeCount, buildParameters.EnvironmentPropertiesInternal, loggingService, new ProjectItemInstanceFactory(this), buildParameters.ToolsetProvider, ProjectRootElementCache, buildEventContext, this /* for debugging only */); + _initialGlobalsForDebugging = Evaluator.Evaluate(this, xml, ProjectLoadSettings.Default, buildParameters.MaxNodeCount, buildParameters.EnvironmentPropertiesInternal, loggingService, new ProjectItemInstanceFactory(this), buildParameters.ToolsetProvider, ProjectRootElementCache, buildEventContext, this /* for debugging only */, sdkResolution); } /// diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index 21934f99e27..856453207b3 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -218,6 +218,9 @@ + + + @@ -235,6 +238,7 @@ true + diff --git a/src/Build/Resources/Strings.resx b/src/Build/Resources/Strings.resx index 00aa7b12e58..d9875c15e9f 100644 --- a/src/Build/Resources/Strings.resx +++ b/src/Build/Resources/Strings.resx @@ -1190,6 +1190,18 @@ MSB4229: The value "{0}" is not valid for an Sdk specification. The attribute should be a semicolon-delimited list of Sdk-name/minimum-version pairs, separated by a forward slash. {StrBegin="MSB4229: "} + + MSB4236: The SDK '{0}' specified could not be found. + {StrBegin="MSB4236: "} + + + 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: "} @@ -1597,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 MSB4236. + 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/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj b/src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj index 44c8e825aa4..57f5631e2a4 100644 --- a/src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj +++ b/src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj @@ -1,6 +1,6 @@  - + Debug AnyCPU @@ -37,6 +37,7 @@ + @@ -62,7 +63,7 @@ - diff --git a/src/Framework.UnitTests/SdkReference_Tests.cs b/src/Framework.UnitTests/SdkReference_Tests.cs new file mode 100644 index 00000000000..7cd43c6ddae --- /dev/null +++ b/src/Framework.UnitTests/SdkReference_Tests.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Xunit; + +namespace Microsoft.Build.Framework.UnitTests +{ + public class SdkReference_Tests + { + [Fact] + public void VerifySdkReferenceParseNoVersion() + { + string sdkString = "Name"; + SdkReference sdk; + var parsed = SdkReference.TryParse(sdkString, out sdk); + + Assert.True(parsed); + Assert.Equal("Name", sdk.Name); + Assert.Null(sdk.Version); + Assert.Null(sdk.MinimumVersion); + } + + [Fact] + public void VerifySdkReferenceParseWithVersion() + { + string sdkString = "Name/Version"; + SdkReference sdk; + var parsed = SdkReference.TryParse(sdkString, out sdk); + + Assert.True(parsed); + Assert.Equal("Name", sdk.Name); + Assert.Equal("Version", sdk.Version); + Assert.Null(sdk.MinimumVersion); + Assert.Equal(sdkString, sdk.ToString()); + } + + [Fact] + public void VerifySdkReferenceParseWithMinimumVersion() + { + string sdkString = "Name/min=Version"; + SdkReference sdk; + var parsed = SdkReference.TryParse(sdkString, out sdk); + + Assert.True(parsed); + Assert.Equal("Name", sdk.Name); + Assert.Null(sdk.Version); + Assert.Equal("Version", sdk.MinimumVersion); + Assert.Equal(sdkString, sdk.ToString()); + } + + [Fact] + public void VerifySdkReferenceParseWithWhitespace() + { + string sdkString = " \r\n \t Name \t \n \n \r / min=Version \t "; + SdkReference sdk; + var parsed = SdkReference.TryParse(sdkString, out sdk); + + Assert.True(parsed); + Assert.Equal("Name", sdk.Name); + Assert.Null(sdk.Version); + Assert.Equal("Version", sdk.MinimumVersion); + Assert.Equal("Name/min=Version", sdk.ToString()); + } + + [Theory] + [InlineData("")] + [InlineData("/")] + [InlineData("/Version")] + public void VerifySdkReferenceParseWith(string sdkString) + { + SdkReference sdk; + var parsed = SdkReference.TryParse(sdkString, out sdk); + Assert.False(parsed); + Assert.Null(sdk); + } + + [Fact] + public void VerifySdkReferenceEquality() + { + SdkReference sdk = new SdkReference("Name", "Version", "Min"); + + Assert.Equal(sdk, new SdkReference("Name", "Version", "Min")); + Assert.NotEqual(sdk, new SdkReference("Name", "Version", null)); + Assert.NotEqual(sdk, new SdkReference("Name", null, "Min")); + Assert.NotEqual(sdk, new SdkReference("Name", null, null)); + Assert.NotEqual(sdk, new SdkReference("Name", "version", "Min")); + Assert.NotEqual(sdk, new SdkReference("name", "Version", "Min")); + Assert.NotEqual(sdk, new SdkReference("Name", "Version", "min")); + Assert.NotEqual(sdk, new SdkReference("Name2", "Version", "Min")); + } + } +} diff --git a/src/Framework/Microsoft.Build.Framework.csproj b/src/Framework/Microsoft.Build.Framework.csproj index 5a42bea92bc..27f307d2218 100644 --- a/src/Framework/Microsoft.Build.Framework.csproj +++ b/src/Framework/Microsoft.Build.Framework.csproj @@ -121,6 +121,7 @@ true + true @@ -129,6 +130,11 @@ + + + + + true diff --git a/src/Framework/Sdk/SdkLogger.cs b/src/Framework/Sdk/SdkLogger.cs new file mode 100644 index 00000000000..883ddb109dd --- /dev/null +++ b/src/Framework/Sdk/SdkLogger.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Build.Framework +{ + /// + /// An abstract interface class to providing real-time logging and status while resolving + /// an SDK. + /// + public abstract class SdkLogger + { + /// + /// Log a build message to MSBuild. + /// + /// Message string. + /// Optional message importances. Default to low. + public abstract void LogMessage(string message, MessageImportance messageImportance = MessageImportance.Low); + } +} diff --git a/src/Framework/Sdk/SdkReference.cs b/src/Framework/Sdk/SdkReference.cs new file mode 100644 index 00000000000..d095fd3a5a3 --- /dev/null +++ b/src/Framework/Sdk/SdkReference.cs @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Linq; + +namespace Microsoft.Build.Framework +{ + /// + /// Represents a software development kit (SDK) that is referenced in a <Project /> or <Import /> element. + /// + public sealed class SdkReference : IEquatable + { + /// + /// Initializes a new instance of the SdkReference class. + /// + /// The name of the SDK. + /// The version of the SDK. + /// Minimum SDK version required by the project. + public SdkReference(string name, string version, string minimumVersion) + { + Name = name; + Version = version; + MinimumVersion = minimumVersion; + } + + /// + /// Gets the name of the SDK. + /// + public string Name { get; } + + /// + /// Gets the version of the SDK. + /// + public string Version { get; } + + /// + /// Gets the minimum version required. This value is specified by the project to indicate the minimum version of the + /// SDK that is required in order to build. This is useful in order to produce an error message if a name match can + /// be found but no acceptable version could be resolved. + /// + public string MinimumVersion { get; } + + /// + /// + /// + /// + public bool Equals(SdkReference other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return string.Equals(Name, other.Name) && string.Equals(Version, other.Version) && + string.Equals(MinimumVersion, other.MinimumVersion); + } + + /// + /// Attempts to parse the specified string as a . The expected format is: + /// SDK, SDK/Version, or SDK/min=MinimumVersion + /// Values are not required to specify a version or MinimumVersion. + /// + /// An SDK name and version to parse in the format "SDK/Version,min=MinimumVersion". + /// A parsed if the specified value is a valid SDK name. + /// true if the SDK name was successfully parsed, otherwise false. + public static bool TryParse(string sdk, out SdkReference sdkReference) + { + sdkReference = null; + if (string.IsNullOrWhiteSpace(sdk)) return false; + + var parts = sdk.Split('/').Select(i => i.Trim()).ToArray(); + + if (parts.Length < 1 || parts.Length > 2) return false; + if (string.IsNullOrWhiteSpace(parts[0])) return false; + + if (parts.Length == 1 || string.IsNullOrWhiteSpace(parts[1])) + { + sdkReference = new SdkReference(parts[0], null, null); + } + else if (parts.Length == 2) + { + // If the version specified starts with "min=" treat the string as a minimum version, otherwise + // treat it as a version. + sdkReference = parts[1].StartsWith("min=", StringComparison.OrdinalIgnoreCase) + ? new SdkReference(parts[0], null, parts[1].Substring(4)) + : new SdkReference(parts[0], parts[1], null); + } + + return sdkReference != null; + } + + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj is SdkReference && Equals((SdkReference) obj); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashCode = (Name != null ? Name.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (Version != null ? Version.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (MinimumVersion != null ? MinimumVersion.GetHashCode() : 0); + return hashCode; + } + } + + /// + public override string ToString() + { + if (string.IsNullOrWhiteSpace(Version) && string.IsNullOrWhiteSpace(MinimumVersion)) + { + return Name; + } + + return string.IsNullOrWhiteSpace(Version) ? + $"{Name}/min={MinimumVersion}" : + $"{Name}/{Version}"; + } + } +} diff --git a/src/Framework/Sdk/SdkResolver.cs b/src/Framework/Sdk/SdkResolver.cs new file mode 100644 index 00000000000..0dd77816872 --- /dev/null +++ b/src/Framework/Sdk/SdkResolver.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Build.Framework +{ + /// + /// An abstract interface for classes that can resolve a Software Development Kit (SDK). + /// + public abstract class SdkResolver + { + /// + /// Name of the SDK resolver to be displayed in build output log. + /// + public abstract string Name { get; } + + /// + /// Gets the self-described resolution priority order. MSBuild will sort resolvers + /// by this value. + /// + public abstract int Priority { get; } + + /// + /// Resolves the specified SDK reference. + /// + /// A containing the referenced SDKs be resolved. + /// Context for resolving the SDK. + /// Factory class to create an + /// + /// An containing the resolved SDKs or associated error / reason + /// the SDK could not be resolved. + /// + /// Note: You must use the to return a result. + /// + /// + public abstract SdkResult Resolve(SdkReference sdkReference, SdkResolverContext resolverContext, + SdkResultFactory factory); + } +} diff --git a/src/Framework/Sdk/SdkResolverContext.cs b/src/Framework/Sdk/SdkResolverContext.cs new file mode 100644 index 00000000000..d98230af57f --- /dev/null +++ b/src/Framework/Sdk/SdkResolverContext.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Build.Framework +{ + /// + /// Context used by an to resolve an SDK. + /// + public abstract class SdkResolverContext + { + /// + /// Logger to log real-time messages back to MSBuild. + /// + public virtual SdkLogger Logger { get; protected set; } + + /// + /// Path to the project file being built. + /// + public virtual string ProjectFilePath { get; protected set; } + + /// + /// Path to the solution file being built, if known. May be null. + /// + public virtual string SolutionFilePath { get; protected set; } + } +} diff --git a/src/Framework/Sdk/SdkResult.cs b/src/Framework/Sdk/SdkResult.cs new file mode 100644 index 00000000000..035fed29153 --- /dev/null +++ b/src/Framework/Sdk/SdkResult.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Build.Framework +{ + /// + /// An abstract interface class to indicate SDK resolver success or failure. + /// + /// Note: Use to create instances of this class. Do not + /// inherit from this class. + /// + /// + public abstract class SdkResult + { + /// + /// Indicates the resolution was successful. + /// + public bool Success { get; protected set; } + } +} diff --git a/src/Framework/Sdk/SdkResultFactory.cs b/src/Framework/Sdk/SdkResultFactory.cs new file mode 100644 index 00000000000..cca970e6d0e --- /dev/null +++ b/src/Framework/Sdk/SdkResultFactory.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +namespace Microsoft.Build.Framework +{ + /// + /// An abstract interface class provided to to create an + /// object indicating success / failure. + /// + public abstract class SdkResultFactory + { + /// + /// Create an object indicating success resolving the SDK. + /// + /// Path to the SDK. + /// Version of the SDK that was resolved. + /// Optional warnings to display during resolution. + /// + public abstract SdkResult IndicateSuccess(string path, string version, IEnumerable warnings = null); + + /// + /// Create an object indicating failure resolving the SDK. + /// + /// + /// Errors / reasons the SDK could not be resolved. Will be logged as a + /// build error if no other SdkResolvers were able to indicate success. + /// + /// + /// + public abstract SdkResult IndicateFailure(IEnumerable errors, IEnumerable warnings = null); + } +} diff --git a/src/Shared/XMakeAttributes.cs b/src/Shared/XMakeAttributes.cs index b34759d7653..85e6d822d2e 100644 --- a/src/Shared/XMakeAttributes.cs +++ b/src/Shared/XMakeAttributes.cs @@ -44,6 +44,9 @@ 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 sdkMinimumVersion = "MinimumVersion"; 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[] { '$', '@', '(', ')', '%', '*', '?', '.' };