Skip to content

Dotnet Tool impementation#1900

Merged
nkolev92 merged 67 commits intodevfrom
dev-nkolev92-globalTools
Jan 18, 2018
Merged

Dotnet Tool impementation#1900
nkolev92 merged 67 commits intodevfrom
dev-nkolev92-globalTools

Conversation

@nkolev92
Copy link
Copy Markdown
Member

@nkolev92 nkolev92 commented Dec 22, 2017

Bug

Fixes: NuGet/Home#6199, NuGet/Home#6198, NuGet/Home#6197, NuGet/Home#6200

Regression: No
If Regression then when did it last work:
If Regression then how are we preventing it in future:

Fix

Details:
Here's the spec for the global tools implementation.
https://github.com/NuGet/Home/wiki/Global-Tools---NuGet-Implementation

The brief is, we introduce a new package type for dotnet tools, with compatible asset as "tools/tfm/rid/any.extension.

That package is only compatible with a special project type that the CLI will create, DotnetToolReference.
1 package per project, that package has to be a dotnettool.
DotnetTooReference works almost exactly the same way as Package Reference with the above restrictions in place.
Notable: Tool Restore does not generate Msbuild props and targets, for failures
No no-op artifacts for tool restore.

The core logic is in:
CompatibilityChecker
CompatibilityIssue
RestoreCommand
LockFileUtils
LockFileFormat
LockFileTargetLibrary
ManagedCodeConventions

Most relevant tests:
DotnetToolTests
ContentModelToolsTests

Testing/Validation

Tests Added: Yes
Reason for not adding tests:
Validation done: Manual, by Will from CLI team, automation

PS. This will go in dev when it's shifted to 15.7
//cc @wli3 @rrelyea

@nkolev92 nkolev92 force-pushed the dev-nkolev92-globalTools branch 2 times, most recently from 1e1109e to 5284a9e Compare December 28, 2017 10:18
@nkolev92 nkolev92 force-pushed the dev-nkolev92-globalTools branch from 333cded to 5e4a630 Compare January 3, 2018 20:30
}
}

private bool IsProjectBuildIntegrated(PackageSpec packageSpec)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This wasn't used at all.

@nkolev92 nkolev92 changed the title [WIP] Global tools implementation Dotnet Tool impementation Jan 3, 2018
@nkolev92 nkolev92 force-pushed the dev-nkolev92-globalTools branch from 9fb6b9d to c0a3152 Compare January 5, 2018 22:41
<_RestoreProjectName>$(MSBuildProjectName)</_RestoreProjectName>
<_RestoreProjectName Condition=" '$(RestoreProjectStyle)' == 'PackageReference' AND '$(AssemblyName)' != '' ">$(AssemblyName)</_RestoreProjectName>
<_RestoreProjectName Condition=" '$(RestoreProjectStyle)' == 'PackageReference' AND '$(PackageId)' != '' ">$(PackageId)</_RestoreProjectName>
<_RestoreProjectName Condition=" ('$(RestoreProjectStyle)' == 'PackageReference' OR '$(RestoreProjectStyle)' == 'DotnetToolReference') AND '$(AssemblyName)' != '' ">$(AssemblyName)</_RestoreProjectName>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is used a lot, I suggest storing the value of ('$(RestoreProjectStyle)' == 'PackageReference' OR '$(RestoreProjectStyle)' == 'DotnetToolReference') as a property and re-using it for these conditions. It can be done local to this target.

contentItems.Load(compatibilityData.Files);


if (compatibilityData.TargetLibrary.PackageType.Any(e => e.Equals(PackageType.DotnetTool)))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

.Contains(PackageType.DotnetTool) might be a better fit than Any

new PackageIdentity(node.Key.Name, node.Key.Version));

issues.Add(issue);
await _log.LogAsync(GetErrorMessage(NuGetLogCode.NU1204, issue, graph));//
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

remove comment

}

if (containsDotnetToolPackageType &&
!(HasCompatibleAssets(compatibilityData.TargetLibrary) || !compatibilityData.Files.Any(p => p.StartsWith("tools/", StringComparison.OrdinalIgnoreCase))))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why allow non-tools compatible assets? Shouldn't this only look at the tools folder for a package of that type?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not sure I understand this method, should there be one path for tool projects and another for everything else?


if (ProjectStyle.DotnetToolReference == compatibilityData.PackageSpec.RestoreMetadata?.ProjectStyle)
{
if (compatibilityData.PackageSpec.GetAllPackageDependencies().Any(e => e.Name.Equals(compatibilityData.TargetLibrary.Name)))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Use ignore case here

targetLibrary.Build.Count > 0 || // Build
targetLibrary.BuildMultiTargeting.Count > 0; // Cross targeting build
targetLibrary.BuildMultiTargeting.Count > 0 || // Cross targeting build
targetLibrary.ToolsAssemblies.Count > 0; // Tools assemblies - This makes a package not backwards compatible potentially, but do tools assets matter
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should be separate check, a regular project should not see tool packages as compatible. I am guessing that this wouldn't be populated if it wasn't a tools project, but it would be more clear if there were a different method to check tool assets.

@nkolev92
Copy link
Copy Markdown
Member Author

nkolev92 commented Jan 9, 2018

🔔 @emgarten

// cc
@jainaashish @rohit21agrawal @mishra14

</PropertyGroup>
<!-- Determine the restore output path -->
<PropertyGroup Condition=" '$(RestoreProjectStyle)' == 'PackageReference' OR '$(RestoreProjectStyle)' == 'ProjectJson' ">
<PropertyGroup Condition=" '$(PackageReferenceCompatibleProjectStyle)' == 'true' OR '$(RestoreProjectStyle)' == 'ProjectJson' ">
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

👍

/// DotnetToolReference project
/// </summary>
DotnetCliTool = 3,
DotnetToolReference = 3,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It shouldn't matter since I don't think anyone is using this enum where there would be a mix of NuGet.*.dll versions, but I'd typically expect this to be added at the end and not change the numbers of existing values.

},
pathPatterns: new PatternDefinition[]
{
new PatternDefinition("tools/{tfm}/{rid}/{any}", table: DotnetAnyTable),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Use DefaultTfmAny instead of DotnetAnyTable for both. At this point dotnet is deprecated so it would cause problems to use that for netstandard.

You also want the real Any like build/ uses.

You'll need to update the tests around this also.

[PlatformTheory(Platform.Windows)]
[InlineData("net461")]
[InlineData("netcoreapp1.0")]
public void DotnetToolTests_NoPackageReferenceToolRestore_ThrowsError(string tfm)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🏆 great tests

using NuGet.Packaging.PackageExtraction;
using NuGet.Protocol;
using Xunit;
using NuGet.Packaging.Core;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

sort

RestorePackagesPath="$(RestorePackagesPath)"
RestoreFallbackFolders="$(RestoreFallbackFolders)"
RestoreConfigFile="$(RestoreConfigFile)"
RestoreRootConfigDirectory="$(RestoreRootConfigDirectory)"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

do we wanna support this RestoreRootConfigDirectory via commandLine restore as well?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

What do you mean by commandline?
People can set the MSBuild property if they want to, because we can't really hide it.
It's not called in the normal nuget.exe/dotnet.exe workflows.

{
await logger.LogAsync(LogLevel.Verbose, string.Format(CultureInfo.CurrentCulture, Strings.Log_CheckingCompatibility, graph.Name));
if (!(ProjectStyle.DotnetToolReference == project.RestoreMetadata?.ProjectStyle && string.IsNullOrEmpty(graph.RuntimeIdentifier)))
{ // Don't do compat checks for the ridless graph of DotnetTooReference restore. Everything relevant will be caught in the graph with the rid
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: next line

Copy link
Copy Markdown

@jainaashish jainaashish left a comment

Choose a reason for hiding this comment

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

Looks good to me.

@nkolev92
Copy link
Copy Markdown
Member Author

@emgarten
🔔

I'll do the change for nested folders separately.

Don't want to overextend this PR.

@jainaashish
Copy link
Copy Markdown

Just out of curiosity, how this handles local tool installation?

@nkolev92
Copy link
Copy Markdown
Member Author

@jainaashish

From a NuGet perspective, there is no design/implementation work on global vs local (not even sure what/how they're doing that, it's still a WIP).

There's 2 difference between global and local tools...how the tools can be invoked and where they are installed.

The global tools, are as the name suggests, global, can be invoked from anywhere, so the tool location, which is at a global location, is added to the global path.
Each global tool will be installed in a self contained location, which is controlled by what the global packages folder for the restore will be.

The local tools are just meant to be invokable in a certain context, and they'll be installed to the global packages folder of the user.

So the only thing that changes for us, is the global packages folder, which is a core scenario of ours anyways.

@nkolev92 nkolev92 force-pushed the dev-nkolev92-globalTools branch from 5c3e98e to ade7829 Compare January 11, 2018 21:23
-->
<Target Name="_GetRestoreSettingsAllFrameworks"
Condition=" '$(TargetFrameworks)' != '' AND '$(RestoreProjectStyle)' == 'PackageReference' "
Condition=" '$(TargetFrameworks)' != '' AND ( '$(RestoreProjectStyle)' == 'PackageReference' OR '$(RestoreProjectStyle)' == 'DotnetToolReference' ) "
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why not use PackageReferenceCompatibleProjectStyle here?

-->
<Target Name="_GetRestoreSettingsCurrentProject"
Condition=" '$(TargetFrameworks)' == '' AND '$(RestoreProjectStyle)' == 'PackageReference' "
Condition=" '$(TargetFrameworks)' == '' AND ( '$(RestoreProjectStyle)' == 'PackageReference' OR '$(RestoreProjectStyle)' == 'DotnetToolReference' ) "
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

PackageReferenceCompatibleProjectStyle

Copy link
Copy Markdown
Member Author

@nkolev92 nkolev92 Jan 11, 2018

Choose a reason for hiding this comment

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

The scope of that property is local.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

as long as the target in which it is defined is executed before this one, that property can be used. it's much easier to maintain if there is only one place to add a future PackageReference compatible project style

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Refactored it.

I was more comfortable to just add it to the _GetRestoreProjectStyle returns list.


if (packageSpec.RestoreMetadata?.ProjectStyle == ProjectStyle.DotnetToolReference)
{
if (packageSpec.GetAllPackageDependencies().Count() != 1)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can this return a null list for zero package dependencies?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

No, it'll return an empty set.

graph.Conventions.Patterns.CompileRefAssemblies,
graph.Conventions.Patterns.RuntimeAssemblies,
graph.Conventions.Patterns.ContentFiles
graph.Conventions.Patterns.ContentFiles,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why the , in the end?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Stuff was added, then removed in a later commit, but the , got left over.

group.Properties.TryGetValue(ManagedCodeConventions.PropertyNames.RuntimeIdentifier, out var ridObj);
group.Properties.TryGetValue(ManagedCodeConventions.PropertyNames.TargetFrameworkMoniker, out var tfmObj);

var tfm = tfmObj as NuGetFramework;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

you are assumbing TryGetValue returned the right value for both of these, is that a safe assumption?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

What do you mean by the right value?
Nulls are ok, won't cause any problems, but that'll be caught way earlier.
This is mostly for tests.
We already do the same thing on L269

}

if (containsDotnetToolPackageType &&
!(HasCompatibleToolsAssets(compatibilityData.TargetLibrary) || !compatibilityData.Files.Any(p => p.StartsWith("tools/", StringComparison.OrdinalIgnoreCase))))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

since you do a check for files fairly often, can we abstract out functions that would tell us if there are any lib files or tool files like this:

compatibilityData.HasLibFiles
compatibilityData.HasToolsFiles etc etc.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

HasCompatibleToolsAssetts is a static used once in the compat checker, and once in the lock file builder.
It can't be bound to the CompatibilityData model.

!compatibilityData.Files.Any(p =>
p.StartsWith("ref/", StringComparison.OrdinalIgnoreCase)
|| p.StartsWith("lib/", StringComparison.OrdinalIgnoreCase)); // No assemblies at all (for any TxM)
|| p.StartsWith("lib/", StringComparison.OrdinalIgnoreCase)); // No assemblies at all (for any TxM)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'd rather not refactor code that doesn't need changing.
Same as above, can't tie to the model.

{
if (compatibilityData.PackageSpec.GetAllPackageDependencies().Any(e => e.Name.Equals(compatibilityData.TargetLibrary.Name, StringComparison.OrdinalIgnoreCase)))
{
if (!containsDotnetToolPackageType)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

change this to :
if(!containsDotnetToolPackageType && compatibilityData.PackageSpec.GetAllPackageDependencies().Any(e => e.Name.Equals(compatibilityData.TargetLibrary.Name, StringComparison.OrdinalIgnoreCase)))

this way it will not have to evaluate the more expensive check if containsDotnetToolPackageType is false.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch. Not sure how I missed that simplification.

@nkolev92 nkolev92 force-pushed the dev-nkolev92-globalTools branch from 6a581f6 to 90dbfd5 Compare January 17, 2018 10:57
Copy link
Copy Markdown
Member

@emgarten emgarten left a comment

Choose a reason for hiding this comment

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

Minor comments. This looks very complete and with lots of tests great! 👏

<comment>{0} is the certificate store name</comment>
</data>
<data name="Error_ProjectWithIncorrectDependenciesCount" xml:space="preserve">
<value>Project {0} must have exactly {1} package reference(s).</value>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: this could just say a single package reference, I'm not sure that writing in the number is needed.

<value>Invalid project-package combination for {0} {1}. DotnetToolReference project style can only contain references of the DotnetTool type</value>
</data>
<data name="Error_ToolsPackageWithExtraPackageTypes" xml:space="preserve">
<value>Invalid tools package {0} {1}. Tools packages cannot contain more than 1 PackageType.</value>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: as a general rule numbers smaller than 10 should be written out as the word, not as the numeral. So: more than one PackageType here


var res = StringComparer.OrdinalIgnoreCase.Compare(Name, other.Name);

if(res != 0)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: spacing

return res;
}

return Version.CompareTo(other.Version);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks like Version could be null here and throw. Maybe the parser prevents this, but this class seems to allow it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

There's a null version check, so version can not be null.

}
try
{
File.Delete(targetFilePath);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: format the file


// Assert
Assert.Equal(1, groups.Count);
Assert.Equal(NuGetFramework.AnyFramework, (NuGetFramework)groups.First().Properties["tfm"]);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🥇 good test

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.

4 participants