aboutsummaryrefslogtreecommitdiffstats
path: root/lua/mason-core
diff options
context:
space:
mode:
authorWilliam Boman <william@redwill.se>2023-03-12 08:21:15 +0100
committerGitHub <noreply@github.com>2023-03-12 08:21:15 +0100
commita01d02ad7f680aec98a1e2ec35b04cedd307cfa8 (patch)
tree1a09e274a1f2a4da85b911abcbb182a211035501 /lua/mason-core
parentfeat(golangci-lint): support linux_arm64 (#1089) (diff)
downloadmason-a01d02ad7f680aec98a1e2ec35b04cedd307cfa8.tar
mason-a01d02ad7f680aec98a1e2ec35b04cedd307cfa8.tar.gz
mason-a01d02ad7f680aec98a1e2ec35b04cedd307cfa8.tar.bz2
mason-a01d02ad7f680aec98a1e2ec35b04cedd307cfa8.tar.lz
mason-a01d02ad7f680aec98a1e2ec35b04cedd307cfa8.tar.xz
mason-a01d02ad7f680aec98a1e2ec35b04cedd307cfa8.tar.zst
mason-a01d02ad7f680aec98a1e2ec35b04cedd307cfa8.zip
feat: add github registry source capabilities (#1091)
Diffstat (limited to 'lua/mason-core')
-rw-r--r--lua/mason-core/functional/init.lua2
-rw-r--r--lua/mason-core/installer/context.lua8
-rw-r--r--lua/mason-core/installer/init.lua12
-rw-r--r--lua/mason-core/installer/managers/cargo.lua47
-rw-r--r--lua/mason-core/installer/managers/composer.lua42
-rw-r--r--lua/mason-core/installer/managers/gem.lua65
-rw-r--r--lua/mason-core/installer/managers/golang.lua52
-rw-r--r--lua/mason-core/installer/managers/luarocks.lua40
-rw-r--r--lua/mason-core/installer/managers/npm.lua64
-rw-r--r--lua/mason-core/installer/managers/nuget.lua37
-rw-r--r--lua/mason-core/installer/managers/opam.lua38
-rw-r--r--lua/mason-core/installer/managers/pypi.lua106
-rw-r--r--lua/mason-core/installer/managers/std.lua241
-rw-r--r--lua/mason-core/installer/registry/init.lua196
-rw-r--r--lua/mason-core/installer/registry/link.lua300
-rw-r--r--lua/mason-core/installer/registry/providers/cargo.lua64
-rw-r--r--lua/mason-core/installer/registry/providers/composer.lua34
-rw-r--r--lua/mason-core/installer/registry/providers/gem.lua47
-rw-r--r--lua/mason-core/installer/registry/providers/generic.lua47
-rw-r--r--lua/mason-core/installer/registry/providers/github.lua179
-rw-r--r--lua/mason-core/installer/registry/providers/golang.lua50
-rw-r--r--lua/mason-core/installer/registry/providers/luarocks.lua45
-rw-r--r--lua/mason-core/installer/registry/providers/npm.lua51
-rw-r--r--lua/mason-core/installer/registry/providers/nuget.lua25
-rw-r--r--lua/mason-core/installer/registry/providers/opam.lua25
-rw-r--r--lua/mason-core/installer/registry/providers/pypi.lua59
-rw-r--r--lua/mason-core/installer/registry/util.lua83
-rw-r--r--lua/mason-core/package/init.lua132
-rw-r--r--lua/mason-core/path.lua4
-rw-r--r--lua/mason-core/platform.lua21
30 files changed, 2087 insertions, 29 deletions
diff --git a/lua/mason-core/functional/init.lua b/lua/mason-core/functional/init.lua
index 20293bd8..5e0ac680 100644
--- a/lua/mason-core/functional/init.lua
+++ b/lua/mason-core/functional/init.lua
@@ -10,6 +10,8 @@ local function lazy_require(module)
})
end
+_.lazy_require = lazy_require
+
---@module "mason-core.functional.data"
local data = lazy_require "mason-core.functional.data"
_.table_pack = data.table_pack
diff --git a/lua/mason-core/installer/context.lua b/lua/mason-core/installer/context.lua
index 8a1e3678..d637707a 100644
--- a/lua/mason-core/installer/context.lua
+++ b/lua/mason-core/installer/context.lua
@@ -118,6 +118,12 @@ function ContextualFs:mkdir(dir_path)
end
---@async
+---@param dir_path string
+function ContextualFs:mkdirp(dir_path)
+ return fs.async.mkdirp(path.concat { self.cwd:get(), dir_path })
+end
+
+---@async
---@param file_path string
---@param mode integer
function ContextualFs:chmod(file_path, mode)
@@ -179,7 +185,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, handle.package.spec.schema ~= "registry+v1"),
+ spawn = ContextualSpawn.new(cwd_manager, handle, not handle.package:is_registry_spec()),
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 9f150269..ee0e6397 100644
--- a/lua/mason-core/installer/init.lua
+++ b/lua/mason-core/installer/init.lua
@@ -64,13 +64,19 @@ function M.prepare_installer(context)
try(Result.pcall(fs.async.mkdirp, package_build_prefix))
context.cwd:set(package_build_prefix)
- return context.package.spec.install
+ 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
end)
end
----@async
+---@generic T
---@param context InstallContext
----@param fn async fun(context: InstallContext)
+---@param fn fun(context: InstallContext): T
+---@return T
function M.exec_in_context(context, fn)
local thread = coroutine.create(function(...)
-- We wrap the function to allow it to be a spy instance (in which case it's not actually a function, but a
diff --git a/lua/mason-core/installer/managers/cargo.lua b/lua/mason-core/installer/managers/cargo.lua
new file mode 100644
index 00000000..72355c9c
--- /dev/null
+++ b/lua/mason-core/installer/managers/cargo.lua
@@ -0,0 +1,47 @@
+local Result = require "mason-core.result"
+local _ = require "mason-core.functional"
+local installer = require "mason-core.installer"
+local log = require "mason-core.log"
+local platform = require "mason-core.platform"
+local path = require "mason-core.path"
+
+local M = {}
+
+---@async
+---@param crate string
+---@param version string
+---@param opts? { features?: string, locked?: boolean, git?: { url: string, rev?: boolean } }
+function M.install(crate, version, opts)
+ opts = opts or {}
+ log.fmt_debug("cargo: install %s %s %s", crate, version, opts)
+ local ctx = installer.context()
+
+ return ctx.spawn.cargo {
+ "install",
+ "--root",
+ ".",
+ opts.git and {
+ "--git",
+ opts.git.url,
+ opts.git.rev and "--rev" or "--tag",
+ version,
+ } or { "--version", version },
+ opts.features and { "--features", opts.features } or vim.NIL,
+ opts.locked and "--locked" or vim.NIL,
+ crate,
+ }
+end
+
+---@param bin string
+function M.bin_path(bin)
+ return Result.pcall(platform.when, {
+ unix = function()
+ return path.concat { "bin", bin }
+ end,
+ win = function()
+ return path.concat { "bin", ("%s.exe"):format(bin) }
+ end,
+ })
+end
+
+return M
diff --git a/lua/mason-core/installer/managers/composer.lua b/lua/mason-core/installer/managers/composer.lua
new file mode 100644
index 00000000..faa01bc4
--- /dev/null
+++ b/lua/mason-core/installer/managers/composer.lua
@@ -0,0 +1,42 @@
+local Result = require "mason-core.result"
+local _ = require "mason-core.functional"
+local installer = require "mason-core.installer"
+local log = require "mason-core.log"
+local path = require "mason-core.path"
+local platform = require "mason-core.platform"
+
+local M = {}
+
+---@async
+---@param package string
+---@param version string
+---@nodiscard
+function M.install(package, version)
+ log.fmt_debug("composer: install %s %s", package, version)
+ local ctx = installer.context()
+ return Result.try(function(try)
+ try(ctx.spawn.composer {
+ "init",
+ "--no-interaction",
+ "--stability=stable",
+ })
+ try(ctx.spawn.composer {
+ "require",
+ ("%s:%s"):format(package, version),
+ })
+ end)
+end
+
+---@param executable string
+function M.bin_path(executable)
+ return Result.pcall(platform.when, {
+ unix = function()
+ return path.concat { "vendor", "bin", executable }
+ end,
+ win = function()
+ return path.concat { "vendor", "bin", ("%s.bat"):format(executable) }
+ end,
+ })
+end
+
+return M
diff --git a/lua/mason-core/installer/managers/gem.lua b/lua/mason-core/installer/managers/gem.lua
new file mode 100644
index 00000000..0eac275e
--- /dev/null
+++ b/lua/mason-core/installer/managers/gem.lua
@@ -0,0 +1,65 @@
+local installer = require "mason-core.installer"
+local log = require "mason-core.log"
+local platform = require "mason-core.platform"
+local path = require "mason-core.path"
+local Result = require "mason-core.result"
+
+local M = {}
+
+---@async
+---@param pkg string
+---@param version string
+---@param opts? { extra_packages?: string[] }
+---@nodiscard
+function M.install(pkg, version, opts)
+ opts = opts or {}
+ log.fmt_debug("gem: install %s %s %s", pkg, version, opts)
+ local ctx = installer.context()
+
+ return ctx.spawn.gem {
+ "install",
+ "--no-user-install",
+ "--no-format-executable",
+ "--install-dir=.",
+ "--bindir=bin",
+ "--no-document",
+ ("%s:%s"):format(pkg, version),
+ opts.extra_packages or vim.NIL,
+ env = {
+ GEM_HOME = ctx.cwd:get(),
+ },
+ }
+end
+
+---@async
+---@param bin string
+---@nodiscard
+function M.create_bin_wrapper(bin)
+ local ctx = installer.context()
+
+ local bin_path = platform.when {
+ unix = function()
+ return path.concat { "bin", bin }
+ end,
+ win = function()
+ return path.concat { "bin", ("%s.bat"):format(bin) }
+ end,
+ }
+
+ if not ctx.fs:file_exists(bin_path) then
+ return Result.failure(("Cannot link Gem executable %q because it doesn't exist."):format(bin))
+ end
+
+ return Result.pcall(ctx.write_shell_exec_wrapper, ctx, bin, path.concat { ctx.package:get_install_path(), bin_path }, {
+ GEM_PATH = platform.when {
+ unix = function()
+ return ("%s:$GEM_PATH"):format(ctx.package:get_install_path())
+ end,
+ win = function()
+ return ("%s;%%GEM_PATH%%"):format(ctx.package:get_install_path())
+ end,
+ },
+ })
+end
+
+return M
diff --git a/lua/mason-core/installer/managers/golang.lua b/lua/mason-core/installer/managers/golang.lua
new file mode 100644
index 00000000..fdc000b3
--- /dev/null
+++ b/lua/mason-core/installer/managers/golang.lua
@@ -0,0 +1,52 @@
+local Result = require "mason-core.result"
+local _ = require "mason-core.functional"
+local installer = require "mason-core.installer"
+local log = require "mason-core.log"
+local platform = require "mason-core.platform"
+
+local M = {}
+
+---@async
+---@param pkg string
+---@param version string
+---@param opts? { extra_packages?: string[] }
+function M.install(pkg, version, opts)
+ return Result.try(function(try)
+ opts = opts or {}
+ log.fmt_debug("golang: install %s %s %s", pkg, version, opts)
+ local ctx = installer.context()
+ local env = {
+ GOBIN = ctx.cwd:get(),
+ }
+ try(ctx.spawn.go {
+ "install",
+ "-v",
+ ("%s@%s"):format(pkg, version),
+ env = env,
+ })
+ if opts.extra_packages then
+ for _, pkg in ipairs(opts.extra_packages) do
+ try(ctx.spawn.go {
+ "install",
+ "-v",
+ ("%s@latest"):format(pkg),
+ env = env,
+ })
+ end
+ end
+ end)
+end
+
+---@param bin string
+function M.bin_path(bin)
+ return Result.pcall(platform.when, {
+ unix = function()
+ return bin
+ end,
+ win = function()
+ return ("%s.exe"):format(bin)
+ end,
+ })
+end
+
+return M
diff --git a/lua/mason-core/installer/managers/luarocks.lua b/lua/mason-core/installer/managers/luarocks.lua
new file mode 100644
index 00000000..7f636e2b
--- /dev/null
+++ b/lua/mason-core/installer/managers/luarocks.lua
@@ -0,0 +1,40 @@
+local Result = require "mason-core.result"
+local _ = require "mason-core.functional"
+local installer = require "mason-core.installer"
+local log = require "mason-core.log"
+local platform = require "mason-core.platform"
+local path = require "mason-core.path"
+
+local M = {}
+
+---@async
+---@param pkg string
+---@param version string
+---@param opts { server?: string, dev?: boolean }
+function M.install(pkg, version, opts)
+ opts = opts or {}
+ log.fmt_debug("luarocks: install %s %s %s", pkg, version, opts)
+ local ctx = installer.context()
+ ctx:promote_cwd() -- luarocks encodes absolute paths during installation
+ return ctx.spawn.luarocks {
+ "install",
+ { "--tree", ctx.cwd:get() },
+ opts.dev and "--dev" or vim.NIL,
+ opts.server and ("--server=%s"):format(opts.server) or vim.NIL,
+ { pkg, version },
+ }
+end
+
+---@param exec string
+function M.bin_path(exec)
+ return Result.pcall(platform.when, {
+ unix = function()
+ return path.concat { "bin", exec }
+ end,
+ win = function()
+ return path.concat { "bin", ("%s.bat"):format(exec) }
+ end,
+ })
+end
+
+return M
diff --git a/lua/mason-core/installer/managers/npm.lua b/lua/mason-core/installer/managers/npm.lua
new file mode 100644
index 00000000..5eec7627
--- /dev/null
+++ b/lua/mason-core/installer/managers/npm.lua
@@ -0,0 +1,64 @@
+local Result = require "mason-core.result"
+local _ = require "mason-core.functional"
+local installer = require "mason-core.installer"
+local log = require "mason-core.log"
+local platform = require "mason-core.platform"
+local path = require "mason-core.path"
+
+local M = {}
+
+---@async
+function M.init()
+ log.debug "npm: init"
+ local ctx = installer.context()
+ return Result.try(function(try)
+ try(ctx.spawn.npm {
+ "init",
+ "--yes",
+ "--scope=mason",
+ })
+
+ -- Use global-style. The reasons for this are:
+ -- a) To avoid polluting the executables (aka bin-links) that npm creates.
+ -- b) The installation is, after all, more similar to a "global" installation. We don't really gain
+ -- any of the benefits of not using global style (e.g., deduping the dependency tree).
+ --
+ -- We write to .npmrc manually instead of going through npm because managing a local .npmrc file
+ -- is a bit unreliable across npm versions (especially <7), so we take extra measures to avoid
+ -- inadvertently polluting global npm config.
+ try(Result.pcall(function()
+ ctx.fs:append_file(".npmrc", "global-style=true")
+ end))
+
+ ctx.stdio_sink.stdout "Initialized npm root\n"
+ end)
+end
+
+---@async
+---@param pkg string
+---@param version string
+---@param opts? { extra_packages?: string[] }
+function M.install(pkg, version, opts)
+ opts = opts or {}
+ log.fmt_debug("npm: install %s %s %s", pkg, version, opts)
+ local ctx = installer.context()
+ return ctx.spawn.npm {
+ "install",
+ ("%s@%s"):format(pkg, version),
+ opts.extra_packages or vim.NIL,
+ }
+end
+
+---@param exec string
+function M.bin_path(exec)
+ return Result.pcall(platform.when, {
+ unix = function()
+ return path.concat { "node_modules", ".bin", exec }
+ end,
+ win = function()
+ return path.concat { "node_modules", ".bin", ("%s.cmd"):format(exec) }
+ end,
+ })
+end
+
+return M
diff --git a/lua/mason-core/installer/managers/nuget.lua b/lua/mason-core/installer/managers/nuget.lua
new file mode 100644
index 00000000..f547d81b
--- /dev/null
+++ b/lua/mason-core/installer/managers/nuget.lua
@@ -0,0 +1,37 @@
+local installer = require "mason-core.installer"
+local platform = require "mason-core.platform"
+local Result = require "mason-core.result"
+local log = require "mason-core.log"
+
+local M = {}
+
+---@async
+---@param package string
+---@param version string
+---@nodiscard
+function M.install(package, version)
+ log.fmt_debug("nuget: install %s %s", package, version)
+ local ctx = installer.context()
+ return ctx.spawn.dotnet {
+ "tool",
+ "update",
+ "--tool-path",
+ ".",
+ { "--version", version },
+ package,
+ }
+end
+
+---@param bin string
+function M.bin_path(bin)
+ return Result.pcall(platform.when, {
+ unix = function()
+ return bin
+ end,
+ win = function()
+ return ("%s.exe"):format(bin)
+ end,
+ })
+end
+
+return M
diff --git a/lua/mason-core/installer/managers/opam.lua b/lua/mason-core/installer/managers/opam.lua
new file mode 100644
index 00000000..2a07c4f8
--- /dev/null
+++ b/lua/mason-core/installer/managers/opam.lua
@@ -0,0 +1,38 @@
+local Result = require "mason-core.result"
+local _ = require "mason-core.functional"
+local installer = require "mason-core.installer"
+local log = require "mason-core.log"
+local platform = require "mason-core.platform"
+local path = require "mason-core.path"
+
+local M = {}
+
+---@async
+---@param package string
+---@param version string
+---@nodiscard
+function M.install(package, version)
+ log.fmt_debug("opam: install %s %s", package, version)
+ local ctx = installer.context()
+ return ctx.spawn.opam {
+ "install",
+ "--destdir=.",
+ "--yes",
+ "--verbose",
+ ("%s.%s"):format(package, version),
+ }
+end
+
+---@param bin string
+function M.bin_path(bin)
+ return Result.pcall(platform.when, {
+ unix = function()
+ return path.concat { "bin", bin }
+ end,
+ win = function()
+ return path.concat { "bin", ("%s.exe"):format(bin) }
+ end,
+ })
+end
+
+return M
diff --git a/lua/mason-core/installer/managers/pypi.lua b/lua/mason-core/installer/managers/pypi.lua
new file mode 100644
index 00000000..0800b155
--- /dev/null
+++ b/lua/mason-core/installer/managers/pypi.lua
@@ -0,0 +1,106 @@
+local Optional = require "mason-core.optional"
+local _ = require "mason-core.functional"
+local a = require "mason-core.async"
+local installer = require "mason-core.installer"
+local log = require "mason-core.log"
+local path = require "mason-core.path"
+local platform = require "mason-core.platform"
+local Result = require "mason-core.result"
+
+local M = {}
+
+local VENV_DIR = "venv"
+
+---@async
+---@param py_executables string[]
+local function create_venv(py_executables)
+ local ctx = installer.context()
+ return Optional.of_nilable(_.find_first(function(executable)
+ return ctx.spawn[executable]({ "-m", "venv", VENV_DIR }):is_success()
+ end, py_executables)):ok_or "Failed to create python3 virtual environment."
+end
+
+---@async
+---@param args SpawnArgs
+local function venv_python(args)
+ local ctx = installer.context()
+ local python_path = path.concat {
+ ctx.cwd:get(),
+ VENV_DIR,
+ platform.is.win and "Scripts" or "bin",
+ platform.is.win and "python.exe" or "python",
+ }
+ return ctx.spawn[python_path](args)
+end
+
+---@async
+---@param pkgs string[]
+---@param extra_args? string[]
+local function pip_install(pkgs, extra_args)
+ return venv_python {
+ "-m",
+ "pip",
+ "--disable-pip-version-check",
+ "install",
+ "-U",
+ extra_args or vim.NIL,
+ pkgs,
+ }
+end
+
+---@async
+---@param opts { upgrade_pip: boolean, install_extra_args?: string[] }
+function M.init(opts)
+ return Result.try(function(try)
+ log.fmt_debug("pypi: init", opts)
+ local ctx = installer.context()
+
+ if vim.in_fast_event() then
+ a.scheduler()
+ end
+
+ local executables = platform.is.win
+ and _.list_not_nil(
+ vim.g.python3_host_prog and vim.fn.expand(vim.g.python3_host_prog),
+ "python",
+ "python3"
+ )
+ or _.list_not_nil(vim.g.python3_host_prog and vim.fn.expand(vim.g.python3_host_prog), "python3", "python")
+
+ -- pip3 will hardcode the full path to venv executables, so we need to promote cwd to make sure pip uses the final destination path.
+ ctx:promote_cwd()
+
+ try(create_venv(executables))
+
+ if opts.upgrade_pip then
+ try(pip_install({ "pip" }, opts.install_extra_args))
+ end
+ end)
+end
+
+---@async
+---@param pkg string
+---@param version string
+---@param opts? { extra?: string, extra_packages?: string[], install_extra_args?: string[] }
+function M.install(pkg, version, opts)
+ opts = opts or {}
+ log.fmt_debug("pypi: install %s %s", pkg, version, opts)
+ return pip_install({
+ opts.extra and ("%s[%s]==%s"):format(pkg, opts.extra, version) or ("%s==%s"):format(pkg, version),
+ opts.extra_packages or vim.NIL,
+ }, opts.install_extra_args)
+end
+
+---@param exec string
+function M.bin_path(exec)
+ return Result.pcall(platform.when, {
+ unix = function()
+ return path.concat { "venv", "bin", exec }
+ end,
+ win = function()
+ return path.concat { "venv", "Scripts", ("%s.exe"):format(exec) }
+ end,
+ })
+end
+
+return M
diff --git a/lua/mason-core/installer/managers/std.lua b/lua/mason-core/installer/managers/std.lua
new file mode 100644
index 00000000..4ae3fc7b
--- /dev/null
+++ b/lua/mason-core/installer/managers/std.lua
@@ -0,0 +1,241 @@
+local _ = require "mason-core.functional"
+local installer = require "mason-core.installer"
+local fetch = require "mason-core.fetch"
+local path = require "mason-core.path"
+local platform = require "mason-core.platform"
+local powershell = require "mason-core.managers.powershell"
+local Result = require "mason-core.result"
+local log = require "mason-core.log"
+
+local M = {}
+
+---@async
+---@param rel_path string
+---@nodiscard
+local function unpack_7z(rel_path)
+ log.fmt_debug("std: unpack_7z %s", rel_path)
+ local ctx = installer.context()
+ return ctx.spawn["7z"] { "x", "-y", "-r", rel_path }
+end
+
+---@async
+---@param rel_path string
+---@nodiscard
+local function unpack_peazip(rel_path)
+ log.fmt_debug("std: unpack_peazip %s", rel_path)
+ local ctx = installer.context()
+ return ctx.spawn.peazip { "-ext2here", path.concat { ctx.cwd:get(), rel_path } } -- peazip requires absolute paths
+end
+
+---@async
+---@param rel_path string
+---@nodiscard
+local function wzunzip(rel_path)
+ log.fmt_debug("std: wzunzip %s", rel_path)
+ local ctx = installer.context()
+ return ctx.spawn.wzunzip { rel_path }
+end
+
+---@async
+---@param rel_path string
+---@nodiscard
+local function unpack_winrar(rel_path)
+ log.fmt_debug("std: unpack_winrar %s", rel_path)
+ local ctx = installer.context()
+ return ctx.spawn.winrar { "e", rel_path }
+end
+
+---@async
+---@param rel_path string
+---@nodiscard
+local function gunzip_unix(rel_path)
+ log.fmt_debug("std: gunzip_unix %s", rel_path)
+ local ctx = installer.context()
+ return ctx.spawn.gzip { "-d", rel_path }
+end
+
+---@async
+---@param rel_path string
+---@nodiscard
+local function unpack_arc(rel_path)
+ log.fmt_debug("std: unpack_arc %s", rel_path)
+ local ctx = installer.context()
+ return ctx.spawn.arc { "unarchive", rel_path }
+end
+
+---@param rel_path string
+---@return Result
+local function win_decompress(rel_path)
+ local ctx = installer.context()
+ return gunzip_unix(rel_path)
+ :or_else(function()
+ return unpack_7z(rel_path)
+ end)
+ :or_else(function()
+ return unpack_peazip(rel_path)
+ end)
+ :or_else(function()
+ return wzunzip(rel_path)
+ end)
+ :or_else(function()
+ return unpack_winrar(rel_path)
+ end)
+ :on_success(function()
+ pcall(function()
+ ctx.fs:unlink(rel_path)
+ end)
+ end)
+end
+
+---@async
+---@param url string
+---@param out_file string
+---@nodiscard
+function M.download_file(url, out_file)
+ log.fmt_debug("std: downloading file %s", url, out_file)
+ local ctx = installer.context()
+ ctx.stdio_sink.stdout(("Downloading file %q...\n"):format(url))
+ return fetch(url, {
+ out_file = path.concat { ctx.cwd:get(), out_file },
+ }):map_err(function(err)
+ return ("%s\nFailed to download file %q."):format(err, url)
+ end)
+end
+
+---@async
+---@param rel_path string
+---@nodiscard
+local function untar(rel_path)
+ log.fmt_debug("std: untar %s", rel_path)
+ local ctx = installer.context()
+ return ctx.spawn.tar({ "--no-same-owner", "-xvf", rel_path }):on_success(function()
+ pcall(function()
+ ctx.fs:unlink(rel_path)
+ end)
+ end)
+end
+
+---@async
+---@param rel_path string
+---@nodiscard
+local function unzip(rel_path)
+ log.fmt_debug("std: unzip %s", rel_path)
+ local ctx = installer.context()
+ return platform.when {
+ unix = function()
+ return ctx.spawn.unzip({ "-d", ".", rel_path }):on_success(function()
+ pcall(function()
+ ctx.fs:unlink(rel_path)
+ end)
+ end)
+ end,
+ win = function()
+ return Result.pcall(function()
+ -- Expand-Archive seems to be hard-coded to only allow .zip extensions. Bit weird but ok.
+ if not _.matches("%.zip$", rel_path) then
+ local zip_file = ("%s.zip"):format(rel_path)
+ ctx.fs:rename(rel_path, zip_file)
+ return zip_file
+ end
+ return rel_path
+ end):and_then(function(zip_file)
+ return powershell
+ .command(
+ ("Microsoft.PowerShell.Archive\\Expand-Archive -Path %q -DestinationPath ."):format(zip_file),
+ {},
+ ctx.spawn
+ )
+ :on_success(function()
+ pcall(function()
+ ctx.fs:unlink(zip_file)
+ end)
+ end)
+ end)
+ end,
+ }
+end
+
+---@async
+---@param rel_path string
+---@nodiscard
+local function gunzip(rel_path)
+ log.fmt_debug("std: gunzip %s", rel_path)
+ return platform.when {
+ unix = function()
+ return gunzip_unix(rel_path)
+ end,
+ win = function()
+ return win_decompress(rel_path)
+ end,
+ }
+end
+
+---@async
+---@param rel_path string
+---@return Result
+---@nodiscard
+local function untar_compressed(rel_path)
+ log.fmt_debug("std: untar_compressed %s", rel_path)
+ return platform.when {
+ unix = function()
+ return untar(rel_path)
+ end,
+ win = function()
+ return win_decompress(rel_path)
+ :and_then(function()
+ return untar(_.gsub("%.tar%..*$", ".tar", rel_path))
+ end)
+ :or_else(function()
+ -- arc both decompresses and unpacks tar in one go
+ return unpack_arc(rel_path)
+ end)
+ end,
+ }
+end
+
+-- Order is important.
+local unpack_by_filename = _.cond {
+ { _.matches "%.tar$", untar },
+ { _.matches "%.tar%.gz$", untar },
+ { _.matches "%.tar%.bz2$", untar },
+ { _.matches "%.tar%.xz$", untar_compressed },
+ { _.matches "%.tar%.zst$", untar_compressed },
+ { _.matches "%.zip$", unzip },
+ { _.matches "%.vsix$", unzip },
+ { _.matches "%.gz$", gunzip },
+ { _.T, _.compose(Result.success, _.format "%q doesn't need unpacking.") },
+}
+
+---@async
+---@param rel_path string The relative path to the file to unpack.
+---@nodiscard
+function M.unpack(rel_path)
+ log.fmt_debug("std: unpack %s", rel_path)
+ return unpack_by_filename(rel_path)
+end
+
+---@async
+---@param git_url string
+---@param opts? { rev?: string, recursive?: boolean }
+---@nodiscard
+function M.clone(git_url, opts)
+ opts = opts or {}
+ log.fmt_debug("std: clone %s %s", git_url, opts)
+ local ctx = installer.context()
+ return Result.try(function(try)
+ try(ctx.spawn.git {
+ "clone",
+ "--depth",
+ "1",
+ opts.recursive and "--recursive" or vim.NIL,
+ git_url,
+ ".",
+ })
+ if opts.rev then
+ try(ctx.spawn.git { "fetch", "--depth", "1", "origin", opts.rev })
+ try(ctx.spawn.git { "checkout", "FETCH_HEAD" })
+ end
+ end)
+end
+
+return M
diff --git a/lua/mason-core/installer/registry/init.lua b/lua/mason-core/installer/registry/init.lua
new file mode 100644
index 00000000..7c27f1ef
--- /dev/null
+++ b/lua/mason-core/installer/registry/init.lua
@@ -0,0 +1,196 @@
+local a = require "mason-core.async"
+local Result = require "mason-core.result"
+local _ = require "mason-core.functional"
+local Purl = require "mason-core.purl"
+local Optional = require "mason-core.optional"
+local link = require "mason-core.installer.registry.link"
+local log = require "mason-core.log"
+local semver = require "mason-core.semver"
+
+local M = {}
+
+M.SCHEMA_CAP = _.set_of {
+ "registry+v1",
+}
+
+---@type table<string, InstallerProvider>
+local PROVIDERS = {}
+
+---@param id string
+---@param provider InstallerProvider
+function M.register_provider(id, provider)
+ PROVIDERS[id] = provider
+end
+
+M.register_provider("cargo", _.lazy_require "mason-core.installer.registry.providers.cargo")
+M.register_provider("composer", _.lazy_require "mason-core.installer.registry.providers.composer")
+M.register_provider("gem", _.lazy_require "mason-core.installer.registry.providers.gem")
+M.register_provider("generic", _.lazy_require "mason-core.installer.registry.providers.generic")
+M.register_provider("github", _.lazy_require "mason-core.installer.registry.providers.github")
+M.register_provider("golang", _.lazy_require "mason-core.installer.registry.providers.golang")
+M.register_provider("luarocks", _.lazy_require "mason-core.installer.registry.providers.luarocks")
+M.register_provider("npm", _.lazy_require "mason-core.installer.registry.providers.npm")
+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")
+
+---@param purl Purl
+local function get_provider(purl)
+ return Optional.of_nilable(PROVIDERS[purl.type]):ok_or(("Unknown purl type: %s"):format(purl.type))
+end
+
+---@class InstallerProvider
+---@field parse fun(source: RegistryPackageSource, purl: Purl, opts: PackageInstallOpts): Result
+---@field install async fun(ctx: InstallContext, source: ParsedPackageSource): Result
+
+---@class ParsedPackageSource
+
+---Upserts {dst} with contents of {src}. List table values will be merged, with contents of {src} prepended.
+---@param dst table
+---@param src table
+local function upsert(dst, src)
+ for k, v in pairs(src) do
+ if type(v) == "table" then
+ if vim.tbl_islist(v) then
+ dst[k] = _.concat(v, dst[k] or {})
+ else
+ dst[k] = upsert(dst[k] or {}, src[k])
+ end
+ else
+ dst[k] = v
+ end
+ end
+ return dst
+end
+
+---@param source RegistryPackageSource
+---@param version string
+local function coalesce_source(source, version)
+ if 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))
+ if version_type == "semver" then
+ local version_match = Result.try(function(try)
+ local requested_version = try(semver.parse(version))
+ if _.starts_with("<=", constraint) then
+ local rule_version = try(semver.parse(_.strip_prefix("<=", constraint)))
+ return requested_version <= rule_version
+ elseif _.starts_with(">=", constraint) then
+ local rule_version = try(semver.parse(_.strip_prefix(">=", constraint)))
+ return requested_version >= rule_version
+ else
+ local rule_version = try(semver.parse(constraint))
+ return requested_version == rule_version
+ end
+ 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
+ end
+ end
+ end
+ end
+ return source
+end
+
+---@param spec RegistryPackageSpec
+---@param opts PackageInstallOpts
+function M.parse(spec, opts)
+ log.debug("Parsing spec", spec.name, opts)
+ return Result.try(function(try)
+ if not M.SCHEMA_CAP[spec.schema] then
+ return Result.failure(
+ ("Current version of mason.nvim is not capable of parsing package schema version %q."):format(
+ spec.schema
+ )
+ )
+ end
+
+ local source = opts.version and coalesce_source(spec.source, opts.version) or spec.source
+
+ ---@type Purl
+ local purl = try(Purl.parse(source.id))
+ log.trace("Parsed purl.", source.id, purl)
+ if opts.version then
+ purl.version = opts.version
+ end
+
+ ---@type InstallerProvider
+ local provider = try(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)
+ return {
+ provider = provider,
+ source = parsed_source,
+ purl = purl,
+ }
+ end)
+end
+
+---@async
+---@param spec RegistryPackageSpec
+---@param opts PackageInstallOpts
+function M.compile(spec, opts)
+ log.debug("Compiling installer.", spec.name, opts)
+ return Result.try(function(try)
+ if vim.in_fast_event() then
+ -- Parsers run synchronously and may access API functions, so we schedule before-hand.
+ a.scheduler()
+ end
+
+ local map_parse_err = _.cond {
+ {
+ _.equals "PLATFORM_UNSUPPORTED",
+ function()
+ if opts.target then
+ return ("Platform %q is unsupported."):format(opts.target)
+ else
+ return "The current platform is unsupported."
+ end
+ end,
+ },
+ { _.T, _.identity },
+ }
+
+ ---@type { purl: Purl, provider: InstallerProvider, source: ParsedPackageSource }
+ local parsed = try(M.parse(spec, opts):map_err(map_parse_err))
+
+ ---@async
+ ---@param ctx InstallContext
+ return function(ctx)
+ return Result.try(function(try)
+ -- Run installer
+ try(parsed.provider.install(ctx, parsed.source))
+
+ -- Expand & register links
+ if spec.bin then
+ try(link.bin(ctx, spec, parsed.purl, parsed.source))
+ end
+ if spec.share then
+ try(link.share(ctx, spec, parsed.purl, parsed.source))
+ end
+ if spec.opt then
+ try(link.opt(ctx, spec, parsed.purl, parsed.source))
+ end
+
+ ctx.receipt:with_primary_source {
+ type = ctx.package.spec.schema,
+ id = Purl.compile(parsed.purl),
+ source = parsed.source,
+ }
+ end):on_failure(function(err)
+ error(err, 0)
+ end)
+ end
+ end)
+end
+
+return M
diff --git a/lua/mason-core/installer/registry/link.lua b/lua/mason-core/installer/registry/link.lua
new file mode 100644
index 00000000..d66809e0
--- /dev/null
+++ b/lua/mason-core/installer/registry/link.lua
@@ -0,0 +1,300 @@
+local expr = require "mason-core.installer.registry.expr"
+local log = require "mason-core.log"
+local _ = require "mason-core.functional"
+local path = require "mason-core.path"
+local platform = require "mason-core.platform"
+local fs = require "mason-core.fs"
+local Result = require "mason-core.result"
+local Optional = require "mason-core.optional"
+local a = require "mason-core.async"
+
+local M = {}
+
+local filter_empty_values = _.compose(
+ _.from_pairs,
+ _.filter(function(pair)
+ return pair[2] ~= ""
+ end),
+ _.to_pairs
+)
+
+local bin_delegates = {
+ ["luarocks"] = function(target)
+ return require("mason-core.installer.managers.luarocks").bin_path(target)
+ end,
+ ["composer"] = function(target)
+ return require("mason-core.installer.managers.composer").bin_path(target)
+ end,
+ ["opam"] = function(target)
+ return require("mason-core.installer.managers.opam").bin_path(target)
+ end,
+ ["python"] = function(target, bin)
+ local installer = require "mason-core.installer"
+ local ctx = installer.context()
+ if not ctx.fs:file_exists(target) then
+ return Result.failure(("Cannot write python wrapper for path %q as it doesn't exist."):format(target))
+ end
+ return Result.pcall(function()
+ 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 })
+ )
+ end)
+ end,
+ ["php"] = function(target, bin)
+ local installer = require "mason-core.installer"
+ local ctx = installer.context()
+ return Result.pcall(function()
+ return ctx:write_php_exec_wrapper(bin, target)
+ end)
+ end,
+ ["pyvenv"] = function(target, bin)
+ local installer = require "mason-core.installer"
+ local ctx = installer.context()
+ return Result.pcall(function()
+ return ctx:write_pyvenv_exec_wrapper(bin, target)
+ end)
+ end,
+ ["dotnet"] = function(target, bin)
+ local installer = require "mason-core.installer"
+ local ctx = installer.context()
+ if not ctx.fs:file_exists(target) then
+ return Result.failure(("Cannot write dotnet wrapper for path %q as it doesn't exist."):format(target))
+ end
+ return Result.pcall(function()
+ return ctx:write_shell_exec_wrapper(
+ bin,
+ ("dotnet %q"):format(path.concat {
+ ctx.package:get_install_path(),
+ target,
+ })
+ )
+ end)
+ end,
+ ["node"] = function(target, bin)
+ local installer = require "mason-core.installer"
+ local ctx = installer.context()
+ return Result.pcall(function()
+ return ctx:write_node_exec_wrapper(bin, target)
+ end)
+ end,
+ ["exec"] = function(target, bin)
+ local installer = require "mason-core.installer"
+ local ctx = installer.context()
+ return Result.pcall(function()
+ return ctx:write_exec_wrapper(bin, target)
+ end)
+ end,
+ ["java-jar"] = function(target, bin)
+ local installer = require "mason-core.installer"
+ local ctx = installer.context()
+ if not ctx.fs:file_exists(target) then
+ return Result.failure(("Cannot write Java JAR wrapper for path %q as it doesn't exist."):format(target))
+ end
+ return Result.pcall(function()
+ return ctx:write_shell_exec_wrapper(
+ bin,
+ ("java -jar %q"):format(path.concat {
+ ctx.package:get_install_path(),
+ target,
+ })
+ )
+ end)
+ end,
+ ["nuget"] = function(target)
+ return require("mason-core.installer.managers.nuget").bin_path(target)
+ end,
+ ["npm"] = function(target)
+ return require("mason-core.installer.managers.npm").bin_path(target)
+ end,
+ ["gem"] = function(target)
+ return require("mason-core.installer.managers.gem").create_bin_wrapper(target)
+ end,
+ ["cargo"] = function(target)
+ return require("mason-core.installer.managers.cargo").bin_path(target)
+ end,
+ ["pypi"] = function(target)
+ return require("mason-core.installer.managers.pypi").bin_path(target)
+ end,
+ ["golang"] = function(target)
+ return require("mason-core.installer.managers.golang").bin_path(target)
+ end,
+}
+
+---@async
+---@param ctx InstallContext
+---@param target string
+local function chmod_exec(ctx, target)
+ local bit = require "bit"
+ -- see chmod(2)
+ local USR_EXEC = 0x40
+ local GRP_EXEC = 0x8
+ local ALL_EXEC = 0x1
+ local EXEC = bit.bor(USR_EXEC, GRP_EXEC, ALL_EXEC)
+ local fstat = ctx.fs:fstat(target)
+ if bit.band(fstat.mode, EXEC) ~= EXEC then
+ local plus_exec = bit.bor(fstat.mode, EXEC)
+ log.fmt_debug("Setting exec flags on file %s %o -> %o", target, fstat.mode, plus_exec)
+ ctx.fs:chmod(target, plus_exec) -- chmod +x
+ end
+end
+
+---Expands bin specification from spec and registers bins to be linked.
+---@async
+---@param ctx InstallContext
+---@param spec RegistryPackageSpec
+---@param purl Purl
+---@param source ParsedPackageSource
+local function expand_bin(ctx, spec, purl, source)
+ log.debug("Registering bin links", ctx.package, spec.bin)
+ return Result.try(function(try)
+ local expr_ctx = { version = purl.version, source = source }
+
+ local bin_table = spec.bin
+ if not bin_table then
+ log.fmt_debug("%s spec provides no bin.", ctx.package)
+ return
+ end
+
+ local interpolated_bins = filter_empty_values(try(expr.tbl_interpolate(bin_table, expr_ctx)))
+
+ local expanded_bin_table = {}
+ for bin, target in pairs(interpolated_bins) do
+ -- Expand "npm:typescript-language-server"-like expressions
+ local delegated_bin = _.match("^(.+):(.+)$", target)
+ if #delegated_bin > 0 then
+ local bin_type, executable = unpack(delegated_bin)
+ log.fmt_trace("Transforming managed executable=%s via %s", executable, bin_type)
+ local delegate =
+ try(Optional.of_nilable(bin_delegates[bin_type]):ok_or(("Unknown bin type: %s"):format(bin_type)))
+ target = try(delegate(executable, bin))
+ end
+
+ log.fmt_debug("Expanded bin link %s -> %s", bin, target)
+ if not ctx.fs:file_exists(target) then
+ return Result.failure(("Tried to link bin %q to non-existent target %q."):format(bin, target))
+ end
+
+ if platform.is.unix then
+ chmod_exec(ctx, target)
+ end
+
+ expanded_bin_table[bin] = target
+ end
+ return expanded_bin_table
+ end)
+end
+
+local is_dir_path = _.matches "/$"
+
+---Expands symlink path specifications from spec and returns symlink file table.
+---@async
+---@param ctx InstallContext
+---@param purl Purl
+---@param source ParsedPackageSource
+---@param file_spec_table table<string, string>
+local function expand_file_spec(ctx, purl, source, file_spec_table)
+ log.debug("Registering symlinks", ctx.package, file_spec_table)
+ return Result.try(function(try)
+ local expr_ctx = { version = purl.version, source = source }
+
+ ---@type table<string, string>
+ local interpolated_paths = filter_empty_values(try(expr.tbl_interpolate(file_spec_table, expr_ctx)))
+
+ ---@type table<string, string>
+ local expanded_links = {}
+
+ for dest, source_path in pairs(interpolated_paths) do
+ local cwd = ctx.cwd:get()
+
+ if is_dir_path(dest) then
+ -- linking dir -> dir
+ if not is_dir_path(source_path) then
+ return Result.failure(("Cannot link file %q to dir %q."):format(source_path, dest))
+ end
+
+ if vim.in_fast_event() then
+ a.scheduler()
+ end
+
+ local glob = path.concat { cwd, source_path } .. "**/*"
+ log.fmt_trace("Symlink glob for %s: %s", ctx.package, glob)
+
+ ---@type string[]
+ local files = _.filter_map(function(abs_path)
+ -- fs.sync because async causes stack overflow on many files (TODO fix that)
+ if not fs.sync.file_exists(abs_path) then
+ -- only link actual files (e.g. exclude directory entries from glob)
+ return Optional.empty()
+ end
+ -- turn into relative paths
+ return Optional.of(abs_path:sub(#cwd + 2)) -- + 2 to remove leading path separator (/)
+ end, vim.fn.glob(glob, false, true))
+
+ log.fmt_trace("Expanded glob %s: %s", glob, files)
+
+ for __, file in ipairs(files) do
+ -- File destination should be relative to the source directory. For example, should the source_path
+ -- be "gh_2.22.1_macOS_amd64/share/man/" and dest be "man/", it should link source files to the
+ -- following destinations:
+ --
+ -- gh_2.22.1_macOS_amd64/share/man/ man/
+ -- -------------------------------------------------------------------------
+ -- gh_2.22.1_macOS_amd64/share/man/man1/gh.1 man/man1/gh.1
+ -- gh_2.22.1_macOS_amd64/share/man/man1/gh-run.1 man/man1/gh-run.1
+ -- gh_2.22.1_macOS_amd64/share/man/man1/gh-ssh-key.1 man/man1/gh-run.1
+ --
+ local file_dest = path.concat {
+ _.trim_end_matches("/", dest),
+ file:sub(#source_path + 1),
+ }
+ expanded_links[file_dest] = file
+ end
+ else
+ -- linking file -> file
+ if is_dir_path(source_path) then
+ return Result.failure(("Cannot link dir %q to file %q."):format(source_path, dest))
+ end
+ expanded_links[dest] = source_path
+ end
+ end
+
+ return expanded_links
+ end)
+end
+
+---@async
+---@param ctx InstallContext
+---@param spec RegistryPackageSpec
+---@param purl Purl
+---@param source ParsedPackageSource
+M.bin = function(ctx, spec, purl, source)
+ return expand_bin(ctx, spec, purl, source):on_success(function(links)
+ ctx.links.bin = links
+ end)
+end
+
+---@async
+---@param ctx InstallContext
+---@param spec RegistryPackageSpec
+---@param purl Purl
+---@param source ParsedPackageSource
+M.share = function(ctx, spec, purl, source)
+ return expand_file_spec(ctx, purl, source, spec.share):on_success(function(links)
+ ctx.links.share = links
+ end)
+end
+
+---@async
+---@param ctx InstallContext
+---@param spec RegistryPackageSpec
+---@param purl Purl
+---@param source ParsedPackageSource
+M.opt = function(ctx, spec, purl, source)
+ return expand_file_spec(ctx, purl, source, spec.opt):on_success(function(links)
+ ctx.links.opt = links
+ end)
+end
+
+return M
diff --git a/lua/mason-core/installer/registry/providers/cargo.lua b/lua/mason-core/installer/registry/providers/cargo.lua
new file mode 100644
index 00000000..4c609be6
--- /dev/null
+++ b/lua/mason-core/installer/registry/providers/cargo.lua
@@ -0,0 +1,64 @@
+local Result = require "mason-core.result"
+local _ = require "mason-core.functional"
+local util = require "mason-core.installer.registry.util"
+
+local M = {}
+
+---@class CargoSource : RegistryPackageSource
+---@field supported_platforms? string[]
+
+---@param source CargoSource
+---@param purl Purl
+function M.parse(source, purl)
+ return Result.try(function(try)
+ if source.supported_platforms then
+ try(util.ensure_valid_platform(source.supported_platforms))
+ end
+
+ local repository_url = _.path({ "qualifiers", "repository_url" }, purl)
+
+ local git
+ if repository_url then
+ git = {
+ url = repository_url,
+ rev = _.path({ "qualifiers", "rev" }, purl) == "true",
+ }
+ end
+
+ ---@type string?
+ local features = _.path({ "qualifiers", "features" }, purl)
+ local locked = _.path({ "qualifiers", "locked" }, purl)
+
+ ---@class ParsedCargoSource : ParsedPackageSource
+ local parsed_source = {
+ crate = purl.name,
+ version = purl.version,
+ features = features,
+ locked = locked ~= "false",
+ git = git,
+ }
+ return parsed_source
+ end)
+end
+
+---@async
+---@param ctx InstallContext
+---@param source ParsedCargoSource
+function M.install(ctx, source)
+ local cargo = require "mason-core.installer.managers.cargo"
+ local providers = require "mason-core.providers"
+
+ return Result.try(function(try)
+ try(util.ensure_valid_version(function()
+ return providers.crates.get_all_versions(source.crate)
+ end))
+
+ try(cargo.install(source.crate, source.version, {
+ git = source.git,
+ features = source.features,
+ locked = source.locked,
+ }))
+ end)
+end
+
+return M
diff --git a/lua/mason-core/installer/registry/providers/composer.lua b/lua/mason-core/installer/registry/providers/composer.lua
new file mode 100644
index 00000000..59c0ae61
--- /dev/null
+++ b/lua/mason-core/installer/registry/providers/composer.lua
@@ -0,0 +1,34 @@
+local Result = require "mason-core.result"
+local util = require "mason-core.installer.registry.util"
+
+local M = {}
+
+---@param source RegistryPackageSource
+---@param purl Purl
+function M.parse(source, purl)
+ ---@class ParsedComposerSource : ParsedPackageSource
+ local parsed_source = {
+ package = ("%s/%s"):format(purl.namespace, purl.name),
+ version = purl.version,
+ }
+
+ return Result.success(parsed_source)
+end
+
+---@async
+---@param ctx InstallContext
+---@param source ParsedComposerSource
+function M.install(ctx, source)
+ local composer = require "mason-core.installer.managers.composer"
+ local providers = require "mason-core.providers"
+
+ return Result.try(function(try)
+ try(util.ensure_valid_version(function()
+ return providers.packagist.get_all_versions(source.package)
+ end))
+
+ try(composer.install(source.package, source.version))
+ end)
+end
+
+return M
diff --git a/lua/mason-core/installer/registry/providers/gem.lua b/lua/mason-core/installer/registry/providers/gem.lua
new file mode 100644
index 00000000..ba829b9a
--- /dev/null
+++ b/lua/mason-core/installer/registry/providers/gem.lua
@@ -0,0 +1,47 @@
+local _ = require "mason-core.functional"
+local Result = require "mason-core.result"
+local util = require "mason-core.installer.registry.util"
+
+local M = {}
+
+---@class GemSource : RegistryPackageSource
+---@field supported_platforms? string[]
+---@field extra_packages? string[]
+
+---@param source GemSource
+---@param purl Purl
+function M.parse(source, purl)
+ return Result.try(function(try)
+ if source.supported_platforms then
+ try(util.ensure_valid_platform(source.supported_platforms))
+ end
+
+ ---@class ParsedGemSource : ParsedPackageSource
+ local parsed_source = {
+ package = purl.name,
+ version = purl.version,
+ extra_packages = source.extra_packages,
+ }
+ return parsed_source
+ end)
+end
+
+---@async
+---@param ctx InstallContext
+---@param source GemSource
+function M.install(ctx, source)
+ local gem = require "mason-core.installer.managers.gem"
+ local providers = require "mason-core.providers"
+
+ return Result.try(function(try)
+ try(util.ensure_valid_version(function()
+ return providers.rubygems.get_all_versions(source.package)
+ end))
+
+ try(gem.install(source.package, source.version, {
+ extra_packages = source.extra_packages,
+ }))
+ end)
+end
+
+return M
diff --git a/lua/mason-core/installer/registry/providers/generic.lua b/lua/mason-core/installer/registry/providers/generic.lua
new file mode 100644
index 00000000..5c493ae6
--- /dev/null
+++ b/lua/mason-core/installer/registry/providers/generic.lua
@@ -0,0 +1,47 @@
+local _ = require "mason-core.functional"
+local Result = require "mason-core.result"
+local expr = require "mason-core.installer.registry.expr"
+local util = require "mason-core.installer.registry.util"
+
+local M = {}
+
+---@class GenericDownload
+---@field target (Platform | Platform[])?
+---@field files table<string, string>
+
+---@class GenericSource : RegistryPackageSource
+---@field download GenericDownload | GenericDownload[]
+
+---@param source GenericSource
+---@param purl Purl
+---@param opts PackageInstallOpts
+function M.parse(source, purl, opts)
+ return Result.try(function(try)
+ local download = try(util.coalesce_by_target(source.download, opts):ok_or "PLATFORM_UNSUPPORTED")
+
+ local expr_ctx = { version = purl.version }
+ ---@type { files: table<string, string> }
+ local interpolated_download = try(expr.tbl_interpolate(download, expr_ctx))
+
+ ---@class ParsedGenericSource : ParsedPackageSource
+ local parsed_source = {
+ download = interpolated_download,
+ }
+ return parsed_source
+ end)
+end
+
+---@async
+---@param ctx InstallContext
+---@param source ParsedGenericSource
+function M.install(ctx, source)
+ local std = require "mason-core.installer.managers.std"
+ return Result.try(function(try)
+ for out_file, url in pairs(source.download.files) do
+ try(std.download_file(url, out_file))
+ try(std.unpack(out_file))
+ end
+ end)
+end
+
+return M
diff --git a/lua/mason-core/installer/registry/providers/github.lua b/lua/mason-core/installer/registry/providers/github.lua
new file mode 100644
index 00000000..2dda6fe6
--- /dev/null
+++ b/lua/mason-core/installer/registry/providers/github.lua
@@ -0,0 +1,179 @@
+local a = require "mason-core.async"
+local async_uv = require "mason-core.async.uv"
+local Result = require "mason-core.result"
+local _ = require "mason-core.functional"
+local platform = require "mason-core.platform"
+local path = require "mason-core.path"
+local settings = require "mason.settings"
+local expr = require "mason-core.installer.registry.expr"
+local util = require "mason-core.installer.registry.util"
+
+local build = {
+ ---@param source GitHubBuildSource
+ ---@param purl Purl
+ ---@param opts PackageInstallOpts
+ parse = function(source, purl, opts)
+ return Result.try(function(try)
+ ---@type { run: string }
+ local build_instruction = try(util.coalesce_by_target(source.build, opts):ok_or "PLATFORM_UNSUPPORTED")
+
+ ---@class ParsedGitHubBuildSource : ParsedPackageSource
+ local parsed_source = {
+ build = build_instruction,
+ repo = ("https://github.com/%s/%s.git"):format(purl.namespace, purl.name),
+ rev = purl.version,
+ }
+ return parsed_source
+ end)
+ end,
+
+ ---@async
+ ---@param ctx InstallContext
+ ---@param source ParsedGitHubBuildSource
+ install = function(ctx, source)
+ local std = require "mason-core.installer.managers.std"
+ return Result.try(function(try)
+ try(std.clone(source.repo, { rev = source.rev }))
+ try(platform.when {
+ unix = function()
+ return ctx.spawn.bash {
+ on_spawn = a.scope(function(_, stdio)
+ local stdin = stdio[1]
+ async_uv.write(stdin, "set -euxo pipefail;\n")
+ async_uv.write(stdin, source.build.run)
+ async_uv.shutdown(stdin)
+ async_uv.close(stdin)
+ end),
+ }
+ end,
+ win = function()
+ local powershell = require "mason-core.managers.powershell"
+ return powershell.command(source.build.run, {}, ctx.spawn)
+ end,
+ })
+ end)
+ end,
+}
+
+local release = {
+ ---@param source GitHubReleaseSource
+ ---@param purl Purl
+ ---@param opts PackageInstallOpts
+ parse = function(source, purl, opts)
+ return Result.try(function(try)
+ local asset = try(util.coalesce_by_target(source.asset, opts):ok_or "PLATFORM_UNSUPPORTED")
+
+ local expr_ctx = { version = purl.version }
+
+ ---@type { out_file: string, download_url: string }[]
+ local downloads = {}
+
+ for __, file in ipairs(type(asset.file) == "string" and { asset.file } or asset.file) do
+ local asset_file_components = _.split(":", file)
+ local source_file = try(expr.interpolate(_.head(asset_file_components), expr_ctx))
+ local out_file = try(expr.interpolate(_.last(asset_file_components), expr_ctx))
+
+ if _.matches("/$", out_file) then
+ -- out_file is a dir expression (e.g. "libexec/")
+ out_file = out_file .. source_file
+ end
+
+ table.insert(downloads, {
+ out_file = out_file,
+ download_url = settings.current.github.download_url_template:format(
+ ("%s/%s"):format(purl.namespace, purl.name),
+ purl.version,
+ source_file
+ ),
+ })
+ end
+
+ local interpolated_asset = try(expr.tbl_interpolate(asset, expr_ctx))
+
+ ---@class ParsedGitHubReleaseSource : ParsedPackageSource
+ local parsed_source = {
+ repo = ("%s/%s"):format(purl.namespace, purl.name),
+ asset = interpolated_asset,
+ downloads = downloads,
+ }
+ return parsed_source
+ end)
+ end,
+
+ ---@async
+ ---@param ctx InstallContext
+ ---@param source ParsedGitHubReleaseSource
+ install = function(ctx, source)
+ local std = require "mason-core.installer.managers.std"
+ local providers = require "mason-core.providers"
+
+ return Result.try(function(try)
+ try(util.ensure_valid_version(function()
+ return providers.github.get_all_release_versions(source.repo)
+ end))
+
+ for __, download in ipairs(source.downloads) do
+ if vim.in_fast_event() then
+ a.scheduler()
+ end
+ local out_dir = vim.fn.fnamemodify(download.out_file, ":h")
+ local out_file = vim.fn.fnamemodify(download.out_file, ":t")
+ if out_dir ~= "." then
+ try(Result.pcall(function()
+ ctx.fs:mkdirp(out_dir)
+ end))
+ end
+ try(ctx:chdir(out_dir, function()
+ return Result.try(function(try)
+ try(std.download_file(download.download_url, out_file))
+ try(std.unpack(out_file))
+ end)
+ end))
+ end
+ end)
+ end,
+}
+
+local M = {}
+
+---@class GitHubReleaseAsset
+---@field target? Platform | Platform[]
+---@field file string | string[]
+
+---@class GitHubReleaseSource : RegistryPackageSource
+---@field asset GitHubReleaseAsset | GitHubReleaseAsset[]
+
+---@class GitHubBuildInstruction
+---@field target? Platform | Platform[]
+---@field run string
+
+---@class GitHubBuildSource : RegistryPackageSource
+---@field build GitHubBuildInstruction | GitHubBuildInstruction[]
+
+---@param source GitHubReleaseSource | GitHubBuildSource
+---@param purl Purl
+---@param opts PackageInstallOpts
+function M.parse(source, purl, opts)
+ if source.asset then
+ return release.parse(source --[[@as GitHubReleaseSource]], purl, opts)
+ elseif source.build then
+ return build.parse(source --[[@as GitHubBuildSource]], purl, opts)
+ else
+ return Result.failure "Unknown source type."
+ end
+end
+
+---@async
+---@param ctx InstallContext
+---@param source ParsedGitHubReleaseSource | ParsedGitHubBuildSource
+function M.install(ctx, source)
+ if source.asset then
+ return release.install(ctx, source)
+ elseif source.build then
+ return build.install(ctx, source)
+ else
+ return Result.failure "Unknown source type."
+ end
+end
+
+return M
diff --git a/lua/mason-core/installer/registry/providers/golang.lua b/lua/mason-core/installer/registry/providers/golang.lua
new file mode 100644
index 00000000..34d8d160
--- /dev/null
+++ b/lua/mason-core/installer/registry/providers/golang.lua
@@ -0,0 +1,50 @@
+local Result = require "mason-core.result"
+local _ = require "mason-core.functional"
+local util = require "mason-core.installer.registry.util"
+
+local M = {}
+
+---@param purl Purl
+local function get_package_name(purl)
+ if purl.subpath then
+ return ("%s/%s/%s"):format(purl.namespace, purl.name, purl.subpath)
+ else
+ return ("%s/%s"):format(purl.namespace, purl.name)
+ end
+end
+
+---@class GolangSource : RegistryPackageSource
+---@field extra_packages? string[]
+
+---@param source GolangSource
+---@param purl Purl
+function M.parse(source, purl)
+ ---@class ParsedGolangSource : ParsedPackageSource
+ local parsed_source = {
+ package = get_package_name(purl),
+ version = purl.version,
+ extra_packages = source.extra_packages,
+ }
+
+ return Result.success(parsed_source)
+end
+
+---@async
+---@param ctx InstallContext
+---@param source ParsedGolangSource
+function M.install(ctx, source)
+ local golang = require "mason-core.installer.managers.golang"
+ local providers = require "mason-core.providers"
+
+ return Result.try(function(try)
+ try(util.ensure_valid_version(function()
+ return providers.golang.get_all_versions(source.package)
+ end))
+
+ try(golang.install(source.package, source.version, {
+ extra_packages = source.extra_packages,
+ }))
+ end)
+end
+
+return M
diff --git a/lua/mason-core/installer/registry/providers/luarocks.lua b/lua/mason-core/installer/registry/providers/luarocks.lua
new file mode 100644
index 00000000..78b0fc7f
--- /dev/null
+++ b/lua/mason-core/installer/registry/providers/luarocks.lua
@@ -0,0 +1,45 @@
+local Result = require "mason-core.result"
+local _ = require "mason-core.functional"
+
+local M = {}
+
+---@param purl Purl
+local function parse_package_name(purl)
+ if purl.namespace then
+ return ("%s/%s"):format(purl.namespace, purl.name)
+ else
+ return purl.name
+ end
+end
+
+local parse_server = _.path { "qualifiers", "repository_url" }
+local parse_dev = _.compose(_.equals "true", _.path { "qualifiers", "dev" })
+
+---@param source RegistryPackageSource
+---@param purl Purl
+function M.parse(source, purl)
+ ---@class ParsedLuaRocksSource : ParsedPackageSource
+ local parsed_source = {
+ package = parse_package_name(purl),
+ version = purl.version,
+ ---@type string?
+ server = parse_server(purl),
+ ---@type boolean?
+ dev = parse_dev(purl),
+ }
+
+ return Result.success(parsed_source)
+end
+
+---@async
+---@param ctx InstallContext
+---@param source ParsedLuaRocksSource
+function M.install(ctx, source)
+ local luarocks = require "mason-core.installer.managers.luarocks"
+ return luarocks.install(source.package, source.version, {
+ server = source.server,
+ dev = source.dev,
+ })
+end
+
+return M
diff --git a/lua/mason-core/installer/registry/providers/npm.lua b/lua/mason-core/installer/registry/providers/npm.lua
new file mode 100644
index 00000000..4b14c084
--- /dev/null
+++ b/lua/mason-core/installer/registry/providers/npm.lua
@@ -0,0 +1,51 @@
+local Result = require "mason-core.result"
+local _ = require "mason-core.functional"
+local util = require "mason-core.installer.registry.util"
+
+---@param purl Purl
+local function purl_to_npm(purl)
+ if purl.namespace then
+ return ("%s/%s"):format(purl.namespace, purl.name)
+ else
+ return purl.name
+ end
+end
+
+local M = {}
+
+---@class NpmSource : RegistryPackageSource
+---@field extra_packages? string[]
+
+---@param source NpmSource
+---@param purl Purl
+function M.parse(source, purl)
+ ---@class ParsedNpmSource : ParsedPackageSource
+ local parsed_source = {
+ package = purl_to_npm(purl),
+ version = purl.version,
+ extra_packages = source.extra_packages,
+ }
+
+ return Result.success(parsed_source)
+end
+
+---@async
+---@param ctx InstallContext
+---@param source ParsedNpmSource
+function M.install(ctx, source)
+ local npm = require "mason-core.installer.managers.npm"
+ local providers = require "mason-core.providers"
+
+ return Result.try(function(try)
+ try(util.ensure_valid_version(function()
+ return providers.npm.get_all_versions(source.package)
+ end))
+
+ try(npm.init())
+ try(npm.install(source.package, source.version, {
+ extra_packages = source.extra_packages,
+ }))
+ end)
+end
+
+return M
diff --git a/lua/mason-core/installer/registry/providers/nuget.lua b/lua/mason-core/installer/registry/providers/nuget.lua
new file mode 100644
index 00000000..55bc689d
--- /dev/null
+++ b/lua/mason-core/installer/registry/providers/nuget.lua
@@ -0,0 +1,25 @@
+local Result = require "mason-core.result"
+
+local M = {}
+
+---@param source RegistryPackageSource
+---@param purl Purl
+function M.parse(source, purl)
+ ---@class ParsedNugetSource : ParsedPackageSource
+ local parsed_source = {
+ package = purl.name,
+ version = purl.version,
+ }
+
+ return Result.success(parsed_source)
+end
+
+---@async
+---@param ctx InstallContext
+---@param source ParsedNugetSource
+function M.install(ctx, source)
+ local nuget = require "mason-core.installer.managers.nuget"
+ return nuget.install(source.package, source.version)
+end
+
+return M
diff --git a/lua/mason-core/installer/registry/providers/opam.lua b/lua/mason-core/installer/registry/providers/opam.lua
new file mode 100644
index 00000000..78608e85
--- /dev/null
+++ b/lua/mason-core/installer/registry/providers/opam.lua
@@ -0,0 +1,25 @@
+local Result = require "mason-core.result"
+
+local M = {}
+
+---@param source RegistryPackageSource
+---@param purl Purl
+function M.parse(source, purl)
+ ---@class ParsedOpamSource : ParsedPackageSource
+ local parsed_source = {
+ package = purl.name,
+ version = purl.version,
+ }
+
+ return Result.success(parsed_source)
+end
+
+---@async
+---@param ctx InstallContext
+---@param source ParsedOpamSource
+function M.install(ctx, source)
+ local opam = require "mason-core.installer.managers.opam"
+ return opam.install(source.package, source.version)
+end
+
+return M
diff --git a/lua/mason-core/installer/registry/providers/pypi.lua b/lua/mason-core/installer/registry/providers/pypi.lua
new file mode 100644
index 00000000..8281d07e
--- /dev/null
+++ b/lua/mason-core/installer/registry/providers/pypi.lua
@@ -0,0 +1,59 @@
+local Result = require "mason-core.result"
+local _ = require "mason-core.functional"
+local settings = require "mason.settings"
+local util = require "mason-core.installer.registry.util"
+
+local M = {}
+
+---@class PypiSource : RegistryPackageSource
+---@field extra_packages? string[]
+---@field supported_platforms? string[]
+
+---@param source PypiSource
+---@param purl Purl
+function M.parse(source, purl)
+ return Result.try(function(try)
+ if source.supported_platforms then
+ try(util.ensure_valid_platform(source.supported_platforms))
+ end
+
+ ---@class ParsedPypiSource : ParsedPackageSource
+ local parsed_source = {
+ package = purl.name,
+ version = purl.version,
+ extra = _.path({ "qualifiers", "extra" }, purl),
+ extra_packages = source.extra_packages,
+ pip = {
+ upgrade = settings.current.pip.upgrade_pip,
+ extra_args = settings.current.pip.install_args,
+ },
+ }
+
+ return parsed_source
+ end)
+end
+
+---@async
+---@param ctx InstallContext
+---@param source ParsedPypiSource
+function M.install(ctx, source)
+ local pypi = require "mason-core.installer.managers.pypi"
+ local providers = require "mason-core.providers"
+
+ return Result.try(function(try)
+ try(util.ensure_valid_version(function()
+ return providers.pypi.get_all_versions(source.package)
+ end))
+
+ try(pypi.init {
+ upgrade_pip = source.pip.upgrade,
+ install_extra_args = source.pip.extra_args,
+ })
+ try(pypi.install(source.package, source.version, {
+ extra = source.extra,
+ extra_packages = source.extra_packages,
+ }))
+ end)
+end
+
+return M
diff --git a/lua/mason-core/installer/registry/util.lua b/lua/mason-core/installer/registry/util.lua
new file mode 100644
index 00000000..e49b5b82
--- /dev/null
+++ b/lua/mason-core/installer/registry/util.lua
@@ -0,0 +1,83 @@
+local _ = require "mason-core.functional"
+local installer = require "mason-core.installer"
+local Optional = require "mason-core.optional"
+local platform = require "mason-core.platform"
+local Result = require "mason-core.result"
+local log = require "mason-core.log"
+
+local M = {}
+
+---@generic T : { target: Platform | Platform[] }
+---@param candidates T[] | T
+---@param opts PackageInstallOpts
+---@return Optional # Optional<T>
+function M.coalesce_by_target(candidates, opts)
+ if not vim.tbl_islist(candidates) then
+ return Optional.of(candidates)
+ end
+ return Optional.of_nilable(_.find_first(function(asset)
+ if opts.target then
+ -- Matching against a provided target rather than the current platform is an escape hatch primarily meant
+ -- for automated testing purposes.
+ if type(asset.target) == "table" then
+ return _.any(_.equals(opts.target), asset.target)
+ else
+ return asset.target == opts.target
+ end
+ else
+ if type(asset.target) == "table" then
+ return _.any(function(target)
+ return platform.is[target]
+ end, asset.target)
+ else
+ return platform.is[asset.target]
+ end
+ end
+ end, candidates))
+end
+
+---Checks whether a custom version of a package installation corresponds to a valid version.
+---@async
+---@param versions_thunk async fun(): Result Result<string>
+function M.ensure_valid_version(versions_thunk)
+ local ctx = installer.context()
+ local version = ctx.opts.version
+
+ if version and not ctx.opts.force then
+ ctx.stdio_sink.stdout "Fetching available versions…\n"
+ local all_versions = versions_thunk()
+ if all_versions:is_failure() then
+ log.warn("Failed to fetch versions for package %s", ctx.package)
+ -- Gracefully fail (i.e. optimistically continue package installation)
+ return Result.success()
+ end
+ all_versions = all_versions:get_or_else {}
+
+ if not _.any(_.equals(version), all_versions) then
+ ctx.stdio_sink.stderr(("Tried to install invalid version %q. Available versions:\n"):format(version))
+ ctx.stdio_sink.stderr(_.compose(_.join "\n", _.map(_.join ", "), _.split_every(15))(all_versions))
+ ctx.stdio_sink.stderr "\n\n"
+ ctx.stdio_sink.stderr(
+ ("Run with --force flag to bypass version validation:\n :MasonInstall --force %s@%s\n\n"):format(
+ ctx.package.name,
+ version
+ )
+ )
+ return Result.failure(("Version %q is not available."):format(version))
+ end
+ end
+
+ return Result.success()
+end
+
+---@param platforms string[]
+function M.ensure_valid_platform(platforms)
+ if not _.any(function(target)
+ return platform.is[target]
+ end, platforms) then
+ return Result.failure "PLATFORM_UNSUPPORTED"
+ end
+ return Result.success()
+end
+
+return M
diff --git a/lua/mason-core/package/init.lua b/lua/mason-core/package/init.lua
index 88e89c41..16250ead 100644
--- a/lua/mason-core/package/init.lua
+++ b/lua/mason-core/package/init.lua
@@ -6,10 +6,16 @@ local log = require "mason-core.log"
local EventEmitter = require "mason-core.EventEmitter"
local fs = require "mason-core.fs"
local path = require "mason-core.path"
+local Result = require "mason-core.result"
+local Purl = require "mason-core.purl"
+
+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 PackageSpec
+---@field spec RegistryPackageSpec | PackageSpec
---@field private handle InstallHandle The currently associated handle.
local Package = setmetatable({}, { __index = EventEmitter })
@@ -50,16 +56,52 @@ local PackageMt = { __index = Package }
---@field languages PackageLanguage[]
---@field install async fun(ctx: InstallContext)
----@param spec PackageSpec
+---@class RegistryPackageSourceVersionOverride : RegistryPackageSource
+---@field constraint string
+
+---@class RegistryPackageSource
+---@field id string PURL-compliant identifier.
+---@field version_overrides? RegistryPackageSourceVersionOverride[]
+
+---@class RegistryPackageSpec
+---@field schema '"registry+v1"'
+---@field name string
+---@field description string
+---@field homepage string
+---@field licenses string[]
+---@field languages string[]
+---@field categories string[]
+---@field source RegistryPackageSource
+---@field bin table<string, string>?
+---@field share table<string, string>?
+---@field opt table<string, string>?
+
+---@param spec PackageSpec | RegistryPackageSpec
function Package.new(spec)
- 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" },
- }
+ 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" } },
+ }
+ -- XXX: this is for compatibilty with the PackageSpec structure
+ spec.desc = spec.description
+ 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
return EventEmitter.init(setmetatable({
name = spec.name, -- for convenient access
@@ -190,20 +232,65 @@ end
---@param callback fun(success: boolean, version_or_err: string)
function Package:get_installed_version(callback)
- 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_installed_version(receipt, self:get_install_path()):get_or_throw()
- end, callback)
+ self:get_receipt()
+ :if_present(
+ ---@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
+ end
+ )
+ :if_not_present(function()
+ callback(false, "Unable to get receipt.")
+ end)
end
---@param callback fun(success: boolean, result_or_err: NewPackageVersion)
function Package:check_new_version(callback)
- 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)
+ if self:is_registry_spec() then
+ self:get_installed_version(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
end
function Package:get_lsp_settings_schema()
@@ -214,6 +301,11 @@ function Package:get_lsp_settings_schema()
return Optional.of(schema)
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/path.lua b/lua/mason-core/path.lua
index b63b6c4a..1e0038be 100644
--- a/lua/mason-core/path.lua
+++ b/lua/mason-core/path.lua
@@ -58,4 +58,8 @@ function M.package_build_prefix(name)
return M.concat { M.install_prefix ".packages", name }
end
+function M.registry_prefix()
+ return M.install_prefix "registries"
+end
+
return M
diff --git a/lua/mason-core/platform.lua b/lua/mason-core/platform.lua
index 0a7a3e33..cd958abc 100644
--- a/lua/mason-core/platform.lua
+++ b/lua/mason-core/platform.lua
@@ -5,11 +5,22 @@ local M = {}
local uname = vim.loop.os_uname()
---@alias Platform
----| '"win"'
----| '"unix"'
----| '"linux"'
----| '"mac"'
----| '"darwin"'
+---| '"darwin_arm64"'
+---| '"darwin_x64"'
+---| '"linux_arm"'
+---| '"linux_arm64"'
+---| '"linux_arm64_gnu"'
+---| '"linux_arm64_openbsd"'
+---| '"linux_arm_gnu"'
+---| '"linux_x64"'
+---| '"linux_x64_gnu"'
+---| '"linux_x64_openbsd"'
+---| '"linux_x86"'
+---| '"linux_x86_gnu"'
+---| '"win_arm"'
+---| '"win_arm64"'
+---| '"win_x64"'
+---| '"win_x86"'
local arch_aliases = {
["x86_64"] = "x64",