Skip to content
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

Merged
merged 1 commit into from
Apr 25, 2017

Conversation

AndyGerlicher
Copy link
Contributor

@AndyGerlicher AndyGerlicher commented Apr 22, 2017

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:

  • Test on CoreCLR (haven't tried it yet).
  • Further testing within Visual Studio.
  • Load resolver(s) in a unit test (didn't have a good way to do this, may have to come later).

Copy link
Member

@rainersigwald rainersigwald left a 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>
Copy link
Member

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
Copy link
Member

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;
Copy link
Member

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;
Copy link
Member

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)
Copy link
Member

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.

Copy link
Member

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();
Copy link
Member

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);
Copy link
Member

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();
Copy link
Member

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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FindResolvers()

Copy link
Member

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.

Copy link
Member

@rainersigwald rainersigwald left a 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>
Copy link
Member

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; }
Copy link
Member

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.
Copy link
Member

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. SdkResolvers?

var resultFactory = new SdkResultFactoryImpl(sdk);
try
{
var result = (SdkResultImpl)sdkResolver.Resolve(sdk, context, resultFactory);
Copy link
Member

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?

Copy link
Contributor

@cdmihai cdmihai Apr 24, 2017

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)

Copy link
Member

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));
Copy link
Member

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.
Copy link
Member

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.

if (string.IsNullOrEmpty(sdkRootPath))
{
ProjectErrorUtilities.ThrowInvalidProject(importElement.SdkLocation, "CouldNotResolveSdk", importElement.ParsedReferencedSdk.ToString());
}
Copy link
Member

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.

return false;
if (ReferenceEquals(this, other))
return true;
return string.Equals(Name, other.Name) && Equals(Version, other.Version);
Copy link
Member

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.

{
unchecked
{
return ((Name?.GetHashCode() ?? 0) * 397) ^ (Version?.GetHashCode() ?? 0);
Copy link
Member

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?

Copy link
Contributor Author

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.

/// <summary>
/// Resolves the specified SDKs.
/// </summary>
/// <param name="referencedSdk">A <see cref="ReferencedSdk" /> containing the referenced SDKs be resolved.</param>
Copy link
Member

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;
Copy link
Contributor

@cdmihai cdmihai Apr 24, 2017

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)
Copy link
Contributor

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>
Copy link
Contributor

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?

@livarcocc
Copy link
Contributor

@nguerrera


foreach (var result in results)
{
LogWarnings(logger, buildEventContext, importElementLocation, result);
Copy link
Contributor

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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Never mind, did not see the return on success.


In reply to: 113014254 [](ancestors = 113014254)

{
LogWarnings(logger, buildEventContext, importElementLocation, result);

if (result.Errors != null)
Copy link
Contributor

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Never mind, did not see the return on success.


In reply to: 113016412 [](ancestors = 113016412)

Copy link
Contributor

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)

Copy link
Contributor

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);
Copy link
Contributor

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; }
Copy link
Contributor

@cdmihai cdmihai Apr 24, 2017

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()
Copy link
Contributor

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()};
Copy link
Contributor

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.


if (!string.IsNullOrWhiteSpace(sdk))
{
var parts = sdk.Split('/').Select(i => i.Trim()).ToArray();
Copy link
Contributor

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; }
Copy link
Contributor

@cdmihai cdmihai Apr 24, 2017

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>())
Copy link
Contributor

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.

@AndyGerlicher AndyGerlicher force-pushed the sdkresolvers branch 3 times, most recently from 6749ad4 to c74c8e5 Compare April 25, 2017 02:34
Copy link
Member

@rainersigwald rainersigwald left a 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:
Copy link
Member

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 ";
Copy link
Member

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);
Copy link
Member

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?

@@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SdkResultFactory?

@AndyGerlicher AndyGerlicher changed the base branch from master to preview/vs15.3 April 25, 2017 18:16
* 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
@AndyGerlicher AndyGerlicher changed the base branch from preview/vs15.3 to master April 25, 2017 18:34
@AndyGerlicher AndyGerlicher merged commit 9e22548 into dotnet:master Apr 25, 2017
@nguerrera
Copy link
Contributor

Let me know when packages are available and what version to use.

AndyGerlicher added a commit to AndyGerlicher/msbuild that referenced this pull request May 1, 2017
* 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
AndyGerlicher added a commit to AndyGerlicher/msbuild that referenced this pull request May 1, 2017
* 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
AndyGerlicher added a commit to AndyGerlicher/msbuild that referenced this pull request May 1, 2017
* 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
AndyGerlicher added a commit to AndyGerlicher/msbuild that referenced this pull request May 1, 2017
* 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
AndyGerlicher added a commit that referenced this pull request May 1, 2017
* This feature was regressed in #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 #2034
@AndyGerlicher AndyGerlicher deleted the sdkresolvers branch June 26, 2018 20:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants