| File: GenerateBundle.cs | Web Access |
| Project: src\sdk\src\Tasks\Microsoft.NET.Build.Tasks\Microsoft.NET.Build.Tasks.csproj (Microsoft.NET.Build.Tasks) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Build.Framework; using Microsoft.NET.HostModel.Bundle; namespace Microsoft.NET.Build.Tasks { [MSBuildMultiThreadableTask] public class GenerateBundle : TaskBase, ICancelableTask, IMultiThreadableTask { /// <inheritdoc /> public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback; private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private readonly Random _jitter = #if NET Random.Shared; #else new Random(); #endif [Required] public ITaskItem[] FilesToBundle { get; set; } = null!; [Required] public string AppHostName { get; set; } = null!; [Required] public bool IncludeSymbols { get; set; } [Required] public bool IncludeNativeLibraries { get; set; } [Required] public bool IncludeAllContent { get; set; } [Required] public string TargetFrameworkVersion { get; set; } = null!; [Required] public string RuntimeIdentifier { get; set; } = null!; [Required] public string OutputDir { get; set; } = null!; [Required] public bool ShowDiagnosticOutput { get; set; } [Required] public bool EnableCompressionInSingleFile { get; set; } public bool EnableMacOsCodeSign { get; set; } = true; [Output] public ITaskItem[] ExcludedFiles { get; set; } = null!; public int? RetryCount { get; set; } = 3; public void Cancel() => _cancellationTokenSource.Cancel(); /// <summary> /// Resolves a possibly-relative <paramref name="outputDir"/> to an absolute path, /// anchored at <see cref="TaskEnvironment.ProjectDirectory"/> rather than the process's /// current working directory. Exposed as internal for unit testing; production callers /// reach it through <see cref="ExecuteCore"/>. /// </summary> internal static string ResolveOutputDir(TaskEnvironment taskEnvironment, string outputDir) { AbsolutePath outputPath = string.IsNullOrEmpty(outputDir) ? taskEnvironment.ProjectDirectory : taskEnvironment.GetAbsolutePath(outputDir); return outputPath.Value; } private string ResolveFileSpecSourcePath(string sourcePath) { return string.IsNullOrWhiteSpace(sourcePath) ? sourcePath : TaskEnvironment.GetAbsolutePath(sourcePath).Value; } protected override void ExecuteCore() { ExecuteWithRetry().GetAwaiter().GetResult(); } private async System.Threading.Tasks.Task ExecuteWithRetry() { OSPlatform targetOS = RuntimeIdentifier.StartsWith("win") ? OSPlatform.Windows : RuntimeIdentifier.StartsWith("osx") ? OSPlatform.OSX : RuntimeIdentifier.StartsWith("freebsd") ? OSPlatform.Create("FREEBSD") : RuntimeIdentifier.StartsWith("openbsd") ? OSPlatform.Create("OPENBSD") : RuntimeIdentifier.StartsWith("illumos") ? OSPlatform.Create("ILLUMOS") : RuntimeIdentifier.StartsWith("solaris") ? OSPlatform.Create("SOLARIS") : RuntimeIdentifier.StartsWith("haiku") ? OSPlatform.Create("HAIKU") : OSPlatform.Linux; Architecture targetArch = RuntimeIdentifier.EndsWith("-x64") || RuntimeIdentifier.Contains("-x64-") ? Architecture.X64 : RuntimeIdentifier.EndsWith("-x86") || RuntimeIdentifier.Contains("-x86-") ? Architecture.X86 : RuntimeIdentifier.EndsWith("-arm64") || RuntimeIdentifier.Contains("-arm64-") ? Architecture.Arm64 : RuntimeIdentifier.EndsWith("-arm") || RuntimeIdentifier.Contains("-arm-") ? Architecture.Arm : #if !NETFRAMEWORK RuntimeIdentifier.EndsWith("-riscv64") || RuntimeIdentifier.Contains("-riscv64-") ? Architecture.RiscV64 : RuntimeIdentifier.EndsWith("-loongarch64") || RuntimeIdentifier.Contains("-loongarch64-") ? Architecture.LoongArch64 : #endif throw new ArgumentException(nameof(RuntimeIdentifier)); // Resolve OutputDir to an absolute path using TaskEnvironment so that relative // paths are interpreted against the project directory, not the process CWD. string resolvedOutputDir = ResolveOutputDir(TaskEnvironment, OutputDir); Version version = new(TargetFrameworkVersion); await RunBundler(resolvedOutputDir, targetOS, targetArch, version); } internal virtual async System.Threading.Tasks.Task RunBundler( string resolvedOutputDir, OSPlatform targetOS, Architecture targetArch, Version version) { BundleOptions options = BundleOptions.None; options |= IncludeNativeLibraries ? BundleOptions.BundleNativeBinaries : BundleOptions.None; options |= IncludeAllContent ? BundleOptions.BundleAllContent : BundleOptions.None; options |= IncludeSymbols ? BundleOptions.BundleSymbolFiles : BundleOptions.None; options |= EnableCompressionInSingleFile ? BundleOptions.EnableCompression : BundleOptions.None; var bundler = new Bundler( AppHostName, resolvedOutputDir, options, targetOS, targetArch, version, ShowDiagnosticOutput, macosCodesign: EnableMacOsCodeSign); var fileSpec = new List<FileSpec>(FilesToBundle.Length); foreach (var item in FilesToBundle) { fileSpec.Add(new FileSpec(sourcePath: ResolveFileSpecSourcePath(item.ItemSpec), bundleRelativePath: item.GetMetadata(MetadataKeys.RelativePath))); } // GenerateBundle has been throwing IOException intermittently in CI runs when accessing the singlefilehost binary specifically. // We hope that it's a Defender issue and that a quick retry will paper over the intermittent delay. await DoWithRetry(() => bundler.GenerateBundle(fileSpec)); // Certain files are excluded from the bundle, based on BundleOptions. // For example: // Native files and contents files are excluded by default. // hostfxr and hostpolicy are excluded until singlefilehost is available. // Return the set of excluded files in ExcludedFiles, so that they can be placed in the publish directory. ExcludedFiles = FilesToBundle.Zip(fileSpec, (item, spec) => (spec.Excluded) ? item : null).Where(x => x != null).ToArray()!; } public async System.Threading.Tasks.Task DoWithRetry(Action action) { bool triedOnce = false; while (RetryCount > 0 || !triedOnce) { if (_cancellationTokenSource.IsCancellationRequested) { break; } try { action(); break; } catch (IOException) when (RetryCount > 0) { Log.LogMessage(MessageImportance.High, $"Unable to access file during bundling. Retrying {RetryCount} more times..."); RetryCount--; await System.Threading.Tasks.Task.Delay(_jitter.Next(10, 50)); } } } } }