Skip to content

Commit

Permalink
Organize Sdk targets:
Browse files Browse the repository at this point in the history
- organize targets into seperate files
- support restore plugins via Directory.Solution.targets
- set `PublishRelease` msbuild prop by default
  • Loading branch information
Cryptoc1 committed Jan 5, 2024
1 parent 0cfca2f commit 3310ae9
Show file tree
Hide file tree
Showing 9 changed files with 277 additions and 214 deletions.
65 changes: 49 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,29 @@ An [MSBuild Sdk](https://learn.microsoft.com/en-us/visualstudio/msbuild/how-to-u
- Optimizes Build Defaults
- Enables Modern Language Features with [`PolySharp`](https://github.com/Sergio0694/PolySharp)
- References Publicized Binaries from [`LethalAPI.GameLibs`](https://github.com/dhkatz/LethalAPI.GameLibs)
- References BepInEx packages from the [BepInEx Registry](https://nuget.bepinex.dev/)
- Restores & Resolves References to Thunderstore Dependencies
- Creates Thunderstore Packages with `dotnet publish`
- Stages plugins to local a Thunderstore profile
- Stages Directly to a Thunderstore/r2modman Profile
- And More...


## Usage

### Requirements
### Prerequisites
- MSBuild 17.8.3+
- .NET 8.0+
- VSCode/VS2022+
- Thunderstore/r2modman

To start using the Sdk, create a new Class Library:
## Getting Started

### Create a Class Library

Create a new Class Library for the plugin:
```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/{LATEST-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/{VERSION}"`, and replace any existing content with metadata about the plugin:
```xml
<Project Sdk="LethalCompany.Plugin.Sdk/...">
<Project Sdk="LethalCompany.Plugin.Sdk/{VERSION}">

<PropertyGroup>
<Title>Plugin Example</Title>
Expand All @@ -42,7 +43,27 @@ In the new `.csproj`, update the `Sdk="Microsoft.NET.Sdk"` attribute at the top
</Project>
```

Add a new `.cs` file, and define the plugin:
***Or***, create a `global.json` file in the root of your solution, and specify the `LethalCompany.Plugin.Sdk`:
```json
{
"msbuild-sdk": {
"LethalCompany.Plugin.Sdk": "{VERSION}"
}
}
```

In your project file, update the `Sdk` attribute:
```xml
<Project Sdk="LethalCompany.Plugin.Sdk">
<!-- ... -->
</Project>
```

> _For more information on using MSBuild Sdks, see ["How project SDKs are resolved"](https://learn.microsoft.com/en-us/visualstudio/msbuild/how-to-use-project-sdk?view=vs-2022#how-project-sdks-are-resolved)_
### Define Plugin

In your project, add a new `.cs` file to define the `BepInEx` plugin:
```csharp
[BepInPlugin(GeneratedPluginInfo.Identifier, GeneratedPluginInfo.Name, GeneratedPluginInfo.Version)]
public sealed class SamplePlugin : BaseUnityPlugin
Expand All @@ -57,15 +78,15 @@ public sealed class SamplePlugin : BaseUnityPlugin
> _By default, the generated class is `internal static`, this can be changed using the `<PluginInfoTypeModifiers />` MSBuild property._

### Publish to Thunderstore
## 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 changed 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
<Project Sdk="LethalCompany.Plugin.Sdk/1.0.0">
<Project Sdk="LethalCompany.Plugin.Sdk">

<PropertyGroup>
<!-- ... -->
Expand Down Expand Up @@ -106,7 +127,7 @@ MSBuild version 17.8.3+195e7f5a3 for .NET
p".
```

#### Staging Plugins
### Staging Plugins

"Staging" a plugin refers to the process of publishing a plugin directly to a local Thunderstore profile, and is performed by specifiying the `PluginStagingProfile` MSBuild property when publishing:
```bash
Expand All @@ -115,7 +136,7 @@ dotnet publish -p:PluginStagingProfile="..."

> _It is recommended to set the `<PluginStagingProfile />` MSBuild property in a `.csproj.user` file._
#### Specify Thunderstore Dependencies
### Specify Thunderstore Dependencies

To specify a dependency on another Thunderstore plugin, use the `ThunderDependency` item:
```xml
Expand All @@ -124,7 +145,7 @@ To specify a dependency on another Thunderstore plugin, use the `ThunderDependen
</ItemGroup>
```

##### Configure Referenced Assemblies
#### Configure Referenced Assemblies

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

Expand All @@ -142,3 +163,15 @@ Assembly resolution can be configured by specifying glob patterns for the `Exclu
> _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`._
#### Restore Plugins During Solution Restore

Due to limitiations in how MSBuild handles solution files, the Sdk is unable restore Thunderstore dependencies when directly restoring a solution (e.g. via `dotnet restore example-plugin.sln`).

To workaround this, create a `Directory.Solution.targets` file adjacent to the `.sln` file, that directly imports the `Restore.targets` from the Sdk:
```xml
<Project>

<Import Project="Restore.targets" Sdk="LethalCompany.Plugin.Sdk" />

<Project>
```
56 changes: 56 additions & 0 deletions src/Sdk/Common.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

<PropertyGroup>
<PluginSdkCommonTargetsHasBeenImported>true</PluginSdkCommonTargetsHasBeenImported>
</PropertyGroup>

<PropertyGroup>
<PluginManagerDataDir Condition=" '$(PluginManagerDataDir)' == '' AND '$(PluginManager)' == 'r2modman' ">$(AppData)\r2modmanPlus-local\LethalCompany\</PluginManagerDataDir>
<PluginManagerDataDir Condition=" '$(PluginManagerDataDir)' == '' ">$(AppData)\Thunderstore Mod Manager\DataFolder\LethalCompany\</PluginManagerDataDir>

<PluginCacheDir Condition=" '$(PluginCacheDir)' == '' AND '$(PLUGIN_SDK_CACHE)' != '' ">$([MSBuild]::EnsureTrailingSlash($(PLUGIN_SDK_CACHE)))</PluginCacheDir>
<PluginCacheDir Condition=" '$(PluginCacheDir)' == '' ">$(PluginManagerDataDir)cache\</PluginCacheDir>
<PluginProfileDir Condition=" '$(PluginStagingProfile)' != '' AND '$(PluginProfileDir)' == '' AND '$(PluginManagerDataDir)' != '' ">$(PluginManagerDataDir)profiles\$(PluginStagingProfile)\</PluginProfileDir>

<PluginDescription Condition=" '$(PluginDescription)' == '' ">$(Description)</PluginDescription>
<PluginId Condition=" '$(PluginId)' == '' ">$(AssemblyName)</PluginId>

<!-- NOTE: backwards compatibility with renaming PluginInfoTypeAccessModifier -> PluginInfoModifiers -->
<PluginInfoTypeModifiers Condition=" '$(PluginInfoTypeModifiers)' == '' ">$(PluginInfoTypeAccessModifier)</PluginInfoTypeModifiers>
<PluginInfoTypeModifiers Condition=" '$(PluginInfoTypeModifiers)' == '' ">internal static</PluginInfoTypeModifiers>
<PluginInfoTypeName Condition=" '$(PluginInfoTypeName)' == '' ">GeneratedPluginInfo</PluginInfoTypeName>

<PluginName Condition=" '$(PluginName)' == '' ">$(Title)</PluginName>
<PluginName Condition=" '$(PluginName)' == '' ">$(Product)</PluginName>
<PluginName Condition=" '$(PluginName)' == '' ">$(AssemblyName)</PluginName>

<PluginChangeLogFile Condition=" '$(PluginChangeLogFile)' == '' ">CHANGELOG.md</PluginChangeLogFile>
<PluginReadMeFile Condition=" '$(PluginReadMeFile)' == '' ">$(PackageReadMeFile)</PluginReadMeFile>
<PluginReadMeFile Condition=" '$(PluginReadMeFile)' == '' ">README.md</PluginReadMeFile>
<PluginVersion Condition=" '$(PluginVersion)' == '' ">$(Version)</PluginVersion>

<!-- Thunderstore metadata -->
<ThunderDescription Condition=" '$(ThunderDescription)' == '' ">$(PluginDescription)</ThunderDescription>
<ThunderId Condition=" '$(ThunderId)' == '' ">$(PackageId)</ThunderId>
<ThunderId Condition=" '$(ThunderId)' == '' ">$(PluginId)</ThunderId>
<ThunderVersion Condition=" '$(ThunderVersion)' == '' ">$(PluginVersion)</ThunderVersion>
<ThunderWebsiteUrl Condition=" '$(ThunderWebsiteUrl)' == '' ">$(PackageProjectUrl)</ThunderWebsiteUrl>
</PropertyGroup>

<ItemGroup>
<Content Include="$(PluginChangeLogFile)" Condition=" '$(PluginChangeLogFile)' != '' AND Exists('$(MSBuildProjectDirectory)\$(PluginChangeLogFile)') ">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>

<Content Include="icon.png;$(PluginReadMeFile)">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>

<ItemGroup Condition=" '$(ImplicitUsings)' == 'true' OR '$(ImplicitUsings)' == 'enable' ">
<Using Include="System.Collections" />
<Using Include="BepInEx" />
<Using Include="HarmonyLib" />
</ItemGroup>

</Project>
42 changes: 42 additions & 0 deletions src/Sdk/Generate.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="Common.targets" Condition=" '$(PluginSdkCommonTargetsHasBeenImported)' != 'true' " />

<PropertyGroup>
<PluginSdkGenerateTargetsHasBeenImported>true</PluginSdkGenerateTargetsHasBeenImported>
</PropertyGroup>

<!--
Executes the `GeneratePluginInfoCode` task to generate a type containing plugin metadata defined via MSBuild properties.
-->
<Target Name="_GeneratePluginInfo" BeforeTargets="BeforeCompile;CoreCompile" Inputs="$(MSBuildAllProjects)" Outputs="$(IntermediateOutputPath)$(MSBuildProjectName).PluginInfo.g.cs">
<ItemGroup>
<Compile Include="$(IntermediateOutputPath)$(MSBuildProjectName).PluginInfo.g.cs" />
<FileWrites Include="$$(IntermediateOutputPath)$(MSBuildProjectName).PluginInfo.g.cs" />
</ItemGroup>

<GeneratePluginInfoCode Identifier="$(PluginId)" Name="$(PluginName)" Namespace="$(RootNamespace)" Version="$(PluginVersion)" TypeModifiers="$(PluginInfoTypeModifiers)" TypeName="$(PluginInfoTypeName)">
<Output PropertyName="_GeneratedPluginInfoCode" TaskParameter="GeneratedText" />
</GeneratePluginInfoCode>
<WriteLinesToFile File="$(IntermediateOutputPath)$(MSBuildProjectName).PluginInfo.g.cs" Lines="$(_GeneratedPluginInfoCode)" Overwrite="true" WriteOnlyWhenDifferent="true" Condition=" '$(_GeneratedPluginInfoCode)' != '' " />
</Target>

<!--
Executes the `GeneratePluginManifestJson` task to generate the json text of the `manifest.json` used when publishing a Thunderstore package.
-->
<Target Name="_GeneratePluginManifest" BeforeTargets="BeforeCompile;CoreCompile" Inputs="$(MSBuildAllProjects)" Outputs="$(IntermediateOutputPath)$(MSBuildProjectName).manifest.g.json">
<ItemGroup>
<Content Include="$(IntermediateOutputPath)$(MSBuildProjectName).manifest.g.json">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<Link>manifest.json</Link>
<Visible>false</Visible>
</Content>
<FileWrites Include="$(IntermediateOutputPath)$(MSBuildProjectName).manifest.g.json" />
</ItemGroup>

<GeneratePluginManifestJson Dependencies="@(ThunderDependency)" Description="$(ThunderDescription)" Name="$(ThunderId)" Version="$(ThunderVersion)" WebsiteUrl="$(ThunderWebsiteUrl)">
<Output PropertyName="_GeneratedManifestJson" TaskParameter="GeneratedText" />
</GeneratePluginManifestJson>
<WriteLinesToFile File="$(IntermediateOutputPath)$(MSBuildProjectName).manifest.g.json" Lines="$(_GeneratedManifestJson)" Overwrite="true" WriteOnlyWhenDifferent="true" Condition=" '$(_GeneratedManifestJson)' != '' " />
</Target>

</Project>
74 changes: 74 additions & 0 deletions src/Sdk/Publish.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="Common.targets" Condition=" '$(PluginSdkCommonTargetsHasBeenImported)' != 'true' " />

<PropertyGroup>
<PluginSdkPublishTargetsHasBeenImported>true</PluginSdkPublishTargetsHasBeenImported>
</PropertyGroup>

<!-- NOTE: enforce optimizations in release mode -->
<PropertyGroup Condition=" '$(Configuration)' == 'Release' AND '$(DisableReleaseOptimizations)' != 'true' ">
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
<IncludeSymbols>false</IncludeSymbols>
<Optimize>true</Optimize>
</PropertyGroup>

<!--
Configures properties+directories required for publishing a Thunderstore package.
-->
<Target Name="PrepareForPluginPublish" BeforeTargets="PrepareForPublish;PublishPlugin">
<PropertyGroup>
<_PublishDir>$([MSBuild]::EnsureTrailingSlash('$(PublishDir)'))</_PublishDir>
<PublishDir>$(OutputPath)publish\</PublishDir>

<PluginStagingDir Condition=" '$(PluginStagingProfile)' != '' ">$(PluginProfileDir)BepInEx\plugins\$(ThunderId)\</PluginStagingDir>
<StagePlugin Condition=" '$(StagePlugin)' != 'false' AND '$(PluginStagingProfile)' != '' ">true</StagePlugin>

<_PluginPackage>$(_PublishDir)$(ThunderId)-$(PluginVersion).zip</_PluginPackage>

<!-- NOTE: a custom output path was NOT specified, place the archive in 'bin\', rather than 'bin\publish\' (can't place archive in the folder being archived) -->
<_PluginPackage Condition=" '$(PublishDir)' == '$(_PublishDir)' ">$(OutputPath)$(ThunderId)-$(PluginVersion).zip</_PluginPackage>
</PropertyGroup>

<MakeDir Directories="$(_PublishDir)" Condition=" '$(StagePlugin)' != 'true' " />
</Target>

<!--
Copies custom build assets to the publish directory, prior to the default publishing process.
-->
<Target Name="_CopyToPublishDir" AfterTargets="Publish" BeforeTargets="PublishPlugin">
<Copy SourceFiles="$(IntermediateOutputPath)$(MSBuildProjectName).manifest.g.json" DestinationFiles="$(PublishDir)manifest.json" SkipUnchangedFiles="true" Condition=" Exists('$(IntermediateOutputPath)$(MSBuildProjectName).manifest.g.json') " />
</Target>

<!--
Creates a Thunderstore package from the content of the default publish directory.
-->
<Target Name="PublishPlugin" AfterTargets="Publish">
<Warning Code="LC001" Text="Plugin was not built in Release mode, users may experience an impact to performance!" Condition=" '$(Configuration)' != 'Release' AND '$(StagePlugin)' != 'true' AND '$(DisableReleaseOptimizations)' != 'true' " />

<Error Text="Failed to Stage Plugin, PluginStagingProfile directory '$(PluginProfileDir)' does not exist." Condition=" '$(StagePlugin)' == 'true' AND !Exists('$(PluginProfileDir)') " />
<MakeDir Directories="$(PluginStagingDir)" />

<!-- clean -->
<ItemGroup>
<_PublishFilesToDelete Include="$(PublishDir)\$(AssemblyName).deps.json" />
<_PublishFilesToDelete Include="$(PublishDir)\*.pdb" Condition=" '$(Configuration)' == 'Release' " />
</ItemGroup>
<Delete Files="@(_PublishFilesToDelete)" />

<ItemGroup>
<_PluginFiles Include="$(PublishDir)\**\*.*" />
<_PluginStagingDirFiles Include="$(PluginStagingDir)**\*.*" Condition=" '$(StagePlugin)' == 'true' " />
</ItemGroup>

<!-- stage -->
<Delete Files="@(_PluginStagingDirFiles)" Condition=" '$(StagePlugin)' == 'true' " />
<Copy SourceFiles="@(_PluginFiles)" DestinationFiles="@(_PluginFiles -> '$(PluginStagingDir)%(RecursiveDir)%(Filename)%(Extension)')" SkipUnchangedFiles="true" Condition=" '$(StagePlugin)' == 'true' " />

<Message Importance="High" Text="$(ThunderId) -> $(PluginStagingDir)" Condition=" '$(StagePlugin)' == 'true' " />

<!-- package -->
<ZipDirectory SourceDirectory="$(PublishDir)" DestinationFile="$(_PluginPackage)" Overwrite="true" Condition=" '$(StagePlugin)' != 'true' " />
</Target>

</Project>
49 changes: 49 additions & 0 deletions src/Sdk/Restore.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="Sdk.tasks" Condition=" '$(PluginSdkAssemblyHasBeenImported)' != 'true' " />
<Import Project="Common.targets" Condition=" '$(PluginSdkCommonTargetsHasBeenImported)' != 'true' " />

<PropertyGroup>
<PluginSdkRestoreTargetsHasBeenImported>true</PluginSdkRestoreTargetsHasBeenImported>
</PropertyGroup>

<PropertyGroup>
<RestorePluginsWithLockFile Condition=" '$(RestorePluginsWithLockFile)' == '' AND '$(RestorePackagesWithLockFile)' == 'true' ">true</RestorePluginsWithLockFile>
<PluginLockFile Condition=" '$(RestorePluginsWithLockFile)' == 'true' AND '$(PluginLockFile)' == '' ">$(MSBuildProjectDirectory)\plugins.lock.json</PluginLockFile>
<PluginLockFile Condition=" '$(RestorePluginsWithLockFile)' != 'true' "></PluginLockFile>

<PluginAssetsFile>$(MSBuildProjectDirectory)\$(BaseIntermediateOutputPath)plugin.assets.json</PluginAssetsFile>
<PluginRestoreOutputs>$(PluginRestoreOutputs);$(PluginAssetsFile);</PluginRestoreOutputs>
<PluginRestoreOutputs Condition=" '$(PluginLockFile)' != '' ">$(PluginRestoreOutputs)$(PluginLockFile);</PluginRestoreOutputs>

<ResolveAssemblyReferencesDependsOn>$(ResolveAssemblyReferencesDependsOn)ResolvePluginReferences;</ResolveAssemblyReferencesDependsOn>
</PropertyGroup>

<!--
Executes the `RestorePluginDependencies` task to restore `ThunderDependency` items
-->
<Target Name="RestorePlugins" BeforeTargets="Restore" AfterTargets="_GetAllRestoreProjectPathItems" Inputs="$(MSBuildAllProjects)" Outputs="$(PluginRestoreOutputs)">
<ItemGroup>
<FileWrites Include="$(PluginLockFile)" Condition=" '$(PluginLockFile)' != '' " />
<FileWrites Include="$(PluginAssetsFile)" />
</ItemGroup>

<RestorePluginDependencies CacheDirectory="$(PluginCacheDir)" Dependencies="@(ThunderDependency)" LockedMode="$(RestoreLockedMode)" PluginLockFile="$(PluginLockFile)" WithLockFile="$(RestorePluginsWithLockFile)">
<Output PropertyName="_GeneratedAssetsJson" TaskParameter="GeneratedAssetsJson" />
</RestorePluginDependencies>
<WriteLinesToFile File="$(PluginAssetsFile)" Lines="$(_GeneratedAssetsJson)" Overwrite="true" WriteOnlyWhenDifferent="true" Condition=" '$(_GeneratedAssetsJson)' != '' " />
</Target>

<!--
Executes the `ResolvePluginAssemblies` task to resolve the reference assemblies of `ThunderDependency` items.
-->
<Target Name="ResolvePluginReferences" BeforeTargets="CoreCompile" AfterTargets="RestorePlugins;Restore" DependsOnTargets="ResolveProjectReferences">
<ResolvePluginAssemblies AssetsFile="$(PluginAssetsFile)">
<Output ItemName="_PluginReference" TaskParameter="ResolvedAssemblies" />
</ResolvePluginAssemblies>

<ItemGroup>
<Reference Include="@(_PluginReference)" Private="false" />
</ItemGroup>
</Target>

</Project>
1 change: 1 addition & 0 deletions src/Sdk/Sdk.props
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<OutputType>Library</OutputType>
<PublishRelease>true</PublishRelease>
<TargetFramework>netstandard2.1</TargetFramework>

<!-- analyzers -->
Expand Down
Loading

0 comments on commit 3310ae9

Please sign in to comment.