aboutsummaryrefslogtreecommitdiffstats
path: root/lua/mason-core/spawn.lua
blob: 0da675698def1223cb8a7c245bc774f0d2e3e19a (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
129
130
131
132
133
134
135
136
137
138
139
140
141
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
---@param path? string
local function exepath(cmd, path)
    local function get_exepath(cmd)
        if path then
            local old_path = vim.env.PATH
            vim.env.PATH = path
            local expanded_cmd = vim.fn.exepath(cmd)
            vim.env.PATH = old_path
            return expanded_cmd
        else
            return vim.fn.exepath(cmd)
        end
    end

    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 = get_exepath(cmd)
        vim.o.shell = old_shell
        return expanded_cmd
    else
        return get_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 get_path_from_env_list = _.compose(_.strip_prefix "PATH=", _.find_first(_.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 then
                a.scheduler()
                local expanded_cmd = exepath(canonical_cmd, spawn_args.env and get_path_from_env_list(spawn_args.env))
                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