Add symlink/junction support for bundle installs with versioned directories#16408
Add symlink/junction support for bundle installs with versioned directories#16408
Conversation
Extract the static OpenPayload() and s_isBundle from BundleService into an injectable IBundlePayloadProvider interface. This decouples payload access from the assembly resource, enabling integration tests to inject synthetic tar.gz payloads and exercise the full extract -> link -> upgrade -> cleanup pipeline in-process. Changes: - Add IBundlePayloadProvider interface (HasPayload, OpenPayload) - Add EmbeddedBundlePayloadProvider (preserves existing behavior) - Add ProcessPathOverride property to BundleService for test seam - Split ExtractPayloadAsync into instance + static(Stream) overloads - Register IBundlePayloadProvider in Program.cs and CliTestHelper DI - Add TestBundlePayloadProvider test helper (byte[] -> fresh MemoryStream) - Add 6 BundleServiceIntegrationTests covering: - Fresh extraction with synthetic payload - Already-up-to-date short-circuit - Version upgrade with link flipping and stale cleanup - Stale temp/bad directory cleanup - Static ExtractPayloadAsync with strip-components behavior - Reparse point replacement on upgrade Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 16408Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 16408" |
There was a problem hiding this comment.
Pull request overview
This PR reworks Aspire CLI bundle install/upgrade to use versioned bundle directories and reparse points (symlinks/junctions) so upgrades can switch the active bundle atomically instead of updating in-place. It also refactors bundle payload access behind an injectable provider and adds extensive tests around reparse points and the new extraction/upgrade pipeline.
Changes:
- Introduce cross-platform
ReparsePointutilities (Windows symlink-with-junction-fallback) and Win32 constants for junction P/Invoke. - Update
BundleServiceto extract intoversions/<version-id>/and flipmanaged/+dcp/via reparse points, with cleanup of stale versions. - Add
IBundlePayloadProvider(+ embedded/test/null implementations) and new unit/integration tests covering extraction, upgrades, and link behaviors.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Aspire.Cli.Tests/Utils/ReparsePointTests.cs | Adds unit tests covering creation/replacement/removal and Windows junction/symlink migration behaviors. |
| tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs | Extends test DI setup with a default no-op IBundlePayloadProvider factory. |
| tests/Aspire.Cli.Tests/TestServices/TestBundlePayloadProvider.cs | Adds a test payload provider that returns a fresh stream per call. |
| tests/Aspire.Cli.Tests/Layout/LayoutDiscoveryReparsePointTests.cs | Validates layout discovery works when managed/ and dcp/ are reparse points. |
| tests/Aspire.Cli.Tests/BundleServiceTests.cs | Updates unit tests for provider-based bundle detection and new versioned-layout helpers. |
| tests/Aspire.Cli.Tests/BundleServiceIntegrationTests.cs | Adds end-to-end tests for extract → link flip → upgrade → cleanup using synthetic tar.gz payloads. |
| src/Aspire.Cli/Utils/Win32Constants.cs | Introduces shared Win32 constants for reparse-point P/Invoke. |
| src/Aspire.Cli/Utils/ReparsePoint.cs | Implements reparse-point creation/replacement/removal and Windows junction P/Invoke + tag detection. |
| src/Aspire.Cli/Program.cs | Registers IBundlePayloadProvider (embedded provider) in DI. |
| src/Aspire.Cli/Bundles/IBundlePayloadProvider.cs | Defines injectable bundle payload provider interface. |
| src/Aspire.Cli/Bundles/EmbeddedBundlePayloadProvider.cs | Implements payload provider backed by an embedded resource. |
| src/Aspire.Cli/Bundles/BundleService.cs | Implements versioned extraction layout, link flipping/rollback, stale version cleanup, and provider-based payload extraction. |
… migration - CreateOrReplace now throws InvalidOperationException when existing path is a real directory (not a reparse point), preventing silent recursive deletion of directory contents - WriteMountPointReparseData validates reparseDataLength fits in ushort and total buffer size stays within 16KB maximum, throwing PathTooLongException for overly long junction targets - TryFlipLinks always renames legacy directories to .old siblings instead of deleting them, deferring cleanup until after post-flip validation succeeds to prevent data loss if link creation fails Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The InvalidOperationException guard for existing real directories was inside the Windows-only branch. On macOS/Linux, CreateOrReplace would fall through to rename(2) which fails with EISDIR instead. Move the guard before the platform-specific branches so it applies consistently. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
🧪 I tested the dogfood CLI for this PR on Windows. ✅ What worked
❌ Issues found
📌 SummaryThere look to be two issues here:
|
I was thinking about the initial upgrade (as well as the potential for needing to downgrade the bundle from the new layout back to the old layout for a time during development to test against stable releases or other PRs). I'm thinking to introduce a |
Replace the two top-level reparse points (managed/ and dcp/) with a single 'bundle/' reparse point that links directly to the active versioned directory. Components (managed/, dcp/) are resolved as subdirectories of the bundle link target. Layout discovery checks new bundle/ layout first, then falls back to the legacy managed/+dcp/ layout for backward compatibility. Legacy top-level managed/ and dcp/ directories are cleaned up best-effort after successful extraction. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CreateLayout.FindDcpPath() was hardcoded to look for NuGet packages in ~/.nuget/packages. On machines where the global packages folder is configured elsewhere (e.g. via NUGET_PACKAGES env var or nuget.config), DCP discovery would fail with 'DCP package not found'. Now checks the NUGET_PACKAGES environment variable first, falling back to the default ~/.nuget/packages location. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Instead of manually creating the versioned directory layout and symlinks/junctions, the localhive scripts now embed the bundle payload (tar.gz) in the CLI binary via BundlePayloadPath. The CLI's built-in EnsureExtractedAsync handles versioned layout creation, symlink/junction management, and cleanup on first run. All three build modes (native AOT, cross-RID, default dotnet tool) now produce a CLI binary with an embedded bundle when the bundle build step is not skipped. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
🎬 CLI E2E Test Recordings — 75 recordings uploaded (commit View all recordings
📹 Recordings uploaded automatically from CI run #24910736356 |
|
@davidfowl okay, updated to use the new folder layout. The first issue identified in the smoke test ( For the second issue I've run multiple smoke tests and been able to successfully update from old to new layout. Main thing is that the bundle doesn't unpack until a command that actually validates the bundle executes the first time so after running the PR install script the bundle won't unpack until a command like |
Description
This PR adds support for using symlinks and junctions (reparse points) for managing bundle asset directories (
managed/,dcp/) during CLI install and upgrade, replacing the previous in-place update approach with versioned directories and atomic link flipping.Key changes
Bundle install/upgrade architecture:
versions/<version-id>/managed/,dcp/) become reparse points (symlinks preferred, junctions as fallback on Windows without Developer Mode) pointing at the active versioned directoryReparse point infrastructure (
ReparsePoint.cs,Win32Constants.cs):ReparsePointutility class for creating, replacing, and removing directory symlinks/junctions cross-platformSeCreateSymbolicLinkPrivilegeis not availableGetReparseTag()method for distinguishing symlinks from junctions viaFSCTL_GET_REPARSE_POINTDeviceIoControlTestability refactor (
IBundlePayloadProvider):OpenPayload()ands_isBundlefromBundleServiceinto an injectableIBundlePayloadProviderinterfaceEmbeddedBundlePayloadProviderpreserves existing behavior (reads from assembly manifest resource)ProcessPathOverrideproperty enables tests to simulate different CLI binary versionsExtractPayloadAsyncsplit into instance method (uses provider) and staticStreamoverload (pure extraction logic)Comprehensive test coverage:
ReparsePointTests: 14 tests covering symlink/junction creation, replacement, removal, migration from junction→symlink, and capability-gated scenariosLayoutDiscoveryReparsePointTests: Validates layout discovery resolves through reparse pointsBundleServiceIntegrationTests: 6 in-process integration tests exercising the full extract→link→upgrade→cleanup pipeline with synthetic tar.gz payloadsBundleServiceTests: Updated existing tests for the new provider-based architecturetry/finallycleanup to preventDirectory.Delete(recursive: true)from following junctions and deleting target contentsValidation
Checklist