aboutsummaryrefslogtreecommitdiffstats
path: root/lua/mason-core/installer/linker.lua
blob: 6de951606caf1febf5f61c2d7db3cd9e87171629 (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
142
143
144
145
146
147
148
149
local path = require "mason-core.path"
local Result = require "mason-core.result"
local platform = require "mason-core.platform"
local _ = require "mason-core.functional"
local log = require "mason-core.log"
local fs = require "mason-core.fs"
local a = require "mason-core.async"

local M = {}

---@param receipt InstallReceipt
local function unlink_bin(receipt)
    local bin = receipt.links.bin
    if not bin then
        return
    end
    -- Windows executables did not include file extension in bin receipts on 1.0.
    local should_append_cmd = platform.is.win and receipt.schema_version == "1.0"
    for executable in pairs(bin) do
        if should_append_cmd then
            executable = executable .. ".cmd"
        end
        local bin_path = path.bin_prefix(executable)
        fs.sync.unlink(bin_path)
    end
end

---@param receipt InstallReceipt
local function unlink_share(receipt)
    local share = receipt.links.share
    if not share then
        return
    end
    for share_file in pairs(share) do
        local bin_path = path.share_prefix(share_file)
        fs.sync.unlink(bin_path)
    end
end

---@param pkg Package
---@param receipt InstallReceipt
function M.unlink(pkg, receipt)
    log.fmt_debug("Unlinking %s", pkg, receipt.links)
    unlink_bin(receipt)
    unlink_share(receipt)
end

---@async
---@param context InstallContext
local function link_bin(context)
    return Result.try(function(try)
        local links = context.links.bin
        local pkg = context.package
        for name, rel_path in pairs(links) do
            if platform.is.win then
                name = ("%s.cmd"):format(name)
            end
            local target_abs_path = path.concat { pkg:get_install_path(), rel_path }
            local bin_path = path.bin_prefix(name)

            if not context.opts.force and fs.async.file_exists(bin_path) then
                return Result.failure(("bin/%s is already linked."):format(name))
            end
            if not fs.async.file_exists(target_abs_path) then
                return Result.failure(("Link target %q does not exist."):format(target_abs_path))
            end

            log.fmt_debug("Linking bin %s to %s", name, target_abs_path)

            platform.when {
                unix = function()
                    try(Result.pcall(fs.async.symlink, target_abs_path, bin_path))
                end,
                win = function()
                    -- We don't "symlink" on Windows because:
                    -- 1) .LNK is not commonly found in PATHEXT
                    -- 2) some executables can only run from their true installation location
                    -- 3) many utilities only consider .COM, .EXE, .CMD, .BAT files as candidates by default when resolving executables (e.g. neovim's |exepath()| and |executable()|)
                    try(Result.pcall(
                        fs.async.write_file,
                        bin_path,
                        _.dedent(([[
                        @ECHO off
                        GOTO start
                        :find_dp0
                        SET dp0=%%~dp0
                        EXIT /b
                        :start
                        SETLOCAL
                        CALL :find_dp0

                        endLocal & goto #_undefined_# 2>NUL || title %%COMSPEC%% & "%s" %%*
                ]]):format(target_abs_path))
                    ))
                end,
            }
            context.receipt:with_link("bin", name, rel_path)
        end
    end)
end

---@async
---@param context InstallContext
local function link_share(context)
    return Result.try(function(try)
        for name, rel_path in pairs(context.links.share) do
            local dest = path.share_prefix(name)

            do
                if vim.in_fast_event() then
                    a.scheduler()
                end

                local dir = vim.fn.fnamemodify(dest, ":h")
                if not fs.async.dir_exists(dir) then
                    try(Result.pcall(fs.async.mkdirp, dir))
                end
            end

            local target_abs_path = path.concat { context.package:get_install_path(), rel_path }

            if context.opts.force then
                if fs.async.file_exists(dest) then
                    try(Result.pcall(fs.async.unlink, dest))
                end
            elseif fs.async.file_exists(dest) then
                return Result.failure(("share/%s is already linked."):format(name))
            end
            if not fs.async.file_exists(target_abs_path) then
                return Result.failure(("Link target %q does not exist."):format(target_abs_path))
            end

            try(Result.pcall(fs.async.symlink, target_abs_path, dest))
            context.receipt:with_link("share", name, rel_path)
        end
    end)
end

---@async
---@param context InstallContext
function M.link(context)
    log.fmt_debug("Linking %s", context.package)
    return Result.try(function(try)
        try(link_bin(context))
        try(link_share(context))
    end)
end

return M