Skip to content

Commit

Permalink
Added support for PluginAPI #564
Browse files Browse the repository at this point in the history
  • Loading branch information
igor84 committed Oct 24, 2023
1 parent 5a1e296 commit 91dcd51
Show file tree
Hide file tree
Showing 54 changed files with 1,781 additions and 193 deletions.
2 changes: 1 addition & 1 deletion .github/actions/create-dll/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ runs:
source ./dotnet-install.sh &&
rm ./dotnet-install.sh &&
dotnet build ./src/NuGetForUnity.CreateDll/NuGetForUnity.CreateDll.csproj --nologo -p:AppxBundle=Always -p:Platform='Any CPU' --configuration Release
-p:ReferencePath=$UNITY_PATH/Editor/Data/Managed -p:Version=${{ inputs.version }}
-p:ReferencePath=$UNITY_PATH/Editor/Data/Managed -p:Version=${{ inputs.version }} -p:SolutionDir=./
7 changes: 5 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,14 @@ jobs:
with:
version: ${{ needs.determineVersionNumber.outputs.version }}

- name: Upload NuGetForUnity.dll
- name: Upload NuGetForUnity dlls
uses: actions/upload-artifact@v3
with:
name: NuGetForUnity.dll
path: ./src/NuGetForUnity.CreateDll/bin/Release/NugetForUnity.dll
path: |
./src/NuGetForUnity.CreateDll/bin/Release/NugetForUnity.dll
./src/NuGetForUnity.CreateDll/bin/Release/NugetForUnity.PluginAPI.dll
./src/NuGetForUnity.CreateDll/bin/Release/NugetForUnity.PluginAPI.xml
if-no-files-found: error

packageOnLinux:
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,14 @@ For more information see [.Net Tool Documentation](https://learn.microsoft.com/e

Restore nuget packages of a single Unity Project: `dotnet nugetforunity restore <PROJECT_PATH>`. If installed as a global tool it can be called without the `dotnet` prefix: `nugetforunity restore <PROJECT_PATH>`.

# Plugin support

NugetForUnity has plugin support. If you open the NugetForUnity section in Unity preferences it will list the plugins you have installed in your project and you can enable them from there.

Plugins are any dlls which contain NugetForUnityPlugin in their name and have a class inside them that implements `INugetPlugin` interface. They can be placed anywhere inside the project and even installed as a nuget package.

If you are interested in implementing a plugin read the [plugin development documentation](plugin-dev-readme.md).

# Common issues when installing NuGet packages

In the .Net ecosystem Unity is relatively special as it doesn't use the standard .Net runtime from Microsoft instead, it uses a fork of the [Mono](https://docs.unity3d.com/Manual/Mono.html) runtime. For some platforms Unity even uses [IL2CPP](https://docs.unity3d.com/Manual/IL2CPP.html) that compiles all C# code to C++ code. So Unity also uses a different build-system. This can lead to some issues with NuGet packages that heavily depend on the standard .Net build-system. NuGetForUnity tries to handle most of the changes needed to allow using NuGet packages inside Unity but it is not able to resolve all issues. This section contains some common issues and potential solutions.
Expand Down
97 changes: 97 additions & 0 deletions plugin-dev-readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Plugin Development

## Introduction

In order to develop a NugetForUnity Plugin you need to start with these steps:

1. Decide on your plugin name. It could just be your company's name for example.
2. You plugin project name should be <PluginName>.NugetForUnityPlugin since the plugin loader will look for dlls which contain NugetForUnityPlugin in its name.
3. If you are targeting Unity older than 2021.3 create a .netstandard2.0 C# library project.
4. If you are targeting Unity 2021.3 or newer create a .netstandard2.1 C# library project.
5. NugetForUnity contains NugetForUnity.PluginAPI.dll that you need to add as a reference in your project. You can copy it to your project or reference using a relative path from some other place.
6. Depending on the needs of your plugin you might also need to add references to UnityEngine.dll and UnityEditor.dll from your Unity installation.
7. Write a class that implements `INugetPlugin` interface. In the `Register` method you will get a `INugetPluginRegistry` that has methods you can use to register your classes that implement custom handling of certain functionalities like installing and uninstalling the packages.

Note that `INugetPluginRegistry` provides you a few things you can use in your plugin:

- `IsRunningInUnity` property will be true if the plugin is being run from Unity and false if it is run from command line.
- `PluginService` property that you can pass to your custom handlers if they need to use any of these:
- `ProjectAssetsDir` property that gives you the absolute path to the project's Assets directory.
- `LogError`, `LogErrorFormat` and `LogVerbose` methods that you can use for logging. You should not use `UnityEngine.Debug.Log` methods since they will not work if plugin is used from command line.

## Extension points

NugetForUnity implements a certain extension points that your plugin can register to in order to provide custom processing. It can add new extension points in the future without breaking backward compatibility with existing ones.

### Custom action buttons in Nuget window

If you want to provide a custom action button next to some packages in NugetForUnity Manage Packages window you can write a class that implements `IPackageButtonsHandler` interface. It will give you a method bellow that you need to implement:

```cs
void DrawButtons(INugetPackage package, INugetPackage? installedPackage, bool existsInUnity);
```

Inside this method you will get info about an online `package` that is being rendered. The `installedPackage` is that same info about the version of that package that is installed if it is installed. It will be null if the package is not currently installed in the project. The third parameter tells you if this package is actually included in Unity itself which means installing it should be disabled.

Inside the method you can use `GUILayout.Button` Unity method to render additional buttons that will be rendered to the left of current Install/Uninstall/Update and similar buttons.

Since code here will use UnityEditor functionality which is not available when NugetForUnity is run from command line you should only register this class in plugin registry if it ruinning from Unity. You can do so in your `INugetPlugin.Register` implementation like this:

```cs
if (registry.IsRunningInUnity)
{
var myButtonHandler = new MyButtonHandler(registry.PluginService);
registry.RegisterPackageButtonDrawer(linkUnlinkSourceButton);
}
```

### Custom package installation

If you want to customize how packages are installed or just how certain files from packages are extracted and handled you can write a class that implements `IPackageInstallFileHandler` interface. It declares this method:

```cs
bool HandleFileExtraction(INugetPackage package, ZipArchiveEntry entry, string extractDirectory);`
```

When you implement that method you can choose if you want to handle each specific entry from nupkg file and how. If you handle the entry your self and you do not want the default installation of that file to occur you should return true from this method indicating that you have done all the processing you need for this entry. If you still want default installation logic to handle this entry just return false from this method.

### Custom handling of uninstallation

If you implement custom handling of installation you will often also need to implement custom handling of uninstallation. For that you need to write a class that implements `IPackageUninstallHandler` interface. It declares two methods:

```cs
void HandleUninstall(INugetPackage package, PackageUninstallReason uninstallReason);
void HandleUninstalledAll();
```

The first method is called for each package that is being uninstalled. The `uninstallReason` can be:

- `IndividualUninstall` when individual package uninstallation has be requested by the user.
- `UninstallAll` when user requested all packages from the project to be uninstalled.
- `IndividualUpdate` when user requested a package to be updated so we are uninstalling the current version.
- `UpdateAll` when user requested all packages to be updated so we are uninstalling old versions.

The second method, `HandleUninstalledAll()` will only be called if user requested all packages to be unininstalled after all the default uninstall processing has been done. If you don't need to do anything special in this case you can leave this method empty.

## New extension points

In case you have an idea for a plugin that requires some new extension points please open an issue requesting it with a description of how are you planing to use it. Pull requests implementing new extension points are also welcome as long as a clear description for their need is given.

# Plugin support implementation details

This section explains how plugin support is implemented in NugetForUnity which should also explain how new extension points can be added.

Under src/NugetForUnity.CreateDll there is a NuGetForUnity.CreateDll.sln solution. That solution contains two projects: NugetForUnity.CreateDll itself and NugetForUnity.PluginAPI project. PluginAPI project defines all the interfaces that should be visible to plugin implementations. NugetForUnity project references this one since it also implements and extends some of these interfaces.

PluginAPI project is setup so that it copies the built NugetForUnity.PluginAPI.dll to src/NuGetForUnity/Editor/ folder where the rest of actual source files of NugetForUnity reside. This is needed because src/NugetForUnity folder contains package.json file identifying that folder as a Unity package that can be locally referenced from the file system.

CreateDll project has two classes under PluginSupport folder:

- `NugetPluginSupport` which implements the `INugetPluginService`
- `PluginRegistry` that implements `INugetPluginRegistry` and also has `InitPlugins` method that is called after Nuget.config is loaded and a list of enabled plugins is read from it.

Note that `AssemblyLoader` class it uses to load the plugins has a different implementation in NugetForUnity.Cli project which is for running from command line. It also has a different implementation of `SessionStorage` class that will return "false" for `IsRunningInUnity` key.

`NugetPreferences` constructor has code that looks for all plugins installed in the project by checking all assemblies whose name contains "NugetForUnityPlugin" in its name. It will list these plugins in the preferences window so each can be enables or disabled.

In order to find where are extension points executed in the code you can just search for `PluginRegistry.Instance` through the entire solution. For example you will find that `PluginRegistry.Instance.HandleFileExtraction(...)` is called in `NugetPackageInstaller.Install()` method within the loop that handles the entries for nupgk file that is being installed.
25 changes: 25 additions & 0 deletions src/NuGetForUnity.Cli/Fakes/AssemblyLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#nullable enable
using System.Reflection;
using NuGetForUnity.Cli;
using NugetForUnity.Configuration;

namespace NugetForUnity.Helper
{
/// <summary>
/// Assembly load implementation for CLI version. There is another implementation for Unity Editor version.
/// </summary>
internal static class AssemblyLoader
{
/// <summary>
/// Loads the assembly for the given pluginId
/// </summary>
/// <param name="pluginId">Plugin Id to load.</param>
/// <returns>Assembly of the loaded plugin.</returns>
internal static Assembly Load(NugetForUnityPluginId pluginId)
{
var loadContext = new NugetAssemblyLoadContext(pluginId.Path);
var assembly = loadContext.LoadFromAssemblyPath(pluginId.Path);
return assembly;
}
}
}
2 changes: 1 addition & 1 deletion src/NuGetForUnity.Cli/Fakes/SessionState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ internal static class SessionState
{
internal static string GetString(string key, string defaultValue)
{
return null;
return key == "IsRunningInUnity" ? "false" : null;
}

internal static void SetString(string key, object value)
Expand Down
6 changes: 6 additions & 0 deletions src/NuGetForUnity.Cli/NuGetForUnity.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<ItemGroup>
<Compile Remove="..\NuGetForUnity\Editor\Ui\*.cs" />
<Compile Remove="..\NuGetForUnity\Editor\NugetAssetPostprocessor.cs" />
<Compile Remove="..\NuGetForUnity\Editor\Helper\AssemblyLoader.cs"/>
<Compile Remove="..\NuGetForUnity\Editor\Helper\NugetPackageTextureHelper.cs" />
<Compile Remove="..\NuGetForUnity\Editor\OnLoadNugetPackageRestorer.cs" />
<Compile Remove="..\NuGetForUnity\Editor\UnityPreImportedLibraryResolver.cs" />
Expand All @@ -36,4 +37,9 @@
<PackageReference Include="JetBrains.Annotations" Version="2023.2.0" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="7.0.1" />
</ItemGroup>
<ItemGroup>
<Reference Include="NugetForUnity.PluginAPI">
<HintPath>..\NuGetForUnity\Editor\NugetForUnity.PluginAPI.dll</HintPath>
</Reference>
</ItemGroup>
</Project>
29 changes: 29 additions & 0 deletions src/NuGetForUnity.Cli/NugetAssemblyLoadContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Reflection;
using System.Runtime.Loader;

namespace NuGetForUnity.Cli
{
/// <summary>
/// Class for loading an assembly.
/// </summary>
internal sealed class NugetAssemblyLoadContext : AssemblyLoadContext
{
private readonly AssemblyDependencyResolver resolver;

/// <summary>
/// Initializes a new instance of the <see cref="NugetAssemblyLoadContext"/> class.
/// </summary>
/// <param name="pluginPath">Path to the plugin that will be loaded.</param>
internal NugetAssemblyLoadContext(string pluginPath)
{
resolver = new AssemblyDependencyResolver(pluginPath);
}

/// <inheritdoc/>
protected override Assembly Load(AssemblyName assemblyName)
{
var assemblyPath = resolver.ResolveAssemblyToPath(assemblyName);
return assemblyPath != null ? LoadFromAssemblyPath(assemblyPath) : null;
}
}
}
3 changes: 3 additions & 0 deletions src/NuGetForUnity.CreateDll/NuGetForUnity.CreateDll.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,7 @@
</Compile>
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="NugetForUnity.PluginAPI\NugetForUnity.PluginAPI.csproj" />
</ItemGroup>
</Project>
6 changes: 6 additions & 0 deletions src/NuGetForUnity.CreateDll/NuGetForUnity.CreateDll.sln
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ VisualStudioVersion = 12.0.31101.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGetForUnity.CreateDll", "NuGetForUnity.CreateDll.csproj", "{3FB0B522-A1E7-4FC6-8083-885621A5B4B0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NugetForUnity.PluginAPI", "NugetForUnity.PluginAPI\NugetForUnity.PluginAPI.csproj", "{67591B40-D208-4BCF-9D82-7B6138F6355F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -15,6 +17,10 @@ Global
{3FB0B522-A1E7-4FC6-8083-885621A5B4B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3FB0B522-A1E7-4FC6-8083-885621A5B4B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3FB0B522-A1E7-4FC6-8083-885621A5B4B0}.Release|Any CPU.Build.0 = Release|Any CPU
{67591B40-D208-4BCF-9D82-7B6138F6355F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{67591B40-D208-4BCF-9D82-7B6138F6355F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{67591B40-D208-4BCF-9D82-7B6138F6355F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{67591B40-D208-4BCF-9D82-7B6138F6355F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Nupkg/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
Binary file not shown.
Loading

0 comments on commit 91dcd51

Please sign in to comment.