diff --git a/src/Aspire.Cli/Aspire.Cli.csproj b/src/Aspire.Cli/Aspire.Cli.csproj index a3633f025f6..0140c148a9c 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,6 +42,8 @@ To do this we make sure it doesn't PublishAOT, and ensure that it doesn't publish for any RIDs at all. --> false + + true diff --git a/src/Aspire.Cli/Bundles/BundleService.cs b/src/Aspire.Cli/Bundles/BundleService.cs index c06ff55b5b1..fee20a63601 100644 --- a/src/Aspire.Cli/Bundles/BundleService.cs +++ b/src/Aspire.Cli/Bundles/BundleService.cs @@ -29,6 +29,9 @@ internal sealed class BundleService(ILayoutDiscovery layoutDiscovery, ILogger public bool IsBundle => s_isBundle; + /// + public bool IsSelfExtracting => false; + /// /// Opens a read-only stream over the embedded bundle payload. /// Returns if no payload is embedded. @@ -54,6 +57,9 @@ public async Task EnsureExtractedAsync(CancellationToken cancellationToken = def return; } + // TODO: When IsSelfExtracting is wired to the MSBuild property (future PR), + // non-self-extracting distributions can skip extraction when the layout is on disk. + 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..d01f9fbc304 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,39 @@ 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); + } + + [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(); + } } 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;