local Optional = require "mason-core.optional" local Purl = require "mason-core.purl" local Result = require "mason-core.result" local _ = require "mason-core.functional" local a = require "mason-core.async" local link = require "mason-core.installer.compiler.link" local log = require "mason-core.log" local schemas = require "mason-core.installer.compiler.schemas" local util = require "mason-core.installer.compiler.util" local M = {} ---@type table M.SCHEMA_CAP = _.set_of { "registry+v1", } ---@type table local COMPILERS = {} ---@param id string ---@param compiler InstallerCompiler function M.register_compiler(id, compiler) COMPILERS[id] = compiler end M.register_compiler("cargo", _.lazy_require "mason-core.installer.compiler.compilers.cargo") M.register_compiler("composer", _.lazy_require "mason-core.installer.compiler.compilers.composer") M.register_compiler("gem", _.lazy_require "mason-core.installer.compiler.compilers.gem") M.register_compiler("generic", _.lazy_require "mason-core.installer.compiler.compilers.generic") M.register_compiler("github", _.lazy_require "mason-core.installer.compiler.compilers.github") M.register_compiler("golang", _.lazy_require "mason-core.installer.compiler.compilers.golang") M.register_compiler("luarocks", _.lazy_require "mason-core.installer.compiler.compilers.luarocks") M.register_compiler("mason", _.lazy_require "mason-core.installer.compiler.compilers.mason") M.register_compiler("npm", _.lazy_require "mason-core.installer.compiler.compilers.npm") M.register_compiler("nuget", _.lazy_require "mason-core.installer.compiler.compilers.nuget") M.register_compiler("opam", _.lazy_require "mason-core.installer.compiler.compilers.opam") M.register_compiler("openvsx", _.lazy_require "mason-core.installer.compiler.compilers.openvsx") M.register_compiler("pypi", _.lazy_require "mason-core.installer.compiler.compilers.pypi") ---@param purl Purl ---@return Result # Result function M.get_compiler(purl) return Optional.of_nilable(COMPILERS[purl.type]) :ok_or(("Current version of mason.nvim is not capable of parsing package type %q."):format(purl.type)) end ---@class InstallerCompiler ---@field parse fun(source: RegistryPackageSource, purl: Purl, opts: PackageInstallOpts): Result ---@field install async fun(ctx: InstallContext, source: ParsedPackageSource, purl: Purl): Result ---@field get_versions async fun(purl: Purl, source: RegistryPackageSource): Result # 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 _.is_list(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? ---@return RegistryPackageSource local function coalesce_source(source, version) if version and 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 semver = require "mason-core.semver" 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 return _.dissoc("constraint", version_override) end end end end return _.dissoc("version_overrides", source) end ---@param spec RegistryPackageSpec ---@param opts PackageInstallOpts function M.parse(spec, opts) log.trace("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 = coalesce_source(spec.source, opts.version) if source.supported_platforms then try(util.ensure_valid_platform(source.supported_platforms)) end ---@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 InstallerCompiler local compiler = try(M.get_compiler(purl)) log.trace("Found compiler for purl.", source.id) local parsed_source = try(compiler.parse(source, purl, opts)) log.trace("Parsed source for purl.", source.id, parsed_source) return { compiler = compiler, source = vim.tbl_extend("keep", parsed_source, source), raw_source = source, purl = purl, } end):on_failure(function(err) log.debug("Failed to parse spec spec", spec.name, err) end) end ---@async ---@param spec RegistryPackageSpec ---@param opts PackageInstallOpts function M.compile_installer(spec, opts) log.debug("Compiling installer.", spec.name, opts) return Result.try(function(try) -- Parsers run synchronously and may access API functions, so we schedule before-hand. a.scheduler() 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, compiler: InstallerCompiler, source: ParsedPackageSource, raw_source: RegistryPackageSource } local parsed = try(M.parse(spec, opts):map_err(map_parse_err)) ---@async ---@param ctx InstallContext return function(ctx) return Result.try(function(try) if ctx.opts.version then try(util.ensure_valid_version(function() return parsed.compiler.get_versions(parsed.purl, parsed.raw_source) end)) end -- Run installer a.scheduler() try(parsed.compiler.install(ctx, parsed.source, parsed.purl)) if spec.schemas then local result = schemas.download(ctx, spec, parsed.purl, parsed.source):on_failure(function(err) log.error("Failed to download schemas", ctx.package, err) end) if opts.strict then -- schema download sources are not considered stable nor a critical feature, so we only fail in strict mode try(result) end end -- 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_source { type = ctx.package.spec.schema, id = Purl.compile(parsed.purl), -- Exclude the "install" field from "mason" sources because this is a Lua function. raw = parsed.purl.type == "mason" and _.dissoc("install", parsed.raw_source) or parsed.raw_source, } ctx.receipt:with_install_options(opts) end) end end) end return M