Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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
158 changes: 154 additions & 4 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 fmt.Errorf("failed to determine current path: %w", err)
Comment thread
hemarina marked this conversation as resolved.
Outdated
}
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,7 +743,12 @@ 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)
// Use "rm -f && cp" instead of plain "cp" to avoid "Text file busy" (ETXTBSY) errors.
// On Linux, writing to a running binary's inode fails because the kernel holds a
// write lock on executing files. Removing first unlinks the filename (the running
// process keeps the old inode alive via its fd), then cp creates a new inode.
shellCmd := fmt.Sprintf("rm -f %q && cp %q %q", currentBinaryPath, newBinaryPath, currentBinaryPath)
runArgs := exec.NewRunArgs("sudo", "sh", "-c", shellCmd)
runArgs = runArgs.WithInteractive(true)
Comment thread
hemarina marked this conversation as resolved.
Outdated
result, sudoErr := m.commandRunner.Run(ctx, runArgs)
if sudoErr != nil {
Expand Down Expand Up @@ -766,7 +916,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