diff options
| author | William Boman <william@redwill.se> | 2023-10-11 16:31:50 +0200 |
|---|---|---|
| committer | William Boman <william@redwill.se> | 2025-02-19 09:22:40 +0100 |
| commit | 047ec18da56ad8f331e5c6bc7417dc5a9a6e71cc (patch) | |
| tree | c50c22cd05d3605fc5a1e8eb902ffeb11e339697 /lua/mason-core/installer/compiler | |
| parent | refactor(receipt): change receipt structure and remove old builder APIs (#1521) (diff) | |
| download | mason-047ec18da56ad8f331e5c6bc7417dc5a9a6e71cc.tar mason-047ec18da56ad8f331e5c6bc7417dc5a9a6e71cc.tar.gz mason-047ec18da56ad8f331e5c6bc7417dc5a9a6e71cc.tar.bz2 mason-047ec18da56ad8f331e5c6bc7417dc5a9a6e71cc.tar.lz mason-047ec18da56ad8f331e5c6bc7417dc5a9a6e71cc.tar.xz mason-047ec18da56ad8f331e5c6bc7417dc5a9a6e71cc.tar.zst mason-047ec18da56ad8f331e5c6bc7417dc5a9a6e71cc.zip | |
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.
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 |
