├── .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 | [](https://www.nuget.org/packages/BepInEx.AssemblyPublicizer)
4 | [](https://www.nuget.org/packages/BepInEx.AssemblyPublicizer.MSBuild)
5 | [](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 |
--------------------------------------------------------------------------------