diff --git a/cli/azd/pkg/tools/powershell/powershell.go b/cli/azd/pkg/tools/powershell/powershell.go index 9a5c43c6043..d2c35742024 100644 --- a/cli/azd/pkg/tools/powershell/powershell.go +++ b/cli/azd/pkg/tools/powershell/powershell.go @@ -6,6 +6,7 @@ package powershell import ( "context" "fmt" + "runtime" "strings" "github.com/azure/azure-dev/cli/azd/internal" @@ -54,11 +55,26 @@ func checkPath(options tools.ExecOptions) (err error) { // Executes the specified powershell script // When interactive is true will attach to stdin, stdout & stderr func (bs *powershellScript) Execute(ctx context.Context, path string, options tools.ExecOptions) (exec.RunResult, error) { - if err := bs.checkInstalled(options); err != nil { - return exec.RunResult{}, &internal.ErrorWithSuggestion{ - Err: err, - Suggestion: fmt.Sprintf("PowerShell 7 is not installed or not in the path. To install PowerShell 7, visit %s", - output.WithLinkFormat("https://learn.microsoft.com/powershell/scripting/install/installing-powershell")), + noPwshError := bs.checkInstalled(options) + if noPwshError != nil { + + if runtime.GOOS != "windows" { + return exec.RunResult{}, &internal.ErrorWithSuggestion{ + Err: noPwshError, + Suggestion: fmt.Sprintf( + "PowerShell 7 is not installed or not in the path. To install PowerShell 7, visit %s", + output.WithLinkFormat("https://learn.microsoft.com/powershell/scripting/install/installing-powershell")), + } + } + + options.UserPwsh = "powershell" + if err := bs.checkInstalled(options); err != nil { + return exec.RunResult{}, &internal.ErrorWithSuggestion{ + Err: err, + Suggestion: fmt.Sprintf( + "Make sure pwsh (PowerShell 7) or powershell (PowerShell 5) is installed on your system, visit %s", + output.WithLinkFormat("https://learn.microsoft.com/powershell/scripting/install/installing-powershell")), + } } } @@ -75,5 +91,18 @@ func (bs *powershellScript) Execute(ctx context.Context, path string, options to runArgs = runArgs.WithStdOut(options.StdOut) } - return bs.commandRunner.Run(ctx, runArgs) + result, err := bs.commandRunner.Run(ctx, runArgs) + if err != nil { + if noPwshError != nil { + err = &internal.ErrorWithSuggestion{ + Err: err, + Suggestion: fmt.Sprintf("pwsh (PowerShell 7) was not found and powershell (PowerShell 5) was automatically"+ + " used instead. You can try installing pwsh and trying again in case this script is not compatible "+ + "with PowerShell 5. See: %s", + output.WithLinkFormat("https://learn.microsoft.com/powershell/scripting/install/installing-powershell")), + } + } + } + + return result, err } diff --git a/cli/azd/pkg/tools/powershell/powershell_test.go b/cli/azd/pkg/tools/powershell/powershell_test.go index 317fac4f4ae..8f049aa17af 100644 --- a/cli/azd/pkg/tools/powershell/powershell_test.go +++ b/cli/azd/pkg/tools/powershell/powershell_test.go @@ -6,6 +6,8 @@ package powershell import ( "context" "errors" + "fmt" + "runtime" "strings" "testing" @@ -57,6 +59,57 @@ func Test_Powershell_Execute(t *testing.T) { require.NoError(t, err) }) + t.Run("Success - alternative", func(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skip("pwsh alternative is only for Windows") + } + mockContext := mocks.NewMockContext(context.Background()) + + // #nosec G101 + userPwsh := "pwsh -NoProfile" + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains(args.Cmd, userPwsh) + }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { + require.Equal(t, userPwsh, args.Cmd) + require.Equal(t, workingDir, args.Cwd) + require.Equal(t, scriptPath, args.Args[0]) + require.Equal(t, env, args.Env) + + return exec.NewRunResult(1, "not found", "not found"), nil + }) + + userPwshAlternative := "powershell" + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains(args.Cmd, userPwshAlternative) + }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { + require.Equal(t, userPwshAlternative, args.Cmd) + require.Equal(t, workingDir, args.Cwd) + require.Equal(t, scriptPath, args.Args[0]) + require.Equal(t, env, args.Env) + + return exec.NewRunResult(0, "", ""), nil + }) + + PowershellScript := NewPowershellScriptWithMockCheckPath( + mockContext.CommandRunner, + workingDir, + env, + func(options tools.ExecOptions) error { + if strings.Contains(options.UserPwsh, "pwsh") { + return fmt.Errorf("failed to find PowerShell executable") + } + return nil + }) + runResult, err := PowershellScript.Execute( + *mockContext.Context, + scriptPath, + tools.ExecOptions{UserPwsh: userPwsh, Interactive: to.Ptr(true)}, + ) + + require.NotNil(t, runResult) + require.NoError(t, err) + }) + t.Run("Error", func(t *testing.T) { mockContext := mocks.NewMockContext(context.Background()) @@ -86,7 +139,13 @@ func Test_Powershell_Execute(t *testing.T) { t.Run("NoPowerShellInstalled", func(t *testing.T) { mockContext := mocks.NewMockContext(context.Background()) - PowershellScript := NewPowershellScript(mockContext.CommandRunner, workingDir, env) + PowershellScript := NewPowershellScriptWithMockCheckPath( + mockContext.CommandRunner, + workingDir, + env, + func(options tools.ExecOptions) error { + return fmt.Errorf("failed to find PowerShell executable") + }) _, err := PowershellScript.Execute( *mockContext.Context, scriptPath,