From 12af53a6a3e0c598b57e1cdc7ddbb980b3529976 Mon Sep 17 00:00:00 2001 From: Chris Roberts Date: Tue, 15 Apr 2025 11:25:21 -0700 Subject: [PATCH] Prefer pwsh executable over powershell excutable Prefer to use the pwsh executable over the powershell executable as the pwsh exectuable will be faster loading than the powershell executable. If the pwsh executable is not found, the powershell executable will be used instead. The preference can be overridden using the VAGRANT_PREFERRED_POWERSHELL environment variable. --- lib/vagrant/util/powershell.rb | 57 ++++++++++++++----- test/unit/vagrant/util/powershell_test.rb | 42 ++++++++++++-- .../docs/other/environmental-variables.mdx | 8 +++ 3 files changed, 90 insertions(+), 17 deletions(-) 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.