aboutsummaryrefslogtreecommitdiffstats
path: root/lua/mason-core/installer
diff options
context:
space:
mode:
authorWilliam Boman <william@redwill.se>2023-11-07 00:29:18 +0100
committerWilliam Boman <william@redwill.se>2025-02-19 12:15:48 +0100
commit6a7662760c515c74f2c37fc825776ead65d307f9 (patch)
tree0f4496d0678c7029b10236cbf48cc0f5ff63c1dc /lua/mason-core/installer
parentfix(pypi): remove -U flag and fix log message (diff)
downloadmason-6a7662760c515c74f2c37fc825776ead65d307f9.tar
mason-6a7662760c515c74f2c37fc825776ead65d307f9.tar.gz
mason-6a7662760c515c74f2c37fc825776ead65d307f9.tar.bz2
mason-6a7662760c515c74f2c37fc825776ead65d307f9.tar.lz
mason-6a7662760c515c74f2c37fc825776ead65d307f9.tar.xz
mason-6a7662760c515c74f2c37fc825776ead65d307f9.tar.zst
mason-6a7662760c515c74f2c37fc825776ead65d307f9.zip
refactor!: change Package API
This changes the following public APIs: **(_breaking_) Events on the `Package` class** The `uninstall:success` event on the `Package` class now receives an `InstallReceipt` as argument, instead of an `InstallHandle`. This receipt is an in-memory representation of what was uninstalled. There's also a new `uninstall:failed` event for situations where uninstallation for some reason fails. Note: this also applies to the registry events (i.e. `package:uninstall:success` and `package:uninstall:failed`). --- **(_breaking_) `Package:uninstall()` is now asynchronous and receives two new arguments, similarly to `Package:install()`** While package uninstallations remain synchronous under the hood, the public API has been changed from synchronous -> asynchronous. Users of this method are recommended to provide a callback in situations where code needs to execute after uninstallation fully completes. --- **(_breaking_) `Package:get_install_path()` has been removed. --- **`Package:install()` now takes an optional callback** This callback allows consumers to be informed whether installation was successful or not without having to go through a different, low-level, API. See below for a comparison between the old and new APIs: ```lua -- before local handle = pkg:install() handle:once("closed", function () -- ... end) -- after pkg:install({}, function (success, result) -- ... end) ```
Diffstat (limited to 'lua/mason-core/installer')
-rw-r--r--lua/mason-core/installer/InstallHandle.lua (renamed from lua/mason-core/installer/handle.lua)10
-rw-r--r--lua/mason-core/installer/InstallLocation.lua (renamed from lua/mason-core/installer/location.lua)11
-rw-r--r--lua/mason-core/installer/InstallRunner.lua (renamed from lua/mason-core/installer/runner.lua)123
-rw-r--r--lua/mason-core/installer/UninstallRunner.lua119
-rw-r--r--lua/mason-core/installer/compiler/compilers/github/init.lua2
-rw-r--r--lua/mason-core/installer/compiler/init.lua23
-rw-r--r--lua/mason-core/installer/compiler/link.lua6
-rw-r--r--lua/mason-core/installer/context/InstallContextCwd.lua (renamed from lua/mason-core/installer/context/cwd.lua)12
-rw-r--r--lua/mason-core/installer/context/InstallContextFs.lua (renamed from lua/mason-core/installer/context/fs.lua)0
-rw-r--r--lua/mason-core/installer/context/InstallContextSpawn.lua (renamed from lua/mason-core/installer/context/spawn.lua)0
-rw-r--r--lua/mason-core/installer/context/init.lua54
-rw-r--r--lua/mason-core/installer/linker.lua38
-rw-r--r--lua/mason-core/installer/managers/gem.lua6
-rw-r--r--lua/mason-core/installer/managers/npm.lua8
-rw-r--r--lua/mason-core/installer/managers/pypi.lua13
15 files changed, 295 insertions, 130 deletions
diff --git a/lua/mason-core/installer/handle.lua b/lua/mason-core/installer/InstallHandle.lua
index 62da5bae..f5a42c53 100644
--- a/lua/mason-core/installer/handle.lua
+++ b/lua/mason-core/installer/InstallHandle.lua
@@ -43,10 +43,11 @@ function InstallHandleSpawnHandle:__tostring()
end
---@class InstallHandle : EventEmitter
----@field package Package
+---@field package AbstractPackage
---@field state InstallHandleState
---@field stdio { buffers: { stdout: string[], stderr: string[] }, sink: StdioSink }
---@field is_terminated boolean
+---@field location InstallLocation
---@field private spawn_handles InstallHandleSpawnHandle[]
local InstallHandle = {}
InstallHandle.__index = InstallHandle
@@ -70,14 +71,17 @@ local function new_sink(handle)
}
end
----@param pkg Package
-function InstallHandle:new(pkg)
+---@param pkg AbstractPackage
+---@param location InstallLocation
+function InstallHandle:new(pkg, location)
+ ---@type InstallHandle
local instance = EventEmitter.new(self)
instance.state = "IDLE"
instance.package = pkg
instance.spawn_handles = {}
instance.stdio = new_sink(instance)
instance.is_terminated = false
+ instance.location = location
return instance
end
diff --git a/lua/mason-core/installer/location.lua b/lua/mason-core/installer/InstallLocation.lua
index 9cdf097f..00b517b9 100644
--- a/lua/mason-core/installer/location.lua
+++ b/lua/mason-core/installer/InstallLocation.lua
@@ -59,9 +59,9 @@ function InstallLocation:opt(path)
return Path.concat { self.dir, "opt", path }
end
----@param path string?
-function InstallLocation:package(path)
- return Path.concat { self.dir, "packages", path }
+---@param pkg string?
+function InstallLocation:package(pkg)
+ return Path.concat { self.dir, "packages", pkg }
end
---@param path string?
@@ -79,6 +79,11 @@ function InstallLocation:registry(path)
return Path.concat { self.dir, "registries", path }
end
+---@param pkg string
+function InstallLocation:receipt(pkg)
+ return Path.concat { self:package(pkg), "mason-receipt.json" }
+end
+
---@param opts { PATH: '"append"' | '"prepend"' | '"skip"' }
function InstallLocation:set_env(opts)
vim.env.MASON = self.dir
diff --git a/lua/mason-core/installer/runner.lua b/lua/mason-core/installer/InstallRunner.lua
index 64aa605d..fa2b3fcf 100644
--- a/lua/mason-core/installer/runner.lua
+++ b/lua/mason-core/installer/InstallRunner.lua
@@ -2,41 +2,45 @@ local Result = require "mason-core.result"
local _ = require "mason-core.functional"
local a = require "mason-core.async"
local compiler = require "mason-core.installer.compiler"
+local control = require "mason-core.async.control"
local fs = require "mason-core.fs"
local linker = require "mason-core.installer.linker"
local log = require "mason-core.log"
local registry = require "mason-registry"
+local OneShotChannel = control.OneShotChannel
+
local InstallContext = require "mason-core.installer.context"
---@class InstallRunner
----@field location InstallLocation
---@field handle InstallHandle
----@field semaphore Semaphore
----@field permit Permit?
+---@field global_semaphore Semaphore
+---@field global_permit Permit?
+---@field package_permit Permit?
local InstallRunner = {}
InstallRunner.__index = InstallRunner
----@param location InstallLocation
---@param handle InstallHandle
---@param semaphore Semaphore
-function InstallRunner:new(location, handle, semaphore)
+function InstallRunner:new(handle, semaphore)
---@type InstallRunner
local instance = {}
setmetatable(instance, self)
instance.location = location
- instance.semaphore = semaphore
+ instance.global_semaphore = semaphore
instance.handle = handle
return instance
end
+---@alias InstallRunnerCallback fun(success: true, receipt: InstallReceipt) | fun(success: false, handle: InstallHandle, error: any)
+
---@param opts PackageInstallOpts
----@param callback? fun(success: boolean, result: any)
+---@param callback? InstallRunnerCallback
function InstallRunner:execute(opts, callback)
local handle = self.handle
log.fmt_info("Executing installer for %s %s", handle.package, opts)
- local context = InstallContext:new(handle, self.location, opts)
+ local context = InstallContext:new(handle, opts)
local tailed_output = {}
@@ -79,25 +83,27 @@ function InstallRunner:execute(opts, callback)
self:release_lock()
self:release_permit()
- if callback then
- callback(success, result)
- end
-
if success then
log.fmt_info("Installation succeeded for %s", handle.package)
- handle.package:emit("install:success", handle)
- registry:emit("package:install:success", handle.package, handle)
+ if callback then
+ callback(true, result.receipt)
+ end
+ handle.package:emit("install:success", result.receipt)
+ registry:emit("package:install:success", handle.package, result.receipt)
else
log.fmt_error("Installation failed for %s error=%s", handle.package, result)
- handle.package:emit("install:failed", handle, result)
- registry:emit("package:install:failed", handle.package, handle, result)
+ if callback then
+ callback(false, result)
+ end
+ handle.package:emit("install:failed", result)
+ registry:emit("package:install:failed", handle.package, result)
end
end)
local cancel_execution = a.run(function()
return Result.try(function(try)
- try(self:acquire_permit())
- try(self.location:initialize())
+ try(self.handle.location:initialize())
+ try(self:acquire_permit()):receive()
try(self:acquire_lock(opts.force))
context.receipt:with_start_time(vim.loop.gettimeofday())
@@ -107,7 +113,7 @@ function InstallRunner:execute(opts, callback)
-- 2. run installer
---@type async fun(ctx: InstallContext): Result
- local installer = try(compiler.compile(handle.package.spec, opts))
+ local installer = try(compiler.compile_installer(handle.package.spec, opts))
try(context:execute(installer))
-- 3. promote temporary installation dir
@@ -116,28 +122,23 @@ function InstallRunner:execute(opts, callback)
end))
-- 4. link package & write receipt
- return linker
- .link(context)
- :and_then(function()
- return context:build_receipt(context)
- end)
- :and_then(
+ try(linker.link(context):on_failure(function()
+ -- unlink any links that were made before failure
+ context:build_receipt():on_success(
---@param receipt InstallReceipt
function(receipt)
- return receipt:write(context.cwd:get())
+ linker.unlink(handle.package, receipt, self.handle.location):on_failure(function(err)
+ log.error("Failed to unlink failed installation.", err)
+ end)
end
)
- :on_failure(function()
- -- unlink any links that were made before failure
- context:build_receipt():on_success(
- ---@param receipt InstallReceipt
- function(receipt)
- linker.unlink(handle.package, receipt, self.location):on_failure(function(err)
- log.error("Failed to unlink failed installation.", err)
- end)
- end
- )
- end)
+ end))
+ ---@type InstallReceipt
+ local receipt = try(context:build_receipt())
+ try(Result.pcall(fs.sync.write_file, handle.location:receipt(handle.package.name), receipt:to_json()))
+ return {
+ receipt = receipt,
+ }
end):get_or_throw()
end, finalize)
@@ -157,7 +158,7 @@ end
---@async
---@private
function InstallRunner:release_lock()
- pcall(fs.async.unlink, self.location:lockfile(self.handle.package.name))
+ pcall(fs.async.unlink, self.handle.location:lockfile(self.handle.package.name))
end
---@async
@@ -166,7 +167,7 @@ end
function InstallRunner:acquire_lock(force)
local pkg = self.handle.package
log.debug("Attempting to lock package", pkg)
- local lockfile = self.location:lockfile(pkg.name)
+ local lockfile = self.handle.location:lockfile(pkg.name)
if force ~= true and fs.async.file_exists(lockfile) then
log.error("Lockfile already exists.", pkg)
return Result.failure(
@@ -181,33 +182,45 @@ function InstallRunner:acquire_lock(force)
return Result.success(lockfile)
end
----@async
---@private
function InstallRunner:acquire_permit()
+ local channel = OneShotChannel:new()
+ log.fmt_debug("Acquiring permit for %s", self.handle.package)
local handle = self.handle
- if handle:is_active() or handle:is_closed() then
- log.fmt_debug("Received active or closed handle %s", handle)
+ if handle:is_active() or handle:is_closing() then
+ log.fmt_debug("Received active or closing handle %s", handle)
return Result.failure "Invalid handle state."
end
handle:queued()
- local permit = self.semaphore:acquire()
- if handle:is_closed() then
- permit:forget()
- log.fmt_trace("Installation was aborted %s", handle)
- return Result.failure "Installation was aborted."
- end
- log.fmt_trace("Activating handle %s", handle)
- handle:active()
- self.permit = permit
- return Result.success()
+ a.run(function()
+ self.global_permit = self.global_semaphore:acquire()
+ self.package_permit = handle.package:acquire_permit()
+ end, function(success, err)
+ if not success or handle:is_closing() then
+ if not success then
+ log.error("Acquiring permits failed", err)
+ end
+ self:release_permit()
+ else
+ log.fmt_debug("Activating handle %s", handle)
+ handle:active()
+ channel:send()
+ end
+ end)
+
+ return Result.success(channel)
end
---@private
function InstallRunner:release_permit()
- if self.permit then
- self.permit:forget()
- self.permit = nil
+ if self.global_permit then
+ self.global_permit:forget()
+ self.global_permit = nil
+ end
+ if self.package_permit then
+ self.package_permit:forget()
+ self.package_permit = nil
end
end
diff --git a/lua/mason-core/installer/UninstallRunner.lua b/lua/mason-core/installer/UninstallRunner.lua
new file mode 100644
index 00000000..661bfefa
--- /dev/null
+++ b/lua/mason-core/installer/UninstallRunner.lua
@@ -0,0 +1,119 @@
+local InstallContext = require "mason-core.installer.context"
+local Result = require "mason-core.result"
+local _ = require "mason-core.functional"
+local a = require "mason-core.async"
+local compiler = require "mason-core.installer.compiler"
+local control = require "mason-core.async.control"
+local fs = require "mason-core.fs"
+local log = require "mason-core.log"
+local registry = require "mason-registry"
+
+local OneShotChannel = control.OneShotChannel
+
+---@class UninstallRunner
+---@field handle InstallHandle
+---@field global_semaphore Semaphore
+---@field package_permit Permit?
+---@field global_permit Permit?
+local UninstallRunner = {}
+UninstallRunner.__index = UninstallRunner
+
+---@param handle InstallHandle
+---@param global_semaphore Semaphore
+---@return UninstallRunner
+function UninstallRunner:new(handle, global_semaphore)
+ local instance = {}
+ setmetatable(instance, self)
+ instance.handle = handle
+ instance.global_semaphore = global_semaphore
+ return instance
+end
+
+---@param opts PackageUninstallOpts
+---@param callback? InstallRunnerCallback
+function UninstallRunner:execute(opts, callback)
+ local pkg = self.handle.package
+ local location = self.handle.location
+ log.fmt_info("Executing uninstaller for %s %s", pkg, opts)
+ a.run(function()
+ Result.try(function(try)
+ if not opts.bypass_permit then
+ try(self:acquire_permit()):receive()
+ end
+ ---@type InstallReceipt?
+ local receipt = pkg:get_receipt(location):or_else(nil)
+ if receipt == nil then
+ log.fmt_warn("Receipt not found when uninstalling %s", pkg)
+ end
+ try(pkg:unlink(location))
+ fs.sync.rmrf(location:package(pkg.name))
+ return receipt
+ end):get_or_throw()
+ end, function(success, result)
+ if not self.handle:is_closing() then
+ self.handle:close()
+ end
+ self:release_permit()
+
+ if success then
+ local receipt = result
+ log.fmt_info("Uninstallation succeeded for %s", pkg)
+ if callback then
+ callback(true, receipt)
+ end
+ pkg:emit("uninstall:success", receipt)
+ registry:emit("package:uninstall:success", pkg, receipt)
+ else
+ log.fmt_error("Uninstallation failed for %s error=%s", pkg, result)
+ if callback then
+ callback(false, result)
+ end
+ pkg:emit("uninstall:failed", result)
+ registry:emit("package:uninstall:failed", pkg, result)
+ end
+ end)
+end
+
+---@private
+function UninstallRunner:acquire_permit()
+ local channel = OneShotChannel:new()
+ log.fmt_debug("Acquiring permit for %s", self.handle.package)
+ local handle = self.handle
+ if handle:is_active() or handle:is_closing() then
+ log.fmt_debug("Received active or closing handle %s", handle)
+ return Result.failure "Invalid handle state."
+ end
+
+ handle:queued()
+ a.run(function()
+ self.global_permit = self.global_semaphore:acquire()
+ self.package_permit = handle.package:acquire_permit()
+ end, function(success, err)
+ if not success or handle:is_closing() then
+ if not success then
+ log.error("Acquiring permits failed", err)
+ end
+ self:release_permit()
+ else
+ log.fmt_debug("Activating handle %s", handle)
+ handle:active()
+ channel:send()
+ end
+ end)
+
+ return Result.success(channel)
+end
+
+---@private
+function UninstallRunner:release_permit()
+ if self.global_permit then
+ self.global_permit:forget()
+ self.global_permit = nil
+ end
+ if self.package_permit then
+ self.package_permit:forget()
+ self.package_permit = nil
+ end
+end
+
+return UninstallRunner
diff --git a/lua/mason-core/installer/compiler/compilers/github/init.lua b/lua/mason-core/installer/compiler/compilers/github/init.lua
index d8646975..5a8dfce5 100644
--- a/lua/mason-core/installer/compiler/compilers/github/init.lua
+++ b/lua/mason-core/installer/compiler/compilers/github/init.lua
@@ -20,7 +20,7 @@ end
---@async
---@param ctx InstallContext
---@param source ParsedGitHubReleaseSource | ParsedGitHubBuildSource
-function M.install(ctx, source, purl)
+function M.install(ctx, source)
if source.asset then
source = source--[[@as ParsedGitHubReleaseSource]]
return require("mason-core.installer.compiler.compilers.github.release").install(ctx, source)
diff --git a/lua/mason-core/installer/compiler/init.lua b/lua/mason-core/installer/compiler/init.lua
index e1df6784..4eed986b 100644
--- a/lua/mason-core/installer/compiler/init.lua
+++ b/lua/mason-core/installer/compiler/init.lua
@@ -71,9 +71,9 @@ local function upsert(dst, src)
end
---@param source RegistryPackageSource
----@param version string
+---@param version string?
local function coalesce_source(source, version)
- if source.version_overrides then
+ if version and source.version_overrides then
for i = #source.version_overrides, 1, -1 do
local version_override = source.version_overrides[i]
local version_type, constraint = unpack(_.split(":", version_override.constraint))
@@ -94,18 +94,12 @@ local function coalesce_source(source, version)
end):get_or_else(false)
if version_match then
- if version_override.id then
- -- Because this entry provides its own purl id, it overrides the entire source definition.
- return version_override
- else
- -- Upsert the default source with the contents of the version override.
- return upsert(vim.deepcopy(source), _.dissoc("constraint", version_override))
- end
+ return _.dissoc("constraint", version_override)
end
end
end
end
- return source
+ return _.dissoc("version_overrides", source)
end
---@param spec RegistryPackageSpec
@@ -121,7 +115,7 @@ function M.parse(spec, opts)
)
end
- local source = opts.version and coalesce_source(spec.source, opts.version) or spec.source
+ local source = coalesce_source(spec.source, opts.version)
---@type Purl
local purl = try(Purl.parse(source.id))
@@ -149,7 +143,7 @@ end
---@async
---@param spec RegistryPackageSpec
---@param opts PackageInstallOpts
-function M.compile(spec, opts)
+function M.compile_installer(spec, opts)
log.debug("Compiling installer.", spec.name, opts)
return Result.try(function(try)
-- Parsers run synchronously and may access API functions, so we schedule before-hand.
@@ -210,9 +204,10 @@ function M.compile(spec, opts)
ctx.receipt:with_source {
type = ctx.package.spec.schema,
id = Purl.compile(parsed.purl),
+ -- Exclude the "install" field from "mason" sources because this is a Lua function.
+ raw = parsed.purl.type == "mason" and _.dissoc("install", parsed.raw_source) or parsed.raw_source,
}
- end):on_failure(function(err)
- error(err, 0)
+ ctx.receipt:with_install_options(opts)
end)
end
end)
diff --git a/lua/mason-core/installer/compiler/link.lua b/lua/mason-core/installer/compiler/link.lua
index 9719eaa9..d60fce47 100644
--- a/lua/mason-core/installer/compiler/link.lua
+++ b/lua/mason-core/installer/compiler/link.lua
@@ -38,7 +38,7 @@ local bin_delegates = {
local python = platform.is.win and "python" or "python3"
return ctx:write_shell_exec_wrapper(
bin,
- ("%s %q"):format(python, path.concat { ctx.package:get_install_path(), target })
+ ("%s %q"):format(python, path.concat { ctx:get_install_path(), target })
)
end)
end,
@@ -66,7 +66,7 @@ local bin_delegates = {
return ctx:write_shell_exec_wrapper(
bin,
("dotnet %q"):format(path.concat {
- ctx.package:get_install_path(),
+ ctx:get_install_path(),
target,
})
)
@@ -103,7 +103,7 @@ local bin_delegates = {
return ctx:write_shell_exec_wrapper(
bin,
("java -jar %q"):format(path.concat {
- ctx.package:get_install_path(),
+ ctx:get_install_path(),
target,
})
)
diff --git a/lua/mason-core/installer/context/cwd.lua b/lua/mason-core/installer/context/InstallContextCwd.lua
index 2b74bf55..b365cbd9 100644
--- a/lua/mason-core/installer/context/cwd.lua
+++ b/lua/mason-core/installer/context/InstallContextCwd.lua
@@ -3,20 +3,16 @@ local fs = require "mason-core.fs"
local path = require "mason-core.path"
---@class InstallContextCwd
----@field private location InstallLocation Defines the upper boundary for which paths are allowed as cwd.
---@field private handle InstallHandle
---@field private cwd string?
local InstallContextCwd = {}
InstallContextCwd.__index = InstallContextCwd
---@param handle InstallHandle
----@param location InstallLocation
-function InstallContextCwd:new(handle, location)
- assert(location, "location not provided")
+function InstallContextCwd:new(handle)
---@type InstallContextCwd
local instance = {}
setmetatable(instance, self)
- instance.location = location
instance.handle = handle
instance.cwd = nil
return instance
@@ -24,7 +20,7 @@ end
function InstallContextCwd:initialize()
return Result.try(function(try)
- local staging_dir = self.location:staging(self.handle.package.name)
+ local staging_dir = self.handle.location:staging(self.handle.package.name)
if fs.sync.dir_exists(staging_dir) then
try(Result.pcall(fs.sync.rmrf, staging_dir))
end
@@ -42,8 +38,8 @@ end
function InstallContextCwd:set(new_abs_cwd)
assert(type(new_abs_cwd) == "string", "new_cwd is not a string")
assert(
- path.is_subdirectory(self.location:get_dir(), new_abs_cwd),
- ("%q is not a subdirectory of %q"):format(new_abs_cwd, self.location)
+ path.is_subdirectory(self.handle.location:get_dir(), new_abs_cwd),
+ ("%q is not a subdirectory of %q"):format(new_abs_cwd, self.handle.location)
)
self.cwd = new_abs_cwd
return self
diff --git a/lua/mason-core/installer/context/fs.lua b/lua/mason-core/installer/context/InstallContextFs.lua
index 93379017..93379017 100644
--- a/lua/mason-core/installer/context/fs.lua
+++ b/lua/mason-core/installer/context/InstallContextFs.lua
diff --git a/lua/mason-core/installer/context/spawn.lua b/lua/mason-core/installer/context/InstallContextSpawn.lua
index f2ce8df2..f2ce8df2 100644
--- a/lua/mason-core/installer/context/spawn.lua
+++ b/lua/mason-core/installer/context/InstallContextSpawn.lua
diff --git a/lua/mason-core/installer/context/init.lua b/lua/mason-core/installer/context/init.lua
index 425bf39c..097ea696 100644
--- a/lua/mason-core/installer/context/init.lua
+++ b/lua/mason-core/installer/context/init.lua
@@ -1,8 +1,9 @@
-local InstallContextCwd = require "mason-core.installer.context.cwd"
-local InstallContextFs = require "mason-core.installer.context.fs"
-local InstallContextSpawn = require "mason-core.installer.context.spawn"
+local InstallContextCwd = require "mason-core.installer.context.InstallContextCwd"
+local InstallContextFs = require "mason-core.installer.context.InstallContextFs"
+local InstallContextSpawn = require "mason-core.installer.context.InstallContextSpawn"
local Result = require "mason-core.result"
local _ = require "mason-core.functional"
+local a = require "mason-core.async"
local fs = require "mason-core.fs"
local log = require "mason-core.log"
local path = require "mason-core.path"
@@ -15,7 +16,7 @@ local receipt = require "mason-core.receipt"
---@field location InstallLocation
---@field spawn InstallContextSpawn
---@field handle InstallHandle
----@field package Package
+---@field package AbstractPackage
---@field cwd InstallContextCwd
---@field opts PackageInstallOpts
---@field stdio_sink StdioSink
@@ -24,17 +25,16 @@ local InstallContext = {}
InstallContext.__index = InstallContext
---@param handle InstallHandle
----@param location InstallLocation
---@param opts PackageInstallOpts
-function InstallContext:new(handle, location, opts)
- local cwd = InstallContextCwd:new(handle, location)
+function InstallContext:new(handle, opts)
+ local cwd = InstallContextCwd:new(handle)
local spawn = InstallContextSpawn:new(handle, cwd, false)
local fs = InstallContextFs:new(cwd)
return setmetatable({
cwd = cwd,
spawn = spawn,
handle = handle,
- location = location,
+ location = handle.location, -- for convenience
package = handle.package, -- for convenience
fs = fs,
receipt = receipt.InstallReceiptBuilder:new(),
@@ -51,23 +51,35 @@ end
---@async
function InstallContext:promote_cwd()
local cwd = self.cwd:get()
- local install_path = self.package:get_install_path()
+ local install_path = self:get_install_path()
if install_path == cwd then
- log.fmt_debug("cwd %s is already promoted (at %s)", cwd, install_path)
+ log.fmt_debug("cwd %s is already promoted", cwd)
return
end
log.fmt_debug("Promoting cwd %s to %s", cwd, install_path)
+
-- 1. Uninstall any existing installation
- self.handle.package:uninstall()
+ if self.handle.package:is_installed() then
+ a.wait(function(resolve, reject)
+ self.handle.package:uninstall({ bypass_permit = true }, function(success, result)
+ if not success then
+ reject(result)
+ else
+ resolve()
+ end
+ end)
+ end)
+ end
+
-- 2. Prepare for renaming cwd to destination
if platform.is.unix then
-- Some Unix systems will raise an error when renaming a directory to a destination that does not already exist.
fs.async.mkdir(install_path)
end
- -- 3. Move the cwd to the final installation directory
- fs.async.rename(cwd, install_path)
- -- 4. Update cwd
+ -- 3. Update cwd
self.cwd:set(install_path)
+ -- 4. Move the cwd to the final installation directory
+ fs.async.rename(cwd, install_path)
end
---@param rel_path string The relative path from the current working directory to change cwd to. Will only restore to the initial cwd after execution of fn (if provided).
@@ -94,7 +106,7 @@ function InstallContext:write_node_exec_wrapper(new_executable_rel_path, script_
return self:write_shell_exec_wrapper(
new_executable_rel_path,
("node %q"):format(path.concat {
- self.package:get_install_path(),
+ self:get_install_path(),
script_rel_path,
})
)
@@ -109,7 +121,7 @@ function InstallContext:write_ruby_exec_wrapper(new_executable_rel_path, script_
return self:write_shell_exec_wrapper(
new_executable_rel_path,
("ruby %q"):format(path.concat {
- self.package:get_install_path(),
+ self:get_install_path(),
script_rel_path,
})
)
@@ -124,7 +136,7 @@ function InstallContext:write_php_exec_wrapper(new_executable_rel_path, script_r
return self:write_shell_exec_wrapper(
new_executable_rel_path,
("php %q"):format(path.concat {
- self.package:get_install_path(),
+ self:get_install_path(),
script_rel_path,
})
)
@@ -149,7 +161,7 @@ function InstallContext:write_pyvenv_exec_wrapper(new_executable_rel_path, modul
new_executable_rel_path,
("%q -m %s"):format(
path.concat {
- pypi.venv_path(self.package:get_install_path()),
+ pypi.venv_path(self:get_install_path()),
"python",
},
module
@@ -169,7 +181,7 @@ function InstallContext:write_exec_wrapper(new_executable_rel_path, target_execu
return self:write_shell_exec_wrapper(
new_executable_rel_path,
("%q"):format(path.concat {
- self.package:get_install_path(),
+ self:get_install_path(),
target_executable_rel_path,
})
)
@@ -264,4 +276,8 @@ function InstallContext:build_receipt()
end)
end
+function InstallContext:get_install_path()
+ return self.location:package(self.package.name)
+end
+
return InstallContext
diff --git a/lua/mason-core/installer/linker.lua b/lua/mason-core/installer/linker.lua
index a5c54273..a26d2592 100644
--- a/lua/mason-core/installer/linker.lua
+++ b/lua/mason-core/installer/linker.lua
@@ -57,7 +57,7 @@ local function unlink(receipt, link_context, location)
end)
end
----@param pkg Package
+---@param pkg AbstractPackage
---@param receipt InstallReceipt
---@param location InstallLocation
---@nodiscard
@@ -82,31 +82,27 @@ local function link(context, link_context, link_fn)
name = ("%s.cmd"):format(name)
end
local new_abs_path = link_context.prefix(name, context.location)
- local target_abs_path = path.concat { context.package:get_install_path(), rel_path }
+ local target_abs_path = path.concat { context:get_install_path(), rel_path }
local target_rel_path = path.relative(new_abs_path, target_abs_path)
- do
- -- 1. Ensure destination directory exists
- a.scheduler()
- local dir = vim.fn.fnamemodify(new_abs_path, ":h")
- if not fs.async.dir_exists(dir) then
- try(Result.pcall(fs.async.mkdirp, dir))
- end
+ -- 1. Ensure destination directory exists
+ a.scheduler()
+ local dir = vim.fn.fnamemodify(new_abs_path, ":h")
+ if not fs.async.dir_exists(dir) then
+ try(Result.pcall(fs.async.mkdirp, dir))
end
- 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(new_abs_path) then
- try(Result.pcall(fs.async.unlink, new_abs_path))
- end
- elseif fs.async.file_exists(new_abs_path) then
- return Result.failure(("%q is already linked."):format(new_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))
+ -- 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(new_abs_path) then
+ try(Result.pcall(fs.async.unlink, new_abs_path))
end
+ elseif fs.async.file_exists(new_abs_path) then
+ return Result.failure(("%q is already linked."):format(new_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
-- 3. Execute link.
diff --git a/lua/mason-core/installer/managers/gem.lua b/lua/mason-core/installer/managers/gem.lua
index cb502de5..e8723d7e 100644
--- a/lua/mason-core/installer/managers/gem.lua
+++ b/lua/mason-core/installer/managers/gem.lua
@@ -54,14 +54,14 @@ function M.create_bin_wrapper(bin)
ctx.write_shell_exec_wrapper,
ctx,
bin,
- path.concat { ctx.package:get_install_path(), bin_path },
+ path.concat { ctx:get_install_path(), bin_path },
{
GEM_PATH = platform.when {
unix = function()
- return ("%s:$GEM_PATH"):format(ctx.package:get_install_path())
+ return ("%s:$GEM_PATH"):format(ctx:get_install_path())
end,
win = function()
- return ("%s;%%GEM_PATH%%"):format(ctx.package:get_install_path())
+ return ("%s;%%GEM_PATH%%"):format(ctx:get_install_path())
end,
},
}
diff --git a/lua/mason-core/installer/managers/npm.lua b/lua/mason-core/installer/managers/npm.lua
index 10a3e9fd..df8ece35 100644
--- a/lua/mason-core/installer/managers/npm.lua
+++ b/lua/mason-core/installer/managers/npm.lua
@@ -70,6 +70,14 @@ function M.install(pkg, version, opts)
}
end
+---@async
+---@param pkg string
+function M.uninstall(pkg)
+ local ctx = installer.context()
+ ctx.stdio_sink.stdout(("Uninstalling npm package %s…\n"):format(pkg))
+ return ctx.spawn.npm { "uninstall", pkg }
+end
+
---@param exec string
function M.bin_path(exec)
return Result.pcall(platform.when, {
diff --git a/lua/mason-core/installer/managers/pypi.lua b/lua/mason-core/installer/managers/pypi.lua
index c569e0fd..85fadc9f 100644
--- a/lua/mason-core/installer/managers/pypi.lua
+++ b/lua/mason-core/installer/managers/pypi.lua
@@ -214,6 +214,19 @@ function M.install(pkg, version, opts)
}, opts.install_extra_args)
end
+---@async
+---@param pkg string
+function M.uninstall(pkg)
+ log.fmt_debug("pypi: uninstall %s", pkg)
+ return venv_python {
+ "-m",
+ "pip",
+ "uninstall",
+ "-y",
+ pkg,
+ }
+end
+
---@param executable string
function M.bin_path(executable)
local ctx = installer.context()