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

Implicit Sdk imports #1403

Merged

Conversation

rainersigwald
Copy link
Member

First iteration of #1392.

Known issues:

  • Doesn't define a default location for the SDKs. MUST FIX
  • Allows Sdk attribute on any <Project> element, even ones that are (recursively) imported. That's confusing and wrong.
  • Preprocessed output is not buildable (will double-import SDK imports)
  • Not enough tests (I'll start working on this next, wanted to get this out)

Example of use:
test.proj

<Project Sdk="TestSdk">
  <PropertyGroup>
    <PropertyDefinedInProjectFromImportedProperty>Local/$(PropertyFromSdkImport)</PropertyDefinedInProjectFromImportedProperty>
  </PropertyGroup>

  <Target Name="Build">
    <Message Importance="High"
             Text="Build target. Prop: $(PropertyFromSdkImport) LocalProp: $(PropertyDefinedInProjectFromImportedProperty)" />
  </Target>
</Project>

TestSdk\Sdk\Sdk.props

<Project>
  <PropertyGroup>
    <PropertyFromSdkImport>Imported Property!</PropertyFromSdkImport>
  </PropertyGroup>
</Project>

TestSdk\Sdk\Sdk.targets

<Project>
  <Target Name="SdkTarget"
          BeforeTargets="Build">
    <Message Importance="High"
             Text="Target from the SDK" />
  </Target>
</Project>
S:\work\autoimports>s:\msbuild\bin\Bootstrap\15.0\Bin\MSBuild.exe test.proj
Microsoft (R) Build Engine version 15.1.425.23802
Copyright (C) Microsoft Corporation. All rights reserved.

Build started 11/28/2016 2:58:47 PM.
Project "S:\work\autoimports\test.proj" on node 1 (default targets).
SdkTarget:
  Target from the SDK
Build:
  Build target. Prop: Imported Property! LocalProp: Local/Imported Property!
Done Building Project "S:\work\autoimports\test.proj" (default targets).


Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:00.55

preprocessor output:

<?xml version="1.0" encoding="utf-8"?>
<!--
============================================================================================================================================
S:\work\autoimports\test.proj
============================================================================================================================================
-->
<Project Sdk="TestSdk">
  <!--
============================================================================================================================================
  Import of "S:\work\autoimports\TestSdk\Sdk\Sdk.props" from Sdk "TestSdk" was implied by the Project element's Sdk attribute.

S:\work\autoimports\TestSdk\Sdk\Sdk.props
============================================================================================================================================
-->
  <PropertyGroup>
    <PropertyFromSdkImport>Imported Property!</PropertyFromSdkImport>
  </PropertyGroup>
  <!--
============================================================================================================================================
  </Import>

S:\work\autoimports\test.proj
============================================================================================================================================
-->
  <PropertyGroup>
    <PropertyDefinedInProjectFromImportedProperty>Local/$(PropertyFromSdkImport)</PropertyDefinedInProjectFromImportedProperty>
  </PropertyGroup>
  <Target Name="Build">
    <Message Importance="High" Text="Build target. Prop: $(PropertyFromSdkImport) LocalProp: $(PropertyDefinedInProjectFromImportedProperty)" />
  </Target>
  <!--
============================================================================================================================================
  Import of "S:\work\autoimports\TestSdk\Sdk\Sdk.targets" from Sdk "TestSdk" was implied by the Project element's Sdk attribute.

S:\work\autoimports\TestSdk\Sdk\Sdk.targets
============================================================================================================================================
-->
  <Target Name="SdkTarget" BeforeTargets="Build">
    <Message Importance="High" Text="Target from the SDK" />
  </Target>
  <!--
============================================================================================================================================
  </Import>

S:\work\autoimports\test.proj
============================================================================================================================================
-->
</Project>

@@ -540,6 +545,15 @@ internal void AddToXml(ProjectElement child)
}
}

private static bool HasXmlRepresentation(ProjectElement element)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the IsImplicit property should be enough, do you think we need a method to call instead of checking the property?

Copy link
Member Author

Choose a reason for hiding this comment

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

It wasn't enough in the original PR because I only put Implicit on ProjectImportElement. But you're right: if I just extend it to all ProjectElements this becomes much cleaner.

private readonly string _name;
private readonly string _originalValue;

public TemporaryEnvironment(string name, string value)
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make sense for this to take an IDictionary<string, string> so you could have one or more variables set?

Copy link
Member Author

Choose a reason for hiding this comment

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

I envisioned a cascading set of usings but yeah, that's more elegant. Mind if we wait until after RC2?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'll leave it up to you

@@ -83,6 +83,7 @@ public partial class ProjectExtensionsElement : Microsoft.Build.Construction.Pro
public partial class ProjectImportElement : Microsoft.Build.Construction.ProjectElement
{
internal ProjectImportElement() { }
public bool Implicit { get { throw null; } }
Copy link
Contributor

Choose a reason for hiding this comment

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

IsImplicit sounds better to me but I'll leave it up to you.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, looks like that matches existing ones better.


var sdksString = element.GetAttribute(XMakeAttributes.sdk);

var sdks = sdksString.Contains(";") ? sdksString.Split(';') : new[] {sdksString};
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 use .Split(';', RemoveEmptyEntries).Select(i => i.Trim()).Where(i => !String.IsNullOrWhiteSpace()) to be as robust as possible.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, absolutely! This file didn't use LINQ so I did it manually.

/// </summary>
public ElementLocation ProjectLocation
// TODO: *should* this be public? if it's not, you can't determine if an import is implicit from the public OM.
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it should be public. With no public setter, it seems safe.

var implicitElements =
xmlWithNoImplicits.SelectNodes($"//*[@{XMakeAttributes.@implicit}]");

if (implicitElements == null)
Copy link
Contributor

Choose a reason for hiding this comment

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

Would this ever happen? I think if the document is parsed and successfully cloned, this would always return a value?

Copy link
Member Author

Choose a reason for hiding this comment

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

I can't think of a reason for this to fail either, but it seemed worth checking and throwing. Would you rather I just let a NRE happen later?

Copy link
Contributor

Choose a reason for hiding this comment

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

I did some searching and couldn't find an instance where it would return null unless the document couldn't be parsed. I think its safe to remove the check personally.

if (((XmlElement)child).GetAttribute(XMakeAttributes.@implicit).Length > 0)
{
importTag =
$" Import of \"{importProject}\" from Sdk \"{importSdk}\" was implied by the {XMakeElements.project} element's {XMakeAttributes.sdk} attribute.";
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

Copy link
Member

@dsplaisted dsplaisted left a comment

Choose a reason for hiding this comment

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

LGTM, the only thing I really want changed is for you to get a build error if you specify an SDK that doesn't exist, or is misspelled. I also think we may need to switch to handling the Sdk attribute in the evaluator rather than in the parser (for example, to handle dependencies between Sdks and importing the props/targets in the right order). However, let's go with what works for now for the first preview with this feature.

  • Doesn't define a default location for the SDKs. MUST FIX

Do we actually need to fix this? My understanding is that we were going to set this for full framework MSBuild in its app.config file, and the .NET CLI would set the environment variable when it launched an MSBuild process.

  • Allows Sdk attribute on any <Project> element, even ones that are (recursively) imported. That's confusing and wrong.

Agreed that this should be disabled now, but I'm noodling on a design for handling Sdk dependencies where an Sdk's Sdk.props file would list it's dependencies using the Sdk attribute of the Project element.

@@ -619,7 +633,11 @@ private void AddInitialChild(ProjectElement child)
AddToXml(child);

_count++;
MarkDirty("Add child element named '{0}'", child.ElementName);

if (HasXmlRepresentation(child))
Copy link
Member

Choose a reason for hiding this comment

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

Wouldn't you also want to suppress marking the tree as dirty if the child is implicit in InsertBeforeChild and InsertAfterChild?

/// Location of the project attribute
/// </summary>
/// <remarks>
/// For an implicit import, the location points to the Sdk attribute on the Project element.
Copy link
Member

Choose a reason for hiding this comment

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

This line of the comment doesn't seem to match the behavior in the code.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch--remnant of a previous attempt.

{
get { return XmlElement.GetAttributeLocation(XMakeAttributes.project); }
get { return XmlElement.HasAttribute(XMakeAttributes.@implicit); }
Copy link
Member

Choose a reason for hiding this comment

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

Could we represent whether an import is implicit or not with a field in this class instead of a separate attribute? What happens if someone actually uses the attribute?

<Import Project="foo.targets" _Implicit="Foo" />

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Also, I tried using a field in this class, but it caused problems with the preprocessor, since it walks only the XML tree.

@@ -1714,8 +1714,23 @@ public void Save(Encoding saveEncoding)
{
using (ProjectWriter projectWriter = new ProjectWriter(_projectFileLocation.File, saveEncoding))
{
projectWriter.Initialize(XmlDocument);
XmlDocument.Save(projectWriter);
var xmlWithNoImplicits = (XmlDocument)XmlDocument.Clone();
Copy link
Member

Choose a reason for hiding this comment

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

At first glance the variable name led me to believe that it wouldn't have any implicit imports when it was cloned, not that you were going to remove them afterwards. It might be possible to make this clearer with a better name.

@@ -1081,7 +1081,6 @@ private void PerformDepthFirstPass(ProjectRootElement currentProjectOrImport)
}

ProjectImportElement import = element as ProjectImportElement;

Copy link
Member

Choose a reason for hiding this comment

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

No-op whitespace change here.

var finalImportPath = Path.Combine(Environment.GetEnvironmentVariable("MSBuildSDKsPath"),
sdkName, "Sdk", "Sdk.targets");

if (File.Exists(initialImportPath))
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 we need to generate an error if an Sdk is specified that is not found. However, that should be an error at evaluation, not parse time. To get that behavior for now, I think we should always add both implicit import elements whether the files exist or not, which will mean that all of our Sdks will need to have both Sdk.props and Sdk.targets.

When we add support for an Sdk attribute on an Import and implement the Project-level Sdk attribute in terms of Sdk imports, we may be able to change how this works.

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
Copy link
Member

Choose a reason for hiding this comment

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

No-op changes (EOL probably) here and at end of file.

{
Directory.CreateDirectory(testSdkDirectory);

string sdkPropsPath = Path.Combine(testSdkDirectory, "Sdk.props");
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 needs to be Path.Combine(testSdkDirectory, "Sdk", "Sdk.props") to match the path where the parser is going to look for the Sdk imports.

@@ -45,53 +45,56 @@ public void ReadNone()
public void ReadInvalidMissingProject()
{
Assert.Throws<InvalidProjectFileException>(() =>
{
string content = @"
{
Copy link
Member

Choose a reason for hiding this comment

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

Lots of whitespace changes in this file.

File.WriteAllText(sdkPropsPath, string.Empty);
File.WriteAllText(sdkTargetsPath, string.Empty);

using (new Helpers.TemporaryEnvironment("MSBUILDMAGICIMPORTDIRECTORY", testSdkRoot))
Copy link
Member

Choose a reason for hiding this comment

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

Change to MSBuildSDKsPath environment variable.

if (sdkName.Contains("/"))
{
ProjectErrorUtilities.ThrowInvalidProject(element.GetAttributeLocation(XMakeAttributes.sdk),
"InvalidSdkFormatTooManySlashes");
Copy link
Member Author

Choose a reason for hiding this comment

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

Self, actually add this string to the resx.

@@ -1714,8 +1714,18 @@ public void Save(Encoding saveEncoding)
{
using (ProjectWriter projectWriter = new ProjectWriter(_projectFileLocation.File, saveEncoding))
{
projectWriter.Initialize(XmlDocument);
XmlDocument.Save(projectWriter);
var xmlWithNoImplicits = (XmlDocument)XmlDocument.Clone();
Copy link
Member

Choose a reason for hiding this comment

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

The Clone() method doesn't seem to be available in the .NET Core build. Probably the fix is to switch to CloneNode() instead.

Copy link
Member Author

Choose a reason for hiding this comment

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

ah, I apparently hadn't committed and pushed that fix when I made it. You're right--the replacement is CloneNode(true).

@rainersigwald
Copy link
Member Author

rainersigwald commented Nov 30, 2016

Consensus has emerged that the default value of the SDK location should be {MSBuild directory}\Sdks. See dotnet/sdk#444 for example.

I need to:

  • Create an internal variable for that
  • Make a property function to expose it
  • Make a property with the same name in toolsets
    • Full Framework
    • .NET Core

@rainersigwald rainersigwald changed the base branch from xplat to vs15-rc2 November 30, 2016 13:25
@rainersigwald
Copy link
Member Author

@dotnet-bot test Windows_NT Build for Desktop please

@rainersigwald rainersigwald force-pushed the sdk-imports-virtual-elements branch 3 times, most recently from b3f2666 to fb6455d Compare November 30, 2016 14:06
@rainersigwald
Copy link
Member Author

I'm going to rebase -i to a semi-coherent commit structure to resolve the merge to rc2

@rainersigwald rainersigwald force-pushed the sdk-imports-virtual-elements branch from 5b5283b to 7303fcd Compare November 30, 2016 16:40
@@ -40,9 +40,11 @@ internal static class XMakeAttributes
internal const string taskName = "TaskName";
internal const string continueOnError = "ContinueOnError";
internal const string project = "Project";
internal const string @implicit = "_Implicit";
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this be isImplicit so you can get rid of the @? Should the attribute name just be IsImplicit?

@rainersigwald rainersigwald force-pushed the sdk-imports-virtual-elements branch from 7303fcd to 681b98a Compare November 30, 2016 16:57
@rainersigwald rainersigwald force-pushed the sdk-imports-virtual-elements branch from 681b98a to 9801c6d Compare November 30, 2016 17:14
@@ -1264,6 +1264,11 @@ internal static string[] CreateFiles(params string[] files)
/// </summary>
internal static string[] CreateFilesInDirectory(string rootDirectory, params string[] files)
{
if (files == null)
{
return null;
Copy link
Contributor

Choose a reason for hiding this comment

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

return null; [](start = 16, length = 12)

Return empty array so the upstream code can work without checking for null?

@rainersigwald rainersigwald force-pushed the sdk-imports-virtual-elements branch from cc2122a to 94214d3 Compare November 30, 2016 18:57
@@ -57,6 +57,7 @@ protected override IEnumerable<ToolsetPropertyDefinition> ToolsVersions
protected override IEnumerable<ToolsetPropertyDefinition> GetPropertyDefinitions(string toolsVersion)
{
yield return new ToolsetPropertyDefinition(MSBuildConstants.ToolsPath, BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory, _sourceLocation);
yield return new ToolsetPropertyDefinition(MSBuildConstants.SdksPath, BuildEnvironmentHelper.Instance.MSBuildSDKsPath, _sourceLocation);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this new ToolsetProperty read from anywhere? If it is, it looks like ToolsetConfigurationReader needs to be updated as well to support reading the SDK from the app.config too.
Apparently the local reader is only used on xplat as hardcoded values because there are no app configs on .net core.

Copy link
Member Author

Choose a reason for hiding this comment

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

Not by MSBuild itself, but the first iteration of some of the magic SDKs plan to do SDK-to-SDK references using the property. It's set here for .NET Core and in app.config for full.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think you may also have to change ToolsetConfigurationReader to make the property flow from the app config too.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, wait, no, that happens automatically, no need to change anything :)


var sdksString = element.GetAttribute(XMakeAttributes.sdk);

var sdks = sdksString.Contains(";") ? sdksString.Split(';') : new[] {sdksString};
Copy link
Contributor

Choose a reason for hiding this comment

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

sdksString.Split(';') [](start = 54, length = 21)

You could also look into using ExpressionShredder.SplitSemiColonSeparatedList. Though it might be overkill for this situation.

Copy link
Member Author

Choose a reason for hiding this comment

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

worth thinking about for vnext

The if-exists checks meant that if you had a badly defined MSBuildSDKsPath
or an SDK name that didn't exist, you would get no error and just silently
get no imports. That's no good.

This adds the implicit import unconditionally, requiring all SDKs to have
both an Sdk.props and an Sdk.targets for the moment.
@rainersigwald rainersigwald force-pushed the sdk-imports-virtual-elements branch from eec49d9 to b3f22f8 Compare November 30, 2016 19:15
@rainersigwald rainersigwald dismissed dsplaisted’s stale review November 30, 2016 19:50

Addressed the describe "only blocking issue"

@rainersigwald rainersigwald merged commit 6490226 into dotnet:vs15-rc2 Nov 30, 2016
@rainersigwald rainersigwald modified the milestone: MSBuild Sdks Dec 4, 2016
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.

6 participants