Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
233 changes: 171 additions & 62 deletions src/Blake.BuildTools/Utils/PluginLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

namespace Blake.BuildTools.Utils;

internal record NuGetPluginInfo(string PackageName, string Version, string DllPath);
internal record ProjectRefPluginInfo(string ProjectPath, string DllPath);

internal static class PluginLoader
{
internal static List<PluginContext> LoadPlugins(string directory, string config, ILogger? logger)
Expand All @@ -21,7 +24,7 @@ internal static List<PluginContext> LoadPlugins(string directory, string config,

if (csprojFile == null)
{
logger.LogWarning("No .csproj file found in the specified directory.");
logger?.LogWarning("No .csproj file found in the specified directory.");
return plugins;
}

Expand All @@ -32,46 +35,62 @@ internal static List<PluginContext> LoadPlugins(string directory, string config,
}
catch (Exception ex)
{
logger.LogError("Error loading .csproj file: {message}, {error}", ex.Message, ex);
logger?.LogError("Error loading .csproj file: {message}, {error}", ex.Message, ex);
return plugins;
}

var fullCsprojPath = Path.GetFullPath(csprojFile);

// ensure dotnet restore has been run
Process process = new()
// Get plugin information first
var nugetPlugins = GetNuGetPluginInfo(doc);
var projectPlugins = GetProjectRefPluginInfo(doc, directory, config);

// Check if all plugins are already valid (DLLs exist and have correct versions)
bool needsRestore = !AreAllPluginsValid(nugetPlugins, projectPlugins, logger);

if (needsRestore)
{
StartInfo = new ProcessStartInfo
logger?.LogDebug("Some plugin DLLs are missing or outdated, running dotnet restore...");

// ensure dotnet restore has been run
Process process = new()
{
FileName = "dotnet",
Arguments = $"restore {fullCsprojPath}",
WorkingDirectory = directory,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
};
StartInfo = new ProcessStartInfo
{
FileName = "dotnet",
Arguments = $"restore {fullCsprojPath}",
WorkingDirectory = directory,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
};

process.Start();
process.Start();

process.WaitForExit();
process.WaitForExit();

if (process.ExitCode != 0)
if (process.ExitCode != 0)
{
logger?.LogError("dotnet restore failed with exit code {exitCode}.", process.ExitCode);
logger?.LogDebug(process.StandardOutput.ReadToEnd());
logger?.LogError(process.StandardError.ReadToEnd());
return plugins;
}
}
else
{
logger.LogError("dotnet restore failed with exit code {exitCode}.", process.ExitCode);
logger.LogDebug(process.StandardOutput.ReadToEnd());
logger.LogError(process.StandardError.ReadToEnd());
return plugins;
logger?.LogDebug("All plugin DLLs are present and valid, skipping dotnet restore.");
}

LoadNuGetPlugins(doc, plugins, logger);
LoadProjectRefPlugins(doc, directory, config, plugins, logger);
LoadNuGetPlugins(nugetPlugins, plugins, logger);
LoadProjectRefPlugins(projectPlugins, plugins, config, logger);

return plugins;
}

private static void LoadNuGetPlugins(XDocument project, List<PluginContext> plugins, ILogger? logger)
private static List<NuGetPluginInfo> GetNuGetPluginInfo(XDocument project)
{
// Find the target framework dynamically from the .csproj file
var targetFramework = project.Descendants("TargetFramework")
Expand All @@ -81,7 +100,7 @@ private static void LoadNuGetPlugins(XDocument project, List<PluginContext> plug
var userHomeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var globalPackagesFolder = Path.Combine(userHomeDirectory, ".nuget", "packages");

var pluginFiles = project.Descendants("PackageReference")
return project.Descendants("PackageReference")
.Where(p => p.Attribute("Include")?.Value.StartsWith("BlakePlugin.", StringComparison.OrdinalIgnoreCase) == true)
.Where(p => !string.IsNullOrWhiteSpace(p.Attribute("Version")?.Value))
.Select(p =>
Expand All @@ -90,52 +109,146 @@ private static void LoadNuGetPlugins(XDocument project, List<PluginContext> plug
var packageVersion = p.Attribute("Version")!.Value;
var packageNameLower = packageName.ToLowerInvariant();

return Path.Combine(
var dllPath = Path.Combine(
globalPackagesFolder,
packageNameLower,
packageVersion,
"lib",
targetFramework, // e.g. "net9.0"
$"{packageName}.dll" // case-sensitive match here
);

return new NuGetPluginInfo(packageName, packageVersion, dllPath);
})
.Where(File.Exists)
.ToList();
}

if (pluginFiles.Count > 0)
private static List<ProjectRefPluginInfo> GetProjectRefPluginInfo(XDocument project, string projectDirectory, string configuration)
{
var projectReferences = project.Descendants("ProjectReference")
.Select(p => p.Attribute("Include")?.Value)
.Where(path => path is not null && Path.GetFileName(path).StartsWith("BlakePlugin."))
.Select(path => Path.GetFullPath(Path.Combine(projectDirectory, path!)))
.ToList();

if (projectReferences.Count == 0)
{
logger.LogInformation("Found {pluginCount} NuGet plugins in the .csproj file.", pluginFiles.Count);
LoadPluginDLLs(pluginFiles, plugins, logger);
return new List<ProjectRefPluginInfo>();
}
else

// Find the target framework dynamically from the .csproj file
var targetFramework = project.Descendants("TargetFramework")
.Select(tf => tf.Value)
.FirstOrDefault() ?? "net9.0";

return projectReferences.Select(pluginProject =>
{
logger.LogInformation("No NuGet plugins found in the .csproj file.");
var pluginName = Path.GetFileNameWithoutExtension(pluginProject);
var outputPath = Path.Combine(Path.GetDirectoryName(pluginProject)!, "bin", configuration, targetFramework, $"{pluginName}.dll");
return new ProjectRefPluginInfo(pluginProject, outputPath);
}).ToList();
}

private static bool IsNuGetPluginValid(NuGetPluginInfo plugin, ILogger? logger)
{
if (!File.Exists(plugin.DllPath))
{
logger?.LogDebug("NuGet plugin DLL not found: {dllPath}", plugin.DllPath);
return false;
}

try
{
var fileVersionInfo = FileVersionInfo.GetVersionInfo(plugin.DllPath);
var fileVersion = fileVersionInfo.FileVersion;

if (string.IsNullOrEmpty(fileVersion))
{
logger?.LogDebug("No file version found for NuGet plugin: {dllPath}", plugin.DllPath);
return true; // Assume valid if no version info
}

// For NuGet packages, the file version should match the package version
// Some packages may have different versioning schemes, so we'll be lenient
if (fileVersion.StartsWith(plugin.Version))
{
return true;
}
Comment on lines +173 to +176
Copy link

Copilot AI Aug 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using StartsWith for version comparison is unreliable and could lead to false positives. For example, version '1.0' would incorrectly match file version '1.0.1.2' or '1.00.0'. Consider using System.Version for proper semantic version comparison or exact string matching.

Suggested change
if (fileVersion.StartsWith(plugin.Version))
{
return true;
}
// Compare versions component-wise for leniency, but avoid false positives
if (Version.TryParse(plugin.Version, out var expectedVersion) && Version.TryParse(fileVersion, out var actualVersion))
{
// Compare only as many components as are present in plugin.Version
bool matches = true;
if (expectedVersion.Major != actualVersion.Major) matches = false;
if (expectedVersion.Minor != -1 && expectedVersion.Minor != actualVersion.Minor) matches = false;
if (expectedVersion.Build != -1 && expectedVersion.Build != actualVersion.Build) matches = false;
if (expectedVersion.Revision != -1 && expectedVersion.Revision != actualVersion.Revision) matches = false;
if (matches)
{
return true;
}
}
else
{
// Fallback: exact string match
if (fileVersion == plugin.Version)
{
return true;
}
}

Copilot uses AI. Check for mistakes.

logger?.LogDebug("Version mismatch for NuGet plugin {packageName}: expected {expectedVersion}, found {actualVersion}",
plugin.PackageName, plugin.Version, fileVersion);
return false;
}
catch (Exception ex)
{
logger?.LogDebug(ex, "Error checking version for NuGet plugin: {dllPath}", plugin.DllPath);
return false; // Assume invalid if we can't check version
Comment on lines +182 to +185
Copy link

Copilot AI Aug 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment suggests assuming invalid when version checking fails, but this could cause unnecessary restores on systems where FileVersionInfo.GetVersionInfo consistently fails due to file format issues. Consider logging the specific exception type and potentially having different fallback behavior for different exception types.

Suggested change
catch (Exception ex)
{
logger?.LogDebug(ex, "Error checking version for NuGet plugin: {dllPath}", plugin.DllPath);
return false; // Assume invalid if we can't check version
catch (NotSupportedException ex)
{
logger?.LogWarning(ex, "File format not supported when checking version for NuGet plugin: {dllPath}. Assuming valid.", plugin.DllPath);
return true; // Assume valid if file format is not supported
}
catch (Exception ex)
{
logger?.LogError(ex, "Unexpected error checking version for NuGet plugin: {dllPath}. Assuming invalid.", plugin.DllPath);
return false; // Assume invalid for other exceptions

Copilot uses AI. Check for mistakes.
}
}

private static void LoadProjectRefPlugins(XDocument project, string projectDirectory, string configuration, List<PluginContext> plugins, ILogger? logger)
private static bool AreAllPluginsValid(List<NuGetPluginInfo> nugetPlugins, List<ProjectRefPluginInfo> projectPlugins, ILogger? logger)
{
var projectReferences = project.Descendants("ProjectReference")
.Select(p => p.Attribute("Include")?.Value)
.Where(path => path is not null && Path.GetFileName(path).StartsWith("BlakePlugin."))
.Select(path => Path.GetFullPath(Path.Combine(projectDirectory, path!)))
// Check NuGet plugins (with version validation)
foreach (var plugin in nugetPlugins)
{
if (!IsNuGetPluginValid(plugin, logger))
{
return false;
}
}

// Check project reference plugins (just existence)
foreach (var plugin in projectPlugins)
{
if (!File.Exists(plugin.DllPath))
{
logger?.LogDebug("Project reference plugin DLL not found: {dllPath}", plugin.DllPath);
return false;
}
}

return true;
}

private static void LoadNuGetPlugins(List<NuGetPluginInfo> nugetPlugins, List<PluginContext> plugins, ILogger? logger)
{
var validPluginFiles = nugetPlugins
.Where(p => File.Exists(p.DllPath))
.Select(p => p.DllPath)
.ToList();

var dllFilePaths = new List<string>();
if (validPluginFiles.Count > 0)
{
logger?.LogInformation("Found {pluginCount} NuGet plugins in the .csproj file.", validPluginFiles.Count);
LoadPluginDLLs(validPluginFiles, plugins, logger);
}
else
{
logger?.LogInformation("No NuGet plugins found in the .csproj file.");
}
}

if (projectReferences.Count > 0)
private static void LoadProjectRefPlugins(List<ProjectRefPluginInfo> projectPlugins, List<PluginContext> plugins, string configuration, ILogger? logger)
{
if (projectPlugins.Count == 0)
{
// Find the target framework dynamically from the .csproj file
var targetFramework = project.Descendants("TargetFramework")
.Select(tf => tf.Value)
.FirstOrDefault() ?? "net9.0";
logger?.LogDebug("No project references found in the .csproj file.");
return;
}

foreach (var pluginProject in projectReferences)
var dllFilePaths = new List<string>();

foreach (var plugin in projectPlugins)
{
// Check if DLL exists, if not, build the project
if (!File.Exists(plugin.DllPath))
{
logger?.LogDebug("Project reference plugin DLL not found, building: {projectPath}", plugin.ProjectPath);

var result = Process.Start(new ProcessStartInfo
{
FileName = "dotnet",
Arguments = $"build \"{pluginProject}\" -c {configuration}",
Arguments = $"build \"{plugin.ProjectPath}\" -c {configuration}",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
Expand All @@ -145,29 +258,25 @@ private static void LoadProjectRefPlugins(XDocument project, string projectDirec

if (result?.ExitCode != 0)
{
logger.LogError("Failed to build {pluginProject}", pluginProject);
logger?.LogError("Failed to build {pluginProject}", plugin.ProjectPath);
continue;
}

var pluginName = Path.GetFileNameWithoutExtension(pluginProject);
var outputPath = Path.Combine(Path.GetDirectoryName(pluginProject)!, "bin", configuration, targetFramework, $"{pluginName}.dll");

dllFilePaths.Add(outputPath);
}

if (dllFilePaths.Count > 0)
if (File.Exists(plugin.DllPath))
{
logger.LogInformation("Found {dllFilePathsCount} project references in the .csproj file.", dllFilePaths.Count);
LoadPluginDLLs(dllFilePaths, plugins, logger);
}
else
{
logger.LogDebug("No project references found in the .csproj file.");
dllFilePaths.Add(plugin.DllPath);
}
}

if (dllFilePaths.Count > 0)
{
logger?.LogInformation("Found {dllFilePathsCount} project references in the .csproj file.", dllFilePaths.Count);
LoadPluginDLLs(dllFilePaths, plugins, logger);
}
else
{
logger.LogDebug("No project references found in the .csproj file.");
logger?.LogDebug("No project references found in the .csproj file.");
}
}

Expand All @@ -177,7 +286,7 @@ private static void LoadPluginDLLs(List<string> files, List<PluginContext> plugi
{
if (!File.Exists(file))
{
logger.LogError("Plugin file {file} does not exist.", file);
logger?.LogError("Plugin file {file} does not exist.", file);
continue;
}

Expand All @@ -202,8 +311,8 @@ private static void LoadPluginDLLs(List<string> files, List<PluginContext> plugi
}
catch (Exception ex)
{
logger.LogError("Error loading plugin from {file}: {message}", file, ex.Message);
logger.LogDebug(ex, "Full error details for plugin {file}", file);
logger?.LogError("Error loading plugin from {file}: {message}", file, ex.Message);
logger?.LogDebug(ex, "Full error details for plugin {file}", file);
}
}
}
Expand Down
Loading
Loading