diff options
Diffstat (limited to 'lua/mason-core/installer/managers')
| -rw-r--r-- | lua/mason-core/installer/managers/cargo.lua | 47 | ||||
| -rw-r--r-- | lua/mason-core/installer/managers/composer.lua | 42 | ||||
| -rw-r--r-- | lua/mason-core/installer/managers/gem.lua | 65 | ||||
| -rw-r--r-- | lua/mason-core/installer/managers/golang.lua | 52 | ||||
| -rw-r--r-- | lua/mason-core/installer/managers/luarocks.lua | 40 | ||||
| -rw-r--r-- | lua/mason-core/installer/managers/npm.lua | 64 | ||||
| -rw-r--r-- | lua/mason-core/installer/managers/nuget.lua | 37 | ||||
| -rw-r--r-- | lua/mason-core/installer/managers/opam.lua | 38 | ||||
| -rw-r--r-- | lua/mason-core/installer/managers/pypi.lua | 106 | ||||
| -rw-r--r-- | lua/mason-core/installer/managers/std.lua | 241 |
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 |
