aboutsummaryrefslogtreecommitdiffstats
path: root/lua/mason-core/installer/compiler
diff options
context:
space:
mode:
Diffstat (limited to 'lua/mason-core/installer/compiler')
-rw-r--r--lua/mason-core/installer/compiler/compilers/cargo.lua85
-rw-r--r--lua/mason-core/installer/compiler/compilers/composer.lua33
-rw-r--r--lua/mason-core/installer/compiler/compilers/gem.lua46
-rw-r--r--lua/mason-core/installer/compiler/compilers/generic/build.lua40
-rw-r--r--lua/mason-core/installer/compiler/compilers/generic/download.lua52
-rw-r--r--lua/mason-core/installer/compiler/compilers/generic/init.lua42
-rw-r--r--lua/mason-core/installer/compiler/compilers/github/build.lua51
-rw-r--r--lua/mason-core/installer/compiler/compilers/github/init.lua49
-rw-r--r--lua/mason-core/installer/compiler/compilers/github/release.lua57
-rw-r--r--lua/mason-core/installer/compiler/compilers/golang.lua50
-rw-r--r--lua/mason-core/installer/compiler/compilers/luarocks.lua51
-rw-r--r--lua/mason-core/installer/compiler/compilers/mason.lua43
-rw-r--r--lua/mason-core/installer/compiler/compilers/npm.lua52
-rw-r--r--lua/mason-core/installer/compiler/compilers/nuget.lua31
-rw-r--r--lua/mason-core/installer/compiler/compilers/opam.lua31
-rw-r--r--lua/mason-core/installer/compiler/compilers/openvsx.lua64
-rw-r--r--lua/mason-core/installer/compiler/compilers/pypi.lua66
-rw-r--r--lua/mason-core/installer/compiler/expr.lua110
-rw-r--r--lua/mason-core/installer/compiler/init.lua221
-rw-r--r--lua/mason-core/installer/compiler/link.lua293
-rw-r--r--lua/mason-core/installer/compiler/schemas.lua75
-rw-r--r--lua/mason-core/installer/compiler/util.lua83
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