aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorWilliam Boman <william@redwill.se>2025-02-22 01:26:53 +0100
committerGitHub <noreply@github.com>2025-02-22 01:26:53 +0100
commitc8fa5dfaa3ecd104aa1a8c5f77de9c6287adbde3 (patch)
treee9482aecc0677975b000f739047f5d0dd9031016
parentstyle: fix stylua and selene errors (diff)
downloadmason-c8fa5dfaa3ecd104aa1a8c5f77de9c6287adbde3.tar
mason-c8fa5dfaa3ecd104aa1a8c5f77de9c6287adbde3.tar.gz
mason-c8fa5dfaa3ecd104aa1a8c5f77de9c6287adbde3.tar.bz2
mason-c8fa5dfaa3ecd104aa1a8c5f77de9c6287adbde3.tar.lz
mason-c8fa5dfaa3ecd104aa1a8c5f77de9c6287adbde3.tar.xz
mason-c8fa5dfaa3ecd104aa1a8c5f77de9c6287adbde3.tar.zst
mason-c8fa5dfaa3ecd104aa1a8c5f77de9c6287adbde3.zip
fix(spawn): expand executable paths on Windows before passing to uv_spawn (#1885)
This fixes issues on Windows where uv_spawn fails to locate certain types of executables in PATH.
-rw-r--r--lua/mason-core/process.lua2
-rw-r--r--lua/mason-core/spawn.lua37
-rw-r--r--tests/mason-core/spawn_spec.lua106
3 files changed, 85 insertions, 60 deletions
diff --git a/lua/mason-core/process.lua b/lua/mason-core/process.lua
index 22610ef1..882a8d41 100644
--- a/lua/mason-core/process.lua
+++ b/lua/mason-core/process.lua
@@ -224,7 +224,7 @@ function M.spawn(cmd, opts, callback)
if handle == nil then
log.fmt_error("Failed to spawn process. cmd=%s, err=%s", cmd, pid_or_err)
if type(pid_or_err) == "string" and pid_or_err:find "ENOENT" == 1 then
- opts.stdio_sink:stderr(("Could not find executable %q in path.\n"):format(cmd))
+ opts.stdio_sink:stderr(("Could not find executable %q in PATH.\n"):format(cmd))
else
opts.stdio_sink:stderr(("Failed to spawn process cmd=%s err=%s\n"):format(cmd, pid_or_err))
end
diff --git a/lua/mason-core/spawn.lua b/lua/mason-core/spawn.lua
index 26434bd9..0a1bb828 100644
--- a/lua/mason-core/spawn.lua
+++ b/lua/mason-core/spawn.lua
@@ -10,15 +10,6 @@ local is_not_nil = _.complement(_.equals(vim.NIL))
---@alias JobSpawn table<string, async fun(opts: SpawnArgs): Result>
---@type JobSpawn
local spawn = {
- _aliases = {
- npm = platform.is.win and "npm.cmd" or "npm",
- gem = platform.is.win and "gem.cmd" or "gem",
- composer = platform.is.win and "composer.bat" or "composer",
- gradlew = platform.is.win and "gradlew.bat" or "gradlew",
- -- for hererocks installations
- luarocks = (platform.is.win and vim.fn.executable "luarocks.bat" == 1) and "luarocks.bat" or "luarocks",
- rebar3 = platform.is.win and "rebar3.cmd" or "rebar3",
- },
_flatten_cmd_args = _.compose(_.filter(is_not_nil), _.flatten),
}
@@ -35,10 +26,7 @@ local function Failure(err, cmd)
}))
end
-local is_executable = _.memoize(function(cmd)
- a.scheduler()
- return vim.fn.executable(cmd) == 1
-end, _.identity)
+local has_path = _.any(_.starts_with "PATH=")
---@class SpawnArgs
---@field with_paths string[]? Paths to add to the PATH environment variable.
@@ -47,11 +35,10 @@ end, _.identity)
---@field stdio_sink StdioSink? If provided, will be used to write to stdout and stderr.
---@field cwd string?
---@field on_spawn (fun(handle: luv_handle, stdio: luv_pipe[], pid: integer))? Will be called when the process successfully spawns.
----@field check_executable boolean? Whether to check if the provided command is executable (defaults to true).
setmetatable(spawn, {
- ---@param normalized_cmd string
- __index = function(self, normalized_cmd)
+ ---@param canonical_cmd string
+ __index = function(self, canonical_cmd)
---@param args SpawnArgs
return function(args)
local cmd_args = self._flatten_cmd_args(args)
@@ -74,13 +61,15 @@ setmetatable(spawn, {
spawn_args.stdio_sink = process.BufferedSink:new()
end
- local cmd = self._aliases[normalized_cmd] or normalized_cmd
+ local cmd = canonical_cmd
- if (env and env.PATH) == nil and args.check_executable ~= false and not is_executable(cmd) then
- log.fmt_debug("%s is not executable", cmd)
- return Failure({
- stderr = ("%s is not executable"):format(cmd),
- }, cmd)
+ -- Find the executable path via vim.fn.exepath on Windows because libuv fails to resolve certain executables
+ -- in PATH.
+ if platform.is.win and (spawn_args.env and has_path(spawn_args.env)) == nil then
+ local expanded_cmd = vim.fn.exepath(canonical_cmd)
+ if expanded_cmd ~= "" then
+ cmd = expanded_cmd
+ end
end
local _, exit_code, signal = a.wait(function(resolve)
@@ -108,12 +97,12 @@ setmetatable(spawn, {
signal = signal,
stdout = table.concat(sink.buffers.stdout, "") or nil,
stderr = table.concat(sink.buffers.stderr, "") or nil,
- }, cmd)
+ }, canonical_cmd)
else
return Failure({
exit_code = exit_code,
signal = signal,
- }, cmd)
+ }, canonical_cmd)
end
end
end
diff --git a/tests/mason-core/spawn_spec.lua b/tests/mason-core/spawn_spec.lua
index 9fc91200..db8f9575 100644
--- a/tests/mason-core/spawn_spec.lua
+++ b/tests/mason-core/spawn_spec.lua
@@ -1,5 +1,6 @@
local a = require "mason-core.async"
local match = require "luassert.match"
+local platform = require "mason-core.platform"
local process = require "mason-core.process"
local spawn = require "mason-core.spawn"
local spy = require "luassert.spy"
@@ -146,46 +147,81 @@ describe("async spawn", function()
)
end)
- it("should check whether command is executable", function()
- local result = a.run_blocking(spawn.my_cmd, {})
- assert.is_true(result:is_failure())
- assert.equals(
- "spawn: my_cmd failed with exit code - and signal -. my_cmd is not executable",
- tostring(result:err_or_nil())
- )
- end)
+ describe("Windows", function()
+ before_each(function()
+ platform.is.win = true
+ end)
- it("should skip checking whether command is executable", function()
- stub(process, "spawn", function(_, _, callback)
- callback(false, 127)
+ after_each(function()
+ platform.is.win = nil
end)
- local result = a.run_blocking(spawn.my_cmd, { "arg1", check_executable = false })
- assert.is_true(result:is_failure())
- assert.spy(process.spawn).was_called(1)
- assert.spy(process.spawn).was_called_with(
- "my_cmd",
- match.tbl_containing {
- args = match.same { "arg1" },
- },
- match.is_function()
- )
- end)
+ it("should use exepath to get absolute path to executable", function()
+ stub(process, "spawn", function(_, _, callback)
+ callback(true, 0, 0)
+ end)
- it("should skip checking whether command is executable if with_paths is provided", function()
- stub(process, "spawn", function(_, _, callback)
- callback(false, 127)
+ local result = a.run_blocking(spawn.bash, { "arg1" })
+ assert.is_true(result:is_success())
+ assert.spy(process.spawn).was_called(1)
+ assert.spy(process.spawn).was_called_with(
+ vim.fn.exepath "bash",
+ match.tbl_containing {
+ args = match.same { "arg1" },
+ },
+ match.is_function()
+ )
end)
- local result = a.run_blocking(spawn.my_cmd, { "arg1", with_paths = {} })
- assert.is_true(result:is_failure())
- assert.spy(process.spawn).was_called(1)
- assert.spy(process.spawn).was_called_with(
- "my_cmd",
- match.tbl_containing {
- args = match.same { "arg1" },
- },
- match.is_function()
- )
+ it("should not use exepath if env.PATH is set", function()
+ stub(process, "spawn", function(_, _, callback)
+ callback(true, 0, 0)
+ end)
+
+ local result = a.run_blocking(spawn.bash, { "arg1", env = { PATH = "C:\\some\\path" } })
+ assert.is_true(result:is_success())
+ assert.spy(process.spawn).was_called(1)
+ assert.spy(process.spawn).was_called_with(
+ "bash",
+ match.tbl_containing {
+ args = match.same { "arg1" },
+ },
+ match.is_function()
+ )
+ end)
+
+ it("should not use exepath if env_raw.PATH is set", function()
+ stub(process, "spawn", function(_, _, callback)
+ callback(true, 0, 0)
+ end)
+
+ local result = a.run_blocking(spawn.bash, { "arg1", env_raw = { "PATH=C:\\some\\path" } })
+ assert.is_true(result:is_success())
+ assert.spy(process.spawn).was_called(1)
+ assert.spy(process.spawn).was_called_with(
+ "bash",
+ match.tbl_containing {
+ args = match.same { "arg1" },
+ },
+ match.is_function()
+ )
+ end)
+
+ it("should not use exepath if with_paths is provided", function()
+ stub(process, "spawn", function(_, _, callback)
+ callback(true, 0, 0)
+ end)
+
+ local result = a.run_blocking(spawn.bash, { "arg1", with_paths = { "C:\\some\\path" } })
+ assert.is_true(result:is_success())
+ assert.spy(process.spawn).was_called(1)
+ assert.spy(process.spawn).was_called_with(
+ "bash",
+ match.tbl_containing {
+ args = match.same { "arg1" },
+ },
+ match.is_function()
+ )
+ end)
end)
end)