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
|
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): 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 }
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))
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_abs_path)
return Result.pcall(fs.async.symlink, target_abs_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_abs_path)
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%% & "%s" %%*
]]):format(target_abs_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
|