aboutsummaryrefslogtreecommitdiffstats
path: root/lua
diff options
context:
space:
mode:
Diffstat (limited to 'lua')
-rw-r--r--lua/mason-core/installer/context.lua18
-rw-r--r--lua/mason-core/installer/init.lua123
-rw-r--r--lua/mason-core/installer/linker.lua152
-rw-r--r--lua/mason-core/package/init.lua6
-rw-r--r--lua/mason-core/path.lua5
-rw-r--r--lua/mason-core/receipt.lua7
-rw-r--r--lua/mason-core/result.lua3
-rw-r--r--lua/mason-core/terminator.lua33
8 files changed, 231 insertions, 116 deletions
diff --git a/lua/mason-core/installer/context.lua b/lua/mason-core/installer/context.lua
index 0b07b528..8acfa5ef 100644
--- a/lua/mason-core/installer/context.lua
+++ b/lua/mason-core/installer/context.lua
@@ -146,8 +146,9 @@ end
---@field public handle InstallHandle
---@field public package Package
---@field public cwd CwdManager
+---@field public opts PackageInstallOpts
---@field public stdio_sink StdioSink
----@field private bin_links table<string, string>
+---@field links { bin: table<string, string>, share: table<string, string> }
local InstallContext = {}
InstallContext.__index = InstallContext
@@ -164,7 +165,11 @@ function InstallContext.new(handle, opts)
receipt = receipt.InstallReceiptBuilder.new(),
requested_version = Optional.of_nilable(opts.version),
stdio_sink = handle.stdio.sink,
- bin_links = {},
+ links = {
+ bin = {},
+ share = {},
+ },
+ opts = opts,
}, InstallContext)
end
@@ -318,7 +323,14 @@ end
---@param executable string
---@param rel_path string
function InstallContext:link_bin(executable, rel_path)
- self.bin_links[executable] = rel_path
+ self.links.bin[executable] = rel_path
+ return self
+end
+
+---@param rel_dest string
+---@param rel_source string
+function InstallContext:link_share(rel_dest, rel_source)
+ self.links.share[rel_dest] = rel_source
return self
end
diff --git a/lua/mason-core/installer/init.lua b/lua/mason-core/installer/init.lua
index a11de45d..8e05cb20 100644
--- a/lua/mason-core/installer/init.lua
+++ b/lua/mason-core/installer/init.lua
@@ -17,24 +17,34 @@ local M = {}
---@async
local function create_prefix_dirs()
- for _, p in ipairs { path.install_prefix(), path.bin_prefix(), path.package_prefix(), path.package_build_prefix() } do
- if not fs.async.dir_exists(p) then
- fs.async.mkdirp(p)
+ return Result.try(function(try)
+ for _, p in ipairs {
+ path.install_prefix(),
+ path.bin_prefix(),
+ path.share_prefix(),
+ path.package_prefix(),
+ path.package_build_prefix(),
+ } do
+ if not fs.async.dir_exists(p) then
+ try(Result.pcall(fs.async.mkdirp, p))
+ end
end
- end
+ end)
end
---@async
---@param context InstallContext
local function write_receipt(context)
- log.fmt_debug("Writing receipt for %s", context.package)
- context.receipt
- :with_name(context.package.name)
- :with_schema_version("1.0")
- :with_completion_time(vim.loop.gettimeofday())
- local receipt_path = path.concat { context.cwd:get(), "mason-receipt.json" }
- local install_receipt = context.receipt:build()
- fs.async.write_file(receipt_path, vim.json.encode(install_receipt))
+ return Result.pcall(function()
+ log.fmt_debug("Writing receipt for %s", context.package)
+ context.receipt
+ :with_name(context.package.name)
+ :with_schema_version("1.1")
+ :with_completion_time(vim.loop.gettimeofday())
+ local receipt_path = path.concat { context.cwd:get(), "mason-receipt.json" }
+ local install_receipt = context.receipt:build()
+ fs.async.write_file(receipt_path, vim.json.encode(install_receipt))
+ end)
end
local CONTEXT_REQUEST = {}
@@ -47,13 +57,17 @@ end
---@async
---@param context InstallContext
function M.prepare_installer(context)
- create_prefix_dirs()
- local package_build_prefix = path.package_build_prefix(context.package.name)
- if fs.async.dir_exists(package_build_prefix) then
- fs.async.rmrf(package_build_prefix)
- end
- fs.async.mkdirp(package_build_prefix)
- context.cwd:set(package_build_prefix)
+ return Result.try(function(try)
+ try(create_prefix_dirs())
+ local package_build_prefix = path.package_build_prefix(context.package.name)
+ if fs.async.dir_exists(package_build_prefix) then
+ try(Result.pcall(fs.async.rmrf, package_build_prefix))
+ end
+ try(Result.pcall(fs.async.mkdirp, package_build_prefix))
+ context.cwd:set(package_build_prefix)
+
+ return context.package.spec.install
+ end)
end
---@async
@@ -81,12 +95,40 @@ function M.exec_in_context(context, fn)
end
end
context.receipt:with_start_time(vim.loop.gettimeofday())
- M.prepare_installer(context)
step(context)
return ret_val
end
---@async
+---@param context InstallContext
+---@param installer async fun(ctx: InstallContext)
+local function run_installer(context, installer)
+ local handle = context.handle
+ return Result.pcall(function()
+ return a.wait(function(resolve, reject)
+ local cancel_thread = a.run(M.exec_in_context, function(success, result)
+ if success then
+ resolve(result)
+ else
+ reject(result)
+ end
+ end, context, installer)
+
+ handle:once("terminate", function()
+ cancel_thread()
+ if handle:is_closed() then
+ reject "Installation was aborted."
+ else
+ handle:once("closed", function()
+ reject "Installation was aborted."
+ end)
+ end
+ end)
+ end)
+ end)
+end
+
+---@async
---@param handle InstallHandle
---@param opts PackageInstallOpts
function M.execute(handle, opts)
@@ -117,34 +159,25 @@ function M.execute(handle, opts)
handle:on("stderr", append_log)
end
- log.fmt_info("Executing installer for %s", pkg)
- return Result.run_catching(function()
- -- 1. run installer
- a.wait(function(resolve, reject)
- local cancel_thread = a.run(M.exec_in_context, function(success, result)
- if success then
- resolve(result)
- else
- reject(result)
- end
- end, context, pkg.spec.install)
+ log.fmt_info("Executing installer for %s version=%s", pkg, opts.version or "latest")
- handle:once("terminate", function()
- handle:once("closed", function()
- reject "Installation was aborted."
- end)
- cancel_thread()
- end)
- end)
+ return Result.try(function(try)
+ -- 1. prepare directories and initialize cwd
+ local installer = try(M.prepare_installer(context))
+
+ -- 2. execute installer
+ try(run_installer(context, installer))
- -- 2. promote temporary installation dir
- context:promote_cwd()
+ -- 3. promote temporary installation dir
+ try(Result.pcall(function()
+ context:promote_cwd()
+ end))
- -- 3. link package
- linker.link(context)
+ -- 4. link package
+ try(linker.link(context))
- -- 4. write receipt
- write_receipt(context)
+ -- 5. write receipt
+ try(write_receipt(context))
end)
:on_success(function()
permit:forget()
@@ -173,7 +206,7 @@ function M.execute(handle, opts)
end
-- unlink linked executables (in the rare occasion an error occurs after linking)
- linker.unlink(context.package, context.receipt.links)
+ linker.unlink(context.package, context.receipt)
if not handle:is_closed() and not handle.is_terminated then
handle:close()
diff --git a/lua/mason-core/installer/linker.lua b/lua/mason-core/installer/linker.lua
index a1cc53d0..6de95160 100644
--- a/lua/mason-core/installer/linker.lua
+++ b/lua/mason-core/installer/linker.lua
@@ -1,62 +1,85 @@
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 pkg Package
----@param links InstallReceiptLinks
-local function unlink_bin(pkg, links)
- for executable in pairs(links.bin) do
+---@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 pkg Package
----@param links InstallReceiptLinks
-function M.unlink(pkg, links)
- log.fmt_debug("Unlinking %s", pkg)
- unlink_bin(pkg, links)
+---@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 to string
-local function relative_path_from_bin(to)
- local _, match_end = to:find(path.install_prefix(), 1, true)
- assert(match_end, "Failed to produce relative path.")
- local relative_path = to:sub(match_end + 1)
- return ".." .. relative_path
+---@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)
- local links = context.bin_links
- local pkg = context.package
- for name, rel_path in pairs(links) do
- local target_abs_path = path.concat { pkg:get_install_path(), rel_path }
- local target_rel_path = relative_path_from_bin(target_abs_path)
- local bin_path = path.bin_prefix(name)
+ 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)
- assert(not fs.async.file_exists(bin_path), ("bin/%s is already linked."):format(name))
- assert(fs.async.file_exists(target_abs_path), ("Link target %q does not exist."):format(target_abs_path))
+ 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_rel_path)
+ log.fmt_debug("Linking bin %s to %s", name, target_abs_path)
- platform.when {
- unix = function()
- fs.async.symlink(target_rel_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()|)
- fs.async.write_file(
- ("%s.cmd"):format(bin_path),
- _.dedent(([[
+ 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
@@ -66,20 +89,61 @@ local function link_bin(context)
SETLOCAL
CALL :find_dp0
- endLocal & goto #_undefined_# 2>NUL || title %%COMSPEC%% & "%%dp0%%\%s" %%*
- ]]):format(target_rel_path))
- )
- end,
- }
- context.receipt:with_link("bin", name, rel_path)
- end
+ 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)
- link_bin(context)
+ return Result.try(function(try)
+ try(link_bin(context))
+ try(link_share(context))
+ end)
end
return M
diff --git a/lua/mason-core/package/init.lua b/lua/mason-core/package/init.lua
index 9e1f0d19..a7ee6f14 100644
--- a/lua/mason-core/package/init.lua
+++ b/lua/mason-core/package/init.lua
@@ -92,7 +92,7 @@ function Package:new_handle()
return handle
end
----@alias PackageInstallOpts { version: string?, debug: boolean?, target: string? }
+---@alias PackageInstallOpts { version?: string, debug?: boolean, target?: string, force?: boolean }
---@param opts? PackageInstallOpts
---@return InstallHandle
@@ -152,8 +152,8 @@ function Package:unlink()
log.fmt_trace("Unlinking %s", self)
local install_path = self:get_install_path()
-- 1. Unlink
- self:get_receipt():map(_.prop "links"):if_present(function(links)
- linker.unlink(self, links)
+ self:get_receipt():if_present(function(receipt)
+ linker.unlink(self, receipt)
end)
-- 2. Remove installation artifacts
diff --git a/lua/mason-core/path.lua b/lua/mason-core/path.lua
index 5986c1d7..6a4c5663 100644
--- a/lua/mason-core/path.lua
+++ b/lua/mason-core/path.lua
@@ -38,6 +38,11 @@ function M.bin_prefix(executable)
return M.concat { M.install_prefix "bin", executable }
end
+---@param share string?
+function M.share_prefix(share)
+ return M.concat { M.install_prefix "share", share }
+end
+
---@param name string?
function M.package_prefix(name)
return M.concat { M.install_prefix "packages", name }
diff --git a/lua/mason-core/receipt.lua b/lua/mason-core/receipt.lua
index 68f6cf12..281d7148 100644
--- a/lua/mason-core/receipt.lua
+++ b/lua/mason-core/receipt.lua
@@ -2,6 +2,7 @@ local M = {}
---@alias InstallReceiptSchemaVersion
---| '"1.0"'
+---| '"1.1"'
---@alias InstallReceiptSourceType
---| '"npm"'
@@ -22,7 +23,8 @@ local M = {}
---@alias InstallReceiptSource {type: InstallReceiptSourceType}
---@class InstallReceiptLinks
----@field bin table<string, string>
+---@field bin? table<string, string>
+---@field share? table<string, string>
---@class InstallReceiptBuilder
---@field private secondary_sources InstallReceiptSource[]
@@ -36,6 +38,7 @@ function InstallReceiptBuilder.new()
secondary_sources = {},
links = {
bin = vim.empty_dict(),
+ share = vim.empty_dict(),
},
}, InstallReceiptBuilder)
end
@@ -64,7 +67,7 @@ function InstallReceiptBuilder:with_secondary_source(source)
return self
end
----@param typ '"bin"'
+---@param typ '"bin"' | '"share"'
---@param name string
---@param rel_path string
function InstallReceiptBuilder:with_link(typ, name, rel_path)
diff --git a/lua/mason-core/result.lua b/lua/mason-core/result.lua
index 6e7f942c..f3b76339 100644
--- a/lua/mason-core/result.lua
+++ b/lua/mason-core/result.lua
@@ -185,8 +185,7 @@ function Result.try(fn)
step = function(...)
local ok, result = coroutine.resume(thread, ...)
if not ok then
- -- l'exception! panique!!!
- error(result, 0)
+ return Result.failure(result)
end
if coroutine.status(thread) == "dead" then
if getmetatable(result) == Result then
diff --git a/lua/mason-core/terminator.lua b/lua/mason-core/terminator.lua
index f855834a..4c7d0125 100644
--- a/lua/mason-core/terminator.lua
+++ b/lua/mason-core/terminator.lua
@@ -27,26 +27,25 @@ local function terminate_handles(handles, grace_ms)
---@param handle InstallHandle
function(handle)
return function()
- a.wait_first {
- function()
- if not handle:is_closed() then
- handle:terminate()
- end
- a.wait(function(resolve)
- if handle:is_closed() then
- resolve()
- else
- handle:once("closed", resolve)
- end
- end)
- end,
- function()
- a.sleep(grace_ms)
+ local timer
+ if not handle:is_closed() then
+ handle:terminate()
+ timer = vim.defer_fn(function()
if not handle:is_closed() then
handle:kill(9) -- SIGKILL
end
- end,
- }
+ end, grace_ms)
+ end
+ a.wait(function(resolve)
+ if handle:is_closed() then
+ resolve()
+ else
+ handle:once("closed", resolve)
+ end
+ end)
+ if timer then
+ timer:stop()
+ end
end
end,
handles