diff --git a/cli/azd/pkg/update/msi_windows.go b/cli/azd/pkg/update/msi_windows.go index 95d2c3d8d9f..bf2751876e9 100644 --- a/cli/azd/pkg/update/msi_windows.go +++ b/cli/azd/pkg/update/msi_windows.go @@ -150,35 +150,35 @@ func isStandardMSIInstall() error { return nil } -// versionFlag returns the install script parameter value for the given channel. -func versionFlag(channel Channel) string { +// buildInstallScriptArgs constructs the PowerShell arguments to run install-azd.ps1. +// +// For the stable channel the script is piped directly through Invoke-Expression. +// No named parameters are needed because the default MSI install location is correct. +// +// For other channels (e.g. daily) the script must be downloaded to a temp directory +// first, because Invoke-Expression from a pipe does not support passing named +// parameters such as -Version or -InstallFolder to the script. +// +// Returns the arguments to pass to the "powershell" command. +func buildInstallScriptArgs(channel Channel) []string { switch channel { case ChannelDaily: - return "daily" - case ChannelStable: - return "stable" + script := fmt.Sprintf( + "$tmpScript = Join-Path $env:TEMP 'azd-install.ps1'; "+ + "Invoke-RestMethod '%s' -OutFile $tmpScript; "+ + "& $tmpScript -Version 'daily' -InstallFolder '%s'; "+ + "Remove-Item $tmpScript -Force -ErrorAction SilentlyContinue", + installScriptURL, expectedPerUserInstallDir(), + ) + return []string{"-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script} default: - return "stable" + script := fmt.Sprintf( + "$tmpScript = Join-Path $env:TEMP 'azd-install.ps1'; "+ + "Invoke-RestMethod '%s' -OutFile $tmpScript; "+ + "& $tmpScript; "+ + "Remove-Item $tmpScript -Force -ErrorAction SilentlyContinue", + installScriptURL, + ) + return []string{"-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script} } } - -// buildInstallScriptArgs constructs the PowerShell arguments to download and run -// install-azd.ps1 with the appropriate -Version flag. -// The -SkipVerify flag is passed because Authenticode verification via -// Get-AuthenticodeSignature failed. -// The MSI is already downloaded over HTTPS from a Microsoft-controlled domain, -// so the transport-level integrity is sufficient. -// Returns the arguments to pass to the "powershell" command. -func buildInstallScriptArgs(channel Channel) []string { - version := versionFlag(channel) - // Download the script to a temp file, then invoke it with the appropriate -Version flag. - // Using -ExecutionPolicy Bypass ensures the script runs even if the system policy is restrictive. - script := fmt.Sprintf( - `$script = Join-Path $env:TEMP 'install-azd.ps1'; `+ - `Invoke-RestMethod '%s' -OutFile $script; `+ - `& $script -Version '%s' -SkipVerify; `+ - `Remove-Item $script -Force -ErrorAction SilentlyContinue`, - installScriptURL, version, - ) - return []string{"-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script} -} diff --git a/cli/azd/pkg/update/msi_windows_test.go b/cli/azd/pkg/update/msi_windows_test.go index 9516908e503..0fc78e9767b 100644 --- a/cli/azd/pkg/update/msi_windows_test.go +++ b/cli/azd/pkg/update/msi_windows_test.go @@ -42,31 +42,16 @@ func TestExpectedPerUserInstallDir(t *testing.T) { } } -func TestVersionFlag(t *testing.T) { - tests := []struct { - name string - channel Channel - want string - }{ - {"stable channel", ChannelStable, "stable"}, - {"daily channel", ChannelDaily, "daily"}, - {"unknown defaults to stable", Channel("nightly"), "stable"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := versionFlag(tt.channel) - require.Equal(t, tt.want, got) - }) - } -} - func TestBuildInstallScriptArgs(t *testing.T) { + t.Setenv("LOCALAPPDATA", `C:\Users\testuser\AppData\Local`) + expectedDir := expectedPerUserInstallDir() + tests := []struct { name string channel Channel // We check that certain substrings appear in the constructed args - wantContains []string + wantContains []string + wantNotContains []string }{ { name: "stable", @@ -76,8 +61,12 @@ func TestBuildInstallScriptArgs(t *testing.T) { "-ExecutionPolicy", "Bypass", "-Command", installScriptURL, - "-Version 'stable'", - "-SkipVerify", + "Invoke-Expression", + }, + wantNotContains: []string{ + "-Version", + "-InstallFolder", + "Remove-Item", }, }, { @@ -89,7 +78,9 @@ func TestBuildInstallScriptArgs(t *testing.T) { "-Command", installScriptURL, "-Version 'daily'", - "-SkipVerify", + "-InstallFolder", + expectedDir, + "Remove-Item", }, }, } @@ -105,26 +96,45 @@ func TestBuildInstallScriptArgs(t *testing.T) { for _, s := range tt.wantContains { require.Contains(t, joined, s, "expected args to contain %q", s) } + for _, s := range tt.wantNotContains { + require.NotContains(t, joined, s, "expected args NOT to contain %q", s) + } }) } } func TestBuildInstallScriptArgs_Structure(t *testing.T) { + t.Setenv("LOCALAPPDATA", `C:\Users\testuser\AppData\Local`) + expectedDir := expectedPerUserInstallDir() + args := buildInstallScriptArgs(ChannelStable) - // The args should be: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command",