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

[Feature] Implement Support for NuGet Authentication Plugins as .NET Tools #6138

Merged
merged 4 commits into from
Nov 7, 2024

Conversation

Nigusu-Allehu
Copy link
Contributor

@Nigusu-Allehu Nigusu-Allehu commented Nov 6, 2024

Bug

Fixes: NuGet/Home#12567
Fixes: NuGet/Home#13742

Description

This PR implements support for deploying and discovering NuGet authentication plugins as .NET tools, as outlined in the design. With this change, plugin authors can package their tools as global .NET tools by naming them with the prefix nuget-plugin-*, enabling NuGet to discover and execute them by scanning the PATH. On Windows, .exe and .bat extensions are recognized, while Linux/Mac platforms identify plugins via the executable bit.

This is a feature branch PR.
Here are the individual commits into the feature branch
#5990
#6113

PR Checklist

  • Meaningful title, helpful description and a linked NuGet/Home issue
  • Added tests
  • Link to an issue or pull request to update docs if this PR changes settings, environment variables, new feature, etc. Document .net tools Plugin feature Home#13858

@Nigusu-Allehu Nigusu-Allehu requested a review from a team as a code owner November 6, 2024 20:36
@Nigusu-Allehu
Copy link
Contributor Author

I will address the two comments regarding improving PluginDiscovererTests in #5990, in a follow up PR

@Nigusu-Allehu Nigusu-Allehu changed the title Implement Support for NuGet Authentication Plugins as .NET Tools [Feature] Implement Support for NuGet Authentication Plugins as .NET Tools Nov 6, 2024
@Nigusu-Allehu Nigusu-Allehu self-assigned this Nov 6, 2024
jeffkl
jeffkl previously approved these changes Nov 6, 2024
if (Directory.Exists(directoryPath))
{
var directoryInfo = new DirectoryInfo(directoryPath);
var files = directoryInfo.GetFiles("nuget-plugin-*");
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe the case sensitivity of this match will be platform-dependent, meaning on Windows it will be commonly (not guaranteed) case insensitive, but on Linux it will be case sensitive.

However, the file name comparison here is always case-insensitive.

Can you explain your reasoning?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are right, that's inconsistent. I will address this in a follow up PR.

using (var process = new System.Diagnostics.Process())
{
// Use a shell command to check if the file is executable
process.StartInfo.FileName = "/bin/bash";
Copy link
Contributor

Choose a reason for hiding this comment

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

This code could run on Alpine Linux, where /bin/bash does not exist by default.

Is there an even more portable way of checking? I don't know, but maybe ls -l <path>?

Copy link
Contributor

Choose a reason for hiding this comment

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

On unix systems, FileInfo (and DirectoryInfo) has a UnixFileMode property that is a flags enum with the typical unix properties. You can use HasFlag or your favorite enum comparisons to see if a given file has any executable flags:

$ dotnet fsi

Microsoft (R) F# Interactive version 12.8.102.0 for F# 8.0
Copyright (c) Microsoft Corporation. All Rights Reserved.

For help type #help;;

> open System.IO;;
> let f = new FileInfo("/usr/bin/curl");;
val f: FileInfo = /usr/bin/curl

> f.UnixFileMode;;
val it: UnixFileMode =
  OtherExecute, OtherRead, GroupExecute, GroupRead, UserExecute, UserWrite, UserRead

Copy link
Member

Choose a reason for hiding this comment

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

This code is in a #if !NET8_0_OR_GREATER code block because the APIs that @baronfel mentioned are not available before .NET 8. When @Nigusu-Allehu created the original PR to add this code to the feature branch (this PR is merging the feature branch into dev) I asked him to use the .NET 8 APIs already, which is being done on line 303.

I also suggested to @Nigusu-Allehu that we could consider skipping this PATH scanning for plugins for .NET Framework on Linux and Mac, since that only means Mono, which we don't officially support, but I don't remember any replies to that comment, and clearly it wasn't actioned either.

But I think it's also ok to keep here, because I really can't imagine anyone using Alpine wanting to install Mono and use NuGet.exe. And even if they do, the whole thing is wrapped in a try-catch block. The plugin won't be discovered, but it won't break restore either (apart from not being able to authenticate if a nuget-plugin-* app is installed)

Comment on lines +215 to +217
IPlugin plugin;

plugin = await _pluginFactory.GetOrCreateAsync(
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
IPlugin plugin;
plugin = await _pluginFactory.GetOrCreateAsync(
IPlugin plugin = await _pluginFactory.GetOrCreateAsync(

var reqHandler = new RequestHandlers();
var options = ConnectionOptions.CreateDefault();

var pluginFactory = new PluginFactory(Timeout.InfiniteTimeSpan);
Copy link
Contributor

Choose a reason for hiding this comment

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

This is disposable and should be disposed.

@@ -66,9 +66,21 @@ internal TestExpectation(

internal sealed class PluginManagerMock : IDisposable
{
public static bool IsDesktop
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 all of these IsDesktop properties would be better off named RequiresDotNetHost, since that's how it's used. IsDesktop is a little more difficult to understand than RequiresDotNetHost. It would simplify call sites too. Instead of requiresDotnetHost: !IsDesktop, you could simply pass RequiresDotNetHost.

Your call though.

Comment on lines +30 to +37
Exception exception = Record.Exception(() =>
{
}
using (new PluginDiscoverer())
{
}
});

Assert.Null(exception);
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 simply?

   using (new PluginDiscoverer())
   {
   }

An exception would fail the test.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@@ -11,20 +11,20 @@ namespace NuGet.Protocol.Plugins
/// <summary>
/// A plugin factory.
/// </summary>
public interface IPluginFactory : IDisposable
internal interface IPluginFactory : IDisposable
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you explain this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We had to update the API for IPluginFactory so that it is able to create plugins for dotnet tools plugins. Since we needed to break the API, I used it as an opportunity to un-expose the interface. The reason for making it internal is that the interface only has one implementation and there is no need for it to be a public API #6113 (comment)

@Nigusu-Allehu
Copy link
Contributor Author

Thank you for the review @dtivel! I will address the comments in a follow up PR.

@Nigusu-Allehu Nigusu-Allehu merged commit 4f24d6d into dev Nov 7, 2024
24 checks passed
@Nigusu-Allehu Nigusu-Allehu deleted the dev-feature-dot-net-tool-plugin-support branch November 7, 2024 22:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
6 participants