diff options
| author | William Boman <william@redwill.se> | 2023-10-11 16:31:50 +0200 |
|---|---|---|
| committer | William Boman <william@redwill.se> | 2025-02-19 09:22:40 +0100 |
| commit | 047ec18da56ad8f331e5c6bc7417dc5a9a6e71cc (patch) | |
| tree | c50c22cd05d3605fc5a1e8eb902ffeb11e339697 /lua/mason-core | |
| parent | refactor(receipt): change receipt structure and remove old builder APIs (#1521) (diff) | |
| download | mason-047ec18da56ad8f331e5c6bc7417dc5a9a6e71cc.tar mason-047ec18da56ad8f331e5c6bc7417dc5a9a6e71cc.tar.gz mason-047ec18da56ad8f331e5c6bc7417dc5a9a6e71cc.tar.bz2 mason-047ec18da56ad8f331e5c6bc7417dc5a9a6e71cc.tar.lz mason-047ec18da56ad8f331e5c6bc7417dc5a9a6e71cc.tar.xz mason-047ec18da56ad8f331e5c6bc7417dc5a9a6e71cc.tar.zst mason-047ec18da56ad8f331e5c6bc7417dc5a9a6e71cc.zip | |
refactor!: refactor installer internals and add new Package class methods (#1523)
This contains the following changes:
1) `Package:install()` now accepts a second, optional, callback argument which is called when installation finishes
(successfully or not).
2) Adds a `Package:is_installing()` method.
This contains the following breaking changes:
1) `Package:install()` will now error when called while an installation is already ongoing. Use the new
`Package:is_installing()` method to check whether an installation is already running.
This also refactors large portions of the tests by removing test globals, removing async_test, and adding the
`mason-test` Lua module instead. Test helpers via globals are problematic to work with due to not being detected through
tools like the Lua language server without additional configuration. This has been replaced with a Lua module
`mason-test`. `async_test` has also been removed in favour of explicitly making use of the `mason-core.async` API. These
changes stands for a significant portion of the diff.
Diffstat (limited to 'lua/mason-core')
| -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 |
32 files changed, 666 insertions, 582 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 |
