Skip to content
Merged
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
177 changes: 167 additions & 10 deletions cli/azd/pkg/update/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ const (
stableVersionURL = "https://aka.ms/azure-dev/versions/cli/latest"
// blobBaseURL is the base URL for Azure Blob Storage where azd binaries are hosted.
blobBaseURL = "https://azuresdkartifacts.z5.web.core.windows.net/azd/standalone/release"
// installShScriptURL is the shell install script for azd on Linux/macOS.
installShScriptURL = "https://aka.ms/install-azd.sh"
)

// VersionInfo holds the result of a version check.
Expand Down Expand Up @@ -271,12 +273,17 @@ func (m *Manager) Update(ctx context.Context, cfg *UpdateConfig, writer io.Write

switch installedBy {
case installer.InstallTypeBrew:
return m.updateViaPackageManager(ctx, "brew", []string{"upgrade", "azure/azd/azd"}, writer)
return m.updateViaBrew(ctx, cfg, writer)
case installer.InstallTypeWinget:
return m.updateViaPackageManager(ctx, "winget", []string{"upgrade", "Microsoft.Azd"}, writer)
case installer.InstallTypeChoco:
return m.updateViaPackageManager(ctx, "choco", []string{"upgrade", "azd"}, writer)
case installer.InstallTypePs, installer.InstallTypeSh, installer.InstallTypeDeb,
case installer.InstallTypeSh:
if runtime.GOOS == "windows" {
return m.updateViaMSI(ctx, cfg, writer)
}
return m.updateViaInstallScript(ctx, cfg, writer)
case installer.InstallTypePs, installer.InstallTypeDeb,
installer.InstallTypeRpm, installer.InstallTypeUnknown:
if runtime.GOOS == "windows" {
return m.updateViaMSI(ctx, cfg, writer)
Expand All @@ -290,6 +297,144 @@ func (m *Manager) Update(ctx context.Context, cfg *UpdateConfig, writer io.Write
}
}

func (m *Manager) updateViaBrew(ctx context.Context, cfg *UpdateConfig, writer io.Writer) error {
fmt.Fprintf(writer, "Checking Homebrew cask installation...\n")

// Determine which cask is currently installed by checking `brew list --cask`.
listArgs := exec.NewRunArgs("brew", "list", "--cask")
listResult, err := m.commandRunner.Run(ctx, listArgs)
if err != nil {
log.Printf("brew list --cask failed: %v", err)
}

caskOutput := ""
if err == nil {
caskOutput = listResult.Stdout
}

hasAzd := false
hasAzdDaily := false
for line := range strings.SplitSeq(caskOutput, "\n") {
name := strings.TrimSpace(line)
if name == "azd@daily" {
hasAzdDaily = true
} else if name == "azd" {
hasAzd = true
}
}

targetChannel := cfg.Channel

if !hasAzd && !hasAzdDaily {
// azd is not installed as a cask (formula install or other).
// Uninstall the non-cask version and install the correct cask.
fmt.Fprintf(writer, "azd is not installed as a Homebrew cask. Reinstalling as cask...\n")
if err := m.updateViaPackageManager(ctx, "brew", []string{"uninstall", "azd"}, writer); err != nil {
log.Printf("brew uninstall azd failed: %v", err)
}
switch targetChannel {
case ChannelStable:
return m.updateViaPackageManager(ctx, "brew", []string{"install", "--cask", "azure/azd/azd"}, writer)
case ChannelDaily:
return m.updateViaPackageManager(ctx, "brew", []string{"install", "--cask", "azure/azd/azd@daily"}, writer)
default:
return fmt.Errorf("unsupported channel: %s", targetChannel)
}
}

// Determine if the user is switching channels.
currentlyDaily := hasAzdDaily
currentlyStable := hasAzd
Comment on lines +346 to +347

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are redundant. Why not use hasAzd or hasAzdDaily directly?


if currentlyDaily && targetChannel == ChannelStable {
// Switching from daily to stable
fmt.Fprintf(writer, "Switching from daily to stable channel...\n")
if err := m.updateViaPackageManager(ctx, "brew", []string{"uninstall", "--cask", "azd@daily"}, writer); err != nil {
return err
}
return m.updateViaPackageManager(ctx, "brew", []string{"install", "--cask", "azure/azd/azd"}, writer)
}

if currentlyStable && targetChannel == ChannelDaily {
// Switching from stable to daily
fmt.Fprintf(writer, "Switching from stable to daily channel...\n")
if err := m.updateViaPackageManager(ctx, "brew", []string{"uninstall", "--cask", "azd"}, writer); err != nil {
return err
}
return m.updateViaPackageManager(ctx, "brew", []string{"install", "--cask", "azure/azd/azd@daily"}, writer)
}

// Same channel — update in place.
switch targetChannel {
case ChannelStable:
fmt.Fprintf(writer, "Updating azd (stable channel)...\n")
return m.updateViaPackageManager(ctx, "brew", []string{"upgrade", "--cask", "azure/azd/azd"}, writer)
case ChannelDaily:
fmt.Fprintf(writer, "Updating azd (daily channel)...\n")
return m.updateViaPackageManager(ctx, "brew", []string{"upgrade", "--cask", "azure/azd/azd@daily"}, writer)
default:
return fmt.Errorf("unsupported channel: %s", targetChannel)
}
}

func (m *Manager) updateViaInstallScript(ctx context.Context, cfg *UpdateConfig, writer io.Writer) error {
fmt.Fprintf(writer, "Updating azd via install script...\n")

currentPath, err := currentExePath()
if err != nil {
return newUpdateError(CodeReplaceFailed, fmt.Errorf("failed to determine current path: %w", err))
}
installFolder := filepath.Dir(currentPath)

// Download install-azd.sh into a uniquely-created temp directory with restrictive permissions
scriptDir, err := os.MkdirTemp("", "azd-update-*")
if err != nil {
return newUpdateError(CodeDownloadFailed, fmt.Errorf("failed to create temp directory: %w", err))
}
defer os.RemoveAll(scriptDir)

// Restrict the temp directory so only the current user can read/write/traverse it.
if err := os.Chmod(scriptDir, 0o700); err != nil {
return newUpdateError(CodeDownloadFailed, fmt.Errorf("failed to set temp directory permissions: %w", err))
}

scriptPath := filepath.Join(scriptDir, "install-azd.sh")

if err := m.downloadFile(ctx, installShScriptURL, scriptPath, writer); err != nil {
return newUpdateError(CodeDownloadFailed, fmt.Errorf("failed to download install script: %w", err))
}

// Make the script executable
if err := os.Chmod(scriptPath, 0o500); err != nil {
return newUpdateError(CodeReplaceFailed, fmt.Errorf("failed to set script permissions: %w", err))
}

versionArg := string(cfg.Channel)
runArgs := exec.NewRunArgs("bash", scriptPath,
"--version", versionArg,
"--install-folder", installFolder,
"--symlink-folder", "",
)
runArgs = runArgs.WithStdOut(writer).WithStdErr(writer).WithInteractive(true)

log.Printf("Running install script: bash %s --version %s --install-folder %s --symlink-folder \"\"",
scriptPath, versionArg, installFolder)
fmt.Fprintf(writer, "Installing azd %s channel to %s...\n", cfg.Channel, installFolder)

result, err := m.commandRunner.Run(ctx, runArgs)
if err != nil {
return newUpdateError(CodeReplaceFailed, fmt.Errorf("install script failed: %w", err))
}

if result.ExitCode != 0 {
return newUpdateErrorf(CodeReplaceFailed,
"install script failed with exit code %d", result.ExitCode)
}

log.Printf("Install script completed successfully")
return nil
}

func (m *Manager) updateViaPackageManager(
ctx context.Context,
command string,
Expand Down Expand Up @@ -598,15 +743,27 @@ func (m *Manager) replaceBinary(ctx context.Context, newBinaryPath, currentBinar
// On unix, try with sudo if permission denied
if runtime.GOOS != "windows" {
log.Printf("direct replacement failed (%v), trying with sudo", err)
runArgs := exec.NewRunArgs("sudo", "cp", newBinaryPath, currentBinaryPath)
runArgs = runArgs.WithInteractive(true)
result, sudoErr := m.commandRunner.Run(ctx, runArgs)
if sudoErr != nil {
return newUpdateError(CodeElevationFailed, sudoErr)
// Remove the destination first to avoid "Text file busy" (ETXTBSY) errors.
rmArgs := exec.NewRunArgs("sudo", "rm", "-f", currentBinaryPath)
rmArgs = rmArgs.WithInteractive(true)
rmResult, rmErr := m.commandRunner.Run(ctx, rmArgs)
if rmErr != nil {
return newUpdateError(CodeElevationFailed, fmt.Errorf("sudo rm failed: %w", rmErr))
}
if rmResult.ExitCode != 0 {
return newUpdateErrorf(CodeElevationFailed,
"sudo rm failed with exit code %d", rmResult.ExitCode)
}

cpArgs := exec.NewRunArgs("sudo", "cp", newBinaryPath, currentBinaryPath)
cpArgs = cpArgs.WithInteractive(true)
cpResult, cpErr := m.commandRunner.Run(ctx, cpArgs)
if cpErr != nil {
return newUpdateError(CodeElevationFailed, fmt.Errorf("sudo cp failed: %w", cpErr))
}
if result.ExitCode != 0 {
if cpResult.ExitCode != 0 {
return newUpdateErrorf(CodeElevationFailed,
"sudo copy failed with exit code %d", result.ExitCode)
"sudo cp failed with exit code %d", cpResult.ExitCode)
}
return nil
}
Expand Down Expand Up @@ -766,7 +923,7 @@ func extractFromZip(archivePath, binaryName, destPath string) error {
// IsPackageManagerInstall returns true if azd was installed via a package manager.
func IsPackageManagerInstall() bool {
switch installer.InstalledBy() {
case installer.InstallTypeBrew, installer.InstallTypeWinget, installer.InstallTypeChoco:
case installer.InstallTypeWinget, installer.InstallTypeChoco:
return true
default:
return false
Expand Down
Loading
Loading