├── .github └── workflows │ └── main.yml ├── .gitignore ├── BepInEx.AssemblyPublicizer.Cli ├── BepInEx.AssemblyPublicizer.Cli.csproj ├── Program.cs └── PublicizeCommand.cs ├── BepInEx.AssemblyPublicizer.MSBuild ├── BepInEx.AssemblyPublicizer.MSBuild.csproj ├── BepInEx.AssemblyPublicizer.MSBuild.props ├── Extensions.cs ├── IgnoresAccessChecksToAttribute.cs └── PublicizeTask.cs ├── BepInEx.AssemblyPublicizer.sln ├── BepInEx.AssemblyPublicizer ├── AssemblyPublicizer.cs ├── AssemblyPublicizerOptions.cs ├── BepInEx.AssemblyPublicizer.csproj ├── FatalAsmResolver.cs ├── NoopAssemblyResolver.cs └── OriginalAttributesAttribute.cs ├── Directory.Build.props ├── LICENSE ├── README.md ├── TestLibrary ├── InternalClass.cs └── TestLibrary.csproj ├── TestProject ├── Program.cs └── TestProject.csproj └── build.cake /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [ "push", "pull_request" ] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-24.04 8 | 9 | steps: 10 | - uses: actions/checkout@v4 11 | with: 12 | submodules: true 13 | 14 | - name: Setup .NET 15 | uses: actions/setup-dotnet@v4 16 | with: 17 | dotnet-version: 9.x 18 | 19 | - name: Run the Cake script 20 | uses: cake-build/cake-action@v3 21 | with: 22 | verbosity: Diagnostic 23 | 24 | - uses: actions/upload-artifact@v4 25 | with: 26 | name: nuget packages 27 | path: ./BepInEx.AssemblyPublicizer*/bin/Release/*.nupkg 28 | 29 | - name: Push NuGet package 30 | if: github.ref_type == 'tag' 31 | run: | 32 | dotnet nuget push ./BepInEx.AssemblyPublicizer*/bin/Release/*.nupkg --source ${{ secrets.NUGET_SOURCE }} --api-key ${{ secrets.NUGET_KEY }} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | /packages/ 4 | riderModule.iml 5 | /_ReSharper.Caches/ 6 | .idea 7 | *.user -------------------------------------------------------------------------------- /BepInEx.AssemblyPublicizer.Cli/BepInEx.AssemblyPublicizer.Cli.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net6.0 5 | enable 6 | enable 7 | 8 | CLI tool for BepInEx.AssemblyPublicizer 9 | 10 | true 11 | true 12 | assembly-publicizer 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /BepInEx.AssemblyPublicizer.Cli/Program.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine.Builder; 2 | using System.CommandLine.Parsing; 3 | using BepInEx.AssemblyPublicizer.Cli; 4 | using Serilog; 5 | 6 | Log.Logger = new LoggerConfiguration() 7 | .MinimumLevel.Information() 8 | .WriteTo.Console() 9 | .CreateLogger(); 10 | 11 | try 12 | { 13 | return await new CommandLineBuilder(new PublicizeCommand()) 14 | .UseDefaults() 15 | .UseExceptionHandler((ex, _) => Log.Fatal(ex, "Exception, cannot continue!"), -1) 16 | .Build() 17 | .InvokeAsync(args); 18 | } 19 | finally 20 | { 21 | Log.CloseAndFlush(); 22 | } 23 | -------------------------------------------------------------------------------- /BepInEx.AssemblyPublicizer.Cli/PublicizeCommand.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine; 2 | using System.CommandLine.NamingConventionBinder; 3 | using System.Diagnostics; 4 | using Serilog; 5 | 6 | namespace BepInEx.AssemblyPublicizer.Cli; 7 | 8 | public sealed class PublicizeCommand : RootCommand 9 | { 10 | public PublicizeCommand() 11 | { 12 | Name = "assembly-publicizer"; 13 | Description = "Publicize given assemblies"; 14 | 15 | Add(new Argument("input") { Arity = ArgumentArity.OneOrMore }.ExistingOnly()); 16 | Add(new Option(new[] { "--output", "-o" }).LegalFilePathsOnly()); 17 | Add(new Option("--target", () => PublicizeTarget.All, "Targets for publicizing")); 18 | Add(new Option("--publicize-compiler-generated", "Publicize compiler generated types and members")); 19 | Add(new Option("--dont-add-attribute", "Skip injecting OriginalAttributes attribute")); 20 | Add(new Option("--strip", "Strips all method bodies by setting them to `throw null;`")); 21 | Add(new Option("--strip-only", "Strips without publicizing, equivalent to `--target None --strip`")); 22 | Add(new Option(new[] { "--overwrite", "-f" }, "Overwrite existing files instead appending a postfix")); 23 | Add(new Option("--disable-parallel", "Don't publicize in parallel")); 24 | 25 | Handler = HandlerDescriptor.FromDelegate(Handle).GetCommandHandler(); 26 | } 27 | 28 | private static void Handle(FileSystemInfo[] input, string? output, PublicizeTarget target, bool publicizeCompilerGenerated, bool dontAddAttribute, bool strip, bool stripOnly, bool overwrite, bool disableParallel) 29 | { 30 | var assemblies = new List(); 31 | 32 | foreach (var fileSystemInfo in input) 33 | { 34 | switch (fileSystemInfo) 35 | { 36 | case DirectoryInfo directoryInfo: 37 | assemblies.AddRange(directoryInfo.GetFiles("*.dll")); 38 | break; 39 | case FileInfo fileInfo: 40 | assemblies.Add(fileInfo); 41 | break; 42 | } 43 | } 44 | 45 | Log.Information("Publicizing {Count} assemblies {Assemblies}", assemblies.Count, assemblies.Select(x => x.Name)); 46 | 47 | var options = new AssemblyPublicizerOptions 48 | { 49 | Target = stripOnly ? PublicizeTarget.None : target, 50 | PublicizeCompilerGenerated = publicizeCompilerGenerated, 51 | IncludeOriginalAttributesAttribute = false, 52 | Strip = stripOnly || strip, 53 | }; 54 | 55 | var stopwatch = Stopwatch.StartNew(); 56 | 57 | void Publicize(FileInfo fileInfo) 58 | { 59 | var outputPath = output ?? fileInfo.DirectoryName!; 60 | 61 | if (Directory.Exists(outputPath) || string.IsNullOrEmpty(Path.GetExtension(outputPath))) 62 | { 63 | Directory.CreateDirectory(outputPath); 64 | outputPath = Path.Combine(outputPath, overwrite ? fileInfo.Name : Path.GetFileNameWithoutExtension(fileInfo.Name) + "-publicized" + Path.GetExtension(fileInfo.Name)); 65 | } 66 | else if (Path.GetFullPath(outputPath) == fileInfo.FullName) 67 | { 68 | Log.Warning("Can't write to {OutputPath} without --overwrite flag", outputPath); 69 | return; 70 | } 71 | 72 | AssemblyPublicizer.Publicize(fileInfo.FullName, outputPath, options); 73 | Log.Information("Publicized {InputPath} -> {OutputPath}", fileInfo.Name, outputPath); 74 | } 75 | 76 | if (disableParallel || assemblies.Count <= 1) 77 | { 78 | assemblies.ForEach(Publicize); 79 | } 80 | else 81 | { 82 | Parallel.ForEach(assemblies, Publicize); 83 | } 84 | 85 | stopwatch.Stop(); 86 | Log.Information("Done in {Time}", stopwatch.Elapsed); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /BepInEx.AssemblyPublicizer.MSBuild/BepInEx.AssemblyPublicizer.MSBuild.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.1;net472 4 | latest 5 | embedded 6 | 7 | MSBuild integration for BepInEx.AssemblyPublicizer 8 | 9 | true 10 | true 11 | true 12 | true 13 | true 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | <_PackageFiles Include="bin\$(Configuration)\*\BepInEx.AssemblyPublicizer.dll;bin\$(Configuration)\*\AsmResolver*.dll"> 33 | lib%(RecursiveDir) 34 | false 35 | Content 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /BepInEx.AssemblyPublicizer.MSBuild/BepInEx.AssemblyPublicizer.MSBuild.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | <_BepInExAssemblyPublicizer_TaskFolder Condition="'$(MSBuildRuntimeType)' == 'Core'">netstandard2.1 4 | <_BepInExAssemblyPublicizer_TaskFolder Condition="'$(MSBuildRuntimeType)' != 'Core'">net472 5 | <_BepInExAssemblyPublicizer_TaskAssembly>$(MSBuildThisFileDirectory)..\lib\$(_BepInExAssemblyPublicizer_TaskFolder)\$(MSBuildThisFileName).dll 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | $(IntermediateOutputPath)$(MSBuildProjectName).IgnoresAccessChecksTo.cs 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | true 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | false 48 | 49 | 50 | -------------------------------------------------------------------------------- /BepInEx.AssemblyPublicizer.MSBuild/Extensions.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Diagnostics.CodeAnalysis; 6 | using Microsoft.Build.Framework; 7 | 8 | namespace BepInEx.AssemblyPublicizer.MSBuild; 9 | 10 | internal static class Extensions 11 | { 12 | public static bool HasMetadata(this ITaskItem taskItem, string metadataName) 13 | { 14 | var metadataNames = (ICollection)taskItem.MetadataNames; 15 | return metadataNames.Contains(metadataName); 16 | } 17 | 18 | public static bool TryGetMetadata(this ITaskItem taskItem, string metadataName, [NotNullWhen(true)] out string? metadata) 19 | { 20 | if (taskItem.HasMetadata(metadataName)) 21 | { 22 | metadata = taskItem.GetMetadata(metadataName); 23 | return true; 24 | } 25 | 26 | metadata = null; 27 | return false; 28 | } 29 | 30 | public static bool GetBoolMetadata(this ITaskItem taskItem, string metadataName) 31 | { 32 | return taskItem.GetMetadata(metadataName).Equals("true", StringComparison.OrdinalIgnoreCase); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /BepInEx.AssemblyPublicizer.MSBuild/IgnoresAccessChecksToAttribute.cs: -------------------------------------------------------------------------------- 1 | // 2 | 3 | namespace System.Runtime.CompilerServices 4 | { 5 | [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] 6 | internal sealed class IgnoresAccessChecksToAttribute : Attribute 7 | { 8 | public IgnoresAccessChecksToAttribute(string assemblyName) 9 | { 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /BepInEx.AssemblyPublicizer.MSBuild/PublicizeTask.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Security.Cryptography; 7 | using System.Text; 8 | using Microsoft.Build.Framework; 9 | using Microsoft.Build.Utilities; 10 | 11 | namespace BepInEx.AssemblyPublicizer.MSBuild; 12 | 13 | public class PublicizeTask : Task 14 | { 15 | [Required] 16 | public string IntermediateOutputPath { get; set; } 17 | 18 | [Required] 19 | public string GeneratedIgnoresAccessChecksToFile { get; set; } 20 | 21 | [Required] 22 | public ITaskItem[] ReferencePath { get; set; } 23 | 24 | [Required] 25 | public ITaskItem[] PackageReference { get; set; } 26 | 27 | [Required] 28 | public ITaskItem[] Publicize { get; set; } 29 | 30 | [Output] 31 | public ITaskItem[] RemovedReferences { get; private set; } 32 | 33 | [Output] 34 | public ITaskItem[] PublicizedReferences { get; private set; } 35 | 36 | public override bool Execute() 37 | { 38 | var outputDirectory = Path.Combine(IntermediateOutputPath, "publicized"); 39 | Directory.CreateDirectory(outputDirectory); 40 | 41 | var packagesToPublicize = PackageReference.Where(x => x.GetBoolMetadata("Publicize")).ToDictionary(x => x.ItemSpec); 42 | var assemblyNamesToPublicize = Publicize.ToDictionary(x => x.ItemSpec); 43 | 44 | var removedReferences = new List(); 45 | var publicizedReferences = new List(); 46 | 47 | foreach (var taskItem in ReferencePath) 48 | { 49 | var fileName = taskItem.GetMetadata("FileName"); 50 | 51 | ITaskItem optionsHolder; 52 | 53 | if (taskItem.GetBoolMetadata("Publicize")) 54 | { 55 | optionsHolder = taskItem; 56 | } 57 | else if (assemblyNamesToPublicize.TryGetValue(fileName, out optionsHolder)) 58 | { 59 | } 60 | else if (taskItem.TryGetMetadata("NuGetPackageId", out var nuGetPackageId) && packagesToPublicize.TryGetValue(nuGetPackageId, out optionsHolder)) 61 | { 62 | } 63 | else 64 | { 65 | continue; 66 | } 67 | 68 | var options = new AssemblyPublicizerOptions(); 69 | 70 | if (optionsHolder.TryGetMetadata("PublicizeTarget", out var rawTarget)) 71 | { 72 | if (Enum.TryParse(rawTarget, true, out var parsedTarget)) 73 | { 74 | options.Target = parsedTarget; 75 | } 76 | else 77 | { 78 | throw new FormatException($"String '{rawTarget}' was not recognized as a valid PublicizeTarget."); 79 | } 80 | } 81 | 82 | if (optionsHolder.TryGetMetadata("PublicizeCompilerGenerated", out var rawPublicizeCompilerGenerated)) 83 | { 84 | options.PublicizeCompilerGenerated = bool.Parse(rawPublicizeCompilerGenerated); 85 | } 86 | 87 | if (optionsHolder.TryGetMetadata("IncludeOriginalAttributesAttribute", out var rawIncludeOriginalAttributesAttribute)) 88 | { 89 | options.IncludeOriginalAttributesAttribute = bool.Parse(rawIncludeOriginalAttributesAttribute); 90 | } 91 | 92 | if (optionsHolder.TryGetMetadata("Strip", out var rawStrip)) 93 | { 94 | options.Strip = bool.Parse(rawStrip); 95 | } 96 | 97 | var assemblyPath = taskItem.GetMetadata("FullPath"); 98 | var hash = ComputeHash(File.ReadAllBytes(assemblyPath), options); 99 | 100 | var publicizedAssemblyPath = Path.Combine(outputDirectory, Path.GetFileName(assemblyPath)); 101 | var hashPath = publicizedAssemblyPath + ".md5"; 102 | 103 | removedReferences.Add(taskItem); 104 | 105 | var publicizedReference = new TaskItem(publicizedAssemblyPath); 106 | taskItem.CopyMetadataTo(publicizedReference); 107 | publicizedReference.RemoveMetadata("ReferenceAssembly"); 108 | publicizedReferences.Add(publicizedReference); 109 | 110 | if (File.Exists(hashPath) && File.ReadAllText(hashPath) == hash) 111 | { 112 | Log.LogMessage($"{fileName} was already publicized, skipping"); 113 | continue; 114 | } 115 | 116 | AssemblyPublicizer.Publicize(assemblyPath, publicizedAssemblyPath, options); 117 | 118 | var originalDocumentationPath = Path.Combine(Path.GetDirectoryName(assemblyPath)!, fileName + ".xml"); 119 | if (File.Exists(originalDocumentationPath)) 120 | { 121 | File.Copy(originalDocumentationPath, Path.Combine(outputDirectory, fileName + ".xml"), true); 122 | } 123 | 124 | File.WriteAllText(hashPath, hash); 125 | 126 | Log.LogMessage($"Publicized {fileName}"); 127 | } 128 | 129 | GenerateIgnoresAccessChecksToFile(publicizedReferences); 130 | 131 | RemovedReferences = removedReferences.ToArray(); 132 | PublicizedReferences = publicizedReferences.ToArray(); 133 | 134 | return true; 135 | } 136 | 137 | private void GenerateIgnoresAccessChecksToFile(List publicizedReferences) 138 | { 139 | var stringBuilder = new StringBuilder(); 140 | 141 | stringBuilder.AppendLine("// "); 142 | stringBuilder.AppendLine("#pragma warning disable CS0436 // Type conflicts with imported type"); 143 | stringBuilder.AppendLine(); 144 | 145 | foreach (var publicizedReference in publicizedReferences) 146 | { 147 | var assemblyName = AssemblyName.GetAssemblyName(publicizedReference.GetMetadata("FullPath")).Name; 148 | stringBuilder.AppendLine($"[assembly: System.Runtime.CompilerServices.IgnoresAccessChecksToAttribute(\"{assemblyName}\")]"); 149 | } 150 | 151 | File.WriteAllText(GeneratedIgnoresAccessChecksToFile, stringBuilder.ToString()); 152 | } 153 | 154 | private static string ComputeHash(byte[] bytes, AssemblyPublicizerOptions options) 155 | { 156 | static void Hash(ICryptoTransform hash, byte[] buffer) 157 | { 158 | hash.TransformBlock(buffer, 0, buffer.Length, buffer, 0); 159 | } 160 | 161 | static void HashString(ICryptoTransform hash, string str) => Hash(hash, Encoding.UTF8.GetBytes(str)); 162 | static void HashBool(ICryptoTransform hash, bool value) => Hash(hash, BitConverter.GetBytes(value)); 163 | static void HashInt(ICryptoTransform hash, int value) => Hash(hash, BitConverter.GetBytes(value)); 164 | 165 | using var md5 = MD5.Create(); 166 | 167 | HashString(md5, typeof(AssemblyPublicizer).Assembly.GetCustomAttribute().InformationalVersion); 168 | HashString(md5, typeof(PublicizeTask).Assembly.GetCustomAttribute().InformationalVersion); 169 | 170 | HashInt(md5, (int)options.Target); 171 | HashBool(md5, options.PublicizeCompilerGenerated); 172 | HashBool(md5, options.IncludeOriginalAttributesAttribute); 173 | HashBool(md5, options.Strip); 174 | 175 | md5.TransformFinalBlock(bytes, 0, bytes.Length); 176 | 177 | return ByteArrayToString(md5.Hash); 178 | } 179 | 180 | private static string ByteArrayToString(IReadOnlyCollection data) 181 | { 182 | var builder = new StringBuilder(data.Count * 2); 183 | 184 | foreach (var b in data) 185 | { 186 | builder.AppendFormat("{0:x2}", b); 187 | } 188 | 189 | return builder.ToString(); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /BepInEx.AssemblyPublicizer.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BepInEx.AssemblyPublicizer", "BepInEx.AssemblyPublicizer\BepInEx.AssemblyPublicizer.csproj", "{20F6246E-5512-4B23-AFEB-009021FFDF35}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BepInEx.AssemblyPublicizer.Cli", "BepInEx.AssemblyPublicizer.Cli\BepInEx.AssemblyPublicizer.Cli.csproj", "{84075B65-9F75-4B72-A1D9-CF5F9D597E4E}" 6 | EndProject 7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BepInEx.AssemblyPublicizer.MSBuild", "BepInEx.AssemblyPublicizer.MSBuild\BepInEx.AssemblyPublicizer.MSBuild.csproj", "{FDE0951E-8435-4514-B221-FF58BC3A0234}" 8 | EndProject 9 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestProject", "TestProject\TestProject.csproj", "{12ACB697-3891-4CB8-A531-058434CF01ED}" 10 | EndProject 11 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestLibrary", "TestLibrary\TestLibrary.csproj", "{F2EC6047-4B6A-48B4-BFFD-230972DA7F8A}" 12 | EndProject 13 | Global 14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 15 | Debug|Any CPU = Debug|Any CPU 16 | Release|Any CPU = Release|Any CPU 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {20F6246E-5512-4B23-AFEB-009021FFDF35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {20F6246E-5512-4B23-AFEB-009021FFDF35}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {20F6246E-5512-4B23-AFEB-009021FFDF35}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {20F6246E-5512-4B23-AFEB-009021FFDF35}.Release|Any CPU.Build.0 = Release|Any CPU 23 | {84075B65-9F75-4B72-A1D9-CF5F9D597E4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {84075B65-9F75-4B72-A1D9-CF5F9D597E4E}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {84075B65-9F75-4B72-A1D9-CF5F9D597E4E}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {84075B65-9F75-4B72-A1D9-CF5F9D597E4E}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {FDE0951E-8435-4514-B221-FF58BC3A0234}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {FDE0951E-8435-4514-B221-FF58BC3A0234}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {FDE0951E-8435-4514-B221-FF58BC3A0234}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {FDE0951E-8435-4514-B221-FF58BC3A0234}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {12ACB697-3891-4CB8-A531-058434CF01ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {12ACB697-3891-4CB8-A531-058434CF01ED}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {12ACB697-3891-4CB8-A531-058434CF01ED}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {12ACB697-3891-4CB8-A531-058434CF01ED}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {F2EC6047-4B6A-48B4-BFFD-230972DA7F8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {F2EC6047-4B6A-48B4-BFFD-230972DA7F8A}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {F2EC6047-4B6A-48B4-BFFD-230972DA7F8A}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {F2EC6047-4B6A-48B4-BFFD-230972DA7F8A}.Release|Any CPU.Build.0 = Release|Any CPU 39 | EndGlobalSection 40 | EndGlobal 41 | -------------------------------------------------------------------------------- /BepInEx.AssemblyPublicizer/AssemblyPublicizer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using AsmResolver; 5 | using AsmResolver.DotNet; 6 | using AsmResolver.DotNet.Code.Cil; 7 | using AsmResolver.PE.DotNet.Cil; 8 | using AsmResolver.PE.DotNet.Metadata.Tables.Rows; 9 | 10 | namespace BepInEx.AssemblyPublicizer; 11 | 12 | public static class AssemblyPublicizer 13 | { 14 | public static void Publicize(string assemblyPath, string outputPath, AssemblyPublicizerOptions? options = null) 15 | { 16 | var assembly = FatalAsmResolver.FromFile(assemblyPath); 17 | var module = assembly.ManifestModule ?? throw new NullReferenceException(); 18 | module.MetadataResolver = new DefaultMetadataResolver(NoopAssemblyResolver.Instance); 19 | 20 | Publicize(assembly, options); 21 | module.FatalWrite(outputPath); 22 | } 23 | 24 | public static AssemblyDefinition Publicize(AssemblyDefinition assembly, AssemblyPublicizerOptions? options = null) 25 | { 26 | options ??= new AssemblyPublicizerOptions(); 27 | 28 | var module = assembly.ManifestModule!; 29 | 30 | var attribute = options.IncludeOriginalAttributesAttribute ? new OriginalAttributesAttribute(module) : null; 31 | 32 | foreach (var typeDefinition in module.GetAllTypes()) 33 | { 34 | if (attribute != null && typeDefinition == attribute.Type) 35 | continue; 36 | 37 | Publicize(typeDefinition, attribute, options); 38 | } 39 | 40 | return assembly; 41 | } 42 | 43 | private static void Publicize(TypeDefinition typeDefinition, OriginalAttributesAttribute? attribute, AssemblyPublicizerOptions options) 44 | { 45 | if (options.Strip && !typeDefinition.IsEnum && !typeDefinition.IsInterface) 46 | { 47 | foreach (var methodDefinition in typeDefinition.Methods) 48 | { 49 | if (!methodDefinition.HasMethodBody) 50 | continue; 51 | 52 | var newBody = methodDefinition.CilMethodBody = new CilMethodBody(methodDefinition); 53 | newBody.Instructions.Add(CilOpCodes.Ldnull); 54 | newBody.Instructions.Add(CilOpCodes.Throw); 55 | methodDefinition.NoInlining = true; 56 | } 57 | } 58 | 59 | if (!options.PublicizeCompilerGenerated && typeDefinition.IsCompilerGenerated()) 60 | return; 61 | 62 | if (options.HasTarget(PublicizeTarget.Types) && (!typeDefinition.IsNested && !typeDefinition.IsPublic || typeDefinition.IsNested && !typeDefinition.IsNestedPublic)) 63 | { 64 | if (attribute != null) 65 | typeDefinition.CustomAttributes.Add(attribute.ToCustomAttribute(typeDefinition.Attributes & TypeAttributes.VisibilityMask)); 66 | 67 | typeDefinition.Attributes &= ~TypeAttributes.VisibilityMask; 68 | typeDefinition.Attributes |= typeDefinition.IsNested ? TypeAttributes.NestedPublic : TypeAttributes.Public; 69 | } 70 | 71 | if (options.HasTarget(PublicizeTarget.Methods)) 72 | { 73 | foreach (var methodDefinition in typeDefinition.Methods) 74 | { 75 | Publicize(methodDefinition, attribute, options); 76 | } 77 | 78 | // Special case for accessors generated from auto properties, publicize them regardless of PublicizeCompilerGenerated 79 | if (!options.PublicizeCompilerGenerated) 80 | { 81 | foreach (var propertyDefinition in typeDefinition.Properties) 82 | { 83 | if (propertyDefinition.IsCompilerGenerated()) continue; 84 | 85 | if (propertyDefinition.GetMethod is { } getMethod) Publicize(getMethod, attribute, options, true); 86 | if (propertyDefinition.SetMethod is { } setMethod) Publicize(setMethod, attribute, options, true); 87 | } 88 | } 89 | } 90 | 91 | if (options.HasTarget(PublicizeTarget.Fields)) 92 | { 93 | var eventNames = new HashSet(typeDefinition.Events.Select(e => e.Name)); 94 | foreach (var fieldDefinition in typeDefinition.Fields) 95 | { 96 | if (fieldDefinition.IsPrivateScope) 97 | continue; 98 | 99 | if (!fieldDefinition.IsPublic) 100 | { 101 | // Skip event backing fields 102 | if (eventNames.Contains(fieldDefinition.Name)) 103 | continue; 104 | 105 | if (!options.PublicizeCompilerGenerated && fieldDefinition.IsCompilerGenerated()) 106 | continue; 107 | 108 | if (attribute != null) 109 | fieldDefinition.CustomAttributes.Add(attribute.ToCustomAttribute(fieldDefinition.Attributes & FieldAttributes.FieldAccessMask)); 110 | 111 | fieldDefinition.Attributes &= ~FieldAttributes.FieldAccessMask; 112 | fieldDefinition.Attributes |= FieldAttributes.Public; 113 | } 114 | } 115 | } 116 | } 117 | 118 | private static void Publicize(MethodDefinition methodDefinition, OriginalAttributesAttribute? attribute, AssemblyPublicizerOptions options, bool ignoreCompilerGeneratedCheck = false) 119 | { 120 | if (methodDefinition.IsCompilerControlled) 121 | return; 122 | 123 | // Ignore explicit interface implementations because you can't call them directly anyway and it confuses IDEs 124 | if (methodDefinition is { IsVirtual: true, IsFinal: true, DeclaringType: not null }) 125 | { 126 | foreach (var implementation in methodDefinition.DeclaringType.MethodImplementations) 127 | { 128 | if (implementation.Body == methodDefinition) 129 | { 130 | return; 131 | } 132 | } 133 | } 134 | 135 | if (!methodDefinition.IsPublic) 136 | { 137 | if (!ignoreCompilerGeneratedCheck && !options.PublicizeCompilerGenerated && methodDefinition.IsCompilerGenerated()) 138 | return; 139 | 140 | if (attribute != null) 141 | methodDefinition.CustomAttributes.Add(attribute.ToCustomAttribute(methodDefinition.Attributes & MethodAttributes.MemberAccessMask)); 142 | 143 | methodDefinition.Attributes &= ~MethodAttributes.MemberAccessMask; 144 | methodDefinition.Attributes |= MethodAttributes.Public; 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /BepInEx.AssemblyPublicizer/AssemblyPublicizerOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BepInEx.AssemblyPublicizer; 4 | 5 | public class AssemblyPublicizerOptions 6 | { 7 | public PublicizeTarget Target { get; set; } = PublicizeTarget.All; 8 | public bool PublicizeCompilerGenerated { get; set; } = false; 9 | public bool IncludeOriginalAttributesAttribute { get; set; } = true; 10 | 11 | public bool Strip { get; set; } = false; 12 | 13 | internal bool HasTarget(PublicizeTarget target) 14 | { 15 | return (Target & target) != 0; 16 | } 17 | } 18 | 19 | [Flags] 20 | public enum PublicizeTarget 21 | { 22 | All = Types | Methods | Fields, 23 | None = 0, 24 | Types = 1 << 0, 25 | Methods = 1 << 1, 26 | Fields = 1 << 2, 27 | } 28 | -------------------------------------------------------------------------------- /BepInEx.AssemblyPublicizer/BepInEx.AssemblyPublicizer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0 4 | latest 5 | enable 6 | 7 | Yet another assembly publicizer/stripper 8 | 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /BepInEx.AssemblyPublicizer/FatalAsmResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using AsmResolver; 5 | using AsmResolver.DotNet; 6 | using AsmResolver.DotNet.Builder; 7 | using AsmResolver.DotNet.Serialized; 8 | using AsmResolver.IO; 9 | using AsmResolver.PE; 10 | using AsmResolver.PE.DotNet.Builder; 11 | 12 | namespace BepInEx.AssemblyPublicizer; 13 | 14 | internal static class FatalAsmResolver 15 | { 16 | /// Same as but throws only on fatal errors 17 | public static AssemblyDefinition FromFile(string filePath) 18 | { 19 | return AssemblyDefinition.FromImage(PEImage.FromFile(filePath), new ModuleReaderParameters(FatalThrowErrorListener.Instance)); 20 | } 21 | 22 | private sealed class FatalThrowErrorListener : IErrorListener 23 | { 24 | public static FatalThrowErrorListener Instance { get; } = new(); 25 | 26 | public IList Exceptions { get; } = new List(); 27 | 28 | /// 29 | public void MarkAsFatal() 30 | { 31 | throw new AggregateException(Exceptions); 32 | } 33 | 34 | /// 35 | public void RegisterException(Exception exception) => Exceptions.Add(exception); 36 | } 37 | 38 | /// Same as but throws only on fatal errors 39 | public static void FatalWrite(this ModuleDefinition module, string filePath) 40 | { 41 | var result = new ManagedPEImageBuilder().CreateImage(module); 42 | if (result.HasFailed) 43 | { 44 | var errorListener = (FatalThrowErrorListener) result.ErrorListener; 45 | throw new AggregateException("Construction of the PE image failed with one or more errors.", errorListener.Exceptions); 46 | } 47 | 48 | using var fileStream = File.Create(filePath); 49 | new ManagedPEFileBuilder().CreateFile(result.ConstructedImage).Write(new BinaryStreamWriter(fileStream)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /BepInEx.AssemblyPublicizer/NoopAssemblyResolver.cs: -------------------------------------------------------------------------------- 1 | using AsmResolver.DotNet; 2 | 3 | namespace BepInEx.AssemblyPublicizer; 4 | 5 | internal class NoopAssemblyResolver : IAssemblyResolver 6 | { 7 | internal static NoopAssemblyResolver Instance { get; } = new(); 8 | 9 | public AssemblyDefinition? Resolve(AssemblyDescriptor assembly) 10 | { 11 | return null; 12 | } 13 | 14 | public void AddToCache(AssemblyDescriptor descriptor, AssemblyDefinition definition) 15 | { 16 | } 17 | 18 | public bool RemoveFromCache(AssemblyDescriptor descriptor) 19 | { 20 | return false; 21 | } 22 | 23 | public bool HasCached(AssemblyDescriptor descriptor) 24 | { 25 | return false; 26 | } 27 | 28 | public void ClearCache() 29 | { 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /BepInEx.AssemblyPublicizer/OriginalAttributesAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using AsmResolver.DotNet; 3 | using AsmResolver.DotNet.Code.Cil; 4 | using AsmResolver.DotNet.Signatures; 5 | using AsmResolver.DotNet.Signatures.Types; 6 | using AsmResolver.PE.DotNet.Cil; 7 | using AsmResolver.PE.DotNet.Metadata.Tables.Rows; 8 | 9 | namespace BepInEx.AssemblyPublicizer; 10 | 11 | internal class OriginalAttributesAttribute 12 | { 13 | private static Dictionary _typeNames = new() 14 | { 15 | [PublicizeTarget.Types] = "TypeAttributes", 16 | [PublicizeTarget.Methods] = "MethodAttributes", 17 | [PublicizeTarget.Fields] = "FieldAttributes", 18 | }; 19 | 20 | private Dictionary _attributesTypes = new(); 21 | private Dictionary _constructors = new(); 22 | 23 | public TypeDefinition Type { get; } 24 | 25 | public OriginalAttributesAttribute(ModuleDefinition module) 26 | { 27 | var corLibScope = module.CorLibTypeFactory.CorLibScope; 28 | var attributeReference = corLibScope.CreateTypeReference("System", "Attribute").ImportWith(module.DefaultImporter); 29 | var baseConstructorReference = attributeReference.CreateMemberReference(".ctor", MethodSignature.CreateInstance(module.CorLibTypeFactory.Void)).ImportWith(module.DefaultImporter); 30 | 31 | Type = new TypeDefinition( 32 | "BepInEx.AssemblyPublicizer", "OriginalAttributesAttribute", 33 | TypeAttributes.NotPublic | TypeAttributes.Sealed, 34 | attributeReference 35 | ); 36 | module.TopLevelTypes.Add(Type); 37 | 38 | foreach (var pair in _typeNames) 39 | { 40 | var attributesType = _attributesTypes[pair.Key] = corLibScope.CreateTypeReference("System.Reflection", pair.Value).ImportWith(module.DefaultImporter).ToTypeSignature(); 41 | 42 | var constructorDefinition = new MethodDefinition(".ctor", 43 | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RuntimeSpecialName | MethodAttributes.Public, 44 | MethodSignature.CreateInstance(module.CorLibTypeFactory.Void, attributesType) 45 | ); 46 | Type.Methods.Add(constructorDefinition); 47 | 48 | var body = constructorDefinition.CilMethodBody = new CilMethodBody(constructorDefinition); 49 | body.Instructions.Add(CilOpCodes.Ldarg_0); 50 | body.Instructions.Add(CilOpCodes.Call, baseConstructorReference); 51 | body.Instructions.Add(CilOpCodes.Ret); 52 | 53 | _constructors[pair.Key] = constructorDefinition; 54 | } 55 | } 56 | 57 | private CustomAttribute ToCustomAttribute(PublicizeTarget target, int value) 58 | { 59 | return new CustomAttribute( 60 | _constructors[target], 61 | new CustomAttributeSignature(new[] { new CustomAttributeArgument(_attributesTypes[target], value) }) 62 | ); 63 | } 64 | 65 | public CustomAttribute ToCustomAttribute(TypeAttributes attributes) => ToCustomAttribute(PublicizeTarget.Types, (int)attributes); 66 | public CustomAttribute ToCustomAttribute(MethodAttributes attributes) => ToCustomAttribute(PublicizeTarget.Methods, (int)attributes); 67 | public CustomAttribute ToCustomAttribute(FieldAttributes attributes) => ToCustomAttribute(PublicizeTarget.Fields, (int)attributes); 68 | } 69 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | BepInEx 4 | MIT 5 | https://github.com/BepInEx/BepInEx.AssemblyPublicizer 6 | git 7 | 8 | 0.4.3 9 | dev 10 | 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 BepInEx 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BepInEx.AssemblyPublicizer 2 | 3 | [![NuGet](https://img.shields.io/nuget/v/BepInEx.AssemblyPublicizer?label=BepInEx.AssemblyPublicizer&logo=NuGet)](https://www.nuget.org/packages/BepInEx.AssemblyPublicizer) 4 | [![NuGet](https://img.shields.io/nuget/v/BepInEx.AssemblyPublicizer.MSBuild?label=BepInEx.AssemblyPublicizer.MSBuild&logo=NuGet)](https://www.nuget.org/packages/BepInEx.AssemblyPublicizer.MSBuild) 5 | [![NuGet](https://img.shields.io/nuget/v/BepInEx.AssemblyPublicizer.Cli?label=BepInEx.AssemblyPublicizer.Cli&logo=NuGet)](https://www.nuget.org/packages/BepInEx.AssemblyPublicizer.Cli) 6 | 7 | Yet another assembly publicizer/stripper 8 | 9 | ## Using 10 | 11 | ### from code 12 | ```cs 13 | AssemblyPublicizer.Publicize("./Test.dll", "./Test-publicized.dll"); 14 | ``` 15 | 16 | ### from console 17 | `dotnet tool install -g BepInEx.AssemblyPublicizer.Cli` 18 | `assembly-publicizer ./Test.dll` - publicizes 19 | `assembly-publicizer ./Test.dll --strip` - publicizes and strips method bodies 20 | `assembly-publicizer ./Test.dll --strip-only` - strips without publicizing 21 | 22 | ### from msbuild 23 | ```xml 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ``` 36 | 37 | works with both .NET (generates IgnoresAccessChecksTo attributes) and Mono (AllowUnsafeBlocks) 38 | -------------------------------------------------------------------------------- /TestLibrary/InternalClass.cs: -------------------------------------------------------------------------------- 1 | namespace TestLibrary; 2 | 3 | internal class InternalClass 4 | { 5 | private void PrivateMethod() 6 | { 7 | Console.WriteLine("InternalClass.PrivateMethod"); 8 | } 9 | 10 | private int PrivateProperty => 1; 11 | 12 | private int PrivateAutoProperty { get; set; } 13 | } 14 | 15 | public class SecondClass 16 | { 17 | private int _field; 18 | 19 | private int Property { get; set; } 20 | 21 | private int Method() 22 | { 23 | return 0; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /TestLibrary/TestLibrary.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0 4 | latest 5 | enable 6 | enable 7 | 8 | 9 | -------------------------------------------------------------------------------- /TestProject/Program.cs: -------------------------------------------------------------------------------- 1 | using TestLibrary; 2 | 3 | var internalClass = new InternalClass(); 4 | internalClass.PrivateMethod(); 5 | Console.WriteLine(internalClass.PrivateProperty); 6 | internalClass.PrivateAutoProperty = 123; 7 | Console.WriteLine(internalClass.PrivateAutoProperty); 8 | -------------------------------------------------------------------------------- /TestProject/TestProject.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | <_BepInExAssemblyPublicizer_TaskAssembly>$(MSBuildThisFileDirectory)\..\BepInEx.AssemblyPublicizer.MSBuild\bin\$(Configuration)\netstandard2.1\BepInEx.AssemblyPublicizer.MSBuild.dll 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /build.cake: -------------------------------------------------------------------------------- 1 | var target = Argument("target", "Build"); 2 | 3 | var workflow = BuildSystem.GitHubActions.Environment.Workflow; 4 | var buildId = workflow.RunNumber; 5 | var tag = workflow.RefType == GitHubActionsRefType.Tag ? workflow.RefName : null; 6 | 7 | Task("Build") 8 | .Does(() => 9 | { 10 | var settings = new DotNetBuildSettings 11 | { 12 | Configuration = "Release", 13 | MSBuildSettings = new DotNetMSBuildSettings(), 14 | }; 15 | 16 | if (tag != null) 17 | { 18 | settings.MSBuildSettings.Version = tag; 19 | } 20 | else if (buildId != 0) 21 | { 22 | settings.MSBuildSettings.VersionSuffix = "ci." + buildId; 23 | } 24 | 25 | DotNetBuild(".", settings); 26 | }); 27 | 28 | RunTarget(target); 29 | --------------------------------------------------------------------------------