Files
nrwl--nx/packages/dotnet/analyzer/Program.cs
T
2026-07-13 12:38:36 +08:00

185 lines
6.1 KiB
C#

using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Build.Locator;
using MsbuildAnalyzer;
using MsbuildAnalyzer.Models;
using MsbuildAnalyzer.Utilities;
// Parse input - either from stdin or command line arguments
// Format (stdin): newline-separated file paths. Files are partitioned here by name:
// .csproj/.fsproj/.vbproj go to projectFiles, the canonical Directory.* files go to
// directoryFiles. MSBuild evaluation finds those directory files on its own; we need
// them in this process to declare the right per-project inputs back to Nx.
// Args: MsbuildAnalyzer <workspace-root> [plugin-options-json]
string workspaceRoot;
List<string> projectFiles;
List<string> directoryFiles = new();
PluginOptions? pluginOptions = null;
var directoryFileNameSet = new HashSet<string>(
ProjectUtilities.DirectoryBuildFileNames,
StringComparer.OrdinalIgnoreCase
);
// Check if stdin has data for project files
if (Console.IsInputRedirected)
{
// Read from stdin
if (args.Length < 1)
{
Console.Error.WriteLine("Usage (stdin mode): MsbuildAnalyzer <workspace-root> [plugin-options-json]");
Console.Error.WriteLine(" workspace-root: Absolute path to the workspace root");
Console.Error.WriteLine(" plugin-options-json: JSON string with plugin options (optional)");
Console.Error.WriteLine(" Project files should be provided via stdin (newline-separated)");
return 1;
}
workspaceRoot = args[0];
// Parse plugin options if provided
if (args.Length >= 2 && !string.IsNullOrEmpty(args[1]))
{
try
{
pluginOptions = JsonSerializer.Deserialize<PluginOptions>(args[1], new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
}
catch (Exception ex)
{
Console.Error.WriteLine($"Failed to parse plugin options: {ex.Message}");
return 1;
}
}
projectFiles = new List<string>();
string? line;
while ((line = Console.ReadLine()) != null)
{
line = line.Trim();
if (string.IsNullOrEmpty(line))
{
continue;
}
var fileName = Path.GetFileName(line);
if (ProjectUtilities.IsProjectFile(line))
{
projectFiles.Add(line);
}
else if (directoryFileNameSet.Contains(fileName))
{
directoryFiles.Add(line);
}
// Anything else is silently dropped; the glob shouldn't surface unrelated files,
// but we don't want a stray path to abort the run.
}
}
else
{
// Read from command line arguments
if (args.Length < 2)
{
Console.Error.WriteLine("Usage (args mode): MsbuildAnalyzer <workspace-root> <project-file-1> [project-file-2] ...");
Console.Error.WriteLine(" workspace-root: Absolute path to the workspace root");
Console.Error.WriteLine(" project-files: Relative paths to .csproj/.fsproj/.vbproj files from workspace root");
Console.Error.WriteLine();
Console.Error.WriteLine("Alternative (stdin mode): MsbuildAnalyzer <workspace-root> [plugin-options-json] < files.txt");
Console.Error.WriteLine(" Provide project files via stdin (newline-separated)");
return 1;
}
workspaceRoot = args[0];
projectFiles = args.Skip(1).ToList();
}
if (projectFiles.Count == 0)
{
Console.Error.WriteLine("No project files provided.");
return 1;
}
// Use default plugin options if not provided
pluginOptions ??= new PluginOptions();
// Register MSBuild BEFORE any MSBuild types are referenced
try
{
using var msbuildRegisterPerf = PerfLogger.Start("MSBuild registration");
var queryOptions = new VisualStudioInstanceQueryOptions
{
AllowAllDotnetLocations = true,
AllowAllRuntimeVersions = true,
DiscoveryTypes = DiscoveryType.DotNetSdk
};
var instances = MSBuildLocator
.QueryVisualStudioInstances(queryOptions)
.Distinct(new VSInstanceComparer())
.ToList();
// Select an SDK that is compatible with the current runtime
var currentMajor = Environment.Version.Major;
var selectedInstance = instances
.Where(i => i.Version.Major == currentMajor)
.OrderByDescending(i => i.Version)
.FirstOrDefault();
if (selectedInstance == null)
{
var sb = new System.Text.StringBuilder();
sb.AppendLine("No compatible .NET SDK found.");
sb.AppendLine($" Current runtime: .NET {Environment.Version.Major}");
sb.AppendLine(" Available SDKs:");
foreach (var inst in instances)
{
sb.AppendLine($" - .NET {inst.Version} at {inst.MSBuildPath}");
}
if (instances.Any(i => i.MSBuildPath.Contains("preview", StringComparison.OrdinalIgnoreCase) || i.MSBuildPath.Contains("-rc.", StringComparison.OrdinalIgnoreCase)))
{
sb.AppendLine();
sb.AppendLine("By default, dotnet will not run via a preview SDK.");
sb.AppendLine("If you are using a preview SDK, set DOTNET_ROLL_FORWARD_TO_PRERELEASE=1 in the environment.");
}
Console.Error.WriteLine(sb.ToString());
Console.Error.WriteLine("To install the correct SDK:");
Console.Error.WriteLine($" - Visit: https://dotnet.microsoft.com/download/dotnet/{currentMajor}.0");
return 2;
}
MSBuildLocator.RegisterInstance(selectedInstance);
}
catch (Exception ex)
{
Console.Error.WriteLine($"Failed to register MSBuild: {ex.Message}");
return 2;
}
// JSON serialization options (cached for reuse)
var jsonOptions = new JsonSerializerOptions
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
// Run the analyzer
AnalysisResult result;
using (var analyzePerf = PerfLogger.Start("analyze workspace"))
{
result = Analyzer.AnalyzeWorkspace(projectFiles, directoryFiles, workspaceRoot, pluginOptions);
}
// Serialize and output results
using (PerfLogger.Start("serialize results"))
{
Console.WriteLine(JsonSerializer.Serialize(result, jsonOptions));
}
return 0;