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 | |
| 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')
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, } |
