aboutsummaryrefslogtreecommitdiffstats
path: root/lua
diff options
context:
space:
mode:
authorWilliam Boman <william@redwill.se>2023-02-26 07:57:42 +0100
committerGitHub <noreply@github.com>2023-02-26 07:57:42 +0100
commit2e83e412d877a7e6daf04b2b6359521f6fb8c20e (patch)
tree5c9488569a0e8b9f2aa3d492c666aed4fb55b194 /lua
parentchore: autogenerate (#1028) (diff)
downloadmason-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.lua10
-rw-r--r--lua/mason-core/installer/init.lua26
-rw-r--r--lua/mason-core/installer/linker.lua189
-rw-r--r--lua/mason-core/package/init.lua2
-rw-r--r--lua/mason-core/path.lua11
-rw-r--r--lua/mason-core/receipt.lua50
-rw-r--r--lua/mason-core/result.lua6
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