diff options
Diffstat (limited to 'lua/mason-core/installer/compiler')
22 files changed, 1625 insertions, 0 deletions
diff --git a/lua/mason-core/installer/compiler/compilers/cargo.lua b/lua/mason-core/installer/compiler/compilers/cargo.lua new file mode 100644 index 00000000..e0f281c5 --- /dev/null +++ b/lua/mason-core/installer/compiler/compilers/cargo.lua @@ -0,0 +1,85 @@ +local Result = require "mason-core.result" +local _ = require "mason-core.functional" +local providers = require "mason-core.providers" +local util = require "mason-core.installer.compiler.util" + +local M = {} + +---@class CargoSource : RegistryPackageSource +---@field supported_platforms? string[] + +---@param source CargoSource +---@param purl Purl +function M.parse(source, purl) + return Result.try(function(try) + if source.supported_platforms then + try(util.ensure_valid_platform(source.supported_platforms)) + end + + local repository_url = _.path({ "qualifiers", "repository_url" }, purl) + + local git + if repository_url then + git = { + url = repository_url, + rev = _.path({ "qualifiers", "rev" }, purl) == "true", + } + end + + ---@type string? + local features = _.path({ "qualifiers", "features" }, purl) + local locked = _.path({ "qualifiers", "locked" }, purl) + + ---@class ParsedCargoSource : ParsedPackageSource + local parsed_source = { + crate = purl.name, + version = purl.version, + features = features, + locked = locked ~= "false", + git = git, + } + return parsed_source + end) +end + +---@async +---@param ctx InstallContext +---@param source ParsedCargoSource +function M.install(ctx, source) + local cargo = require "mason-core.installer.managers.cargo" + + return cargo.install(source.crate, source.version, { + git = source.git, + features = source.features, + locked = source.locked, + }) +end + +---@async +---@param purl Purl +function M.get_versions(purl) + ---@type string? + local repository_url = _.path({ "qualifiers", "repository_url" }, purl) + local rev = _.path({ "qualifiers", "rev" }, purl) + if repository_url then + if rev == "true" then + -- When ?rev=true we're targeting a commit SHA. It's not feasible to retrieve all commit SHAs for a + -- repository so we fail instead. + return Result.failure "Unable to retrieve commit SHAs." + end + + ---@type Result? + local git_tags = _.cond { + { + _.matches "github.com/(.+)", + _.compose(providers.github.get_all_tags, _.head, _.match "github.com/(.+)"), + }, + }(repository_url) + if git_tags then + return git_tags + end + end + return providers.crates.get_all_versions(purl.name) +end + +return M diff --git a/lua/mason-core/installer/compiler/compilers/composer.lua b/lua/mason-core/installer/compiler/compilers/composer.lua new file mode 100644 index 00000000..259512a2 --- /dev/null +++ b/lua/mason-core/installer/compiler/compilers/composer.lua @@ -0,0 +1,33 @@ +local Result = require "mason-core.result" +local providers = require "mason-core.providers" +local util = require "mason-core.installer.compiler.util" + +local M = {} + +---@param source RegistryPackageSource +---@param purl Purl +function M.parse(source, purl) + ---@class ParsedComposerSource : ParsedPackageSource + local parsed_source = { + package = ("%s/%s"):format(purl.namespace, purl.name), + version = purl.version, + } + + return Result.success(parsed_source) +end + +---@async +---@param ctx InstallContext +---@param source ParsedComposerSource +function M.install(ctx, source) + local composer = require "mason-core.installer.managers.composer" + return composer.install(source.package, source.version) +end + +---@async +---@param purl Purl +function M.get_versions(purl) + return providers.packagist.get_all_versions(("%s/%s"):format(purl.namespace, purl.name)) +end + +return M diff --git a/lua/mason-core/installer/compiler/compilers/gem.lua b/lua/mason-core/installer/compiler/compilers/gem.lua new file mode 100644 index 00000000..7a343eec --- /dev/null +++ b/lua/mason-core/installer/compiler/compilers/gem.lua @@ -0,0 +1,46 @@ +local Result = require "mason-core.result" +local _ = require "mason-core.functional" +local providers = require "mason-core.providers" +local util = require "mason-core.installer.compiler.util" + +local M = {} + +---@class GemSource : RegistryPackageSource +---@field supported_platforms? string[] +---@field extra_packages? string[] + +---@param source GemSource +---@param purl Purl +function M.parse(source, purl) + return Result.try(function(try) + if source.supported_platforms then + try(util.ensure_valid_platform(source.supported_platforms)) + end + + ---@class ParsedGemSource : ParsedPackageSource + local parsed_source = { + package = purl.name, + version = purl.version, + extra_packages = source.extra_packages, + } + return parsed_source + end) +end + +---@async +---@param ctx InstallContext +---@param source ParsedGemSource +function M.install(ctx, source) + local gem = require "mason-core.installer.managers.gem" + return gem.install(source.package, source.version, { + extra_packages = source.extra_packages, + }) +end + +---@async +---@param purl Purl +function M.get_versions(purl) + return providers.rubygems.get_all_versions(purl.name) +end + +return M diff --git a/lua/mason-core/installer/compiler/compilers/generic/build.lua b/lua/mason-core/installer/compiler/compilers/generic/build.lua new file mode 100644 index 00000000..df97a118 --- /dev/null +++ b/lua/mason-core/installer/compiler/compilers/generic/build.lua @@ -0,0 +1,40 @@ +local Result = require "mason-core.result" +local _ = require "mason-core.functional" +local common = require "mason-core.installer.managers.common" +local expr = require "mason-core.installer.compiler.expr" +local util = require "mason-core.installer.compiler.util" + +local M = {} + +---@class GenericBuildSource : RegistryPackageSource +---@field build BuildInstruction | BuildInstruction[] + +---@param source GenericBuildSource +---@param purl Purl +---@param opts PackageInstallOpts +function M.parse(source, purl, opts) + return Result.try(function(try) + ---@type BuildInstruction + local build_instruction = try(util.coalesce_by_target(source.build, opts)) + + if build_instruction.env then + local expr_ctx = { version = purl.version, target = build_instruction.target } + build_instruction.env = try(expr.tbl_interpolate(build_instruction.env, expr_ctx)) + end + + ---@class ParsedGenericBuildSource : ParsedPackageSource + local parsed_source = { + build = build_instruction, + } + return parsed_source + end) +end + +---@async +---@param ctx InstallContext +---@param source ParsedGenericBuildSource +function M.install(ctx, source) + return common.run_build_instruction(source.build) +end + +return M diff --git a/lua/mason-core/installer/compiler/compilers/generic/download.lua b/lua/mason-core/installer/compiler/compilers/generic/download.lua new file mode 100644 index 00000000..37e54d96 --- /dev/null +++ b/lua/mason-core/installer/compiler/compilers/generic/download.lua @@ -0,0 +1,52 @@ +local Result = require "mason-core.result" +local _ = require "mason-core.functional" +local common = require "mason-core.installer.managers.common" +local expr = require "mason-core.installer.compiler.expr" +local util = require "mason-core.installer.compiler.util" + +local M = {} + +---@class GenericDownload +---@field target (Platform | Platform[])? +---@field files table<string, string> + +---@class GenericDownloadSource : RegistryPackageSource +---@field download GenericDownload | GenericDownload[] + +---@param source GenericDownloadSource +---@param purl Purl +---@param opts PackageInstallOpts +function M.parse(source, purl, opts) + return Result.try(function(try) + local download = try(util.coalesce_by_target(source.download, opts)) + + local expr_ctx = { version = purl.version } + ---@type { files: table<string, string> } + local interpolated_download = try(expr.tbl_interpolate(download, expr_ctx)) + + ---@type DownloadItem[] + local downloads = _.map(function(pair) + ---@type DownloadItem + return { + out_file = pair[1], + download_url = pair[2], + } + end, _.to_pairs(interpolated_download.files)) + + ---@class ParsedGenericDownloadSource : ParsedPackageSource + local parsed_source = { + download = interpolated_download, + downloads = downloads, + } + return parsed_source + end) +end + +---@async +---@param ctx InstallContext +---@param source ParsedGenericDownloadSource +function M.install(ctx, source) + return common.download_files(ctx, source.downloads) +end + +return M diff --git a/lua/mason-core/installer/compiler/compilers/generic/init.lua b/lua/mason-core/installer/compiler/compilers/generic/init.lua new file mode 100644 index 00000000..8206883f --- /dev/null +++ b/lua/mason-core/installer/compiler/compilers/generic/init.lua @@ -0,0 +1,42 @@ +local Result = require "mason-core.result" +local _ = require "mason-core.functional" + +local M = {} + +---@param source GenericDownloadSource | GenericBuildSource +---@param purl Purl +---@param opts PackageInstallOpts +function M.parse(source, purl, opts) + if source.download then + source = source --[[@as GenericDownloadSource]] + return require("mason-core.installer.compiler.compilers.generic.download").parse(source, purl, opts) + elseif source.build then + source = source --[[@as GenericBuildSource]] + return require("mason-core.installer.compiler.compilers.generic.build").parse(source, purl, opts) + else + return Result.failure "Unknown source type." + end +end + +---@async +---@param ctx InstallContext +---@param source ParsedGenericDownloadSource | ParsedGenericBuildSource +function M.install(ctx, source) + if source.download then + source = source --[[@as ParsedGenericDownloadSource]] + return require("mason-core.installer.compiler.compilers.generic.download").install(ctx, source) + elseif source.build then + source = source --[[@as ParsedGenericBuildSource]] + return require("mason-core.installer.compiler.compilers.generic.build").install(ctx, source) + else + return Result.failure "Unknown source type." + end +end + +---@async +---@param purl Purl +function M.get_versions(purl) + return Result.failure "Unimplemented." +end + +return M diff --git a/lua/mason-core/installer/compiler/compilers/github/build.lua b/lua/mason-core/installer/compiler/compilers/github/build.lua new file mode 100644 index 00000000..22f3e3cc --- /dev/null +++ b/lua/mason-core/installer/compiler/compilers/github/build.lua @@ -0,0 +1,51 @@ +local Result = require "mason-core.result" +local _ = require "mason-core.functional" +local common = require "mason-core.installer.managers.common" +local expr = require "mason-core.installer.compiler.expr" +local util = require "mason-core.installer.compiler.util" + +local M = {} + +---@class GitHubBuildSource : RegistryPackageSource +---@field build BuildInstruction | BuildInstruction[] + +---@param source GitHubBuildSource +---@param purl Purl +---@param opts PackageInstallOpts +function M.parse(source, purl, opts) + return Result.try(function(try) + ---@type BuildInstruction + local build_instruction = try(util.coalesce_by_target(source.build, opts)) + + local expr_ctx = { version = purl.version } + + -- TODO: In a few releases of the core registry, r-languageserver reads $MASON_VERSION directly. Remove this + -- some time in the future. + local default_env = { + MASON_VERSION = purl.version, + } + build_instruction.env = + vim.tbl_extend("force", default_env, try(expr.tbl_interpolate(build_instruction.env or {}, expr_ctx))) + + ---@class ParsedGitHubBuildSource : ParsedPackageSource + local parsed_source = { + build = build_instruction, + repo = ("https://github.com/%s/%s.git"):format(purl.namespace, purl.name), + rev = purl.version, + } + return parsed_source + end) +end + +---@async +---@param ctx InstallContext +---@param source ParsedGitHubBuildSource +function M.install(ctx, source) + local std = require "mason-core.installer.managers.std" + return Result.try(function(try) + try(std.clone(source.repo, { rev = source.rev })) + try(common.run_build_instruction(source.build)) + end) +end + +return M diff --git a/lua/mason-core/installer/compiler/compilers/github/init.lua b/lua/mason-core/installer/compiler/compilers/github/init.lua new file mode 100644 index 00000000..d8646975 --- /dev/null +++ b/lua/mason-core/installer/compiler/compilers/github/init.lua @@ -0,0 +1,49 @@ +local Result = require "mason-core.result" + +local M = {} + +---@param source GitHubReleaseSource | GitHubBuildSource +---@param purl Purl +---@param opts PackageInstallOpts +function M.parse(source, purl, opts) + if source.asset then + source = source --[[@as GitHubReleaseSource]] + return require("mason-core.installer.compiler.compilers.github.release").parse(source, purl, opts) + elseif source.build then + source = source --[[@as GitHubBuildSource]] + return require("mason-core.installer.compiler.compilers.github.build").parse(source, purl, opts) + else + return Result.failure "Unknown source type." + end +end + +---@async +---@param ctx InstallContext +---@param source ParsedGitHubReleaseSource | ParsedGitHubBuildSource +function M.install(ctx, source, purl) + if source.asset then + source = source--[[@as ParsedGitHubReleaseSource]] + return require("mason-core.installer.compiler.compilers.github.release").install(ctx, source) + elseif source.build then + source = source--[[@as ParsedGitHubBuildSource]] + return require("mason-core.installer.compiler.compilers.github.build").install(ctx, source) + else + return Result.failure "Unknown source type." + end +end + +---@async +---@param purl Purl +---@param source GitHubReleaseSource | GitHubBuildSource +function M.get_versions(purl, source) + if source.asset then + return require("mason-core.installer.compiler.compilers.github.release").get_versions(purl) + elseif source.build then + -- We can't yet reliably determine the true source (release, tag, commit, etc.) for "build" sources. + return Result.failure "Unimplemented." + else + return Result.failure "Unknown source type." + end +end + +return M diff --git a/lua/mason-core/installer/compiler/compilers/github/release.lua b/lua/mason-core/installer/compiler/compilers/github/release.lua new file mode 100644 index 00000000..39f7d862 --- /dev/null +++ b/lua/mason-core/installer/compiler/compilers/github/release.lua @@ -0,0 +1,57 @@ +local Result = require "mason-core.result" +local _ = require "mason-core.functional" +local common = require "mason-core.installer.managers.common" +local expr = require "mason-core.installer.compiler.expr" +local providers = require "mason-core.providers" +local settings = require "mason.settings" +local util = require "mason-core.installer.compiler.util" + +---@class GitHubReleaseSourceAsset : FileDownloadSpec +---@field target? Platform | Platform[] + +---@class GitHubReleaseSource : RegistryPackageSource +---@field asset GitHubReleaseSourceAsset | GitHubReleaseSourceAsset[] + +local M = {} + +---@param source GitHubReleaseSource +---@param purl Purl +---@param opts PackageInstallOpts +function M.parse(source, purl, opts) + return Result.try(function(try) + local expr_ctx = { version = purl.version } + ---@type GitHubReleaseSourceAsset + local asset = try(util.coalesce_by_target(try(expr.tbl_interpolate(source.asset, expr_ctx)), opts)) + + local downloads = common.parse_downloads(asset, function(file) + return settings.current.github.download_url_template:format( + ("%s/%s"):format(purl.namespace, purl.name), + purl.version, + file + ) + end) + + ---@class ParsedGitHubReleaseSource : ParsedPackageSource + local parsed_source = { + repo = ("%s/%s"):format(purl.namespace, purl.name), + asset = common.normalize_files(asset), + downloads = downloads, + } + return parsed_source + end) +end + +---@async +---@param ctx InstallContext +---@param source ParsedGitHubReleaseSource +function M.install(ctx, source) + return common.download_files(ctx, source.downloads) +end + +---@async +---@param purl Purl +function M.get_versions(purl) + return providers.github.get_all_release_versions(("%s/%s"):format(purl.namespace, purl.name)) +end + +return M diff --git a/lua/mason-core/installer/compiler/compilers/golang.lua b/lua/mason-core/installer/compiler/compilers/golang.lua new file mode 100644 index 00000000..01807088 --- /dev/null +++ b/lua/mason-core/installer/compiler/compilers/golang.lua @@ -0,0 +1,50 @@ +local Result = require "mason-core.result" +local _ = require "mason-core.functional" +local providers = require "mason-core.providers" +local util = require "mason-core.installer.compiler.util" + +local M = {} + +---@param purl Purl +local function get_package_name(purl) + if purl.subpath then + return ("%s/%s/%s"):format(purl.namespace, purl.name, purl.subpath) + else + return ("%s/%s"):format(purl.namespace, purl.name) + end +end + +---@class GolangSource : RegistryPackageSource +---@field extra_packages? string[] + +---@param source GolangSource +---@param purl Purl +function M.parse(source, purl) + ---@class ParsedGolangSource : ParsedPackageSource + local parsed_source = { + package = get_package_name(purl), + version = purl.version, + extra_packages = source.extra_packages, + } + + return Result.success(parsed_source) +end + +---@async +---@param ctx InstallContext +---@param source ParsedGolangSource +function M.install(ctx, source) + local golang = require "mason-core.installer.managers.golang" + + return golang.install(source.package, source.version, { + extra_packages = source.extra_packages, + }) +end + +---@async +---@param purl Purl +function M.get_versions(purl) + return providers.golang.get_all_versions(("%s/%s"):format(purl.namespace, purl.name)) +end + +return M diff --git a/lua/mason-core/installer/compiler/compilers/luarocks.lua b/lua/mason-core/installer/compiler/compilers/luarocks.lua new file mode 100644 index 00000000..356857c0 --- /dev/null +++ b/lua/mason-core/installer/compiler/compilers/luarocks.lua @@ -0,0 +1,51 @@ +local Result = require "mason-core.result" +local _ = require "mason-core.functional" + +local M = {} + +---@param purl Purl +local function parse_package_name(purl) + if purl.namespace then + return ("%s/%s"):format(purl.namespace, purl.name) + else + return purl.name + end +end + +local parse_server = _.path { "qualifiers", "repository_url" } +local parse_dev = _.compose(_.equals "true", _.path { "qualifiers", "dev" }) + +---@param source RegistryPackageSource +---@param purl Purl +function M.parse(source, purl) + ---@class ParsedLuaRocksSource : ParsedPackageSource + local parsed_source = { + package = parse_package_name(purl), + version = purl.version, + ---@type string? + server = parse_server(purl), + ---@type boolean? + dev = parse_dev(purl), + } + + return Result.success(parsed_source) +end + +---@async +---@param ctx InstallContext +---@param source ParsedLuaRocksSource +function M.install(ctx, source) + local luarocks = require "mason-core.installer.managers.luarocks" + return luarocks.install(source.package, source.version, { + server = source.server, + dev = source.dev, + }) +end + +---@async +---@param purl Purl +function M.get_versions(purl) + return Result.failure "Unimplemented." +end + +return M diff --git a/lua/mason-core/installer/compiler/compilers/mason.lua b/lua/mason-core/installer/compiler/compilers/mason.lua new file mode 100644 index 00000000..3490ebaa --- /dev/null +++ b/lua/mason-core/installer/compiler/compilers/mason.lua @@ -0,0 +1,43 @@ +local Result = require "mason-core.result" +local _ = require "mason-core.functional" + +local M = {} + +---@param source RegistryPackageSource +---@param purl Purl +function M.parse(source, purl) + if type(source.install) ~= "function" and type((getmetatable(source.install) or {}).__call) ~= "function" then + return Result.failure "source.install is not a function." + end + + ---@class ParsedMasonSource : ParsedPackageSource + local parsed_source = { + purl = purl, + ---@type async fun(ctx: InstallContext, purl: Purl) + install = source.install, + } + + return Result.success(parsed_source) +end + +---@async +---@param ctx InstallContext +---@param source ParsedMasonSource +function M.install(ctx, source) + ctx.spawn.strict_mode = true + return Result.pcall(source.install, ctx, source.purl) + :on_success(function() + ctx.spawn.strict_mode = false + end) + :on_failure(function() + ctx.spawn.strict_mode = false + end) +end + +---@async +---@param purl Purl +function M.get_versions(purl) + return Result.failure "Unimplemented." +end + +return M diff --git a/lua/mason-core/installer/compiler/compilers/npm.lua b/lua/mason-core/installer/compiler/compilers/npm.lua new file mode 100644 index 00000000..e8489fe8 --- /dev/null +++ b/lua/mason-core/installer/compiler/compilers/npm.lua @@ -0,0 +1,52 @@ +local Result = require "mason-core.result" +local _ = require "mason-core.functional" +local providers = require "mason-core.providers" + +---@param purl Purl +local function purl_to_npm(purl) + if purl.namespace then + return ("%s/%s"):format(purl.namespace, purl.name) + else + return purl.name + end +end + +local M = {} + +---@class NpmSource : RegistryPackageSource +---@field extra_packages? string[] + +---@param source NpmSource +---@param purl Purl +function M.parse(source, purl) + ---@class ParsedNpmSource : ParsedPackageSource + local parsed_source = { + package = purl_to_npm(purl), + version = purl.version, + extra_packages = source.extra_packages, + } + + return Result.success(parsed_source) +end + +---@async +---@param ctx InstallContext +---@param source ParsedNpmSource +function M.install(ctx, source) + local npm = require "mason-core.installer.managers.npm" + + return Result.try(function(try) + try(npm.init()) + try(npm.install(source.package, source.version, { + extra_packages = source.extra_packages, + })) + end) +end + +---@async +---@param purl Purl +function M.get_versions(purl) + return providers.npm.get_all_versions(purl_to_npm(purl)) +end + +return M diff --git a/lua/mason-core/installer/compiler/compilers/nuget.lua b/lua/mason-core/installer/compiler/compilers/nuget.lua new file mode 100644 index 00000000..370c7b95 --- /dev/null +++ b/lua/mason-core/installer/compiler/compilers/nuget.lua @@ -0,0 +1,31 @@ +local Result = require "mason-core.result" + +local M = {} + +---@param source RegistryPackageSource +---@param purl Purl +function M.parse(source, purl) + ---@class ParsedNugetSource : ParsedPackageSource + local parsed_source = { + package = purl.name, + version = purl.version, + } + + return Result.success(parsed_source) +end + +---@async +---@param ctx InstallContext +---@param source ParsedNugetSource +function M.install(ctx, source) + local nuget = require "mason-core.installer.managers.nuget" + return nuget.install(source.package, source.version) +end + +---@async +---@param purl Purl +function M.get_versions(purl) + return Result.failure "Unimplemented." +end + +return M diff --git a/lua/mason-core/installer/compiler/compilers/opam.lua b/lua/mason-core/installer/compiler/compilers/opam.lua new file mode 100644 index 00000000..276686ae --- /dev/null +++ b/lua/mason-core/installer/compiler/compilers/opam.lua @@ -0,0 +1,31 @@ +local Result = require "mason-core.result" + +local M = {} + +---@param source RegistryPackageSource +---@param purl Purl +function M.parse(source, purl) + ---@class ParsedOpamSource : ParsedPackageSource + local parsed_source = { + package = purl.name, + version = purl.version, + } + + return Result.success(parsed_source) +end + +---@async +---@param ctx InstallContext +---@param source ParsedOpamSource +function M.install(ctx, source) + local opam = require "mason-core.installer.managers.opam" + return opam.install(source.package, source.version) +end + +---@async +---@param purl Purl +function M.get_versions(purl) + return Result.failure "Unimplemented." +end + +return M diff --git a/lua/mason-core/installer/compiler/compilers/openvsx.lua b/lua/mason-core/installer/compiler/compilers/openvsx.lua new file mode 100644 index 00000000..bf31e2f9 --- /dev/null +++ b/lua/mason-core/installer/compiler/compilers/openvsx.lua @@ -0,0 +1,64 @@ +local Result = require "mason-core.result" +local common = require "mason-core.installer.managers.common" +local expr = require "mason-core.installer.compiler.expr" +local providers = require "mason-core.providers" +local util = require "mason-core.installer.compiler.util" + +local M = {} + +---@class OpenVSXSourceDownload : FileDownloadSpec +---@field target? Platform | Platform[] +---@field target_platform? string + +---@class OpenVSXSource : RegistryPackageSource +---@field download OpenVSXSourceDownload | OpenVSXSourceDownload[] + +---@param source OpenVSXSource +---@param purl Purl +---@param opts PackageInstallOpts +function M.parse(source, purl, opts) + return Result.try(function(try) + local expr_ctx = { version = purl.version } + ---@type OpenVSXSourceDownload + local download = try(util.coalesce_by_target(try(expr.tbl_interpolate(source.download, expr_ctx)), opts)) + + local downloads = common.parse_downloads(download, function(file) + if download.target_platform then + return ("https://open-vsx.org/api/%s/%s/%s/%s/file/%s"):format( + purl.namespace, + purl.name, + download.target_platform, + purl.version, + file + ) + else + return ("https://open-vsx.org/api/%s/%s/%s/file/%s"):format( + purl.namespace, + purl.name, + purl.version, + file + ) + end + end) + + ---@class ParsedOpenVSXSource : ParsedPackageSource + local parsed_source = { + download = common.normalize_files(download), + downloads = downloads, + } + return parsed_source + end) +end + +---@param ctx InstallContext +---@param source ParsedOpenVSXSource +function M.install(ctx, source) + return common.download_files(ctx, source.downloads) +end + +---@param purl Purl +function M.get_versions(purl) + return providers.openvsx.get_all_versions(purl.namespace, purl.name) +end + +return M diff --git a/lua/mason-core/installer/compiler/compilers/pypi.lua b/lua/mason-core/installer/compiler/compilers/pypi.lua new file mode 100644 index 00000000..c44fcfe1 --- /dev/null +++ b/lua/mason-core/installer/compiler/compilers/pypi.lua @@ -0,0 +1,66 @@ +local Result = require "mason-core.result" +local _ = require "mason-core.functional" +local providers = require "mason-core.providers" +local settings = require "mason.settings" +local util = require "mason-core.installer.compiler.util" + +local M = {} + +---@class PypiSource : RegistryPackageSource +---@field extra_packages? string[] +---@field supported_platforms? string[] + +---@param source PypiSource +---@param purl Purl +function M.parse(source, purl) + return Result.try(function(try) + if source.supported_platforms then + try(util.ensure_valid_platform(source.supported_platforms)) + end + + ---@class ParsedPypiSource : ParsedPackageSource + local parsed_source = { + package = purl.name, + version = purl.version --[[ @as string ]], + extra = _.path({ "qualifiers", "extra" }, purl), + extra_packages = source.extra_packages, + pip = { + upgrade = settings.current.pip.upgrade_pip, + extra_args = settings.current.pip.install_args, + }, + } + + return parsed_source + end) +end + +---@async +---@param ctx InstallContext +---@param source ParsedPypiSource +function M.install(ctx, source) + local pypi = require "mason-core.installer.managers.pypi" + + return Result.try(function(try) + try(pypi.init { + package = { + name = source.package, + version = source.version, + }, + upgrade_pip = source.pip.upgrade, + install_extra_args = source.pip.extra_args, + }) + try(pypi.install(source.package, source.version, { + extra = source.extra, + extra_packages = source.extra_packages, + install_extra_args = source.pip.extra_args, + })) + end) +end + +---@async +---@param purl Purl +function M.get_versions(purl) + return providers.pypi.get_all_versions(purl.name) +end + +return M diff --git a/lua/mason-core/installer/compiler/expr.lua b/lua/mason-core/installer/compiler/expr.lua new file mode 100644 index 00000000..a07fc00d --- /dev/null +++ b/lua/mason-core/installer/compiler/expr.lua @@ -0,0 +1,110 @@ +local Result = require "mason-core.result" +local _ = require "mason-core.functional" +local platform = require "mason-core.platform" + +local M = {} + +local parse_expr = _.compose( + _.apply_spec { + value_expr = _.head, + filters = _.drop(1), + }, + _.filter(_.complement(_.equals "")), + _.map(_.trim), + _.split "|" +) + +---@param predicate (fun(value: string): boolean) | boolean +---@param value string +local take_if = _.curryN(function(predicate, value) + if type(predicate) == "boolean" then + predicate = _.always(predicate) + end + return predicate(value) and value or nil +end, 2) + +---@param predicate (fun(value: string): boolean) | boolean +---@param value string +local take_if_not = _.curryN(function(predicate, value) + if type(predicate) == "boolean" then + predicate = _.always(predicate) + end + return (not predicate(value)) and value or nil +end, 2) + +local FILTERS = { + equals = _.equals, + not_equals = _.not_equals, + strip_prefix = _.strip_prefix, + strip_suffix = _.strip_suffix, + take_if = take_if, + take_if_not = take_if_not, + to_lower = _.to_lower, + to_upper = _.to_upper, + is_platform = function(target) + return platform.is[target] + end, +} + +---@generic T : table +---@param tbl T +---@return T +local function shallow_clone(tbl) + local res = {} + for k, v in pairs(tbl) do + res[k] = v + end + return res +end + +---@param expr string +---@param ctx table<string, any> +local function eval(expr, ctx) + return setfenv(assert(loadstring("return " .. expr), ("Failed to parse expression: %q"):format(expr)), ctx)() +end + +---@param str string +---@param ctx table<string, any> +function M.interpolate(str, ctx) + ctx = shallow_clone(ctx) + setmetatable(ctx, { __index = FILTERS }) + return Result.pcall(function() + return _.gsub("{{([^}]+)}}", function(expr) + local components = parse_expr(expr) + + local value = eval(components.value_expr, ctx) + + local filters = _.map(function(filter_expr) + local filter = eval(filter_expr, ctx) + assert(type(filter) == "function", ("Invalid filter expression: %q"):format(filter_expr)) + return filter + end, components.filters) + + local reduced_value = _.reduce(_.apply_to, value, filters) + + return reduced_value ~= nil and tostring(reduced_value) or "" + end, str) + end) +end + +---@generic T : table +---@param tbl T +---@param ctx table +---@return Result # Result<T> +function M.tbl_interpolate(tbl, ctx) + return Result.try(function(try) + local interpolated = {} + for k, v in pairs(tbl) do + if type(v) == "string" then + interpolated[k] = try(M.interpolate(v, ctx)) + elseif type(v) == "table" then + interpolated[k] = try(M.tbl_interpolate(v, ctx)) + else + interpolated[k] = v + end + end + return interpolated + end) +end + +return M 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<RegistryPackageSpecSchema, boolean> +M.SCHEMA_CAP = _.set_of { + "registry+v1", +} + +---@type table<string, InstallerCompiler> +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<InstallerCompiler> +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<string[]> + +---@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 diff --git a/lua/mason-core/installer/compiler/link.lua b/lua/mason-core/installer/compiler/link.lua new file mode 100644 index 00000000..5d136322 --- /dev/null +++ b/lua/mason-core/installer/compiler/link.lua @@ -0,0 +1,293 @@ +local Optional = require "mason-core.optional" +local Result = require "mason-core.result" +local _ = require "mason-core.functional" +local a = require "mason-core.async" +local expr = require "mason-core.installer.compiler.expr" +local fs = require "mason-core.fs" +local log = require "mason-core.log" +local path = require "mason-core.path" +local platform = require "mason-core.platform" + +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, + ["ruby"] = function(target, bin) + local installer = require "mason-core.installer" + local ctx = installer.context() + return Result.pcall(function() + return ctx:write_ruby_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, +} + +---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 + ctx.fs:chmod_exec(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<string, string> +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<string, string> + local interpolated_paths = filter_empty_values(try(expr.tbl_interpolate(file_spec_table, expr_ctx))) + + ---@type table<string, string> + 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 + + a.scheduler() + + 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 +---@nodiscard +M.bin = function(ctx, spec, purl, source) + return expand_bin(ctx, spec, purl, source):on_success(function(links) + ctx.links.bin = vim.tbl_extend("force", ctx.links.bin, links) + end) +end + +---@async +---@param ctx InstallContext +---@param spec RegistryPackageSpec +---@param purl Purl +---@param source ParsedPackageSource +---@nodiscard +M.share = function(ctx, spec, purl, source) + return expand_file_spec(ctx, purl, source, spec.share):on_success(function(links) + ctx.links.share = vim.tbl_extend("force", ctx.links.share, links) + end) +end + +---@async +---@param ctx InstallContext +---@param spec RegistryPackageSpec +---@param purl Purl +---@param source ParsedPackageSource +---@nodiscard +M.opt = function(ctx, spec, purl, source) + return expand_file_spec(ctx, purl, source, spec.opt):on_success(function(links) + ctx.links.opt = vim.tbl_extend("force", ctx.links.opt, links) + end) +end + +return M diff --git a/lua/mason-core/installer/compiler/schemas.lua b/lua/mason-core/installer/compiler/schemas.lua new file mode 100644 index 00000000..5e578dbd --- /dev/null +++ b/lua/mason-core/installer/compiler/schemas.lua @@ -0,0 +1,75 @@ +local Result = require "mason-core.result" +local _ = require "mason-core.functional" +local a = require "mason-core.async" +local expr = require "mason-core.installer.compiler.expr" +local fetch = require "mason-core.fetch" +local log = require "mason-core.log" +local path = require "mason-core.path" +local std = require "mason-core.installer.managers.std" + +local M = {} + +---@async +---@param ctx InstallContext +---@param url string +local function download_lsp_schema(ctx, url) + return Result.try(function(try) + local is_vscode_schema = _.starts_with("vscode:", url) + local out_file = path.concat { "mason-schemas", "lsp.json" } + local share_file = path.concat { "mason-schemas", "lsp", ("%s.json"):format(ctx.package.name) } + + if is_vscode_schema then + local url = unpack(_.match("^vscode:(.+)$", url)) + ctx.stdio_sink.stdout(("Downloading LSP configuration schema from %q…\n"):format(url)) + local json = try(fetch(url)) + + ---@type { contributes?: { configuration?: table } } + local schema = try(Result.pcall(vim.json.decode, json)) + local configuration = schema.contributes and schema.contributes.configuration + + if configuration then + ctx.fs:write_file(out_file, vim.json.encode(configuration) --[[@as string]]) + ctx.links.share[share_file] = out_file + else + return Result.failure "Unable to find LSP entry in VSCode schema." + end + else + ctx.stdio_sink.stdout(("Downloading LSP configuration schema from %q…\n"):format(url)) + try(std.download_file(url, out_file)) + ctx.links.share[share_file] = out_file + end + end) +end + +---@async +---@param ctx InstallContext +---@param spec RegistryPackageSpec +---@param purl Purl +---@param source ParsedPackageSource +---@nodiscard +function M.download(ctx, spec, purl, source) + return Result.try(function(try) + log.debug("schemas: download", ctx.package, spec.schemas) + local schemas = spec.schemas + if not schemas then + return + end + ---@type RegistryPackageSchemas + local interpolated_schemas = try(expr.tbl_interpolate(schemas, { version = purl.version, source = source })) + ctx.fs:mkdir "mason-schemas" + + if interpolated_schemas.lsp then + try(a.wait_first { + function() + return download_lsp_schema(ctx, interpolated_schemas.lsp) + end, + function() + a.sleep(5000) + return Result.failure "Schema download timed out." + end, + }) + end + end) +end + +return M diff --git a/lua/mason-core/installer/compiler/util.lua b/lua/mason-core/installer/compiler/util.lua new file mode 100644 index 00000000..b3735c9c --- /dev/null +++ b/lua/mason-core/installer/compiler/util.lua @@ -0,0 +1,83 @@ +local Optional = require "mason-core.optional" +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 = {} + +---@generic T : { target: Platform | Platform[] } +---@param candidates T[] | T +---@param opts PackageInstallOpts +---@return Result # Result<T> +function M.coalesce_by_target(candidates, opts) + if not _.is_list(candidates) then + return Result.success(candidates) + end + return Optional.of_nilable(_.find_first(function(asset) + if opts.target then + -- Matching against a provided target rather than the current platform is an escape hatch primarily meant + -- for automated testing purposes. + if type(asset.target) == "table" then + return _.any(_.equals(opts.target), asset.target) + else + return asset.target == opts.target + end + else + if type(asset.target) == "table" then + return _.any(function(target) + return platform.is[target] + end, asset.target) + else + return platform.is[asset.target] + end + end + end, candidates)):ok_or "PLATFORM_UNSUPPORTED" +end + +---Checks whether a custom version of a package installation corresponds to a valid version. +---@async +---@param versions_thunk async fun(): Result Result<string[]> +function M.ensure_valid_version(versions_thunk) + local ctx = installer.context() + local version = ctx.opts.version + + if version and not ctx.opts.force then + ctx.stdio_sink.stdout "Fetching available versions…\n" + local all_versions = versions_thunk() + if all_versions:is_failure() then + log.warn("Failed to fetch versions for package", ctx.package) + -- Gracefully fail (i.e. optimistically continue package installation) + return Result.success() + end + all_versions = all_versions:get_or_else {} + + if not _.any(_.equals(version), all_versions) then + ctx.stdio_sink.stderr(("Tried to install invalid version %q. Available versions:\n"):format(version)) + ctx.stdio_sink.stderr(_.compose(_.join "\n", _.map(_.join ", "), _.split_every(15))(all_versions)) + ctx.stdio_sink.stderr "\n\n" + ctx.stdio_sink.stderr( + ("Run with --force flag to bypass version validation:\n :MasonInstall --force %s@%s\n\n"):format( + ctx.package.name, + version + ) + ) + return Result.failure(("Version %q is not available."):format(version)) + end + end + + return Result.success() +end + +---@param platforms string[] +function M.ensure_valid_platform(platforms) + if not _.any(function(target) + return platform.is[target] + end, platforms) then + return Result.failure "PLATFORM_UNSUPPORTED" + end + return Result.success() +end + +return M |
