From 047ec18da56ad8f331e5c6bc7417dc5a9a6e71cc Mon Sep 17 00:00:00 2001 From: William Boman Date: Wed, 11 Oct 2023 16:31:50 +0200 Subject: refactor!: refactor installer internals and add new Package class methods (#1523) This contains the following changes: 1) `Package:install()` now accepts a second, optional, callback argument which is called when installation finishes (successfully or not). 2) Adds a `Package:is_installing()` method. This contains the following breaking changes: 1) `Package:install()` will now error when called while an installation is already ongoing. Use the new `Package:is_installing()` method to check whether an installation is already running. This also refactors large portions of the tests by removing test globals, removing async_test, and adding the `mason-test` Lua module instead. Test helpers via globals are problematic to work with due to not being detected through tools like the Lua language server without additional configuration. This has been replaced with a Lua module `mason-test`. `async_test` has also been removed in favour of explicitly making use of the `mason-core.async` API. These changes stands for a significant portion of the diff. --- lua/mason-core/installer/compiler/init.lua | 221 +++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 lua/mason-core/installer/compiler/init.lua (limited to 'lua/mason-core/installer/compiler/init.lua') diff --git a/lua/mason-core/installer/compiler/init.lua b/lua/mason-core/installer/compiler/init.lua new file mode 100644 index 00000000..e1df6784 --- /dev/null +++ b/lua/mason-core/installer/compiler/init.lua @@ -0,0 +1,221 @@ +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 +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 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 + 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.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 = 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 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(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), + } + end):on_failure(function(err) + error(err, 0) + end) + end + end) +end + +return M -- cgit v1.2.3-70-g09d2