aboutsummaryrefslogtreecommitdiffstats
path: root/lua/mason-core/spawn.lua
blob: d604dfe2de8eba2ebe6aa47b2a4cab3ff482570f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
local Result = require "mason-core.result"
local _ = require "mason-core.functional"
local a = require "mason-core.async"
local log = require "mason-core.log"
local platform = require "mason-core.platform"
local process = require "mason-core.process"

local is_not_nil = _.complement(_.equals(vim.NIL))

---@alias JobSpawn table<string, async fun(opts: SpawnArgs): Result>
---@type JobSpawn
local spawn = {
    _flatten_cmd_args = _.compose(_.filter(is_not_nil), _.flatten),
}

---@param cmd string
local function exepath(cmd)
    if platform.is.win then
        -- On Windows, exepath() assumes the system is capable of executing "Unix-like" executables if the shell is a Unix
        -- shell. We temporarily override it to a Windows shell ("powershell") to avoid that behaviour.
        local old_shell = vim.o.shell
        vim.o.shell = "powershell"
        local expanded_cmd = vim.fn.exepath(cmd)
        vim.o.shell = old_shell
        return expanded_cmd
    else
        return vim.fn.exepath(cmd)
    end
end

local function Failure(err, cmd)
    return Result.failure(setmetatable(err, {
        __tostring = function()
            return ("spawn: %s failed with exit code %s and signal %s. %s"):format(
                cmd,
                err.exit_code or "-",
                err.signal or "-",
                err.stderr or ""
            )
        end,
    }))
end

local has_path = _.any(_.starts_with "PATH=")

---@class SpawnArgs
---@field with_paths string[]? Paths to add to the PATH environment variable.
---@field env table<string, string>? Example { SOME_ENV = "value", SOME_OTHER_ENV = "some_value" }
---@field env_raw string[]? Example: { "SOME_ENV=value", "SOME_OTHER_ENV=some_value" }
---@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.

setmetatable(spawn, {
    ---@param canonical_cmd string
    __index = function(self, canonical_cmd)
        ---@param args SpawnArgs
        return function(args)
            local cmd_args = self._flatten_cmd_args(args)
            local env = args.env

            if args.with_paths then
                env = env or {}
                env.PATH = process.extend_path(args.with_paths)
            end

            ---@type JobSpawnOpts
            local spawn_args = {
                stdio_sink = args.stdio_sink,
                cwd = args.cwd,
                env = env and process.graft_env(env) or args.env_raw,
                args = cmd_args,
            }

            if not spawn_args.stdio_sink then
                spawn_args.stdio_sink = process.BufferedSink:new()
            end

            local cmd = canonical_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
                a.scheduler()
                local expanded_cmd = exepath(canonical_cmd)
                if expanded_cmd ~= "" then
                    cmd = expanded_cmd
                end
            end

            local _, exit_code, signal = a.wait(function(resolve)
                local handle, stdio, pid = process.spawn(cmd, spawn_args, resolve)
                if args.on_spawn and handle and stdio and pid then
                    args.on_spawn(handle, stdio, pid)
                end
            end)

            if exit_code == 0 and signal == 0 then
                if getmetatable(spawn_args.stdio_sink) == process.BufferedSink then
                    local sink = spawn_args.stdio_sink --[[@as BufferedSink]]
                    return Result.success {
                        stdout = table.concat(sink.buffers.stdout, "") or nil,
                        stderr = table.concat(sink.buffers.stderr, "") or nil,
                    }
                else
                    return Result.success()
                end
            else
                if getmetatable(spawn_args.stdio_sink) == process.BufferedSink then
                    local sink = spawn_args.stdio_sink --[[@as BufferedSink]]
                    return Failure({
                        exit_code = exit_code,
                        signal = signal,
                        stdout = table.concat(sink.buffers.stdout, "") or nil,
                        stderr = table.concat(sink.buffers.stderr, "") or nil,
                    }, canonical_cmd)
                else
                    return Failure({
                        exit_code = exit_code,
                        signal = signal,
                    }, canonical_cmd)
                end
            end
        end
    end,
})

return spawn