All checks were successful
Publish NuGets / build (release) Successful in 40s
Replace ProtoFileSourceGenerator and WriteProtoFileTask with a new GenerateProtoFileTask that creates its own Roslyn compilation. This solves the timing issue where source generators run too late for Grpc.Tools to process the generated proto files. The new task runs after ResolveAssemblyReferences but before _gRPC_GetProtoc and CoreCompile, ensuring the proto file exists when Grpc.Tools needs it. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
257 lines
8.9 KiB
C#
257 lines
8.9 KiB
C#
#pragma warning disable RS1035 // Do not use APIs banned for analyzers - This is an MSBuild task, not an analyzer
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using Microsoft.Build.Framework;
|
|
using Microsoft.Build.Utilities;
|
|
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
|
|
namespace Svrnty.CQRS.Grpc.Generators;
|
|
|
|
/// <summary>
|
|
/// MSBuild task that generates .proto files by creating its own Roslyn compilation.
|
|
/// This runs BEFORE CoreCompile to solve the source generator timing issue.
|
|
/// </summary>
|
|
public class GenerateProtoFileTask : Task
|
|
{
|
|
/// <summary>
|
|
/// The project directory
|
|
/// </summary>
|
|
[Required]
|
|
public string ProjectDirectory { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// The output directory where the proto file should be written (typically Protos/)
|
|
/// </summary>
|
|
[Required]
|
|
public string OutputDirectory { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// The name of the proto file to generate (typically cqrs_services.proto)
|
|
/// </summary>
|
|
[Required]
|
|
public string ProtoFileName { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// The C# source files to compile (from @(Compile) ItemGroup)
|
|
/// </summary>
|
|
[Required]
|
|
public ITaskItem[] SourceFiles { get; set; } = Array.Empty<ITaskItem>();
|
|
|
|
/// <summary>
|
|
/// The assembly references (from @(ReferencePath) ItemGroup)
|
|
/// </summary>
|
|
[Required]
|
|
public ITaskItem[] References { get; set; } = Array.Empty<ITaskItem>();
|
|
|
|
/// <summary>
|
|
/// The root namespace of the project
|
|
/// </summary>
|
|
public string RootNamespace { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// The assembly name of the project
|
|
/// </summary>
|
|
public string AssemblyName { get; set; } = string.Empty;
|
|
|
|
public override bool Execute()
|
|
{
|
|
try
|
|
{
|
|
Log.LogMessage(MessageImportance.High,
|
|
"Svrnty.CQRS.Grpc: Generating proto file via MSBuild task...");
|
|
|
|
// Determine the namespace for the proto file
|
|
var projectNamespace = !string.IsNullOrEmpty(RootNamespace) ? RootNamespace
|
|
: !string.IsNullOrEmpty(AssemblyName) ? AssemblyName
|
|
: "Generated";
|
|
var grpcNamespace = $"{projectNamespace}.Grpc";
|
|
var packageName = "cqrs";
|
|
|
|
// Create the compilation
|
|
var compilation = CreateCompilation();
|
|
if (compilation == null)
|
|
{
|
|
Log.LogWarning("Svrnty.CQRS.Grpc: Could not create compilation. Writing placeholder proto file.");
|
|
WritePlaceholderProto(grpcNamespace);
|
|
return true;
|
|
}
|
|
|
|
// Check for compilation errors that would prevent proper analysis
|
|
var diagnostics = compilation.GetDiagnostics()
|
|
.Where(d => d.Severity == DiagnosticSeverity.Error)
|
|
.ToList();
|
|
|
|
if (diagnostics.Count > 0)
|
|
{
|
|
Log.LogMessage(MessageImportance.Normal,
|
|
$"Svrnty.CQRS.Grpc: Compilation has {diagnostics.Count} errors. Attempting to generate proto anyway...");
|
|
|
|
// Log first few errors for debugging
|
|
foreach (var diag in diagnostics.Take(5))
|
|
{
|
|
Log.LogMessage(MessageImportance.Low, $" {diag.GetMessage()}");
|
|
}
|
|
}
|
|
|
|
// Use the ProtoFileGenerator to generate content
|
|
var generator = new ProtoFileGenerator(compilation);
|
|
var protoContent = generator.Generate(packageName, grpcNamespace);
|
|
|
|
// Check if we got meaningful content
|
|
if (string.IsNullOrWhiteSpace(protoContent) || !protoContent.Contains("rpc "))
|
|
{
|
|
Log.LogMessage(MessageImportance.High,
|
|
"Svrnty.CQRS.Grpc: No commands/queries/notifications found. Writing minimal proto file.");
|
|
WritePlaceholderProto(grpcNamespace);
|
|
return true;
|
|
}
|
|
|
|
// Ensure output directory exists
|
|
var fullOutputPath = Path.IsPathRooted(OutputDirectory)
|
|
? OutputDirectory
|
|
: Path.Combine(ProjectDirectory, OutputDirectory);
|
|
Directory.CreateDirectory(fullOutputPath);
|
|
|
|
// Write the proto file
|
|
var protoFilePath = Path.Combine(fullOutputPath, ProtoFileName);
|
|
File.WriteAllText(protoFilePath, protoContent);
|
|
|
|
Log.LogMessage(MessageImportance.High,
|
|
$"Svrnty.CQRS.Grpc: Successfully generated proto file at {protoFilePath}");
|
|
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.LogErrorFromException(ex, true);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private CSharpCompilation? CreateCompilation()
|
|
{
|
|
try
|
|
{
|
|
// Parse all source files into syntax trees
|
|
var syntaxTrees = new List<SyntaxTree>();
|
|
foreach (var sourceFile in SourceFiles)
|
|
{
|
|
var filePath = sourceFile.ItemSpec;
|
|
if (!Path.IsPathRooted(filePath))
|
|
{
|
|
filePath = Path.Combine(ProjectDirectory, filePath);
|
|
}
|
|
|
|
if (!File.Exists(filePath))
|
|
{
|
|
Log.LogMessage(MessageImportance.Low, $"Source file not found: {filePath}");
|
|
continue;
|
|
}
|
|
|
|
try
|
|
{
|
|
var sourceText = File.ReadAllText(filePath);
|
|
var syntaxTree = CSharpSyntaxTree.ParseText(
|
|
sourceText,
|
|
CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Latest),
|
|
path: filePath);
|
|
syntaxTrees.Add(syntaxTree);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.LogMessage(MessageImportance.Low, $"Failed to parse {filePath}: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
if (syntaxTrees.Count == 0)
|
|
{
|
|
Log.LogWarning("Svrnty.CQRS.Grpc: No source files could be parsed.");
|
|
return null;
|
|
}
|
|
|
|
Log.LogMessage(MessageImportance.Normal,
|
|
$"Svrnty.CQRS.Grpc: Parsed {syntaxTrees.Count} source files");
|
|
|
|
// Create metadata references from the References
|
|
var metadataReferences = new List<MetadataReference>();
|
|
foreach (var reference in References)
|
|
{
|
|
var refPath = reference.ItemSpec;
|
|
if (!File.Exists(refPath))
|
|
{
|
|
Log.LogMessage(MessageImportance.Low, $"Reference not found: {refPath}");
|
|
continue;
|
|
}
|
|
|
|
try
|
|
{
|
|
var metadataRef = MetadataReference.CreateFromFile(refPath);
|
|
metadataReferences.Add(metadataRef);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.LogMessage(MessageImportance.Low, $"Failed to load reference {refPath}: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
Log.LogMessage(MessageImportance.Normal,
|
|
$"Svrnty.CQRS.Grpc: Loaded {metadataReferences.Count} references");
|
|
|
|
// Create the compilation
|
|
var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
|
|
.WithNullableContextOptions(NullableContextOptions.Enable);
|
|
|
|
var assemblyName = !string.IsNullOrEmpty(AssemblyName) ? AssemblyName : "TempCompilation";
|
|
var compilation = CSharpCompilation.Create(
|
|
assemblyName,
|
|
syntaxTrees,
|
|
metadataReferences,
|
|
compilationOptions);
|
|
|
|
return compilation;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.LogMessage(MessageImportance.High,
|
|
$"Svrnty.CQRS.Grpc: Failed to create compilation: {ex.Message}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private void WritePlaceholderProto(string grpcNamespace)
|
|
{
|
|
var placeholderProto = $@"syntax = ""proto3"";
|
|
|
|
option csharp_namespace = ""{grpcNamespace}"";
|
|
|
|
package cqrs;
|
|
|
|
// Placeholder proto file - will be regenerated when commands/queries are available
|
|
// Using namespace: {grpcNamespace}
|
|
|
|
// Empty service definitions so Grpc.Tools generates base classes
|
|
service CommandService {{
|
|
}}
|
|
|
|
service QueryService {{
|
|
}}
|
|
|
|
service DynamicQueryService {{
|
|
}}
|
|
";
|
|
var fullOutputPath = Path.IsPathRooted(OutputDirectory)
|
|
? OutputDirectory
|
|
: Path.Combine(ProjectDirectory, OutputDirectory);
|
|
Directory.CreateDirectory(fullOutputPath);
|
|
var protoFilePath = Path.Combine(fullOutputPath, ProtoFileName);
|
|
File.WriteAllText(protoFilePath, placeholderProto);
|
|
|
|
Log.LogMessage(MessageImportance.High,
|
|
$"Svrnty.CQRS.Grpc: Wrote placeholder proto file at {protoFilePath}");
|
|
}
|
|
}
|