aboutsummaryrefslogtreecommitdiffstats
path: root/lua/mason-core/package.lua
diff options
context:
space:
mode:
Diffstat (limited to 'lua/mason-core/package.lua')
-rw-r--r--lua/mason-core/package.lua274
1 files changed, 274 insertions, 0 deletions
diff --git a/lua/mason-core/package.lua b/lua/mason-core/package.lua
new file mode 100644
index 00000000..b0da8a61
--- /dev/null
+++ b/lua/mason-core/package.lua
@@ -0,0 +1,274 @@
+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 = setmetatable({}, { __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,
+})
+
+local PackageMt = { __index = Package }
+
+---@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)
+ return EventEmitter.init(setmetatable({
+ name = spec.name, -- for convenient access
+ spec = spec,
+ }, PackageMt))
+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.new(settings.current.install_root_dir), 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):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 path.package_prefix(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 = path.share_prefix(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 PackageMt.__tostring(self)
+ return ("Package(name=%s)"):format(self.name)
+end
+
+function Package:get_aliases()
+ return require("mason-registry").get_package_aliases(self.name)
+end
+
+return Package