Skip to content
Draft
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
4 changes: 3 additions & 1 deletion eng/Bundle.proj
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,10 @@
<_CliBinlog Condition="'$(ContinuousIntegrationBuild)' == 'true'">-bl:$(ArtifactsLogDir)PublishCli.binlog</_CliBinlog>
<!-- The tar.gz archive produced by CreateLayout is embedded as a resource in the CLI binary -->
<_BundleArchivePath>$(ArtifactsDir)bundle\aspire-$(BundleVersion)-$(TargetRid).tar.gz</_BundleArchivePath>
<_CliChannelArg Condition="'$(CliChannel)' != ''">/p:CliChannel=$(CliChannel)</_CliChannelArg>
</PropertyGroup>
<Message Importance="high" Text="Publishing native AOT CLI with embedded bundle: $(_BundleArchivePath)" />
<Exec Command="dotnet publish &quot;$(CliProjectPath)&quot; -c $(Configuration) -r $(TargetRid) /p:BundlePayloadPath=&quot;$(_BundleArchivePath)&quot; $(_VersionSuffixArg) $(_CliBinlog)" />
<Exec Command="dotnet publish &quot;$(CliProjectPath)&quot; -c $(Configuration) -r $(TargetRid) /p:BundlePayloadPath=&quot;$(_BundleArchivePath)&quot; $(_VersionSuffixArg) $(_CliChannelArg) $(_CliBinlog)" />
</Target>

<Target Name="_PublishManagedProjects" Condition="'$(SkipManagedBuild)' != 'true'">
Expand All @@ -118,6 +119,7 @@
<_ArtifactsDirArg>$(ArtifactsDir.TrimEnd('\').TrimEnd('/'))</_ArtifactsDirArg>

<_CreateLayoutArgs>--output "$(_BundleOutputDirArg)" --artifacts "$(_ArtifactsDirArg)" --rid $(TargetRid) --bundle-version $(BundleVersion) --verbose --archive</_CreateLayoutArgs>
<_CreateLayoutArgs Condition="'$(UpdateInstructions)' != ''">$(_CreateLayoutArgs) --update-instructions "$(UpdateInstructions)"</_CreateLayoutArgs>
</PropertyGroup>

<Message Importance="high" Text="Creating bundle layout..." />
Expand Down
29 changes: 29 additions & 0 deletions eng/clipack/Common.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@
<ArchiveFormat Condition="$(CliRuntime.StartsWith('win-'))">zip</ArchiveFormat>
<ArchiveFormat Condition="!$(CliRuntime.StartsWith('win-'))">tar.gz</ArchiveFormat>

<!-- Update instructions for package-manager installations.
Windows archives get WinGet instructions, macOS archives get Homebrew instructions.
Linux archives don't include the file since there's no standard package manager. -->
<CliUpdateInstructions Condition="$(CliRuntime.StartsWith('win-'))">winget upgrade Microsoft.Aspire</CliUpdateInstructions>
<CliUpdateInstructions Condition="$(CliRuntime.StartsWith('osx-'))">brew upgrade --cask aspire</CliUpdateInstructions>

<!-- Distribution channel embedded in the CLI binary.
Defaults to 'stable' for archive builds; overridden to 'daily' or 'pr' by CI pipelines. -->
<CliChannel Condition="'$(CliChannel)' == ''">stable</CliChannel>

<!-- PublishNativeAot is explicitly set to false in the projects for cases where we don't want to AOT at all.
For the rest, publish native AOT if running on the target platfrom -->
<PublishNativeAot Condition="'$(PublishNativeAot)' == '' and $([System.OperatingSystem]::IsWindows()) and $(CliRuntime.StartsWith('win-'))">true</PublishNativeAot>
Expand Down Expand Up @@ -37,6 +47,23 @@
<RemoveDir Directories="$(OutputPath)\%(DirectoriesToRemove.Identity)" />
</Target>

<!--
Write .aspire-update.json next to the CLI binary when CliUpdateInstructions is set.
This disables self-update for package-manager installations (WinGet, Homebrew) while
leaving install-script users unaffected (install scripts delete this file after extraction).
-->
<Target Name="_WriteAspireUpdateJson"
AfterTargets="PrepareOutputPathForPublishToDisk"
Condition="'$(CliUpdateInstructions)' != ''">
<PropertyGroup>
<_AspireUpdateJsonContent>{"selfUpdateDisabled":true,"updateInstructions":"$(CliUpdateInstructions)"}</_AspireUpdateJsonContent>
</PropertyGroup>
<WriteLinesToFile File="$(OutputPath)\.aspire-update.json"
Lines="$(_AspireUpdateJsonContent)"
Overwrite="true" />
<Message Importance="high" Text="Wrote .aspire-update.json with instructions: $(CliUpdateInstructions)" />
</Target>

<Target Name="_PublishProject">
<!--
Bundle payload flow:
Expand All @@ -54,6 +81,8 @@
<AdditionalProperties Include="PublishDir=$(OutputPath)" />
<AdditionalProperties Include="SelfContained=true" />
<AdditionalProperties Condition="'$(BundlePayloadPath)' != ''" Include="BundlePayloadPath=$(BundlePayloadPath)" />
<!-- Forward the distribution channel to embed in the CLI binary via AssemblyMetadata -->
<AdditionalProperties Condition="'$(CliChannel)' != ''" Include="CliChannel=$(CliChannel)" />
<!-- Pass through version properties so the CLI gets the correct version -->
<AdditionalProperties Condition="'$(VersionSuffix)' != ''" Include="VersionSuffix=$(VersionSuffix)" />
<AdditionalProperties Condition="'$(OfficialBuildId)' != ''" Include="OfficialBuildId=$(OfficialBuildId)" />
Expand Down
7 changes: 7 additions & 0 deletions eng/homebrew/aspire.rb.template
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,12 @@ cask "aspire" do

binary "aspire"

postflight do
# Write .aspire-update.json next to the binary to disable self-update.
# Users should update via 'brew upgrade --cask aspire' instead.
config_file = staged_path.join(".aspire-update.json")
config_file.write('{"selfUpdateDisabled":true,"updateInstructions":"brew upgrade --cask aspire"}')
end

zap trash: "~/.aspire"
end
9 changes: 9 additions & 0 deletions eng/scripts/get-aspire-cli.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,15 @@ function Expand-AspireCliArchive {
Remove-OldCliBackupFiles -TargetExePath $targetExePath
}

# Remove .aspire-update.json if present. This file disables self-update for
# package-manager installations (WinGet, Homebrew) but install-script users
# should retain self-update capability.
$aspireUpdateJson = Join-Path $DestinationPath ".aspire-update.json"
if (Test-Path $aspireUpdateJson) {
Remove-Item -Path $aspireUpdateJson -Force -ErrorAction SilentlyContinue
Write-Message "Removed .aspire-update.json (self-update remains enabled for script installs)" -Level Verbose
}

Write-Message "Successfully unpacked archive" -Level Verbose
}
catch {
Expand Down
5 changes: 5 additions & 0 deletions eng/scripts/get-aspire-cli.sh
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,11 @@ install_archive() {
fi
fi

# Remove .aspire-update.json if present. This file disables self-update for
# package-manager installations (WinGet, Homebrew) but install-script users
# should retain self-update capability.
rm -f "${destination_path}/.aspire-update.json"

say_verbose "Successfully installed archive"
}

Expand Down
7 changes: 2 additions & 5 deletions localhive.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ if (-not $SkipBundle) {
$skipNativeArg = if ($NativeAot) { '' } else { '/p:SkipNativeBuild=true' }

Write-Log "Building bundle (aspire-managed + DCP$(if ($NativeAot) { ' + native AOT CLI' }))..."
$buildArgs = @($bundleProjPath, '-c', $effectiveConfig, "/p:VersionSuffix=$VersionSuffix")
$buildArgs = @($bundleProjPath, '-c', $effectiveConfig, "/p:VersionSuffix=$VersionSuffix", "/p:CliChannel=pr")
if (-not $NativeAot) {
$buildArgs += '/p:SkipNativeBuild=true'
}
Expand Down Expand Up @@ -376,10 +376,7 @@ if (-not $SkipCli) {

$installedCliPath = Join-Path $cliBinDir $cliExeName
Write-Log "Aspire CLI installed to: $installedCliPath"

# Set the channel to the local hive so templates and packages resolve from it
& $installedCliPath config set channel $Name -g 2>$null
Write-Log "Set global channel to '$Name'"
Write-Log "CLI has embedded channel 'pr' - hive packages at $aspireRoot\packages\"

# Check if the bin directory is in PATH
$pathSeparator = [System.IO.Path]::PathSeparator
Expand Down
11 changes: 3 additions & 8 deletions localhive.sh
Original file line number Diff line number Diff line change
Expand Up @@ -246,10 +246,10 @@ if [[ $SKIP_BUNDLE -eq 0 ]]; then

if [[ $NATIVE_AOT -eq 1 ]]; then
log "Building bundle (aspire-managed + DCP + native AOT CLI)..."
dotnet build "$BUNDLE_PROJ" -c "$EFFECTIVE_CONFIG" "/p:VersionSuffix=$VERSION_SUFFIX"
dotnet build "$BUNDLE_PROJ" -c "$EFFECTIVE_CONFIG" "/p:VersionSuffix=$VERSION_SUFFIX" "/p:CliChannel=pr"
else
log "Building bundle (aspire-managed + DCP)..."
dotnet build "$BUNDLE_PROJ" -c "$EFFECTIVE_CONFIG" /p:SkipNativeBuild=true "/p:VersionSuffix=$VERSION_SUFFIX"
dotnet build "$BUNDLE_PROJ" -c "$EFFECTIVE_CONFIG" /p:SkipNativeBuild=true "/p:VersionSuffix=$VERSION_SUFFIX" "/p:CliChannel=pr"
fi
if [[ $? -ne 0 ]]; then
error "Bundle build failed."
Expand Down Expand Up @@ -318,12 +318,7 @@ if [[ $SKIP_CLI -eq 0 ]]; then
chmod +x "$CLI_BIN_DIR/aspire"

log "Aspire CLI installed to: $CLI_BIN_DIR/aspire"

if "$CLI_BIN_DIR/aspire" config set channel "$HIVE_NAME" -g >/dev/null 2>&1; then
log "Set global channel to '$HIVE_NAME'"
else
warn "Failed to set global channel to '$HIVE_NAME'. Run: aspire config set channel '$HIVE_NAME' -g"
fi
log "CLI has embedded channel 'pr' — hive packages at $ASPIRE_ROOT/packages/"

# Check if the bin directory is in PATH
if [[ ":$PATH:" != *":$CLI_BIN_DIR:"* ]]; then
Expand Down
12 changes: 12 additions & 0 deletions src/Aspire.Cli/Aspire.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,20 @@
<DefineConstants>$(DefineConstants);CLI</DefineConstants>
<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
<CopyOutputSymbolsToPublishDirectory>false</CopyOutputSymbolsToPublishDirectory>

<!-- Distribution channel baked into the binary via AssemblyMetadata.
Set by CI/packaging: "stable", "daily", or "pr".
Empty/unset means dev/inner-loop build (uses implicit channel). -->
<CliChannel Condition="'$(CliChannel)' == ''"></CliChannel>
</PropertyGroup>

<ItemGroup>
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>CliChannel</_Parameter1>
<_Parameter2>$(CliChannel)</_Parameter2>
</AssemblyAttribute>
</ItemGroup>

<!-- Set the nuget properties when building as a global tool. -->
<PropertyGroup Condition="'$(IsCliToolProject)' == 'true'">
<IsPackable>true</IsPackable>
Expand Down
16 changes: 8 additions & 8 deletions src/Aspire.Cli/Commands/AddCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ internal sealed class AddCommand : BaseCommand
private readonly IDotNetSdkInstaller _sdkInstaller;
private readonly ICliHostEnvironment _hostEnvironment;
private readonly IAppHostProjectFactory _projectFactory;
private readonly IConfigurationService _configurationService;

private static readonly Argument<string> s_integrationArgument = new("integration")
{
Expand All @@ -44,7 +45,7 @@ internal sealed class AddCommand : BaseCommand
Description = AddCommandStrings.SourceArgumentDescription
};

public AddCommand(IPackagingService packagingService, IInteractionService interactionService, IProjectLocator projectLocator, IAddCommandPrompter prompter, AspireCliTelemetry telemetry, IDotNetSdkInstaller sdkInstaller, IFeatures features, ICliUpdateNotifier updateNotifier, CliExecutionContext executionContext, ICliHostEnvironment hostEnvironment, IAppHostProjectFactory projectFactory)
public AddCommand(IPackagingService packagingService, IInteractionService interactionService, IProjectLocator projectLocator, IAddCommandPrompter prompter, AspireCliTelemetry telemetry, IDotNetSdkInstaller sdkInstaller, IFeatures features, ICliUpdateNotifier updateNotifier, CliExecutionContext executionContext, ICliHostEnvironment hostEnvironment, IAppHostProjectFactory projectFactory, IConfigurationService configurationService)
: base("add", AddCommandStrings.Description, features, updateNotifier, executionContext, interactionService, telemetry)
{
_packagingService = packagingService;
Expand All @@ -53,6 +54,7 @@ public AddCommand(IPackagingService packagingService, IInteractionService intera
_sdkInstaller = sdkInstaller;
_hostEnvironment = hostEnvironment;
_projectFactory = projectFactory;
_configurationService = configurationService;

Arguments.Add(s_integrationArgument);
Options.Add(s_appHostOption);
Expand Down Expand Up @@ -93,22 +95,20 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell

var source = parseResult.GetValue(s_sourceOption);

// For non-.NET projects, read the channel from the local Aspire configuration if available.
// Unlike .NET projects which have a nuget.config, polyglot apphosts persist the channel
// in aspire.config.json (or the legacy settings.json during migration).
// For non-.NET projects, read the channel from the configuration service,
// which supports both the global config (aspire config set) and legacy settings.
// Falls back to the embedded channel baked into the binary.
string? configuredChannel = null;
if (project.LanguageId != KnownLanguageId.CSharp)
{
var appHostDirectory = effectiveAppHostProjectFile.Directory!.FullName;
var isProjectReferenceMode = AspireRepositoryDetector.DetectRepositoryRoot(appHostDirectory) is not null;
if (!isProjectReferenceMode)
{
// TODO: Remove legacy AspireJsonConfiguration fallback once confident most users
// have migrated. Tracked by https://github.com/microsoft/aspire/issues/15239
try
{
configuredChannel = AspireConfigFile.Load(appHostDirectory)?.Channel
?? AspireJsonConfiguration.Load(appHostDirectory)?.Channel;
configuredChannel = await _configurationService.GetConfigurationAsync("channel", cancellationToken)
?? PackagingService.GetEmbeddedChannel();
}
catch (JsonException ex)
{
Expand Down
5 changes: 3 additions & 2 deletions src/Aspire.Cli/Commands/InitCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -736,10 +736,11 @@ private static bool IsSupportedTfm(string tfm)
// Check if --channel option was provided (highest priority)
var channelName = parseResult.GetValue(_channelOption);

// If no --channel option, check for global channel setting
// If no --channel option, check for global channel setting, then embedded channel
if (string.IsNullOrEmpty(channelName))
{
channelName = await _configurationService.GetConfigurationAsync("channel", cancellationToken);
channelName = await _configurationService.GetConfigurationAsync("channel", cancellationToken)
?? PackagingService.GetEmbeddedChannel();
}

IEnumerable<PackageChannel> channels;
Expand Down
3 changes: 2 additions & 1 deletion src/Aspire.Cli/Commands/NewCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,8 @@ private async Task<ResolveTemplateVersionResult> ResolveCliTemplateVersionAsync(
var configuredChannelName = parseResult.GetValue(_channelOption);
if (string.IsNullOrWhiteSpace(configuredChannelName))
{
configuredChannelName = await _configurationService.GetConfigurationAsync("channel", cancellationToken);
configuredChannelName = await _configurationService.GetConfigurationAsync("channel", cancellationToken)
?? PackagingService.GetEmbeddedChannel();
}

var selectedChannel = string.IsNullOrWhiteSpace(configuredChannelName)
Expand Down
Loading
Loading