diff options
Diffstat (limited to 'lua')
| -rw-r--r-- | lua/mason-core/installer/compiler/compilers/cargo.lua (renamed from lua/mason-core/installer/registry/providers/cargo.lua) | 2 | ||||
| -rw-r--r-- | lua/mason-core/installer/compiler/compilers/composer.lua (renamed from lua/mason-core/installer/registry/providers/composer.lua) | 2 | ||||
| -rw-r--r-- | lua/mason-core/installer/compiler/compilers/gem.lua (renamed from lua/mason-core/installer/registry/providers/gem.lua) | 2 | ||||
| -rw-r--r-- | lua/mason-core/installer/compiler/compilers/generic/build.lua (renamed from lua/mason-core/installer/registry/providers/generic/build.lua) | 4 | ||||
| -rw-r--r-- | lua/mason-core/installer/compiler/compilers/generic/download.lua (renamed from lua/mason-core/installer/registry/providers/generic/download.lua) | 4 | ||||
| -rw-r--r-- | lua/mason-core/installer/compiler/compilers/generic/init.lua (renamed from lua/mason-core/installer/registry/providers/generic/init.lua) | 8 | ||||
| -rw-r--r-- | lua/mason-core/installer/compiler/compilers/github/build.lua (renamed from lua/mason-core/installer/registry/providers/github/build.lua) | 4 | ||||
| -rw-r--r-- | lua/mason-core/installer/compiler/compilers/github/init.lua (renamed from lua/mason-core/installer/registry/providers/github/init.lua) | 10 | ||||
| -rw-r--r-- | lua/mason-core/installer/compiler/compilers/github/release.lua (renamed from lua/mason-core/installer/registry/providers/github/release.lua) | 11 | ||||
| -rw-r--r-- | lua/mason-core/installer/compiler/compilers/golang.lua (renamed from lua/mason-core/installer/registry/providers/golang.lua) | 2 | ||||
| -rw-r--r-- | lua/mason-core/installer/compiler/compilers/luarocks.lua (renamed from lua/mason-core/installer/registry/providers/luarocks.lua) | 0 | ||||
| -rw-r--r-- | lua/mason-core/installer/compiler/compilers/mason.lua (renamed from lua/mason-core/installer/registry/providers/mason.lua) | 0 | ||||
| -rw-r--r-- | lua/mason-core/installer/compiler/compilers/npm.lua (renamed from lua/mason-core/installer/registry/providers/npm.lua) | 0 | ||||
| -rw-r--r-- | lua/mason-core/installer/compiler/compilers/nuget.lua (renamed from lua/mason-core/installer/registry/providers/nuget.lua) | 0 | ||||
| -rw-r--r-- | lua/mason-core/installer/compiler/compilers/opam.lua (renamed from lua/mason-core/installer/registry/providers/opam.lua) | 0 | ||||
| -rw-r--r-- | lua/mason-core/installer/compiler/compilers/openvsx.lua (renamed from lua/mason-core/installer/registry/providers/openvsx.lua) | 7 | ||||
| -rw-r--r-- | lua/mason-core/installer/compiler/compilers/pypi.lua (renamed from lua/mason-core/installer/registry/providers/pypi.lua) | 2 | ||||
| -rw-r--r-- | lua/mason-core/installer/compiler/expr.lua (renamed from lua/mason-core/installer/registry/expr.lua) | 0 | ||||
| -rw-r--r-- | lua/mason-core/installer/compiler/init.lua (renamed from lua/mason-core/installer/registry/init.lua) | 66 | ||||
| -rw-r--r-- | lua/mason-core/installer/compiler/link.lua (renamed from lua/mason-core/installer/registry/link.lua) | 2 | ||||
| -rw-r--r-- | lua/mason-core/installer/compiler/schemas.lua (renamed from lua/mason-core/installer/registry/schemas.lua) | 2 | ||||
| -rw-r--r-- | lua/mason-core/installer/compiler/util.lua (renamed from lua/mason-core/installer/registry/util.lua) | 0 | ||||
| -rw-r--r-- | lua/mason-core/installer/context/cwd.lua | 48 | ||||
| -rw-r--r-- | lua/mason-core/installer/context/fs.lua | 108 | ||||
| -rw-r--r-- | lua/mason-core/installer/context/init.lua (renamed from lua/mason-core/installer/context.lua) | 250 | ||||
| -rw-r--r-- | lua/mason-core/installer/context/spawn.lua | 46 | ||||
| -rw-r--r-- | lua/mason-core/installer/handle.lua | 4 | ||||
| -rw-r--r-- | lua/mason-core/installer/init.lua | 255 | ||||
| -rw-r--r-- | lua/mason-core/installer/location.lua | 63 | ||||
| -rw-r--r-- | lua/mason-core/installer/runner.lua | 218 | ||||
| -rw-r--r-- | lua/mason-core/package/init.lua | 114 | ||||
| -rw-r--r-- | lua/mason-core/receipt.lua | 14 | ||||
| -rw-r--r-- | lua/mason-registry/sources/util.lua | 4 | ||||
| -rw-r--r-- | lua/mason-test/helpers.lua | 33 |
34 files changed, 701 insertions, 584 deletions
diff --git a/lua/mason-core/installer/registry/providers/cargo.lua b/lua/mason-core/installer/compiler/compilers/cargo.lua index f4904b73..e0f281c5 100644 --- a/lua/mason-core/installer/registry/providers/cargo.lua +++ b/lua/mason-core/installer/compiler/compilers/cargo.lua @@ -1,7 +1,7 @@ local Result = require "mason-core.result" local _ = require "mason-core.functional" local providers = require "mason-core.providers" -local util = require "mason-core.installer.registry.util" +local util = require "mason-core.installer.compiler.util" local M = {} diff --git a/lua/mason-core/installer/registry/providers/composer.lua b/lua/mason-core/installer/compiler/compilers/composer.lua index d85dd2ba..259512a2 100644 --- a/lua/mason-core/installer/registry/providers/composer.lua +++ b/lua/mason-core/installer/compiler/compilers/composer.lua @@ -1,6 +1,6 @@ local Result = require "mason-core.result" local providers = require "mason-core.providers" -local util = require "mason-core.installer.registry.util" +local util = require "mason-core.installer.compiler.util" local M = {} diff --git a/lua/mason-core/installer/registry/providers/gem.lua b/lua/mason-core/installer/compiler/compilers/gem.lua index 9653f116..7a343eec 100644 --- a/lua/mason-core/installer/registry/providers/gem.lua +++ b/lua/mason-core/installer/compiler/compilers/gem.lua @@ -1,7 +1,7 @@ local Result = require "mason-core.result" local _ = require "mason-core.functional" local providers = require "mason-core.providers" -local util = require "mason-core.installer.registry.util" +local util = require "mason-core.installer.compiler.util" local M = {} diff --git a/lua/mason-core/installer/registry/providers/generic/build.lua b/lua/mason-core/installer/compiler/compilers/generic/build.lua index a0d517d8..df97a118 100644 --- a/lua/mason-core/installer/registry/providers/generic/build.lua +++ b/lua/mason-core/installer/compiler/compilers/generic/build.lua @@ -1,8 +1,8 @@ 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.registry.expr" -local util = require "mason-core.installer.registry.util" +local expr = require "mason-core.installer.compiler.expr" +local util = require "mason-core.installer.compiler.util" local M = {} diff --git a/lua/mason-core/installer/registry/providers/generic/download.lua b/lua/mason-core/installer/compiler/compilers/generic/download.lua index 4622a844..37e54d96 100644 --- a/lua/mason-core/installer/registry/providers/generic/download.lua +++ b/lua/mason-core/installer/compiler/compilers/generic/download.lua @@ -1,8 +1,8 @@ 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.registry.expr" -local util = require "mason-core.installer.registry.util" +local expr = require "mason-core.installer.compiler.expr" +local util = require "mason-core.installer.compiler.util" local M = {} diff --git a/lua/mason-core/installer/registry/providers/generic/init.lua b/lua/mason-core/installer/compiler/compilers/generic/init.lua index 1bf79e94..8206883f 100644 --- a/lua/mason-core/installer/registry/providers/generic/init.lua +++ b/lua/mason-core/installer/compiler/compilers/generic/init.lua @@ -9,10 +9,10 @@ local M = {} function M.parse(source, purl, opts) if source.download then source = source --[[@as GenericDownloadSource]] - return require("mason-core.installer.registry.providers.generic.download").parse(source, purl, opts) + 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.registry.providers.generic.build").parse(source, purl, opts) + return require("mason-core.installer.compiler.compilers.generic.build").parse(source, purl, opts) else return Result.failure "Unknown source type." end @@ -24,10 +24,10 @@ end function M.install(ctx, source) if source.download then source = source --[[@as ParsedGenericDownloadSource]] - return require("mason-core.installer.registry.providers.generic.download").install(ctx, source) + 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.registry.providers.generic.build").install(ctx, source) + return require("mason-core.installer.compiler.compilers.generic.build").install(ctx, source) else return Result.failure "Unknown source type." end diff --git a/lua/mason-core/installer/registry/providers/github/build.lua b/lua/mason-core/installer/compiler/compilers/github/build.lua index 1c17bb1a..22f3e3cc 100644 --- a/lua/mason-core/installer/registry/providers/github/build.lua +++ b/lua/mason-core/installer/compiler/compilers/github/build.lua @@ -1,8 +1,8 @@ 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.registry.expr" -local util = require "mason-core.installer.registry.util" +local expr = require "mason-core.installer.compiler.expr" +local util = require "mason-core.installer.compiler.util" local M = {} diff --git a/lua/mason-core/installer/registry/providers/github/init.lua b/lua/mason-core/installer/compiler/compilers/github/init.lua index 0d68f3a5..d8646975 100644 --- a/lua/mason-core/installer/registry/providers/github/init.lua +++ b/lua/mason-core/installer/compiler/compilers/github/init.lua @@ -8,10 +8,10 @@ local M = {} function M.parse(source, purl, opts) if source.asset then source = source --[[@as GitHubReleaseSource]] - return require("mason-core.installer.registry.providers.github.release").parse(source, purl, opts) + 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.registry.providers.github.build").parse(source, purl, opts) + return require("mason-core.installer.compiler.compilers.github.build").parse(source, purl, opts) else return Result.failure "Unknown source type." end @@ -23,10 +23,10 @@ end function M.install(ctx, source, purl) if source.asset then source = source--[[@as ParsedGitHubReleaseSource]] - return require("mason-core.installer.registry.providers.github.release").install(ctx, source) + 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.registry.providers.github.build").install(ctx, source) + return require("mason-core.installer.compiler.compilers.github.build").install(ctx, source) else return Result.failure "Unknown source type." end @@ -37,7 +37,7 @@ end ---@param source GitHubReleaseSource | GitHubBuildSource function M.get_versions(purl, source) if source.asset then - return require("mason-core.installer.registry.providers.github.release").get_versions(purl) + 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." diff --git a/lua/mason-core/installer/registry/providers/github/release.lua b/lua/mason-core/installer/compiler/compilers/github/release.lua index 8c8a8a8f..39f7d862 100644 --- a/lua/mason-core/installer/registry/providers/github/release.lua +++ b/lua/mason-core/installer/compiler/compilers/github/release.lua @@ -1,13 +1,16 @@ 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.registry.expr" +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.registry.util" +local util = require "mason-core.installer.compiler.util" + +---@class GitHubReleaseSourceAsset : FileDownloadSpec +---@field target? Platform | Platform[] ---@class GitHubReleaseSource : RegistryPackageSource ----@field asset FileDownloadSpec | FileDownloadSpec[] +---@field asset GitHubReleaseSourceAsset | GitHubReleaseSourceAsset[] local M = {} @@ -17,7 +20,7 @@ local M = {} function M.parse(source, purl, opts) return Result.try(function(try) local expr_ctx = { version = purl.version } - ---@type FileDownloadSpec + ---@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) diff --git a/lua/mason-core/installer/registry/providers/golang.lua b/lua/mason-core/installer/compiler/compilers/golang.lua index 896d9bf9..01807088 100644 --- a/lua/mason-core/installer/registry/providers/golang.lua +++ b/lua/mason-core/installer/compiler/compilers/golang.lua @@ -1,7 +1,7 @@ local Result = require "mason-core.result" local _ = require "mason-core.functional" local providers = require "mason-core.providers" -local util = require "mason-core.installer.registry.util" +local util = require "mason-core.installer.compiler.util" local M = {} diff --git a/lua/mason-core/installer/registry/providers/luarocks.lua b/lua/mason-core/installer/compiler/compilers/luarocks.lua index 356857c0..356857c0 100644 --- a/lua/mason-core/installer/registry/providers/luarocks.lua +++ b/lua/mason-core/installer/compiler/compilers/luarocks.lua diff --git a/lua/mason-core/installer/registry/providers/mason.lua b/lua/mason-core/installer/compiler/compilers/mason.lua index 3490ebaa..3490ebaa 100644 --- a/lua/mason-core/installer/registry/providers/mason.lua +++ b/lua/mason-core/installer/compiler/compilers/mason.lua diff --git a/lua/mason-core/installer/registry/providers/npm.lua b/lua/mason-core/installer/compiler/compilers/npm.lua index e8489fe8..e8489fe8 100644 --- a/lua/mason-core/installer/registry/providers/npm.lua +++ b/lua/mason-core/installer/compiler/compilers/npm.lua diff --git a/lua/mason-core/installer/registry/providers/nuget.lua b/lua/mason-core/installer/compiler/compilers/nuget.lua index 370c7b95..370c7b95 100644 --- a/lua/mason-core/installer/registry/providers/nuget.lua +++ b/lua/mason-core/installer/compiler/compilers/nuget.lua diff --git a/lua/mason-core/installer/registry/providers/opam.lua b/lua/mason-core/installer/compiler/compilers/opam.lua index 276686ae..276686ae 100644 --- a/lua/mason-core/installer/registry/providers/opam.lua +++ b/lua/mason-core/installer/compiler/compilers/opam.lua diff --git a/lua/mason-core/installer/registry/providers/openvsx.lua b/lua/mason-core/installer/compiler/compilers/openvsx.lua index df52807a..bf31e2f9 100644 --- a/lua/mason-core/installer/registry/providers/openvsx.lua +++ b/lua/mason-core/installer/compiler/compilers/openvsx.lua @@ -1,16 +1,17 @@ local Result = require "mason-core.result" local common = require "mason-core.installer.managers.common" -local expr = require "mason-core.installer.registry.expr" +local expr = require "mason-core.installer.compiler.expr" local providers = require "mason-core.providers" -local util = require "mason-core.installer.registry.util" +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 FileDownloadSpec | FileDownloadSpec[] +---@field download OpenVSXSourceDownload | OpenVSXSourceDownload[] ---@param source OpenVSXSource ---@param purl Purl diff --git a/lua/mason-core/installer/registry/providers/pypi.lua b/lua/mason-core/installer/compiler/compilers/pypi.lua index 3fe6f89e..c44fcfe1 100644 --- a/lua/mason-core/installer/registry/providers/pypi.lua +++ b/lua/mason-core/installer/compiler/compilers/pypi.lua @@ -2,7 +2,7 @@ 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.registry.util" +local util = require "mason-core.installer.compiler.util" local M = {} diff --git a/lua/mason-core/installer/registry/expr.lua b/lua/mason-core/installer/compiler/expr.lua index a07fc00d..a07fc00d 100644 --- a/lua/mason-core/installer/registry/expr.lua +++ b/lua/mason-core/installer/compiler/expr.lua diff --git a/lua/mason-core/installer/registry/init.lua b/lua/mason-core/installer/compiler/init.lua index 7376db86..e1df6784 100644 --- a/lua/mason-core/installer/registry/init.lua +++ b/lua/mason-core/installer/compiler/init.lua @@ -3,10 +3,10 @@ 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.registry.link" +local link = require "mason-core.installer.compiler.link" local log = require "mason-core.log" -local schemas = require "mason-core.installer.registry.schemas" -local util = require "mason-core.installer.registry.util" +local schemas = require "mason-core.installer.compiler.schemas" +local util = require "mason-core.installer.compiler.util" local M = {} @@ -15,35 +15,37 @@ M.SCHEMA_CAP = _.set_of { "registry+v1", } ----@type table<string, InstallerProvider> -local PROVIDERS = {} +---@type table<string, InstallerCompiler> +local COMPILERS = {} ---@param id string ----@param provider InstallerProvider -function M.register_provider(id, provider) - PROVIDERS[id] = provider +---@param compiler InstallerCompiler +function M.register_compiler(id, compiler) + COMPILERS[id] = compiler end -M.register_provider("cargo", _.lazy_require "mason-core.installer.registry.providers.cargo") -M.register_provider("composer", _.lazy_require "mason-core.installer.registry.providers.composer") -M.register_provider("gem", _.lazy_require "mason-core.installer.registry.providers.gem") -M.register_provider("generic", _.lazy_require "mason-core.installer.registry.providers.generic") -M.register_provider("github", _.lazy_require "mason-core.installer.registry.providers.github") -M.register_provider("golang", _.lazy_require "mason-core.installer.registry.providers.golang") -M.register_provider("luarocks", _.lazy_require "mason-core.installer.registry.providers.luarocks") -M.register_provider("npm", _.lazy_require "mason-core.installer.registry.providers.npm") -M.register_provider("nuget", _.lazy_require "mason-core.installer.registry.providers.nuget") -M.register_provider("opam", _.lazy_require "mason-core.installer.registry.providers.opam") -M.register_provider("openvsx", _.lazy_require "mason-core.installer.registry.providers.openvsx") -M.register_provider("pypi", _.lazy_require "mason-core.installer.registry.providers.pypi") -M.register_provider("mason", _.lazy_require "mason-core.installer.registry.providers.mason") +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 -function M.get_provider(purl) - return Optional.of_nilable(PROVIDERS[purl.type]):ok_or(("Unknown purl type: %s"):format(purl.type)) +---@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 InstallerProvider +---@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[]> @@ -128,13 +130,13 @@ function M.parse(spec, opts) purl.version = opts.version end - ---@type InstallerProvider - local provider = try(M.get_provider(purl)) - log.trace("Found provider for purl.", source.id) - local parsed_source = try(provider.parse(source, purl, opts)) + ---@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 { - provider = provider, + compiler = compiler, source = vim.tbl_extend("keep", parsed_source, source), raw_source = source, purl = purl, @@ -167,7 +169,7 @@ function M.compile(spec, opts) { _.T, _.identity }, } - ---@type { purl: Purl, provider: InstallerProvider, source: ParsedPackageSource, raw_source: RegistryPackageSource } + ---@type { purl: Purl, compiler: InstallerCompiler, source: ParsedPackageSource, raw_source: RegistryPackageSource } local parsed = try(M.parse(spec, opts):map_err(map_parse_err)) ---@async @@ -176,13 +178,13 @@ function M.compile(spec, opts) return Result.try(function(try) if ctx.opts.version then try(util.ensure_valid_version(function() - return parsed.provider.get_versions(parsed.purl, parsed.raw_source) + return parsed.compiler.get_versions(parsed.purl, parsed.raw_source) end)) end -- Run installer a.scheduler() - try(parsed.provider.install(ctx, parsed.source, parsed.purl)) + 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) diff --git a/lua/mason-core/installer/registry/link.lua b/lua/mason-core/installer/compiler/link.lua index 85e751b7..5d136322 100644 --- a/lua/mason-core/installer/registry/link.lua +++ b/lua/mason-core/installer/compiler/link.lua @@ -2,7 +2,7 @@ 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.registry.expr" +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" diff --git a/lua/mason-core/installer/registry/schemas.lua b/lua/mason-core/installer/compiler/schemas.lua index f9d044af..5e578dbd 100644 --- a/lua/mason-core/installer/registry/schemas.lua +++ b/lua/mason-core/installer/compiler/schemas.lua @@ -1,7 +1,7 @@ local Result = require "mason-core.result" local _ = require "mason-core.functional" local a = require "mason-core.async" -local expr = require "mason-core.installer.registry.expr" +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" diff --git a/lua/mason-core/installer/registry/util.lua b/lua/mason-core/installer/compiler/util.lua index b3735c9c..b3735c9c 100644 --- a/lua/mason-core/installer/registry/util.lua +++ b/lua/mason-core/installer/compiler/util.lua diff --git a/lua/mason-core/installer/context/cwd.lua b/lua/mason-core/installer/context/cwd.lua new file mode 100644 index 00000000..4f645fbb --- /dev/null +++ b/lua/mason-core/installer/context/cwd.lua @@ -0,0 +1,48 @@ +local Result = require "mason-core.result" +local fs = require "mason-core.fs" +local path = require "mason-core.path" + +---@class InstallContextCwd +---@field private location InstallLocation Defines the upper boundary for which paths are allowed as cwd. +---@field private cwd string? +local InstallContextCwd = {} +InstallContextCwd.__index = InstallContextCwd + +---@param location InstallLocation +function InstallContextCwd.new(location) + assert(location, "location not provided") + return setmetatable({ + location = location, + cwd = nil, + }, InstallContextCwd) +end + +---@param handle InstallHandle +function InstallContextCwd:initialize(handle) + return Result.try(function(try) + local staging_dir = self.location:staging(handle.package.name) + if fs.async.dir_exists(staging_dir) then + try(Result.pcall(fs.async.rmrf, staging_dir)) + end + try(Result.pcall(fs.async.mkdirp, staging_dir)) + self:set(staging_dir) + end) +end + +function InstallContextCwd:get() + assert(self.cwd ~= nil, "Tried to access cwd before it was set.") + return self.cwd +end + +---@param new_abs_cwd string +function InstallContextCwd:set(new_abs_cwd) + assert(type(new_abs_cwd) == "string", "new_cwd is not a string") + assert( + path.is_subdirectory(self.location:get_dir(), new_abs_cwd), + ("%q is not a subdirectory of %q"):format(new_abs_cwd, self.location) + ) + self.cwd = new_abs_cwd + return self +end + +return InstallContextCwd diff --git a/lua/mason-core/installer/context/fs.lua b/lua/mason-core/installer/context/fs.lua new file mode 100644 index 00000000..5c51fb56 --- /dev/null +++ b/lua/mason-core/installer/context/fs.lua @@ -0,0 +1,108 @@ +local fs = require "mason-core.fs" +local log = require "mason-core.log" +local path = require "mason-core.path" + +---@class InstallContextFs +---@field private cwd InstallContextCwd +local InstallContextFs = {} +InstallContextFs.__index = InstallContextFs + +---@param cwd InstallContextCwd +function InstallContextFs.new(cwd) + return setmetatable({ cwd = cwd }, InstallContextFs) +end + +---@async +---@param rel_path string The relative path from the current working directory to the file to append. +---@param contents string +function InstallContextFs:append_file(rel_path, contents) + return fs.async.append_file(path.concat { self.cwd:get(), rel_path }, contents) +end + +---@async +---@param rel_path string The relative path from the current working directory to the file to write. +---@param contents string +function InstallContextFs:write_file(rel_path, contents) + return fs.async.write_file(path.concat { self.cwd:get(), rel_path }, contents) +end + +---@async +---@param rel_path string The relative path from the current working directory to the file to read. +function InstallContextFs:read_file(rel_path) + return fs.async.read_file(path.concat { self.cwd:get(), rel_path }) +end + +---@async +---@param rel_path string The relative path from the current working directory. +function InstallContextFs:file_exists(rel_path) + return fs.async.file_exists(path.concat { self.cwd:get(), rel_path }) +end + +---@async +---@param rel_path string The relative path from the current working directory. +function InstallContextFs:dir_exists(rel_path) + return fs.async.dir_exists(path.concat { self.cwd:get(), rel_path }) +end + +---@async +---@param rel_path string The relative path from the current working directory. +function InstallContextFs:rmrf(rel_path) + return fs.async.rmrf(path.concat { self.cwd:get(), rel_path }) +end + +---@async +---@param rel_path string The relative path from the current working directory. +function InstallContextFs:unlink(rel_path) + return fs.async.unlink(path.concat { self.cwd:get(), rel_path }) +end + +---@async +---@param old_path string +---@param new_path string +function InstallContextFs:rename(old_path, new_path) + return fs.async.rename(path.concat { self.cwd:get(), old_path }, path.concat { self.cwd:get(), new_path }) +end + +---@async +---@param dir_path string +function InstallContextFs:mkdir(dir_path) + return fs.async.mkdir(path.concat { self.cwd:get(), dir_path }) +end + +---@async +---@param dir_path string +function InstallContextFs:mkdirp(dir_path) + return fs.async.mkdirp(path.concat { self.cwd:get(), dir_path }) +end + +---@async +---@param file_path string +function InstallContextFs:chmod_exec(file_path) + local bit = require "bit" + -- see chmod(2) + local USR_EXEC = 0x40 + local GRP_EXEC = 0x8 + local ALL_EXEC = 0x1 + local EXEC = bit.bor(USR_EXEC, GRP_EXEC, ALL_EXEC) + local fstat = self:fstat(file_path) + if bit.band(fstat.mode, EXEC) ~= EXEC then + local plus_exec = bit.bor(fstat.mode, EXEC) + log.fmt_debug("Setting exec flags on file %s %o -> %o", file_path, fstat.mode, plus_exec) + self:chmod(file_path, plus_exec) -- chmod +x + end +end + +---@async +---@param file_path string +---@param mode integer +function InstallContextFs:chmod(file_path, mode) + return fs.async.chmod(path.concat { self.cwd:get(), file_path }, mode) +end + +---@async +---@param file_path string +function InstallContextFs:fstat(file_path) + return fs.async.fstat(path.concat { self.cwd:get(), file_path }) +end + +return InstallContextFs diff --git a/lua/mason-core/installer/context.lua b/lua/mason-core/installer/context/init.lua index a991cd9f..0d178c4e 100644 --- a/lua/mason-core/installer/context.lua +++ b/lua/mason-core/installer/context/init.lua @@ -1,213 +1,37 @@ -local Optional = require "mason-core.optional" +local Result = require "mason-core.result" local _ = require "mason-core.functional" 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 receipt = require "mason-core.receipt" -local spawn = require "mason-core.spawn" - ----@class ContextualSpawn ----@field strict_mode boolean Whether spawn failures should raise an exception rather then return a Result. ----@field cwd CwdManager ----@field handle InstallHandle ----@field [string] async fun(opts: SpawnArgs): Result -local ContextualSpawn = {} - ----@param cwd CwdManager ----@param handle InstallHandle ----@param strict_mode boolean -function ContextualSpawn.new(cwd, handle, strict_mode) - return setmetatable({ cwd = cwd, handle = handle, strict_mode = strict_mode }, ContextualSpawn) -end - ----@param cmd string -function ContextualSpawn:__index(cmd) - ---@param args JobSpawnOpts - return function(args) - args.cwd = args.cwd or self.cwd:get() - args.stdio_sink = args.stdio_sink or self.handle.stdio.sink - local on_spawn = args.on_spawn - local captured_handle - args.on_spawn = function(handle, stdio, pid, ...) - captured_handle = handle - self.handle:register_spawn_handle(handle, pid, cmd, spawn._flatten_cmd_args(args)) - if on_spawn then - on_spawn(handle, stdio, pid, ...) - end - end - local function pop_spawn_stack() - if captured_handle then - self.handle:deregister_spawn_handle(captured_handle) - end - end - local result = spawn[cmd](args):on_success(pop_spawn_stack):on_failure(pop_spawn_stack) - if self.strict_mode then - return result:get_or_throw() - else - return result - end - end -end - ----@class ContextualFs ----@field private cwd CwdManager -local ContextualFs = {} -ContextualFs.__index = ContextualFs - ----@param cwd CwdManager -function ContextualFs.new(cwd) - return setmetatable({ cwd = cwd }, ContextualFs) -end - ----@async ----@param rel_path string The relative path from the current working directory to the file to append. ----@param contents string -function ContextualFs:append_file(rel_path, contents) - return fs.async.append_file(path.concat { self.cwd:get(), rel_path }, contents) -end - ----@async ----@param rel_path string The relative path from the current working directory to the file to write. ----@param contents string -function ContextualFs:write_file(rel_path, contents) - return fs.async.write_file(path.concat { self.cwd:get(), rel_path }, contents) -end - ----@async ----@param rel_path string The relative path from the current working directory to the file to read. -function ContextualFs:read_file(rel_path) - return fs.async.read_file(path.concat { self.cwd:get(), rel_path }) -end - ----@async ----@param rel_path string The relative path from the current working directory. -function ContextualFs:file_exists(rel_path) - return fs.async.file_exists(path.concat { self.cwd:get(), rel_path }) -end - ----@async ----@param rel_path string The relative path from the current working directory. -function ContextualFs:dir_exists(rel_path) - return fs.async.dir_exists(path.concat { self.cwd:get(), rel_path }) -end - ----@async ----@param rel_path string The relative path from the current working directory. -function ContextualFs:rmrf(rel_path) - return fs.async.rmrf(path.concat { self.cwd:get(), rel_path }) -end - ----@async ----@param rel_path string The relative path from the current working directory. -function ContextualFs:unlink(rel_path) - return fs.async.unlink(path.concat { self.cwd:get(), rel_path }) -end - ----@async ----@param old_path string ----@param new_path string -function ContextualFs:rename(old_path, new_path) - return fs.async.rename(path.concat { self.cwd:get(), old_path }, path.concat { self.cwd:get(), new_path }) -end - ----@async ----@param dir_path string -function ContextualFs:mkdir(dir_path) - return fs.async.mkdir(path.concat { self.cwd:get(), dir_path }) -end - ----@async ----@param dir_path string -function ContextualFs:mkdirp(dir_path) - return fs.async.mkdirp(path.concat { self.cwd:get(), dir_path }) -end - ----@async ----@param file_path string -function ContextualFs:chmod_exec(file_path) - local bit = require "bit" - -- see chmod(2) - local USR_EXEC = 0x40 - local GRP_EXEC = 0x8 - local ALL_EXEC = 0x1 - local EXEC = bit.bor(USR_EXEC, GRP_EXEC, ALL_EXEC) - local fstat = self:fstat(file_path) - if bit.band(fstat.mode, EXEC) ~= EXEC then - local plus_exec = bit.bor(fstat.mode, EXEC) - log.fmt_debug("Setting exec flags on file %s %o -> %o", file_path, fstat.mode, plus_exec) - self:chmod(file_path, plus_exec) -- chmod +x - end -end - ----@async ----@param file_path string ----@param mode integer -function ContextualFs:chmod(file_path, mode) - return fs.async.chmod(path.concat { self.cwd:get(), file_path }, mode) -end - ----@async ----@param file_path string -function ContextualFs:fstat(file_path) - return fs.async.fstat(path.concat { self.cwd:get(), file_path }) -end - ----@class CwdManager ----@field private install_prefix string Defines the upper boundary for which paths are allowed as cwd. ----@field private cwd string -local CwdManager = {} -CwdManager.__index = CwdManager - -function CwdManager.new(install_prefix) - assert(type(install_prefix) == "string", "install_prefix not provided") - return setmetatable({ - install_prefix = install_prefix, - cwd = nil, - }, CwdManager) -end - -function CwdManager:get() - assert(self.cwd ~= nil, "Tried to access cwd before it was set.") - return self.cwd -end - ----@param new_cwd string -function CwdManager:set(new_cwd) - assert(type(new_cwd) == "string", "new_cwd is not a string") - assert( - path.is_subdirectory(self.install_prefix, new_cwd), - ("%q is not a subdirectory of %q"):format(new_cwd, self.install_prefix) - ) - self.cwd = new_cwd -end ---@class InstallContext ----@field public receipt InstallReceiptBuilder ----@field public requested_version Optional ----@field public fs ContextualFs ----@field public spawn ContextualSpawn ----@field public handle InstallHandle ----@field public package Package ----@field public cwd CwdManager ----@field public opts PackageInstallOpts ----@field public stdio_sink StdioSink +---@field receipt InstallReceiptBuilder +---@field fs InstallContextFs +---@field spawn InstallContextSpawn +---@field handle InstallHandle +---@field package Package +---@field cwd InstallContextCwd +---@field opts PackageInstallOpts +---@field stdio_sink StdioSink ---@field links { bin: table<string, string>, share: table<string, string>, opt: table<string, string> } local InstallContext = {} InstallContext.__index = InstallContext ---@param handle InstallHandle +---@param cwd InstallContextCwd +---@param spawn InstallContextSpawn +---@param fs InstallContextFs ---@param opts PackageInstallOpts -function InstallContext.new(handle, opts) - local cwd_manager = CwdManager.new(path.install_prefix()) +function InstallContext.new(handle, cwd, spawn, fs, opts) return setmetatable({ - cwd = cwd_manager, - spawn = ContextualSpawn.new(cwd_manager, handle, false), + cwd = cwd, + spawn = spawn, handle = handle, package = handle.package, -- for convenience - fs = ContextualFs.new(cwd_manager), + fs = fs, receipt = receipt.InstallReceiptBuilder.new(), - requested_version = Optional.of_nilable(opts.version), stdio_sink = handle.stdio.sink, links = { bin = {}, @@ -227,8 +51,8 @@ function InstallContext:promote_cwd() return end log.fmt_debug("Promoting cwd %s to %s", cwd, install_path) - -- 1. Unlink any existing installation - self.handle.package:unlink() + -- 1. Uninstall any existing installation + self.handle.package:uninstall() -- 2. Prepare for renaming cwd to destination if platform.is.unix then -- Some Unix systems will raise an error when renaming a directory to a destination that does not already exist. @@ -396,4 +220,42 @@ function InstallContext:link_bin(executable, rel_path) return self end +InstallContext.CONTEXT_REQUEST = {} + +---@generic T +---@param fn fun(context: InstallContext): T +---@return T +function InstallContext:execute(fn) + local thread = coroutine.create(function(...) + -- We wrap the function to allow it to be a spy instance (in which case it's not actually a function, but a + -- callable metatable - coroutine.create strictly expects functions only) + return fn(...) + end) + local step + local ret_val + step = function(...) + local ok, result = coroutine.resume(thread, ...) + if not ok then + error(result, 0) + elseif result == InstallContext.CONTEXT_REQUEST then + step(self) + elseif coroutine.status(thread) == "suspended" then + -- yield to parent coroutine + step(coroutine.yield(result)) + else + ret_val = result + end + end + step(self) + return ret_val +end + +---@async +function InstallContext:build_receipt() + log.fmt_debug("Building receipt for %s", self.package) + return Result.pcall(function() + return self.receipt:with_name(self.package.name):with_completion_time(vim.loop.gettimeofday()):build() + end) +end + return InstallContext diff --git a/lua/mason-core/installer/context/spawn.lua b/lua/mason-core/installer/context/spawn.lua new file mode 100644 index 00000000..6528c4b3 --- /dev/null +++ b/lua/mason-core/installer/context/spawn.lua @@ -0,0 +1,46 @@ +local spawn = require "mason-core.spawn" + +---@class InstallContextSpawn +---@field strict_mode boolean Whether spawn failures should raise an exception rather then return a Result. +---@field private cwd InstallContextCwd +---@field private handle InstallHandle +---@field [string] async fun(opts: SpawnArgs): Result +local InstallContextSpawn = {} + +---@param cwd InstallContextCwd +---@param handle InstallHandle +---@param strict_mode boolean +function InstallContextSpawn.new(cwd, handle, strict_mode) + return setmetatable({ cwd = cwd, handle = handle, strict_mode = strict_mode }, InstallContextSpawn) +end + +---@param cmd string +function InstallContextSpawn:__index(cmd) + ---@param args JobSpawnOpts + return function(args) + args.cwd = args.cwd or self.cwd:get() + args.stdio_sink = args.stdio_sink or self.handle.stdio.sink + local on_spawn = args.on_spawn + local captured_handle + args.on_spawn = function(handle, stdio, pid, ...) + captured_handle = handle + self.handle:register_spawn_handle(handle, pid, cmd, spawn._flatten_cmd_args(args)) + if on_spawn then + on_spawn(handle, stdio, pid, ...) + end + end + local function pop_spawn_stack() + if captured_handle then + self.handle:deregister_spawn_handle(captured_handle) + end + end + local result = spawn[cmd](args):on_success(pop_spawn_stack):on_failure(pop_spawn_stack) + if self.strict_mode then + return result:get_or_throw() + else + return result + end + end +end + +return InstallContextSpawn diff --git a/lua/mason-core/installer/handle.lua b/lua/mason-core/installer/handle.lua index f9b03557..96acbdd1 100644 --- a/lua/mason-core/installer/handle.lua +++ b/lua/mason-core/installer/handle.lua @@ -120,6 +120,10 @@ function InstallHandle:is_closed() return self.state == "CLOSED" end +function InstallHandle:is_closing() + return self:is_closed() or self.is_terminated +end + ---@param new_state InstallHandleState function InstallHandle:set_state(new_state) local old_state = self.state diff --git a/lua/mason-core/installer/init.lua b/lua/mason-core/installer/init.lua index 45bba46b..37c74fcb 100644 --- a/lua/mason-core/installer/init.lua +++ b/lua/mason-core/installer/init.lua @@ -1,263 +1,10 @@ local InstallContext = require "mason-core.installer.context" -local Result = require "mason-core.result" -local _ = require "mason-core.functional" -local a = require "mason-core.async" -local control = require "mason-core.async.control" -local fs = require "mason-core.fs" -local linker = require "mason-core.installer.linker" -local log = require "mason-core.log" -local path = require "mason-core.path" -local settings = require "mason.settings" - -local Semaphore = control.Semaphore - -local sem = Semaphore.new(settings.current.max_concurrent_installers) local M = {} ----@async -function M.create_prefix_dirs() - return Result.try(function(try) - for _, p in ipairs { - path.install_prefix(), - path.bin_prefix(), - path.share_prefix(), - path.package_prefix(), - path.package_build_prefix(), - } do - if not fs.async.dir_exists(p) then - try(Result.pcall(fs.async.mkdirp, p)) - end - end - end) -end - ----@async ----@param context InstallContext -local function build_receipt(context) - return Result.pcall(function() - log.fmt_debug("Building receipt for %s", context.package) - return context.receipt:with_name(context.package.name):with_completion_time(vim.loop.gettimeofday()):build() - end) -end - -local CONTEXT_REQUEST = {} - ---@return InstallContext function M.context() - return coroutine.yield(CONTEXT_REQUEST) -end - ----@async ----@param ctx InstallContext -local function lock_package(ctx) - log.debug("Attempting to lock package", ctx.package) - local lockfile = path.package_lock(ctx.package.name) - if not ctx.opts.force and fs.async.file_exists(lockfile) then - log.error("Lockfile already exists.", ctx.package) - return Result.failure( - ("Lockfile exists, installation is already running in another process (pid: %s). Run with :MasonInstall --force to bypass."):format( - fs.sync.read_file(lockfile) - ) - ) - end - a.scheduler() - fs.async.write_file(lockfile, vim.fn.getpid()) - log.debug("Wrote lockfile", ctx.package) - return Result.success(lockfile) -end - ----@async ----@param context InstallContext -function M.prepare_installer(context) - local installer = require "mason-core.installer.registry" - return Result.try(function(try) - local package_build_prefix = path.package_build_prefix(context.package.name) - if fs.async.dir_exists(package_build_prefix) then - try(Result.pcall(fs.async.rmrf, package_build_prefix)) - end - try(Result.pcall(fs.async.mkdirp, package_build_prefix)) - context.cwd:set(package_build_prefix) - - return try(installer.compile(context.handle.package.spec, context.opts)) - end) -end - ----@generic T ----@param context InstallContext ----@param fn fun(context: InstallContext): T ----@return T -function M.exec_in_context(context, fn) - local thread = coroutine.create(function(...) - -- We wrap the function to allow it to be a spy instance (in which case it's not actually a function, but a - -- callable metatable - coroutine.create strictly expects functions only) - return fn(...) - end) - local step - local ret_val - step = function(...) - local ok, result = coroutine.resume(thread, ...) - if not ok then - error(result, 0) - elseif result == CONTEXT_REQUEST then - step(context) - elseif coroutine.status(thread) == "suspended" then - -- yield to parent coroutine - step(coroutine.yield(result)) - else - ret_val = result - end - end - context.receipt:with_start_time(vim.loop.gettimeofday()) - step(context) - return ret_val -end - ----@async ----@param context InstallContext ----@param installer async fun(ctx: InstallContext) -local function run_installer(context, installer) - local handle = context.handle - return Result.pcall(function() - return a.wait(function(resolve, reject) - local cancel_thread = a.run(M.exec_in_context, function(success, result) - if success then - resolve(result) - else - reject(result) - end - end, context, installer) - - handle:once("terminate", function() - cancel_thread() - if handle:is_closed() then - reject "Installation was aborted." - else - handle:once("closed", function() - reject "Installation was aborted." - end) - end - end) - end) - end) -end - ----@async ----@param handle InstallHandle ----@param opts PackageInstallOpts -function M.execute(handle, opts) - if handle:is_active() or handle:is_closed() then - log.fmt_debug("Received active or closed handle %s", handle) - return Result.failure "Invalid handle state." - end - - handle:queued() - local permit = sem:acquire() - if handle:is_closed() then - permit:forget() - log.fmt_trace("Installation was aborted %s", handle) - return Result.failure "Installation was aborted." - end - log.fmt_trace("Activating handle %s", handle) - handle:active() - - local pkg = handle.package - local context = InstallContext.new(handle, opts) - local tailed_output = {} - - if opts.debug then - local function append_log(chunk) - tailed_output[#tailed_output + 1] = chunk - end - handle:on("stdout", append_log) - handle:on("stderr", append_log) - end - - log.fmt_info("Executing installer for %s %s", pkg, opts) - - return M.create_prefix_dirs() - :and_then(function() - return lock_package(context) - end) - :and_then(function(lockfile) - local release_lock = _.partial(pcall, fs.async.unlink, lockfile) - return Result.try(function(try) - -- 1. prepare directories and initialize cwd - local installer = try(M.prepare_installer(context)) - - -- 2. execute installer - try(run_installer(context, installer)) - - -- 3. promote temporary installation dir - try(Result.pcall(function() - context:promote_cwd() - end)) - - -- 4. link package - try(linker.link(context)) - - -- 5. build & write receipt - ---@type InstallReceipt - local receipt = try(build_receipt(context)) - try(Result.pcall(function() - receipt:write(context.cwd:get()) - end)) - end) - :on_success(function() - release_lock() - if opts.debug then - context.fs:write_file("mason-debug.log", table.concat(tailed_output, "")) - end - end) - :on_failure(function() - release_lock() - if not opts.debug then - -- clean up installation dir - pcall(function() - fs.async.rmrf(context.cwd:get()) - end) - else - context.fs:write_file("mason-debug.log", table.concat(tailed_output, "")) - context.stdio_sink.stdout( - ("[debug] Installation directory retained at %q.\n"):format(context.cwd:get()) - ) - end - - -- unlink linked executables (in the occasion an error occurs after linking) - build_receipt(context):on_success(function(receipt) - linker.unlink(context.package, receipt):on_failure(function(err) - log.error("Failed to unlink failed installation", err) - end) - end) - end) - end) - :on_success(function() - permit:forget() - handle:close() - log.fmt_info("Installation succeeded for %s", pkg) - end) - :on_failure(function(failure) - permit:forget() - log.fmt_error("Installation failed for %s error=%s", pkg, failure) - context.stdio_sink.stderr(tostring(failure)) - context.stdio_sink.stderr "\n" - - if not handle:is_closed() and not handle.is_terminated then - handle:close() - end - end) -end - ----Runs the provided async functions concurrently and returns their result, once all are resolved. ----This is really just a wrapper around a.wait_all() that makes sure to patch the coroutine context before creating the ----new async execution contexts. ----@async ----@param suspend_fns async fun(ctx: InstallContext)[] -function M.run_concurrently(suspend_fns) - local context = M.context() - return a.wait_all(_.map(function(suspend_fn) - return _.partial(M.exec_in_context, context, suspend_fn) - end, suspend_fns)) + return coroutine.yield(InstallContext.CONTEXT_REQUEST) end return M diff --git a/lua/mason-core/installer/location.lua b/lua/mason-core/installer/location.lua new file mode 100644 index 00000000..2cc038e4 --- /dev/null +++ b/lua/mason-core/installer/location.lua @@ -0,0 +1,63 @@ +local Path = require "mason-core.path" +local Result = require "mason-core.result" +local fs = require "mason-core.fs" + +---@class InstallLocation +---@field private dir string +local InstallLocation = {} +InstallLocation.__index = InstallLocation + +---@param dir string +function InstallLocation.new(dir) + return setmetatable({ + dir = dir, + }, InstallLocation) +end + +function InstallLocation:get_dir() + return self.dir +end + +---@async +function InstallLocation:initialize() + return Result.try(function(try) + for _, p in ipairs { + self.dir, + self:bin(), + self:share(), + self:package(), + self:staging(), + } do + if not fs.async.dir_exists(p) then + try(Result.pcall(fs.async.mkdirp, p)) + end + end + end) +end + +---@param path string? +function InstallLocation:bin(path) + return Path.concat { self.dir, "bin", path } +end + +---@param path string? +function InstallLocation:share(path) + return Path.concat { self.dir, "share", path } +end + +---@param path string? +function InstallLocation:package(path) + return Path.concat { self.dir, "packages", path } +end + +---@param path string? +function InstallLocation:staging(path) + return Path.concat { self.dir, "staging", path } +end + +---@param name string +function InstallLocation:lockfile(name) + return self:staging(("%s.lock"):format(name)) +end + +return InstallLocation diff --git a/lua/mason-core/installer/runner.lua b/lua/mason-core/installer/runner.lua new file mode 100644 index 00000000..175610d5 --- /dev/null +++ b/lua/mason-core/installer/runner.lua @@ -0,0 +1,218 @@ +local Result = require "mason-core.result" +local _ = require "mason-core.functional" +local a = require "mason-core.async" +local compiler = require "mason-core.installer.compiler" +local fs = require "mason-core.fs" +local linker = require "mason-core.installer.linker" +local log = require "mason-core.log" +local registry = require "mason-registry" + +local InstallContext = require "mason-core.installer.context" +local InstallContextCwd = require "mason-core.installer.context.cwd" +local InstallContextFs = require "mason-core.installer.context.fs" +local InstallContextSpawn = require "mason-core.installer.context.spawn" + +---@class InstallRunner +---@field location InstallLocation +---@field handle InstallHandle +---@field semaphore Semaphore +---@field permit Permit? +local InstallRunner = {} +InstallRunner.__index = InstallRunner + +---@param location InstallLocation +---@param handle InstallHandle +---@param semaphore Semaphore +function InstallRunner.new(location, handle, semaphore) + return setmetatable({ + location = location, + semaphore = semaphore, + handle = handle, + }, InstallRunner) +end + +---@param opts PackageInstallOpts +---@param callback? fun(success: boolean, result: any) +function InstallRunner:execute(opts, callback) + local handle = self.handle + log.fmt_info("Executing installer for %s %s", handle.package, opts) + + local context_cwd = InstallContextCwd.new(self.location) + local context_spawn = InstallContextSpawn.new(context_cwd, handle, false) + local context_fs = InstallContextFs.new(context_cwd) + local context = InstallContext.new(handle, context_cwd, context_spawn, context_fs, opts) + + local tailed_output = {} + + if opts.debug then + local function append_log(chunk) + tailed_output[#tailed_output + 1] = chunk + end + handle:on("stdout", append_log) + handle:on("stderr", append_log) + end + + ---@async + local function finalize_logs(success, result) + if not success then + context.stdio_sink.stderr(tostring(result)) + context.stdio_sink.stderr "\n" + end + + if opts.debug then + context.fs:write_file("mason-debug.log", table.concat(tailed_output, "")) + context.stdio_sink.stdout(("[debug] Installation directory retained at %q.\n"):format(context.cwd:get())) + end + end + + ---@async + local finalize = a.scope(function(success, result) + finalize_logs(success, result) + + if not opts.debug and not success then + -- clean up installation dir + pcall(function() + fs.async.rmrf(context.cwd:get()) + end) + end + + if not handle:is_closing() then + handle:close() + end + + self:release_lock() + self:release_permit() + + if callback then + callback(success, result) + end + + if success then + log.fmt_info("Installation succeeded for %s", handle.package) + handle.package:emit("install:success", handle) + registry:emit("package:install:success", handle.package, handle) + else + log.fmt_error("Installation failed for %s error=%s", handle.package, result) + handle.package:emit("install:failed", handle, result) + registry:emit("package:install:failed", handle.package, handle, result) + end + end) + + local cancel_execution = a.run(function() + return Result.try(function(try) + try(self:acquire_permit()) + try(self.location:initialize()) + try(self:acquire_lock(opts.force)) + + context.receipt:with_start_time(vim.loop.gettimeofday()) + + -- 1. initialize working directory + try(context_cwd:initialize(handle)) + + -- 2. run installer + ---@type async fun(ctx: InstallContext): Result + local installer = try(compiler.compile(handle.package.spec, opts)) + try(context:execute(installer)) + + -- 3. promote temporary installation dir + try(Result.pcall(function() + context:promote_cwd() + end)) + + -- 4. link package & write receipt + return linker + .link(context) + :and_then(function() + return context:build_receipt(context) + end) + :and_then( + ---@param receipt InstallReceipt + function(receipt) + return receipt:write(context.cwd:get()) + end + ) + :on_failure(function() + -- unlink any links that were made before failure + context:build_receipt():on_success( + ---@param receipt InstallReceipt + function(receipt) + linker.unlink(handle.package, receipt):on_failure(function(err) + log.error("Failed to unlink failed installation.", err) + end) + end + ) + end) + end):get_or_throw() + end, finalize) + + handle:once("terminate", function() + cancel_execution() + local function on_close() + finalize(false, "Installation was aborted.") + end + if handle:is_closed() then + on_close() + else + handle:once("closed", on_close) + end + end) +end + +---@async +---@private +function InstallRunner:release_lock() + pcall(fs.async.unlink, self.location:lockfile(self.handle.package.name)) +end + +---@async +---@param force boolean? +---@private +function InstallRunner:acquire_lock(force) + local pkg = self.handle.package + log.debug("Attempting to lock package", pkg) + local lockfile = self.location:lockfile(pkg.name) + if force ~= true and fs.async.file_exists(lockfile) then + log.error("Lockfile already exists.", pkg) + return Result.failure( + ("Lockfile exists, installation is already running in another process (pid: %s). Run with :MasonInstall --force to bypass."):format( + fs.async.read_file(lockfile) + ) + ) + end + a.scheduler() + fs.async.write_file(lockfile, vim.fn.getpid()) + log.debug("Wrote lockfile", pkg) + return Result.success(lockfile) +end + +---@async +---@private +function InstallRunner:acquire_permit() + local handle = self.handle + if handle:is_active() or handle:is_closed() then + log.fmt_debug("Received active or closed handle %s", handle) + return Result.failure "Invalid handle state." + end + + handle:queued() + local permit = self.semaphore:acquire() + if handle:is_closed() then + permit:forget() + log.fmt_trace("Installation was aborted %s", handle) + return Result.failure "Installation was aborted." + end + log.fmt_trace("Activating handle %s", handle) + handle:active() + self.permit = permit + return Result.success() +end + +---@private +function InstallRunner:release_permit() + if self.permit then + self.permit:forget() + self.permit = nil + end +end + +return InstallRunner diff --git a/lua/mason-core/package/init.lua b/lua/mason-core/package/init.lua index bc98a72a..b0da8a61 100644 --- a/lua/mason-core/package/init.lua +++ b/lua/mason-core/package/init.lua @@ -1,14 +1,17 @@ local EventEmitter = require "mason-core.EventEmitter" +local InstallLocation = require "mason-core.installer.location" +local InstallRunner = require "mason-core.installer.runner" 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 fs = require "mason-core.fs" local log = require "mason-core.log" local path = require "mason-core.path" local platform = require "mason-core.platform" local registry = require "mason-registry" +local settings = require "mason.settings" +local Semaphore = require("mason-core.async.control").Semaphore ---@class Package : EventEmitter ---@field name string @@ -135,81 +138,56 @@ end ---@alias PackageInstallOpts { version?: string, debug?: boolean, target?: string, force?: boolean, strict?: boolean } ----@param opts? PackageInstallOpts ----@return InstallHandle -function Package:install(opts) - opts = opts or {} +-- TODO this needs to be elsewhere +local semaphore = Semaphore.new(settings.current.max_concurrent_installers) + +function Package:is_installing() return self:get_handle() - :map(function(handle) - if not handle:is_closed() then - log.fmt_debug("Handle %s already exist for package %s", handle, self) - return handle + :map( + ---@param handle InstallHandle + function(handle) + return not handle:is_closed() end - end) - :or_else_get(function() - local handle = self:new_handle() - a.run( - require("mason-core.installer").execute, - ---@param success boolean - ---@param result Result - function(success, result) - if not success then - -- Installer failed abnormally (i.e. unexpected exception in the installer code itself). - log.error("Unexpected error", result) - handle.stdio.sink.stderr(tostring(result)) - handle.stdio.sink.stderr "\nInstallation failed abnormally. Please report this error." - self:emit("install:failed", handle) - registry:emit("package:install:failed", self, handle) + ) + :or_else(false) +end - -- We terminate _after_ emitting failure events because [termination -> failed] have different - -- meaning than [failed -> terminate] ([termination -> failed] is interpreted as a triggered - -- termination). - if not handle:is_closed() and not handle.is_terminated then - handle:terminate() - end - return - end - result - :on_success(function() - self:emit("install:success", handle) - registry:emit("package:install:success", self, handle) - end) - :on_failure(function() - self:emit("install:failed", handle) - registry:emit("package:install:failed", self, handle) - end) - end, - handle, - opts - ) - return handle - end) +---@param opts? PackageInstallOpts +---@param callback? fun(success: boolean, result: any) +---@return InstallHandle +function Package:install(opts, callback) + opts = opts or {} + assert(not self:is_installing(), "Package is already installing.") + local handle = self:new_handle() + local runner = InstallRunner.new(InstallLocation.new(settings.current.install_root_dir), handle, semaphore) + runner:execute(opts, callback) + return handle end +---@return boolean function Package:uninstall() - local was_unlinked = self:unlink() - if was_unlinked then - self:emit "uninstall:success" - registry:emit("package:uninstall:success", self) - end - return was_unlinked + return self:get_receipt() + :map(function(receipt) + self:unlink(receipt) + self:emit("uninstall:success", receipt) + registry:emit("package:uninstall:success", self, receipt) + return true + end) + :or_else(false) end -function Package:unlink() +---@private +---@param receipt InstallReceipt +function Package:unlink(receipt) log.fmt_trace("Unlinking %s", self) local install_path = self:get_install_path() + -- 1. Unlink - self:get_receipt():if_present(function(receipt) - local linker = require "mason-core.installer.linker" - linker.unlink(self, receipt):get_or_throw() - end) + local linker = require "mason-core.installer.linker" + linker.unlink(self, receipt):get_or_throw() -- 2. Remove installation artifacts - if fs.sync.dir_exists(install_path) then - fs.sync.rmrf(install_path) - return true - end - return false + fs.sync.rmrf(install_path) end function Package:is_installed() @@ -260,18 +238,18 @@ end ---@param opts? PackageInstallOpts function Package:is_installable(opts) - return require("mason-core.installer.registry").parse(self.spec, opts or {}):is_success() + return require("mason-core.installer.compiler").parse(self.spec, opts or {}):is_success() end ---@return Result # Result<string[]> function Package:get_all_versions() - local registry_installer = require "mason-core.installer.registry" + local compiler = require "mason-core.installer.compiler" return Result.try(function(try) ---@type Purl local purl = try(Purl.parse(self.spec.source.id)) - ---@type InstallerProvider - local provider = try(registry_installer.get_provider(purl)) - return provider.get_versions(purl, self.spec.source) + ---@type InstallerCompiler + local compiler = try(compiler.get_compiler(purl)) + return compiler.get_versions(purl, self.spec.source) end) end diff --git a/lua/mason-core/receipt.lua b/lua/mason-core/receipt.lua index d9fe9d88..748cab38 100644 --- a/lua/mason-core/receipt.lua +++ b/lua/mason-core/receipt.lua @@ -1,3 +1,7 @@ +local Result = require "mason-core.result" +local fs = require "mason-core.fs" +local path = require "mason-core.path" + local M = {} ---@alias InstallReceiptSchemaVersion @@ -56,11 +60,11 @@ function InstallReceipt:get_links() end ---@async ----@param cwd string -function InstallReceipt:write(cwd) - local path = require "mason-core.path" - local fs = require "mason-core.fs" - fs.async.write_file(path.concat { cwd, "mason-receipt.json" }, vim.json.encode(self)) +---@param dir string +function InstallReceipt:write(dir) + return Result.pcall(function() + fs.async.write_file(path.concat { dir, "mason-receipt.json" }, vim.json.encode(self)) + end) end ---@class InstallReceiptBuilder diff --git a/lua/mason-registry/sources/util.lua b/lua/mason-registry/sources/util.lua index 80d5f16f..04ab7845 100644 --- a/lua/mason-registry/sources/util.lua +++ b/lua/mason-registry/sources/util.lua @@ -1,8 +1,8 @@ local Optional = require "mason-core.optional" local Pkg = require "mason-core.package" local _ = require "mason-core.functional" +local compiler = require "mason-core.installer.compiler" local log = require "mason-core.log" -local registry_installer = require "mason-core.installer.registry" local M = {} @@ -10,7 +10,7 @@ local M = {} function M.map_registry_spec(spec) spec.schema = spec.schema or "registry+v1" - if not registry_installer.SCHEMA_CAP[spec.schema] then + if not compiler.SCHEMA_CAP[spec.schema] then log.fmt_debug("Excluding package=%s with unsupported schema_version=%s", spec.name, spec.schema) return Optional.empty() end diff --git a/lua/mason-test/helpers.lua b/lua/mason-test/helpers.lua new file mode 100644 index 00000000..57b486ea --- /dev/null +++ b/lua/mason-test/helpers.lua @@ -0,0 +1,33 @@ +local InstallContext = require "mason-core.installer.context" +local InstallContextCwd = require "mason-core.installer.context.cwd" +local InstallContextFs = require "mason-core.installer.context.fs" +local InstallContextSpawn = require "mason-core.installer.context.spawn" +local InstallHandle = require "mason-core.installer.handle" +local InstallLocation = require "mason-core.installer.location" +local Result = require "mason-core.result" +local registry = require "mason-registry" +local spy = require "luassert.spy" + +local M = {} + +---@param opts? { install_opts?: PackageInstallOpts, package?: string } +function M.create_context(opts) + local pkg = registry.get_package(opts and opts.package or "dummy") + local handle = InstallHandle.new(pkg) + local location = InstallLocation.new "/tmp/install-dir" + local context_cwd = InstallContextCwd.new(location):set(location.dir) + local context_spawn = InstallContextSpawn.new(context_cwd, handle, false) + local context_fs = InstallContextFs.new(context_cwd) + local context = InstallContext.new(handle, context_cwd, context_spawn, context_fs, opts and opts.install_opts or {}) + context.spawn = setmetatable({}, { + __index = function(s, cmd) + s[cmd] = spy.new(function() + return Result.success { stdout = nil, stderr = nil } + end) + return s[cmd] + end, + }) + return context +end + +return M |
