aboutsummaryrefslogtreecommitdiffstats
path: root/lua/mason-core/installer/linker.lua
diff options
context:
space:
mode:
Diffstat (limited to 'lua/mason-core/installer/linker.lua')
-rw-r--r--lua/mason-core/installer/linker.lua189
1 files changed, 92 insertions, 97 deletions
diff --git a/lua/mason-core/installer/linker.lua b/lua/mason-core/installer/linker.lua
index 6de95160..529df563 100644
--- a/lua/mason-core/installer/linker.lua
+++ b/lua/mason-core/installer/linker.lua
@@ -8,141 +8,136 @@ 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
+---@alias LinkContext { type: '"bin"' | '"opt"' | '"share"', prefix: fun(path: string): string }
+
+---@type table<'"BIN"' | '"OPT"' | '"SHARE"', LinkContext>
+local LinkContext = {
+ BIN = { type = "bin", prefix = path.bin_prefix },
+ OPT = { type = "opt", prefix = path.opt_prefix },
+ SHARE = { type = "share", prefix = path.share_prefix },
+}
---@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
+---@param link_context LinkContext
+local function unlink(receipt, link_context)
+ return Result.pcall(function()
+ local links = receipt.links[link_context.type]
+ if not links then
+ return
+ end
+ for linked_file in pairs(links) do
+ if receipt.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)
+ fs.sync.unlink(share_path)
+ end
+ end)
end
---@param pkg Package
---@param receipt InstallReceipt
+---@nodiscard
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
+ try(unlink(receipt, LinkContext.BIN))
+ try(unlink(receipt, LinkContext.SHARE))
+ try(unlink(receipt, LinkContext.OPT))
end)
end
---@async
---@param context InstallContext
-local function link_share(context)
+---@param link_context LinkContext
+---@param link_fn async fun(dest: string, target: 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.share) do
- local dest = path.share_prefix(name)
+ 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 dest_abs_path = link_context.prefix(name)
+ local target_abs_path = path.concat { context.package:get_install_path(), rel_path }
do
+ -- 1. Ensure destination directory exists
if vim.in_fast_event() then
a.scheduler()
end
- local dir = vim.fn.fnamemodify(dest, ":h")
+ local dir = vim.fn.fnamemodify(dest_abs_path, ":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))
+ 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(dest_abs_path) then
+ try(Result.pcall(fs.async.unlink, dest_abs_path))
+ end
+ elseif fs.async.file_exists(dest_abs_path) then
+ return Result.failure(("%q is already linked."):format(dest_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
- 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)
+ -- 3. Execute link.
+ try(link_fn(dest_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(target, dest)
+ return Result.pcall(fs.async.symlink, dest, target)
+ end)
+end
+
+---@param context InstallContext
+local function win_bin_wrapper(context)
+ return link(context, LinkContext.BIN, function(dest, target)
+ return Result.pcall(
+ fs.async.write_file,
+ dest,
+ _.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))
+ )
+ end)
+end
+
---@async
---@param context InstallContext
+---@nodiscard
function M.link(context)
log.fmt_debug("Linking %s", context.package)
return Result.try(function(try)
- try(link_bin(context))
- try(link_share(context))
+ if platform.is.win then
+ try(win_bin_wrapper(context))
+ else
+ try(symlink(context, LinkContext.BIN))
+ end
+ try(symlink(context, LinkContext.SHARE))
+ try(symlink(context, LinkContext.OPT))
end)
end