#pragma warning disable RS1035 // Do not use APIs banned for analyzers - This is an MSBuild task, not an analyzer using System; using System.IO; using System.Linq; using System.Text.RegularExpressions; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; namespace Svrnty.CQRS.Grpc.Generators; /// /// MSBuild task that extracts the auto-generated proto file content from the source generator /// output and writes it to disk so Grpc.Tools can process it /// public class WriteProtoFileTask : Task { /// /// The project directory where we should look for generated files /// [Required] public string ProjectDirectory { get; set; } = string.Empty; /// /// The intermediate output path (typically obj/Debug/net10.0) /// [Required] public string IntermediateOutputPath { get; set; } = string.Empty; /// /// The output directory where the proto file should be written (typically Protos/) /// [Required] public string OutputDirectory { get; set; } = string.Empty; /// /// The name of the proto file to generate (typically cqrs_services.proto) /// [Required] public string ProtoFileName { get; set; } = string.Empty; public override bool Execute() { try { Log.LogMessage(MessageImportance.High, "Svrnty.CQRS.Grpc: Extracting auto-generated proto file..."); // Look for the generated C# file containing the proto content // Source generators output to obj/Generated, not IntermediateOutputPath/Generated var generatedFilePath = Path.Combine( ProjectDirectory, "obj", "Generated", "Svrnty.CQRS.Grpc.Generators", "Svrnty.CQRS.Grpc.Generators.ProtoFileSourceGenerator", "GeneratedProtoFile.g.cs" ); if (!File.Exists(generatedFilePath)) { Log.LogWarning( $"Generated proto file not found at {generatedFilePath}. " + "The proto file may not have been generated yet. This is normal on first build."); return true; // Don't fail the build, just skip } // Read the generated C# file var csContent = File.ReadAllText(generatedFilePath); // Extract the proto content using a more robust approach // Looking for: public const string Content = @"..."; var startMarker = "public const string Content = @\""; var startIndex = csContent.IndexOf(startMarker); if (startIndex < 0) { Log.LogError($"Could not find Content property in {generatedFilePath}"); return false; } startIndex += startMarker.Length; // Find the closing "; - We need the LAST occurrence because the content contains escaped quotes // The pattern is: Content = @"...content..."; // where content has "" for literal quotes var endMarker = "\";"; // Find where the next field starts or class ends to limit our search var nextFieldOrEnd = csContent.IndexOf("\n }", startIndex); // End of class if (nextFieldOrEnd < 0) { nextFieldOrEnd = csContent.Length; } var endIndex = csContent.LastIndexOf(endMarker, nextFieldOrEnd, nextFieldOrEnd - startIndex); if (endIndex < 0 || endIndex < startIndex) { Log.LogError($"Could not find end of Content property in {generatedFilePath}"); return false; } // Extract and unescape doubled quotes var protoContent = csContent.Substring(startIndex, endIndex - startIndex); protoContent = protoContent.Replace("\"\"", "\""); Log.LogMessage(MessageImportance.High, $"Extracted proto content length: {protoContent.Length} characters"); // Ensure output directory exists var fullOutputPath = 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; } } }