From 3d6c0661ef81371ea5267f75a84b88af520e20d4 Mon Sep 17 00:00:00 2001 From: William Boman Date: Sun, 10 Sep 2023 10:15:21 +0200 Subject: [PATCH] refactor!: consolidate Lua registry sources and the Package API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **This removes the following APIs:** - `Package:check_new_version()`. Instead use the new `Package:get_latest_version()`. **This has a breaking change in the following APIs:** - `Package:get_installed_version()` now no longer takes a callback but instead returns the installed version or `nil` if not installed.
To handle these breaking changes in plugins, leverage the `mason.version` module, for example: ```lua local mason_version = require("mason.version") local registry = require("mason-registry") local pkg = registry.get_package("rust-analyzer") if mason_version.MAJOR_VERSION < 2 then -- before pkg:check_new_version(function (success, new_version) -- … end) pkg:get_installed_version(function (success, installed_version) -- … end) else -- after local new_version = pkg:get_latest_version() local installed_version = pkg:get_installed_version() fi ```
---
This change also introduces breaking changes for Lua registry sources, by consolidating the package schema with the registry. The following is an example of a package defined in a Lua registry, following the new schema: ```lua local Pkg = require("mason-core.package") return Pkg.new { schema = "registry+v1", name = "ripgrep", description = "ripgrep recursively searches directories for a regex pattern while respecting your gitignore.", homepage = "https://github.com/BurntSushi/ripgrep", licenses = { Pkg.License.MIT }, languages = {}, categories = {}, source = { id = "pkg:mason/ripgrep@13.0.0", ---@param ctx InstallContext ---@param purl Purl install = function(ctx, purl) -- Arbitrary installation code. end, }, bin = { rg = "./bin/rg", }, } ```
--- doc/mason.txt | 14 +- doc/reference.md | 120 +++++++------- lua/mason-core/installer/context.lua | 2 +- lua/mason-core/installer/init.lua | 8 +- lua/mason-core/installer/registry/init.lua | 17 +- .../installer/registry/providers/mason.lua | 43 +++++ lua/mason-core/package/init.lua | 148 ++++++------------ lua/mason-core/package/version-check.lua | 80 ---------- lua/mason-registry/sources/lua.lua | 2 +- lua/mason-registry/sources/util.lua | 7 +- lua/mason/api/command.lua | 42 +++-- lua/mason/ui/components/main/package_list.lua | 16 +- lua/mason/ui/instance.lua | 56 +++---- .../lua/dummy-registry/dummy2_package.lua | 19 ++- .../lua/dummy-registry/dummy_package.lua | 19 ++- tests/mason-core/installer/installer_spec.lua | 84 +++++----- tests/mason-core/package/package_spec.lua | 33 ++-- tests/mason-core/terminator_spec.lua | 18 +-- 18 files changed, 307 insertions(+), 421 deletions(-) create mode 100644 lua/mason-core/installer/registry/providers/mason.lua delete mode 100644 lua/mason-core/package/version-check.lua diff --git a/doc/mason.txt b/doc/mason.txt index e7a2d3bf6..27361576f 100644 --- a/doc/mason.txt +++ b/doc/mason.txt @@ -455,15 +455,19 @@ to redo whatever is failing after changing the log level in order to capture new log entries. ============================================================================== -Lua module: mason - +Lua module: "mason" +>lua + require("mason") +< *mason.setup()* setup({config}) Sets up mason with the provided {config} (see |mason-settings|). ============================================================================== -Lua module: mason-registry - +Lua module: "mason-registry" +>lua + require("mason-registry") +< *mason-registry.is_installed()* is_installed({package_name}) Checks whether the provided package name is installed. In many situations, @@ -537,7 +541,7 @@ get_all_package_specs() |mason-registry.get_all_packages()| because it loads fewer modules. Returns: - (PackageSpec | RegistryPackageSpec)[] + RegistryPackageSpec[] *mason-registry.update()* update({callback}) diff --git a/doc/reference.md b/doc/reference.md index b9ff564e5..2f23e7933 100644 --- a/doc/reference.md +++ b/doc/reference.md @@ -23,23 +23,25 @@ RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as de [rfc2119]: https://tools.ietf.org/html/rfc2119 [rfc8174]: https://tools.ietf.org/html/rfc8174 + - [Architecture diagram](#architecture-diagram) - [Registry events](#registry-events) -- [`PackageSpec`](#packagespec) - [`RegistryPackageSpec`](#registrypackagespec) - [`Package`](#package) - [`Package.Parse({package_identifier})`](#packageparsepackage_identifier) - [`Package.Lang`](#packagelang) - [`Package.Cat`](#packagecat) + - [`Package.License`](#packagelicense) - [`Package.new({spec})`](#packagenewspec) - - [`Package.spec`](#packagespec-1) - - [`Package:install({opts})`](#packageinstallopts) + - [`Package.spec`](#packagespec) + - [`Package:install({opts?})`](#packageinstallopts) - [`Package:uninstall()`](#packageuninstall) - [`Package:is_installed()`](#packageis_installed) - [`Package:get_install_path()`](#packageget_install_path) - - [`Package:get_installed_version({callback})`](#packageget_installed_versioncallback) - - [`Package:check_new_version({callback})`](#packagecheck_new_versioncallback) -- [`NewPackageVersion`](#newpackageversion) + - [`Package:get_installed_version()`](#packageget_installed_version) + - [`Package:get_latest_version()`](#packageget_latest_version) + - [`Package:is_installable({opts?})`](#packageis_installableopts) +- [`PackageInstallOpts`](#packageinstallopts-1) - [`InstallContext`](#installcontext) - [`InstallContext.package`](#installcontextpackage) - [`InstallContext.handle`](#installcontexthandle) @@ -48,10 +50,15 @@ RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as de - [`InstallContext.fs`](#installcontextfs) - [`InstallContext.requested_version`](#installcontextrequested_version) - [`InstallContext.stdio_sink`](#installcontextstdio_sink) +- [`ContextualFs`](#contextualfs) +- [`ContextualSpawn`](#contextualspawn) +- [`CwdManager`](#cwdmanager) + - [`CwdManager:set({cwd)})`](#cwdmanagersetcwd) + - [`CwdManager:get()`](#cwdmanagerget) - [`InstallHandleState`](#installhandlestate) - [`InstallHandle`](#installhandle) - [`InstallHandle.package`](#installhandlepackage) - - [`InstallHandle.state`](#installhandlestate-1) + - [`InstallHandle.state`](#installhandlestate) - [`InstallHandle.is_terminated`](#installhandleis_terminated) - [`InstallHandle:is_idle()`](#installhandleis_idle) - [`InstallHandle:is_queued()`](#installhandleis_queued) @@ -63,6 +70,7 @@ RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as de - [`EventEmitter:on({event}, {handler})`](#eventemitteronevent-handler) - [`EventEmitter:once({event, handler})`](#eventemitteronceevent-handler) - [`EventEmitter:off({event}, {handler})`](#eventemitteroffevent-handler) + ## Architecture diagram @@ -101,34 +109,21 @@ registry:on( ) ``` -## `PackageSpec` - -**Type:** - -| Key | Value | -| ---------- | ----------------------------------- | -| name | `string` | -| desc | `string` | -| homepage | `string` | -| categories | [`PackageCategory[]`](#packagecat) | -| languages | [`PackageLanguage[]`](#packagelang) | -| install | `async fun(ctx: InstallContext)` | - ## `RegistryPackageSpec` -| Key | Value | -| ----------- | ----------------------------------- | -| schema | `"registry+v1"` | -| name | `string` | -| description | `string` | -| homepage | `string` | -| licenses | `string` | -| categories | [`PackageCategory[]`](#packagecat) | -| languages | [`PackageLanguage[]`](#packagelang) | -| source | `table` | -| bin | `table?` | -| share | `table?` | -| opt | `table?` | +| Key | Value | +| ----------- | ------------------------------------- | +| schema | `"registry+v1"` | +| name | `string` | +| description | `string` | +| homepage | `string` | +| licenses | [`PackageLicense[]`](#packagelicense) | +| categories | [`PackageCategory[]`](#packagecat) | +| languages | [`PackageLanguage[]`](#packagelang) | +| source | `table` | +| bin | `table?` | +| share | `table?` | +| opt | `table?` | ## `Package` @@ -184,23 +179,25 @@ Package.Cat = { } ``` -All the available categories a package can be tagged with. +### `Package.License` + +Similar as [`Package.Lang`](#packagelang) but for SPDX license identifiers. ### `Package.new({spec})` **Parameters:** -- `spec`: [`PackageSpec`](#packagespec) +- `spec`: [`RegistryPackageSpec`](#registrypackagespec) ### `Package.spec` -**Type**: [`PackageSpec`](#packagespec) or [`RegistryPackageSpec`](#registrypackagespec) +**Type**: [`RegistryPackageSpec`](#registrypackagespec) -### `Package:install({opts})` +### `Package:install({opts?})` **Parameters:** -- `opts`: `{ version: string|nil } | nil` (optional) +- `opts?`: [`PackageInstallOpts`](#packageinstallopts-1) (optional) **Returns:** [`InstallHandle`](#installhandle) @@ -226,40 +223,45 @@ Uninstalls the package instance this method is being called on. **Returns:** `string` The full path where this package is installed. _Note that this will always return a string, regardless of whether the package is actually installed or not._ -### `Package:get_installed_version({callback})` +### `Package:get_installed_version()` -**Parameters:** +**Returns:** `string?` The currently installed version of the package. Returns `nil` if the package is not installed. -- `callback`: `fun(success: boolean, version_or_err: string)` +### `Package:get_latest_version()` -This method will asynchronously get the currently installed version, and invoke the provided `{callback}` with the -results. +**Returns:** `string` The latest package version as provided by the currently installed version of the registry. -### `Package:check_new_version({callback})` +_Note that this method will not check if one or more registries are outdated. If it's desired to retrieve the latest +upstream version, refresh/update registries first (`:h mason-registry.refresh()`, `:h mason-registry.update()`), for +example:_ -**Parameters:** +```lua +local registry = require "mason-registry" +registry.refresh(function() + local pkg = registry.get_package "rust-analyzer" + local latest_version = pkg:get_latest_version() +end) +``` -- `callback`: `fun(success: boolean, result_or_err: NewPackageVersion | string)` +### `Package:is_installable({opts?})` -This method will asynchronously check whether there's a newer version of the package, and invoke the provided -`{callback}` with the results. +**Parameters:** -_Note that the `{callback}` will only be invoked with `success = true` when there is a new version available (i.e. a -version that is considered newer/greater than the one currently installed). When a new version can not be found, either -because the current version is the latest or due to other issues, `{callback}` will be invoked with `success = false`._ +- `opts?`: [`PackageInstallOpts`](#packageinstallopts-1) (optional) -_Note that this method will result in network calls and will error when there is no internet connection. Also, one -should call this method with care as to not cause high network traffic as well as respecting user's online privacy._ +**Returns:** `boolean` Returns `true` if the package is installable on the current platform. -## `NewPackageVersion` +## `PackageInstallOpts` **Type:** -| Key | Value | -| --------------- | -------- | -| name | `string` | -| current_version | `string` | -| latest_version | `string` | +| Key | Value | Description | +| ------- | ---------- | -------------------------------------------------------------------------------------------------------- | +| version | `string?` | The desired version of the package. | +| target | `string?` | The desired target of the package to install (e.g. `darwin_arm64`, `linux_x64`). | +| debug | `boolean?` | If debug logs should be written. | +| force | `boolean?` | If installation should continue if there are conditions that would normally cause installation to fail. | +| strict | `boolean?` | If installation should NOT continue if there are errors that are not necessary for package to be usable. | ## `InstallContext` diff --git a/lua/mason-core/installer/context.lua b/lua/mason-core/installer/context.lua index 44bec770a..88200f9ff 100644 --- a/lua/mason-core/installer/context.lua +++ b/lua/mason-core/installer/context.lua @@ -202,7 +202,7 @@ function InstallContext.new(handle, opts) local cwd_manager = CwdManager.new(path.install_prefix()) return setmetatable({ cwd = cwd_manager, - spawn = ContextualSpawn.new(cwd_manager, handle, not handle.package:is_registry_spec()), + spawn = ContextualSpawn.new(cwd_manager, handle, false), handle = handle, package = handle.package, -- for convenience fs = ContextualFs.new(cwd_manager), diff --git a/lua/mason-core/installer/init.lua b/lua/mason-core/installer/init.lua index 994ab8478..961c5c47b 100644 --- a/lua/mason-core/installer/init.lua +++ b/lua/mason-core/installer/init.lua @@ -74,6 +74,7 @@ end ---@async ---@param context InstallContext function M.prepare_installer(context) + local installer = require "mason-core.installer.registry" return Result.try(function(try) local package_build_prefix = path.package_build_prefix(context.package.name) if fs.async.dir_exists(package_build_prefix) then @@ -82,12 +83,7 @@ function M.prepare_installer(context) try(Result.pcall(fs.async.mkdirp, package_build_prefix)) context.cwd:set(package_build_prefix) - if context.package:is_registry_spec() then - local registry_installer = require "mason-core.installer.registry" - return try(registry_installer.compile(context.handle.package.spec, context.opts)) - else - return context.package.spec.install - end + return try(installer.compile(context.handle.package.spec, context.opts)) end) end diff --git a/lua/mason-core/installer/registry/init.lua b/lua/mason-core/installer/registry/init.lua index 0cec2161c..91ab58e6e 100644 --- a/lua/mason-core/installer/registry/init.lua +++ b/lua/mason-core/installer/registry/init.lua @@ -34,9 +34,10 @@ M.register_provider("npm", _.lazy_require "mason-core.installer.registry.provide M.register_provider("nuget", _.lazy_require "mason-core.installer.registry.providers.nuget") M.register_provider("opam", _.lazy_require "mason-core.installer.registry.providers.opam") M.register_provider("pypi", _.lazy_require "mason-core.installer.registry.providers.pypi") +M.register_provider("mason", _.lazy_require "mason-core.installer.registry.providers.mason") ---@param purl Purl -local function get_provider(purl) +function M.get_provider(purl) return Optional.of_nilable(PROVIDERS[purl.type]):ok_or(("Unknown purl type: %s"):format(purl.type)) end @@ -126,7 +127,7 @@ function M.parse(spec, opts) end ---@type InstallerProvider - local provider = try(get_provider(purl)) + local provider = try(M.get_provider(purl)) log.trace("Found provider for purl.", source.id) local parsed_source = try(provider.parse(source, purl, opts)) log.trace("Parsed source for purl.", source.id, parsed_source) @@ -212,16 +213,4 @@ function M.compile(spec, opts) end) end ----@async ----@param spec RegistryPackageSpec -function M.get_versions(spec) - return Result.try(function(try) - ---@type Purl - local purl = try(Purl.parse(spec.source.id)) - ---@type InstallerProvider - local provider = try(get_provider(purl)) - return provider.get_versions(purl, spec.source) - end) -end - return M diff --git a/lua/mason-core/installer/registry/providers/mason.lua b/lua/mason-core/installer/registry/providers/mason.lua new file mode 100644 index 000000000..3490ebaad --- /dev/null +++ b/lua/mason-core/installer/registry/providers/mason.lua @@ -0,0 +1,43 @@ +local Result = require "mason-core.result" +local _ = require "mason-core.functional" + +local M = {} + +---@param source RegistryPackageSource +---@param purl Purl +function M.parse(source, purl) + if type(source.install) ~= "function" and type((getmetatable(source.install) or {}).__call) ~= "function" then + return Result.failure "source.install is not a function." + end + + ---@class ParsedMasonSource : ParsedPackageSource + local parsed_source = { + purl = purl, + ---@type async fun(ctx: InstallContext, purl: Purl) + install = source.install, + } + + return Result.success(parsed_source) +end + +---@async +---@param ctx InstallContext +---@param source ParsedMasonSource +function M.install(ctx, source) + ctx.spawn.strict_mode = true + return Result.pcall(source.install, ctx, source.purl) + :on_success(function() + ctx.spawn.strict_mode = false + end) + :on_failure(function() + ctx.spawn.strict_mode = false + end) +end + +---@async +---@param purl Purl +function M.get_versions(purl) + return Result.failure "Unimplemented." +end + +return M diff --git a/lua/mason-core/package/init.lua b/lua/mason-core/package/init.lua index 57f4868de..25d14a044 100644 --- a/lua/mason-core/package/init.lua +++ b/lua/mason-core/package/init.lua @@ -9,13 +9,9 @@ local log = require "mason-core.log" local path = require "mason-core.path" local registry = require "mason-registry" -local is_not_nil = _.complement(_.is_nil) -local is_registry_schema_id = _.matches "^registry%+v[1-9]+$" -local is_registry_spec = _.prop_satisfies(_.all_pass { is_not_nil, is_registry_schema_id }, "schema") - ---@class Package : EventEmitter ---@field name string ----@field spec RegistryPackageSpec | PackageSpec +---@field spec RegistryPackageSpec ---@field private handle InstallHandle The currently associated handle. local Package = setmetatable({}, { __index = EventEmitter }) @@ -46,15 +42,17 @@ Package.Cat = { Formatter = "Formatter", } -local PackageMt = { __index = Package } +---@alias PackageLicense string ----@class PackageSpec ----@field name string ----@field desc string ----@field homepage string ----@field categories PackageCategory[] ----@field languages PackageLanguage[] ----@field install async fun(ctx: InstallContext) +---@type table +Package.License = setmetatable({}, { + __index = function(s, license) + s[license] = license + return s[license] + end, +}) + +local PackageMt = { __index = Package } ---@class RegistryPackageSourceVersionOverride : RegistryPackageSource ---@field constraint string @@ -85,30 +83,20 @@ local PackageMt = { __index = Package } ---@field share table? ---@field opt table? ----@param spec PackageSpec | RegistryPackageSpec +---@param spec RegistryPackageSpec function Package.new(spec) - if is_registry_spec(spec) then - vim.validate { - name = { spec.name, "s" }, - description = { spec.description, "s" }, - homepage = { spec.homepage, "s" }, - licenses = { spec.licenses, "t" }, - categories = { spec.categories, "t" }, - languages = { spec.languages, "t" }, - source = { spec.source, "t" }, - bin = { spec.bin, { "t", "nil" } }, - share = { spec.share, { "t", "nil" } }, - } - else - vim.validate { - name = { spec.name, "s" }, - desc = { spec.desc, "s" }, - homepage = { spec.homepage, "s" }, - categories = { spec.categories, "t" }, - languages = { spec.languages, "t" }, - install = { spec.install, "f" }, - } - end + vim.validate { + schema = { spec.schema, "s" }, + name = { spec.name, "s" }, + description = { spec.description, "s" }, + homepage = { spec.homepage, "s" }, + licenses = { spec.licenses, "t" }, + categories = { spec.categories, "t" }, + languages = { spec.languages, "t" }, + source = { spec.source, "t" }, + bin = { spec.bin, { "t", "nil" } }, + share = { spec.share, { "t", "nil" } }, + } return EventEmitter.init(setmetatable({ name = spec.name, -- for convenient access @@ -237,67 +225,40 @@ function Package:get_receipt() return Optional.empty() end ----@param callback fun(success: boolean, version_or_err: string) -function Package:get_installed_version(callback) - self:get_receipt() - :if_present( +---@return string? +function Package:get_installed_version() + return self:get_receipt() + :and_then( ---@param receipt InstallReceipt function(receipt) - if is_registry_schema_id(receipt.primary_source.type) then - local resolve = _.curryN(callback, 2) - Purl.parse(receipt.primary_source.id) - :map(_.prop "version") - :on_success(resolve(true)) - :on_failure(resolve(false)) - else - a.run(function() - local version_checks = require "mason-core.package.version-check" - return version_checks.get_installed_version(receipt, self:get_install_path()):get_or_throw() - end, callback) - end + return Purl.parse(receipt.primary_source.id):map(_.prop "version"):ok() end ) - :if_not_present(function() - callback(false, "Unable to get receipt.") - end) + :or_else(nil) end ----@param callback fun(success: boolean, result_or_err: NewPackageVersion) -function Package:check_new_version(callback) - if self:is_registry_spec() then - self:get_installed_version(_.scheduler_wrap(function(success, installed_version) - if not success then - return callback(false, installed_version) - end - local resolve = _.curryN(callback, 2) - Result.try(function(try) - -- This is a bit goofy, but it's done to verify that a new version is supported by the - -- current platform (parse fails if it's not). We don't want to surface new versions that - -- are unsupported. - try(require("mason-core.installer.registry").parse(self.spec, {})) - - ---@type Purl - local purl = try(Purl.parse(self.spec.source.id)) - if purl.version and installed_version ~= purl.version then - return { - name = purl.name, - current_version = installed_version, - latest_version = purl.version, - } - else - return Result.failure "Package is not outdated." - end - end) - :on_success(resolve(true)) - :on_failure(resolve(false)) - end)) - else - a.run(function() - local receipt = self:get_receipt():or_else_throw "Unable to get receipt." - local version_checks = require "mason-core.package.version-check" - return version_checks.get_new_version(receipt, self:get_install_path()):get_or_throw() - end, callback) - 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.registry").parse(self.spec, opts or {}):is_success() +end + +---@return Result # Result +function Package:get_all_versions() + local registry_installer = require "mason-core.installer.registry" + return Result.try(function(try) + ---@type Purl + local purl = try(Purl.parse(self.spec.source.id)) + ---@type InstallerProvider + local provider = try(registry_installer.get_provider(purl)) + return provider.get_versions(purl, self.spec.source) + end) end function Package:get_lsp_settings_schema() @@ -310,11 +271,6 @@ function Package:get_lsp_settings_schema() return Optional.empty() end ----@return boolean -function Package:is_registry_spec() - return is_registry_spec(self.spec) -end - function PackageMt.__tostring(self) return ("Package(name=%s)"):format(self.name) end diff --git a/lua/mason-core/package/version-check.lua b/lua/mason-core/package/version-check.lua deleted file mode 100644 index 66d9ad135..000000000 --- a/lua/mason-core/package/version-check.lua +++ /dev/null @@ -1,80 +0,0 @@ -local Result = require "mason-core.result" -local cargo = require "mason-core.managers.cargo" -local composer = require "mason-core.managers.composer" -local gem = require "mason-core.managers.gem" -local git = require "mason-core.managers.git" -local github = require "mason-core.managers.github" -local go = require "mason-core.managers.go" -local log = require "mason-core.log" -local luarocks = require "mason-core.managers.luarocks" -local npm = require "mason-core.managers.npm" -local pip3 = require "mason-core.managers.pip3" - ----@param field_name string -local function version_in_receipt(field_name) - ---@param receipt InstallReceipt - ---@return Result - return function(receipt) - return Result.success(receipt.primary_source[field_name]) - end -end - ----@type table -local get_installed_version_by_type = { - ["npm"] = npm.get_installed_primary_package_version, - ["pip3"] = pip3.get_installed_primary_package_version, - ["gem"] = gem.get_installed_primary_package_version, - ["cargo"] = cargo.get_installed_primary_package_version, - ["composer"] = composer.get_installed_primary_package_version, - ["git"] = git.get_installed_revision, - ["go"] = go.get_installed_primary_package_version, - ["luarocks"] = luarocks.get_installed_primary_package_version, - ["github_release_file"] = version_in_receipt "release", - ["github_release"] = version_in_receipt "release", - ["github_tag"] = version_in_receipt "tag", -} - ----@class NewPackageVersion ----@field name string ----@field current_version string ----@field latest_version string - -local get_new_version_by_type = { - ["npm"] = npm.check_outdated_primary_package, - ["pip3"] = pip3.check_outdated_primary_package, - ["git"] = git.check_outdated_git_clone, - ["cargo"] = cargo.check_outdated_primary_package, - ["composer"] = composer.check_outdated_primary_package, - ["gem"] = gem.check_outdated_primary_package, - ["go"] = go.check_outdated_primary_package, - ["luarocks"] = luarocks.check_outdated_primary_package, - ["github_release_file"] = github.check_outdated_primary_package_release, - ["github_release"] = github.check_outdated_primary_package_release, - ["github_tag"] = github.check_outdated_primary_package_tag, -} - ----@param provider_mapping table -local function version_check(provider_mapping) - ---@param receipt InstallReceipt - ---@param install_dir string - return function(receipt, install_dir) - local check = provider_mapping[receipt.primary_source.type] - if not check then - return Result.failure( - ("Packages installed via %s does not yet support version check."):format(receipt.primary_source.type) - ) - end - return check(receipt, install_dir) - :on_success(function(version) - log.debug("Version check", version) - end) - :on_failure(function(failure) - log.debug("Version check failed", tostring(failure)) - end) - end -end - -return { - get_installed_version = version_check(get_installed_version_by_type), - get_new_version = version_check(get_new_version_by_type), -} diff --git a/lua/mason-registry/sources/lua.lua b/lua/mason-registry/sources/lua.lua index 1675f2321..1ca88a8d3 100644 --- a/lua/mason-registry/sources/lua.lua +++ b/lua/mason-registry/sources/lua.lua @@ -39,7 +39,7 @@ function LuaRegistrySource:get_all_package_names() return vim.tbl_keys(index) end ----@return PackageSpec[] +---@return RegistryPackageSpec[] function LuaRegistrySource:get_all_package_specs() return _.filter_map(function(name) return Optional.of_nilable(self:get_package(name)):map(_.prop "spec") diff --git a/lua/mason-registry/sources/util.lua b/lua/mason-registry/sources/util.lua index 262cbef2a..80d5f16fd 100644 --- a/lua/mason-registry/sources/util.lua +++ b/lua/mason-registry/sources/util.lua @@ -15,18 +15,19 @@ function M.map_registry_spec(spec) return Optional.empty() end - -- XXX: this is for compatibilty with the PackageSpec structure - spec.desc = spec.description return Optional.of(spec) end ---@param buffer table ---@param spec RegistryPackageSpec M.hydrate_package = _.curryN(function(buffer, spec) - -- hydrate Pkg.Lang index + -- hydrate Pkg.Lang/License index _.each(function(lang) local _ = Pkg.Lang[lang] end, spec.languages) + _.each(function(lang) + local _ = Pkg.License[lang] + end, spec.licenses) local pkg = buffer[spec.name] if pkg then diff --git a/lua/mason/api/command.lua b/lua/mason/api/command.lua index 65c1ee84a..38b1742b0 100644 --- a/lua/mason/api/command.lua +++ b/lua/mason/api/command.lua @@ -163,30 +163,26 @@ end, { if not ok then return {} end - if pkg:is_registry_spec() then - local a = require "mason-core.async" - local registry_installer = require "mason-core.installer.registry" - return a.run_blocking(function() - return a.wait_first { - function() - return registry_installer - .get_versions(pkg.spec --[[@as RegistryPackageSpec]]) - :map( - _.compose( - _.map(_.concat(arg_lead)), - _.map(_.strip_prefix(version)), - _.filter(_.starts_with(version)) - ) + local a = require "mason-core.async" + return a.run_blocking(function() + return a.wait_first { + function() + return pkg:get_all_versions() + :map( + _.compose( + _.map(_.concat(arg_lead)), + _.map(_.strip_prefix(version)), + _.filter(_.starts_with(version)) ) - :get_or_else {} - end, - function() - a.sleep(4000) - return {} - end, - } - end) - end + ) + :get_or_else {} + end, + function() + a.sleep(4000) + return {} + end, + } + end) end local all_pkg_names = registry.get_all_package_names() diff --git a/lua/mason/ui/components/main/package_list.lua b/lua/mason/ui/components/main/package_list.lua index 455a81963..62d654af8 100644 --- a/lua/mason/ui/components/main/package_list.lua +++ b/lua/mason/ui/components/main/package_list.lua @@ -57,18 +57,17 @@ local function ExpandedPackageInfo(state, pkg, is_installed) end), Ui.HlTextNode(_.map(function(line) return { p.Comment(line) } - end, _.split("\n", pkg.spec.desc))), + end, _.split("\n", pkg.spec.description))), Ui.EmptyLine(), Ui.Table(_.concat( _.filter(_.identity, { is_installed and { p.muted "installed version", - pkg_state.version and p.Bold(pkg_state.version) - or (pkg_state.is_checking_version and p.muted "Loading…" or p.muted "-"), + pkg_state.version and p.Bold(pkg_state.version) or p.muted "-", }, pkg_state.new_version and { p.muted "latest version", - p.muted(pkg_state.new_version.latest_version), + p.muted(pkg_state.new_version), }, { p.muted "homepage", @@ -154,18 +153,11 @@ local function PackageComponent(state, pkg, opts) source = ("Deprecated since version %s"):format(pkg.spec.deprecation.since), } end), - Ui.When(pkg_state.is_checking_new_version, function() - return Ui.VirtualTextNode { p.Comment " checking for new version…" } - end), Ui.Keybind(settings.current.ui.keymaps.check_package_version, "CHECK_NEW_PACKAGE_VERSION", pkg), Ui.When(pkg_state.new_version ~= nil, function() return Ui.DiagnosticsNode { - message = ("new version available: %s -> %s"):format( - pkg_state.new_version.current_version, - pkg_state.new_version.latest_version - ), + message = ("new version available: %s -> %s"):format(pkg_state.version or "-", pkg_state.new_version), severity = vim.diagnostic.severity.INFO, - source = pkg_state.new_version.name, } end), Ui.Node(opts.keybinds), diff --git a/lua/mason/ui/instance.lua b/lua/mason/ui/instance.lua index c8f7856b4..92cfc587e 100644 --- a/lua/mason/ui/instance.lua +++ b/lua/mason/ui/instance.lua @@ -42,15 +42,13 @@ end ---@field expanded_json_schemas table ---@field has_expanded_before boolean ---@field has_transitioned boolean ----@field is_checking_new_version boolean ----@field is_checking_version boolean ---@field is_terminated boolean ---@field is_log_expanded boolean ---@field has_failed boolean ---@field latest_spawn string? ---@field linked_executables table? ---@field lsp_settings_schema table? ----@field new_version NewPackageVersion? +---@field new_version string? ---@field short_tailed_output string? ---@field tailed_output string[] ---@field version string? @@ -212,8 +210,6 @@ local function create_initial_package_state() expanded_json_schemas = {}, has_expanded_before = false, has_transitioned = false, - is_checking_new_version = false, - is_checking_version = false, is_terminated = false, is_log_expanded = false, has_failed = false, @@ -300,20 +296,11 @@ end ---@param pkg Package local function hydrate_detailed_package_state(pkg) mutate_state(function(state) - state.packages.states[pkg.name].is_checking_version = true -- initialize expanded keys table state.packages.states[pkg.name].expanded_json_schema_keys["lsp"] = state.packages.states[pkg.name].expanded_json_schema_keys["lsp"] or {} state.packages.states[pkg.name].lsp_settings_schema = pkg:get_lsp_settings_schema():or_else(nil) - end) - - pkg:get_installed_version(function(success, version_or_err) - mutate_state(function(state) - state.packages.states[pkg.name].is_checking_version = false - if success then - state.packages.states[pkg.name].version = version_or_err - end - end) + state.packages.states[pkg.name].version = pkg:get_installed_version() end) pkg:get_receipt():if_present( @@ -443,29 +430,22 @@ end ---@async ---@param pkg Package local function check_new_package_version(pkg) - if get_state().packages.states[pkg.name].is_checking_new_version then - return - end + local installed_version = pkg:get_installed_version() mutate_state(function(state) - state.packages.states[pkg.name].is_checking_new_version = true + state.packages.states[pkg.name].version = installed_version end) - return a.wait(function(resolve, reject) - pkg:check_new_version(function(success, new_version) - mutate_state(function(state) - state.packages.states[pkg.name].is_checking_new_version = false - if success then - state.packages.states[pkg.name].new_version = new_version - else - state.packages.states[pkg.name].new_version = nil - end - end) - if success then - resolve(new_version) - else - reject(new_version) - end + local latest_version = pkg:get_latest_version() + if latest_version ~= installed_version and pkg:is_installable { version = latest_version } then + mutate_state(function(state) + state.packages.states[pkg.name].new_version = latest_version end) - end) + return true + else + mutate_state(function(state) + state.packages.states[pkg.name].new_version = nil + end) + return false + end end ---@async @@ -511,16 +491,16 @@ local function check_new_visible_package_versions() end local sem = Semaphore.new(5) - a.wait_all(_.map(function(package) + a.wait_all(_.map(function(pkg) return function() local permit = sem:acquire() - local has_new_version = pcall(check_new_package_version, package) + local has_new_version = check_new_package_version(pkg) mutate_state(function(state) state.packages.new_versions_check.current = state.packages.new_versions_check.current + 1 state.packages.new_versions_check.percentage_complete = state.packages.new_versions_check.current / state.packages.new_versions_check.total if has_new_version then - table.insert(state.packages.outdated_packages, package) + table.insert(state.packages.outdated_packages, pkg) end end) permit:forget() diff --git a/tests/helpers/lua/dummy-registry/dummy2_package.lua b/tests/helpers/lua/dummy-registry/dummy2_package.lua index 424e47d75..793404ea1 100644 --- a/tests/helpers/lua/dummy-registry/dummy2_package.lua +++ b/tests/helpers/lua/dummy-registry/dummy2_package.lua @@ -1,14 +1,17 @@ local Pkg = require "mason-core.package" return Pkg.new { + schema = "registry+v1", name = "dummy2", - desc = [[This is a dummy2 package.]], - categories = { Pkg.Cat.LSP }, - languages = { Pkg.Lang.Dummy2Lang }, + description = [[This is a dummy2 package.]], homepage = "https://example.com", - ---@async - ---@param ctx InstallContext - install = function(ctx) - ctx.receipt:with_primary_source { type = "dummy2" } - end, + licenses = { Pkg.License.MIT }, + languages = { Pkg.Lang.Dummy2Lang }, + categories = { Pkg.Cat.LSP }, + source = { + id = "pkg:mason/dummy2@1.0.0", + ---@async + ---@param ctx InstallContext + install = function(ctx) end, + }, } diff --git a/tests/helpers/lua/dummy-registry/dummy_package.lua b/tests/helpers/lua/dummy-registry/dummy_package.lua index b38d1cd83..52a709ad1 100644 --- a/tests/helpers/lua/dummy-registry/dummy_package.lua +++ b/tests/helpers/lua/dummy-registry/dummy_package.lua @@ -1,14 +1,17 @@ local Pkg = require "mason-core.package" return Pkg.new { + schema = "registry+v1", name = "dummy", - desc = [[This is a dummy package.]], - categories = { Pkg.Cat.LSP }, - languages = { Pkg.Lang.DummyLang }, + description = [[This is a dummy package.]], homepage = "https://example.com", - ---@async - ---@param ctx InstallContext - install = function(ctx) - ctx.receipt:with_primary_source { type = "dummy" } - end, + licenses = { Pkg.License.MIT }, + languages = { Pkg.Lang.DummyLang }, + categories = { Pkg.Cat.LSP }, + source = { + id = "pkg:mason/dummy@1.0.0", + ---@async + ---@param ctx InstallContext + install = function(ctx) end, + }, } diff --git a/tests/mason-core/installer/installer_spec.lua b/tests/mason-core/installer/installer_spec.lua index 23eeb69ea..04de82bad 100644 --- a/tests/mason-core/installer/installer_spec.lua +++ b/tests/mason-core/installer/installer_spec.lua @@ -25,12 +25,14 @@ describe("installer", function() spy.on(fs.async, "rename") local handle = InstallHandleGenerator "dummy" - spy.on(handle.package.spec, "install") + spy.on(handle.package.spec.source, "install") local result = installer.execute(handle, {}) assert.is_nil(result:err_or_nil()) - assert.spy(handle.package.spec.install).was_called(1) - assert.spy(handle.package.spec.install).was_called_with(match.instanceof(InstallContext)) + assert.spy(handle.package.spec.source.install).was_called(1) + assert + .spy(handle.package.spec.source.install) + .was_called_with(match.instanceof(InstallContext), match.is_table()) assert.spy(fs.async.mkdirp).was_called_with(path.package_build_prefix "dummy") assert.spy(fs.async.rename).was_called_with(path.package_build_prefix "dummy", path.package_prefix "dummy") end) @@ -45,8 +47,7 @@ describe("installer", function() error("something went wrong. don't try again.", 0) end) local handler = InstallHandleGenerator "dummy" - stub(handler.package.spec, "install") - handler.package.spec.install.invokes(installer_fn) + stub(handler.package.spec.source, "install", installer_fn) local result = installer.execute(handler, {}) assert.spy(installer_fn).was_called(1) assert.is_true(result:is_failure()) @@ -60,40 +61,45 @@ describe("installer", function() "should write receipt", async_test(function() spy.on(fs.async, "write_file") - local handler = InstallHandleGenerator "dummy" - ---@param ctx InstallContext - handler.package.spec.install = function(ctx) - ctx.receipt:with_primary_source { type = "source", source = {} } - + local handle = InstallHandleGenerator "dummy" + stub(handle.package.spec.source, "install", function(ctx) ctx.fs:write_file("target", "") ctx.fs:write_file("file.jar", "") ctx.fs:write_file("opt-cmd", "") - - ctx.links.bin = { - ["executable"] = "target", - } - ctx.links.share = { - ["package/file.jar"] = "file.jar", - } - ctx.links.opt = { - ["package/bin/opt-cmd"] = "opt-cmd", - } - end - installer.execute(handler, {}) + end) + handle.package.spec.bin = { + ["executable"] = "target", + } + handle.package.spec.share = { + ["package/file.jar"] = "file.jar", + } + handle.package.spec.opt = { + ["package/bin/opt-cmd"] = "opt-cmd", + } + installer.execute(handle, {}) + handle.package.spec.bin = {} + handle.package.spec.share = {} + handle.package.spec.opt = {} assert.spy(fs.async.write_file).was_called_with( - ("%s/mason-receipt.json"):format(handler.package:get_install_path()), + ("%s/mason-receipt.json"):format(handle.package:get_install_path()), match.capture(function(arg) ---@type InstallReceipt local receipt = vim.json.decode(arg) - assert.equals("dummy", receipt.name) - assert.same({ type = "source", source = {} }, receipt.primary_source) - assert.same({}, receipt.secondary_sources) - assert.same("1.1", receipt.schema_version) - assert.same({ - bin = { executable = "target" }, - share = { ["package/file.jar"] = "file.jar" }, - opt = { ["package/bin/opt-cmd"] = "opt-cmd" }, - }, receipt.links) + assert.is_true(match.tbl_containing { + name = "dummy", + primary_source = match.same { + type = handle.package.spec.schema, + id = handle.package.spec.source.id, + }, + secondary_sources = match.same {}, + schema_version = "1.1", + metrics = match.is_table(), + links = match.same { + bin = { executable = "target" }, + share = { ["package/file.jar"] = "file.jar" }, + opt = { ["package/bin/opt-cmd"] = "opt-cmd" }, + }, + }(receipt)) end) ) end) @@ -106,7 +112,7 @@ describe("installer", function() local capture = spy.new() local start = timestamp() local handle = InstallHandleGenerator "dummy" - handle.package.spec.install = function(ctx) + stub(handle.package.spec.source, "install", function(ctx) capture(installer.run_concurrently { function() a.sleep(100) @@ -121,8 +127,7 @@ describe("installer", function() return "three" end, }) - ctx.receipt:with_primary_source { type = "dummy" } - end + end) installer.execute(handle, {}) local stop = timestamp() local grace_ms = 25 @@ -136,11 +141,10 @@ describe("installer", function() async_test(function() spy.on(fs.async, "write_file") local handle = InstallHandleGenerator "dummy" - stub(handle.package.spec, "install", function(ctx) + stub(handle.package.spec.source, "install", function(ctx) ctx.stdio_sink.stdout "Hello stdout!\n" ctx.stdio_sink.stderr "Hello " ctx.stdio_sink.stderr "stderr!" - ctx.receipt:with_primary_source { type = "unmanaged" } end) installer.execute(handle, { debug = true }) assert @@ -153,7 +157,7 @@ describe("installer", function() "should raise spawn errors in strict mode", async_test(function() local handle = InstallHandleGenerator "dummy" - stub(handle.package.spec, "install", function(ctx) + stub(handle.package.spec.source, "install", function(ctx) ctx.spawn.bash { "-c", "exit 42" } end) local result = installer.execute(handle, { debug = true }) @@ -173,7 +177,7 @@ describe("installer", function() async_test(function() local handle = InstallHandleGenerator "dummy" local callback = spy.new() - stub(handle.package.spec, "install", function() + stub(handle.package.spec.source, "install", function() a.sleep(3000) end) @@ -197,7 +201,7 @@ describe("installer", function() async_test(function() local handle = InstallHandleGenerator "dummy" local install = spy.new() - stub(handle.package.spec, "install", install) + stub(handle.package.spec.source, "install", install) fs.sync.write_file(path.package_lock "dummy", "dummypid") local result = installer.execute(handle, { debug = true }) diff --git a/tests/mason-core/package/package_spec.lua b/tests/mason-core/package/package_spec.lua index 0343d8bdf..5712b87b4 100644 --- a/tests/mason-core/package/package_spec.lua +++ b/tests/mason-core/package/package_spec.lua @@ -25,55 +25,53 @@ describe("package", function() it("should validate spec", function() local valid_spec = { + schema = "registry+v1", name = "Package name", - desc = "Package description", + description = "Package description", homepage = "https://example.com", categories = { Pkg.Cat.LSP }, languages = { Pkg.Lang.Rust }, - install = function() end, + licenses = { Pkg.License.MIT }, + source = { + id = "pkg:mason/package@1.0.0", + install = function() end, + }, } - local function spec(fields) + local function modify_spec(fields) return setmetatable(fields, { __index = valid_spec }) end assert.equals( "name: expected string, got number", assert.has_error(function() - Pkg.new(spec { name = 23 }) + Pkg.new(modify_spec { name = 23 }) end) ) assert.equals( - "desc: expected string, got number", + "description: expected string, got number", assert.has_error(function() - Pkg.new(spec { desc = 23 }) + Pkg.new(modify_spec { description = 23 }) end) ) assert.equals( "homepage: expected string, got number", assert.has_error(function() - Pkg.new(spec { homepage = 23 }) + Pkg.new(modify_spec { homepage = 23 }) end) ) assert.equals( "categories: expected table, got number", assert.has_error(function() - Pkg.new(spec { categories = 23 }) + Pkg.new(modify_spec { categories = 23 }) end) ) assert.equals( "languages: expected table, got number", assert.has_error(function() - Pkg.new(spec { languages = 23 }) - end) - ) - - assert.equals( - "install: expected function, got number", - assert.has_error(function() - Pkg.new(spec { install = 23 }) + Pkg.new(modify_spec { languages = 23 }) end) ) end) @@ -137,8 +135,7 @@ describe("package", function() "should fail to install package", async_test(function() local dummy = registry.get_package "dummy" - stub(dummy.spec, "install") - dummy.spec.install.invokes(function() + stub(dummy.spec.source, "install", function() error "I simply refuse to be installed." end) local package_install_success_handler = spy.new() diff --git a/tests/mason-core/terminator_spec.lua b/tests/mason-core/terminator_spec.lua index 66b48ba4a..24c1ec255 100644 --- a/tests/mason-core/terminator_spec.lua +++ b/tests/mason-core/terminator_spec.lua @@ -15,7 +15,7 @@ describe("terminator", function() local dummy = registry.get_package "dummy" local dummy2 = registry.get_package "dummy2" for _, pkg in ipairs { dummy, dummy2 } do - stub(pkg.spec, "install", function() + stub(pkg.spec.source, "install", function() a.sleep(10000) end) end @@ -24,8 +24,8 @@ describe("terminator", function() local dummy2_handle = dummy2:install() assert.wait_for(function() - assert.spy(dummy.spec.install).was_called() - assert.spy(dummy2.spec.install).was_called() + assert.spy(dummy.spec.source.install).was_called() + assert.spy(dummy2.spec.source.install).was_called() end) terminator.terminate(5000) @@ -49,7 +49,7 @@ describe("terminator", function() local dummy = registry.get_package "dummy" local dummy2 = registry.get_package "dummy2" for _, pkg in ipairs { dummy, dummy2 } do - stub(pkg.spec, "install", function() + stub(pkg.spec.source, "install", function() a.sleep(10000) end) end @@ -58,8 +58,8 @@ describe("terminator", function() local dummy2_handle = dummy2:install() assert.wait_for(function() - assert.spy(dummy.spec.install).was_called() - assert.spy(dummy2.spec.install).was_called() + assert.spy(dummy.spec.source.install).was_called() + assert.spy(dummy2.spec.source.install).was_called() end) terminator.terminate(5000) @@ -92,8 +92,8 @@ describe("terminator", function() async_test(function() spy.on(InstallHandle, "kill") local dummy = registry.get_package "dummy" - stub(dummy.spec, "install") - dummy.spec.install.invokes(function(ctx) + stub(dummy.spec.source, "install") + dummy.spec.source.install.invokes(function(ctx) -- your signals have no power here ctx.spawn.bash { "-c", "function noop { :; }; trap noop SIGTERM; sleep 999999;" } end) @@ -101,7 +101,7 @@ describe("terminator", function() local handle = dummy:install() assert.wait_for(function() - assert.spy(dummy.spec.install).was_called() + assert.spy(dummy.spec.source.install).was_called() end) terminator.terminate(50)