From 4b4b476ed93721903cfdb5b1b1064fca02faee97 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:46:14 +0000 Subject: [PATCH 1/5] Initial plan From 911579cdbcb907ea88cfd8f7b24978d4c74c0e39 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:56:05 +0000 Subject: [PATCH 2/5] Make CLI bundle self-extraction conditional based on distribution mode Add SelfExtractingBundle MSBuild property (default false, true for dotnet-tool) that gets baked into the assembly as metadata. BundleService reads the flag and skips self-extraction when disabled, relying on pre-extracted layouts for archive/installer distributions. Agent-Logs-Url: https://github.com/microsoft/aspire/sessions/62830c5d-e5f0-4741-a5f9-5dc8dc84a82a Co-authored-by: radical <1472+radical@users.noreply.github.com> --- src/Aspire.Cli/Aspire.Cli.csproj | 13 +++++++++++++ src/Aspire.Cli/Bundles/BundleService.cs | 16 ++++++++++++++++ src/Aspire.Cli/Bundles/IBundleService.cs | 6 ++++++ tests/Aspire.Cli.Tests/BundleServiceTests.cs | 12 ++++++++++++ tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs | 6 +++++- 5 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/Aspire.Cli/Aspire.Cli.csproj b/src/Aspire.Cli/Aspire.Cli.csproj index a3633f025f6..4ebd7520cd5 100644 --- a/src/Aspire.Cli/Aspire.Cli.csproj +++ b/src/Aspire.Cli/Aspire.Cli.csproj @@ -24,6 +24,9 @@ $(DefineConstants);CLI true false + + false @@ -39,12 +42,22 @@ To do this we make sure it doesn't PublishAOT, and ensure that it doesn't publish for any RIDs at all. --> false + + true + + + + <_Parameter1>SelfExtractingBundle + <_Parameter2>$(SelfExtractingBundle) + + + diff --git a/src/Aspire.Cli/Bundles/BundleService.cs b/src/Aspire.Cli/Bundles/BundleService.cs index c06ff55b5b1..7cb56d1bd78 100644 --- a/src/Aspire.Cli/Bundles/BundleService.cs +++ b/src/Aspire.Cli/Bundles/BundleService.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Formats.Tar; using System.IO.Compression; +using System.Reflection; using Aspire.Cli.Layout; using Aspire.Cli.Utils; using Aspire.Shared; @@ -26,9 +27,18 @@ internal sealed class BundleService(ILayoutDiscovery layoutDiscovery, ILogger() + .FirstOrDefault(m => string.Equals(m.Key, "SelfExtractingBundle", StringComparison.OrdinalIgnoreCase))?.Value, + "true", + StringComparison.OrdinalIgnoreCase); + /// public bool IsBundle => s_isBundle; + /// + public bool IsSelfExtracting => s_isSelfExtracting; + /// /// Opens a read-only stream over the embedded bundle payload. /// Returns if no payload is embedded. @@ -54,6 +64,12 @@ public async Task EnsureExtractedAsync(CancellationToken cancellationToken = def return; } + if (!IsSelfExtracting) + { + logger.LogDebug("Self-extraction is disabled for this distribution, skipping extraction."); + return; + } + var processPath = Environment.ProcessPath; if (string.IsNullOrEmpty(processPath)) { diff --git a/src/Aspire.Cli/Bundles/IBundleService.cs b/src/Aspire.Cli/Bundles/IBundleService.cs index a486ef788a1..06427b1804e 100644 --- a/src/Aspire.Cli/Bundles/IBundleService.cs +++ b/src/Aspire.Cli/Bundles/IBundleService.cs @@ -15,6 +15,12 @@ internal interface IBundleService /// bool IsBundle { get; } + /// + /// Gets whether the CLI binary should perform self-extraction of the embedded bundle at runtime. + /// When , the layout is expected to already be on disk (e.g. archive/installer distributions). + /// + bool IsSelfExtracting { get; } + /// /// Ensures the bundle is extracted for the current CLI binary if it contains an embedded payload. /// No-ops if no payload is embedded, or if the layout is already extracted and up to date. diff --git a/tests/Aspire.Cli.Tests/BundleServiceTests.cs b/tests/Aspire.Cli.Tests/BundleServiceTests.cs index 9d8f5f015f5..83c0b354918 100644 --- a/tests/Aspire.Cli.Tests/BundleServiceTests.cs +++ b/tests/Aspire.Cli.Tests/BundleServiceTests.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Cli.Bundles; +using Aspire.Cli.Layout; +using Microsoft.Extensions.Logging.Abstractions; namespace Aspire.Cli.Tests; @@ -74,4 +76,14 @@ public void GetCurrentVersion_ReturnsNonNull() Assert.NotNull(version); Assert.NotEqual("unknown", version); } + + [Fact] + public void IsSelfExtracting_ReturnsFalse_WhenMetadataNotSetToTrue() + { + // Test assembly builds with default SelfExtractingBundle=false, + // so IsSelfExtracting should be false. + var layoutDiscovery = new LayoutDiscovery(NullLogger.Instance); + var service = new BundleService(layoutDiscovery, NullLogger.Instance); + Assert.False(service.IsSelfExtracting); + } } diff --git a/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs b/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs index 6d4d2f1ea84..c3d80740eb4 100644 --- a/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs +++ b/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs @@ -606,6 +606,8 @@ internal sealed class NullBundleService : IBundleService { public bool IsBundle => false; + public bool IsSelfExtracting => false; + public Task EnsureExtractedAsync(CancellationToken cancellationToken = default) => Task.CompletedTask; public Task ExtractAsync(string destinationPath, bool force = false, CancellationToken cancellationToken = default) @@ -618,10 +620,12 @@ public Task ExtractAsync(string destinationPath, bool force /// /// A configurable bundle service for testing bundle-dependent behavior. /// -internal sealed class TestBundleService(bool isBundle) : IBundleService +internal sealed class TestBundleService(bool isBundle, bool isSelfExtracting = false) : IBundleService { public bool IsBundle => isBundle; + public bool IsSelfExtracting => isSelfExtracting; + public Layout.LayoutConfiguration? Layout { get; set; } public Task EnsureExtractedAsync(CancellationToken cancellationToken = default) => Task.CompletedTask; From 13ff66da5a58232f714e6b3d686fbd91ebb7e920 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Tue, 7 Apr 2026 21:54:33 -0400 Subject: [PATCH 3/5] Fix: skip self-extraction only when layout already exists on disk When IsSelfExtracting=false, EnsureExtractedAsync was unconditionally short-circuiting, even if no layout existed on disk. This broke archive distributions where the binary still has an embedded bundle but the layout wasn't pre-extracted. Now it checks DiscoverLayout first and falls back to extraction when the layout is missing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Aspire.Cli/Bundles/BundleService.cs | 11 +++++++-- tests/Aspire.Cli.Tests/BundleServiceTests.cs | 25 ++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/Aspire.Cli/Bundles/BundleService.cs b/src/Aspire.Cli/Bundles/BundleService.cs index 7cb56d1bd78..01e71adaaf4 100644 --- a/src/Aspire.Cli/Bundles/BundleService.cs +++ b/src/Aspire.Cli/Bundles/BundleService.cs @@ -66,8 +66,15 @@ public async Task EnsureExtractedAsync(CancellationToken cancellationToken = def if (!IsSelfExtracting) { - logger.LogDebug("Self-extraction is disabled for this distribution, skipping extraction."); - return; + // Non-self-extracting distributions (archive/installer) expect the layout + // to already be on disk. Only skip extraction if the layout is actually present. + if (layoutDiscovery.DiscoverLayout() is not null) + { + logger.LogDebug("Self-extraction is disabled and layout is already on disk, skipping extraction."); + return; + } + + logger.LogDebug("Self-extraction is disabled but no layout found on disk, proceeding with extraction as fallback."); } var processPath = Environment.ProcessPath; diff --git a/tests/Aspire.Cli.Tests/BundleServiceTests.cs b/tests/Aspire.Cli.Tests/BundleServiceTests.cs index 83c0b354918..d01f9fbc304 100644 --- a/tests/Aspire.Cli.Tests/BundleServiceTests.cs +++ b/tests/Aspire.Cli.Tests/BundleServiceTests.cs @@ -86,4 +86,29 @@ public void IsSelfExtracting_ReturnsFalse_WhenMetadataNotSetToTrue() var service = new BundleService(layoutDiscovery, NullLogger.Instance); Assert.False(service.IsSelfExtracting); } + + [Fact] + public async Task EnsureExtractedAndGetLayoutAsync_ReturnsNull_WhenNoBundleAndNoLayout() + { + // Test assembly has no embedded bundle and IsSelfExtracting=false. + // EnsureExtractedAsync should no-op and DiscoverLayout should return null. + var layoutDiscovery = new LayoutDiscovery(NullLogger.Instance); + var service = new BundleService(layoutDiscovery, NullLogger.Instance); + + var layout = await service.EnsureExtractedAndGetLayoutAsync(); + Assert.Null(layout); + } + + [Fact] + public async Task EnsureExtractedAsync_DoesNotThrow_WhenNotSelfExtractingAndNoLayout() + { + // When IsSelfExtracting=false and no layout exists, EnsureExtractedAsync + // should not throw — it should gracefully no-op (since IsBundle is also false + // in the test assembly). This verifies the method doesn't unconditionally + // short-circuit in a way that prevents DiscoverLayout from being called. + var layoutDiscovery = new LayoutDiscovery(NullLogger.Instance); + var service = new BundleService(layoutDiscovery, NullLogger.Instance); + + await service.EnsureExtractedAsync(); + } } From 7f11e7ff7ee1dde5253d790230aca82fd21a957e Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Wed, 8 Apr 2026 03:13:05 -0400 Subject: [PATCH 4/5] Retrigger CI to verify E2E failures are transient The previous CI run had E2E test failures with 'Arithmetic overflow while reading bundle' in aspire-managed, which is unrelated to our code changes. The aspire-managed binary is built independently and our extraction code is unchanged from main. Other PRs pass E2E tests, suggesting this was a transient artifact corruption. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> From 366dfcfed765a464af3b248b3f78e84dd28b8ddb Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Wed, 8 Apr 2026 13:31:33 -0400 Subject: [PATCH 5/5] Use DefineConstants instead of AssemblyMetadataAttribute for SelfExtractingBundle Replace the AssemblyMetadataAttribute + reflection approach with a compile-time DefineConstants approach. The AssemblyMetadataAttribute was causing 'Arithmetic overflow while reading bundle' errors in native AOT builds, likely due to how ILC handles assembly-level attributes alongside large embedded resources. The new approach defines SELF_EXTRACTING_BUNDLE as a compile-time constant when SelfExtractingBundle=true, avoiding any reflection or assembly metadata in the native binary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Aspire.Cli/Aspire.Cli.csproj | 12 +++++------- src/Aspire.Cli/Bundles/BundleService.cs | 14 ++++++-------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/Aspire.Cli/Aspire.Cli.csproj b/src/Aspire.Cli/Aspire.Cli.csproj index 4ebd7520cd5..9d5174e3ba7 100644 --- a/src/Aspire.Cli/Aspire.Cli.csproj +++ b/src/Aspire.Cli/Aspire.Cli.csproj @@ -50,13 +50,11 @@ - - - - <_Parameter1>SelfExtractingBundle - <_Parameter2>$(SelfExtractingBundle) - - + + + $(DefineConstants);SELF_EXTRACTING_BUNDLE + diff --git a/src/Aspire.Cli/Bundles/BundleService.cs b/src/Aspire.Cli/Bundles/BundleService.cs index 01e71adaaf4..71332632684 100644 --- a/src/Aspire.Cli/Bundles/BundleService.cs +++ b/src/Aspire.Cli/Bundles/BundleService.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using System.Formats.Tar; using System.IO.Compression; -using System.Reflection; using Aspire.Cli.Layout; using Aspire.Cli.Utils; using Aspire.Shared; @@ -27,17 +26,16 @@ internal sealed class BundleService(ILayoutDiscovery layoutDiscovery, ILogger() - .FirstOrDefault(m => string.Equals(m.Key, "SelfExtractingBundle", StringComparison.OrdinalIgnoreCase))?.Value, - "true", - StringComparison.OrdinalIgnoreCase); - /// public bool IsBundle => s_isBundle; /// - public bool IsSelfExtracting => s_isSelfExtracting; + public bool IsSelfExtracting => +#if SELF_EXTRACTING_BUNDLE + true; +#else + false; +#endif /// /// Opens a read-only stream over the embedded bundle payload.