diff options
| author | William Boman <william@redwill.se> | 2023-11-07 00:29:18 +0100 |
|---|---|---|
| committer | William Boman <william@redwill.se> | 2025-02-19 12:15:48 +0100 |
| commit | 6a7662760c515c74f2c37fc825776ead65d307f9 (patch) | |
| tree | 0f4496d0678c7029b10236cbf48cc0f5ff63c1dc /lua/mason-core/installer | |
| parent | fix(pypi): remove -U flag and fix log message (diff) | |
| download | mason-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')
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() |
