-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for SDK Resolvers #2002
Conversation
2297ba3
to
b9aa944
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Partial feedback on tests + interfaces only = "design feedback". Continuing to review for implementation.
@@ -360,6 +360,17 @@ public partial class ProjectStartedEventArgs : Microsoft.Build.Framework.BuildSt | |||
public string ToolsVersion { get { throw null; } } | |||
} | |||
public delegate void ProjectStartedEventHandler(object sender, Microsoft.Build.Framework.ProjectStartedEventArgs e); | |||
public sealed partial class ReferencedSdk : System.IEquatable<Microsoft.Build.Framework.ReferencedSdk> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be named SdkReference to read more clearly.
@@ -334,6 +335,13 @@ public partial class ProjectRootElement : Microsoft.Build.Construction.ProjectEl | |||
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did we get conceptual buy off on this from the people that vetoed it before?
Assert.Equal(6, children.Count); | ||
|
||
// <Sdk> style will have an extra ProjectElment. | ||
var expected = projectFormatString.Contains("Sdk=") ? 6 : 7; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make this an input instead? (Can wait for a cleanup PR)
@@ -24,6 +24,13 @@ namespace Microsoft.Build.UnitTests.BackEnd | |||
/// </summary> | |||
internal class MockLoggingService : ILoggingService | |||
{ | |||
private Action<string> _writter; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: writer
@@ -24,6 +24,13 @@ namespace Microsoft.Build.UnitTests.BackEnd | |||
/// </summary> | |||
internal class MockLoggingService : ILoggingService | |||
{ | |||
private Action<string> _writter; | |||
|
|||
public MockLoggingService(Action<string> writter = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a fan of this approach. I'll send a commit to use xunit output capture.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can wait.
{ | ||
// 2sdkName will cause MockSdkResolver1 to fail with an error reason. The error will not | ||
// be logged because MockSdkResolver2 will succeed. | ||
var log = new StringBuilder(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment copypasta
|
||
// Resolver2 gives a warning on success or failure. | ||
Assert.Contains("WARNING2", logResult); | ||
Assert.DoesNotContain("ERROR", logResult); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can wait, but: I don't think this is the right logging strategy. We should look at how NuGet handles multiple package sources and learn from their experience/mistakes. I think we should log every message from all resolvers but demote ERROR
to INFO
--otherwise, how will we diagnose the situation "the first resolver was supposed to resolve this but didn't"?
File.WriteAllText(f3, string.Empty); | ||
File.WriteAllText(f4, string.Empty); | ||
|
||
var strategy = new SdkResolverLoadingStrategy(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd probably name this thing SdkResolverLoader
.
File.WriteAllText(f4, string.Empty); | ||
|
||
var strategy = new SdkResolverLoadingStrategy(); | ||
var files = strategy.FindPotentialSdkResolvers(root); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FindResolvers()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, never mind. This is an internal implementation detail and is appropriately named.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems reasonable overall.
{ | ||
/// <summary> | ||
/// Default SDK resolver for compatibility with VS2017 RTM. | ||
/// </summary> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a <remarks>
describing what that means (searching in env var, next to MSBuild)?
{ | ||
protected SdkResolver() { } | ||
public abstract string Name { get; } | ||
public abstract int Priority { get; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really hope we can come up with a better implementation here but we can always ignore this field in the future so it should be reasonably harmless.
{ | ||
/// <summary> | ||
/// Component responsible for resolving an SDK to a file path. Loads and coordinates | ||
/// with <see cref="SdkResolver" /> plug-ins. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I get the name now. But I still don't like it. SdkResolver
s
?
var resultFactory = new SdkResultFactoryImpl(sdk); | ||
try | ||
{ | ||
var result = (SdkResultImpl)sdkResolver.Resolve(sdk, context, resultFactory); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Casting to ...Impl
seems pretty wrong here. Why is it necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same. All usages of result
in this method should be of the supertype members. The Resolver could even choose not to use the resultFactory and use its own result implementation :)
In reply to: 112969534 [](ancestors = 112969534)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still concerned about this. Does it represent a design flaw, or is it just an implementation fix we can deal with later?
} | ||
catch (Exception e) | ||
{ | ||
logger.LogFatalBuildError(buildEventContext, e, new BuildEventFileInfo(importElementLocation)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This catch
looks redundant with the overall one, except it doesn't rethrow? Just to avoid onion-peeling on resolver exceptions?
|
||
// Evaluate the "top" implicit imports as if they were the first entry in the file. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, so that makes the order I complained about above less relevant. Cool.
src/Build/Evaluation/Evaluator.cs
Outdated
if (string.IsNullOrEmpty(sdkRootPath)) | ||
{ | ||
ProjectErrorUtilities.ThrowInvalidProject(importElement.SdkLocation, "CouldNotResolveSdk", importElement.ParsedReferencedSdk.ToString()); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was a bit surprised to see this; I expected GetSdkPath
to throw on failure.
src/Framework/Sdk/ReferencedSdk.cs
Outdated
return false; | ||
if (ReferenceEquals(this, other)) | ||
return true; | ||
return string.Equals(Name, other.Name) && Equals(Version, other.Version); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We decided to be case-sensitive for Sdk names? I think we did as a side effect of the initial implementation on case-sensitive file systems, but I don't remember for sure. Should be doc'ed either way.
src/Framework/Sdk/ReferencedSdk.cs
Outdated
{ | ||
unchecked | ||
{ | ||
return ((Name?.GetHashCode() ?? 0) * 397) ^ (Version?.GetHashCode() ?? 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Every time I see this pattern I weep a bit. Where'd you get 397
from?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jeffkl not sure. I think Resharper generated all of the IEquatable
stuff.
src/Framework/Sdk/SdkResolver.cs
Outdated
/// <summary> | ||
/// Resolves the specified SDKs. | ||
/// </summary> | ||
/// <param name="referencedSdk">A <see cref="ReferencedSdk" /> containing the referenced SDKs be resolved.</param> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: plurality disagreement
{ | ||
public override string Name => "DefaultSdkResolver"; | ||
|
||
public override int Priority => 10000; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll have to come up with a better prioritization scheme for 3rd party support. Even if we do not expect many resolver implementations, there still will be some, and we can't go with a global registry of priorities. :)
|
||
private void Initialize(ILoggingService logger, BuildEventContext buildEventContext, ElementLocation location) | ||
{ | ||
lock (_lockObject) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not a thread safe Lazy?
/// <param name="sdk">SDK referenced by the Project.</param> | ||
/// <param name="logger">Logging service.</param> | ||
/// <param name="buildEventContext">Build event context for logging.</param> | ||
/// <param name="importElementLocation">Location of the element within the project which referenced the SDK.</param> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
importElementLocation makes it sound like SDKs can only come from msbuild import elements. Maybe SDKReferenceLocation would be a better name?
|
||
foreach (var result in results) | ||
{ | ||
LogWarnings(logger, buildEventContext, importElementLocation, result); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doesn't this lead to re-logging the success results again? They also get logged at line 68
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{ | ||
LogWarnings(logger, buildEventContext, importElementLocation, result); | ||
|
||
if (result.Errors != null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd log errors only when:
- there was no successful resolver
- there was at least one successful resolver and the log verbosity is diagnostic. And in this case I'd maybe convert the errors to warnings / messages so they don't fail the build.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
However, I'd still log errors when verbosity is diagnostic. To help users debug all resolvers and understand why a certain one did not work.
In reply to: 113016807 [](ancestors = 113016807,113016412)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Though I'd still convert the errors to messages if the log verbosity is diagnostic.
lock (_lockObject) | ||
{ | ||
if (_resolvers != null) return; | ||
_resolvers = _sdkResolverLoadingStrategy.LoadResolvers(logger, buildEventContext, location); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wonder if it's worth logging the resolver order when the log is diagnostic. Could be really helpful when debugging the resolver chain.
/// <summary> | ||
/// Indicates the resolution was successful. | ||
/// </summary> | ||
public bool Success { get; protected set; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the SdkResultFactory mentions warnings and errors, maybe the SdkResult should do to. Either via Warning / Error properties, or a Diagnostic list, where the Diagnostic.Type is either Warning / Error. This way you could avoid casting to the implementation.
public class SdkResolverLoadingStrategy_Tests | ||
{ | ||
[Fact] | ||
public void AssertDefaultLoadingStretegyReturnsDefaultResolver() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Stretegy typo
ElementLocation location) | ||
{ | ||
// Always add the default resolver | ||
var resolvers = new List<SdkResolver> {new DefaultSdkResolver()}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the default resolver only get added when there are no explicit resolvers set?
If the default is there all the time, it means it might work when the build should actually fail. Maybe the SdkResult
should contain a ShouldFailTheBuild
member.
src/Framework/Sdk/ReferencedSdk.cs
Outdated
|
||
if (!string.IsNullOrWhiteSpace(sdk)) | ||
{ | ||
var parts = sdk.Split('/').Select(i => i.Trim()).ToArray(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about the range syntax (e.g.FooSDK/min:3.4
)? I guess MSBuild should be doing that, otherwise all resolvers would duplicate the logic.
@@ -91,7 +91,12 @@ public string Sdk | |||
/// added because of the <see cref="ProjectRootElement.Sdk"/> attribute and the location where the project was | |||
/// imported. | |||
/// </summary> | |||
public ImplicitImportLocation ImplicitImportLocation { get; internal set; } = ImplicitImportLocation.None; | |||
public ImplicitImportLocation ImplicitImportLocation { get; internal set; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Who sets the default to None
? The ProjectParser
could do it when it hits real import elements.
nodes.Add(ProjectImportElement.CreateImplicit("Sdk.targets", currentProjectOrImport, ImplicitImportLocation.Bottom, referencedSdk)); | ||
} | ||
|
||
foreach (var sdkNode in Children.OfType<ProjectSdkElement>()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if, the ProjectSdkElement
has a condition on it? The ProjectElementContainer allows for it. Then, imports that are placed above an Sdk element might affect that condition via conditions / file system side effects.
6749ad4
to
c74c8e5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only remaining major concern is the Impl
cast, if it's a design flaw.
@@ -9,6 +9,12 @@ namespace Microsoft.Build.BackEnd | |||
{ | |||
/// <summary> | |||
/// Default SDK resolver for compatibility with VS2017 RTM. | |||
/// <remarks> | |||
/// Default Sdk folder will to: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"will to"?
[Fact] | ||
public void VerifySdkReferenceParseWithWhitespace() | ||
{ | ||
string sdkString = " Name / min=Version "; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you make one of these hunks a \t
, just for good measure?
var resultFactory = new SdkResultFactoryImpl(sdk); | ||
try | ||
{ | ||
var result = (SdkResultImpl)sdkResolver.Resolve(sdk, context, resultFactory); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still concerned about this. Does it represent a design flaw, or is it just an implementation fix we can deal with later?
src/Framework/Sdk/SdkResolver.cs
Outdated
@@ -28,6 +28,9 @@ public abstract class SdkResolver | |||
/// <returns> | |||
/// An <see cref="SdkResult" /> containing the resolved SDKs or associated error / reason | |||
/// the SDK could not be resolved. | |||
/// <remarks> | |||
/// Note: You must use the <see cref="SdkResult" /> to return a result. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SdkResultFactory
?
d8d7501
to
3d1fa75
Compare
* 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 dotnet#1493
3d1fa75
to
b0a6d07
Compare
Let me know when packages are available and what version to use. |
* This feature was regressed in dotnet#2002 * Update unit tests to verify functionality * Update ProjectParse to parse values and construct an SdkReference object to be used by the evaluator. Closes dotnet#2034
* This feature was regressed in dotnet#2002 * Update unit tests to verify functionality * Update ProjectParser to parse values and construct an SdkReference object to be used by the evaluator. Closes dotnet#2034
* This feature was regressed in dotnet#2002 * Update unit tests to verify functionality * Update ProjectParser to parse SDK name/version values and construct an SdkReference object to be used by the evaluator. Closes dotnet#2034
* This feature was regressed in dotnet#2002 * Update unit tests to verify functionality * Update ProjectParser to parse SDK name/version values and construct an SdkReference object to be used by the evaluator. Closes dotnet#2034
Sending the PR for some initial feedback. I haven't reviewed it as carefully as I'd like (I'm sure there are nit items), and things I know I still have left to do: