diff --git a/Justfile b/Justfile
index c4e06a6..01a9e2a 100644
--- a/Justfile
+++ b/Justfile
@@ -67,6 +67,13 @@ pipeline-tests *args:
echo "Running tests pipeline..."
"{{ pipeline_tool }}" --Build:RunTests=true --Release:Mode=None {{ args }}
+# Run the pipeline through pack + validate (restore, build, lint, tests, pack, validate pack contents) without publishing/releasing
+[group('Pipeline')]
+pipeline-pack-validate *args:
+ just ensure-pipeline-tool
+ echo "Running pack + validate pipeline..."
+ "{{ pipeline_tool }}" --Build:RunPack=true --Build:ValidatePack=true --Release:Mode=None {{ args }}
+
# Build and test with the specified configuration, defaulting to "Debug"
[group('Build and Test')]
build *args:
diff --git a/docs/wiki/Packaging.md b/docs/wiki/Packaging.md
index 61e43ed..a3ff57b 100644
--- a/docs/wiki/Packaging.md
+++ b/docs/wiki/Packaging.md
@@ -144,8 +144,18 @@ arrangements is an error.
The framework assembly defines `System.Runtime.CompilerServices.IsExternalInit` **publicly** so the
framework's own bundled generators can emit `init`-based attribute types into any consumer
compilation, and so the merge step has a single marker definition to internalize. Consumers
-(generator projects) must **not** declare their own `IsExternalInit`: doing so produces a duplicate
-type definition against the framework reference.
+(generator projects) should not declare their own `IsExternalInit`.
+
+A generator-local marker gives calls to the framework's `init` setters a different required custom
+modifier identity from the setter definitions. Older merge-tool versions passed both identities to
+ILRepack, which could emit `Method reference is used with definition return type / parameter`
+warnings while rewriting the component.
+
+For compatibility with generators that still receive a local marker from legacy source or build
+tooling, the merge tool normalizes those required modifiers to the framework marker in a temporary
+copy before merging. The generator's bin output is not changed, and the shipped self-contained
+analyzer contains one internalized `IsExternalInit` definition. Removing the redundant marker from
+the generator project remains the preferred configuration.
### Generators embedded in another package
diff --git a/package.json b/package.json
index e25a004..ef06027 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "purview-sourcegenerator-framework",
- "version": "1.0.0-prerelease.48",
+ "version": "1.0.0-prerelease.49",
"license": "MIT",
"author": {
"name": "Kieron Lanning",
diff --git a/purview-build.json b/purview-build.json
index b9765e0..8d3e035 100644
--- a/purview-build.json
+++ b/purview-build.json
@@ -10,39 +10,45 @@
"RequireSymbolFiles": true,
"RequiredContent": {
"purview.sourcegeneratorframework": [
- "lib/netstandard2.0/Purview.SourceGeneratorFramework.dll",
- "analyzers/dotnet/cs/Purview.SourceGeneratorFramework.Generators.dll",
+ ".agents/**/*",
"analyzers/dotnet/cs/Purview.SourceGeneratorFramework.Analyzers.dll",
"analyzers/dotnet/cs/Purview.SourceGeneratorFramework.CodeFixers.dll",
+ "analyzers/dotnet/cs/Purview.SourceGeneratorFramework.Generators.dll",
"build/Purview.SourceGeneratorFramework.props",
"build/Purview.SourceGeneratorFramework.targets",
- "tools/net10.0/Purview.SourceGeneratorFramework.MergeTool.dll",
+ "lib/netstandard2.0/Purview.SourceGeneratorFramework.dll",
+ "lib/netstandard2.0/Purview.SourceGeneratorFramework.xml",
+ "purview-logo-light.png",
"README.md",
- "LICENSE.md",
- "purview-logo-light.png"
+ "tools/*/ILRepack.dll",
+ "tools/*/Mono.Cecil.dll",
+ "tools/*/Mono.Cecil.Mdb.dll",
+ "tools/*/Mono.Cecil.Pdb.dll",
+ "tools/*/Mono.Cecil.Rocks.dll",
+ "tools/*/Purview.SourceGeneratorFramework.MergeTool.deps.json",
+ "tools/*/Purview.SourceGeneratorFramework.MergeTool.dll",
+ "tools/*/Purview.SourceGeneratorFramework.MergeTool.runtimeconfig.json",
+ "tools/*/System.IO.Hashing.dll"
],
"purview.sourcegeneratorframework.testing": [
- "lib/netstandard2.0/Purview.SourceGeneratorFramework.Testing.dll",
+ ".agents/**/*",
"build/Purview.SourceGeneratorFramework.Testing.props",
- "README.md",
- "LICENSE.md",
- "purview-logo-light.png"
+ "lib/*/Purview.SourceGeneratorFramework.Testing.dll",
+ "lib/*/Purview.SourceGeneratorFramework.Testing.xml",
+ "purview-logo-light.png",
+ "README.md"
],
"purview.sourcegeneratorframework.testing.tunit": [
- "lib/netstandard2.0/Purview.SourceGeneratorFramework.Testing.TUnit.dll",
+ ".agents/**/*",
"build/Purview.SourceGeneratorFramework.Testing.TUnit.props",
- "README.md",
- "LICENSE.md",
- "purview-logo-light.png"
- ]
- },
- "ForbiddenContent": {
- "*": [
- "*.pdb"
+ "lib/*/Purview.SourceGeneratorFramework.Testing.TUnit.dll",
+ "lib/*/Purview.SourceGeneratorFramework.Testing.TUnit.xml",
+ "purview-logo-light.png",
+ "README.md"
]
}
},
"Release": {
"Mode": "None"
}
-}
+}
\ No newline at end of file
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 18c9499..1ae1b18 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -2,6 +2,7 @@
net8.0;net9.0;net10.0;
netstandard2.1;$(TestingTargetFrameworks)
+ net10.0
Purview.SourceGeneratorFramework
true
https://github.com/purview-dev/sourcegenerator-framework
@@ -43,13 +44,12 @@
$(PurviewProjectUrl)
https://github.com/purview-dev/sourcegenerator-framework
README.md
- LICENSE.md
+ MIT
purview-logo-light.png
false
-
diff --git a/src/SourceGeneratorFramework.slnx b/src/SourceGeneratorFramework.slnx
index 189a73a..685c9ad 100644
--- a/src/SourceGeneratorFramework.slnx
+++ b/src/SourceGeneratorFramework.slnx
@@ -37,6 +37,7 @@
Id="006ab41c-5588-49d6-8d3f-5b14ae3e6396"
/>
+
diff --git a/src/src/SourceGeneratorFramework.MergeTool/IsExternalInitNormalizer.cs b/src/src/SourceGeneratorFramework.MergeTool/IsExternalInitNormalizer.cs
new file mode 100644
index 0000000..1eb3019
--- /dev/null
+++ b/src/src/SourceGeneratorFramework.MergeTool/IsExternalInitNormalizer.cs
@@ -0,0 +1,276 @@
+using Mono.Cecil;
+
+static class IsExternalInitNormalizer
+{
+ const string MarkerFullName = "System.Runtime.CompilerServices.IsExternalInit";
+
+ public static NormalizedAssembly? Normalize(
+ string componentPath,
+ string frameworkPath,
+ string outputPath,
+ IEnumerable searchDirectories
+ )
+ {
+ using var resolver = MergeToolRunner.CreateResolver(searchDirectories);
+ using var framework = AssemblyDefinition.ReadAssembly(
+ frameworkPath,
+ new ReaderParameters { AssemblyResolver = resolver }
+ );
+ var frameworkMarker = framework.MainModule.GetType(MarkerFullName);
+ if (frameworkMarker is null)
+ {
+ return null;
+ }
+
+ var componentPdbPath = Path.ChangeExtension(componentPath, ".pdb");
+ var hasSymbols = File.Exists(componentPdbPath);
+ using var component = AssemblyDefinition.ReadAssembly(
+ componentPath,
+ new ReaderParameters
+ {
+ AssemblyResolver = resolver,
+ ReadSymbols = hasSymbols,
+ InMemory = true,
+ }
+ );
+ var componentMarker = component.MainModule.GetType(MarkerFullName);
+ if (componentMarker is null)
+ {
+ return null;
+ }
+
+ // A component-local marker gives init setter references a different modreq identity from
+ // framework definitions. Normalize a temporary copy so ILRepack can bind those methods.
+ var frameworkMarkerReference = component.MainModule.ImportReference(frameworkMarker);
+ RequiredModifierRewriter rewriter = new(component.MainModule, componentMarker, frameworkMarkerReference);
+ rewriter.Rewrite();
+
+ if (rewriter.RemainingComponentMarkerReferences != 0)
+ {
+ throw new InvalidOperationException(
+ $"Failed to normalize all {MarkerFullName} references in '{componentPath}'."
+ );
+ }
+
+ component.MainModule.Types.Remove(componentMarker);
+
+ // Keep the component's bin output untouched; only the transient merge input is rewritten.
+ var normalizedDirectory = Path.Combine(
+ Path.GetDirectoryName(outputPath)!,
+ $".purview-merge-{Guid.NewGuid():N}"
+ );
+ Directory.CreateDirectory(normalizedDirectory);
+ var normalizedPath = Path.Combine(normalizedDirectory, Path.GetFileName(componentPath));
+ component.Write(normalizedPath, new WriterParameters { WriteSymbols = hasSymbols });
+
+ return new NormalizedAssembly(normalizedPath, normalizedDirectory);
+ }
+
+ sealed class RequiredModifierRewriter(
+ ModuleDefinition componentModule,
+ TypeDefinition componentMarker,
+ TypeReference frameworkMarker
+ )
+ {
+ int _remainingComponentMarkerReferences;
+
+ public int RemainingComponentMarkerReferences => _remainingComponentMarkerReferences;
+
+ public void Rewrite()
+ {
+ foreach (var type in componentModule.Types.SelectMany(MergeToolRunner.Flatten))
+ {
+ Rewrite(type.BaseType);
+ foreach (var @interface in type.Interfaces)
+ {
+ Rewrite(@interface.InterfaceType);
+ }
+
+ Rewrite(type.GenericParameters);
+
+ foreach (var field in type.Fields)
+ {
+ Rewrite(field.FieldType);
+ }
+
+ foreach (var property in type.Properties)
+ {
+ Rewrite(property.PropertyType);
+ Rewrite(property.Parameters);
+ }
+
+ foreach (var @event in type.Events)
+ {
+ Rewrite(@event.EventType);
+ }
+
+ foreach (var method in type.Methods)
+ {
+ Rewrite(method.ReturnType);
+ Rewrite(method.Parameters);
+ Rewrite(method.GenericParameters);
+
+ foreach (var @override in method.Overrides)
+ {
+ Rewrite(@override);
+ }
+
+ if (!method.HasBody)
+ {
+ continue;
+ }
+
+ foreach (var variable in method.Body.Variables)
+ {
+ Rewrite(variable.VariableType);
+ }
+
+ foreach (var handler in method.Body.ExceptionHandlers)
+ {
+ Rewrite(handler.CatchType);
+ }
+
+ foreach (var instruction in method.Body.Instructions)
+ {
+ RewriteOperand(instruction.Operand);
+ }
+ }
+ }
+ }
+
+ void RewriteOperand(object? operand)
+ {
+ switch (operand)
+ {
+ case TypeReference type:
+ Rewrite(type);
+ break;
+ case MethodReference method:
+ Rewrite(method);
+ break;
+ case FieldReference field:
+ Rewrite(field.DeclaringType);
+ Rewrite(field.FieldType);
+ break;
+ case CallSite callSite:
+ Rewrite(callSite.ReturnType);
+ Rewrite(callSite.Parameters);
+ break;
+ default:
+ break;
+ }
+ }
+
+ void Rewrite(MethodReference method)
+ {
+ Rewrite(method.DeclaringType);
+ Rewrite(method.ReturnType);
+ Rewrite(method.Parameters);
+
+ if (method is GenericInstanceMethod genericMethod)
+ {
+ foreach (var argument in genericMethod.GenericArguments)
+ {
+ Rewrite(argument);
+ }
+ }
+ }
+
+ void Rewrite(IEnumerable parameters)
+ {
+ foreach (var parameter in parameters)
+ {
+ Rewrite(parameter.ParameterType);
+ }
+ }
+
+ void Rewrite(IEnumerable parameters)
+ {
+ foreach (var parameter in parameters)
+ {
+ foreach (var constraint in parameter.Constraints)
+ {
+ Rewrite(constraint.ConstraintType);
+ }
+ }
+ }
+
+ void Rewrite(TypeReference? type)
+ {
+ if (type is null)
+ {
+ return;
+ }
+
+ if (type is RequiredModifierType requiredModifier)
+ {
+ if (IsComponentMarker(requiredModifier.ModifierType))
+ {
+ requiredModifier.ModifierType = frameworkMarker;
+ }
+
+ Rewrite(requiredModifier.ModifierType);
+ Rewrite(requiredModifier.ElementType);
+ return;
+ }
+
+ if (type is OptionalModifierType optionalModifier)
+ {
+ Rewrite(optionalModifier.ModifierType);
+ Rewrite(optionalModifier.ElementType);
+ return;
+ }
+
+ if (type is GenericInstanceType genericInstance)
+ {
+ Rewrite(genericInstance.ElementType);
+ foreach (var argument in genericInstance.GenericArguments)
+ {
+ Rewrite(argument);
+ }
+
+ return;
+ }
+
+ if (type is FunctionPointerType functionPointer)
+ {
+ Rewrite(functionPointer.ReturnType);
+ Rewrite(functionPointer.Parameters);
+ return;
+ }
+
+ if (type is TypeSpecification specification)
+ {
+ Rewrite(specification.ElementType);
+ }
+
+ if (IsComponentMarker(type))
+ {
+ _remainingComponentMarkerReferences++;
+ }
+
+ Rewrite(type.DeclaringType);
+ }
+
+ bool IsComponentMarker(TypeReference type) =>
+ type.FullName == MarkerFullName
+ && (
+ ReferenceEquals(type, componentMarker)
+ || ReferenceEquals(type.Scope, componentModule)
+ || (type.Scope is AssemblyNameReference assembly && assembly.Name == componentModule.Assembly.Name.Name)
+ );
+ }
+}
+
+sealed class NormalizedAssembly(string assemblyPath, string directoryPath) : IDisposable
+{
+ public string AssemblyPath { get; } = assemblyPath;
+
+ public void Dispose()
+ {
+ if (Directory.Exists(directoryPath))
+ {
+ Directory.Delete(directoryPath, recursive: true);
+ }
+ }
+}
diff --git a/src/src/SourceGeneratorFramework.MergeTool/MergeToolRunner.cs b/src/src/SourceGeneratorFramework.MergeTool/MergeToolRunner.cs
new file mode 100644
index 0000000..a8a8e34
--- /dev/null
+++ b/src/src/SourceGeneratorFramework.MergeTool/MergeToolRunner.cs
@@ -0,0 +1,161 @@
+using ILRepacking;
+using Mono.Cecil;
+
+static class MergeToolRunner
+{
+ public static int Run(string[] args, TextWriter error, ILogger? logger = null)
+ {
+ if (args.Length < 3)
+ {
+ error.WriteLine("Usage: Purview.SourceGeneratorFramework.MergeTool