diff options
| author | William Boman <william@redwill.se> | 2023-02-26 07:57:42 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-02-26 07:57:42 +0100 |
| commit | 2e83e412d877a7e6daf04b2b6359521f6fb8c20e (patch) | |
| tree | 5c9488569a0e8b9f2aa3d492c666aed4fb55b194 /lua | |
| parent | chore: autogenerate (#1028) (diff) | |
| download | mason-2e83e412d877a7e6daf04b2b6359521f6fb8c20e.tar mason-2e83e412d877a7e6daf04b2b6359521f6fb8c20e.tar.gz mason-2e83e412d877a7e6daf04b2b6359521f6fb8c20e.tar.bz2 mason-2e83e412d877a7e6daf04b2b6359521f6fb8c20e.tar.lz mason-2e83e412d877a7e6daf04b2b6359521f6fb8c20e.tar.xz mason-2e83e412d877a7e6daf04b2b6359521f6fb8c20e.tar.zst mason-2e83e412d877a7e6daf04b2b6359521f6fb8c20e.zip | |
refactor: simplify linker & receipt writing (#1033)
Diffstat (limited to 'lua')
| -rw-r--r-- | lua/mason-core/installer/context.lua | 10 | ||||
| -rw-r--r-- | lua/mason-core/installer/init.lua | 26 | ||||
| -rw-r--r-- | lua/mason-core/installer/linker.lua | 189 | ||||
| -rw-r--r-- | lua/mason-core/package/init.lua | 2 | ||||
| -rw-r--r-- | lua/mason-core/path.lua | 11 | ||||
| -rw-r--r-- | lua/mason-core/receipt.lua | 50 | ||||
| -rw-r--r-- | lua/mason-core/result.lua | 6 |
7 files changed, 152 insertions, 142 deletions
diff --git a/lua/mason-core/installer/context.lua b/lua/mason-core/installer/context.lua index 8acfa5ef..93fe688b 100644 --- a/lua/mason-core/installer/context.lua +++ b/lua/mason-core/installer/context.lua @@ -148,7 +148,7 @@ end ---@field public cwd CwdManager ---@field public opts PackageInstallOpts ---@field public stdio_sink StdioSink ----@field links { bin: table<string, string>, share: table<string, string> } +---@field links { bin: table<string, string>, share: table<string, string>, opt: table<string, string> } local InstallContext = {} InstallContext.__index = InstallContext @@ -168,6 +168,7 @@ function InstallContext.new(handle, opts) links = { bin = {}, share = {}, + opt = {}, }, opts = opts, }, InstallContext) @@ -327,11 +328,4 @@ function InstallContext:link_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 - return InstallContext diff --git a/lua/mason-core/installer/init.lua b/lua/mason-core/installer/init.lua index 8e05cb20..9f150269 100644 --- a/lua/mason-core/installer/init.lua +++ b/lua/mason-core/installer/init.lua @@ -34,16 +34,14 @@ end ---@async ---@param context InstallContext -local function write_receipt(context) +local function build_receipt(context) return Result.pcall(function() - log.fmt_debug("Writing receipt for %s", context.package) - context.receipt + log.fmt_debug("Building receipt for %s", context.package) + return 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)) + :build() end) end @@ -176,8 +174,12 @@ function M.execute(handle, opts) -- 4. link package try(linker.link(context)) - -- 5. write receipt - try(write_receipt(context)) + -- 5. build & write receipt + ---@type InstallReceipt + local receipt = try(build_receipt(context)) + try(Result.pcall(function() + receipt:write(context.cwd:get()) + end)) end) :on_success(function() permit:forget() @@ -205,8 +207,12 @@ function M.execute(handle, opts) ) end - -- unlink linked executables (in the rare occasion an error occurs after linking) - linker.unlink(context.package, context.receipt) + -- unlink linked executables (in the occasion an error occurs after linking) + build_receipt(context):on_success(function(receipt) + linker.unlink(context.package, receipt):on_failure(function(err) + log.error("Failed to unlink failed installation", err) + end) + end) 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 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 diff --git a/lua/mason-core/package/init.lua b/lua/mason-core/package/init.lua index a7ee6f14..05d72751 100644 --- a/lua/mason-core/package/init.lua +++ b/lua/mason-core/package/init.lua @@ -153,7 +153,7 @@ function Package:unlink() local install_path = self:get_install_path() -- 1. Unlink self:get_receipt():if_present(function(receipt) - linker.unlink(self, receipt) + linker.unlink(self, receipt):get_or_throw() end) -- 2. Remove installation artifacts diff --git a/lua/mason-core/path.lua b/lua/mason-core/path.lua index 6a4c5663..b63b6c4a 100644 --- a/lua/mason-core/path.lua +++ b/lua/mason-core/path.lua @@ -38,9 +38,14 @@ 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 } +---@param file string? +function M.share_prefix(file) + return M.concat { M.install_prefix "share", file } +end + +---@param file string? +function M.opt_prefix(file) + return M.concat { M.install_prefix "opt", file } end ---@param name string? diff --git a/lua/mason-core/receipt.lua b/lua/mason-core/receipt.lua index 281d7148..4beeb6aa 100644 --- a/lua/mason-core/receipt.lua +++ b/lua/mason-core/receipt.lua @@ -25,6 +25,33 @@ local M = {} ---@class InstallReceiptLinks ---@field bin? table<string, string> ---@field share? table<string, string> +---@field opt? table<string, string> + +---@class InstallReceipt<T> : { primary_source: T } +---@field public name string +---@field public schema_version InstallReceiptSchemaVersion +---@field public metrics {start_time:integer, completion_time:integer} +---@field public primary_source InstallReceiptSource +---@field public secondary_sources InstallReceiptSource[] +---@field public links InstallReceiptLinks +local InstallReceipt = {} +InstallReceipt.__index = InstallReceipt + +function InstallReceipt.new(data) + return setmetatable(data, InstallReceipt) +end + +function InstallReceipt.from_json(json) + return InstallReceipt.new(json) +end + +---@async +---@param cwd string +function InstallReceipt:write(cwd) + local path = require "mason-core.path" + local fs = require "mason-core.fs" + fs.async.write_file(path.concat { cwd, "mason-receipt.json" }, vim.json.encode(self)) +end ---@class InstallReceiptBuilder ---@field private secondary_sources InstallReceiptSource[] @@ -39,6 +66,7 @@ function InstallReceiptBuilder.new() links = { bin = vim.empty_dict(), share = vim.empty_dict(), + opt = vim.empty_dict(), }, }, InstallReceiptBuilder) end @@ -67,7 +95,7 @@ function InstallReceiptBuilder:with_secondary_source(source) return self end ----@param typ '"bin"' | '"share"' +---@param typ '"bin"' | '"share"' | '"opt"' ---@param name string ---@param rel_path string function InstallReceiptBuilder:with_link(typ, name, rel_path) @@ -104,7 +132,7 @@ function InstallReceiptBuilder:build() assert(self.start_time, "start_time is required") assert(self.completion_time, "completion_time is required") assert(self.primary_source, "primary_source is required") - return { + return InstallReceipt.new { name = self.name, schema_version = self.schema_version, metrics = { @@ -162,24 +190,6 @@ function InstallReceiptBuilder.git_remote(remote_url) return { type = "git", remote = remote_url } end ----@class InstallReceipt<T> : { primary_source: T } ----@field public name string ----@field public schema_version InstallReceiptSchemaVersion ----@field public metrics {start_time:integer, completion_time:integer} ----@field public primary_source InstallReceiptSource ----@field public secondary_sources InstallReceiptSource[] ----@field public links InstallReceiptLinks -local InstallReceipt = {} -InstallReceipt.__index = InstallReceipt - -function InstallReceipt.new(props) - return setmetatable(props, InstallReceipt) -end - -function InstallReceipt.from_json(json) - return InstallReceipt.new(json) -end - M.InstallReceiptBuilder = InstallReceiptBuilder M.InstallReceipt = InstallReceipt diff --git a/lua/mason-core/result.lua b/lua/mason-core/result.lua index f3b76339..1491c31f 100644 --- a/lua/mason-core/result.lua +++ b/lua/mason-core/result.lua @@ -176,9 +176,9 @@ function Result.run_catching(fn) end end ----@generic V ----@param fn fun(try: fun(result: Result): any): V ----@return Result # Result<V> +---@generic T +---@param fn fun(try: fun(result: Result)): T? +---@return Result # Result<T> function Result.try(fn) local thread = coroutine.create(fn) local step |
