diff options
Diffstat (limited to 'lua')
| -rw-r--r-- | lua/mason-core/installer/context.lua | 18 | ||||
| -rw-r--r-- | lua/mason-core/installer/init.lua | 123 | ||||
| -rw-r--r-- | lua/mason-core/installer/linker.lua | 152 | ||||
| -rw-r--r-- | lua/mason-core/package/init.lua | 6 | ||||
| -rw-r--r-- | lua/mason-core/path.lua | 5 | ||||
| -rw-r--r-- | lua/mason-core/receipt.lua | 7 | ||||
| -rw-r--r-- | lua/mason-core/result.lua | 3 | ||||
| -rw-r--r-- | lua/mason-core/terminator.lua | 33 |
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 |
