Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds component root #165

Merged
merged 2 commits into from
May 7, 2024
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -484,4 +484,6 @@ $RECYCLE.BIN/

#nextjs
.next/
out/
out/

.mono
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
<PackageVersion Include="System.Reactive" Version="6.0.0" />
<PackageVersion Include="System.Reflection.MetadataLoadContext" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Text.Json;
using Confix.Tool.Commands.Logging;
using Confix.Tool.Common.Pipelines;
using Confix.Tool.Middlewares.Encryption;
Expand Down
1 change: 1 addition & 0 deletions src/Confix.Tool/src/Confix.Library/Confix.Library.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="System.Reactive" />
<PackageReference Include="System.Reflection.MetadataLoadContext" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using Confix.Tool.Middlewares;
using Confix.Tool.Schema;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,79 @@ public async Task ExecuteAsync(IComponentProviderContext context)
throw new ExitException($"Failed to build project:\n{output}");
}

var projectAssembly = DotnetHelpers.GetAssemblyFileFromCsproj(csproj);

if (projectAssembly is not { Exists: true })
var projectAssembly = DotnetHelpers.GetAssemblyNameFromCsproj(csproj);
var components = await DiscoverResources(context.Logger, projectAssembly, projectDirectory);
foreach (var component in components)
{
context.Logger.ProjectNotFoundInDirectory(projectDirectory);
context.Logger.DotnetProjectWasNotDetected();
return;
context.Components.Add(component);
}
}

var resources =
DiscoverResources(context.Logger, projectAssembly, projectDirectory);
var components = await LoadComponents(resources);
foreach (var component in components)
private static async Task<IReadOnlyList<Component>> DiscoverResources(
IConsoleLogger logger,
string rootAssemblyName,
DirectoryInfo directory)
{
var discoveredResources = new List<DiscoveredResource>();

logger.FoundAssembly(rootAssemblyName);

var assembliesToScan = new Queue<string>();
var processedAssemblies = new HashSet<string>();

assembliesToScan.Enqueue(rootAssemblyName);

var assemblyResolver = DotnetHelpers.CreateAssemblyResolver(directory);
using var metadataLoadContext = new MetadataLoadContext(assemblyResolver);

while (assembliesToScan.TryDequeue(out var assemblyName))
{
context.Components.Add(component);
if (!processedAssemblies.Add(assemblyName))
{
continue;
}

logger.ScanningAssembly(assemblyName);

try
{
var assembly = metadataLoadContext.TryLoadAssembly(assemblyName);
if (assembly is null)
{
logger.AssemblyFileNotFound(assemblyName);
continue;
}

var isComponentRoot = assembly.IsComponentRoot();
if (isComponentRoot)
{
logger.DetectedComponentRoot(assemblyName);
}
else
{
var referencedAssemblies = assembly
.GetReferencedAssemblies()
.Where(x => !string.IsNullOrWhiteSpace(x.Name) &&
!x.Name.StartsWith("System", StringComparison.InvariantCulture) &&
!x.Name.StartsWith("Microsoft", StringComparison.InvariantCulture))
.ToArray();

referencedAssemblies.ForEach(x => assembliesToScan.Enqueue(x.Name!));
}

foreach (var resourceName in assembly.GetManifestResourceNames())
{
logger.FoundManifestResourceInAssembly(resourceName, assemblyName);
discoveredResources.Add(new DiscoveredResource(assembly, resourceName));
}
}
catch (BadImageFormatException ex)
{
logger.CouldNotLoadAssembly(assemblyName, ex);
}
}

return await LoadComponents(discoveredResources);
}

private static async Task<IReadOnlyList<Component>> LoadComponents(
Expand Down Expand Up @@ -142,65 +199,6 @@ private static async ValueTask<ComponentConfiguration> LoadComponentConfiguratio
}
}

private static IReadOnlyList<DiscoveredResource> DiscoverResources(
IConsoleLogger logger,
FileSystemInfo assemblyFile,
DirectoryInfo directory)
{
var discoveredResources = new List<DiscoveredResource>();

logger.FoundAssembly(assemblyFile);

var assembliesToScan = new Queue<string>();
var processedAssemblies = new HashSet<string>();

assembliesToScan.Enqueue(assemblyFile.Name[..^assemblyFile.Extension.Length]);

while (assembliesToScan.TryDequeue(out var assemblyName))
{
if (!processedAssemblies.Add(assemblyName))
{
continue;
}

logger.ScanningAssembly(assemblyName);

var assemblyFilePath = DotnetHelpers
.GetAssemblyInPathByName(directory, assemblyName);

if (assemblyFilePath is not { Exists: true })
{
logger.AssemblyFileNotFound(assemblyName);
continue;
}

try
{
logger.FoundAssemblyFile(assemblyFilePath);
var assembly = Assembly.LoadFile(assemblyFilePath.FullName);

assembly
.GetReferencedAssemblies()
.Where(x => !string.IsNullOrWhiteSpace(x.Name) &&
!x.Name.StartsWith("System", StringComparison.InvariantCulture) &&
!x.Name.StartsWith("Microsoft", StringComparison.InvariantCulture))
.ForEach(x => assembliesToScan.Enqueue(x.Name!));

foreach (var resourceName in assembly.GetManifestResourceNames())
{
logger.FoundManifestResourceInAssembly(resourceName, assemblyName);
discoveredResources.Add(new DiscoveredResource(assembly, resourceName));
}
}
catch (BadImageFormatException ex)
{
logger.CouldNotLoadAssembly(assemblyFile, ex);
}
}

return discoveredResources;
}

private record DiscoveredResource(Assembly Assembly, string ResourceName)
{
public Stream GetStream() => Assembly.GetManifestResourceStream(ResourceName) ??
Expand All @@ -211,6 +209,45 @@ public Stream GetStream() => Assembly.GetManifestResourceStream(ResourceName) ??

file static class Extensions
{
public static Assembly? TryLoadAssembly(this MetadataLoadContext context, string assemblyName)
{
try
{
return context
.GetAssemblies()
.FirstOrDefault(x => x.FullName == assemblyName) ??
context.LoadFromAssemblyName(assemblyName);
}
catch
{
return null;
}
}

public static bool IsComponentRoot(this Assembly assembly)
{
return assembly
.GetCustomAttributesData()
.Any(x =>
{
try
{
// even though both are assembly metadata attributes, they are not of the equal
// type, so we need to compare the full name
return x.AttributeType.FullName ==
typeof(AssemblyMetadataAttribute).FullName &&
x.ConstructorArguments is
[
{ Value: "IsConfixComponentRoot" }, { Value: "true" }
];
}
catch
{
return false;
}
});
}

public static void EnsureSolution(this IComponentProviderContext context)
{
if (context.Solution.Directory is not { Exists: true })
Expand All @@ -237,32 +274,27 @@ public static void StartLoadingComponents(this IConsoleLogger logger, string nam
logger.Debug($"Start loading components from project '{name}'");
}

public static void FoundAssembly(this IConsoleLogger logger, FileSystemInfo assembly)
public static void AssemblyFileNotFound(this IConsoleLogger logger, string assembly)
{
logger.Debug($"Found assembly: {assembly.Name}");
logger.Debug($"Assembly file not found for assembly: {assembly}");
}

public static void ScanningAssembly(this IConsoleLogger logger, string assembly)
public static void FoundAssembly(this IConsoleLogger logger, string assemblyName)
{
logger.Debug($"Scanning assembly: {assembly}");
logger.Debug($"Found assembly: {assemblyName}");
}

public static void FoundAssemblyFile(this IConsoleLogger logger, FileSystemInfo assembly)
public static void ScanningAssembly(this IConsoleLogger logger, string assembly)
{
logger.Debug($"Found assembly file: {assembly.FullName}");
logger.Debug($"Scanning assembly: {assembly}");
}

public static void CouldNotLoadAssembly(
this IConsoleLogger logger,
FileSystemInfo assembly,
string assembly,
Exception ex)
{
logger.Debug($"Could not load assembly: {assembly.FullName}. {ex.Message}");
}

public static void AssemblyFileNotFound(this IConsoleLogger logger, string assembly)
{
logger.Debug($"Assembly file not found for assembly: {assembly}");
logger.Debug($"Could not load assembly: {assembly}. {ex.Message}");
}

public static void FoundDotnetProject(this IConsoleLogger logger, FileSystemInfo csproj)
Expand Down Expand Up @@ -298,4 +330,12 @@ public static void ParsingComponent(
logger.Debug(
$"Parsing component from resource '{resourceName}' in assembly '{assembly.FullName}'");
}

public static void DetectedComponentRoot(
this IConsoleLogger logger,
string assembly)
{
logger.Inform(
$"Detected component root in assembly '{assembly}'. Skipping scanning referenced assemblies.");
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Xml;
using System.Xml.Linq;
Expand Down Expand Up @@ -32,7 +34,7 @@ public static async Task<ProcessExecutionResult> BuildProjectAsync(
return new ProcessExecutionResult(process.ExitCode == 0, output);
}

public static FileInfo? GetAssemblyFileFromCsproj(FileInfo projectFile)
public static string GetAssemblyNameFromCsproj(FileInfo projectFile)
{
// Load the .csproj file as an XDocument
var csprojDoc = XDocument.Load(projectFile.FullName, LoadOptions.PreserveWhitespace);
Expand All @@ -45,8 +47,7 @@ public static async Task<ProcessExecutionResult> BuildProjectAsync(
?.Value ??
Path.GetFileNameWithoutExtension(projectFile.FullName);

// Construct the path to where the assembly should be built
return GetAssemblyInPathByName(projectFile.Directory!, propertyGroup);
return propertyGroup;
}

public static async Task<string> EnsureUserSecretsIdAsync(
Expand Down Expand Up @@ -88,7 +89,7 @@ public static async Task<string> EnsureUserSecretsIdAsync(
propertyGroup.Add(new XElement(Xml.UserSecretsId, userSecretsId));

App.Log.AddedUserSecretsIdToTheCsprojFile(userSecretsId);

await csprojDoc.PrettifyAndSaveAsync(csprojFile.FullName, ct);
}
else
Expand All @@ -100,7 +101,7 @@ public static async Task<string> EnsureUserSecretsIdAsync(
}

public static async Task EnsureEmbeddedResourceAsync(
FileInfo csprojFile,
FileInfo csprojFile,
string path,
CancellationToken ct)
{
Expand Down Expand Up @@ -144,9 +145,7 @@ public static async Task EnsureEmbeddedResourceAsync(
}
}

public static FileInfo? GetAssemblyInPathByName(
DirectoryInfo projectDirectory,
string assemblyName)
public static PathAssemblyResolver CreateAssemblyResolver(DirectoryInfo projectDirectory)
{
var binDirectory = Path.Join(projectDirectory.FullName, "bin");
if (!Directory.Exists(binDirectory))
Expand All @@ -155,11 +154,14 @@ public static async Task EnsureEmbeddedResourceAsync(
$"The directory '{binDirectory}' was not found. Make sure to build the project first.");
}

var firstMatch = Directory
.EnumerateFiles(binDirectory, assemblyName + ".dll", SearchOption.AllDirectories)
.FirstOrDefault();
var appAssembly = Directory
.EnumerateFiles(binDirectory, "*.dll", SearchOption.AllDirectories)
.DistinctBy(Path.GetFileName);

return firstMatch is null ? null : new FileInfo(firstMatch);
var runtimeAssemblies = Directory
.GetFiles(RuntimeEnvironment.GetRuntimeDirectory(), "*.dll");

return new PathAssemblyResolver(appAssembly.Concat(runtimeAssemblies));
}

public static FileInfo? FindProjectFileInPath(DirectoryInfo directory)
Expand All @@ -177,7 +179,7 @@ private static async Task PrettifyAndSaveAsync(
settings.Indent = true;
settings.Async = true;
settings.OmitXmlDeclaration = true;

var formattedCsproj = new StringBuilder();
await using var writer = XmlWriter.Create(formattedCsproj, settings);
await xDocument.WriteToAsync(writer, ct);
Expand Down
1 change: 1 addition & 0 deletions src/Confix.Tool/src/Confix.Tool/Confix.Tool.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="System.Reactive" />
<PackageReference Include="System.Reflection.MetadataLoadContext" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading