aboutsummaryrefslogtreecommitdiffstats
path: root/lua/mason-core/installer/linker.lua
blob: a5c54273d19c607a79492b9bd1efe93045abdcaa (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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
local Result = require "mason-core.result"
local _ = require "mason-core.functional"
local a = require "mason-core.async"
local fs = require "mason-core.fs"
local log = require "mason-core.log"
local path = require "mason-core.path"
local platform = require "mason-core.platform"

local M = {}

---@alias LinkContext { type: '"bin"' | '"opt"' | '"share"', prefix: fun(path: string, location: InstallLocation): string }

---@type table<'"BIN"' | '"OPT"' | '"SHARE"', LinkContext>
local LinkContext = {
    BIN = {
        type = "bin",
        ---@param path string
        ---@param location InstallLocation
        prefix = function(path, location)
            return location:bin(path)
        end,
    },
    OPT = {
        type = "opt",
        ---@param path string
        ---@param location InstallLocation
        prefix = function(path, location)
            return location:opt(path)
        end,
    },
    SHARE = {
        type = "share",
        ---@param path string
        ---@param location InstallLocation
        prefix = function(path, location)
            return location:share(path)
        end,
    },
}

---@param receipt InstallReceipt
---@param link_context LinkContext
---@param location InstallLocation
local function unlink(receipt, link_context, location)
    return Result.pcall(function()
        local links = receipt:get_links()[link_context.type]
        if not links then
            return
        end
        for linked_file in pairs(links) do
            if receipt:get_schema_version() == "1.0" and link_context == LinkContext.BIN and platform.is.win then
                linked_file = linked_file .. ".cmd"
            end
            local share_path = link_context.prefix(linked_file, location)
            fs.sync.unlink(share_path)
        end
    end)
end

---@param pkg Package
---@param receipt InstallReceipt
---@param location InstallLocation
---@nodiscard
function M.unlink(pkg, receipt, location)
    log.fmt_debug("Unlinking %s", pkg, receipt:get_links())
    return Result.try(function(try)
        try(unlink(receipt, LinkContext.BIN, location))
        try(unlink(receipt, LinkContext.SHARE, location))
        try(unlink(receipt, LinkContext.OPT, location))
    end)
end

---@async
---@param context InstallContext
---@param link_context LinkContext
---@param link_fn async fun(new_abs_path: string, target_abs_path: string, target_rel_path: string): Result
local function link(context, link_context, link_fn)
    log.trace("Linking", context.package, link_context.type, context.links[link_context.type])
    return Result.try(function(try)
        for name, rel_path in pairs(context.links[link_context.type]) do
            if platform.is.win and link_context == LinkContext.BIN then
                name = ("%s.cmd"):format(name)
            end
            local new_abs_path = link_context.prefix(name, context.location)
            local target_abs_path = path.concat { context.package:get_install_path(), rel_path }
            local target_rel_path = path.relative(new_abs_path, target_abs_path)

            do
                -- 1. Ensure destination directory exists
                a.scheduler()
                local dir = vim.fn.fnamemodify(new_abs_path, ":h")
                if not fs.async.dir_exists(dir) then
                    try(Result.pcall(fs.async.mkdirp, dir))
                end
            end

            do
                -- 2. Ensure source file exists and target doesn't yet exist OR if --force unlink target if it already
                -- exists.
                if context.opts.force then
                    if fs.async.file_exists(new_abs_path) then
                        try(Result.pcall(fs.async.unlink, new_abs_path))
                    end
                elseif fs.async.file_exists(new_abs_path) then
                    return Result.failure(("%q is already linked."):format(new_abs_path, 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
            end

            -- 3. Execute link.
            try(link_fn(new_abs_path, target_abs_path, target_rel_path))
            context.receipt:with_link(link_context.type, name, rel_path)
        end
    end)
end

---@param context InstallContext
---@param link_context LinkContext
local function symlink(context, link_context)
    return link(context, link_context, function(new_abs_path, _, target_rel_path)
        return Result.pcall(fs.async.symlink, target_rel_path, new_abs_path)
    end)
end

---@param context InstallContext
---@param link_context LinkContext
local function copyfile(context, link_context)
    return link(context, link_context, function(new_abs_path, target_abs_path)
        return Result.pcall(fs.async.copy_file, target_abs_path, new_abs_path, { excl = true })
    end)
end

---@param context InstallContext
local function win_bin_wrapper(context)
    return link(context, LinkContext.BIN, function(new_abs_path, __, target_rel_path)
        local windows_target_rel_path = target_rel_path:gsub("/", "\\")
        return Result.pcall(
            fs.async.write_file,
            new_abs_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%% & "%%dp0%%\%s" %%*
            ]]):format(windows_target_rel_path))
        )
    end)
end

---@async
---@param context InstallContext
---@nodiscard
function M.link(context)
    log.fmt_debug("Linking %s", context.package)
    return Result.try(function(try)
        if platform.is.win then
            try(win_bin_wrapper(context))
            try(copyfile(context, LinkContext.SHARE))
            try(copyfile(context, LinkContext.OPT))
        else
            try(symlink(context, LinkContext.BIN))
            try(symlink(context, LinkContext.SHARE))
            try(symlink(context, LinkContext.OPT))
        end
    end)
end

return M