From a01d02ad7f680aec98a1e2ec35b04cedd307cfa8 Mon Sep 17 00:00:00 2001 From: William Boman Date: Sun, 12 Mar 2023 08:21:15 +0100 Subject: feat: add github registry source capabilities (#1091) --- lua/mason-core/installer/registry/link.lua | 300 +++++++++++++++++++++++++++++ 1 file changed, 300 insertions(+) create mode 100644 lua/mason-core/installer/registry/link.lua (limited to 'lua/mason-core/installer/registry/link.lua') 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 +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 + local interpolated_paths = filter_empty_values(try(expr.tbl_interpolate(file_spec_table, expr_ctx))) + + ---@type table + 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 -- cgit v1.2.3-70-g09d2