Skip to content

Commit

Permalink
Implemented Plugin Restore:
Browse files Browse the repository at this point in the history
- Output MSBuild Tasks assembly from Sdk project
- Implement PluginInfoType+manifest generation as MSBuild Tasks
- Implement MSBuild Tasks for restoring+resolving ThunderDependency plugins
- Update README with documentation on ThunderDependency configuration
- Sdk: Don't generate .deps.json file when building a project
- Sdk: expand configuration of plugin related props
- Sdk: support r2modman (fix #1)
- Sdk: improve logging/warnings
- Sdk: validate generated manifest.json values
- Sdk: support disabling release optimizations
- Sdk: rename PluginInfoTypeAccessModifier -> PluginInfoTypeModifiers
  • Loading branch information
Cryptoc1 committed Jan 4, 2024
1 parent 0927dbd commit 4b1d1ef
Show file tree
Hide file tree
Showing 22 changed files with 1,718 additions and 71 deletions.
38 changes: 30 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,19 @@ An [MSBuild Sdk](https://learn.microsoft.com/en-us/visualstudio/msbuild/how-to-u

## Usage

### Requirements
- .NET 8.0
- VSCode/VS2022
- Thunderstore

To start using the Sdk, create a new Class Library:
```bash
$ dotnet new classlib -n {NAME}
```

In the new `.csproj`, update the `Sdk="Microsoft.NET.Sdk"` attribute at the top of the file to `Sdk="LethalCompany.Plugin.Sdk/{VERSION}"`, and replace any existing content with metadata about the plugin:
In the new `.csproj`, update the `Sdk="Microsoft.NET.Sdk"` attribute at the top of the file to `Sdk="LethalCompany.Plugin.Sdk/{LATEST-VERSION}"`, and replace any existing content with metadata about the plugin:
```xml
<Project Sdk="LethalCompany.Plugin.Sdk/1.0.0">
<Project Sdk="LethalCompany.Plugin.Sdk/1.1.0">

<PropertyGroup>
<Title>Plugin Example</Title>
Expand All @@ -48,14 +53,14 @@ public sealed class SamplePlugin : BaseUnityPlugin
>
> _The name of the generated class can be changed using the `<PluginInfoTypeName />` MSBuild property._
>
> _By default, the generated class is `internal static`, this can be changed using the `<PluginInfoTypeAccessModifier />` MSBuild property._
> _By default, the generated class is `internal static`, this can be changed using the `<PluginInfoTypeModifiers />` MSBuild property._

### Publish to Thunderstore

> _In order to create a Thunderstore Package, the Sdk requires that `icon.png` and `README.md` files exist at the project root._
> _The location of the `CHANGELOG.md` and `README.md` files can be customized using the `<PluginChangelogFile />` and `<PluginReadMeFile />` MSBuild properties._
> _The location of the `CHANGELOG.md` and `README.md` files can be customized using the `<PluginChangeLogFile />` and `<PluginReadMeFile />` MSBuild properties._
In the `.csproj` of the plugin, provide the metadata used to generate a `manifest.json` for publishing:
```xml
Expand All @@ -71,7 +76,7 @@ In the `.csproj` of the plugin, provide the metadata used to generate a `manifes
</PropertyGroup>

<ItemGroup>
<ThunderDependency Include="ExampleTeam-OtherPlugin-1.0.0" />
<ThunderDependency Include="ExampleTeam-OtherPlugin" Version="1.0.0" />
</ItemGroup>

</Project>
Expand Down Expand Up @@ -111,11 +116,28 @@ dotnet publish -p:PluginStagingProfile="..."
#### Specify Thunderstore Dependencies

To specify Thunderstore dependencies in the generated `manifest.json`, use the `ThunderDependency` item:
To specify a dependency on another Thunderstore plugin, use the `ThunderDependency` item:
```xml
<ItemGroup>
<ThunderDependency Include="ExampleTeam-ExamplePlugin" Version="1.0.0" />
</ItemGroup>
```

##### Configure Referenced Assemblies

When a `ThunderDependency` is specified, the Sdk will restore & resolve assemblies for the dependency.

Assembly resolution can be configured by specifying glob patterns for the `ExcludeAssets`/`IncludeAssets` metadata:

```xml
<ItemGroup>
<ThunderDependency Include="ExampleTeam-ExamplePlugin-1.0.0" />
<ThunderDependency Include="ExampleTeam-ExamplePlugin" Version="1.0.0">
<ExludeAssets>path-to-ignore\*.dll</ExcludeAssets>
</ThunderDependency>
</ItemGroup>
```

> _The Sdk specifies a default `ThunderDependency` on `BepInExPack`, specifying one yourself is unnecessary._
> _The Sdk specifies a default `ThunderDependency` on `BepInExPack`, specifying one yourself is unnecessary._
> _When publishing a plugin, the Sdk will use the specified `ThunderDependency` items to produce a value for the `dependencies` key of the generated `manifest.json`._
4 changes: 2 additions & 2 deletions lc-plugin-sdk.sln
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "src", "src\LethalCompany.Plugin.Sdk.csproj", "{85FB813D-1364-46E0-BA26-CA2FC2C273D4}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LethalCompany.Plugin.Sdk", "src\LethalCompany.Plugin.Sdk.csproj", "{85FB813D-1364-46E0-BA26-CA2FC2C273D4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "test", "test\LethalCompany.SdkSample.csproj", "{3B3EAC57-F36B-47B2-9B90-CE0D6ED5B638}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LethalCompany.SdkSample", "test\LethalCompany.SdkSample.csproj", "{3B3EAC57-F36B-47B2-9B90-CE0D6ED5B638}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down
49 changes: 49 additions & 0 deletions src/GeneratePluginInfoCode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Microsoft.Build.Framework;

namespace LethalCompany.Plugin.Sdk;

/// <summary> Generates C# code for a type containing constant values of PluginInfo passed in from MSBuild Properties. </summary>
public sealed class GeneratePluginInfoCode : Microsoft.Build.Utilities.Task
{
/// <summary> The code that was generated. </summary>
[Output]
public string GeneratedText { get; set; } = string.Empty;

/// <summary> The plugin's identifier to include in the generated code. </summary>
[Required]
public string Identifier { get; init; } = string.Empty;

/// <summary> The plugin's name to include in the generated code. </summary>
[Required]
public string Name { get; init; } = string.Empty;

/// <summary> The namespace to generated code in. </summary>
[Required]
public string Namespace { get; init; } = string.Empty;

/// <summary> The access modifiers to be used when generating the PluginInfo type. </summary>
public string TypeModifiers { get; init; } = "internal static";

/// <summary> The name of the PluginInfo type to generate. </summary>
public string TypeName { get; init; } = "GeneratedPluginInfo";

/// <summary> The plugin's version to include in the generated code. </summary>
[Required]
public string Version { get; init; } = string.Empty;

/// <inheritdoc/>
public override bool Execute()
{
GeneratedText = $@"// <auto-generated />
namespace {Namespace};
[System.Runtime.CompilerServices.CompilerGenerated]
{TypeModifiers} class {TypeName}
{{
public const string Identifier = ""{Identifier}"";
public const string Name = ""{Name}"";
public const string Version = ""{Version}"";
}}";
return true;
}
}
56 changes: 56 additions & 0 deletions src/GeneratePluginManifestJson.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System.Text.Json;
using LethalCompany.Plugin.Sdk.Internal;
using Microsoft.Build.Framework;

namespace LethalCompany.Plugin.Sdk;

/// <summary> Generates the JSON text of a Thunderstore manifest. </summary>
public sealed class GeneratePluginManifestJson : Microsoft.Build.Utilities.Task
{
/// <summary> The <c>@(ThunderDependency)</c> items to write to the generated json. </summary>
[Required]
public ITaskItem[] Dependencies { get; init; } = [];

/// <summary> The <c>description</c> to write to the generated json. </summary>
[Required]
public string Description { get; init; } = string.Empty;

/// <summary> The json text that was generated. </summary>
[Output]
public string GeneratedText { get; set; } = string.Empty;

/// <summary> The <c>name</c> to write to the generated json. </summary>
[Required]
public string Name { get; init; } = string.Empty;

/// <summary> The <c>version_number</c> to write to the generated json. </summary>
[Required]
public string Version { get; init; } = string.Empty;

/// <summary> The <c>website_url</c> to write to the generated json. </summary>
public string WebsiteUrl { get; init; } = string.Empty;

/// <inheritdoc/>
public override bool Execute()
{
var manifest = new PluginManifest
{
Dependencies = [.. Dependencies.Select(ThunderDependencyMoniker.From)],
Description = Description,
Name = Name,
Version = SemanticVersion.Parse(Version),
WebsiteUrl = WebsiteUrl,
};

if (!manifest.TryValidate(out var errors))
{
foreach (var error in errors)
{
Log.LogWarning($"Thunderstore Manifest Validation: {error.ErrorMessage}");
}
}

GeneratedText = JsonSerializer.Serialize(manifest, ThunderstoreJsonContext.Default.PluginManifest);
return true;
}
}
31 changes: 31 additions & 0 deletions src/Internal/EnumerableExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace LethalCompany.Plugin.Sdk.Internal;

internal static class EnumerableExtensions
{
public static IEnumerable<TResult> FullJoin<TOuter, TInner, TKey, TResult>(
this IEnumerable<TOuter> outer,
IEnumerable<TInner> inner,
Func<TOuter, TKey> outerKeySelector,
Func<TInner, TKey> innerKeySelector,
Func<TOuter?, TInner?, TResult> resultSelector)
{
var outerLookup = outer.ToLookup(outerKeySelector);
var innerLookup = inner.ToLookup(innerKeySelector);

var keys = new HashSet<TKey>(
outerLookup.Select(group => group.Key)
.Concat(
innerLookup.Select(group => group.Key)));

foreach (var key in keys)
{
foreach (var outerItem in outerLookup[key].DefaultIfEmpty())
{
foreach (var innerItem in innerLookup[key].DefaultIfEmpty())
{
yield return resultSelector(outerItem, innerItem);
}
}
}
}
}
62 changes: 62 additions & 0 deletions src/Internal/PluginAssetsCollection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace LethalCompany.Plugin.Sdk.Internal;

[JsonConverter(typeof(PluginAssetsCollectionJsonConverter))]
internal sealed class PluginAssetsCollection(Dictionary<ThunderDependencyMoniker, string[]>? source = null) : IDictionary<ThunderDependencyMoniker, string[]>
{
private readonly IDictionary<ThunderDependencyMoniker, string[]> source = source ?? [];

public string[] this[ThunderDependencyMoniker key] { get => source[key]; set => source[key] = value; }

public ICollection<ThunderDependencyMoniker> Keys => source.Keys;
public ICollection<string[]> Values => source.Values;
public int Count => source.Count;
public bool IsReadOnly => source.IsReadOnly;

public void Add(ThunderDependencyMoniker key, string[] value) => source.Add(key, value);

public void Add(KeyValuePair<ThunderDependencyMoniker, string[]> item) => source.Add(item);

public void Clear() => source.Clear();

public bool Contains(KeyValuePair<ThunderDependencyMoniker, string[]> item) => source.Contains(item);

public bool ContainsKey(ThunderDependencyMoniker key) => source.ContainsKey(key);

public void CopyTo(KeyValuePair<ThunderDependencyMoniker, string[]>[] array, int arrayIndex) => source.CopyTo(array, arrayIndex);

public IEnumerator<KeyValuePair<ThunderDependencyMoniker, string[]>> GetEnumerator() => source.GetEnumerator();

public bool Remove(ThunderDependencyMoniker key) => source.Remove(key);

public bool Remove(KeyValuePair<ThunderDependencyMoniker, string[]> item) => source.Remove(item);

#pragma warning disable CS8767
public bool TryGetValue(ThunderDependencyMoniker key, [MaybeNullWhen(false)] out string[] value) => source.TryGetValue(key, out value);
#pragma warning restore CS8767

IEnumerator IEnumerable.GetEnumerator() => source.GetEnumerator();
}

internal sealed class PluginAssetsCollectionJsonConverter : JsonConverter<PluginAssetsCollection>
{
public override PluginAssetsCollection? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var assets = JsonSerializer.Deserialize<Dictionary<string, string[]>>(ref reader, options);
return new(
assets?.ToDictionary(asset => ThunderDependencyMoniker.Parse(asset.Key), asset => asset.Value));
}

public override void Write(Utf8JsonWriter writer, PluginAssetsCollection value, JsonSerializerOptions options)
{
var assets = value.ToDictionary(
asset => asset.Key.Value,
asset => asset.Value);

JsonSerializer.Serialize(writer, assets, options);
}
}
Loading

0 comments on commit 4b1d1ef

Please sign in to comment.