Skip to content

Commit

Permalink
refactor!: consolidate Lua registry sources and the Package API (#1498)
Browse files Browse the repository at this point in the history
**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.

<details>
<summary>To handle these breaking changes in plugins, leverage the `mason.version` module, for example:</summary>

```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
```

</details>

---

<details>
<summary>This change also introduces breaking changes for Lua registry sources, by consolidating the package schema with the
registry.</summary>

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",
    },
}
```

</details>
  • Loading branch information
williamboman committed Feb 25, 2024
1 parent a3e39ce commit df679b4
Show file tree
Hide file tree
Showing 18 changed files with 307 additions and 421 deletions.
14 changes: 9 additions & 5 deletions doc/mason.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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})
Expand Down
120 changes: 61 additions & 59 deletions doc/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

<!--toc:start-->
- [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)
Expand All @@ -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)
Expand All @@ -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)
<!--toc:end-->

## Architecture diagram

Expand Down Expand Up @@ -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<string, string>?` |
| share | `table<string, string>?` |
| opt | `table<string, string>?` |
| Key | Value |
| ----------- | ------------------------------------- |
| schema | `"registry+v1"` |
| name | `string` |
| description | `string` |
| homepage | `string` |
| licenses | [`PackageLicense[]`](#packagelicense) |
| categories | [`PackageCategory[]`](#packagecat) |
| languages | [`PackageLanguage[]`](#packagelang) |
| source | `table` |
| bin | `table<string, string>?` |
| share | `table<string, string>?` |
| opt | `table<string, string>?` |

## `Package`

Expand Down Expand Up @@ -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)

Expand All @@ -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`

Expand Down
2 changes: 1 addition & 1 deletion lua/mason-core/installer/context.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
8 changes: 2 additions & 6 deletions lua/mason-core/installer/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
17 changes: 3 additions & 14 deletions lua/mason-core/installer/registry/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ M.register_provider("nuget", _.lazy_require "mason-core.installer.registry.provi
M.register_provider("opam", _.lazy_require "mason-core.installer.registry.providers.opam")
M.register_provider("openvsx", _.lazy_require "mason-core.installer.registry.providers.openvsx")
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

Expand Down Expand Up @@ -127,7 +128,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)
Expand Down Expand Up @@ -213,16 +214,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
43 changes: 43 additions & 0 deletions lua/mason-core/installer/registry/providers/mason.lua
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit df679b4

Please sign in to comment.