diff --git a/lib/vagrant/util/powershell.rb b/lib/vagrant/util/powershell.rb index 2059e1ee9..55c7e5306 100644 --- a/lib/vagrant/util/powershell.rb +++ b/lib/vagrant/util/powershell.rb @@ -19,7 +19,7 @@ module Vagrant # Number of seconds to wait while attempting to get powershell version DEFAULT_VERSION_DETECTION_TIMEOUT = 30 # Names of the powershell executable - POWERSHELL_NAMES = ["powershell", "pwsh"].map(&:freeze).freeze + POWERSHELL_NAMES = ["pwsh", "powershell"].map(&:freeze).freeze # Paths to powershell executable POWERSHELL_PATHS = [ "%SYSTEMROOT%/System32/WindowsPowerShell/v1.0", @@ -33,11 +33,29 @@ module Vagrant # @return [String|nil] a powershell executable, depending on environment def self.executable if !defined?(@_powershell_executable) + prefer_name = ENV["VAGRANT_PREFERRED_POWERSHELL"].to_s.sub(".exe", "") + if !POWERSHELL_NAMES.include?(prefer_name) + prefer_name = POWERSHELL_NAMES.first + end + + LOGGER.debug("preferred powershell executable name: #{prefer_name}") + # First start with detecting executable on configured path - POWERSHELL_NAMES.detect do |psh| - return @_powershell_executable = psh if Which.which(psh) - psh += ".exe" - return @_powershell_executable = psh if Which.which(psh) + found_shells = Hash.new.tap do |found| + POWERSHELL_NAMES.each do |psh| + psh_path = Which.which(psh) + psh_path = Which.which(psh + ".exe") if !psh_path + next if !psh_path + + LOGGER.debug("detected powershell for #{psh.inspect} - #{psh_path}") + found[psh] = psh_path + end + end + + # Done if preferred shell was found + if found_shells.key?(prefer_name) + LOGGER.debug("using preferred powershell #{prefer_name.inspect} - #{found_shells[prefer_name]}") + return @_powershell_executable = found_shells[prefer_name] end # Now attempt with paths @@ -48,17 +66,29 @@ module Vagrant paths.each do |psh_path| POWERSHELL_NAMES.each do |psh| + next if found_shells.key?(psh) + path = File.join(psh_path, psh) - return @_powershell_executable = path if Which.which(path) - - path += ".exe" - return @_powershell_executable = path if Which.which(path) - - # Finally test the msys2 style path - path = path.sub(/^([A-Za-z]):/, "/mnt/\\1") - return @_powershell_executable = path if Which.which(path) + [path, "#{path}.exe", path.sub(/^([A-Za-z]):/, "/mnt/\\1")].each do |full_path| + if File.executable?(full_path) + found_shells[psh] = full_path + break + end + end end end + + # Done if preferred shell was found + if found_shells.key?(prefer_name) + LOGGER.debug("using preferred powershell #{prefer_name.inspect} - #{found_shells[prefer_name]}") + return @_powershell_executable = found_shells[prefer_name] + end + + # Iterate names and return first found + POWERSHELL_NAMES.each do |psh| + LOGGER.debug("using powershell #{prefer_name.inspect} - #{found_shells[prefer_name]}") + return @_powershell_executable = found_shells[psh] if found_shells.key?(psh) + end end @_powershell_executable end @@ -94,6 +124,7 @@ module Vagrant "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", + "-Command", "#{env}&('#{path}')", args ].flatten diff --git a/test/unit/vagrant/util/powershell_test.rb b/test/unit/vagrant/util/powershell_test.rb index bbe36df98..39ef3b30b 100644 --- a/test/unit/vagrant/util/powershell_test.rb +++ b/test/unit/vagrant/util/powershell_test.rb @@ -49,6 +49,7 @@ describe Vagrant::Util::PowerShell do describe ".executable" do before do + allow(ENV).to receive(:[]).with("VAGRANT_PREFERRED_POWERSHELL").and_return(nil) allow(Vagrant::Util::Which).to receive(:which).and_return(nil) allow(Vagrant::Util::Subprocess).to receive(:execute) do |*args| Vagrant::Util::Subprocess::Result.new(0, args.last.sub("echo ", ""), "") @@ -57,7 +58,7 @@ describe Vagrant::Util::PowerShell do context "when powershell found in PATH" do before{ expect(Vagrant::Util::Which).to receive(:which). - with("powershell").and_return(true) } + with("powershell").and_return("powershell") } it "should return powershell string" do expect(described_class.executable).to eq("powershell") @@ -66,7 +67,7 @@ describe Vagrant::Util::PowerShell do context "when pwsh found in PATH" do before { expect(Vagrant::Util::Which).to receive(:which). - with("pwsh").and_return(true) } + with("pwsh").and_return("pwsh") } it "should return pwsh string" do expect(described_class.executable).to eq("pwsh") @@ -74,6 +75,8 @@ describe Vagrant::Util::PowerShell do end context "when not found in PATH" do + before { allow(File).to receive(:executable?) } + it "should return nil" do expect(described_class.executable).to be_nil end @@ -85,15 +88,46 @@ describe Vagrant::Util::PowerShell do it "should return powershell.exe when found" do expect(Vagrant::Util::Which).to receive(:which). - with("powershell.exe").and_return(true) + with("powershell.exe").and_return("powershell.exe") expect(described_class.executable).to eq("powershell.exe") end it "should check for powershell with full path" do - expect(Vagrant::Util::Which).to receive(:which).with(/WindowsPowerShell\/v1.0\/powershell.exe/) + expect(File).to receive(:executable?).with(/WindowsPowerShell\/v1.0\/powershell.exe/) described_class.executable end end + + context "powershell preference" do + before do + allow(Vagrant::Util::Which).to receive(:which) + allow(File).to receive(:executable?) + end + + it "should prefer pwsh found on in the PATH" do + expect(Vagrant::Util::Which).to receive(:which).with("pwsh.exe").and_return("pwsh.exe") + expect(described_class.executable).to eq("pwsh.exe") + end + + it "should use powershell.exe when found on PATH and pwsh.exe is not" do + expect(Vagrant::Util::Which).to receive(:which).with("pwsh.exe").and_return("powershell.exe") + expect(described_class.executable).to eq("powershell.exe") + end + + it "should prefer powershell.exe when env var is set and powershell.exe and pwsh.exe are on PATH" do + expect(ENV).to receive(:[]).with("VAGRANT_PREFERRED_POWERSHELL").and_return("powershell") + expect(Vagrant::Util::Which).to receive(:which).with("pwsh.exe").and_return("pwsh.exe") + expect(Vagrant::Util::Which).to receive(:which).with("powershell.exe").and_return("powershell.exe") + expect(described_class.executable).to eq("powershell.exe") + end + + it "should use pwsh.exe when env var is set to powershell but only pwsh.exe is avaialble" do + expect(ENV).to receive(:[]).with("VAGRANT_PREFERRED_POWERSHELL").and_return("powershell") + expect(Vagrant::Util::Which).to receive(:which).with("pwsh.exe").and_return("pwsh.exe") + expect(Vagrant::Util::Which).to receive(:which).with("powershell.exe").and_return(nil) + expect(described_class.executable).to eq("pwsh.exe") + end + end end describe ".available?" do diff --git a/website/content/docs/other/environmental-variables.mdx b/website/content/docs/other/environmental-variables.mdx index 4ac9f3c20..4b9c86e37 100644 --- a/website/content/docs/other/environmental-variables.mdx +++ b/website/content/docs/other/environmental-variables.mdx @@ -268,6 +268,14 @@ detection. When setting this environment variable, its value will be in seconds. By default, it will use 30 seconds as a timeout. +## `VAGRANT_PREFERRED_POWERSHELL` + +When executing PowerShell commands, Vagrant will prefer to use `pwsh.exe` +over `powershell.exe` by default. This environment variable can be used to +modify this preference and make Vagrant prefer `powershell.exe`. The value +set in this environment variable are any supported PowerShell executables +which currently are: `powershell` and `pwsh`. + ## `VAGRANT_PREFERRED_PROVIDERS` This configures providers that Vagrant should prefer.