From b951695bb788b0d79d6073156d89e3a22f681b73 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Mon, 26 Oct 2026 19:07:52 +0100 Subject: [PATCH] prototype: generate private CoreLib references with System.Reflection.Metadata --- .../IgnoresAccessChecksToGenerator/.gitignore | 4 + .../Consumer/Consumer.csproj | 26 ++ .../Consumer/Program.cs | 61 +++ .../Directory.Build.props | 5 + .../FecSmoke/FecSmoke.csproj | 22 + .../FecSmoke/Program.cs | 42 ++ .../FecSmoke/RuntimeAccessorContractTests.cs | 159 +++++++ .../Generator/Generator.csproj | 12 + .../Generator/Program.cs | 199 ++++++++ .../Generator/PublicizeInternals.cs | 45 ++ .../Generator/ReferenceVerifier.cs | 59 +++ .../IgnoresAccessChecksToGenerator/LICENSE | 21 + .../IgnoresAccessChecksToGenerator/README.md | 57 +++ .../IgnoresAccessChecksToGenerator.targets | 26 ++ .../build/RuntimeReferences.targets | 15 + .../IgnoresAccessChecksToGenerator/replay.py | 89 ++++ .../results.json | 432 ++++++++++++++++++ .../IgnoresAccessChecksToGenerator.targets | 62 +++ .../upstream/PublicizeInternals.cs | 273 +++++++++++ 19 files changed, 1609 insertions(+) create mode 100644 BuildScripts/IgnoresAccessChecksToGenerator/.gitignore create mode 100644 BuildScripts/IgnoresAccessChecksToGenerator/Consumer/Consumer.csproj create mode 100644 BuildScripts/IgnoresAccessChecksToGenerator/Consumer/Program.cs create mode 100644 BuildScripts/IgnoresAccessChecksToGenerator/Directory.Build.props create mode 100644 BuildScripts/IgnoresAccessChecksToGenerator/FecSmoke/FecSmoke.csproj create mode 100644 BuildScripts/IgnoresAccessChecksToGenerator/FecSmoke/Program.cs create mode 100644 BuildScripts/IgnoresAccessChecksToGenerator/FecSmoke/RuntimeAccessorContractTests.cs create mode 100644 BuildScripts/IgnoresAccessChecksToGenerator/Generator/Generator.csproj create mode 100644 BuildScripts/IgnoresAccessChecksToGenerator/Generator/Program.cs create mode 100644 BuildScripts/IgnoresAccessChecksToGenerator/Generator/PublicizeInternals.cs create mode 100644 BuildScripts/IgnoresAccessChecksToGenerator/Generator/ReferenceVerifier.cs create mode 100644 BuildScripts/IgnoresAccessChecksToGenerator/LICENSE create mode 100644 BuildScripts/IgnoresAccessChecksToGenerator/README.md create mode 100644 BuildScripts/IgnoresAccessChecksToGenerator/build/IgnoresAccessChecksToGenerator.targets create mode 100644 BuildScripts/IgnoresAccessChecksToGenerator/build/RuntimeReferences.targets create mode 100644 BuildScripts/IgnoresAccessChecksToGenerator/replay.py create mode 100644 BuildScripts/IgnoresAccessChecksToGenerator/results.json create mode 100644 BuildScripts/IgnoresAccessChecksToGenerator/upstream/IgnoresAccessChecksToGenerator.targets create mode 100644 BuildScripts/IgnoresAccessChecksToGenerator/upstream/PublicizeInternals.cs diff --git a/BuildScripts/IgnoresAccessChecksToGenerator/.gitignore b/BuildScripts/IgnoresAccessChecksToGenerator/.gitignore new file mode 100644 index 000000000..a6ff8d5c7 --- /dev/null +++ b/BuildScripts/IgnoresAccessChecksToGenerator/.gitignore @@ -0,0 +1,4 @@ +artifacts/ +**/bin/ +**/obj/ +*.log diff --git a/BuildScripts/IgnoresAccessChecksToGenerator/Consumer/Consumer.csproj b/BuildScripts/IgnoresAccessChecksToGenerator/Consumer/Consumer.csproj new file mode 100644 index 000000000..97e1eb0dd --- /dev/null +++ b/BuildScripts/IgnoresAccessChecksToGenerator/Consumer/Consumer.csproj @@ -0,0 +1,26 @@ + + + net8.0;net10.0 + Exe + Disable + enable + enable + ../artifacts/$(TargetFramework)/private + true + $(DefineConstants);GRANT_PRIVATE_ACCESS + $(DefineConstants);MISSING_MEMBER + $(DefineConstants);WRONG_SIGNATURE + $(DefineConstants);GENERATED_ACCESS_ATTRIBUTE + + + + <_Parameter1>windows + + + $(FacadeDirectory)/System.Private.CoreLib.dll + false + + + + + diff --git a/BuildScripts/IgnoresAccessChecksToGenerator/Consumer/Program.cs b/BuildScripts/IgnoresAccessChecksToGenerator/Consumer/Program.cs new file mode 100644 index 000000000..37d985919 --- /dev/null +++ b/BuildScripts/IgnoresAccessChecksToGenerator/Consumer/Program.cs @@ -0,0 +1,61 @@ +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; + +#if GRANT_PRIVATE_ACCESS && !GENERATED_ACCESS_ATTRIBUTE +[assembly: IgnoresAccessChecksTo("System.Private.CoreLib")] +#endif + +var actual = typeof(object).Assembly.Location; +var expected = Environment.GetEnvironmentVariable("EXPECTED_CORELIB"); +if (expected != null && !string.Equals(Path.GetFullPath(actual), Path.GetFullPath(expected), StringComparison.OrdinalIgnoreCase)) + throw new Exception("Consumer executed a different runtime than the selected reference: " + actual); +if (!actual.StartsWith(System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory(), StringComparison.OrdinalIgnoreCase)) + throw new Exception("Unexpected runtime CoreLib: " + actual); +Console.WriteLine("Executing installed CoreLib: " + actual); +var method = new DynamicMethod("direct_private_access", typeof(int), Type.EmptyTypes, typeof(Program), true); +var il = (RuntimeILGenerator)method.GetILGenerator(16); +Console.WriteLine($"Original visibility: stream={typeof(RuntimeILGenerator).GetField("m_ILStream", BindingFlags.Instance | BindingFlags.NonPublic)!.Attributes}; increase={typeof(RuntimeILGenerator).GetMethod("IncreaseCapacity", BindingFlags.Instance | BindingFlags.NonPublic)!.Attributes}; nested={typeof(GC.StartNoGCRegionStatus).Attributes}"); +if (!typeof(RuntimeILGenerator).GetField("m_ILStream", BindingFlags.Instance | BindingFlags.NonPublic)!.IsPrivate || + !typeof(RuntimeILGenerator).GetMethod("IncreaseCapacity", BindingFlags.Instance | BindingFlags.NonPublic)!.IsPrivate || + !typeof(GC.StartNoGCRegionStatus).IsNestedPrivate) + throw new Exception("The probe must exercise genuinely private members/types in the installed runtime"); +#if MISSING_MEMBER +Console.WriteLine(il.FieldThatDoesNotExist); +#endif +#if WRONG_SIGNATURE +string wrong = il.m_localCount; +#endif +var oldSize = il.m_ILStream.Length; +il.IncreaseCapacity(4096); // Private method, not just an internal member. +if (il.m_ILStream.Length <= oldSize) throw new Exception("Private method did not grow the stream"); +il.m_localSignature.AddArgument(typeof(int), false); +++il.m_localCount; +var scope = ((DynamicILGenerator)il).m_scope; +scope.m_tokens.Add(typeof(Math).GetMethod(nameof(Math.Abs), new[] { typeof(int) })!.MethodHandle); +var token = (scope.m_tokens.Count - 1) | 0x06000000; +il.m_ILStream[il.m_length++] = (byte)OpCodes.Ldc_I4_S.Value; +il.m_ILStream[il.m_length++] = unchecked((byte)-42); +il.UpdateStackSize(OpCodes.Ldc_I4_S, 1); +il.m_ILStream[il.m_length++] = (byte)OpCodes.Call.Value; +for (var shift = 0; shift != 32; shift += 8) + il.m_ILStream[il.m_length++] = (byte)(token >> shift); +il.UpdateStackSize(OpCodes.Call, 0); +il.Emit(OpCodes.Stloc_0); +il.Emit(OpCodes.Ldloc_0); +il.Emit(OpCodes.Ret); +var result = ((Func)method.CreateDelegate(typeof(Func)))(); +if (result != 42) throw new Exception("Unexpected result: " + result); +Console.WriteLine("Direct private fields + private method + internal types: result=" + result); +Console.WriteLine("Private nested type: " + typeof(GC.StartNoGCRegionStatus).FullName); + +#if !GENERATED_ACCESS_ATTRIBUTE +namespace System.Runtime.CompilerServices +{ + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + internal sealed class IgnoresAccessChecksToAttribute(string assemblyName) : Attribute + { + public string AssemblyName { get; } = assemblyName; + } +} +#endif diff --git a/BuildScripts/IgnoresAccessChecksToGenerator/Directory.Build.props b/BuildScripts/IgnoresAccessChecksToGenerator/Directory.Build.props new file mode 100644 index 000000000..66325ee99 --- /dev/null +++ b/BuildScripts/IgnoresAccessChecksToGenerator/Directory.Build.props @@ -0,0 +1,5 @@ + + + latest + + diff --git a/BuildScripts/IgnoresAccessChecksToGenerator/FecSmoke/FecSmoke.csproj b/BuildScripts/IgnoresAccessChecksToGenerator/FecSmoke/FecSmoke.csproj new file mode 100644 index 000000000..2f41e7572 --- /dev/null +++ b/BuildScripts/IgnoresAccessChecksToGenerator/FecSmoke/FecSmoke.csproj @@ -0,0 +1,22 @@ + + + net8.0;net10.0 + Exe + Disable + enable + disable + $(DefineConstants);LIGHT_EXPRESSION + ../artifacts/$(TargetFramework)/private + + + + <_Parameter1>windows + + + + + + + + + diff --git a/BuildScripts/IgnoresAccessChecksToGenerator/FecSmoke/Program.cs b/BuildScripts/IgnoresAccessChecksToGenerator/FecSmoke/Program.cs new file mode 100644 index 000000000..739896d8f --- /dev/null +++ b/BuildScripts/IgnoresAccessChecksToGenerator/FecSmoke/Program.cs @@ -0,0 +1,42 @@ +using System.Runtime.CompilerServices; +#if LIGHT_EXPRESSION +using FastExpressionCompiler.LightExpression; +using FastExpressionCompiler.LightExpression.UnitTests; +using E = FastExpressionCompiler.LightExpression.Expression; +#else +using FastExpressionCompiler; +using FastExpressionCompiler.UnitTests; +using E = System.Linq.Expressions.Expression; +#endif + +[assembly: IgnoresAccessChecksTo("System.Private.CoreLib")] + +var expected = Environment.GetEnvironmentVariable("EXPECTED_CORELIB"); +if (expected != null && !string.Equals(Path.GetFullPath(typeof(object).Assembly.Location), Path.GetFullPath(expected), StringComparison.OrdinalIgnoreCase)) + throw new Exception("FEC smoke executed a different runtime than the selected reference"); +var parameter = E.Parameter(typeof(int), "value"); +var local = E.Variable(typeof(int), "local"); +var block = E.Block(new[] { local }, + E.Assign(local, E.Add(parameter, E.Constant(1))), + E.TryFinally(E.Add(local, E.Constant(1)), E.Empty())); +var lambda = E.Lambda>(block, parameter); +for (var i = 0; i < 100; ++i) +{ + var compiled = lambda.CompileFast(true); + if (compiled(40) != 42) throw new Exception("Compilation/local/exception smoke failed"); +} +var nested = E.Lambda>>(E.Lambda>(E.Add(parameter, E.Constant(2))), parameter).CompileFast(true); +if (nested(40)() != 42 || nested(50)() != 52) throw new Exception("Nested closure smoke failed"); +var tests = new TestRun(TestFlags.RethrowException); +tests.Run(new RuntimeAccessorContractTests()); +if (tests.FailedTestCount != 0) throw new Exception("Runtime contracts failed"); +Console.WriteLine($"{typeof(ExpressionCompiler).Namespace}: 100 repeated local/exception compilations, nested capture and {tests.TotalTestCount} runtime contracts passed"); + +namespace System.Runtime.CompilerServices +{ + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + internal sealed class IgnoresAccessChecksToAttribute(string assemblyName) : Attribute + { + public string AssemblyName { get; } = assemblyName; + } +} diff --git a/BuildScripts/IgnoresAccessChecksToGenerator/FecSmoke/RuntimeAccessorContractTests.cs b/BuildScripts/IgnoresAccessChecksToGenerator/FecSmoke/RuntimeAccessorContractTests.cs new file mode 100644 index 000000000..230ef8aa1 --- /dev/null +++ b/BuildScripts/IgnoresAccessChecksToGenerator/FecSmoke/RuntimeAccessorContractTests.cs @@ -0,0 +1,159 @@ +#if NET8_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +#if NET10_0_OR_GREATER +using System.Runtime.CompilerServices; +#endif + +#if LIGHT_EXPRESSION +namespace FastExpressionCompiler.LightExpression.UnitTests; +#else +namespace FastExpressionCompiler.UnitTests; +#endif + +// These are intentionally strict contracts for the CoreCLR versions exercised by CI. +// A runtime update must not silently disable an optimization while correctness tests pass. +public struct RuntimeAccessorContractTests : ITestX +{ + private const BindingFlags NonPublicStatic = BindingFlags.NonPublic | BindingFlags.Static; + private const BindingFlags InstanceMembers = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public; + + public void Run(TestRun t) + { + Local_allocation_and_generator_reuse_remain_available(t); +#if NET10_0_OR_GREATER + Accessor_declarations_match_the_runtime_members(t); + Production_accessors_bind_and_emit_a_working_method(t); +#endif + } + + public void Local_allocation_and_generator_reuse_remain_available(TestContext t) + { + var hacks = typeof(DynamicMethodHacks); + t.IsNotNull(hacks.GetField("GetNextLocalVarLocation", NonPublicStatic).GetValue(null)); + t.IsNotNull(hacks.GetField("ReuseDynamicILGenerator", NonPublicStatic).GetValue(null)); + var pool = hacks.GetField("_pooledILGenerator", NonPublicStatic); + var previous = pool.GetValue(null); + pool.SetValue(null, null); + try + { + var first = new DynamicMethod("first", typeof(int), new[] { typeof(int) }, typeof(RuntimeAccessorContractTests), true); + var il = DynamicMethodHacks.RentPooledOrNewILGenerator(first, typeof(int), new[] { typeof(int) }); + t.AreEqual(0, il.GetNextLocalVarIndex(typeof(int))); + il.BeginExceptionBlock(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, typeof(Math).GetMethod(nameof(Math.Abs), new[] { typeof(int) })); + il.Emit(OpCodes.Stloc_0); + il.BeginFinallyBlock(); + il.Emit(OpCodes.Nop); + il.EndExceptionBlock(); + il.Emit(OpCodes.Ldloc_0); + il.Emit(OpCodes.Ret); + var f = (Func)first.CreateDelegate(typeof(Func)); + t.AreEqual(42, f(-42)); + DynamicMethodHacks.FreePooledILGenerator(first, il); + + // Change the signature and local type after tokens, exception regions and locals were used. + var second = new DynamicMethod("second", typeof(string), new[] { typeof(string) }, typeof(RuntimeAccessorContractTests), true); + var reused = DynamicMethodHacks.RentPooledOrNewILGenerator(second, typeof(string), new[] { typeof(string) }); + t.IsTrue(ReferenceEquals(il, reused)); + t.AreEqual(0, reused.GetNextLocalVarIndex(typeof(string))); + reused.Emit(OpCodes.Ldarg_0); + reused.Emit(OpCodes.Stloc_0); + reused.Emit(OpCodes.Ldloc_0); + reused.Emit(OpCodes.Ret); + var g = (Func)second.CreateDelegate(typeof(Func)); + t.AreEqual("second", g("second")); + t.AreEqual(17, f(-17)); // Reuse must not change the already-created method. + DynamicMethodHacks.FreePooledILGenerator(second, reused); + } + finally + { + pool.SetValue(null, previous); + } + } + +#if NET10_0_OR_GREATER + public void Accessor_declarations_match_the_runtime_members(TestContext t) + { + var count = 0; + foreach (var accessor in typeof(DynamicMethodHacks).GetMethods(NonPublicStatic)) + { + var attribute = accessor.GetCustomAttribute(); + if (attribute == null) + continue; + ++count; + var parameters = accessor.GetParameters(); + var ownerName = parameters[0].GetCustomAttribute().TypeName; + var owner = typeof(ILGenerator).Assembly.GetType(ownerName, throwOnError: true); + // UnsafeAccessor does not search base classes. Check the exact declaring type. + if (attribute.Kind == UnsafeAccessorKind.Field) + { + var field = owner.GetField(attribute.Name, InstanceMembers | BindingFlags.DeclaredOnly); + if (field == null) + throw new MissingFieldException(owner.FullName, attribute.Name); + t.IsTrue(!field.IsStatic); + t.AreEqual(field.FieldType.MakeByRefType(), accessor.ReturnType); + t.AreEqual(1, parameters.Length); + } + else + { + t.AreEqual(UnsafeAccessorKind.Method, attribute.Kind); + var argumentTypes = new Type[parameters.Length - 1]; + for (var i = 1; i < parameters.Length; ++i) + argumentTypes[i - 1] = parameters[i].ParameterType; + var method = owner.GetMethod(attribute.Name, InstanceMembers | BindingFlags.DeclaredOnly, null, argumentTypes, null); + if (method == null) + throw new MissingMethodException(owner.FullName, attribute.Name); + t.IsTrue(!method.IsStatic); + t.AreEqual(method.ReturnType, accessor.ReturnType); + } + } + t.AreEqual(6, count); + } + + private delegate ref T FieldAccessor(object instance); + + private static T Bind(string name) where T : Delegate => + (T)typeof(DynamicMethodHacks).GetMethod(name, NonPublicStatic).CreateDelegate(typeof(T)); + + public void Production_accessors_bind_and_emit_a_working_method(TestContext t) + { + // Bind the production declarations, not duplicate UnsafeAccessor stubs in the test assembly. + var signature = Bind>("GetMLocalSignature"); + var localCount = Bind>("GetMLocalCount"); + var length = Bind>("GetMLength"); + var stream = Bind>("GetMILStream"); + var tokens = Bind>>("GetMTokens"); + var updateStack = Bind>("UpdateStackSize"); + var method = new DynamicMethod("accessors", typeof(int), Type.EmptyTypes, typeof(RuntimeAccessorContractTests), true); + var il = method.GetILGenerator(32); + t.AreEqual(0, localCount(il)); + signature(il).AddArgument(typeof(int), false); + ++localCount(il); + t.AreEqual(1, localCount(il)); + + var scope = il.GetType().GetField("m_scope", InstanceMembers).GetValue(il); + var table = tokens(scope); + table.Add(typeof(Math).GetMethod(nameof(Math.Abs), new[] { typeof(int) }).MethodHandle); + var token = (table.Count - 1) | 0x06000000; + var bytes = stream(il); + ref var position = ref length(il); + t.AreEqual(0, position); + bytes[position++] = (byte)OpCodes.Ldc_I4_S.Value; + bytes[position++] = unchecked((byte)-42); + updateStack(il, OpCodes.Ldc_I4_S, 1); + bytes[position++] = (byte)OpCodes.Call.Value; + for (var shift = 0; shift != 32; shift += 8) + bytes[position++] = (byte)(token >> shift); + updateStack(il, OpCodes.Call, 0); // Abs consumes and returns one value. + il.Emit(OpCodes.Stloc_0); + il.Emit(OpCodes.Ldloc_0); + il.Emit(OpCodes.Ret); + t.AreEqual(42, ((Func)method.CreateDelegate(typeof(Func)))()); + } +#endif +} +#endif diff --git a/BuildScripts/IgnoresAccessChecksToGenerator/Generator/Generator.csproj b/BuildScripts/IgnoresAccessChecksToGenerator/Generator/Generator.csproj new file mode 100644 index 000000000..b56604351 --- /dev/null +++ b/BuildScripts/IgnoresAccessChecksToGenerator/Generator/Generator.csproj @@ -0,0 +1,12 @@ + + + net10.0 + Exe + enable + enable + + + + + + diff --git a/BuildScripts/IgnoresAccessChecksToGenerator/Generator/Program.cs b/BuildScripts/IgnoresAccessChecksToGenerator/Generator/Program.cs new file mode 100644 index 000000000..3f178945c --- /dev/null +++ b/BuildScripts/IgnoresAccessChecksToGenerator/Generator/Program.cs @@ -0,0 +1,199 @@ +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; + +if (args.Length >= 3 && args[0] == "--verify") +{ + ReferenceVerifier.Verify(args[1], args[2], args.Contains("--private")); + return; +} +if (args.Length < 2) + throw new ArgumentException("Usage: Generator input-implementation.dll output-reference.dll [--private]"); +ReferencePublicizer.Generate(args[0], args[1], args.Contains("--private")); + +internal static class ReferencePublicizer +{ + public static void Generate(string input, string output, bool includePrivate) + { + using var file = File.OpenRead(input); + using var pe = new PEReader(file); + var r = pe.GetMetadataReader(); + if (r.GetString(r.GetAssemblyDefinition().Name) != "System.Private.CoreLib") + throw new NotSupportedException("This local prototype currently supports CoreLib only, not arbitrary package assemblies."); + var b = new MetadataBuilder(); + StringHandle S(StringHandle h) => h.IsNil ? default : b.GetOrAddString(r.GetString(h)); + BlobHandle B(BlobHandle h) => h.IsNil ? default : b.GetOrAddBlob(r.GetBlobBytes(h)); + GuidHandle G(GuidHandle h) => h.IsNil ? default : b.GetOrAddGuid(r.GetGuid(h)); + + var module = r.GetModuleDefinition(); + b.AddModule(module.Generation, S(module.Name), G(module.Mvid), G(module.GenerationId), G(module.BaseGenerationId)); + var assembly = r.GetAssemblyDefinition(); + b.AddAssembly(S(assembly.Name), assembly.Version, S(assembly.Culture), B(assembly.PublicKey), assembly.Flags, assembly.HashAlgorithm); + foreach (var h in r.AssemblyReferences) + { + var v = r.GetAssemblyReference(h); + b.AddAssemblyReference(S(v.Name), v.Version, S(v.Culture), B(v.PublicKeyOrToken), v.Flags, B(v.HashValue)); + } + foreach (var h in r.TypeReferences) + { + var v = r.GetTypeReference(h); + b.AddTypeReference(v.ResolutionScope, S(v.Namespace), S(v.Name)); + } + // Preserve row numbers in all signature-addressable tables; signature blobs can then be copied unchanged. + for (var i = 1; i <= r.GetTableRowCount(TableIndex.TypeSpec); ++i) + b.AddTypeSpecification(B(r.GetTypeSpecification(MetadataTokens.TypeSpecificationHandle(i)).Signature)); + for (var i = 1; i <= r.GetTableRowCount(TableIndex.ModuleRef); ++i) + b.AddModuleReference(S(r.GetModuleReference(MetadataTokens.ModuleReferenceHandle(i)).Name)); + foreach (var h in r.MemberReferences) + { + var v = r.GetMemberReference(h); + b.AddMemberReference(v.Parent, S(v.Name), B(v.Signature)); + } + for (var i = 1; i <= r.GetTableRowCount(TableIndex.MethodSpec); ++i) + { + var v = r.GetMethodSpecification(MetadataTokens.MethodSpecificationHandle(i)); + b.AddMethodSpecification(v.Method, B(v.Signature)); + } + + var fieldRow = 1; + var methodRow = 1; + MethodDefinitionHandle referenceAttributeCtor = default; + foreach (var h in r.TypeDefinitions) + { + var v = r.GetTypeDefinition(h); + var attributes = v.Attributes; + var visibility = attributes & TypeAttributes.VisibilityMask; + if (r.GetString(v.Name) != "" && + (includePrivate || visibility is TypeAttributes.NotPublic or TypeAttributes.NestedAssembly or TypeAttributes.NestedFamORAssem or TypeAttributes.NestedFamANDAssem)) + attributes = (attributes & ~TypeAttributes.VisibilityMask) | (v.IsNested ? TypeAttributes.NestedPublic : TypeAttributes.Public); + b.AddTypeDefinition(attributes, S(v.Namespace), S(v.Name), v.BaseType, + MetadataTokens.FieldDefinitionHandle(fieldRow), MetadataTokens.MethodDefinitionHandle(methodRow)); + fieldRow += v.GetFields().Count; + methodRow += v.GetMethods().Count; + if (v.IsNested) + b.AddNestedType(h, v.GetDeclaringType()); + var layout = v.GetLayout(); + if (!layout.IsDefault) + b.AddTypeLayout(h, (ushort)layout.PackingSize, (uint)layout.Size); + foreach (var ih in v.GetInterfaceImplementations()) + b.AddInterfaceImplementation(h, r.GetInterfaceImplementation(ih).Interface); + foreach (var mh in v.GetMethodImplementations()) + { + var implementation = r.GetMethodImplementation(mh); + b.AddMethodImplementation(h, implementation.MethodBody, implementation.MethodDeclaration); + } + if (r.GetString(v.Namespace) == "System.Runtime.CompilerServices" && r.GetString(v.Name) == "ReferenceAssemblyAttribute") + referenceAttributeCtor = v.GetMethods().First(m => r.GetString(r.GetMethodDefinition(m).Name) == ".ctor"); + } + foreach (var h in r.FieldDefinitions) + { + var v = r.GetFieldDefinition(h); + var attributes = v.Attributes & ~FieldAttributes.HasFieldRVA; + var access = attributes & FieldAttributes.FieldAccessMask; + if (includePrivate || access is FieldAttributes.Assembly or FieldAttributes.FamORAssem or FieldAttributes.FamANDAssem) + attributes = (attributes & ~FieldAttributes.FieldAccessMask) | FieldAttributes.Public; + b.AddFieldDefinition(attributes, S(v.Name), B(v.Signature)); + var offset = v.GetOffset(); + if (offset >= 0) + b.AddFieldLayout(h, offset); + var marshal = v.GetMarshallingDescriptor(); + if (!marshal.IsNil) + b.AddMarshallingDescriptor(h, B(marshal)); + } + var paramRow = 1; + foreach (var h in r.MethodDefinitions) + { + var v = r.GetMethodDefinition(h); + var attributes = v.Attributes; + var access = attributes & MethodAttributes.MemberAccessMask; + if (includePrivate || access is MethodAttributes.Assembly or MethodAttributes.FamORAssem or MethodAttributes.FamANDAssem) + attributes = (attributes & ~MethodAttributes.MemberAccessMask) | MethodAttributes.Public; + b.AddMethodDefinition(attributes, v.ImplAttributes, S(v.Name), B(v.Signature), -1, MetadataTokens.ParameterHandle(paramRow)); + foreach (var ph in v.GetParameters()) + { + var p = r.GetParameter(ph); + b.AddParameter(p.Attributes, S(p.Name), p.SequenceNumber); + var marshal = p.GetMarshallingDescriptor(); + if (!marshal.IsNil) + b.AddMarshallingDescriptor(ph, B(marshal)); + ++paramRow; + } + var import = v.GetImport(); + if (!import.Module.IsNil) + b.AddMethodImport(h, import.Attributes, S(import.Name), import.Module); + } + for (var i = 1; i <= r.GetTableRowCount(TableIndex.GenericParam); ++i) + { + var v = r.GetGenericParameter(MetadataTokens.GenericParameterHandle(i)); + b.AddGenericParameter(v.Parent, v.Attributes, S(v.Name), v.Index); + } + for (var i = 1; i <= r.GetTableRowCount(TableIndex.GenericParamConstraint); ++i) + { + var v = r.GetGenericParameterConstraint(MetadataTokens.GenericParameterConstraintHandle(i)); + b.AddGenericParameterConstraint(v.Parameter, v.Type); + } + foreach (var h in r.PropertyDefinitions) + { + var v = r.GetPropertyDefinition(h); + b.AddProperty(v.Attributes, S(v.Name), B(v.Signature)); + var a = v.GetAccessors(); + if (!a.Getter.IsNil) b.AddMethodSemantics(h, MethodSemanticsAttributes.Getter, a.Getter); + if (!a.Setter.IsNil) b.AddMethodSemantics(h, MethodSemanticsAttributes.Setter, a.Setter); + foreach (var m in a.Others) b.AddMethodSemantics(h, MethodSemanticsAttributes.Other, m); + } + foreach (var h in r.EventDefinitions) + { + var v = r.GetEventDefinition(h); + b.AddEvent(v.Attributes, S(v.Name), v.Type); + var a = v.GetAccessors(); + if (!a.Adder.IsNil) b.AddMethodSemantics(h, MethodSemanticsAttributes.Adder, a.Adder); + if (!a.Remover.IsNil) b.AddMethodSemantics(h, MethodSemanticsAttributes.Remover, a.Remover); + if (!a.Raiser.IsNil) b.AddMethodSemantics(h, MethodSemanticsAttributes.Raiser, a.Raiser); + foreach (var m in a.Others) b.AddMethodSemantics(h, MethodSemanticsAttributes.Other, m); + } + foreach (var h in r.TypeDefinitions) + { + var v = r.GetTypeDefinition(h); + if (v.GetProperties().Count != 0) b.AddPropertyMap(h, v.GetProperties().First()); + if (v.GetEvents().Count != 0) b.AddEventMap(h, v.GetEvents().First()); + } + for (var i = 1; i <= r.GetTableRowCount(TableIndex.Constant); ++i) + { + var v = r.GetConstant(MetadataTokens.ConstantHandle(i)); + var blob = r.GetBlobReader(v.Value); + b.AddConstant(v.Parent, blob.ReadConstant(v.TypeCode)); + } + for (var i = 1; i <= r.GetTableRowCount(TableIndex.DeclSecurity); ++i) + { + var v = r.GetDeclarativeSecurityAttribute(MetadataTokens.DeclarativeSecurityAttributeHandle(i)); + b.AddDeclarativeSecurityAttribute(v.Parent, v.Action, B(v.PermissionSet)); + } + foreach (var h in r.CustomAttributes) + { + var v = r.GetCustomAttribute(h); + b.AddCustomAttribute(v.Parent, v.Constructor, B(v.Value)); + } + if (referenceAttributeCtor.IsNil) + throw new NotSupportedException("Prototype currently requires ReferenceAssemblyAttribute in the source assembly (CoreLib)."); + b.AddCustomAttribute(MetadataTokens.EntityHandle(TableIndex.Assembly, 1), referenceAttributeCtor, b.GetOrAddBlob(new byte[] { 1, 0, 0, 0 })); + // A new IL-only image: no ReadyToRun/native sections, method bodies, or executable resources are copied. + var builder = new ManagedPEBuilder(new PEHeaderBuilder(imageCharacteristics: Characteristics.ExecutableImage | Characteristics.Dll), + new MetadataRootBuilder(b), new BlobBuilder(), flags: CorFlags.ILOnly); + var image = new BlobBuilder(); + builder.Serialize(image); + Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(output))!); + using var destination = File.Create(output); + image.WriteContentTo(destination); + Console.WriteLine($"{r.GetString(assembly.Name)} {assembly.Version}: {r.TypeDefinitions.Count} types, {r.FieldDefinitions.Count} fields, {r.MethodDefinitions.Count} methods; private={includePrivate}; {destination.Length:N0} bytes"); + var references = new List(); + foreach (var path in Directory.EnumerateFiles(Path.GetDirectoryName(Path.GetFullPath(input))!, "*.dll").Order()) + { + using var source = File.OpenRead(path); + using var runtimeImage = new PEReader(source); + if (runtimeImage.HasMetadata) + references.Add(Path.GetFullPath(path) == Path.GetFullPath(input) ? Path.GetFullPath(output) : Path.GetFullPath(path)); + } + File.WriteAllLines(Path.Combine(Path.GetDirectoryName(Path.GetFullPath(output))!, "runtime-references.txt"), references); + } +} diff --git a/BuildScripts/IgnoresAccessChecksToGenerator/Generator/PublicizeInternals.cs b/BuildScripts/IgnoresAccessChecksToGenerator/Generator/PublicizeInternals.cs new file mode 100644 index 000000000..60f5f9cb6 --- /dev/null +++ b/BuildScripts/IgnoresAccessChecksToGenerator/Generator/PublicizeInternals.cs @@ -0,0 +1,45 @@ +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace IgnoresAccessChecksToGenerator.Tasks; + +// Experimental replacement backend for the upstream task, using only System.Reflection.Metadata. +// Unlike upstream this prototype is .NET-10-MSBuild/CoreLib-only and returns the matching reference universe. +public sealed class PublicizeInternals : Microsoft.Build.Utilities.Task +{ + [Required] public string ImplementationAssembly { get; set; } = ""; + [Required] public string IntermediateOutputPath { get; set; } = ""; + [Required] public string GeneratedCodeFilePath { get; set; } = ""; + public bool IncludePrivateMembers { get; set; } + [Output] public ITaskItem[] ReferencePaths { get; set; } = []; + + public override bool Execute() + { + try + { + var output = Path.Combine(Path.GetFullPath(IntermediateOutputPath), "System.Private.CoreLib.dll"); + ReferencePublicizer.Generate(ImplementationAssembly, output, IncludePrivateMembers); + ReferenceVerifier.Verify(ImplementationAssembly, output, IncludePrivateMembers); + ReferencePaths = File.ReadAllLines(Path.Combine(Path.GetDirectoryName(output)!, "runtime-references.txt")) + .Select(path => (ITaskItem)new TaskItem(path)).ToArray(); + Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(GeneratedCodeFilePath))!); + File.WriteAllText(GeneratedCodeFilePath, """ + [assembly: System.Runtime.CompilerServices.IgnoresAccessChecksTo("System.Private.CoreLib")] + namespace System.Runtime.CompilerServices + { + [System.AttributeUsage(System.AttributeTargets.Assembly, AllowMultiple = true)] + internal sealed class IgnoresAccessChecksToAttribute(string assemblyName) : System.Attribute + { + public string AssemblyName { get; } = assemblyName; + } + } + """); + return true; + } + catch (Exception error) + { + Log.LogErrorFromException(error, true); + return false; + } + } +} diff --git a/BuildScripts/IgnoresAccessChecksToGenerator/Generator/ReferenceVerifier.cs b/BuildScripts/IgnoresAccessChecksToGenerator/Generator/ReferenceVerifier.cs new file mode 100644 index 000000000..35a28460a --- /dev/null +++ b/BuildScripts/IgnoresAccessChecksToGenerator/Generator/ReferenceVerifier.cs @@ -0,0 +1,59 @@ +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; + +internal static class ReferenceVerifier +{ + public static void Verify(string input, string output, bool includePrivate) + { + using var source = new PEReader(File.OpenRead(input)); + using var facade = new PEReader(File.OpenRead(output)); + var a = source.GetMetadataReader(); + var b = facade.GetMetadataReader(); + static void Require(bool condition, string message) + { + if (!condition) throw new InvalidDataException(message); + } + var aa = a.GetAssemblyDefinition(); + var ba = b.GetAssemblyDefinition(); + Require(a.GetString(aa.Name) == b.GetString(ba.Name) && a.GetString(aa.Culture) == b.GetString(ba.Culture) && aa.Version == ba.Version && aa.Flags == ba.Flags && + a.GetBlobBytes(aa.PublicKey).SequenceEqual(b.GetBlobBytes(ba.PublicKey)), "Assembly identity changed"); + Require(facade.PEHeaders.CorHeader!.Flags.HasFlag(CorFlags.ILOnly), "Facade must be IL-only"); + Require(facade.PEHeaders.CorHeader.ManagedNativeHeaderDirectory.Size == 0, "Native implementation leaked into facade"); + Require(a.TypeDefinitions.Count == b.TypeDefinitions.Count && a.FieldDefinitions.Count == b.FieldDefinitions.Count && + a.MethodDefinitions.Count == b.MethodDefinitions.Count, "Definition row counts changed"); + foreach (var handle in a.TypeDefinitions) + { + var x = a.GetTypeDefinition(handle); + var y = b.GetTypeDefinition(handle); + Require(a.GetString(x.Name) == b.GetString(y.Name) && a.GetString(x.Namespace) == b.GetString(y.Namespace) && x.BaseType == y.BaseType && + x.GetDeclaringType() == y.GetDeclaringType() && x.GetFields().SequenceEqual(y.GetFields()) && x.GetMethods().SequenceEqual(y.GetMethods()), "Type token/range changed"); + if (includePrivate && a.GetString(x.Name) != "") + Require((y.Attributes & TypeAttributes.VisibilityMask) == (y.IsNested ? TypeAttributes.NestedPublic : TypeAttributes.Public), "Type not publicized"); + } + foreach (var handle in a.FieldDefinitions) + { + var x = a.GetFieldDefinition(handle); + var y = b.GetFieldDefinition(handle); + Require(a.GetString(x.Name) == b.GetString(y.Name) && a.GetBlobBytes(x.Signature).SequenceEqual(b.GetBlobBytes(y.Signature)), "Field name/signature changed"); + if (includePrivate) Require((y.Attributes & FieldAttributes.FieldAccessMask) == FieldAttributes.Public, "Private field not exposed"); + } + foreach (var handle in a.MethodDefinitions) + { + var x = a.GetMethodDefinition(handle); + var y = b.GetMethodDefinition(handle); + Require(a.GetString(x.Name) == b.GetString(y.Name) && a.GetBlobBytes(x.Signature).SequenceEqual(b.GetBlobBytes(y.Signature)) && + x.GetParameters().SequenceEqual(y.GetParameters()), "Method name/signature/parameter range changed"); + Require(y.RelativeVirtualAddress == 0, "Executable body leaked into facade"); + if (includePrivate) Require((y.Attributes & MethodAttributes.MemberAccessMask) == MethodAttributes.Public, "Private method not exposed"); + } + var refAttribute = b.CustomAttributes.Any(h => + { + var attr = b.GetCustomAttribute(h); + return attr.Parent.Kind == HandleKind.AssemblyDefinition && attr.Constructor.Kind == HandleKind.MethodDefinition && + b.GetString(b.GetTypeDefinition(b.GetMethodDefinition((MethodDefinitionHandle)attr.Constructor).GetDeclaringType()).Name) == "ReferenceAssemblyAttribute"; + }); + Require(refAttribute, "Missing ReferenceAssemblyAttribute"); + Console.WriteLine("Verified identity, IL-only/reference marker, all definition tokens/ranges/signatures and absence of method bodies"); + } +} diff --git a/BuildScripts/IgnoresAccessChecksToGenerator/LICENSE b/BuildScripts/IgnoresAccessChecksToGenerator/LICENSE new file mode 100644 index 000000000..25cb8ca2f --- /dev/null +++ b/BuildScripts/IgnoresAccessChecksToGenerator/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Eli Arbel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/BuildScripts/IgnoresAccessChecksToGenerator/README.md b/BuildScripts/IgnoresAccessChecksToGenerator/README.md new file mode 100644 index 000000000..97374380b --- /dev/null +++ b/BuildScripts/IgnoresAccessChecksToGenerator/README.md @@ -0,0 +1,57 @@ +# Local metadata publicizer experiment for #560 + +**Draft follow-up experiment to #567, for #560.** Branch `experiment/metadata-private-access-560`, based on FEC master `734314ede7c5e58845d42345e1cfb4d59591b998`. It is not wired into FEC's production build or solution. + +This is a local source/backend fork experiment based on [IgnoresAccessChecksToGenerator](https://github.com/aelij/IgnoresAccessChecksToGenerator), upstream revision `882b30bf82fbb72f3bee8878ad9fd31f5c93c875`. The original task/targets snapshots are in `upstream/`, under the retained MIT `LICENSE`. The experimental backend replaces Cecil with `System.Reflection.Metadata` / `System.Reflection.PortableExecutable`, plus an MSBuild task. The compiled generator has no Cecil or NuGet package dependency. This is **not yet a drop-in replacement for the whole upstream NuGet package**. + +## What works + +The backend reads the installed CoreLib with `PEReader`, copies compiler-relevant metadata into `MetadataBuilder`, and writes a **new IL-only reference assembly** with `ManagedPEBuilder` and `ReferenceAssemblyAttribute`. It does not rewrite the runtime executable image. Assembly identity, type/field/method tokens, signature blobs, generic definitions, nested relationships and relevant member metadata are retained. Method bodies, native sections, RVA field data and embedded resources are not copied. + +Default exposure matches the upstream internal-member policy. `--private` additionally exposes private fields, methods and nested types (and the remaining accessibilities) in the generated compiler reference. The actual runtime members remain private; the consumer's `IgnoresAccessChecksTo` attribute authorizes access at runtime. + +Both .NET 8.0.28 and .NET 10.0.9 on Windows ARM64 pass: + +- C# compilation and execution using `RuntimeILGenerator`, genuine private fields `m_ILStream`/`m_length`, private method `IncreaseCapacity`, and private nested enum `GC.StartNoGCRegionStatus`. Runtime reflection confirms these are genuinely private in the installed assembly. The directly emitted method returns 42. +- MSBuild integration: the new `PublicizeInternals` task generates the reference and access attribute, then supplies compiler references. The same consumer compiles and executes. +- Compiling the real Standard and LightExpression FEC source in an isolated host against the generated references. Only an ignored source snapshot changes the local-allocation helper to use direct typed field access on .NET 8/10; production source files are untouched. Each configuration passes 100 repeated local/try-finally compilations, independent nested captures, and the runtime-contract tests from PR #567 (1 per API on .NET 8, 3 per API on .NET 10). +- Disabling private exposure fails compilation; removing the access attribute fails with `TypeAccessException`; a nonexistent field fails compilation (`CS1061`); using the wrong field type fails compilation (`CS0029`). These are expected failures checked by the replay script. +- All 25 replay steps pass. The installed CoreLib's SHA-256 is unchanged before/after. The generated CoreLib is not copied into consumer output directories. See `results.json` for commands, exit codes and input hashes; logs are in ignored `artifacts/logs/`. + +An exploratory `Array.ArrayAssignType` probe compiled on .NET 10 but failed on .NET 8 because that nested type is absent there. The retained cross-version probe uses `GC.StartNoGCRegionStatus`, verified private on both. This illustrates why checks must run against each intended runtime, rather than assuming their internals match. + +## Important build constraint + +Adding a full CoreLib facade alongside SDK reference assemblies causes duplicate core-type definitions: SDK `System.Runtime` defines types that the implementation puts in CoreLib. This prototype therefore uses one consistent set of **managed implementation references from the explicitly selected runtime**, replacing CoreLib with the generated reference. Native DLLs are excluded using `PEReader.HasMetadata`. + +This is deliberately a runtime-version/OS-specific build experiment, not a portable targeting pack. Compiler checks validate the selected implementation version, not every runtime a published FEC package could encounter. Windows implementation references also carry Windows-specific platform annotations; the sample consumers explicitly declare their Windows platform when built on Windows. No reference assembly is substituted at execution time. + +## Replay + +Requires the .NET 10 SDK, Python, and the installed .NET 8.0.28 / 10.0.9 runtimes. From this directory: + +```sh +python replay.py +``` + +The script discovers the two exact installed runtime paths, builds the generator, verifies metadata and positive/negative behavior, runs the FEC smoke configurations, and records `results.json`. Consumer runtime versions are pinned with roll-forward disabled, and the executing CoreLib path must match the selected input. No git commit/push, network publication, runtime replacement or machine configuration change is performed. Build outputs/logs are ignored. The FEC smoke fixture is copied from PR #567 commit `ff41665593d64d18196aed3f202ef1a1944a4e4f` and compiled locally, so replay does not need that commit available as an object. + +CLI use: + +```sh +dotnet run --project Generator/Generator.csproj -c Release -- path/to/System.Private.CoreLib.dll artifacts/custom/System.Private.CoreLib.dll --private +dotnet run --project Generator/Generator.csproj -c Release -- --verify path/to/System.Private.CoreLib.dll artifacts/custom/System.Private.CoreLib.dll --private +``` + +## What is not established + +- No compilation/startup/allocation/runtime speedup has been measured. Typed access is feasible; that alone does not prove it beats UnsafeAccessor or generated helpers. +- The task currently requires .NET 10 MSBuild and supports CoreLib only. Arbitrary packages, upstream exclusion/legacy options, older MSBuild hosts, packaging and incremental-generation caching are not implemented. +- No cross-platform, x64, Framework 4.7.2, preview-runtime, or complete compatibility-suite claim is made. These are focused compile/runtime and metadata checks, not a full FEC regression run. +- The verifier checks assembly identity, the reference/IL-only marker, definition tokens/ranges/signatures, visibility and absence of executable method bodies. This is not a general metadata-rewriter certification. + +## Merge recommendation + +**Keep this as a draft; do not merge the current prototype as a production solution for #560.** It establishes feasibility but adds a runtime-specific compiler-reference strategy without an established performance benefit. It also lacks cross-platform/x64 coverage and the validation needed for a general metadata writer. + +Next useful experiments are a narrower exposure policy/reference strategy and measured direct typed reuse/local-allocation helpers versus FEC's existing helpers, including build-time cost. Production integration should wait for those results and a decision on runtime-version support. The smaller runtime-contract tests in #567 can be reviewed independently; this draft does not replace or modify that PR. diff --git a/BuildScripts/IgnoresAccessChecksToGenerator/build/IgnoresAccessChecksToGenerator.targets b/BuildScripts/IgnoresAccessChecksToGenerator/build/IgnoresAccessChecksToGenerator.targets new file mode 100644 index 000000000..f73bdd1cd --- /dev/null +++ b/BuildScripts/IgnoresAccessChecksToGenerator/build/IgnoresAccessChecksToGenerator.targets @@ -0,0 +1,26 @@ + + + <_MetadataTaskAssembly>$(MSBuildThisFileDirectory)../Generator/bin/Release/net10.0/Generator.dll + + + + + <_MetadataOutput>$(IntermediateOutputPath)PrivateRuntimeReferences + <_MetadataAttributeFile>$(_MetadataOutput)/IgnoresAccessChecksTo.g.cs + + + + + + + + + + + + + + diff --git a/BuildScripts/IgnoresAccessChecksToGenerator/build/RuntimeReferences.targets b/BuildScripts/IgnoresAccessChecksToGenerator/build/RuntimeReferences.targets new file mode 100644 index 000000000..61648518a --- /dev/null +++ b/BuildScripts/IgnoresAccessChecksToGenerator/build/RuntimeReferences.targets @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + diff --git a/BuildScripts/IgnoresAccessChecksToGenerator/replay.py b/BuildScripts/IgnoresAccessChecksToGenerator/replay.py new file mode 100644 index 000000000..0245eb382 --- /dev/null +++ b/BuildScripts/IgnoresAccessChecksToGenerator/replay.py @@ -0,0 +1,89 @@ +"""Local-only, pinned-runtime experiment. Does not modify installed runtimes, commit, push or publish.""" +import hashlib +import json +import os +import pathlib +import re +import subprocess + +root = pathlib.Path(__file__).resolve().parent +repo = root.parents[1] +artifacts = root / "artifacts" +logs = artifacts / "logs" +logs.mkdir(parents=True, exist_ok=True) +results = [] +child_environment = os.environ.copy() + + +def run(label, arguments, expected=0, contains=None): + completed = subprocess.run(arguments, cwd=root, env=child_environment, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + output = completed.stdout.decode("utf-8", errors="replace") + (logs / (label + ".log")).write_text(output, encoding="utf-8") + ok = (completed.returncode == 0 if expected == 0 else completed.returncode != 0) + if contains is not None: + ok = ok and contains in output + results.append({"label": label, "arguments": arguments, "exit_code": completed.returncode, + "expected": "success" if expected == 0 else "failure", "passed": ok}) + print(label + ": " + ("PASS" if ok else "FAIL"), flush=True) + if not ok: + print(output[-6000:]) + raise RuntimeError(label) + + +def digest(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +version = subprocess.check_output(["dotnet", "--version"], text=True).strip() +current = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=repo, text=True).strip() +installed = subprocess.check_output(["dotnet", "--list-runtimes"], text=True) +paths = {v: pathlib.Path(p) / v / "System.Private.CoreLib.dll" + for v, p in re.findall(r"Microsoft.NETCore.App (\S+) \[([^\]]+)\]", installed)} +selected = {"net8.0": "8.0.28", "net10.0": "10.0.9"} + +# Keep the real FEC source untouched. Transform only a throwaway compilation snapshot. +source = (repo / "src/FastExpressionCompiler/FastExpressionCompiler.cs").read_text(encoding="utf-8") +old = "#if NET10_0_OR_GREATER\n // In .NET 10+, use UnsafeAccessorType" +assert source.count(old) == 1 +source = source.replace(old, "#if NET8_0_OR_GREATER\n // Local prototype: direct typed access through a metadata facade", 1) +old = "GetMLocalSignature(il).AddArgument(t, false);\n return PostInc(ref GetMLocalCount(il));" +assert source.count(old) == 1 +source = source.replace(old, "((System.Reflection.Emit.RuntimeILGenerator)il).m_localSignature.AddArgument(t, false);\n return PostInc(ref ((System.Reflection.Emit.RuntimeILGenerator)il).m_localCount);", 1) +(artifacts / "Compiler.Direct.cs").write_bytes(source.encode("utf-8")) + +run("generator-build", ["dotnet", "build", "Generator/Generator.csproj", "-c", "Release"]) +runtime_hashes = {} +try: + for tfm, runtime in selected.items(): + corelib = paths[runtime] + child_environment["EXPECTED_CORELIB"] = str(corelib) + original_hash = digest(corelib) + runtime_hashes[runtime] = {"implementation": str(corelib), "sha256_before": original_hash} + private = artifacts / tfm / "private" / "System.Private.CoreLib.dll" + internal = artifacts / tfm / "internal" / "System.Private.CoreLib.dll" + generator = ["dotnet", str(root / "Generator/bin/Release/net10.0/Generator.dll")] + run(tfm + "-generate-private", generator + [str(corelib), str(private), "--private"]) + run(tfm + "-verify-private", generator + ["--verify", str(corelib), str(private), "--private"]) + run(tfm + "-generate-internal", generator + [str(corelib), str(internal)]) + consumer = ["dotnet", "run", "--project", "Consumer/Consumer.csproj", "-c", "Release", "-f", tfm, + "-p:RuntimeFrameworkVersion=" + runtime] + run(tfm + "-consumer", consumer, contains="result=42") + run(tfm + "-task", consumer + ["-p:UseGeneratorTask=true", "-p:RuntimeCoreLib=" + str(corelib)], contains="result=42") + run(tfm + "-private-disabled", consumer + ["-p:FacadeDirectory=" + str(internal.parent)], expected=1, contains="error CS") + run(tfm + "-permission-disabled", consumer + ["-p:GrantPrivateAccess=false"], expected=1, contains="TypeAccessException") + run(tfm + "-missing-member", consumer + ["-p:MissingMemberProbe=true"], expected=1, contains="CS1061") + run(tfm + "-wrong-signature", consumer + ["-p:WrongSignatureProbe=true"], expected=1, contains="CS0029") + for light in (False, True): + run(tfm + ("-light" if light else "-standard"), + ["dotnet", "run", "--project", "FecSmoke/FecSmoke.csproj", "-c", "Release", "-f", tfm, + "-p:RuntimeFrameworkVersion=" + runtime, "-p:Light=" + str(light).lower()], contains="runtime contracts passed") + # Finish with a successful consumer output, not a deliberately broken negative-test build. + run(tfm + "-consumer-final", consumer, contains="result=42") + runtime_hashes[runtime]["sha256_after"] = digest(corelib) + assert runtime_hashes[runtime]["sha256_after"] == original_hash, "Installed runtime changed" + assert not (root / "Consumer/bin/Release" / tfm / "System.Private.CoreLib.dll").exists(), "Facade leaked into output" +finally: + (root / "results.json").write_bytes((json.dumps({"sdk": version, "fec_base": current, + "upstream_generator": "882b30bf82fbb72f3bee8878ad9fd31f5c93c875", "runtimes": runtime_hashes, + "results": results}, indent=2) + "\n").encode("utf-8")) +print(f"All {len(results)} checks passed; installed runtime hashes unchanged; no runtime DLL copied to consumer outputs") diff --git a/BuildScripts/IgnoresAccessChecksToGenerator/results.json b/BuildScripts/IgnoresAccessChecksToGenerator/results.json new file mode 100644 index 000000000..0c1c387b2 --- /dev/null +++ b/BuildScripts/IgnoresAccessChecksToGenerator/results.json @@ -0,0 +1,432 @@ +{ + "sdk": "10.0.301", + "fec_base": "734314ede7c5e58845d42345e1cfb4d59591b998", + "upstream_generator": "882b30bf82fbb72f3bee8878ad9fd31f5c93c875", + "runtimes": { + "8.0.28": { + "implementation": "C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\8.0.28\\System.Private.CoreLib.dll", + "sha256_before": "1326005d3c6c4dfc129c5b0b21138b4053f45294276be22c5beefa478707323a", + "sha256_after": "1326005d3c6c4dfc129c5b0b21138b4053f45294276be22c5beefa478707323a" + }, + "10.0.9": { + "implementation": "C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\10.0.9\\System.Private.CoreLib.dll", + "sha256_before": "a0b776c3065a323080ed88c2fcce2b3711741346931618428bd1e1ad9144c931", + "sha256_after": "a0b776c3065a323080ed88c2fcce2b3711741346931618428bd1e1ad9144c931" + } + }, + "results": [ + { + "label": "generator-build", + "arguments": [ + "dotnet", + "build", + "Generator/Generator.csproj", + "-c", + "Release" + ], + "exit_code": 0, + "expected": "success", + "passed": true + }, + { + "label": "net8.0-generate-private", + "arguments": [ + "dotnet", + "C:\\Data\\Git\\Github\\FastExpressionCompiler\\BuildScripts\\IgnoresAccessChecksToGenerator\\Generator\\bin\\Release\\net10.0\\Generator.dll", + "C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\8.0.28\\System.Private.CoreLib.dll", + "C:\\Data\\Git\\Github\\FastExpressionCompiler\\BuildScripts\\IgnoresAccessChecksToGenerator\\artifacts\\net8.0\\private\\System.Private.CoreLib.dll", + "--private" + ], + "exit_code": 0, + "expected": "success", + "passed": true + }, + { + "label": "net8.0-verify-private", + "arguments": [ + "dotnet", + "C:\\Data\\Git\\Github\\FastExpressionCompiler\\BuildScripts\\IgnoresAccessChecksToGenerator\\Generator\\bin\\Release\\net10.0\\Generator.dll", + "--verify", + "C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\8.0.28\\System.Private.CoreLib.dll", + "C:\\Data\\Git\\Github\\FastExpressionCompiler\\BuildScripts\\IgnoresAccessChecksToGenerator\\artifacts\\net8.0\\private\\System.Private.CoreLib.dll", + "--private" + ], + "exit_code": 0, + "expected": "success", + "passed": true + }, + { + "label": "net8.0-generate-internal", + "arguments": [ + "dotnet", + "C:\\Data\\Git\\Github\\FastExpressionCompiler\\BuildScripts\\IgnoresAccessChecksToGenerator\\Generator\\bin\\Release\\net10.0\\Generator.dll", + "C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\8.0.28\\System.Private.CoreLib.dll", + "C:\\Data\\Git\\Github\\FastExpressionCompiler\\BuildScripts\\IgnoresAccessChecksToGenerator\\artifacts\\net8.0\\internal\\System.Private.CoreLib.dll" + ], + "exit_code": 0, + "expected": "success", + "passed": true + }, + { + "label": "net8.0-consumer", + "arguments": [ + "dotnet", + "run", + "--project", + "Consumer/Consumer.csproj", + "-c", + "Release", + "-f", + "net8.0", + "-p:RuntimeFrameworkVersion=8.0.28" + ], + "exit_code": 0, + "expected": "success", + "passed": true + }, + { + "label": "net8.0-task", + "arguments": [ + "dotnet", + "run", + "--project", + "Consumer/Consumer.csproj", + "-c", + "Release", + "-f", + "net8.0", + "-p:RuntimeFrameworkVersion=8.0.28", + "-p:UseGeneratorTask=true", + "-p:RuntimeCoreLib=C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\8.0.28\\System.Private.CoreLib.dll" + ], + "exit_code": 0, + "expected": "success", + "passed": true + }, + { + "label": "net8.0-private-disabled", + "arguments": [ + "dotnet", + "run", + "--project", + "Consumer/Consumer.csproj", + "-c", + "Release", + "-f", + "net8.0", + "-p:RuntimeFrameworkVersion=8.0.28", + "-p:FacadeDirectory=C:\\Data\\Git\\Github\\FastExpressionCompiler\\BuildScripts\\IgnoresAccessChecksToGenerator\\artifacts\\net8.0\\internal" + ], + "exit_code": 1, + "expected": "failure", + "passed": true + }, + { + "label": "net8.0-permission-disabled", + "arguments": [ + "dotnet", + "run", + "--project", + "Consumer/Consumer.csproj", + "-c", + "Release", + "-f", + "net8.0", + "-p:RuntimeFrameworkVersion=8.0.28", + "-p:GrantPrivateAccess=false" + ], + "exit_code": 3762504530, + "expected": "failure", + "passed": true + }, + { + "label": "net8.0-missing-member", + "arguments": [ + "dotnet", + "run", + "--project", + "Consumer/Consumer.csproj", + "-c", + "Release", + "-f", + "net8.0", + "-p:RuntimeFrameworkVersion=8.0.28", + "-p:MissingMemberProbe=true" + ], + "exit_code": 1, + "expected": "failure", + "passed": true + }, + { + "label": "net8.0-wrong-signature", + "arguments": [ + "dotnet", + "run", + "--project", + "Consumer/Consumer.csproj", + "-c", + "Release", + "-f", + "net8.0", + "-p:RuntimeFrameworkVersion=8.0.28", + "-p:WrongSignatureProbe=true" + ], + "exit_code": 1, + "expected": "failure", + "passed": true + }, + { + "label": "net8.0-standard", + "arguments": [ + "dotnet", + "run", + "--project", + "FecSmoke/FecSmoke.csproj", + "-c", + "Release", + "-f", + "net8.0", + "-p:RuntimeFrameworkVersion=8.0.28", + "-p:Light=false" + ], + "exit_code": 0, + "expected": "success", + "passed": true + }, + { + "label": "net8.0-light", + "arguments": [ + "dotnet", + "run", + "--project", + "FecSmoke/FecSmoke.csproj", + "-c", + "Release", + "-f", + "net8.0", + "-p:RuntimeFrameworkVersion=8.0.28", + "-p:Light=true" + ], + "exit_code": 0, + "expected": "success", + "passed": true + }, + { + "label": "net8.0-consumer-final", + "arguments": [ + "dotnet", + "run", + "--project", + "Consumer/Consumer.csproj", + "-c", + "Release", + "-f", + "net8.0", + "-p:RuntimeFrameworkVersion=8.0.28" + ], + "exit_code": 0, + "expected": "success", + "passed": true + }, + { + "label": "net10.0-generate-private", + "arguments": [ + "dotnet", + "C:\\Data\\Git\\Github\\FastExpressionCompiler\\BuildScripts\\IgnoresAccessChecksToGenerator\\Generator\\bin\\Release\\net10.0\\Generator.dll", + "C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\10.0.9\\System.Private.CoreLib.dll", + "C:\\Data\\Git\\Github\\FastExpressionCompiler\\BuildScripts\\IgnoresAccessChecksToGenerator\\artifacts\\net10.0\\private\\System.Private.CoreLib.dll", + "--private" + ], + "exit_code": 0, + "expected": "success", + "passed": true + }, + { + "label": "net10.0-verify-private", + "arguments": [ + "dotnet", + "C:\\Data\\Git\\Github\\FastExpressionCompiler\\BuildScripts\\IgnoresAccessChecksToGenerator\\Generator\\bin\\Release\\net10.0\\Generator.dll", + "--verify", + "C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\10.0.9\\System.Private.CoreLib.dll", + "C:\\Data\\Git\\Github\\FastExpressionCompiler\\BuildScripts\\IgnoresAccessChecksToGenerator\\artifacts\\net10.0\\private\\System.Private.CoreLib.dll", + "--private" + ], + "exit_code": 0, + "expected": "success", + "passed": true + }, + { + "label": "net10.0-generate-internal", + "arguments": [ + "dotnet", + "C:\\Data\\Git\\Github\\FastExpressionCompiler\\BuildScripts\\IgnoresAccessChecksToGenerator\\Generator\\bin\\Release\\net10.0\\Generator.dll", + "C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\10.0.9\\System.Private.CoreLib.dll", + "C:\\Data\\Git\\Github\\FastExpressionCompiler\\BuildScripts\\IgnoresAccessChecksToGenerator\\artifacts\\net10.0\\internal\\System.Private.CoreLib.dll" + ], + "exit_code": 0, + "expected": "success", + "passed": true + }, + { + "label": "net10.0-consumer", + "arguments": [ + "dotnet", + "run", + "--project", + "Consumer/Consumer.csproj", + "-c", + "Release", + "-f", + "net10.0", + "-p:RuntimeFrameworkVersion=10.0.9" + ], + "exit_code": 0, + "expected": "success", + "passed": true + }, + { + "label": "net10.0-task", + "arguments": [ + "dotnet", + "run", + "--project", + "Consumer/Consumer.csproj", + "-c", + "Release", + "-f", + "net10.0", + "-p:RuntimeFrameworkVersion=10.0.9", + "-p:UseGeneratorTask=true", + "-p:RuntimeCoreLib=C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\10.0.9\\System.Private.CoreLib.dll" + ], + "exit_code": 0, + "expected": "success", + "passed": true + }, + { + "label": "net10.0-private-disabled", + "arguments": [ + "dotnet", + "run", + "--project", + "Consumer/Consumer.csproj", + "-c", + "Release", + "-f", + "net10.0", + "-p:RuntimeFrameworkVersion=10.0.9", + "-p:FacadeDirectory=C:\\Data\\Git\\Github\\FastExpressionCompiler\\BuildScripts\\IgnoresAccessChecksToGenerator\\artifacts\\net10.0\\internal" + ], + "exit_code": 1, + "expected": "failure", + "passed": true + }, + { + "label": "net10.0-permission-disabled", + "arguments": [ + "dotnet", + "run", + "--project", + "Consumer/Consumer.csproj", + "-c", + "Release", + "-f", + "net10.0", + "-p:RuntimeFrameworkVersion=10.0.9", + "-p:GrantPrivateAccess=false" + ], + "exit_code": 3762504530, + "expected": "failure", + "passed": true + }, + { + "label": "net10.0-missing-member", + "arguments": [ + "dotnet", + "run", + "--project", + "Consumer/Consumer.csproj", + "-c", + "Release", + "-f", + "net10.0", + "-p:RuntimeFrameworkVersion=10.0.9", + "-p:MissingMemberProbe=true" + ], + "exit_code": 1, + "expected": "failure", + "passed": true + }, + { + "label": "net10.0-wrong-signature", + "arguments": [ + "dotnet", + "run", + "--project", + "Consumer/Consumer.csproj", + "-c", + "Release", + "-f", + "net10.0", + "-p:RuntimeFrameworkVersion=10.0.9", + "-p:WrongSignatureProbe=true" + ], + "exit_code": 1, + "expected": "failure", + "passed": true + }, + { + "label": "net10.0-standard", + "arguments": [ + "dotnet", + "run", + "--project", + "FecSmoke/FecSmoke.csproj", + "-c", + "Release", + "-f", + "net10.0", + "-p:RuntimeFrameworkVersion=10.0.9", + "-p:Light=false" + ], + "exit_code": 0, + "expected": "success", + "passed": true + }, + { + "label": "net10.0-light", + "arguments": [ + "dotnet", + "run", + "--project", + "FecSmoke/FecSmoke.csproj", + "-c", + "Release", + "-f", + "net10.0", + "-p:RuntimeFrameworkVersion=10.0.9", + "-p:Light=true" + ], + "exit_code": 0, + "expected": "success", + "passed": true + }, + { + "label": "net10.0-consumer-final", + "arguments": [ + "dotnet", + "run", + "--project", + "Consumer/Consumer.csproj", + "-c", + "Release", + "-f", + "net10.0", + "-p:RuntimeFrameworkVersion=10.0.9" + ], + "exit_code": 0, + "expected": "success", + "passed": true + } + ] +} diff --git a/BuildScripts/IgnoresAccessChecksToGenerator/upstream/IgnoresAccessChecksToGenerator.targets b/BuildScripts/IgnoresAccessChecksToGenerator/upstream/IgnoresAccessChecksToGenerator.targets new file mode 100644 index 000000000..48bb79276 --- /dev/null +++ b/BuildScripts/IgnoresAccessChecksToGenerator/upstream/IgnoresAccessChecksToGenerator.targets @@ -0,0 +1,62 @@ + + + + <_IACTG_TargetFramework Condition="'$(MSBuildRuntimeType)' == 'Core'">netstandard2.0 + <_IACTG_TargetFramework Condition="'$(MSBuildRuntimeType)' != 'Core'">net462 + <_IACTG_TaskAssembly>$(MSBuildThisFileDirectory)../tools/$(_IACTG_TargetFramework)/IgnoresAccessChecksToGenerator.Tasks.dll + <_IACTG_IntermediatePath>$(IntermediateOutputPath)/IgnoresAccessChecksToGenerator + <_IACTG_CodeFilePath>$(_IACTG_IntermediatePath)/IgnoresAccessChecksTo.cs + <_IACTG_ManifestFilePath>$(_IACTG_IntermediatePath)/IgnoresAccessChecksToGenerator.manifest + + + + + + + + + + + + + + <_IACTG_ReferencePathJoin Include="@(ReferencePath)" IgnoresAccessChecksTo="%(IgnoresAccessChecksTo.Identity)" /> + <_IACTG_InputReferencePath Include="@(_IACTG_ReferencePathJoin)" Condition=" '%(Filename)' == '%(IgnoresAccessChecksTo)' " /> + <_IACTG_ReferencePathJoin Remove="@(_IACTG_ReferencePathJoin)" /> + <_IACTG_OutputReferencePath Include="@(_IACTG_InputReferencePath->'$(_IACTG_IntermediatePath)/%(Filename)%(Extension)')" OriginalItemSpec="%(Identity)" /> + + + <_IACTG_Manifest Include="@(IgnoresAccessChecksToExcludeTypeName->'%(Identity)^%(Assembly)')" /> + <_IACTG_Manifest Include="@(_IACTG_InputReferencePath)" /> + + + + + + + + + + + + + + + + <_IACTG_Directory Include="$(_IACTG_IntermediatePath)" /> + + + + + diff --git a/BuildScripts/IgnoresAccessChecksToGenerator/upstream/PublicizeInternals.cs b/BuildScripts/IgnoresAccessChecksToGenerator/upstream/PublicizeInternals.cs new file mode 100644 index 000000000..34f799568 --- /dev/null +++ b/BuildScripts/IgnoresAccessChecksToGenerator/upstream/PublicizeInternals.cs @@ -0,0 +1,273 @@ +using System.Runtime.CompilerServices; +using Microsoft.Build.Framework; +using Mono.Cecil; + +[assembly: InternalsVisibleTo("IgnoresAccessChecksToGenerator.Tasks.Test")] + +#nullable disable + +namespace IgnoresAccessChecksToGenerator.Tasks +{ + public class PublicizeInternals : Microsoft.Build.Utilities.Task + { + private readonly AssemblyResolver _resolver = new AssemblyResolver(); + + [Required] + public ITaskItem[] SourceReferences { get; set; } + + [Required] + public ITaskItem[] AssemblyNames { get; set; } + + [Required] + public string IntermediateOutputPath { get; set; } + + [Required] + public string GeneratedCodeFilePath { get; set; } + + public ITaskItem[] ExcludeTypeNames { get; set; } + + public bool OmitAttributeDefinition { get; set; } + + public override bool Execute() + { + if (SourceReferences == null) throw new ArgumentNullException(nameof(SourceReferences)); + + var assemblyNames = new HashSet(AssemblyNames.Select(t => t.ItemSpec), StringComparer.OrdinalIgnoreCase); + + if (assemblyNames.Count == 0) + { + return true; + } + + var excludedTypeNames = new TypeExclusions(); + if (ExcludeTypeNames != null) + { + foreach (var item in ExcludeTypeNames) + { + excludedTypeNames.Add(item.ItemSpec, item.GetMetadata("Assembly")); + } + } + + var targetPath = IntermediateOutputPath; + Directory.CreateDirectory(targetPath); + + GenerateAttributes(targetPath, assemblyNames, OmitAttributeDefinition); + + foreach (var assemblyPath in SourceReferences + .Select(a => Path.GetDirectoryName(GetFullFilePath(targetPath, a.ItemSpec)))) + { + _resolver.AddSearchDirectory(assemblyPath); + } + + foreach (var assembly in SourceReferences) + { + var assemblyPath = GetFullFilePath(targetPath, assembly.ItemSpec); + var assemblyName = Path.GetFileNameWithoutExtension(assemblyPath); + if (assemblyNames.Contains(assemblyName)) + { + var targetAssemblyPath = Path.Combine(targetPath, Path.GetFileName(assemblyPath)); + + CreatePublicAssembly(assemblyName, assemblyPath, targetAssemblyPath, excludedTypeNames); + Log.LogMessageFromText("Created publicized assembly at " + targetAssemblyPath, MessageImportance.Normal); + } + } + + return true; + } + + private void GenerateAttributes(string path, IEnumerable assemblyNames, bool omitAttributeDefinition) + { + var content = BuildAttributeFileContent(assemblyNames, omitAttributeDefinition); + File.WriteAllText(GeneratedCodeFilePath, content, System.Text.Encoding.UTF8); + + Log.LogMessageFromText("Generated IgnoresAccessChecksTo attributes", MessageImportance.Low); + } + + internal static string BuildAttributeFileContent(IEnumerable assemblyNames, bool omitAttributeDefinition) + { + var attributes = string.Join(Environment.NewLine, + assemblyNames.Select(a => $@"[assembly: System.Runtime.CompilerServices.IgnoresAccessChecksTo(""{a}"")]")); + + var attributeDefinition = omitAttributeDefinition ? "" : """ + +namespace System.Runtime.CompilerServices +{ + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + internal sealed class IgnoresAccessChecksToAttribute : Attribute + { + public IgnoresAccessChecksToAttribute(string assemblyName) + { + } + } +} +"""; + + var content = $$""" +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +{{attributes}} +{{attributeDefinition}} +"""; + return content.TrimEnd() + Environment.NewLine; + } + + private void CreatePublicAssembly(string assemblyName, string source, string target, TypeExclusions excludedTypeNames) + { + var assembly = AssemblyDefinition.ReadAssembly(source, + new ReaderParameters { AssemblyResolver = _resolver }); + + foreach (var module in assembly.Modules) + { + foreach (var type in module.GetTypes().Where(type => !excludedTypeNames.IsExcluded(assemblyName, type.FullName))) + { + if (!type.IsNested && type.IsNotPublic) + { + type.IsPublic = true; + } + else if (type.IsNestedAssembly || + type.IsNestedFamilyOrAssembly || + type.IsNestedFamilyAndAssembly) + { + type.IsNestedPublic = true; + } + + foreach (var field in type.Fields) + { + if (field.IsAssembly || + field.IsFamilyOrAssembly || + field.IsFamilyAndAssembly) + { + field.IsPublic = true; + } + } + + foreach (var method in type.Methods) + { + if (method.IsAssembly || + method.IsFamilyOrAssembly || + method.IsFamilyAndAssembly) + { + method.IsPublic = true; + } + } + } + } + + assembly.Write(target); + // copy XML docs alongside the new assembly + var xmlSource = Path.ChangeExtension(source, ".xml"); + if (File.Exists(xmlSource)) + { + var xmlTarget = Path.ChangeExtension(target, ".xml"); + File.Copy(xmlSource, xmlTarget, overwrite: true); + Log.LogMessageFromText($"Copied XML docs to {xmlTarget}", MessageImportance.Low); + } + } + + private string GetFullFilePath(string basePath, string path) => + Path.IsPathRooted(path) ? Path.GetFullPath(path) : Path.Combine(basePath, path); + + internal sealed class TypeExclusions + { + private readonly HashSet _global = new HashSet(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary> _perAssembly = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + public void Add(string typeName, string assemblyName) + { + if (string.IsNullOrEmpty(typeName)) + { + return; + } + + if (string.IsNullOrEmpty(assemblyName)) + { + _global.Add(typeName); + } + else + { + if (!_perAssembly.TryGetValue(assemblyName, out var set)) + { + set = new HashSet(StringComparer.OrdinalIgnoreCase); + _perAssembly[assemblyName] = set; + } + + set.Add(typeName); + } + } + + public bool IsExcluded(string assemblyName, string typeFullName) + { + return _global.Contains(typeFullName) || + (_perAssembly.TryGetValue(assemblyName, out var set) && set.Contains(typeFullName)); + } + } + + private class AssemblyResolver : IAssemblyResolver + { + private readonly HashSet _directories = new HashSet(StringComparer.OrdinalIgnoreCase); + + public void AddSearchDirectory(string directory) + { + _directories.Add(directory); + } + + public AssemblyDefinition Resolve(AssemblyNameReference name) + { + return Resolve(name, new ReaderParameters()); + } + + public AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters) + { + var assembly = SearchDirectory(name, _directories, parameters); + if (assembly != null) + { + return assembly; + } + + throw new AssemblyResolutionException(name); + } + + public void Dispose() + { + } + + private AssemblyDefinition SearchDirectory(AssemblyNameReference name, IEnumerable directories, ReaderParameters parameters) + { + var extensions = name.IsWindowsRuntime ? new[] { ".winmd", ".dll" } : new[] { ".exe", ".dll" }; + foreach (var directory in directories) + { + foreach (var extension in extensions) + { + var file = Path.Combine(directory, name.Name + extension); + if (!File.Exists(file)) + continue; + try + { + return GetAssembly(file, parameters); + } + catch (BadImageFormatException) + { + } + } + } + + return null; + } + + private AssemblyDefinition GetAssembly(string file, ReaderParameters parameters) + { + if (parameters.AssemblyResolver == null) + parameters.AssemblyResolver = this; + + return ModuleDefinition.ReadModule(file, parameters).Assembly; + } + } + } +} \ No newline at end of file