Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/dogfooding-pull-requests.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,9 +329,9 @@ The file is scoped to the solution directory and only affects projects under it.

The Homebrew cask (`eng/homebrew/aspire.rb.template`) installs Aspire entirely
inside the Caskroom version directory — `brew uninstall aspire` removes
the binary and the route sidecar end-to-end. The cask intentionally carries
no `zap` stanza, because `~/.aspire/` is a shared prefix with the script-route
and PR-route installers and a brew-driven recursive delete would clobber state
the binary and the source sidecar end-to-end. The cask intentionally carries
no `zap` stanza, because `~/.aspire/` is a shared prefix with the script-source
and PR-source installers and a brew-driven recursive delete would clobber state
those installers still own.

If you installed via the Homebrew cask before this change, you may have a
Expand All @@ -343,7 +343,7 @@ Clean it up manually once after upgrading the cask:
rm -rf ~/.aspire/installs/brew-stable
```

NuGet hives under `~/.aspire/hives/` and any script-route or PR-route
NuGet hives under `~/.aspire/hives/` and any script-source or PR-source
binaries under `~/.aspire/bin/` and `~/.aspire/dogfood/` are not touched by
the cask in either direction; manage those with the steps above.

Expand Down
45 changes: 45 additions & 0 deletions docs/specs/cli-output-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,51 @@ The JSON form includes secret values. Do not redirect it to logs or files unless

`status` is one of `pass`, `warning`, or `fail`. Individual checks can include `details`, `fix`, `link`, or command-specific `metadata`.

### `aspire installs list`

`aspire installs list --format json` emits the Aspire CLI installs and orphan package hives that the running CLI can discover:

```json
[
{
"id": "script",
"kind": "script",
"channel": "stable",
"path": "/home/user/.aspire/bin/aspire",
"hive": "/home/user/.aspire/hives/stable",
"status": "active"
},
{
"id": "pr-17400",
"kind": "orphan-hive",
"channel": "pr-17400",
"hive": "/home/user/.aspire/hives/pr-17400",
"status": "no install found",
"statusReason": "No discovered install reports this hive's channel."
},
{
"id": "stable",
"kind": "homebrew",
"channel": "stable",
"path": "/opt/homebrew/Caskroom/aspire/13.2.0/aspire",
"status": "active",
"managedBy": "homebrew"
},
{
"id": "pr-17500",
"kind": "pr",
"channel": "pr-17500",
"path": "/home/user/.aspire/dogfood/pr-17500/bin/aspire",
"status": "failed",
"statusReason": "Peer probe exited with code 137 (SIGKILL)."
}
]
```

`status` uses the install-discovery status (`active`, `shadowed`, `notOnPath`, `failed`, `notProbed`) or `no install found` for orphan hives. When the row has a human-readable reason (failed probes, orphan hives), it rides on the separate `statusReason` field — the `status` value itself stays enum-shaped so programmatic consumers can `switch` on it.

`aspire installs --self --format json` is a hidden command used by the install-discovery peer-probe path so a newer CLI can ask a peer CLI to describe itself. The shape is an internal cross-version contract between Aspire CLI builds — not a stable surface for tooling — and may change without notice.

### `aspire config info`

`aspire config info --json` is a hidden tooling command that emits configuration paths, feature metadata, settings schemas, and advertised CLI capabilities:
Expand Down
34 changes: 17 additions & 17 deletions docs/specs/install-routes.md → docs/specs/install-sources.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Aspire CLI install-route sidecar
# Aspire CLI install-source sidecar

> Pairs with `docs/specs/bundle.md` (bundle extraction layout) and `docs/ci/native-cli-packaging.md` (how archives are produced).

The CLI binary identifies its install route by reading a single
The CLI binary identifies its install source by reading a single
`.aspire-install.json` sidecar that lives next to the binary. The sidecar's
`source` field selects the extract-dir shape used by `BundleService` and, for
portable installs, the Aspire home used for hives and local state.
Expand All @@ -13,10 +13,10 @@ portable installs, the Aspire home used for hives and local state.
(`<binaryDir>/.aspire-install.json`) and contains exactly one field:

```json
{ "source": "<route>" }
{ "source": "<source>" }
```

| `source` value | Install route |
| `source` value | Install source |
|----------------|--------------------------------------------------------|
| `brew` | Homebrew cask |
| `winget` | WinGet portable manifest |
Expand All @@ -37,26 +37,26 @@ install. `script` and `localhive` use the parent of `bin`; `pr` uses the parent
of `dogfood/pr-<N>/bin`. Package-manager installs and sidecar-less binaries keep
the default user-profile Aspire home.

## Per-route authorship
## Per-source authorship

**The shared per-RID CLI archives (`aspire-cli-<rid>-*.zip` / `.tar.gz`) ship sidecar-free.** Those archives are reused across brew, winget, the release script, and the PR script — none of them owns the route label. Each route writes its own sidecar at install time.
**The shared per-RID CLI archives (`aspire-cli-<rid>-*.zip` / `.tar.gz`) ship sidecar-free.** Those archives are reused across Homebrew, WinGet, the release script, and the PR script — none of them owns the source label. Each source writes its own sidecar at install time.

| Route | Archive shape | Sidecar writer |
| Source | Archive shape | Sidecar writer |
|-------------|----------------------------------------|---------------------------------------------------------------------|
| brew | shared per-RID tarball | cask `postflight` block in `eng/homebrew/aspire.rb.template` |
| Homebrew | shared per-RID tarball | cask `postflight` block in `eng/homebrew/aspire.rb.template` |
| winget | shared per-RID zip | CLI first-run probe (`WingetFirstRunProbe`) — uses the WinGet portable ARP registry entry to confirm the running binary was placed by winget, then stamps the sidecar |
| script | shared per-RID archive | `eng/scripts/get-aspire-cli.{sh,ps1}` (post-extraction) |
| PR script | shared per-RID archive | `eng/scripts/get-aspire-cli-pr.{sh,ps1}` (post-extraction) |
| dotnet-tool | route-exclusive nupkg | payload-embedded (staged by `Aspire.Cli.csproj` `_PreparePreBuiltCliBinaryForPackTool`) |
| localhive | local-only (no shared archive) | `localhive.{sh,ps1}` writes the sidecar after copying the CLI binary into `<prefix>/bin/`. When `--output PATH` is used, the sidecar is written inside the output dir, which is appropriate because localhive archives are route-exclusive (only consumed as localhive installs). |
| dotnet-tool | source-exclusive nupkg | payload-embedded (staged by `Aspire.Cli.csproj` `_PreparePreBuiltCliBinaryForPackTool`) |
| localhive | local-only (no shared archive) | `localhive.{sh,ps1}` writes the sidecar after copying the CLI binary into `<prefix>/bin/`. When `--output PATH` is used, the sidecar is written inside the output dir, which is appropriate because localhive archives are source-exclusive (only consumed as localhive installs). |

The dotnet-tool nupkg is the one exception that payload-embeds the sidecar: the nupkg is route-exclusive (only `dotnet tool install` consumes it), so the embedded sidecar cannot leak into another route's prefix.
The dotnet-tool nupkg is the one exception that payload-embeds the sidecar: the nupkg is source-exclusive (only `dotnet tool install` consumes it), so the embedded sidecar cannot leak into another source's prefix.

## Why no payload-embed in shared archives

Until PR 16817 the per-RID archives baked `{"source":"brew"}` (osx-*) and `{"source":"winget"}` (win-*) into the archive root via an MSBuild target. Because the osx-* tarball is also consumed by `get-aspire-cli-pr.sh`, the smuggled `brew` sidecar landed in the script-route prefix at `<prefix>/dogfood/pr-<N>/bin/.aspire-install.json`, and `BundleService` then selected `binaryDir` (the `brew` flat-layout case) as the extract dir — producing `<prefix>/dogfood/pr-<N>/bin/versions/<v>/` instead of `<prefix>/dogfood/pr-<N>/versions/<v>/`.
Until PR 16817 the per-RID archives baked source sidecars (`osx-*` as Homebrew, `win-*` as WinGet) into the archive root via an MSBuild target. Because the `osx-*` tarball is also consumed by `get-aspire-cli-pr.sh`, the smuggled `brew` sidecar landed in the script-source prefix at `<prefix>/dogfood/pr-<N>/bin/.aspire-install.json`, and `BundleService` then selected `binaryDir` (the `brew` flat-layout case) as the extract dir — producing `<prefix>/dogfood/pr-<N>/bin/versions/<v>/` instead of `<prefix>/dogfood/pr-<N>/versions/<v>/`.

Removing the MSBuild target and moving each route to author its own sidecar at install time makes the per-RID archive route-agnostic and prevents the leak by construction.
Removing the MSBuild target and moving each source to author its own sidecar at install time makes the per-RID archive source-agnostic and prevents the leak by construction.

## Producer-side invariants (build / CI)

Expand All @@ -67,10 +67,10 @@ Two mechanical checks guard the contract:

## Reader-side invariants (runtime)

`BundleService.ComputeDefaultExtractDir` is the single point of truth for layout selection. It performs no path-shape detection: layout is a pure function of the sidecar `source` value (or the fallback when the sidecar is absent, unreadable, malformed, or has an unknown `source`). Unknown `source` values fall back to parent-of-binary for typed bundle layout handling. Coverage lives in `tests/Aspire.Cli.Tests/Bundles/BundleServiceCrossRouteExtractionTests.cs` as a theory over (source × prefix-shape) rows, including the cross-route case where a `brew` sidecar lands under a script-style prefix.
`BundleService.ComputeDefaultExtractDir` is the single point of truth for layout selection. It performs no path-shape detection: layout is a pure function of the sidecar `source` value (or the fallback when the sidecar is absent, unreadable, malformed, or has an unknown `source`). Unknown `source` values fall back to parent-of-binary for typed bundle layout handling. Coverage lives in `tests/Aspire.Cli.Tests/Bundles/BundleServiceCrossSourceExtractionTests.cs` as a theory over (source × prefix-shape) rows, including the cross-source case where a `brew` sidecar lands under a script-style prefix.

`CliPathHelper.GetAspireHomeDirectory` is the single point of truth for Aspire-home selection. It reads the same sidecar but only changes home for Aspire-owned portable routes (`script`, `pr`, and `localhive`); package-manager routes use the user-profile home because their install roots are package-manager-owned. Coverage lives in `tests/Aspire.Cli.Tests/Utils/CliPathHelperTests.cs`.
`CliPathHelper.GetAspireHomeDirectory` is the single point of truth for Aspire-home selection. It reads the same sidecar but only changes home for Aspire-owned portable sources (`script`, `pr`, and `localhive`); package-manager sources use the user-profile home because their install roots are package-manager-owned. Coverage lives in `tests/Aspire.Cli.Tests/Utils/CliPathHelperTests.cs`.

> **Discovery scope (dotnet-tool route).** Install discovery walks the default `dotnet tool install -g` location at `~/.dotnet/tools/.store/aspire.cli` only. Custom `--tool-path` installs are not discovered today: the dotnet CLI has no machine-wide registry of arbitrary `--tool-path` installs to enumerate, and walking the filesystem would balloon the cost of `aspire doctor`. Users with a custom-`--tool-path` install can confirm it directly with `<tool-path>/aspire doctor --self`.
> **Discovery scope (dotnet-tool source).** Install discovery walks the default `dotnet tool install -g` location at `~/.dotnet/tools/.store/aspire.cli` only. Custom `--tool-path` installs are not discovered today: the dotnet CLI has no machine-wide registry of arbitrary `--tool-path` installs to enumerate, and walking the filesystem would balloon the cost of `aspire installs list`. Users with a custom-`--tool-path` install can confirm it directly with `<tool-path>/aspire installs --self`.

For read-only install discovery (`aspire doctor --format json`), sidecar existence is the trust signal for peer probing. A candidate with any readable sidecar is probed even when `source` is not in the known route table; the raw `source` string is surfaced as the installation `route` so future package-manager routes can appear before this consumer updates. Sidecar-less, unreadable, or malformed candidates are listed without executing the binary.
For read-only install discovery (`aspire installs list --format json`), sidecar existence is the trust signal for peer probing. A candidate with any readable sidecar is probed even when `source` is not in the known source table; the raw `source` string is surfaced as the installation `source` so future package-manager sources can appear before this consumer updates. Sidecar-less, unreadable, or malformed candidates are listed without executing the binary.
10 changes: 5 additions & 5 deletions eng/clipack/Common.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@
</Target>

<!--
No install-route sidecar is staged into the per-RID CLI archive. The
shared archives are sidecar-free by contract; each route writes its own
No install-source sidecar is staged into the per-RID CLI archive. The
shared archives are sidecar-free by contract; each source writes its own
.aspire-install.json at install time. The dotnet-tool nupkg is the
exception (route-exclusive, payload-embedded by
exception (source-exclusive, payload-embedded by
src/Aspire.Cli/Aspire.Cli.csproj _PreparePreBuiltCliBinaryForPackTool).
See docs/specs/install-routes.md for the full authorship table.
See docs/specs/install-sources.md for the full authorship table.

_AssertNoSidecarInArchiveStaging below is the build-time guard that
fails the build if a regression starts staging the sidecar back in.
Expand All @@ -59,7 +59,7 @@
<_ArchiveSidecarPath>$([MSBuild]::NormalizePath($(OutputPath), '.aspire-install.json'))</_ArchiveSidecarPath>
</PropertyGroup>
<Error Condition="Exists('$(_ArchiveSidecarPath)')"
Text="Per-RID CLI archives must not contain '.aspire-install.json' — shared across install routes. See docs/specs/install-routes.md. Found smuggled sidecar at $(_ArchiveSidecarPath)." />
Text="Per-RID CLI archives must not contain '.aspire-install.json' — shared across install sources. See docs/specs/install-sources.md. Found smuggled sidecar at $(_ArchiveSidecarPath)." />
</Target>

<Target Name="_PublishProject">
Expand Down
22 changes: 11 additions & 11 deletions eng/scripts/get-aspire-cli-pr.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -1539,11 +1539,11 @@ function Test-InstallerModeEnvironment {
}
}

# Writes the PR-route install-source sidecar (.aspire-install.json) next to
# Writes the PR-source install-source sidecar (.aspire-install.json) next to
# the installed binary. Under -WhatIf, prints the target path and skips the
# write so a real user's sidecar is never overwritten by a describe pass.
# Authorship contract: docs/specs/install-routes.md.
function Write-PRRouteSidecar {
# Authorship contract: docs/specs/install-sources.md.
function Write-PRSourceSidecar {
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory = $true)]
Expand All @@ -1557,12 +1557,12 @@ function Write-PRRouteSidecar {
$sidecarPath = Join-Path $sidecarDir '.aspire-install.json'
$sidecarContent = "{""source"":""pr""}`n"

if ($PSCmdlet.ShouldProcess($sidecarPath, "Write route sidecar")) {
if ($PSCmdlet.ShouldProcess($sidecarPath, "Write source sidecar")) {
[System.IO.Directory]::CreateDirectory($sidecarDir) | Out-Null
[System.IO.File]::WriteAllText($sidecarPath, $sidecarContent)
}
else {
Write-Host "What if: Route sidecar would be written to: $sidecarPath"
Write-Host "What if: Source sidecar would be written to: $sidecarPath"
}
}

Expand Down Expand Up @@ -1701,8 +1701,8 @@ function Start-InstallFromLocalDir {
Write-Message "Installing from local directory: $LocalDirPath" -Level Info

# Set installation paths.
# PR-route installs are isolated under <prefix>/dogfood/pr-<N>/bin so they don't
# collide with the script-route prefix or with other PR installs. Hives remain shared
# PR-source installs are isolated under <prefix>/dogfood/pr-<N>/bin so they don't
# collide with the script-source prefix or with other PR installs. Hives remain shared
# under <prefix>/hives/<label>/packages.
$cliBinDir = if ($PRNumber -gt 0) {
Join-Path (Join-Path (Join-Path $resolvedInstallPrefix "dogfood") "pr-$PRNumber") "bin"
Expand Down Expand Up @@ -1791,7 +1791,7 @@ function Start-InstallFromLocalDir {
# PR installs from archives get a sidecar; --local-dir installs are unmanaged,
# and dotnet-tool packages embed their own source=dotnet-tool sidecar.
if (-not $HiveOnly -and $InstallMode -eq 'Archive' -and $PRNumber -gt 0) {
Write-PRRouteSidecar -InstallPrefix $resolvedInstallPrefix -PRNumber $PRNumber
Write-PRSourceSidecar -InstallPrefix $resolvedInstallPrefix -PRNumber $PRNumber
}

# Update PATH environment variables
Expand Down Expand Up @@ -1837,8 +1837,8 @@ function Start-DownloadAndInstall {
Write-Message "Using workflow run https://github.com/$Script:Repository/actions/runs/$runId" -Level Info

# Set installation paths.
# PR-route installs are isolated under <prefix>/dogfood/pr-<N>/bin so they don't
# collide with the script-route prefix or with other PR installs. Hives remain shared
# PR-source installs are isolated under <prefix>/dogfood/pr-<N>/bin so they don't
# collide with the script-source prefix or with other PR installs. Hives remain shared
# under <prefix>/hives/<label>/packages.
$cliBinDir = if ($PRNumber -gt 0) {
Join-Path (Join-Path (Join-Path $resolvedInstallPrefix "dogfood") "pr-$PRNumber") "bin"
Expand Down Expand Up @@ -1927,7 +1927,7 @@ function Start-DownloadAndInstall {
}

if (-not $HiveOnly -and $InstallMode -eq 'Archive' -and $PRNumber -gt 0) {
Write-PRRouteSidecar -InstallPrefix $resolvedInstallPrefix -PRNumber $PRNumber
Write-PRSourceSidecar -InstallPrefix $resolvedInstallPrefix -PRNumber $PRNumber
}

# Update PATH environment variables
Expand Down
Loading
Loading