aboutsummaryrefslogtreecommitdiffstats
path: root/lua/mason-core
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
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')
-rw-r--r--lua/mason-core/functional/init.lua4
-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
-rw-r--r--lua/mason-core/package.lua274
-rw-r--r--lua/mason-core/package/AbstractPackage.lua203
-rw-r--r--lua/mason-core/package/init.lua182
-rw-r--r--lua/mason-core/receipt.lua45
20 files changed, 712 insertions, 421 deletions
diff --git a/lua/mason-core/functional/init.lua b/lua/mason-core/functional/init.lua
index d377d2db..7aa58940 100644
--- a/lua/mason-core/functional/init.lua
+++ b/lua/mason-core/functional/init.lua
@@ -3,9 +3,7 @@ local _ = {}
local function lazy_require(module)
return setmetatable({}, {
__index = function(m, k)
- return function(...)
- return require(module)[k](...)
- end
+ return require(module)[k]
end,
})
end
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()
diff --git a/lua/mason-core/package.lua b/lua/mason-core/package.lua
deleted file mode 100644
index a8a7ac79..00000000
--- a/lua/mason-core/package.lua
+++ /dev/null
@@ -1,274 +0,0 @@
-local EventEmitter = require "mason-core.EventEmitter"
-local InstallLocation = require "mason-core.installer.location"
-local InstallRunner = require "mason-core.installer.runner"
-local Optional = require "mason-core.optional"
-local Purl = require "mason-core.purl"
-local Result = require "mason-core.result"
-local _ = require "mason-core.functional"
-local fs = require "mason-core.fs"
-local log = require "mason-core.log"
-local path = require "mason-core.path"
-local platform = require "mason-core.platform"
-local registry = require "mason-registry"
-local settings = require "mason.settings"
-local Semaphore = require("mason-core.async.control").Semaphore
-
----@class Package : EventEmitter
----@field name string
----@field spec RegistryPackageSpec
----@field private handle InstallHandle The currently associated handle.
-local Package = {}
-Package.__index = Package
-setmetatable(Package, { __index = EventEmitter })
-
----@param package_identifier string
----@return string, string?
-Package.Parse = function(package_identifier)
- local name, version = unpack(vim.split(package_identifier, "@"))
- return name, version
-end
-
----@alias PackageLanguage string
-
----@type table<PackageLanguage, PackageLanguage>
-Package.Lang = setmetatable({}, {
- __index = function(s, lang)
- s[lang] = lang
- return s[lang]
- end,
-})
-
----@enum PackageCategory
-Package.Cat = {
- Compiler = "Compiler",
- Runtime = "Runtime",
- DAP = "DAP",
- LSP = "LSP",
- Linter = "Linter",
- Formatter = "Formatter",
-}
-
----@alias PackageLicense string
-
----@type table<PackageLicense, PackageLicense>
-Package.License = setmetatable({}, {
- __index = function(s, license)
- s[license] = license
- return s[license]
- end,
-})
-
----@class RegistryPackageSourceVersionOverride : RegistryPackageSource
----@field constraint string
-
----@class RegistryPackageSource
----@field id string PURL-compliant identifier.
----@field version_overrides? RegistryPackageSourceVersionOverride[]
-
----@class RegistryPackageSchemas
----@field lsp string?
-
----@class RegistryPackageDeprecation
----@field since string
----@field message string
-
----@alias RegistryPackageSpecSchema
---- | '"registry+v1"'
-
----@class RegistryPackageSpec
----@field schema RegistryPackageSpecSchema
----@field name string
----@field description string
----@field homepage string
----@field licenses string[]
----@field languages string[]
----@field categories string[]
----@field source RegistryPackageSource
----@field deprecation RegistryPackageDeprecation?
----@field schemas RegistryPackageSchemas?
----@field bin table<string, string>?
----@field share table<string, string>?
----@field opt table<string, string>?
-
----@param spec RegistryPackageSpec
-local function validate_spec(spec)
- if platform.cached_features["nvim-0.11"] ~= 1 then
- return
- end
- vim.validate("schema", spec.schema, _.equals "registry+v1", "registry+v1")
- vim.validate("name", spec.name, "string")
- vim.validate("description", spec.description, "string")
- vim.validate("homepage", spec.homepage, "string")
- vim.validate("licenses", spec.licenses, "table")
- vim.validate("categories", spec.categories, "table")
- vim.validate("languages", spec.languages, "table")
- vim.validate("source", spec.source, "table")
- vim.validate("bin", spec.bin, { "table", "nil" })
- vim.validate("share", spec.share, { "table", "nil" })
-end
-
----@param spec RegistryPackageSpec
-function Package:new(spec)
- validate_spec(spec)
- local instance = EventEmitter.new(self) --[[@as Package]]
- instance.name = spec.name -- for convenient access
- instance.spec = spec
- return instance
-end
-
-function Package:new_handle()
- self:get_handle():if_present(function(handle)
- assert(handle:is_closed(), "Cannot create new handle because existing handle is not closed.")
- end)
- log.fmt_trace("Creating new handle for %s", self)
- local InstallationHandle = require "mason-core.installer.handle"
- local handle = InstallationHandle:new(self)
- self.handle = handle
-
- -- Ideally we'd decouple this and leverage Mason's event system, but to allow loading as little as possible during
- -- setup (i.e. not load modules related to Mason's event system) of the mason.nvim plugin we explicitly call into
- -- terminator here.
- require("mason-core.terminator").register(handle)
-
- self:emit("handle", handle)
- registry:emit("package:handle", self, handle)
-
- return handle
-end
-
----@alias PackageInstallOpts { version?: string, debug?: boolean, target?: string, force?: boolean, strict?: boolean }
-
--- TODO this needs to be elsewhere
-local semaphore = Semaphore:new(settings.current.max_concurrent_installers)
-
-function Package:is_installing()
- return self:get_handle()
- :map(
- ---@param handle InstallHandle
- function(handle)
- return not handle:is_closed()
- end
- )
- :or_else(false)
-end
-
----@param opts? PackageInstallOpts
----@param callback? fun(success: boolean, result: any)
----@return InstallHandle
-function Package:install(opts, callback)
- opts = opts or {}
- assert(not self:is_installing(), "Package is already installing.")
- local handle = self:new_handle()
- local runner = InstallRunner:new(InstallLocation.global(), handle, semaphore)
- runner:execute(opts, callback)
- return handle
-end
-
----@return boolean
-function Package:uninstall()
- return self:get_receipt()
- :map(function(receipt)
- self:unlink(receipt)
- self:emit("uninstall:success", receipt)
- registry:emit("package:uninstall:success", self, receipt)
- return true
- end)
- :or_else(false)
-end
-
----@private
----@param receipt InstallReceipt
-function Package:unlink(receipt)
- log.fmt_trace("Unlinking %s", self)
- local install_path = self:get_install_path()
-
- -- 1. Unlink
- local linker = require "mason-core.installer.linker"
- linker.unlink(self, receipt, InstallLocation.global()):get_or_throw()
-
- -- 2. Remove installation artifacts
- fs.sync.rmrf(install_path)
-end
-
-function Package:is_installed()
- return registry.is_installed(self.name)
-end
-
-function Package:get_handle()
- return Optional.of_nilable(self.handle)
-end
-
-function Package:get_install_path()
- return InstallLocation.global():package(self.name)
-end
-
----@return Optional # Optional<InstallReceipt>
-function Package:get_receipt()
- local receipt_path = path.concat { self:get_install_path(), "mason-receipt.json" }
- if fs.sync.file_exists(receipt_path) then
- local receipt = require "mason-core.receipt"
- return Optional.of(receipt.InstallReceipt.from_json(vim.json.decode(fs.sync.read_file(receipt_path))))
- end
- return Optional.empty()
-end
-
----@return string?
-function Package:get_installed_version()
- return self:get_receipt()
- :and_then(
- ---@param receipt InstallReceipt
- function(receipt)
- local source = receipt:get_source()
- if source.id then
- return Purl.parse(source.id):map(_.prop "version"):ok()
- else
- return Optional.empty()
- end
- end
- )
- :or_else(nil)
-end
-
----@return string
-function Package:get_latest_version()
- return Purl.parse(self.spec.source.id)
- :map(_.prop "version")
- :get_or_throw(("Unable to retrieve version from malformed purl: %s."):format(self.spec.source.id))
-end
-
----@param opts? PackageInstallOpts
-function Package:is_installable(opts)
- return require("mason-core.installer.compiler").parse(self.spec, opts or {}):is_success()
-end
-
----@return Result # Result<string[]>
-function Package:get_all_versions()
- local compiler = require "mason-core.installer.compiler"
- return Result.try(function(try)
- ---@type Purl
- local purl = try(Purl.parse(self.spec.source.id))
- ---@type InstallerCompiler
- local compiler = try(compiler.get_compiler(purl))
- return compiler.get_versions(purl, self.spec.source)
- end)
-end
-
-function Package:get_lsp_settings_schema()
- local schema_file = InstallLocation.global()
- :share(path.concat { "mason-schemas", "lsp", ("%s.json"):format(self.name) })
- if fs.sync.file_exists(schema_file) then
- return Result.pcall(vim.json.decode, fs.sync.read_file(schema_file), {
- luanil = { object = true, array = true },
- }):ok()
- end
- return Optional.empty()
-end
-function Package:get_aliases()
- return require("mason-registry").get_package_aliases(self.name)
-end
-
-function Package:__tostring()
- return ("Package(name=%s)"):format(self.name)
-end
-
-return Package
diff --git a/lua/mason-core/package/AbstractPackage.lua b/lua/mason-core/package/AbstractPackage.lua
new file mode 100644
index 00000000..b490fc87
--- /dev/null
+++ b/lua/mason-core/package/AbstractPackage.lua
@@ -0,0 +1,203 @@
+local EventEmitter = require "mason-core.EventEmitter"
+local InstallLocation = require "mason-core.installer.InstallLocation"
+local Optional = require "mason-core.optional"
+local Purl = require "mason-core.purl"
+local Result = require "mason-core.result"
+local _ = require "mason-core.functional"
+local fs = require "mason-core.fs"
+local log = require "mason-core.log"
+local path = require "mason-core.path"
+local settings = require "mason.settings"
+local Semaphore = require("mason-core.async.control").Semaphore
+
+---@alias PackageInstallOpts { version?: string, debug?: boolean, target?: string, force?: boolean, strict?: boolean, location?: InstallLocation }
+---@alias PackageUninstallOpts { bypass_permit?: boolean, location?: InstallLocation }
+
+---@class AbstractPackage : EventEmitter
+---@field name string
+---@field spec RegistryPackageSpec
+---@field private install_handle InstallHandle? The currently associated installation handle.
+---@field private uninstall_handle InstallHandle? The currently associated uninstallation handle.
+local AbstractPackage = {}
+AbstractPackage.__index = AbstractPackage
+setmetatable(AbstractPackage, { __index = EventEmitter })
+
+AbstractPackage.SEMAPHORE = Semaphore:new(settings.current.max_concurrent_installers)
+---@type PackageInstallOpts
+AbstractPackage.DEFAULT_INSTALL_OPTS = {
+ debug = false,
+ force = false,
+ strict = false,
+ target = nil,
+ version = nil,
+}
+
+---@param spec RegistryPackageSpec
+function AbstractPackage:new(spec)
+ local instance = EventEmitter.new(self)
+ instance.name = spec.name -- for convenient access
+ instance.spec = spec
+ return instance
+end
+
+---@return boolean
+function AbstractPackage:is_installing()
+ return self:get_install_handle()
+ :map(
+ ---@param handle InstallHandle
+ function(handle)
+ return not handle:is_closed()
+ end
+ )
+ :or_else(false)
+end
+
+---@return boolean
+function AbstractPackage:is_uninstalling()
+ return self:get_uninstall_handle()
+ :map(
+ ---@param handle InstallHandle
+ function(handle)
+ return not handle:is_closed()
+ end
+ )
+ :or_else(false)
+end
+
+function AbstractPackage:get_install_handle()
+ return Optional.of_nilable(self.install_handle)
+end
+
+function AbstractPackage:get_uninstall_handle()
+ return Optional.of_nilable(self.uninstall_handle)
+end
+
+---@param location InstallLocation
+function AbstractPackage:new_handle(location)
+ assert(location, "Cannot create new handle without a location.")
+ local InstallHandle = require "mason-core.installer.InstallHandle"
+ local handle = InstallHandle:new(self, location)
+ -- Ideally we'd decouple this and leverage Mason's event system, but to allow loading as little as possible during
+ -- setup (i.e. not load modules related to Mason's event system) of the mason.nvim plugin we explicitly call into
+ -- terminator here.
+ require("mason-core.terminator").register(handle)
+ return handle
+end
+
+---@param location? InstallLocation
+function AbstractPackage:new_install_handle(location)
+ location = location or InstallLocation.global()
+ log.fmt_trace("Creating new installation handle for %s", self)
+ self:get_install_handle():if_present(function(handle)
+ assert(handle:is_closed(), "Cannot create new install handle because existing handle is not closed.")
+ end)
+ self.install_handle = self:new_handle(location)
+ self:emit("install:handle", self.install_handle)
+ return self.install_handle
+end
+
+---@param location? InstallLocation
+function AbstractPackage:new_uninstall_handle(location)
+ location = location or InstallLocation.global()
+ log.fmt_trace("Creating new uninstallation handle for %s", self)
+ self:get_uninstall_handle():if_present(function(handle)
+ assert(handle:is_closed(), "Cannot create new uninstall handle because existing handle is not closed.")
+ end)
+ self.uninstall_handle = self:new_handle(location)
+ self:emit("uninstall:handle", self.uninstall_handle)
+ return self.uninstall_handle
+end
+
+---@param opts? PackageInstallOpts
+function AbstractPackage:is_installable(opts)
+ return require("mason-core.installer.compiler").parse(self.spec, opts or {}):is_success()
+end
+
+---@param location? InstallLocation
+---@return Optional # Optional<InstallReceipt>
+function AbstractPackage:get_receipt(location)
+ location = location or InstallLocation.global()
+ local receipt_path = location:receipt(self.name)
+ if fs.sync.file_exists(receipt_path) then
+ local receipt = require "mason-core.receipt"
+ return Optional.of(receipt.InstallReceipt.from_json(vim.json.decode(fs.sync.read_file(receipt_path))))
+ end
+ return Optional.empty()
+end
+
+---@param location? InstallLocation
+---@return boolean
+function AbstractPackage:is_installed(location)
+ error "Unimplemented."
+end
+
+---@return Result # Result<string[]>
+function AbstractPackage:get_all_versions()
+ local compiler = require "mason-core.installer.compiler"
+ return Result.try(function(try)
+ ---@type Purl
+ local purl = try(Purl.parse(self.spec.source.id))
+ ---@type InstallerCompiler
+ local compiler = try(compiler.get_compiler(purl))
+ return compiler.get_versions(purl, self.spec.source)
+ end)
+end
+
+---@return string
+function AbstractPackage:get_latest_version()
+ return Purl.parse(self.spec.source.id)
+ :map(_.prop "version")
+ :get_or_throw(("Unable to retrieve version from malformed purl: %s."):format(self.spec.source.id))
+end
+
+---@param location? InstallLocation
+---@return string?
+function AbstractPackage:get_installed_version(location)
+ return self:get_receipt(location)
+ :and_then(
+ ---@param receipt InstallReceipt
+ function(receipt)
+ local source = receipt:get_source()
+ if source.id then
+ return Purl.parse(source.id):map(_.prop "version"):ok()
+ else
+ return Optional.empty()
+ end
+ end
+ )
+ :or_else(nil)
+end
+
+---@param opts? PackageInstallOpts
+---@param callback? InstallRunnerCallback
+---@return InstallHandle
+function AbstractPackage:install(opts, callback)
+ error "Unimplemented."
+end
+
+---@param opts? PackageUninstallOpts
+---@param callback? InstallRunnerCallback
+---@return InstallHandle
+function AbstractPackage:uninstall(opts, callback)
+ error "Unimplemented."
+end
+
+---@private
+---@param location? InstallLocation
+function AbstractPackage:unlink(location)
+ location = location or InstallLocation.global()
+ log.fmt_trace("Unlinking", self, location)
+ local linker = require "mason-core.installer.linker"
+ return self:get_receipt(location):ok_or("Unable to find receipt."):and_then(function(receipt)
+ return linker.unlink(self, receipt, location)
+ end)
+end
+
+---@async
+---@private
+---@return Permit
+function AbstractPackage:acquire_permit()
+ error "Unimplemented."
+end
+
+return AbstractPackage
diff --git a/lua/mason-core/package/init.lua b/lua/mason-core/package/init.lua
new file mode 100644
index 00000000..09b0ebbf
--- /dev/null
+++ b/lua/mason-core/package/init.lua
@@ -0,0 +1,182 @@
+local AbstractPackage = require "mason-core.package.AbstractPackage"
+local InstallLocation = require "mason-core.installer.InstallLocation"
+local InstallRunner = require "mason-core.installer.InstallRunner"
+local Optional = require "mason-core.optional"
+local Result = require "mason-core.result"
+local UninstallRunner = require "mason-core.installer.UninstallRunner"
+local _ = require "mason-core.functional"
+local fs = require "mason-core.fs"
+local path = require "mason-core.path"
+local registry = require "mason-registry"
+local platform = require "mason-core.platform"
+local Semaphore = require("mason-core.async.control").Semaphore
+
+---@class Package : AbstractPackage
+---@field spec RegistryPackageSpec
+---@field local_semaphore Semaphore
+local Package = {}
+Package.__index = Package
+setmetatable(Package, { __index = AbstractPackage })
+
+---@param package_identifier string
+---@return string, string?
+Package.Parse = function(package_identifier)
+ local name, version = unpack(vim.split(package_identifier, "@"))
+ return name, version
+end
+
+---@alias PackageLanguage string
+
+---@type table<PackageLanguage, PackageLanguage>
+Package.Lang = setmetatable({}, {
+ __index = function(s, lang)
+ s[lang] = lang
+ return s[lang]
+ end,
+})
+
+---@enum PackageCategory
+Package.Cat = {
+ Compiler = "Compiler",
+ Runtime = "Runtime",
+ DAP = "DAP",
+ LSP = "LSP",
+ Linter = "Linter",
+ Formatter = "Formatter",
+}
+
+---@alias PackageLicense string
+
+---@type table<PackageLicense, PackageLicense>
+Package.License = setmetatable({}, {
+ __index = function(s, license)
+ s[license] = license
+ return s[license]
+ end,
+})
+
+---@class RegistryPackageSourceVersionOverride : RegistryPackageSource
+---@field constraint string
+
+---@class RegistryPackageSource
+---@field id string PURL-compliant identifier.
+---@field version_overrides? RegistryPackageSourceVersionOverride[]
+
+---@class RegistryPackageSchemas
+---@field lsp string?
+
+---@class RegistryPackageDeprecation
+---@field since string
+---@field message string
+
+---@alias RegistryPackageSpecSchema
+--- | '"registry+v1"'
+
+---@class RegistryPackageSpec
+---@field schema RegistryPackageSpecSchema
+---@field name string
+---@field description string
+---@field homepage string
+---@field licenses string[]
+---@field languages string[]
+---@field categories string[]
+---@field deprecation RegistryPackageDeprecation?
+---@field source RegistryPackageSource
+---@field schemas RegistryPackageSchemas?
+---@field bin table<string, string>?
+---@field share table<string, string>?
+---@field opt table<string, string>?
+
+---@param spec RegistryPackageSpec
+local function validate_spec(spec)
+ if platform.cached_features["nvim-0.11"] ~= 1 then
+ return
+ end
+ vim.validate("schema", spec.schema, _.equals "registry+v1", "registry+v1")
+ vim.validate("name", spec.name, "string")
+ vim.validate("description", spec.description, "string")
+ vim.validate("homepage", spec.homepage, "string")
+ vim.validate("licenses", spec.licenses, "table")
+ vim.validate("categories", spec.categories, "table")
+ vim.validate("languages", spec.languages, "table")
+ vim.validate("source", spec.source, "table")
+ vim.validate("bin", spec.bin, { "table", "nil" })
+ vim.validate("share", spec.share, { "table", "nil" })
+end
+
+---@param spec RegistryPackageSpec
+function Package:new(spec)
+ validate_spec(spec)
+ ---@type Package
+ local instance = AbstractPackage.new(self, spec)
+ instance.local_semaphore = Semaphore:new(1)
+ return instance
+end
+
+---@param opts? PackageInstallOpts
+---@param callback? InstallRunnerCallback
+---@return InstallHandle
+function Package:install(opts, callback)
+ opts = opts or {}
+ assert(not self:is_installing(), "Package is already installing.")
+ assert(not self:is_uninstalling(), "Package is uninstalling.")
+ opts = vim.tbl_extend("force", self.DEFAULT_INSTALL_OPTS, opts or {})
+
+ local handle = self:new_install_handle(opts.location)
+ registry:emit("package:install:handle", handle)
+ local runner = InstallRunner:new(handle, AbstractPackage.SEMAPHORE)
+
+ runner:execute(opts, callback)
+
+ return handle
+end
+
+---@param opts? PackageUninstallOpts
+---@param callback? fun(success: boolean, error: any)
+function Package:uninstall(opts, callback)
+ opts = opts or {}
+ assert(self:is_installed(opts.location), "Package is not installed.")
+ assert(not self:is_uninstalling(), "Package is already uninstalling.")
+ local handle = self:new_uninstall_handle(opts.location)
+ registry:emit("package:uninstall:handle", handle)
+ local runner = UninstallRunner:new(handle, AbstractPackage.SEMAPHORE)
+ runner:execute(opts, callback)
+ return handle
+end
+
+---@param location? InstallLocation
+function Package:is_installed(location)
+ location = location or InstallLocation.global()
+ local ok, stat = pcall(vim.loop.fs_stat, location:package(self.name))
+ if not ok or not stat then
+ return false
+ end
+ return stat.type == "directory"
+end
+
+function Package:get_lsp_settings_schema()
+ local schema_file = InstallLocation.global()
+ :share(path.concat { "mason-schemas", "lsp", ("%s.json"):format(self.name) })
+ if fs.sync.file_exists(schema_file) then
+ return Result.pcall(vim.json.decode, fs.sync.read_file(schema_file), {
+ luanil = { object = true, array = true },
+ }):ok()
+ end
+ return Optional.empty()
+end
+
+function Package:get_aliases()
+ return require("mason-registry").get_package_aliases(self.name)
+end
+
+---@async
+---@private
+function Package:acquire_permit()
+ return self.local_semaphore:acquire()
+end
+
+function Package:__tostring()
+ return ("Package(name=%s)"):format(self.name)
+end
+
+return Package
diff --git a/lua/mason-core/receipt.lua b/lua/mason-core/receipt.lua
index 63403503..847b8011 100644
--- a/lua/mason-core/receipt.lua
+++ b/lua/mason-core/receipt.lua
@@ -1,15 +1,11 @@
-local Result = require "mason-core.result"
-local fs = require "mason-core.fs"
-local path = require "mason-core.path"
-
local M = {}
---@alias InstallReceiptSchemaVersion
---| '"1.0"'
---| '"1.1"'
----| '"1.2"'
+---| '"2.0"'
----@alias InstallReceiptSource {type: RegistryPackageSpecSchema, id: string}
+---@alias InstallReceiptSource {type: RegistryPackageSpecSchema, id: string, raw: RegistryPackageSource}
---@class InstallReceiptLinks
---@field bin? table<string, string>
@@ -22,6 +18,7 @@ local M = {}
---@field public metrics {start_time:integer, completion_time:integer}
---@field public source InstallReceiptSource
---@field public links InstallReceiptLinks
+---@field public install_options PackageInstallOpts
local InstallReceipt = {}
InstallReceipt.__index = InstallReceipt
@@ -33,6 +30,10 @@ function InstallReceipt.from_json(json)
return InstallReceipt:new(json)
end
+function InstallReceipt:__tostring()
+ return ("InstallReceipt(name=%s, purl=%s)"):format(self.name, self:get_source().id or "N/A")
+end
+
function InstallReceipt:get_name()
return self.name
end
@@ -49,22 +50,30 @@ end
---@return InstallReceiptSource
function InstallReceipt:get_source()
- if self:is_schema_min "1.2" then
+ if self:is_schema_min "2.0" then
return self.source
end
return self.primary_source --[[@as InstallReceiptSource]]
end
+function InstallReceipt:get_raw_source()
+ if self:is_schema_min "2.0" then
+ return self.source.raw
+ else
+ return nil
+ end
+end
+
+function InstallReceipt:get_install_options()
+ return self.install_options
+end
+
function InstallReceipt:get_links()
return self.links
end
----@async
----@param dir string
-function InstallReceipt:write(dir)
- return Result.pcall(function()
- fs.async.write_file(path.concat { dir, "mason-receipt.json" }, vim.json.encode(self))
- end)
+function InstallReceipt:to_json()
+ return vim.json.encode(self)
end
---@class InstallReceiptBuilder
@@ -96,6 +105,12 @@ function InstallReceiptBuilder:with_source(source)
return self
end
+---@param install_options PackageInstallOpts
+function InstallReceiptBuilder:with_install_options(install_options)
+ self.install_options = install_options
+ return self
+end
+
---@param typ '"bin"' | '"share"' | '"opt"'
---@param name string
---@param rel_path string
@@ -132,13 +147,15 @@ function InstallReceiptBuilder:build()
assert(self.start_time, "start_time is required")
assert(self.completion_time, "completion_time is required")
assert(self.source, "source is required")
+ assert(self.install_options, "install_options is required")
return InstallReceipt:new {
name = self.name,
- schema_version = "1.2",
+ schema_version = "2.0",
metrics = {
start_time = self.start_time,
completion_time = self.completion_time,
},
+ install_options = self.install_options,
source = self.source,
links = self.links,
}