Skip to content

Commit

Permalink
Adds component root (#165)
Browse files Browse the repository at this point in the history
  • Loading branch information
PascalSenn authored May 7, 2024
1 parent 0a1a673 commit 9f8a1ee
Show file tree
Hide file tree
Showing 8 changed files with 144 additions and 99 deletions.
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

0 comments on commit 9f8a1ee

Please sign in to comment.