src/Cli/ArtifactAssembler/ArtifactAssembler.cs (338 lines of code) (raw):
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
using System.Text.RegularExpressions;
namespace Azure.Functions.Cli.ArtifactAssembler
{
internal sealed partial class ArtifactAssembler
{
/// <summary>
/// The artifacts for which we want to pack a custom host with it.
/// This dictionary contains the artifact name and the corresponding runtime identifier value.
/// </summary>
private readonly Dictionary<string, string> _visualStudioArtifacts = new()
{
{ "Azure.Functions.Cli.min.win-x64", "win-x64" },
{ "Azure.Functions.Cli.min.win-arm64", "win-arm64" },
{ "Azure.Functions.Cli.linux-x64", "linux-x64" }
};
private readonly string[] _net8OsxArtifacts =
[
"Azure.Functions.Cli.osx-x64",
"Azure.Functions.Cli.osx-arm64",
];
/// <summary>
/// The artifacts for which we want to pack out-of-proc core tools with it (along with inproc6 and inproc8 directories).
/// </summary>
private readonly string[] _cliArtifacts =
[
"Azure.Functions.Cli.min.win-arm64",
"Azure.Functions.Cli.min.win-x86",
"Azure.Functions.Cli.min.win-x64",
"Azure.Functions.Cli.linux-x64",
"Azure.Functions.Cli.osx-x64",
"Azure.Functions.Cli.osx-arm64",
"Azure.Functions.Cli.win-x86",
"Azure.Functions.Cli.win-x64",
"Azure.Functions.Cli.win-arm64"
];
private readonly string _inProcArtifactDirectoryName;
private readonly string _coreToolsHostArtifactDirectoryName;
private readonly string _outOfProcArtifactDirectoryName;
private readonly string _inProc6ArtifactName;
private readonly string _inProc8ArtifactName;
private readonly string _coreToolsHostWindowsArtifactName;
private readonly string _coreToolsHostLinuxArtifactName;
private readonly string _outOfProcArtifactName;
private readonly string _rootWorkingDirectory;
private readonly string _stagingDirectory;
private readonly string _artifactName;
private string _inProc6ExtractedRootDir = string.Empty;
private string _inProc8ExtractedRootDir = string.Empty;
private string _coreToolsHostExtractedRootDir = string.Empty;
private string _outOfProcExtractedRootDir = string.Empty;
internal ArtifactAssembler(string rootWorkingDirectory, string artifactName)
{
_inProcArtifactDirectoryName = GetRequiredEnvironmentVariable(EnvironmentVariables.InProcArtifactAlias);
_coreToolsHostArtifactDirectoryName = GetRequiredEnvironmentVariable(EnvironmentVariables.CoreToolsHostArtifactAlias);
_outOfProcArtifactDirectoryName = GetRequiredEnvironmentVariable(EnvironmentVariables.OutOfProcArtifactAlias);
_inProc6ArtifactName = GetRequiredEnvironmentVariable(EnvironmentVariables.InProc6ArtifactName);
_inProc8ArtifactName = GetRequiredEnvironmentVariable(EnvironmentVariables.InProc8ArtifactName);
_coreToolsHostWindowsArtifactName = GetRequiredEnvironmentVariable(EnvironmentVariables.CoreToolsHostWindowsArtifactName);
_coreToolsHostLinuxArtifactName = GetRequiredEnvironmentVariable(EnvironmentVariables.CoreToolsHostLinuxArtifactName);
_outOfProcArtifactName = GetRequiredEnvironmentVariable(EnvironmentVariables.OutOfProcArtifactName);
_rootWorkingDirectory = rootWorkingDirectory;
_stagingDirectory = CreateStagingDirectory(_rootWorkingDirectory);
_artifactName = artifactName;
}
internal void Assemble()
{
ExtractDownloadedArtifacts();
CreateCliCoreTools();
CreateVisualStudioCoreTools();
}
private static string GetRequiredEnvironmentVariable(string variableName)
{
return Environment.GetEnvironmentVariable(variableName)
?? throw new InvalidDataException($"The `{variableName}` environment variable value is missing!");
}
private void ExtractDownloadedArtifacts()
{
var inProcArtifactDownloadDir = Path.Combine(_rootWorkingDirectory, _inProcArtifactDirectoryName);
var coreToolsHostArtifactDownloadDir = Path.Combine(_rootWorkingDirectory, _coreToolsHostArtifactDirectoryName);
var outOfProcArtifactDownloadDir = Path.Combine(_rootWorkingDirectory, _outOfProcArtifactDirectoryName);
var inProc6ArtifactDirPath = Path.Combine(inProcArtifactDownloadDir, _inProc6ArtifactName);
var inProc8ArtifactDirPath = Path.Combine(inProcArtifactDownloadDir, _inProc8ArtifactName);
var outOfProcArtifactDirPath = Path.Combine(outOfProcArtifactDownloadDir, _outOfProcArtifactName);
var coreToolsHostWindowsArtifactDirPath = Path.Combine(coreToolsHostArtifactDownloadDir, _coreToolsHostWindowsArtifactName);
var coreToolsHostLinuxArtifactDirPath = Path.Combine(coreToolsHostArtifactDownloadDir, _coreToolsHostLinuxArtifactName);
CheckIfArtifactDirectoryExists(inProc6ArtifactDirPath);
CheckIfArtifactDirectoryExists(inProc8ArtifactDirPath);
CheckIfArtifactDirectoryExists(outOfProcArtifactDirPath);
CheckIfArtifactDirectoryExists(coreToolsHostWindowsArtifactDirPath);
CheckIfArtifactDirectoryExists(coreToolsHostLinuxArtifactDirPath);
_inProc6ExtractedRootDir = PrepareStagingDirectory(inProc6ArtifactDirPath, Path.Combine(_stagingDirectory, Constants.InProc6DirectoryName));
_inProc8ExtractedRootDir = PrepareStagingDirectory(inProc8ArtifactDirPath, Path.Combine(_stagingDirectory, Constants.InProc8DirectoryName));
Directory.Delete(inProcArtifactDownloadDir, true);
_coreToolsHostExtractedRootDir = PrepareStagingDirectory(coreToolsHostWindowsArtifactDirPath, Path.Combine(_stagingDirectory, Constants.CoreToolsHostDirectoryName), true);
PrepareStagingDirectory(coreToolsHostLinuxArtifactDirPath, Path.Combine(_stagingDirectory, Constants.CoreToolsHostDirectoryName), true);
Directory.Delete(coreToolsHostArtifactDownloadDir, true);
_outOfProcExtractedRootDir = PrepareStagingDirectory(outOfProcArtifactDirPath, Path.Combine(_stagingDirectory, Constants.OutOfProcDirectoryName));
Directory.Delete(outOfProcArtifactDownloadDir, true);
}
private static void CheckIfArtifactDirectoryExists(string directoryExist)
{
if (!Directory.Exists(directoryExist))
{
throw new InvalidOperationException($"Artifact directory '{directoryExist}' not found!");
}
}
private static void EnsureDirectoryExists(string path)
{
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
}
private static string CreateStagingDirectory(string rootWorkingDirectory)
{
var stagingDirectory = Path.Combine(rootWorkingDirectory, Constants.StagingDirName);
if (Directory.Exists(stagingDirectory))
{
Console.WriteLine($"Directory '{stagingDirectory}' already exists, deleting...");
Directory.Delete(stagingDirectory, true);
}
Directory.CreateDirectory(stagingDirectory);
Console.WriteLine($"Created staging directory: {stagingDirectory}");
return stagingDirectory;
}
private string PrepareStagingDirectory(string artifactZipPath, string destinationDirectory, bool isCoreToolsHost = false)
{
EnsureDirectoryExists(destinationDirectory);
if (!string.IsNullOrEmpty(_artifactName))
{
if (isCoreToolsHost)
{
CopyCoreToolsArtifactIfExists(artifactZipPath, destinationDirectory);
return destinationDirectory;
}
ExtractZipFilesInDirectory(artifactZipPath, destinationDirectory);
}
else
{
FileUtilities.CopyDirectory(artifactZipPath, destinationDirectory);
ExtractZipFilesInDirectory(artifactZipPath, destinationDirectory);
}
DeleteUnnecessaryFiles(destinationDirectory);
return destinationDirectory;
}
private void CopyCoreToolsArtifactIfExists(string artifactZipPath, string destinationDirectory)
{
if (_visualStudioArtifacts.TryGetValue(_artifactName, out var fileNameToSearchFor) &&
!string.IsNullOrEmpty(fileNameToSearchFor))
{
var sourceDir = Path.Combine(artifactZipPath, fileNameToSearchFor);
var destDir = Path.Combine(destinationDirectory, fileNameToSearchFor);
if (Directory.Exists(sourceDir))
{
FileUtilities.CopyDirectory(sourceDir, destDir);
}
}
}
private static void DeleteUnnecessaryFiles(string directory)
{
var filesToBeDeleted = Directory.EnumerateFiles(directory);
foreach (var file in filesToBeDeleted)
{
File.Delete(file);
}
}
// Gets the product version part from the artifact directory name.
// Example input: Azure.Functions.Cli.min.win-x64.4.0.6353
// Example output: 4.0.6353
private static string GetCoreToolsProductVersion(string artifactDirectoryName)
{
var match = CoreToolsVersionRegex().Match(artifactDirectoryName);
if (match.Success)
{
return match.Value;
}
throw new InvalidOperationException($"The artifact directory name '{artifactDirectoryName}' does not include a core tools product version in the expected format (e.g., '4.0.6353'). Please ensure the directory name follows the correct naming convention.");
}
private void CreateVisualStudioCoreTools()
{
Console.WriteLine("Starting to assemble Visual Studio Core Tools artifacts");
bool artifactNameProvided = !string.IsNullOrEmpty(_artifactName);
bool isValidVisualStudioArtifact = _visualStudioArtifacts.ContainsKey(_artifactName);
// Create a directory to store the assembled artifacts.
var customHostTargetArtifactDir = Path.Combine(_stagingDirectory, Constants.VisualStudioOutputArtifactDirectoryName);
Directory.CreateDirectory(customHostTargetArtifactDir);
string[] visualStudioArtifactList = artifactNameProvided ? [_artifactName] : [.. _visualStudioArtifacts.Keys];
foreach (string artifactName in visualStudioArtifactList)
{
// Break early if we don't need to assemble VS artifacts for specified artifactName
if (artifactNameProvided && !isValidVisualStudioArtifact)
{
break;
}
var (artifactDirName, consolidatedArtifactDirPath) = CreateInProc8CoreToolsHostHelper(artifactName, customHostTargetArtifactDir, createDirectory: true);
// Copy in-proc6 files and delete directory after
var inProc6ArtifactDirPath = Path.Combine(_inProc6ExtractedRootDir, artifactDirName);
CheckIfArtifactDirectoryExists(inProc6ArtifactDirPath);
FileUtilities.CopyDirectory(inProc6ArtifactDirPath, Path.Combine(consolidatedArtifactDirPath, Constants.InProc6DirectoryName));
Directory.Delete(inProc6ArtifactDirPath, true);
// Copy core-tools-host files
var rid = GetRuntimeIdentifierForArtifactName(artifactName);
var coreToolsHostArtifactDirPath = Path.Combine(_coreToolsHostExtractedRootDir, rid);
CheckIfArtifactDirectoryExists(coreToolsHostArtifactDirPath);
FileUtilities.CopyDirectory(coreToolsHostArtifactDirPath, consolidatedArtifactDirPath);
Directory.Delete(coreToolsHostArtifactDirPath, true);
}
// Generate .NET 8 OSX fallback artifacts
if (artifactNameProvided && _net8OsxArtifacts.Contains(_artifactName))
{
CreateInProc8CoreToolsHostHelper(_artifactName, customHostTargetArtifactDir, createDirectory: false);
}
else if (!artifactNameProvided)
{
// Create artifacts for .NET 8 OSX to use instead of the custom host
foreach (var osxArtifact in _net8OsxArtifacts)
{
CreateInProc8CoreToolsHostHelper(osxArtifact, customHostTargetArtifactDir, createDirectory: false);
}
}
// Delete directories
Directory.Delete(_inProc6ExtractedRootDir, true);
Directory.Delete(_inProc8ExtractedRootDir, true);
Directory.Delete(_coreToolsHostExtractedRootDir, true);
Console.WriteLine("Finished assembling Visual Studio Core Tools artifacts");
}
// This method creates a new directory for the core tools host and copies the inproc8 files
private (string ArtifactDirName, string ConsolidatedArtifactDirPath) CreateInProc8CoreToolsHostHelper(
string artifactName, string customHostTargetArtifactDir, bool createDirectory)
{
var inProcArtifactDirPath = Directory
.EnumerateDirectories(_inProc8ExtractedRootDir)
.FirstOrDefault(dir => dir.Contains(artifactName))
?? throw new InvalidOperationException($"Artifact directory for '{artifactName}' not found.");
// Create a new directory to store the custom host.
var artifactDirName = Path.GetFileName(inProcArtifactDirPath);
var version = GetCoreToolsProductVersion(artifactDirName);
var consolidatedArtifactDirName = $"{artifactName}{Constants.InProcOutputArtifactNameSuffix}.{version}";
var consolidatedArtifactDirPath = Path.Combine(customHostTargetArtifactDir, consolidatedArtifactDirName);
Directory.CreateDirectory(consolidatedArtifactDirPath);
// Copy in-proc8 files and delete directory after
var copyTargetPath = createDirectory
? Path.Combine(consolidatedArtifactDirPath, Constants.InProc8DirectoryName)
: consolidatedArtifactDirPath;
FileUtilities.CopyDirectory(inProcArtifactDirPath, copyTargetPath);
Directory.Delete(inProcArtifactDirPath, true);
return (artifactDirName, consolidatedArtifactDirPath);
}
private void CreateCliCoreTools()
{
Console.WriteLine("Starting to assemble CLI Core Tools artifacts");
// Create a directory to store the assembled artifacts.
var cliCoreToolsTargetArtifactDir = Path.Combine(_stagingDirectory, Constants.CliOutputArtifactDirectoryName);
Directory.CreateDirectory(cliCoreToolsTargetArtifactDir);
string outOfProcVersion = string.Empty,
inProcVersion = string.Empty,
outOfProcArtifactDirPath = string.Empty,
inProc8ArtifactDirPath = string.Empty;
string[] cliArtifactList = string.IsNullOrEmpty(_artifactName) ? _cliArtifacts : [_artifactName];
foreach (var artifactName in cliArtifactList)
{
// Set up out-of-proc artifact directory and version
if (string.IsNullOrEmpty(outOfProcArtifactDirPath))
{
var (artifactDirectory, version) = GetArtifactDirectoryAndVersionNumber(_outOfProcExtractedRootDir, artifactName);
outOfProcArtifactDirPath = artifactDirectory;
outOfProcVersion = version;
}
else
{
var artifactNameWithVersion = $"{artifactName}.{outOfProcVersion}";
outOfProcArtifactDirPath = Path.Combine(_outOfProcExtractedRootDir, artifactNameWithVersion);
}
// Create a new directory to store the oop core tools with in-proc8 and in-proc6 files.
var consolidatedArtifactDirPath = Path.Combine(cliCoreToolsTargetArtifactDir, Path.GetFileName(outOfProcArtifactDirPath));
Directory.CreateDirectory(consolidatedArtifactDirPath);
// Copy oop core tools and delete old directory
CheckIfArtifactDirectoryExists(outOfProcArtifactDirPath);
FileUtilities.CopyDirectory(outOfProcArtifactDirPath, consolidatedArtifactDirPath);
Directory.Delete(outOfProcArtifactDirPath, true);
// If we are currently on the minified version of the artifacts, we do not want the inproc6/inproc8 subfolders
if (artifactName.Contains("min.win"))
{
Console.WriteLine($"Finished assembling {consolidatedArtifactDirPath}\n");
continue;
}
// If we are running this for the first time, extract the directory path and out of proc version
if (string.IsNullOrEmpty(inProc8ArtifactDirPath))
{
// Get the version number from the in-proc build since it will be different than out-of-proc
var (artifactDirectory, version) = GetArtifactDirectoryAndVersionNumber(_inProc8ExtractedRootDir, artifactName);
inProc8ArtifactDirPath = artifactDirectory;
inProcVersion = version;
}
else
{
var artifactNameWithVersion = $"{artifactName}.{inProcVersion}";
inProc8ArtifactDirPath = Path.Combine(_inProc8ExtractedRootDir, artifactNameWithVersion);
}
// Copy in-proc8 files
var inProc8FinalDestination = Path.Combine(consolidatedArtifactDirPath, Constants.InProc8DirectoryName);
FileUtilities.CopyDirectory(inProc8ArtifactDirPath, inProc8FinalDestination);
Console.WriteLine($"Copied files from {inProc8ArtifactDirPath} => {inProc8FinalDestination}");
// Rename inproc6 directory to have the same version as the out-of-proc artifact before copying
var inProcArtifactName = Path.GetFileName(inProc8ArtifactDirPath);
var inProc6ArtifactDirPath = Path.Combine(_inProc6ExtractedRootDir, inProcArtifactName);
CheckIfArtifactDirectoryExists(inProc6ArtifactDirPath);
// Copy in-proc6 files
var inProc6FinalDestination = Path.Combine(consolidatedArtifactDirPath, Constants.InProc6DirectoryName);
FileUtilities.CopyDirectory(inProc6ArtifactDirPath, inProc6FinalDestination);
Console.WriteLine($"Copied files from {inProc6ArtifactDirPath} => {inProc6FinalDestination}");
Console.WriteLine($"Finished assembling {consolidatedArtifactDirPath}\n");
}
Directory.Delete(_outOfProcExtractedRootDir, true);
Console.WriteLine("Finished assembling CLI Core Tools artifacts\n");
}
private static (string ArtifactDirectory, string Version) GetArtifactDirectoryAndVersionNumber(string extractedRootDirectory, string artifactName)
{
var artifactDirPath = Directory.EnumerateDirectories(extractedRootDirectory)
.FirstOrDefault(dir => dir.Contains(artifactName));
if (artifactDirPath is null)
{
throw new InvalidOperationException($"Artifact directory '{artifactDirPath}' not found!");
}
var version = GetCoreToolsProductVersion(artifactDirPath);
return (artifactDirPath, version);
}
private string GetRuntimeIdentifierForArtifactName(string artifactName)
{
if (_visualStudioArtifacts.TryGetValue(artifactName, out var rid))
{
return rid;
}
throw new InvalidOperationException($"Runtime identifier (RID) not found for artifact name '{artifactName}'.");
}
private void ExtractZipFilesInDirectory(string zipSourceDir, string extractDestinationDir)
{
if (!Directory.Exists(zipSourceDir))
{
Console.WriteLine($"Directory '{zipSourceDir}' does not exist.");
return;
}
var zipFiles = Directory.GetFiles(zipSourceDir, "*.zip");
if (!string.IsNullOrEmpty(_artifactName))
{
zipFiles = [.. zipFiles.Where(file => Path.GetFileName(file).StartsWith(_artifactName, StringComparison.OrdinalIgnoreCase))];
}
Console.WriteLine($"{zipFiles.Length} zip file(s) found in '{zipSourceDir}'.");
foreach (var zipFile in zipFiles)
{
var fileName = Path.GetFileName(zipFile);
var destinationDir = Path.Combine(extractDestinationDir, Path.GetFileNameWithoutExtension(zipFile));
if (!string.IsNullOrEmpty(_artifactName))
{
var destZipFile = Path.Combine(extractDestinationDir, fileName);
File.Copy(zipFile, destZipFile, overwrite: true);
Console.WriteLine($"Copied '{zipFile}' to '{destZipFile}'");
}
Console.WriteLine($"Extracting '{zipFile}' to '{destinationDir}'");
FileUtilities.ExtractToDirectory(zipFile, destinationDir);
File.Delete(zipFile);
}
}
[GeneratedRegex(Constants.CoreToolsProductVersionPattern)]
private static partial Regex CoreToolsVersionRegex();
}
}