File: Commands\LayoutCommand.cs
Web Access
Project: src\src\Aspire.Cli.NuGetHelper\Aspire.Cli.NuGetHelper.csproj (aspire-nuget)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.CommandLine;
using System.Globalization;
using NuGet.ProjectModel;
 
namespace Aspire.Cli.NuGetHelper.Commands;
 
/// <summary>
/// Layout command - creates a flat DLL layout from a project.assets.json file.
/// This enables the AppHost Server to load integration assemblies via probing paths.
/// </summary>
public static class LayoutCommand
{
    /// <summary>
    /// Creates the layout command.
    /// </summary>
    public static Command Create()
    {
        var command = new Command("layout", "Create flat DLL layout from project.assets.json");
 
        var assetsOption = new Option<string>("--assets", "-a")
        {
            Description = "Path to project.assets.json file",
            Required = true
        };
        command.Options.Add(assetsOption);
 
        var outputOption = new Option<string>("--output", "-o")
        {
            Description = "Output directory for flat DLL layout",
            Required = true
        };
        command.Options.Add(outputOption);
 
        var frameworkOption = new Option<string>("--framework", "-f")
        {
            Description = "Target framework (default: net10.0)",
            DefaultValueFactory = _ => "net10.0"
        };
        command.Options.Add(frameworkOption);
 
        var verboseOption = new Option<bool>("--verbose", "-v")
        {
            Description = "Enable verbose output"
        };
        command.Options.Add(verboseOption);
 
        command.SetAction((parseResult, ct) =>
        {
            var assetsPath = parseResult.GetValue(assetsOption)!;
            var outputPath = parseResult.GetValue(outputOption)!;
            var framework = parseResult.GetValue(frameworkOption)!;
            var verbose = parseResult.GetValue(verboseOption);
 
            return Task.FromResult(ExecuteLayout(assetsPath, outputPath, framework, verbose));
        });
 
        return command;
    }
 
    private static int ExecuteLayout(
        string assetsPath,
        string outputPath,
        string framework,
        bool verbose)
    {
        if (!File.Exists(assetsPath))
        {
            Console.Error.WriteLine($"Error: Assets file not found: {assetsPath}");
            return 1;
        }
 
        try
        {
            // Parse the lock file
            var lockFileFormat = new LockFileFormat();
            var lockFile = lockFileFormat.Read(assetsPath);
 
            if (lockFile == null)
            {
                Console.Error.WriteLine("Error: Failed to parse project.assets.json");
                return 1;
            }
 
            // Find the target for our framework
            var target = lockFile.Targets.FirstOrDefault(t =>
                t.TargetFramework.GetShortFolderName().Equals(framework, StringComparison.OrdinalIgnoreCase) ||
                t.TargetFramework.ToString().Equals(framework, StringComparison.OrdinalIgnoreCase));
 
            if (target == null)
            {
                Console.Error.WriteLine($"Error: Target framework '{framework}' not found in assets file");
                Console.Error.WriteLine($"Available targets: {string.Join(", ", lockFile.Targets.Select(t => t.TargetFramework.GetShortFolderName()))}");
                return 1;
            }
 
            // Create output directory
            Directory.CreateDirectory(outputPath);
 
            var copiedCount = 0;
            var skippedCount = 0;
 
            // Get the packages path from the lock file
            var packagesPath = lockFile.PackageFolders.FirstOrDefault()?.Path;
            if (string.IsNullOrEmpty(packagesPath))
            {
                packagesPath = Path.Combine(
                    Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
                    ".nuget", "packages");
            }
 
            if (verbose)
            {
                Console.WriteLine($"Using packages path: {packagesPath}");
                Console.WriteLine($"Target framework: {target.TargetFramework.GetShortFolderName()}");
                Console.WriteLine(string.Format(CultureInfo.InvariantCulture, "Libraries: {0}", target.Libraries.Count));
            }
 
            // Process each library in the target
            foreach (var library in target.Libraries)
            {
                if (library.Type != "package")
                {
                    continue;
                }
 
                // Get the package folder
                var libraryName = library.Name ?? string.Empty;
                var libraryVersion = library.Version?.ToString() ?? string.Empty;
                var packagePath = Path.Combine(packagesPath, libraryName.ToLowerInvariant(), libraryVersion);
 
                if (!Directory.Exists(packagePath))
                {
                    if (verbose)
                    {
                        Console.WriteLine($"  Skip (not found): {libraryName}/{libraryVersion} at {packagePath}");
                    }
 
                    skippedCount++;
                    continue;
                }
 
                // Copy runtime assemblies
                foreach (var runtimeAssembly in library.RuntimeAssemblies)
                {
                    // Skip placeholder files
                    if (runtimeAssembly.Path.EndsWith("_._", StringComparison.OrdinalIgnoreCase))
                    {
                        continue;
                    }
 
                    var sourcePath = Path.Combine(packagePath, runtimeAssembly.Path.Replace('/', Path.DirectorySeparatorChar));
 
                    // Early exit if source doesn't exist
                    if (!File.Exists(sourcePath))
                    {
                        continue;
                    }
 
                    var fileName = Path.GetFileName(sourcePath);
                    var destPath = Path.Combine(outputPath, fileName);
 
                    // Only copy if newer or doesn't exist
                    if (!File.Exists(destPath) ||
                        File.GetLastWriteTimeUtc(sourcePath) > File.GetLastWriteTimeUtc(destPath))
                    {
                        File.Copy(sourcePath, destPath, overwrite: true);
                        copiedCount++;
 
                        if (verbose)
                        {
                            Console.WriteLine($"  Copy: {sourcePath} -> {destPath}");
                        }
                    }
 
                    // Also copy the XML documentation file if it exists alongside the assembly
                    var xmlSourcePath = Path.ChangeExtension(sourcePath, ".xml");
                    if (File.Exists(xmlSourcePath))
                    {
                        var xmlDestPath = Path.ChangeExtension(destPath, ".xml");
                        if (!File.Exists(xmlDestPath) ||
                            File.GetLastWriteTimeUtc(xmlSourcePath) > File.GetLastWriteTimeUtc(xmlDestPath))
                        {
                            File.Copy(xmlSourcePath, xmlDestPath, overwrite: true);
                            copiedCount++;
 
                            if (verbose)
                            {
                                Console.WriteLine($"  Copy (xml): {xmlSourcePath} -> {xmlDestPath}");
                            }
                        }
                    }
                }
 
                // Also copy native libraries if present
                foreach (var nativeLib in library.NativeLibraries)
                {
                    var sourcePath = Path.Combine(packagePath, nativeLib.Path.Replace('/', Path.DirectorySeparatorChar));
 
                    // Early exit if source doesn't exist
                    if (!File.Exists(sourcePath))
                    {
                        continue;
                    }
 
                    var fileName = Path.GetFileName(sourcePath);
                    var destPath = Path.Combine(outputPath, fileName);
 
                    if (!File.Exists(destPath) ||
                        File.GetLastWriteTimeUtc(sourcePath) > File.GetLastWriteTimeUtc(destPath))
                    {
                        File.Copy(sourcePath, destPath, overwrite: true);
                        copiedCount++;
 
                        if (verbose)
                        {
                            Console.WriteLine($"  Copy (native): {sourcePath} -> {destPath}");
                        }
                    }
                }
            }
 
            Console.WriteLine(string.Format(CultureInfo.InvariantCulture, "Layout created: {0} files copied to {1}", copiedCount, outputPath));
            if (skippedCount > 0 && verbose)
            {
                Console.WriteLine(string.Format(CultureInfo.InvariantCulture, "  ({0} packages skipped - not found in cache)", skippedCount));
            }
 
            return 0;
        }
        catch (Exception ex)
        {
            Console.Error.WriteLine($"Error: {ex.Message}");
            if (verbose)
            {
                Console.Error.WriteLine(ex.StackTrace);
            }
 
            return 1;
        }
    }
}