aboutsummaryrefslogtreecommitdiffstats
path: root/lua/mason-core/installer/managers
diff options
context:
space:
mode:
Diffstat (limited to 'lua/mason-core/installer/managers')
-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
10 files changed, 732 insertions, 0 deletions
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