diff options
59 files changed, 1254 insertions, 1372 deletions
diff --git a/doc/reference.md b/doc/reference.md deleted file mode 100644 index 23eb306c..00000000 --- a/doc/reference.md +++ /dev/null @@ -1,504 +0,0 @@ -# Mason API reference - -This document contains the API reference for `mason.nvim`'s' public APIs and is a more in-depth complementary to the -documentation available in `:h mason`. -The intended audience of this document are plugin developers and people who want to further customize their own Neovim -configuration. - -_Note that APIs not listed in this document (or `:h mason`) are not considered public, and are subject to unannounced, -breaking, changes. Use at own risk._ - -Please [reach out](https://github.com/williamboman/mason.nvim/discussions/new?category=api-suggestions) if you think -something is missing or if something could be improved! - ---- - -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT -RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [BCP 14][bcp14], -[RFC2119][rfc2119], and [RFC8174][rfc8174] when, and only when, they appear in all capitals, as shown here. - ---- - -[bcp14]: https://tools.ietf.org/html/bcp14 -[rfc2119]: https://tools.ietf.org/html/rfc2119 -[rfc8174]: https://tools.ietf.org/html/rfc8174 - -<!--toc:start--> -- [Architecture diagram](#architecture-diagram) -- [Registry events](#registry-events) -- [`RegistryPackageSpec`](#registrypackagespec) -- [`Package`](#package) - - [`Package.Parse({package_identifier})`](#packageparsepackage_identifier) - - [`Package.Lang`](#packagelang) - - [`Package.Cat`](#packagecat) - - [`Package.License`](#packagelicense) - - [`Package:new({spec})`](#packagenewspec) - - [`Package.spec`](#packagespec) - - [`Package:is_installing()`](#packageis_installing) - - [`Package:install({opts?}, {callback?})`](#packageinstallopts-callback) - - [`Package:uninstall()`](#packageuninstall) - - [`Package:is_installed()`](#packageis_installed) - - [`Package:get_install_path()`](#packageget_install_path) - - [`Package:get_installed_version()`](#packageget_installed_version) - - [`Package:get_latest_version()`](#packageget_latest_version) - - [`Package:is_installable({opts?})`](#packageis_installableopts) -- [`PackageInstallOpts`](#packageinstallopts-1) -- [`InstallContext`](#installcontext) - - [`InstallContext.package`](#installcontextpackage) - - [`InstallContext.handle`](#installcontexthandle) - - [`InstallContext.cwd`](#installcontextcwd) - - [`InstallContext.spawn`](#installcontextspawn) - - [`InstallContext.fs`](#installcontextfs) - - [`InstallContext.requested_version`](#installcontextrequested_version) - - [`InstallContext.stdio_sink`](#installcontextstdio_sink) -- [`ContextualFs`](#contextualfs) -- [`ContextualSpawn`](#contextualspawn) -- [`CwdManager`](#cwdmanager) - - [`CwdManager:set({cwd)})`](#cwdmanagersetcwd) - - [`CwdManager:get()`](#cwdmanagerget) -- [`InstallHandleState`](#installhandlestate) -- [`InstallHandle`](#installhandle) - - [`InstallHandle.package`](#installhandlepackage) - - [`InstallHandle.state`](#installhandlestate) - - [`InstallHandle.is_terminated`](#installhandleis_terminated) - - [`InstallHandle:is_idle()`](#installhandleis_idle) - - [`InstallHandle:is_queued()`](#installhandleis_queued) - - [`InstallHandle:is_active()`](#installhandleis_active) - - [`InstallHandle:is_closed()`](#installhandleis_closed) - - [`InstallHandle:kill({signal})`](#installhandlekillsignal) - - [`InstallHandle:terminate()`](#installhandleterminate) -- [`EventEmitter`](#eventemitter) - - [`EventEmitter:on({event}, {handler})`](#eventemitteronevent-handler) - - [`EventEmitter:once({event, handler})`](#eventemitteronceevent-handler) - - [`EventEmitter:off({event}, {handler})`](#eventemitteroffevent-handler) -<!--toc:end--> - -## Architecture diagram - -<!-- https://excalidraw.com/#json=vbTmp7nM8H5odJDiaw7Ue,TghucvHHAw8bl7sgX1VuvA --> - - - -## Registry events - -The `mason-registry` Lua module extends the [EventEmitter](#eventemitter) interface and emits the following events: - -| Event | Handler signature | -| --------------------------- | ------------------------------------------------------ | -| `package:handle` | `fun(pkg: Package, handle: InstallHandle)` | -| `package:install:success` | `fun(pkg: Package, handle: InstallHandle)` | -| `package:install:failed` | `fun(pkg: Package, handle: InstallHandle, error: any)` | -| `package:uninstall:success` | `fun(pkg: Package)` | - -The following is an example for how to register handlers for events: - -```lua -local registry = require "mason-registry" - -registry:on( - "package:handle", - vim.schedule_wrap(function(pkg, handle) - print(string.format("Installing %s", pkg.name)) - end) -) - -registry:on( - "package:install:success", - vim.schedule_wrap(function(pkg, handle) - print(string.format("Successfully installed %s", pkg.name)) - end) -) -``` - -## `RegistryPackageSpec` - -| Key | Value | -| ----------- | ------------------------------------- | -| schema | `"registry+v1"` | -| name | `string` | -| description | `string` | -| homepage | `string` | -| licenses | [`PackageLicense[]`](#packagelicense) | -| categories | [`PackageCategory[]`](#packagecat) | -| languages | [`PackageLanguage[]`](#packagelang) | -| source | `table` | -| bin | `table<string, string>?` | -| share | `table<string, string>?` | -| opt | `table<string, string>?` | - -## `Package` - -Module: [`"mason-core.package"`](../lua/mason-core/package/init.lua) - -The `Package` class encapsulates the installation instructions and metadata about a Mason package. - -**Events** - -This class extends the [EventEmitter](#eventemitter) interface and emits the following events: - -| Event | Handler signature | -| ------------------- | ------------------------------------------------------ | -| `install:success` | `fun(handle: InstallHandle)` | -| `install:failed` | `fun(pkg: Package, handle: InstallHandle, error: any)` | -| `uninstall:success` | `fun()` | - -### `Package.Parse({package_identifier})` - -**Parameters:** - -- `package_identifier`: `string` For example, `"rust-analyzer@nightly"` - -**Returns:** `(string, string|nil)` Tuple where the first value is the name and the second value is the specified -version (or `nil`). - -### `Package.Lang` - -**Type:** `table<string, string>` - -Metatable used to declare language identifiers. Any key is valid and will be automatically indexed on first access, for -example: - -```lua -print(vim.inspect(Package.Lang)) -- prints {} -local lang = Package.Lang.SomeMadeUpLanguage -print(lang) -- prints "SomeMadeUpLanguage" -print(vim.inspect(Package.Lang)) -- prints { SomeMadeUpLanguage = "SomeMadeUpLanguage" } -``` - -### `Package.Cat` - -**Type:** - -```lua -Package.Cat = { - Compiler = "Compiler", - Runtime = "Runtime", - DAP = "DAP", - LSP = "LSP", - Linter = "Linter", - Formatter = "Formatter", -} -``` - -### `Package.License` - -Similar as [`Package.Lang`](#packagelang) but for SPDX license identifiers. - -### `Package:new({spec})` - -**Parameters:** - -- `spec`: [`RegistryPackageSpec`](#registrypackagespec) - -### `Package.spec` - -**Type**: [`RegistryPackageSpec`](#registrypackagespec) - -### `Package:is_installing()` - -**Returns:** `boolean` - -### `Package:install({opts?}, {callback?})` - -**Parameters:** - -- `opts?`: [`PackageInstallOpts`](#packageinstallopts-1) (optional) -- `callback?`: `fun(success: boolean, result: any)` (optional) - Callback to be called when package installation completes. _Note: this is called before events (["package:install:success"](#registry-events), ["install:success"](#package)) are emitted._ - -**Returns:** [`InstallHandle`](#installhandle) - -Installs the package instance this method is being called on. Accepts an optional `{opts}` argument which can be used to -for example specify which version to install (see [`PackageInstallOpts`](#packageinstallopts-1)), and an optional -`{callback}` argument which is called when the installation finishes. - -The returned [`InstallHandle`](#installhandle) can be used to observe progress and control the installation process -(e.g., cancelling). - -_Note that if the package is already being installed this method will error. See -[`Package:is_installing()`](#packageis_installing)._ - -### `Package:uninstall()` - -Uninstalls the package instance this method is being called on. - -### `Package:is_installed()` - -**Returns:** `boolean` - -### `Package:get_install_path()` - -**Returns:** `string` The full path where this package is installed. _Note that this will always return a string, -regardless of whether the package is actually installed or not._ - -### `Package:get_installed_version()` - -**Returns:** `string?` The currently installed version of the package. Returns `nil` if the package is not installed. - -### `Package:get_latest_version()` - -**Returns:** `string` The latest package version as provided by the currently installed version of the registry. - -_Note that this method will not check if one or more registries are outdated. If it's desired to retrieve the latest -upstream version, refresh/update registries first (`:h mason-registry.refresh()`, `:h mason-registry.update()`), for -example:_ - -```lua -local registry = require "mason-registry" -registry.refresh(function() - local pkg = registry.get_package "rust-analyzer" - local latest_version = pkg:get_latest_version() -end) -``` - -### `Package:is_installable({opts?})` - -**Parameters:** - -- `opts?`: [`PackageInstallOpts`](#packageinstallopts-1) (optional) - -**Returns:** `boolean` Returns `true` if the package is installable on the current platform. - -## `PackageInstallOpts` - -**Type:** - -| Key | Value | Description | -| ------- | ---------- | -------------------------------------------------------------------------------------------------------- | -| version | `string?` | The desired version of the package. | -| target | `string?` | The desired target of the package to install (e.g. `darwin_arm64`, `linux_x64`). | -| debug | `boolean?` | If debug logs should be written. | -| force | `boolean?` | If installation should continue if there are conditions that would normally cause installation to fail. | -| strict | `boolean?` | If installation should NOT continue if there are errors that are not necessary for package to be usable. | - -## `InstallContext` - -The `InstallContext` class will be instantiated by Mason every time a package installer is executed. The `install` -function of a package will receive an instance of `InstallContext` as its first argument. - -As the name suggests, this class provides contextual information to be used when installing a package. This includes -which package is being installed, a `spawn` method that allow you to spawn processes that (i) use the correct working -directory of the installation, and (ii) automatically registers stdout and stderr with the `InstallHandle`. - -### `InstallContext.package` - -**Type:** [`Package`](#package) - -### `InstallContext.handle` - -**Type:** [`InstallHandle`](#installhandle) - -### `InstallContext.cwd` - -**Type:** [`CwdManager`](#cwdmanager) - -### `InstallContext.spawn` - -**Type:** [`ContextualSpawn`](#contextualspawn) - -### `InstallContext.fs` - -**Type:** [`ContextualFs`](#contextualfs) - -### `InstallContext.requested_version` - -**Type:** `Optional<string>` - -### `InstallContext.stdio_sink` - -**Type:** `{ stdout: fun(chunk: string), stderr: fun(chunk: string) }` - -The `.stdio_sink` property can be used to send stdout or stderr output, to be presented to the user. - -Example: - -```lua -local pkg = Pkg:new { - --- ... - ---@async - ---@param ctx InstallContext - install = function(ctx) - ctx.stdio_sink.stdout "I am doing stuff\n" - ctx.stdio_sink.stderr "Something went wrong!\n" - end, -} -``` - -## `ContextualFs` - -Provides wrapper functions around `mason-core.fs`. These wrapper functions all accept relative paths, which will be -expanded based on the associated `InstallContext`'s current working directory. - -## `ContextualSpawn` - -**Type:** `table<string, async fun(opts: SpawnOpts)>` - -Provides an asynchronous interface to spawn processes (via libuv). Each spawned process will, by default, be spawned -with the current working directory of the `InstallContext` it belongs to. stdout & stderr will also automatically be -registered with the relevant `InstallHandle`. - -Example usage: - -```lua -local pkg = Pkg:new { - --- ... - ---@async - ---@param ctx InstallContext - install = function(ctx) - ctx.spawn.npm { "install", "some-package" } - -- Calls to spawn will raise an error if it exits with a non-OK exit code or signal. - pcall(function() - ctx.spawn.commandoesntexist {} - end) - end, -} -``` - -## `CwdManager` - -Manages the current working directory of an installation (through `InstallContext`). - -### `CwdManager:set({cwd)})` - -**Parameters:** - -- `cwd`: `string` - -Changes the current working directory to `{cwd}`. `{cwd}` MUST be within the user's configured `install_root_dir` -setting. - -### `CwdManager:get()` - -**Returns:** `string` - -## `InstallHandleState` - -**Type:** `"IDLE" | "QUEUED" | "ACTIVE" | "CLOSED"` - -## `InstallHandle` - -An `InstallHandle` is a handle for observing and controlling the installation of a package. -Every package installed via Mason will be managed via a `InstallHandle` instance. - -It has a finite set of states, with an initial (`IDLE`) and terminal (`CLOSED`) one. This state can be accessed via the -`InstallHandle.state` field, or through one of the `:is_idle()`, `:is_queued()`, `:is_active()`, `:is_closed()` methods. -In most cases a handler's state will transition like so: - -```mermaid -stateDiagram-v2 - IDLE: IDLE - QUEUED: QUEUED - note right of QUEUED - The installation has been queued and will be ran when the next permit is available (according to the user's - settings.) - It can now be aborted via the :terminate() method. - end note - ACTIVE: ACTIVE - note right of ACTIVE - The installation has now started. The handler will emit `stdout` and `stderr` events. - The installation can also be cancelled via the :terminate() method, and you can send signals - to running processes via :kill({signal}). - end note - CLOSED: CLOSED - note right of CLOSED - The installation is now finished, and all associated resources have been closed. - This is the final state and the handler will not emit any more events. - end note - [*] --> IDLE - IDLE --> QUEUED - QUEUED --> ACTIVE - ACTIVE --> CLOSED - CLOSED --> [*] -``` - -**Events** - -This class extends the [EventEmitter](#eventemitter) interface and emits the following events: - -| Event | Handler signature | -| -------------- | ------------------------------------------------------------------- | -| `stdout` | `fun(chunk: string)` | -| `stderr` | `fun(chunk: string)` | -| `state:change` | `fun(new_state: InstallHandleState, old_state: InstallHandleState)` | -| `kill` | `fun(signal: integer)` | -| `terminate` | `fun()` | -| `closed` | `fun()` | - -### `InstallHandle.package` - -**Type:** [`Package`](#package) - -### `InstallHandle.state` - -**Type:** [`InstallHandleState`](#installhandlestate) - -### `InstallHandle.is_terminated` - -**Type:** `boolean` - -### `InstallHandle:is_idle()` - -**Returns:** `boolean` - -### `InstallHandle:is_queued()` - -**Returns:** `boolean` - -### `InstallHandle:is_active()` - -**Returns:** `boolean` - -### `InstallHandle:is_closed()` - -**Returns:** `boolean` - -### `InstallHandle:kill({signal})` - -**Parameters:** - -- `signal`: `integer` The `signal(3)` to send. - -### `InstallHandle:terminate()` - -Instructs the handle to terminate itself. On Windows, this will issue a -`taskkill.exe` treekill on all attached libuv handles. On Unix, this will -issue a SIGTERM signal to all attached libuv handles. - -## `EventEmitter` - -The `EventEmitter` interface includes methods to subscribe (and unsubscribe) -to events on the associated object. - -### `EventEmitter:on({event}, {handler})` - -**Parameters:** - -- `event`: `string` -- `handler`: `fun(...)` - -Registers the provided `{handler}`, to be called every time the provided -`{event}` is dispatched. - -_Note that the provided `{handler}` may be executed outside the main Neovim loop (`:h vim.in_fast_event()`), where most -of the Neovim API is disabled._ - -### `EventEmitter:once({event, handler})` - -**Parameters:** - -- `event`: `string` -- `handler`: `fun(...)` - -Registers the provided `{handler}`, to be called only once - the next time the -provided `{event}` is dispatched. - -_Note that the provided `{handler}` may be executed outside the main Neovim loop (`:h vim.in_fast_event()`), where most -of the Neovim API is disabled._ - -### `EventEmitter:off({event}, {handler})` - -**Parameters:** - -- `event`: `string` -- `handler`: `fun(...)` - -Deregisters the provided `{handler}` for the provided `{event}`. diff --git a/lua/mason-core/functional/init.lua b/lua/mason-core/functional/init.lua index d377d2db..7aa58940 100644 --- a/lua/mason-core/functional/init.lua +++ b/lua/mason-core/functional/init.lua @@ -3,9 +3,7 @@ local _ = {} local function lazy_require(module) return setmetatable({}, { __index = function(m, k) - return function(...) - return require(module)[k](...) - end + return require(module)[k] end, }) end diff --git a/lua/mason-core/installer/handle.lua b/lua/mason-core/installer/InstallHandle.lua index 62da5bae..f5a42c53 100644 --- a/lua/mason-core/installer/handle.lua +++ b/lua/mason-core/installer/InstallHandle.lua @@ -43,10 +43,11 @@ function InstallHandleSpawnHandle:__tostring() end ---@class InstallHandle : EventEmitter ----@field package Package +---@field package AbstractPackage ---@field state InstallHandleState ---@field stdio { buffers: { stdout: string[], stderr: string[] }, sink: StdioSink } ---@field is_terminated boolean +---@field location InstallLocation ---@field private spawn_handles InstallHandleSpawnHandle[] local InstallHandle = {} InstallHandle.__index = InstallHandle @@ -70,14 +71,17 @@ local function new_sink(handle) } end ----@param pkg Package -function InstallHandle:new(pkg) +---@param pkg AbstractPackage +---@param location InstallLocation +function InstallHandle:new(pkg, location) + ---@type InstallHandle local instance = EventEmitter.new(self) instance.state = "IDLE" instance.package = pkg instance.spawn_handles = {} instance.stdio = new_sink(instance) instance.is_terminated = false + instance.location = location return instance end diff --git a/lua/mason-core/installer/location.lua b/lua/mason-core/installer/InstallLocation.lua index 9cdf097f..00b517b9 100644 --- a/lua/mason-core/installer/location.lua +++ b/lua/mason-core/installer/InstallLocation.lua @@ -59,9 +59,9 @@ function InstallLocation:opt(path) return Path.concat { self.dir, "opt", path } end ----@param path string? -function InstallLocation:package(path) - return Path.concat { self.dir, "packages", path } +---@param pkg string? +function InstallLocation:package(pkg) + return Path.concat { self.dir, "packages", pkg } end ---@param path string? @@ -79,6 +79,11 @@ function InstallLocation:registry(path) return Path.concat { self.dir, "registries", path } end +---@param pkg string +function InstallLocation:receipt(pkg) + return Path.concat { self:package(pkg), "mason-receipt.json" } +end + ---@param opts { PATH: '"append"' | '"prepend"' | '"skip"' } function InstallLocation:set_env(opts) vim.env.MASON = self.dir diff --git a/lua/mason-core/installer/runner.lua b/lua/mason-core/installer/InstallRunner.lua index 64aa605d..fa2b3fcf 100644 --- a/lua/mason-core/installer/runner.lua +++ b/lua/mason-core/installer/InstallRunner.lua @@ -2,41 +2,45 @@ 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 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 registry = require "mason-registry" +local OneShotChannel = control.OneShotChannel + local InstallContext = require "mason-core.installer.context" ---@class InstallRunner ----@field location InstallLocation ---@field handle InstallHandle ----@field semaphore Semaphore ----@field permit Permit? +---@field global_semaphore Semaphore +---@field global_permit Permit? +---@field package_permit Permit? local InstallRunner = {} InstallRunner.__index = InstallRunner ----@param location InstallLocation ---@param handle InstallHandle ---@param semaphore Semaphore -function InstallRunner:new(location, handle, semaphore) +function InstallRunner:new(handle, semaphore) ---@type InstallRunner local instance = {} setmetatable(instance, self) instance.location = location - instance.semaphore = semaphore + instance.global_semaphore = semaphore instance.handle = handle return instance end +---@alias InstallRunnerCallback fun(success: true, receipt: InstallReceipt) | fun(success: false, handle: InstallHandle, error: any) + ---@param opts PackageInstallOpts ----@param callback? fun(success: boolean, result: any) +---@param callback? InstallRunnerCallback function InstallRunner:execute(opts, callback) local handle = self.handle log.fmt_info("Executing installer for %s %s", handle.package, opts) - local context = InstallContext:new(handle, self.location, opts) + local context = InstallContext:new(handle, opts) local tailed_output = {} @@ -79,25 +83,27 @@ function InstallRunner:execute(opts, callback) 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) + if callback then + callback(true, result.receipt) + end + handle.package:emit("install:success", result.receipt) + registry:emit("package:install:success", handle.package, result.receipt) 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) + if callback then + callback(false, result) + end + handle.package:emit("install:failed", result) + registry:emit("package:install:failed", handle.package, result) end end) local cancel_execution = a.run(function() return Result.try(function(try) - try(self:acquire_permit()) - try(self.location:initialize()) + try(self.handle.location:initialize()) + try(self:acquire_permit()):receive() try(self:acquire_lock(opts.force)) context.receipt:with_start_time(vim.loop.gettimeofday()) @@ -107,7 +113,7 @@ function InstallRunner:execute(opts, callback) -- 2. run installer ---@type async fun(ctx: InstallContext): Result - local installer = try(compiler.compile(handle.package.spec, opts)) + local installer = try(compiler.compile_installer(handle.package.spec, opts)) try(context:execute(installer)) -- 3. promote temporary installation dir @@ -116,28 +122,23 @@ function InstallRunner:execute(opts, callback) end)) -- 4. link package & write receipt - return linker - .link(context) - :and_then(function() - return context:build_receipt(context) - end) - :and_then( + try(linker.link(context):on_failure(function() + -- unlink any links that were made before failure + context:build_receipt():on_success( ---@param receipt InstallReceipt function(receipt) - return receipt:write(context.cwd:get()) + linker.unlink(handle.package, receipt, self.handle.location):on_failure(function(err) + log.error("Failed to unlink failed installation.", err) + end) 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, self.location):on_failure(function(err) - log.error("Failed to unlink failed installation.", err) - end) - end - ) - end) + end)) + ---@type InstallReceipt + local receipt = try(context:build_receipt()) + try(Result.pcall(fs.sync.write_file, handle.location:receipt(handle.package.name), receipt:to_json())) + return { + receipt = receipt, + } end):get_or_throw() end, finalize) @@ -157,7 +158,7 @@ end ---@async ---@private function InstallRunner:release_lock() - pcall(fs.async.unlink, self.location:lockfile(self.handle.package.name)) + pcall(fs.async.unlink, self.handle.location:lockfile(self.handle.package.name)) end ---@async @@ -166,7 +167,7 @@ end function InstallRunner:acquire_lock(force) local pkg = self.handle.package log.debug("Attempting to lock package", pkg) - local lockfile = self.location:lockfile(pkg.name) + local lockfile = self.handle.location:lockfile(pkg.name) if force ~= true and fs.async.file_exists(lockfile) then log.error("Lockfile already exists.", pkg) return Result.failure( @@ -181,33 +182,45 @@ function InstallRunner:acquire_lock(force) return Result.success(lockfile) end ----@async ---@private function InstallRunner:acquire_permit() + local channel = OneShotChannel:new() + log.fmt_debug("Acquiring permit for %s", self.handle.package) local handle = self.handle - if handle:is_active() or handle:is_closed() then - log.fmt_debug("Received active or closed handle %s", handle) + if handle:is_active() or handle:is_closing() then + log.fmt_debug("Received active or closing 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() + a.run(function() + self.global_permit = self.global_semaphore:acquire() + self.package_permit = handle.package:acquire_permit() + end, function(success, err) + if not success or handle:is_closing() then + if not success then + log.error("Acquiring permits failed", err) + end + self:release_permit() + else + log.fmt_debug("Activating handle %s", handle) + handle:active() + channel:send() + end + end) + + return Result.success(channel) end ---@private function InstallRunner:release_permit() - if self.permit then - self.permit:forget() - self.permit = nil + if self.global_permit then + self.global_permit:forget() + self.global_permit = nil + end + if self.package_permit then + self.package_permit:forget() + self.package_permit = nil end end diff --git a/lua/mason-core/installer/UninstallRunner.lua b/lua/mason-core/installer/UninstallRunner.lua new file mode 100644 index 00000000..661bfefa --- /dev/null +++ b/lua/mason-core/installer/UninstallRunner.lua @@ -0,0 +1,119 @@ +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 compiler = require "mason-core.installer.compiler" +local control = require "mason-core.async.control" +local fs = require "mason-core.fs" +local log = require "mason-core.log" +local registry = require "mason-registry" + +local OneShotChannel = control.OneShotChannel + +---@class UninstallRunner +---@field handle InstallHandle +---@field global_semaphore Semaphore +---@field package_permit Permit? +---@field global_permit Permit? +local UninstallRunner = {} +UninstallRunner.__index = UninstallRunner + +---@param handle InstallHandle +---@param global_semaphore Semaphore +---@return UninstallRunner +function UninstallRunner:new(handle, global_semaphore) + local instance = {} + setmetatable(instance, self) + instance.handle = handle + instance.global_semaphore = global_semaphore + return instance +end + +---@param opts PackageUninstallOpts +---@param callback? InstallRunnerCallback +function UninstallRunner:execute(opts, callback) + local pkg = self.handle.package + local location = self.handle.location + log.fmt_info("Executing uninstaller for %s %s", pkg, opts) + a.run(function() + Result.try(function(try) + if not opts.bypass_permit then + try(self:acquire_permit()):receive() + end + ---@type InstallReceipt? + local receipt = pkg:get_receipt(location):or_else(nil) + if receipt == nil then + log.fmt_warn("Receipt not found when uninstalling %s", pkg) + end + try(pkg:unlink(location)) + fs.sync.rmrf(location:package(pkg.name)) + return receipt + end):get_or_throw() + end, function(success, result) + if not self.handle:is_closing() then + self.handle:close() + end + self:release_permit() + + if success then + local receipt = result + log.fmt_info("Uninstallation succeeded for %s", pkg) + if callback then + callback(true, receipt) + end + pkg:emit("uninstall:success", receipt) + registry:emit("package:uninstall:success", pkg, receipt) + else + log.fmt_error("Uninstallation failed for %s error=%s", pkg, result) + if callback then + callback(false, result) + end + pkg:emit("uninstall:failed", result) + registry:emit("package:uninstall:failed", pkg, result) + end + end) +end + +---@private +function UninstallRunner:acquire_permit() + local channel = OneShotChannel:new() + log.fmt_debug("Acquiring permit for %s", self.handle.package) + local handle = self.handle + if handle:is_active() or handle:is_closing() then + log.fmt_debug("Received active or closing handle %s", handle) + return Result.failure "Invalid handle state." + end + + handle:queued() + a.run(function() + self.global_permit = self.global_semaphore:acquire() + self.package_permit = handle.package:acquire_permit() + end, function(success, err) + if not success or handle:is_closing() then + if not success then + log.error("Acquiring permits failed", err) + end + self:release_permit() + else + log.fmt_debug("Activating handle %s", handle) + handle:active() + channel:send() + end + end) + + return Result.success(channel) +end + +---@private +function UninstallRunner:release_permit() + if self.global_permit then + self.global_permit:forget() + self.global_permit = nil + end + if self.package_permit then + self.package_permit:forget() + self.package_permit = nil + end +end + +return UninstallRunner diff --git a/lua/mason-core/installer/compiler/compilers/github/init.lua b/lua/mason-core/installer/compiler/compilers/github/init.lua index d8646975..5a8dfce5 100644 --- a/lua/mason-core/installer/compiler/compilers/github/init.lua +++ b/lua/mason-core/installer/compiler/compilers/github/init.lua @@ -20,7 +20,7 @@ end ---@async ---@param ctx InstallContext ---@param source ParsedGitHubReleaseSource | ParsedGitHubBuildSource -function M.install(ctx, source, purl) +function M.install(ctx, source) if source.asset then source = source--[[@as ParsedGitHubReleaseSource]] return require("mason-core.installer.compiler.compilers.github.release").install(ctx, source) diff --git a/lua/mason-core/installer/compiler/init.lua b/lua/mason-core/installer/compiler/init.lua index e1df6784..4eed986b 100644 --- a/lua/mason-core/installer/compiler/init.lua +++ b/lua/mason-core/installer/compiler/init.lua @@ -71,9 +71,9 @@ local function upsert(dst, src) end ---@param source RegistryPackageSource ----@param version string +---@param version string? local function coalesce_source(source, version) - if source.version_overrides then + if version and source.version_overrides then for i = #source.version_overrides, 1, -1 do local version_override = source.version_overrides[i] local version_type, constraint = unpack(_.split(":", version_override.constraint)) @@ -94,18 +94,12 @@ local function coalesce_source(source, version) end):get_or_else(false) if version_match then - if version_override.id then - -- Because this entry provides its own purl id, it overrides the entire source definition. - return version_override - else - -- Upsert the default source with the contents of the version override. - return upsert(vim.deepcopy(source), _.dissoc("constraint", version_override)) - end + return _.dissoc("constraint", version_override) end end end end - return source + return _.dissoc("version_overrides", source) end ---@param spec RegistryPackageSpec @@ -121,7 +115,7 @@ function M.parse(spec, opts) ) end - local source = opts.version and coalesce_source(spec.source, opts.version) or spec.source + local source = coalesce_source(spec.source, opts.version) ---@type Purl local purl = try(Purl.parse(source.id)) @@ -149,7 +143,7 @@ end ---@async ---@param spec RegistryPackageSpec ---@param opts PackageInstallOpts -function M.compile(spec, opts) +function M.compile_installer(spec, opts) log.debug("Compiling installer.", spec.name, opts) return Result.try(function(try) -- Parsers run synchronously and may access API functions, so we schedule before-hand. @@ -210,9 +204,10 @@ function M.compile(spec, opts) ctx.receipt:with_source { type = ctx.package.spec.schema, id = Purl.compile(parsed.purl), + -- Exclude the "install" field from "mason" sources because this is a Lua function. + raw = parsed.purl.type == "mason" and _.dissoc("install", parsed.raw_source) or parsed.raw_source, } - end):on_failure(function(err) - error(err, 0) + ctx.receipt:with_install_options(opts) end) end end) diff --git a/lua/mason-core/installer/compiler/link.lua b/lua/mason-core/installer/compiler/link.lua index 9719eaa9..d60fce47 100644 --- a/lua/mason-core/installer/compiler/link.lua +++ b/lua/mason-core/installer/compiler/link.lua @@ -38,7 +38,7 @@ local bin_delegates = { local python = platform.is.win and "python" or "python3" return ctx:write_shell_exec_wrapper( bin, - ("%s %q"):format(python, path.concat { ctx.package:get_install_path(), target }) + ("%s %q"):format(python, path.concat { ctx:get_install_path(), target }) ) end) end, @@ -66,7 +66,7 @@ local bin_delegates = { return ctx:write_shell_exec_wrapper( bin, ("dotnet %q"):format(path.concat { - ctx.package:get_install_path(), + ctx:get_install_path(), target, }) ) @@ -103,7 +103,7 @@ local bin_delegates = { return ctx:write_shell_exec_wrapper( bin, ("java -jar %q"):format(path.concat { - ctx.package:get_install_path(), + ctx:get_install_path(), target, }) ) diff --git a/lua/mason-core/installer/context/cwd.lua b/lua/mason-core/installer/context/InstallContextCwd.lua index 2b74bf55..b365cbd9 100644 --- a/lua/mason-core/installer/context/cwd.lua +++ b/lua/mason-core/installer/context/InstallContextCwd.lua @@ -3,20 +3,16 @@ 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 handle InstallHandle ---@field private cwd string? local InstallContextCwd = {} InstallContextCwd.__index = InstallContextCwd ---@param handle InstallHandle ----@param location InstallLocation -function InstallContextCwd:new(handle, location) - assert(location, "location not provided") +function InstallContextCwd:new(handle) ---@type InstallContextCwd local instance = {} setmetatable(instance, self) - instance.location = location instance.handle = handle instance.cwd = nil return instance @@ -24,7 +20,7 @@ end function InstallContextCwd:initialize() return Result.try(function(try) - local staging_dir = self.location:staging(self.handle.package.name) + local staging_dir = self.handle.location:staging(self.handle.package.name) if fs.sync.dir_exists(staging_dir) then try(Result.pcall(fs.sync.rmrf, staging_dir)) end @@ -42,8 +38,8 @@ end 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) + path.is_subdirectory(self.handle.location:get_dir(), new_abs_cwd), + ("%q is not a subdirectory of %q"):format(new_abs_cwd, self.handle.location) ) self.cwd = new_abs_cwd return self diff --git a/lua/mason-core/installer/context/fs.lua b/lua/mason-core/installer/context/InstallContextFs.lua index 93379017..93379017 100644 --- a/lua/mason-core/installer/context/fs.lua +++ b/lua/mason-core/installer/context/InstallContextFs.lua diff --git a/lua/mason-core/installer/context/spawn.lua b/lua/mason-core/installer/context/InstallContextSpawn.lua index f2ce8df2..f2ce8df2 100644 --- a/lua/mason-core/installer/context/spawn.lua +++ b/lua/mason-core/installer/context/InstallContextSpawn.lua diff --git a/lua/mason-core/installer/context/init.lua b/lua/mason-core/installer/context/init.lua index 425bf39c..097ea696 100644 --- a/lua/mason-core/installer/context/init.lua +++ b/lua/mason-core/installer/context/init.lua @@ -1,8 +1,9 @@ -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 InstallContextCwd = require "mason-core.installer.context.InstallContextCwd" +local InstallContextFs = require "mason-core.installer.context.InstallContextFs" +local InstallContextSpawn = require "mason-core.installer.context.InstallContextSpawn" 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" @@ -15,7 +16,7 @@ local receipt = require "mason-core.receipt" ---@field location InstallLocation ---@field spawn InstallContextSpawn ---@field handle InstallHandle ----@field package Package +---@field package AbstractPackage ---@field cwd InstallContextCwd ---@field opts PackageInstallOpts ---@field stdio_sink StdioSink @@ -24,17 +25,16 @@ local InstallContext = {} InstallContext.__index = InstallContext ---@param handle InstallHandle ----@param location InstallLocation ---@param opts PackageInstallOpts -function InstallContext:new(handle, location, opts) - local cwd = InstallContextCwd:new(handle, location) +function InstallContext:new(handle, opts) + local cwd = InstallContextCwd:new(handle) local spawn = InstallContextSpawn:new(handle, cwd, false) local fs = InstallContextFs:new(cwd) return setmetatable({ cwd = cwd, spawn = spawn, handle = handle, - location = location, + location = handle.location, -- for convenience package = handle.package, -- for convenience fs = fs, receipt = receipt.InstallReceiptBuilder:new(), @@ -51,23 +51,35 @@ end ---@async function InstallContext:promote_cwd() local cwd = self.cwd:get() - local install_path = self.package:get_install_path() + local install_path = self:get_install_path() if install_path == cwd then - log.fmt_debug("cwd %s is already promoted (at %s)", cwd, install_path) + log.fmt_debug("cwd %s is already promoted", cwd) return end log.fmt_debug("Promoting cwd %s to %s", cwd, install_path) + -- 1. Uninstall any existing installation - self.handle.package:uninstall() + if self.handle.package:is_installed() then + a.wait(function(resolve, reject) + self.handle.package:uninstall({ bypass_permit = true }, function(success, result) + if not success then + reject(result) + else + resolve() + end + end) + end) + end + -- 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. fs.async.mkdir(install_path) end - -- 3. Move the cwd to the final installation directory - fs.async.rename(cwd, install_path) - -- 4. Update cwd + -- 3. Update cwd self.cwd:set(install_path) + -- 4. Move the cwd to the final installation directory + fs.async.rename(cwd, install_path) end ---@param rel_path string The relative path from the current working directory to change cwd to. Will only restore to the initial cwd after execution of fn (if provided). @@ -94,7 +106,7 @@ function InstallContext:write_node_exec_wrapper(new_executable_rel_path, script_ return self:write_shell_exec_wrapper( new_executable_rel_path, ("node %q"):format(path.concat { - self.package:get_install_path(), + self:get_install_path(), script_rel_path, }) ) @@ -109,7 +121,7 @@ function InstallContext:write_ruby_exec_wrapper(new_executable_rel_path, script_ return self:write_shell_exec_wrapper( new_executable_rel_path, ("ruby %q"):format(path.concat { - self.package:get_install_path(), + self:get_install_path(), script_rel_path, }) ) @@ -124,7 +136,7 @@ function InstallContext:write_php_exec_wrapper(new_executable_rel_path, script_r return self:write_shell_exec_wrapper( new_executable_rel_path, ("php %q"):format(path.concat { - self.package:get_install_path(), + self:get_install_path(), script_rel_path, }) ) @@ -149,7 +161,7 @@ function InstallContext:write_pyvenv_exec_wrapper(new_executable_rel_path, modul new_executable_rel_path, ("%q -m %s"):format( path.concat { - pypi.venv_path(self.package:get_install_path()), + pypi.venv_path(self:get_install_path()), "python", }, module @@ -169,7 +181,7 @@ function InstallContext:write_exec_wrapper(new_executable_rel_path, target_execu return self:write_shell_exec_wrapper( new_executable_rel_path, ("%q"):format(path.concat { - self.package:get_install_path(), + self:get_install_path(), target_executable_rel_path, }) ) @@ -264,4 +276,8 @@ function InstallContext:build_receipt() end) end +function InstallContext:get_install_path() + return self.location:package(self.package.name) +end + return InstallContext diff --git a/lua/mason-core/installer/linker.lua b/lua/mason-core/installer/linker.lua index a5c54273..a26d2592 100644 --- a/lua/mason-core/installer/linker.lua +++ b/lua/mason-core/installer/linker.lua @@ -57,7 +57,7 @@ local function unlink(receipt, link_context, location) end) end ----@param pkg Package +---@param pkg AbstractPackage ---@param receipt InstallReceipt ---@param location InstallLocation ---@nodiscard @@ -82,31 +82,27 @@ local function link(context, link_context, link_fn) name = ("%s.cmd"):format(name) end local new_abs_path = link_context.prefix(name, context.location) - local target_abs_path = path.concat { context.package:get_install_path(), rel_path } + local target_abs_path = path.concat { context:get_install_path(), rel_path } local target_rel_path = path.relative(new_abs_path, target_abs_path) - do - -- 1. Ensure destination directory exists - a.scheduler() - local dir = vim.fn.fnamemodify(new_abs_path, ":h") - if not fs.async.dir_exists(dir) then - try(Result.pcall(fs.async.mkdirp, dir)) - end + -- 1. Ensure destination directory exists + a.scheduler() + local dir = vim.fn.fnamemodify(new_abs_path, ":h") + if not fs.async.dir_exists(dir) then + try(Result.pcall(fs.async.mkdirp, dir)) end - do - -- 2. Ensure source file exists and target doesn't yet exist OR if --force unlink target if it already - -- exists. - if context.opts.force then - if fs.async.file_exists(new_abs_path) then - try(Result.pcall(fs.async.unlink, new_abs_path)) - end - elseif fs.async.file_exists(new_abs_path) then - return Result.failure(("%q is already linked."):format(new_abs_path, name)) - end - if not fs.async.file_exists(target_abs_path) then - return Result.failure(("Link target %q does not exist."):format(target_abs_path)) + -- 2. Ensure source file exists and target doesn't yet exist OR if --force unlink target if it already + -- exists. + if context.opts.force then + if fs.async.file_exists(new_abs_path) then + try(Result.pcall(fs.async.unlink, new_abs_path)) end + elseif fs.async.file_exists(new_abs_path) then + return Result.failure(("%q is already linked."):format(new_abs_path, name)) + end + if not fs.async.file_exists(target_abs_path) then + return Result.failure(("Link target %q does not exist."):format(target_abs_path)) end -- 3. Execute link. diff --git a/lua/mason-core/installer/managers/gem.lua b/lua/mason-core/installer/managers/gem.lua index cb502de5..e8723d7e 100644 --- a/lua/mason-core/installer/managers/gem.lua +++ b/lua/mason-core/installer/managers/gem.lua @@ -54,14 +54,14 @@ function M.create_bin_wrapper(bin) ctx.write_shell_exec_wrapper, ctx, bin, - path.concat { ctx.package:get_install_path(), bin_path }, + path.concat { ctx:get_install_path(), bin_path }, { GEM_PATH = platform.when { unix = function() - return ("%s:$GEM_PATH"):format(ctx.package:get_install_path()) + return ("%s:$GEM_PATH"):format(ctx:get_install_path()) end, win = function() - return ("%s;%%GEM_PATH%%"):format(ctx.package:get_install_path()) + return ("%s;%%GEM_PATH%%"):format(ctx:get_install_path()) end, }, } diff --git a/lua/mason-core/installer/managers/npm.lua b/lua/mason-core/installer/managers/npm.lua index 10a3e9fd..df8ece35 100644 --- a/lua/mason-core/installer/managers/npm.lua +++ b/lua/mason-core/installer/managers/npm.lua @@ -70,6 +70,14 @@ function M.install(pkg, version, opts) } end +---@async +---@param pkg string +function M.uninstall(pkg) + local ctx = installer.context() + ctx.stdio_sink.stdout(("Uninstalling npm package %s…\n"):format(pkg)) + return ctx.spawn.npm { "uninstall", pkg } +end + ---@param exec string function M.bin_path(exec) return Result.pcall(platform.when, { diff --git a/lua/mason-core/installer/managers/pypi.lua b/lua/mason-core/installer/managers/pypi.lua index c569e0fd..85fadc9f 100644 --- a/lua/mason-core/installer/managers/pypi.lua +++ b/lua/mason-core/installer/managers/pypi.lua @@ -214,6 +214,19 @@ function M.install(pkg, version, opts) }, opts.install_extra_args) end +---@async +---@param pkg string +function M.uninstall(pkg) + log.fmt_debug("pypi: uninstall %s", pkg) + return venv_python { + "-m", + "pip", + "uninstall", + "-y", + pkg, + } +end + ---@param executable string function M.bin_path(executable) local ctx = installer.context() diff --git a/lua/mason-core/package.lua b/lua/mason-core/package.lua deleted file mode 100644 index a8a7ac79..00000000 --- a/lua/mason-core/package.lua +++ /dev/null @@ -1,274 +0,0 @@ -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 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 ----@field spec RegistryPackageSpec ----@field private handle InstallHandle The currently associated handle. -local Package = {} -Package.__index = Package -setmetatable(Package, { __index = EventEmitter }) - ----@param package_identifier string ----@return string, string? -Package.Parse = function(package_identifier) - local name, version = unpack(vim.split(package_identifier, "@")) - return name, version -end - ----@alias PackageLanguage string - ----@type table<PackageLanguage, PackageLanguage> -Package.Lang = setmetatable({}, { - __index = function(s, lang) - s[lang] = lang - return s[lang] - end, -}) - ----@enum PackageCategory -Package.Cat = { - Compiler = "Compiler", - Runtime = "Runtime", - DAP = "DAP", - LSP = "LSP", - Linter = "Linter", - Formatter = "Formatter", -} - ----@alias PackageLicense string - ----@type table<PackageLicense, PackageLicense> -Package.License = setmetatable({}, { - __index = function(s, license) - s[license] = license - return s[license] - end, -}) - ----@class RegistryPackageSourceVersionOverride : RegistryPackageSource ----@field constraint string - ----@class RegistryPackageSource ----@field id string PURL-compliant identifier. ----@field version_overrides? RegistryPackageSourceVersionOverride[] - ----@class RegistryPackageSchemas ----@field lsp string? - ----@class RegistryPackageDeprecation ----@field since string ----@field message string - ----@alias RegistryPackageSpecSchema ---- | '"registry+v1"' - ----@class RegistryPackageSpec ----@field schema RegistryPackageSpecSchema ----@field name string ----@field description string ----@field homepage string ----@field licenses string[] ----@field languages string[] ----@field categories string[] ----@field source RegistryPackageSource ----@field deprecation RegistryPackageDeprecation? ----@field schemas RegistryPackageSchemas? ----@field bin table<string, string>? ----@field share table<string, string>? ----@field opt table<string, string>? - ----@param spec RegistryPackageSpec -local function validate_spec(spec) - if platform.cached_features["nvim-0.11"] ~= 1 then - return - end - vim.validate("schema", spec.schema, _.equals "registry+v1", "registry+v1") - vim.validate("name", spec.name, "string") - vim.validate("description", spec.description, "string") - vim.validate("homepage", spec.homepage, "string") - vim.validate("licenses", spec.licenses, "table") - vim.validate("categories", spec.categories, "table") - vim.validate("languages", spec.languages, "table") - vim.validate("source", spec.source, "table") - vim.validate("bin", spec.bin, { "table", "nil" }) - vim.validate("share", spec.share, { "table", "nil" }) -end - ----@param spec RegistryPackageSpec -function Package:new(spec) - validate_spec(spec) - local instance = EventEmitter.new(self) --[[@as Package]] - instance.name = spec.name -- for convenient access - instance.spec = spec - return instance -end - -function Package:new_handle() - self:get_handle():if_present(function(handle) - assert(handle:is_closed(), "Cannot create new handle because existing handle is not closed.") - end) - log.fmt_trace("Creating new handle for %s", self) - local InstallationHandle = require "mason-core.installer.handle" - local handle = InstallationHandle:new(self) - self.handle = handle - - -- Ideally we'd decouple this and leverage Mason's event system, but to allow loading as little as possible during - -- setup (i.e. not load modules related to Mason's event system) of the mason.nvim plugin we explicitly call into - -- terminator here. - require("mason-core.terminator").register(handle) - - self:emit("handle", handle) - registry:emit("package:handle", self, handle) - - return handle -end - ----@alias PackageInstallOpts { version?: string, debug?: boolean, target?: string, force?: boolean, strict?: boolean } - --- TODO this needs to be elsewhere -local semaphore = Semaphore:new(settings.current.max_concurrent_installers) - -function Package:is_installing() - return self:get_handle() - :map( - ---@param handle InstallHandle - function(handle) - return not handle:is_closed() - end - ) - :or_else(false) -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.global(), handle, semaphore) - runner:execute(opts, callback) - return handle -end - ----@return boolean -function Package:uninstall() - 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 - ----@private ----@param receipt InstallReceipt -function Package:unlink(receipt) - log.fmt_trace("Unlinking %s", self) - local install_path = self:get_install_path() - - -- 1. Unlink - local linker = require "mason-core.installer.linker" - linker.unlink(self, receipt, InstallLocation.global()):get_or_throw() - - -- 2. Remove installation artifacts - fs.sync.rmrf(install_path) -end - -function Package:is_installed() - return registry.is_installed(self.name) -end - -function Package:get_handle() - return Optional.of_nilable(self.handle) -end - -function Package:get_install_path() - return InstallLocation.global():package(self.name) -end - ----@return Optional # Optional<InstallReceipt> -function Package:get_receipt() - local receipt_path = path.concat { self:get_install_path(), "mason-receipt.json" } - if fs.sync.file_exists(receipt_path) then - local receipt = require "mason-core.receipt" - return Optional.of(receipt.InstallReceipt.from_json(vim.json.decode(fs.sync.read_file(receipt_path)))) - end - return Optional.empty() -end - ----@return string? -function Package:get_installed_version() - return self:get_receipt() - :and_then( - ---@param receipt InstallReceipt - function(receipt) - local source = receipt:get_source() - if source.id then - return Purl.parse(source.id):map(_.prop "version"):ok() - else - return Optional.empty() - end - end - ) - :or_else(nil) -end - ----@return string -function Package:get_latest_version() - return Purl.parse(self.spec.source.id) - :map(_.prop "version") - :get_or_throw(("Unable to retrieve version from malformed purl: %s."):format(self.spec.source.id)) -end - ----@param opts? PackageInstallOpts -function Package:is_installable(opts) - return require("mason-core.installer.compiler").parse(self.spec, opts or {}):is_success() -end - ----@return Result # Result<string[]> -function Package:get_all_versions() - local compiler = require "mason-core.installer.compiler" - return Result.try(function(try) - ---@type Purl - local purl = try(Purl.parse(self.spec.source.id)) - ---@type InstallerCompiler - local compiler = try(compiler.get_compiler(purl)) - return compiler.get_versions(purl, self.spec.source) - end) -end - -function Package:get_lsp_settings_schema() - local schema_file = InstallLocation.global() - :share(path.concat { "mason-schemas", "lsp", ("%s.json"):format(self.name) }) - if fs.sync.file_exists(schema_file) then - return Result.pcall(vim.json.decode, fs.sync.read_file(schema_file), { - luanil = { object = true, array = true }, - }):ok() - end - return Optional.empty() -end -function Package:get_aliases() - return require("mason-registry").get_package_aliases(self.name) -end - -function Package:__tostring() - return ("Package(name=%s)"):format(self.name) -end - -return Package diff --git a/lua/mason-core/package/AbstractPackage.lua b/lua/mason-core/package/AbstractPackage.lua new file mode 100644 index 00000000..b490fc87 --- /dev/null +++ b/lua/mason-core/package/AbstractPackage.lua @@ -0,0 +1,203 @@ +local EventEmitter = require "mason-core.EventEmitter" +local InstallLocation = require "mason-core.installer.InstallLocation" +local Optional = require "mason-core.optional" +local Purl = require "mason-core.purl" +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 settings = require "mason.settings" +local Semaphore = require("mason-core.async.control").Semaphore + +---@alias PackageInstallOpts { version?: string, debug?: boolean, target?: string, force?: boolean, strict?: boolean, location?: InstallLocation } +---@alias PackageUninstallOpts { bypass_permit?: boolean, location?: InstallLocation } + +---@class AbstractPackage : EventEmitter +---@field name string +---@field spec RegistryPackageSpec +---@field private install_handle InstallHandle? The currently associated installation handle. +---@field private uninstall_handle InstallHandle? The currently associated uninstallation handle. +local AbstractPackage = {} +AbstractPackage.__index = AbstractPackage +setmetatable(AbstractPackage, { __index = EventEmitter }) + +AbstractPackage.SEMAPHORE = Semaphore:new(settings.current.max_concurrent_installers) +---@type PackageInstallOpts +AbstractPackage.DEFAULT_INSTALL_OPTS = { + debug = false, + force = false, + strict = false, + target = nil, + version = nil, +} + +---@param spec RegistryPackageSpec +function AbstractPackage:new(spec) + local instance = EventEmitter.new(self) + instance.name = spec.name -- for convenient access + instance.spec = spec + return instance +end + +---@return boolean +function AbstractPackage:is_installing() + return self:get_install_handle() + :map( + ---@param handle InstallHandle + function(handle) + return not handle:is_closed() + end + ) + :or_else(false) +end + +---@return boolean +function AbstractPackage:is_uninstalling() + return self:get_uninstall_handle() + :map( + ---@param handle InstallHandle + function(handle) + return not handle:is_closed() + end + ) + :or_else(false) +end + +function AbstractPackage:get_install_handle() + return Optional.of_nilable(self.install_handle) +end + +function AbstractPackage:get_uninstall_handle() + return Optional.of_nilable(self.uninstall_handle) +end + +---@param location InstallLocation +function AbstractPackage:new_handle(location) + assert(location, "Cannot create new handle without a location.") + local InstallHandle = require "mason-core.installer.InstallHandle" + local handle = InstallHandle:new(self, location) + -- Ideally we'd decouple this and leverage Mason's event system, but to allow loading as little as possible during + -- setup (i.e. not load modules related to Mason's event system) of the mason.nvim plugin we explicitly call into + -- terminator here. + require("mason-core.terminator").register(handle) + return handle +end + +---@param location? InstallLocation +function AbstractPackage:new_install_handle(location) + location = location or InstallLocation.global() + log.fmt_trace("Creating new installation handle for %s", self) + self:get_install_handle():if_present(function(handle) + assert(handle:is_closed(), "Cannot create new install handle because existing handle is not closed.") + end) + self.install_handle = self:new_handle(location) + self:emit("install:handle", self.install_handle) + return self.install_handle +end + +---@param location? InstallLocation +function AbstractPackage:new_uninstall_handle(location) + location = location or InstallLocation.global() + log.fmt_trace("Creating new uninstallation handle for %s", self) + self:get_uninstall_handle():if_present(function(handle) + assert(handle:is_closed(), "Cannot create new uninstall handle because existing handle is not closed.") + end) + self.uninstall_handle = self:new_handle(location) + self:emit("uninstall:handle", self.uninstall_handle) + return self.uninstall_handle +end + +---@param opts? PackageInstallOpts +function AbstractPackage:is_installable(opts) + return require("mason-core.installer.compiler").parse(self.spec, opts or {}):is_success() +end + +---@param location? InstallLocation +---@return Optional # Optional<InstallReceipt> +function AbstractPackage:get_receipt(location) + location = location or InstallLocation.global() + local receipt_path = location:receipt(self.name) + if fs.sync.file_exists(receipt_path) then + local receipt = require "mason-core.receipt" + return Optional.of(receipt.InstallReceipt.from_json(vim.json.decode(fs.sync.read_file(receipt_path)))) + end + return Optional.empty() +end + +---@param location? InstallLocation +---@return boolean +function AbstractPackage:is_installed(location) + error "Unimplemented." +end + +---@return Result # Result<string[]> +function AbstractPackage:get_all_versions() + local compiler = require "mason-core.installer.compiler" + return Result.try(function(try) + ---@type Purl + local purl = try(Purl.parse(self.spec.source.id)) + ---@type InstallerCompiler + local compiler = try(compiler.get_compiler(purl)) + return compiler.get_versions(purl, self.spec.source) + end) +end + +---@return string +function AbstractPackage:get_latest_version() + return Purl.parse(self.spec.source.id) + :map(_.prop "version") + :get_or_throw(("Unable to retrieve version from malformed purl: %s."):format(self.spec.source.id)) +end + +---@param location? InstallLocation +---@return string? +function AbstractPackage:get_installed_version(location) + return self:get_receipt(location) + :and_then( + ---@param receipt InstallReceipt + function(receipt) + local source = receipt:get_source() + if source.id then + return Purl.parse(source.id):map(_.prop "version"):ok() + else + return Optional.empty() + end + end + ) + :or_else(nil) +end + +---@param opts? PackageInstallOpts +---@param callback? InstallRunnerCallback +---@return InstallHandle +function AbstractPackage:install(opts, callback) + error "Unimplemented." +end + +---@param opts? PackageUninstallOpts +---@param callback? InstallRunnerCallback +---@return InstallHandle +function AbstractPackage:uninstall(opts, callback) + error "Unimplemented." +end + +---@private +---@param location? InstallLocation +function AbstractPackage:unlink(location) + location = location or InstallLocation.global() + log.fmt_trace("Unlinking", self, location) + local linker = require "mason-core.installer.linker" + return self:get_receipt(location):ok_or("Unable to find receipt."):and_then(function(receipt) + return linker.unlink(self, receipt, location) + end) +end + +---@async +---@private +---@return Permit +function AbstractPackage:acquire_permit() + error "Unimplemented." +end + +return AbstractPackage diff --git a/lua/mason-core/package/init.lua b/lua/mason-core/package/init.lua new file mode 100644 index 00000000..09b0ebbf --- /dev/null +++ b/lua/mason-core/package/init.lua @@ -0,0 +1,182 @@ +local AbstractPackage = require "mason-core.package.AbstractPackage" +local InstallLocation = require "mason-core.installer.InstallLocation" +local InstallRunner = require "mason-core.installer.InstallRunner" +local Optional = require "mason-core.optional" +local Result = require "mason-core.result" +local UninstallRunner = require "mason-core.installer.UninstallRunner" +local _ = require "mason-core.functional" +local fs = require "mason-core.fs" +local path = require "mason-core.path" +local registry = require "mason-registry" +local platform = require "mason-core.platform" +local Semaphore = require("mason-core.async.control").Semaphore + +---@class Package : AbstractPackage +---@field spec RegistryPackageSpec +---@field local_semaphore Semaphore +local Package = {} +Package.__index = Package +setmetatable(Package, { __index = AbstractPackage }) + +---@param package_identifier string +---@return string, string? +Package.Parse = function(package_identifier) + local name, version = unpack(vim.split(package_identifier, "@")) + return name, version +end + +---@alias PackageLanguage string + +---@type table<PackageLanguage, PackageLanguage> +Package.Lang = setmetatable({}, { + __index = function(s, lang) + s[lang] = lang + return s[lang] + end, +}) + +---@enum PackageCategory +Package.Cat = { + Compiler = "Compiler", + Runtime = "Runtime", + DAP = "DAP", + LSP = "LSP", + Linter = "Linter", + Formatter = "Formatter", +} + +---@alias PackageLicense string + +---@type table<PackageLicense, PackageLicense> +Package.License = setmetatable({}, { + __index = function(s, license) + s[license] = license + return s[license] + end, +}) + +---@class RegistryPackageSourceVersionOverride : RegistryPackageSource +---@field constraint string + +---@class RegistryPackageSource +---@field id string PURL-compliant identifier. +---@field version_overrides? RegistryPackageSourceVersionOverride[] + +---@class RegistryPackageSchemas +---@field lsp string? + +---@class RegistryPackageDeprecation +---@field since string +---@field message string + +---@alias RegistryPackageSpecSchema +--- | '"registry+v1"' + +---@class RegistryPackageSpec +---@field schema RegistryPackageSpecSchema +---@field name string +---@field description string +---@field homepage string +---@field licenses string[] +---@field languages string[] +---@field categories string[] +---@field deprecation RegistryPackageDeprecation? +---@field source RegistryPackageSource +---@field schemas RegistryPackageSchemas? +---@field bin table<string, string>? +---@field share table<string, string>? +---@field opt table<string, string>? + +---@param spec RegistryPackageSpec +local function validate_spec(spec) + if platform.cached_features["nvim-0.11"] ~= 1 then + return + end + vim.validate("schema", spec.schema, _.equals "registry+v1", "registry+v1") + vim.validate("name", spec.name, "string") + vim.validate("description", spec.description, "string") + vim.validate("homepage", spec.homepage, "string") + vim.validate("licenses", spec.licenses, "table") + vim.validate("categories", spec.categories, "table") + vim.validate("languages", spec.languages, "table") + vim.validate("source", spec.source, "table") + vim.validate("bin", spec.bin, { "table", "nil" }) + vim.validate("share", spec.share, { "table", "nil" }) +end + +---@param spec RegistryPackageSpec +function Package:new(spec) + validate_spec(spec) + ---@type Package + local instance = AbstractPackage.new(self, spec) + instance.local_semaphore = Semaphore:new(1) + return instance +end + +---@param opts? PackageInstallOpts +---@param callback? InstallRunnerCallback +---@return InstallHandle +function Package:install(opts, callback) + opts = opts or {} + assert(not self:is_installing(), "Package is already installing.") + assert(not self:is_uninstalling(), "Package is uninstalling.") + opts = vim.tbl_extend("force", self.DEFAULT_INSTALL_OPTS, opts or {}) + + local handle = self:new_install_handle(opts.location) + registry:emit("package:install:handle", handle) + local runner = InstallRunner:new(handle, AbstractPackage.SEMAPHORE) + + runner:execute(opts, callback) + + return handle +end + +---@param opts? PackageUninstallOpts +---@param callback? fun(success: boolean, error: any) +function Package:uninstall(opts, callback) + opts = opts or {} + assert(self:is_installed(opts.location), "Package is not installed.") + assert(not self:is_uninstalling(), "Package is already uninstalling.") + local handle = self:new_uninstall_handle(opts.location) + registry:emit("package:uninstall:handle", handle) + local runner = UninstallRunner:new(handle, AbstractPackage.SEMAPHORE) + runner:execute(opts, callback) + return handle +end + +---@param location? InstallLocation +function Package:is_installed(location) + location = location or InstallLocation.global() + local ok, stat = pcall(vim.loop.fs_stat, location:package(self.name)) + if not ok or not stat then + return false + end + return stat.type == "directory" +end + +function Package:get_lsp_settings_schema() + local schema_file = InstallLocation.global() + :share(path.concat { "mason-schemas", "lsp", ("%s.json"):format(self.name) }) + if fs.sync.file_exists(schema_file) then + return Result.pcall(vim.json.decode, fs.sync.read_file(schema_file), { + luanil = { object = true, array = true }, + }):ok() + end + return Optional.empty() +end + +function Package:get_aliases() + return require("mason-registry").get_package_aliases(self.name) +end + +---@async +---@private +function Package:acquire_permit() + return self.local_semaphore:acquire() +end + +function Package:__tostring() + return ("Package(name=%s)"):format(self.name) +end + +return Package diff --git a/lua/mason-core/receipt.lua b/lua/mason-core/receipt.lua index 63403503..847b8011 100644 --- a/lua/mason-core/receipt.lua +++ b/lua/mason-core/receipt.lua @@ -1,15 +1,11 @@ -local Result = require "mason-core.result" -local fs = require "mason-core.fs" -local path = require "mason-core.path" - local M = {} ---@alias InstallReceiptSchemaVersion ---| '"1.0"' ---| '"1.1"' ----| '"1.2"' +---| '"2.0"' ----@alias InstallReceiptSource {type: RegistryPackageSpecSchema, id: string} +---@alias InstallReceiptSource {type: RegistryPackageSpecSchema, id: string, raw: RegistryPackageSource} ---@class InstallReceiptLinks ---@field bin? table<string, string> @@ -22,6 +18,7 @@ local M = {} ---@field public metrics {start_time:integer, completion_time:integer} ---@field public source InstallReceiptSource ---@field public links InstallReceiptLinks +---@field public install_options PackageInstallOpts local InstallReceipt = {} InstallReceipt.__index = InstallReceipt @@ -33,6 +30,10 @@ function InstallReceipt.from_json(json) return InstallReceipt:new(json) end +function InstallReceipt:__tostring() + return ("InstallReceipt(name=%s, purl=%s)"):format(self.name, self:get_source().id or "N/A") +end + function InstallReceipt:get_name() return self.name end @@ -49,22 +50,30 @@ end ---@return InstallReceiptSource function InstallReceipt:get_source() - if self:is_schema_min "1.2" then + if self:is_schema_min "2.0" then return self.source end return self.primary_source --[[@as InstallReceiptSource]] end +function InstallReceipt:get_raw_source() + if self:is_schema_min "2.0" then + return self.source.raw + else + return nil + end +end + +function InstallReceipt:get_install_options() + return self.install_options +end + function InstallReceipt:get_links() return self.links end ----@async ----@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) +function InstallReceipt:to_json() + return vim.json.encode(self) end ---@class InstallReceiptBuilder @@ -96,6 +105,12 @@ function InstallReceiptBuilder:with_source(source) return self end +---@param install_options PackageInstallOpts +function InstallReceiptBuilder:with_install_options(install_options) + self.install_options = install_options + return self +end + ---@param typ '"bin"' | '"share"' | '"opt"' ---@param name string ---@param rel_path string @@ -132,13 +147,15 @@ function InstallReceiptBuilder:build() assert(self.start_time, "start_time is required") assert(self.completion_time, "completion_time is required") assert(self.source, "source is required") + assert(self.install_options, "install_options is required") return InstallReceipt:new { name = self.name, - schema_version = "1.2", + schema_version = "2.0", metrics = { start_time = self.start_time, completion_time = self.completion_time, }, + install_options = self.install_options, source = self.source, links = self.links, } diff --git a/lua/mason-registry/init.lua b/lua/mason-registry/init.lua index 9842748b..746e487b 100644 --- a/lua/mason-registry/init.lua +++ b/lua/mason-registry/init.lua @@ -1,5 +1,5 @@ local EventEmitter = require "mason-core.EventEmitter" -local InstallLocation = require "mason-core.installer.location" +local InstallLocation = require "mason-core.installer.InstallLocation" local Optional = require "mason-core.optional" local _ = require "mason-core.functional" local fs = require "mason-core.fs" @@ -123,7 +123,7 @@ function M.get_all_packages() return get_packages(M.get_all_package_names()) end ----@return (RegistryPackageSpec | PackageSpec)[] +---@return RegistryPackageSpec[] function M.get_all_package_specs() local specs = {} for source in sources.iter() do diff --git a/lua/mason-registry/sources/github.lua b/lua/mason-registry/sources/github.lua index d0a782fb..b314d690 100644 --- a/lua/mason-registry/sources/github.lua +++ b/lua/mason-registry/sources/github.lua @@ -1,4 +1,4 @@ -local InstallLocation = require "mason-core.installer.location" +local InstallLocation = require "mason-core.installer.InstallLocation" local Optional = require "mason-core.optional" local Result = require "mason-core.result" local _ = require "mason-core.functional" diff --git a/lua/mason-registry/sources/util.lua b/lua/mason-registry/sources/util.lua index 8be07010..ed399156 100644 --- a/lua/mason-registry/sources/util.lua +++ b/lua/mason-registry/sources/util.lua @@ -1,5 +1,5 @@ local Optional = require "mason-core.optional" -local Pkg = require "mason-core.package" +local Package = require "mason-core.package" local _ = require "mason-core.functional" local compiler = require "mason-core.installer.compiler" local log = require "mason-core.log" @@ -23,19 +23,19 @@ end M.hydrate_package = _.curryN(function(buffer, spec) -- hydrate Pkg.Lang/License index _.each(function(lang) - local _ = Pkg.Lang[lang] + local _ = Package.Lang[lang] end, spec.languages) _.each(function(lang) - local _ = Pkg.License[lang] + local _ = Package.License[lang] end, spec.licenses) local pkg = buffer[spec.name] if pkg then - -- Apply spec to the existing Package instance. This is important as to not have lingering package instances. + -- Apply spec to the existing Package instances. This is important as to not have lingering package instances. pkg.spec = spec return pkg end - return Pkg:new(spec) + return Package:new(spec) end, 2) return M diff --git a/lua/mason-test/helpers.lua b/lua/mason-test/helpers.lua index 2348e9df..88354046 100644 --- a/lua/mason-test/helpers.lua +++ b/lua/mason-test/helpers.lua @@ -1,7 +1,8 @@ local InstallContext = require "mason-core.installer.context" -local InstallHandle = require "mason-core.installer.handle" -local InstallLocation = require "mason-core.installer.location" +local InstallHandle = require "mason-core.installer.InstallHandle" +local InstallLocation = require "mason-core.installer.InstallLocation" local Result = require "mason-core.result" +local a = require "mason-core.async" local registry = require "mason-registry" local spy = require "luassert.spy" @@ -10,9 +11,8 @@ 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.global() - local context = InstallContext:new(handle, location, opts and opts.install_opts or {}) + local handle = InstallHandle:new(pkg, InstallLocation.global()) + local context = InstallContext:new(handle, opts and opts.install_opts or {}) context.spawn = setmetatable({}, { __index = function(s, cmd) s[cmd] = spy.new(function() @@ -25,4 +25,39 @@ function M.create_context(opts) return context end +---@param pkg AbstractPackage +---@param opts? PackageInstallOpts +function M.sync_install(pkg, opts) + return a.run_blocking(function() + return a.wait(function(resolve, reject) + pkg:install(opts, function(success, result) + (success and resolve or reject)(result) + end) + end) + end) +end + +---@param pkg AbstractPackage +---@param opts? PackageUninstallOpts +function M.sync_uninstall(pkg, opts) + return a.run_blocking(function() + return a.wait(function(resolve, reject) + pkg:uninstall(opts, function(success, result) + (success and resolve or reject)(result) + end) + end) + end) +end + +---@param runner InstallRunner +---@param opts PackageInstallOpts +function M.sync_runner_execute(runner, opts) + local callback = spy.new() + runner:execute(opts, callback) + assert.wait(function() + assert.spy(callback).was_called() + end) + return callback +end + return M diff --git a/lua/mason/api/command.lua b/lua/mason/api/command.lua index 3a3c997b..ea466351 100644 --- a/lua/mason/api/command.lua +++ b/lua/mason/api/command.lua @@ -10,18 +10,18 @@ vim.api.nvim_create_user_command("Mason", Mason, { nargs = 0, }) --- This is needed because neovim doesn't do any validation of command args when using custom completion (I think?) -local filter_valid_packages = _.filter(function(pkg_specifier) +local get_valid_packages = _.filter_map(function(pkg_specifier) + local Optional = require "mason-core.optional" local notify = require "mason-core.notify" local Package = require "mason-core.package" local registry = require "mason-registry" - local package_name = Package.Parse(pkg_specifier) - local ok = pcall(registry.get_package, package_name) - if ok then - return true + local package_name, version = Package.Parse(pkg_specifier) + local ok, pkg = pcall(registry.get_package, package_name) + if ok and pkg then + return Optional.of { pkg = pkg, version = version } else notify(("%q is not a valid package."):format(pkg_specifier), vim.log.levels.ERROR) - return false + return Optional.empty() end end) @@ -56,9 +56,7 @@ local function join_handles(handles) handles )) local failed_packages = _.filter_map(function(handle) - -- TODO: The outcome of a package installation is currently not captured in the handle, but is instead - -- internalized in the Package instance itself. Change this to assert on the handle state when it's - -- available. + -- TODO: Use new install callback to determine success. if not handle.package:is_installed() then return Optional.of(handle.package.name) else @@ -79,21 +77,18 @@ local function join_handles(handles) end ---@param package_specifiers string[] ----@param opts? PackageInstallOpts +---@param opts? table<string, string | boolean> local function MasonInstall(package_specifiers, opts) opts = opts or {} - local Package = require "mason-core.package" local registry = require "mason-registry" local Optional = require "mason-core.optional" - local install_packages = _.filter_map(function(pkg_specifier) - local package_name, version = Package.Parse(pkg_specifier) - local pkg = registry.get_package(package_name) - if pkg:is_installing() then + local install_packages = _.filter_map(function(target) + if target.pkg:is_installing() then return Optional.empty() else - return Optional.of(pkg:install { - version = version, + return Optional.of(target.pkg:install { + version = target.version, debug = opts.debug, force = opts.force, strict = opts.strict, @@ -104,7 +99,7 @@ local function MasonInstall(package_specifiers, opts) if platform.is_headless then registry.refresh() - local valid_packages = filter_valid_packages(package_specifiers) + local valid_packages = get_valid_packages(package_specifiers) if #valid_packages ~= #package_specifiers then -- When executing in headless mode we don't allow any of the provided packages to be invalid. -- This is to avoid things like scripts silently not erroring even if they've provided one or more invalid packages. @@ -117,7 +112,7 @@ local function MasonInstall(package_specifiers, opts) -- Important: We start installation of packages _after_ opening the UI. This gives the UI components a chance to -- register the necessary event handlers in time, avoiding desynced state. registry.refresh(function() - local valid_packages = filter_valid_packages(package_specifiers) + local valid_packages = get_valid_packages(package_specifiers) install_packages(valid_packages) vim.schedule(function() ui.set_sticky_cursor "installing-section" @@ -165,7 +160,7 @@ end, { elseif _.matches("^.+@", arg_lead) then local pkg_name, version = unpack(_.match("^(.+)@(.*)", arg_lead)) local ok, pkg = pcall(registry.get_package, pkg_name) - if not ok then + if not ok or not pkg then return {} end local a = require "mason-core.async" @@ -197,12 +192,10 @@ end, { ---@param package_names string[] local function MasonUninstall(package_names) - local registry = require "mason-registry" - local valid_packages = filter_valid_packages(package_names) + local valid_packages = get_valid_packages(package_names) if #valid_packages > 0 then - _.each(function(package_name) - local pkg = registry.get_package(package_name) - pkg:uninstall() + _.each(function(target) + target.pkg:uninstall() end, valid_packages) require("mason.ui").open() end diff --git a/lua/mason/init.lua b/lua/mason/init.lua index 0be007a4..ff26cc8d 100644 --- a/lua/mason/init.lua +++ b/lua/mason/init.lua @@ -1,4 +1,4 @@ -local InstallLocation = require "mason-core.installer.location" +local InstallLocation = require "mason-core.installer.InstallLocation" local settings = require "mason.settings" local M = {} diff --git a/lua/mason/ui/components/main/package_list.lua b/lua/mason/ui/components/main/package_list.lua index 08870306..2d892a82 100644 --- a/lua/mason/ui/components/main/package_list.lua +++ b/lua/mason/ui/components/main/package_list.lua @@ -86,7 +86,6 @@ local function ExpandedPackageInfo(state, pkg, is_installed) return ExecutablesTable(pkg_state.linked_executables) end) )), - -- ExecutablesTable(is_installed and pkg_state.linked_executables or package.spec.executables), Ui.When(pkg_state.lsp_settings_schema ~= nil, function() local has_expanded = pkg_state.expanded_json_schemas["lsp"] return Ui.Node { diff --git a/lua/mason/ui/instance.lua b/lua/mason/ui/instance.lua index ae246887..8a82bc64 100644 --- a/lua/mason/ui/instance.lua +++ b/lua/mason/ui/instance.lua @@ -1,8 +1,10 @@ +-- !!! +-- in dire need of rework, proceed with caution +-- !!! local Package = require "mason-core.package" local Ui = require "mason-core.ui" local _ = require "mason-core.functional" local a = require "mason-core.async" -local control = require "mason-core.async.control" local display = require "mason-core.ui.display" local notify = require "mason-core.notify" local registry = require "mason-registry" @@ -14,8 +16,6 @@ local LanguageFilter = require "mason.ui.components.language-filter" local Main = require "mason.ui.components.main" local Tabs = require "mason.ui.components.tabs" -local Semaphore = control.Semaphore - require "mason.ui.colors" ---@param state InstallerUiState @@ -81,8 +81,6 @@ local INITIAL_STATE = { outdated_packages = {}, new_versions_check = { is_checking = false, - current = 0, - total = 0, percentage_complete = 0, }, ---@type Package[] @@ -288,8 +286,6 @@ local function setup_handle(handle) handle_spawnhandle_change() mutate_state(function(state) state.packages.states[handle.package.name] = create_initial_package_state() - state.packages.outdated_packages = - _.filter(_.complement(_.equals(handle.package)), state.packages.outdated_packages) end) end @@ -372,11 +368,21 @@ end local function terminate_package_handle(event) ---@type Package local pkg = event.payload - vim.schedule_wrap(notify)(("Cancelling installation of %q."):format(pkg.name)) - pkg:get_handle():if_present( + pkg:get_install_handle():if_present( + ---@param handle InstallHandle + function(handle) + if not handle:is_closed() then + vim.schedule_wrap(notify)(("Cancelling installation of %q."):format(pkg.name)) + handle:terminate() + end + end + ) + + pkg:get_uninstall_handle():if_present( ---@param handle InstallHandle function(handle) if not handle:is_closed() then + vim.schedule_wrap(notify)(("Cancelling uninstallation of %q."):format(pkg.name)) handle:terminate() end end @@ -387,7 +393,7 @@ local function terminate_all_package_handles(event) ---@type Package[] local pkgs = _.list_copy(event.payload) -- we copy because list is mutated while iterating it for _, pkg in ipairs(pkgs) do - pkg:get_handle():if_present( + pkg:get_install_handle():if_present( ---@param handle InstallHandle function(handle) if not handle:is_closed() then @@ -399,16 +405,22 @@ local function terminate_all_package_handles(event) end local function install_package(event) - ---@type Package + ---@type AbstractPackage local pkg = event.payload - pkg:install() + if not pkg:is_installing() then + pkg:install() + end + mutate_state(function(state) + state.packages.outdated_packages = _.filter(_.complement(_.equals(pkg)), state.packages.outdated_packages) + end) end local function uninstall_package(event) - ---@type Package + ---@type AbstractPackage local pkg = event.payload - pkg:uninstall() - vim.schedule_wrap(notify)(("%q was successfully uninstalled."):format(pkg.name)) + if not pkg:is_uninstalling() then + pkg:uninstall() + end end local function toggle_expand_package(event) @@ -449,39 +461,22 @@ local function check_new_package_version(pkg) end ---@async -local function check_new_visible_package_versions() +local function check_new_package_versions() local state = get_state() if state.packages.new_versions_check.is_checking then return end - local installed_visible_packages = _.compose( - _.filter( - ---@param package Package - function(package) - return package - :get_handle() - :map(function(handle) - return handle:is_closed() - end) - :or_else(true) - end - ), - _.filter(function(package) - return state.packages.visible[package.name] - end) - )(state.packages.installed) mutate_state(function(state) state.packages.outdated_packages = {} state.packages.new_versions_check.is_checking = true - state.packages.new_versions_check.current = 0 - state.packages.new_versions_check.total = #installed_visible_packages state.packages.new_versions_check.percentage_complete = 0 end) do local success, err = a.wait(registry.update) mutate_state(function(state) + state.packages.new_versions_check.percentage_complete = 1 if not success then state.info.registry_update_error = tostring(_.gsub("\n", " ", err)) else @@ -490,25 +485,25 @@ local function check_new_visible_package_versions() end) end - local sem = Semaphore:new(5) - a.wait_all(_.map(function(pkg) - return function() - local permit = sem:acquire() - local has_new_version = check_new_package_version(pkg) - mutate_state(function(state) - state.packages.new_versions_check.current = state.packages.new_versions_check.current + 1 - state.packages.new_versions_check.percentage_complete = state.packages.new_versions_check.current - / state.packages.new_versions_check.total - if has_new_version then - table.insert(state.packages.outdated_packages, pkg) - end - end) - permit:forget() + local outdated_packages = {} + + mutate_state(function(state) + for _, pkg in ipairs(state.packages.installed) do + local current_version = pkg:get_installed_version() + local latest_version = pkg:get_latest_version() + if current_version ~= latest_version then + state.packages.states[pkg.name].version = current_version + state.packages.states[pkg.name].new_version = latest_version + table.insert(outdated_packages, pkg) + else + state.packages.states[pkg.name].new_version = nil + end end - end, installed_visible_packages)) + end) - a.sleep(800) + a.sleep(1000) mutate_state(function(state) + state.packages.outdated_packages = outdated_packages state.packages.new_versions_check.is_checking = false state.packages.new_versions_check.current = 0 state.packages.new_versions_check.total = 0 @@ -567,7 +562,7 @@ end local function update_all_packages() local state = get_state() _.each(function(pkg) - pkg:install(pkg) + pkg:install() end, state.packages.outdated_packages) mutate_state(function(state) state.packages.outdated_packages = {} @@ -584,7 +579,7 @@ end local effects = { ["CHECK_NEW_PACKAGE_VERSION"] = a.scope(_.compose(_.partial(pcall, check_new_package_version), _.prop "payload")), - ["CHECK_NEW_VISIBLE_PACKAGE_VERSIONS"] = a.scope(check_new_visible_package_versions), + ["CHECK_NEW_VISIBLE_PACKAGE_VERSIONS"] = a.scope(check_new_package_versions), ["CLEAR_LANGUAGE_FILTER"] = clear_filter, ["CLEAR_SEARCH_MODE"] = clear_search_mode, ["CLOSE_WINDOW"] = window.close, @@ -630,10 +625,13 @@ local function setup_package(pkg) table.insert(state.packages[pkg:is_installed() and "installed" or "uninstalled"], pkg) end) - pkg:get_handle():if_present(setup_handle) - pkg:on("handle", setup_handle) + pkg:get_install_handle():if_present(setup_handle) + pkg:on("install:handle", setup_handle) pkg:on("install:success", function() + vim.schedule(function() + notify(("%s was successfully installed."):format(pkg.name)) + end) mutate_state(function(state) state.packages.states[pkg.name] = create_initial_package_state() if state.packages.expanded == pkg.name then @@ -641,7 +639,6 @@ local function setup_package(pkg) end end) mutate_package_grouping(pkg, "installed") - vim.schedule_wrap(notify)(("%q was successfully installed."):format(pkg.name)) end) pkg:on( @@ -655,6 +652,9 @@ local function setup_package(pkg) end) mutate_package_grouping(pkg, pkg:is_installed() and "installed" or "uninstalled") else + vim.schedule(function() + notify(("%s failed to install."):format(pkg.name), vim.log.levels.ERROR) + end) mutate_package_grouping(pkg, "failed") mutate_state(function(state) state.packages.states[pkg.name].has_failed = true @@ -664,8 +664,17 @@ local function setup_package(pkg) ) pkg:on("uninstall:success", function() + if pkg:is_installing() then + -- We don't care about uninstallations that occur during installation because it's expected behaviour and + -- not constructive to surface to users. + return + end + vim.schedule(function() + notify(("%s was successfully uninstalled."):format(pkg.name)) + end) mutate_state(function(state) state.packages.states[pkg.name] = create_initial_package_state() + state.packages.outdated_packages = _.filter(_.complement(_.equals(pkg)), state.packages.outdated_packages) end) mutate_package_grouping(pkg, "uninstalled") end) @@ -688,7 +697,9 @@ end ---@param packages Package[] local function setup_packages(packages) - _.each(setup_package, _.sort_by(_.prop "name", packages)) + for _, pkg in ipairs(_.sort_by(_.prop "name", packages)) do + setup_package(pkg) + end mutate_state(function(state) state.packages.all = packages end) @@ -714,7 +725,7 @@ window.init { if settings.current.ui.check_outdated_packages_on_open then vim.defer_fn( a.scope(function() - check_new_visible_package_versions() + check_new_package_versions() end), 100 ) diff --git a/tests/fixtures/receipts/1.2.json b/tests/fixtures/receipts/1.2.json deleted file mode 100644 index 75a14f09..00000000 --- a/tests/fixtures/receipts/1.2.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "angular-language-server", - "links": { - "bin": { - "ngserver": "node_modules/.bin/ngserver" - }, - "opt": {}, - "share": {} - }, - "metrics": { - "completion_time": 1694752770559, - "start_time": 1694752764840 - }, - "schema_version": "1.2", - "source": { - "type": "registry+v1", - "id": "pkg:npm/%40angular/language-server@16.1.8" - } -} diff --git a/tests/fixtures/receipts/2.0.json b/tests/fixtures/receipts/2.0.json new file mode 100644 index 00000000..b0c9e2f1 --- /dev/null +++ b/tests/fixtures/receipts/2.0.json @@ -0,0 +1,30 @@ +{ + "links": { + "bin": { + "ngserver": "node_modules/.bin/ngserver" + }, + "share": {}, + "opt": {} + }, + "name": "angular-language-server", + "schema_version": "2.0", + "metrics": { + "start_time": 1739692587948, + "completion_time": 1739692591360 + }, + "source": { + "id": "pkg:npm/%40angular/language-server@19.1.0", + "raw": { + "id": "pkg:npm/%40angular/language-server@19.1.0", + "extra_packages": [ + "typescript@5.4.5" + ] + }, + "type": "registry+v1" + }, + "install_options": { + "debug": false, + "strict": false, + "force": false + } +} diff --git a/tests/mason-core/installer/handle_spec.lua b/tests/mason-core/installer/InstallHandle_spec.lua index 780b1cc7..914309b2 100644 --- a/tests/mason-core/installer/handle_spec.lua +++ b/tests/mason-core/installer/InstallHandle_spec.lua @@ -1,9 +1,9 @@ -local InstallHandle = require "mason-core.installer.handle" +local InstallHandle = require "mason-core.installer.InstallHandle" local mock = require "luassert.mock" local spy = require "luassert.spy" local stub = require "luassert.stub" -describe("installer handle", function() +describe("InstallHandle ::", function() local snapshot before_each(function() diff --git a/tests/mason-core/installer/runner_spec.lua b/tests/mason-core/installer/InstallRunner_spec.lua index f4acdcc1..696f7b34 100644 --- a/tests/mason-core/installer/runner_spec.lua +++ b/tests/mason-core/installer/InstallRunner_spec.lua @@ -1,15 +1,17 @@ -local InstallHandle = require "mason-core.installer.handle" -local InstallLocation = require "mason-core.installer.location" -local InstallRunner = require "mason-core.installer.runner" +local InstallHandle = require "mason-core.installer.InstallHandle" +local InstallLocation = require "mason-core.installer.InstallLocation" +local InstallRunner = require "mason-core.installer.InstallRunner" local fs = require "mason-core.fs" local match = require "luassert.match" +local receipt = require "mason-core.receipt" local spy = require "luassert.spy" local stub = require "luassert.stub" local Semaphore = require("mason-core.async.control").Semaphore local a = require "mason-core.async" local registry = require "mason-registry" +local test_helpers = require "mason-test.helpers" -describe("install runner ::", function() +describe("InstallRunner ::", function() local dummy = registry.get_package "dummy" local dummy2 = registry.get_package "dummy2" @@ -17,32 +19,39 @@ describe("install runner ::", function() before_each(function() snapshot = assert.snapshot() + if dummy:is_installed() then + test_helpers.sync_uninstall(dummy) + end + if dummy2:is_installed() then + test_helpers.sync_uninstall(dummy2) + end end) after_each(function() snapshot:revert() end) - before_each(function() - dummy:uninstall() - dummy2:uninstall() - end) - describe("locking ::", function() it("should respect semaphore locks", function() local semaphore = Semaphore:new(1) local location = InstallLocation.global() - local dummy_handle = InstallHandle:new(dummy) - local runner_1 = InstallRunner:new(location, dummy_handle, semaphore) - local runner_2 = InstallRunner:new(location, InstallHandle:new(dummy2), semaphore) + local dummy_handle = InstallHandle:new(dummy, location) + local runner_1 = InstallRunner:new(dummy_handle, semaphore) + local runner_2 = InstallRunner:new(InstallHandle:new(dummy2, location), semaphore) - stub(dummy.spec.source, "install", function() - a.sleep(10000) + stub(dummy.spec.source, "install", function(ctx) + ctx:await(function() end) end) - spy.on(dummy2.spec.source, "install") + spy.on(dummy2.spec.source, "install", function() end) - runner_1:execute {} - runner_2:execute {} + local callback1 = spy.new() + local callback2 = spy.new() + local run = a.scope(function() + runner_1:execute({}, callback1) + runner_2:execute({}, callback2) + end) + + run() assert.wait(function() assert.spy(dummy.spec.source.install).was_called(1) @@ -54,17 +63,22 @@ describe("install runner ::", function() assert.wait(function() assert.spy(dummy2.spec.source.install).was_called(1) end) + + assert.wait(function() + assert.spy(callback1).was_called() + assert.spy(callback2).was_called() + end) end) it("should write lockfile", function() local semaphore = Semaphore:new(1) local location = InstallLocation.global() - local dummy_handle = InstallHandle:new(dummy) - local runner = InstallRunner:new(location, dummy_handle, semaphore) + local dummy_handle = InstallHandle:new(dummy, location) + local runner = InstallRunner:new(dummy_handle, semaphore) spy.on(fs.async, "write_file") - runner:execute {} + test_helpers.sync_runner_execute(runner, {}) assert.wait(function() assert.spy(fs.async.write_file).was_called_with(location:lockfile(dummy.name), vim.fn.getpid()) @@ -74,16 +88,15 @@ describe("install runner ::", function() it("should abort installation if installation lock exists", function() local semaphore = Semaphore:new(1) local location = InstallLocation.global() - local dummy_handle = InstallHandle:new(dummy) - local runner = InstallRunner:new(location, dummy_handle, semaphore) + local dummy_handle = InstallHandle:new(dummy, location) + local runner = InstallRunner:new(dummy_handle, semaphore) stub(fs.async, "file_exists") stub(fs.async, "read_file") fs.async.file_exists.on_call_with(location:lockfile(dummy.name)).returns(true) fs.async.read_file.on_call_with(location:lockfile(dummy.name)).returns "1337" - local callback = spy.new() - runner:execute({}, callback) + local callback = test_helpers.sync_runner_execute(runner, {}) assert.wait(function() assert.spy(callback).was_called() @@ -97,28 +110,30 @@ describe("install runner ::", function() it("should not abort installation if installation lock exists with force=true", function() local semaphore = Semaphore:new(1) local location = InstallLocation.global() - local dummy_handle = InstallHandle:new(dummy) - local runner = InstallRunner:new(location, dummy_handle, semaphore) + local dummy_handle = InstallHandle:new(dummy, location) + local runner = InstallRunner:new(dummy_handle, semaphore) stub(fs.async, "file_exists") stub(fs.async, "read_file") fs.async.file_exists.on_call_with(location:lockfile(dummy.name)).returns(true) fs.async.read_file.on_call_with(location:lockfile(dummy.name)).returns "1337" - local callback = spy.new() - runner:execute({ force = true }, callback) + local callback = test_helpers.sync_runner_execute(runner, { force = true }) assert.wait(function() assert.spy(callback).was_called() - assert.spy(callback).was_called_with(true, nil) + assert.spy(callback).was_called_with(true, match.instanceof(receipt.InstallReceipt)) end) end) it("should release lock after successful installation", function() local semaphore = Semaphore:new(1) local location = InstallLocation.global() - local dummy_handle = InstallHandle:new(dummy) - local runner = InstallRunner:new(location, dummy_handle, semaphore) + local dummy_handle = InstallHandle:new(dummy, location) + local runner = InstallRunner:new(dummy_handle, semaphore) + stub(dummy.spec.source, "install", function() + a.sleep(1000) + end) local callback = spy.new() runner:execute({}, callback) @@ -127,7 +142,7 @@ describe("install runner ::", function() assert.is_true(fs.sync.file_exists(location:lockfile(dummy.name))) end) assert.wait(function() - assert.spy(callback).was_called() + assert.spy(callback).was_called_with(true, match.instanceof(receipt.InstallReceipt)) end) assert.is_false(fs.sync.file_exists(location:lockfile(dummy.name))) end) @@ -135,49 +150,17 @@ describe("install runner ::", function() it("should initialize install location", function() local location = InstallLocation.global() - local runner = InstallRunner:new(location, InstallHandle:new(registry.get_package "dummy"), Semaphore:new(1)) + local runner = InstallRunner:new(InstallHandle:new(dummy, location), Semaphore:new(1)) spy.on(location, "initialize") - runner:execute {} + test_helpers.sync_runner_execute(runner, {}) assert.wait(function() assert.spy(location.initialize).was_called(1) end) end) - describe("receipt ::", function() - it("should write receipt", function() - local location = InstallLocation.global() - local runner = - InstallRunner:new(location, InstallHandle:new(registry.get_package "dummy"), Semaphore:new(1)) - - runner:execute {} - - assert.wait(function() - local receipt_file = location:package "dummy/mason-receipt.json" - assert.is_true(fs.sync.file_exists(receipt_file)) - assert.is_true(match.tbl_containing { - name = "dummy", - schema_version = "1.2", - metrics = match.tbl_containing { - completion_time = match.is_number(), - start_time = match.is_number(), - }, - source = match.same { - id = "pkg:mason/dummy@1.0.0", - type = "registry+v1", - }, - links = match.same { - bin = {}, - opt = {}, - share = {}, - }, - }(vim.json.decode(fs.sync.read_file(receipt_file)))) - end) - end) - end) - it("should emit failures", function() local registry_spy = spy.new() local package_spy = spy.new() @@ -185,115 +168,128 @@ describe("install runner ::", function() dummy:once("install:failed", package_spy) local location = InstallLocation.global() - local handle = InstallHandle:new(registry.get_package "dummy") - local runner = InstallRunner:new(location, handle, Semaphore:new(1)) + local handle = InstallHandle:new(dummy, location) + local runner = InstallRunner:new(handle, Semaphore:new(1)) stub(dummy.spec.source, "install", function() error("I've made a mistake.", 0) end) - local callback = spy.new() - runner:execute({}, callback) + local callback = test_helpers.sync_runner_execute(runner, {}) - assert.wait(function() - assert.spy(registry_spy).was_called(1) - assert.spy(registry_spy).was_called_with(match.is_ref(dummy), match.is_ref(handle), "I've made a mistake.") - assert.spy(package_spy).was_called(1) - assert.spy(package_spy).was_called_with(match.is_ref(handle), "I've made a mistake.") + assert.spy(registry_spy).was_called(1) + assert.spy(registry_spy).was_called_with(match.is_ref(dummy), "I've made a mistake.") + assert.spy(package_spy).was_called(1) + assert.spy(package_spy).was_called_with "I've made a mistake." - assert.spy(callback).was_called(1) - assert.spy(callback).was_called_with(false, "I've made a mistake.") - end, 10) + assert.spy(callback).was_called(1) + assert.spy(callback).was_called_with(false, "I've made a mistake.") end) it("should terminate installation", function() local location = InstallLocation.global() - local handle = InstallHandle:new(registry.get_package "dummy") - local runner = InstallRunner:new(location, handle, Semaphore:new(1)) + local handle = InstallHandle:new(dummy, location) + local runner = InstallRunner:new(handle, Semaphore:new(1)) local capture = spy.new() stub(dummy.spec.source, "install", function() - capture() + capture(1) handle:terminate() a.sleep(0) - capture() + capture(2) end) - local callback = spy.new() - - runner:execute({}, callback) + local callback = test_helpers.sync_runner_execute(runner, {}) - assert.wait(function() - assert.spy(callback).was_called(1) - assert.spy(callback).was_called_with(false, "Installation was aborted.") - - assert.spy(capture).was_called(1) - end) + assert.spy(callback).was_called_with(false, "Installation was aborted.") + assert.spy(capture).was_called(1) + assert.spy(capture).was_called_with(1) end) it("should write debug logs when debug=true", function() local location = InstallLocation.global() - local handle = InstallHandle:new(registry.get_package "dummy") - local runner = InstallRunner:new(location, handle, Semaphore:new(1)) + local handle = InstallHandle:new(dummy, location) + local runner = InstallRunner:new(handle, Semaphore:new(1)) stub(dummy.spec.source, "install", function(ctx) ctx.stdio_sink.stdout "Hello " ctx.stdio_sink.stderr "world!" end) - local callback = spy.new() - runner:execute({ debug = true }, callback) + local callback = test_helpers.sync_runner_execute(runner, { debug = true }) - assert.wait(function() - assert.spy(callback).was_called() - assert.spy(callback).was_called_with(true, nil) - end) + assert.spy(callback).was_called_with(true, match.instanceof(receipt.InstallReceipt)) assert.is_true(fs.sync.file_exists(location:package "dummy/mason-debug.log")) assert.equals("Hello world!", fs.sync.read_file(location:package "dummy/mason-debug.log")) end) it("should not retain installation directory on failure", function() local location = InstallLocation.global() - local handle = InstallHandle:new(registry.get_package "dummy") - local runner = InstallRunner:new(location, handle, Semaphore:new(1)) + local handle = InstallHandle:new(dummy, location) + local runner = InstallRunner:new(handle, Semaphore:new(1)) stub(dummy.spec.source, "install", function(ctx) ctx.stdio_sink.stderr "Something will go terribly wrong.\n" error("This went terribly wrong.", 0) end) - local callback = spy.new() - runner:execute({}, callback) + local callback = test_helpers.sync_runner_execute(runner, {}) - assert.wait(function() - assert.spy(callback).was_called() - assert.spy(callback).was_called_with(false, "This went terribly wrong.") - end) + assert.spy(callback).was_called_with(false, "This went terribly wrong.") assert.is_false(fs.sync.dir_exists(location:staging "dummy")) assert.is_false(fs.sync.dir_exists(location:package "dummy")) end) it("should retain installation directory on failure and debug=true", function() local location = InstallLocation.global() - local handle = InstallHandle:new(registry.get_package "dummy") - local runner = InstallRunner:new(location, handle, Semaphore:new(1)) + local handle = InstallHandle:new(dummy, location) + local runner = InstallRunner:new(handle, Semaphore:new(1)) stub(dummy.spec.source, "install", function(ctx) ctx.stdio_sink.stderr "Something will go terribly wrong.\n" error("This went terribly wrong.", 0) end) - local callback = spy.new() - runner:execute({ debug = true }, callback) + local callback = test_helpers.sync_runner_execute(runner, { debug = true }) - assert.wait(function() - assert.spy(callback).was_called() - assert.spy(callback).was_called_with(false, "This went terribly wrong.") - end) + assert.spy(callback).was_called_with(false, "This went terribly wrong.") assert.is_true(fs.sync.dir_exists(location:staging "dummy")) assert.equals( "Something will go terribly wrong.\nThis went terribly wrong.\n", fs.sync.read_file(location:staging "dummy/mason-debug.log") ) end) + + describe("receipt ::", function() + it("should write receipt", function() + local location = InstallLocation.global() + local runner = InstallRunner:new(InstallHandle:new(dummy, location), Semaphore:new(1)) + + test_helpers.sync_runner_execute(runner, {}) + + local receipt_file = location:package "dummy/mason-receipt.json" + assert.is_true(fs.sync.file_exists(receipt_file)) + assert.is_true(match.tbl_containing { + name = "dummy", + schema_version = "2.0", + install_options = match.same {}, + metrics = match.tbl_containing { + completion_time = match.is_number(), + start_time = match.is_number(), + }, + source = match.same { + id = "pkg:mason/dummy@1.0.0", + type = "registry+v1", + raw = { + id = "pkg:mason/dummy@1.0.0", + }, + }, + links = match.same { + bin = {}, + opt = {}, + share = {}, + }, + }(vim.json.decode(fs.sync.read_file(receipt_file)))) + end) + end) end) diff --git a/tests/mason-core/installer/registry/installer_spec.lua b/tests/mason-core/installer/compiler/compiler_spec.lua index 93c91444..d7e18b25 100644 --- a/tests/mason-core/installer/registry/installer_spec.lua +++ b/tests/mason-core/installer/compiler/compiler_spec.lua @@ -35,7 +35,7 @@ local dummy_compiler = { end, } -describe("registry installer :: parsing", function() +describe("registry compiler :: parsing", function() it("should parse valid package specs", function() compiler.register_compiler("dummy", dummy_compiler) @@ -122,7 +122,7 @@ describe("registry installer :: parsing", function() it("should handle PLATFORM_UNSUPPORTED", function() compiler.register_compiler("dummy", dummy_compiler) - local result = compiler.compile({ + local result = compiler.compile_installer({ schema = "registry+v1", source = { id = "pkg:dummy/package-name@v1.2.3", @@ -136,7 +136,7 @@ describe("registry installer :: parsing", function() it("should error upon parsing failures", function() compiler.register_compiler("dummy", dummy_compiler) - local result = compiler.compile({ + local result = compiler.compile_installer({ schema = "registry+v1", source = { id = "pkg:dummy/package-name@v1.2.3", @@ -148,7 +148,7 @@ describe("registry installer :: parsing", function() end) end) -describe("registry installer :: compiling", function() +describe("registry compiler :: compiling", function() local snapshot before_each(function() @@ -166,7 +166,7 @@ describe("registry installer :: compiling", function() ---@type PackageInstallOpts local opts = {} - local result = compiler.compile({ + local result = compiler.compile_installer({ schema = "registry+v1", source = { id = "pkg:dummy/package-name@v1.2.3", @@ -190,7 +190,7 @@ describe("registry installer :: compiling", function() ---@type PackageInstallOpts local opts = { version = "v2.0.0" } - local result = compiler.compile({ + local result = compiler.compile_installer({ schema = "registry+v1", source = { id = "pkg:dummy/package-name@v1.2.3", @@ -222,7 +222,7 @@ describe("registry installer :: compiling", function() ---@type PackageInstallOpts local opts = { version = "v13.3.7" } - local result = compiler.compile({ + local result = compiler.compile_installer({ schema = "registry+v1", source = { id = "pkg:dummy/package-name@v1.2.3", @@ -234,7 +234,7 @@ describe("registry installer :: compiling", function() local ctx = test_helpers.create_context { install_opts = opts } local err = assert.has_error(function() - ctx:execute(installer_fn) + ctx:execute(installer_fn):get_or_throw() end) assert.equals([[Version "v13.3.7" is not available.]], err) @@ -255,7 +255,7 @@ describe("registry installer :: compiling", function() ---@type PackageInstallOpts local opts = {} - local result = compiler.compile({ + local result = compiler.compile_installer({ schema = "registry+v1", source = { id = "pkg:dummy/package-name@v1.2.3", @@ -268,7 +268,7 @@ describe("registry installer :: compiling", function() local ctx = test_helpers.create_context() local err = assert.has_error(function() - ctx:execute(installer_fn) + ctx:execute(installer_fn):get_or_throw() end) assert.equals("This is a failure.", err) end) @@ -292,7 +292,7 @@ describe("registry installer :: compiling", function() ---@type PackageInstallOpts local opts = {} - local result = compiler.compile(spec, opts) + local result = compiler.compile_installer(spec, opts) assert.is_true(result:is_success()) local installer_fn = result:get_or_nil() diff --git a/tests/mason-core/installer/registry/compilers/cargo_spec.lua b/tests/mason-core/installer/compiler/compilers/cargo_spec.lua index 69ac446d..7cdb7ee4 100644 --- a/tests/mason-core/installer/registry/compilers/cargo_spec.lua +++ b/tests/mason-core/installer/compiler/compilers/cargo_spec.lua @@ -14,7 +14,7 @@ local function purl(overrides) return vim.tbl_deep_extend("force", purl, overrides) end -describe("cargo provider :: parsing", function() +describe("cargo compiler :: parsing", function() it("should parse package", function() assert.same( Result.success { @@ -93,7 +93,7 @@ describe("cargo provider :: parsing", function() end) end) -describe("cargo provider :: installing", function() +describe("cargo compiler :: installing", function() local snapshot before_each(function() @@ -129,7 +129,7 @@ describe("cargo provider :: installing", function() end) end) -describe("cargo provider :: versions", function() +describe("cargo compiler :: versions", function() local snapshot before_each(function() diff --git a/tests/mason-core/installer/registry/compilers/composer_spec.lua b/tests/mason-core/installer/compiler/compilers/composer_spec.lua index c184adf5..ae130dc3 100644 --- a/tests/mason-core/installer/registry/compilers/composer_spec.lua +++ b/tests/mason-core/installer/compiler/compilers/composer_spec.lua @@ -13,7 +13,7 @@ local function purl(overrides) return vim.tbl_deep_extend("force", purl, overrides) end -describe("composer provider :: parsing", function() +describe("composer compiler :: parsing", function() it("should parse package", function() assert.same( Result.success { @@ -25,7 +25,7 @@ describe("composer provider :: parsing", function() end) end) -describe("composer provider :: installing", function() +describe("composer compiler :: installing", function() local snapshot before_each(function() diff --git a/tests/mason-core/installer/registry/compilers/gem_spec.lua b/tests/mason-core/installer/compiler/compilers/gem_spec.lua index b38bba33..9d99da00 100644 --- a/tests/mason-core/installer/registry/compilers/gem_spec.lua +++ b/tests/mason-core/installer/compiler/compilers/gem_spec.lua @@ -13,7 +13,7 @@ local function purl(overrides) return vim.tbl_deep_extend("force", purl, overrides) end -describe("gem provider :: parsing", function() +describe("gem compiler :: parsing", function() it("should parse package", function() assert.same( Result.success { @@ -30,7 +30,7 @@ describe("gem provider :: parsing", function() end) end) -describe("gem provider :: installing", function() +describe("gem compiler :: installing", function() local snapshot before_each(function() diff --git a/tests/mason-core/installer/registry/compilers/generic/build_spec.lua b/tests/mason-core/installer/compiler/compilers/generic/build_spec.lua index 8b8baeab..63a400d1 100644 --- a/tests/mason-core/installer/registry/compilers/generic/build_spec.lua +++ b/tests/mason-core/installer/compiler/compilers/generic/build_spec.lua @@ -13,7 +13,7 @@ local function purl(overrides) return vim.tbl_deep_extend("force", purl, overrides) end -describe("generic provider :: build :: parsing", function() +describe("generic compiler :: build :: parsing", function() it("should parse single build target", function() assert.same( Result.success { @@ -118,7 +118,7 @@ describe("generic provider :: build :: parsing", function() end) end) -describe("generic provider :: build :: installing", function() +describe("generic compiler :: build :: installing", function() local snapshot before_each(function() diff --git a/tests/mason-core/installer/registry/compilers/generic/download_spec.lua b/tests/mason-core/installer/compiler/compilers/generic/download_spec.lua index 4046d898..afe25086 100644 --- a/tests/mason-core/installer/registry/compilers/generic/download_spec.lua +++ b/tests/mason-core/installer/compiler/compilers/generic/download_spec.lua @@ -1,7 +1,7 @@ local Purl = require "mason-core.purl" local Result = require "mason-core.result" -local match = require "luassert.match" local generic = require "mason-core.installer.compiler.compilers.generic" +local match = require "luassert.match" local stub = require "luassert.stub" local test_helpers = require "mason-test.helpers" @@ -14,7 +14,7 @@ local function purl(overrides) return vim.tbl_deep_extend("force", purl, overrides) end -describe("generic provider :: download :: parsing", function() +describe("generic compiler :: download :: parsing", function() it("should parse single download target", function() assert.same( Result.success { @@ -98,7 +98,7 @@ describe("generic provider :: download :: parsing", function() end) end) -describe("generic provider :: download :: installing", function() +describe("generic compiler :: download :: installing", function() local snapshot before_each(function() diff --git a/tests/mason-core/installer/registry/compilers/github/build_spec.lua b/tests/mason-core/installer/compiler/compilers/github/build_spec.lua index 82271fee..8315c272 100644 --- a/tests/mason-core/installer/registry/compilers/github/build_spec.lua +++ b/tests/mason-core/installer/compiler/compilers/github/build_spec.lua @@ -1,6 +1,8 @@ local Purl = require "mason-core.purl" local Result = require "mason-core.result" local github = require "mason-core.installer.compiler.compilers.github" +local stub = require "luassert.stub" +local test_helpers = require "mason-test.helpers" ---@param overrides Purl local function purl(overrides) @@ -11,7 +13,7 @@ local function purl(overrides) return vim.tbl_deep_extend("force", purl, overrides) end -describe("github provider :: build :: parsing", function() +describe("github compiler :: build :: parsing", function() it("should parse build source", function() assert.same( Result.success { @@ -56,3 +58,47 @@ describe("github provider :: build :: parsing", function() ) end) end) + +describe("github compiler :: build :: installing", function() + local snapshot + + before_each(function() + snapshot = assert.snapshot() + end) + + after_each(function() + snapshot:revert() + end) + + it("should install github build sources", function() + local ctx = test_helpers.create_context() + local std = require "mason-core.installer.managers.std" + local common = require "mason-core.installer.managers.common" + stub(std, "clone", mockx.returns(Result.success())) + stub(common, "run_build_instruction", mockx.returns(Result.success())) + + local result = ctx:execute(function() + return github.install(ctx, { + repo = "namespace/name", + rev = "2023-03-09", + build = { + run = [[npm install && npm run compile]], + env = { + SOME_VALUE = "here", + }, + }, + }, purl()) + end) + + assert.is_true(result:is_success()) + assert.spy(std.clone).was_called(1) + assert.spy(std.clone).was_called_with("namespace/name", { rev = "2023-03-09" }) + assert.spy(common.run_build_instruction).was_called(1) + assert.spy(common.run_build_instruction).was_called_with { + run = [[npm install && npm run compile]], + env = { + SOME_VALUE = "here", + }, + } + end) +end) diff --git a/tests/mason-core/installer/registry/compilers/github/release_spec.lua b/tests/mason-core/installer/compiler/compilers/github/release_spec.lua index 7ea9f42e..a59a6b79 100644 --- a/tests/mason-core/installer/registry/compilers/github/release_spec.lua +++ b/tests/mason-core/installer/compiler/compilers/github/release_spec.lua @@ -16,7 +16,7 @@ local function purl(overrides) return vim.tbl_deep_extend("force", purl, overrides) end -describe("github provider :: release :: parsing", function() +describe("github compiler :: release :: parsing", function() it("should parse release asset source", function() assert.same( Result.success { @@ -211,6 +211,7 @@ describe("github provider :: release :: parsing", function() version_overrides = { { constraint = "semver:<=1.0.0", + id = "pkg:github/owner/repo@1.0.0", asset = { { target = "darwin_x64", @@ -225,7 +226,7 @@ describe("github provider :: release :: parsing", function() assert.is_true(result:is_success()) assert.same({ - id = "pkg:github/owner/repo@1.2.3", + id = "pkg:github/owner/repo@1.0.0", asset = { target = "darwin_x64", file = "old-asset.tar.gz", @@ -236,17 +237,6 @@ describe("github provider :: release :: parsing", function() out_file = "old-asset.tar.gz", }, }, - version_overrides = { - { - constraint = "semver:<=1.0.0", - asset = { - { - target = "darwin_x64", - file = "old-asset.tar.gz", - }, - }, - }, - }, repo = "owner/repo", }, parsed.source) end) @@ -279,7 +269,7 @@ describe("github provider :: release :: parsing", function() end) end) -describe("github provider :: release :: installing", function() +describe("github compiler :: release :: installing", function() local snapshot before_each(function() diff --git a/tests/mason-core/installer/registry/compilers/golang_spec.lua b/tests/mason-core/installer/compiler/compilers/golang_spec.lua index 8a3abc8a..fa474870 100644 --- a/tests/mason-core/installer/registry/compilers/golang_spec.lua +++ b/tests/mason-core/installer/compiler/compilers/golang_spec.lua @@ -13,7 +13,7 @@ local function purl(overrides) return vim.tbl_deep_extend("force", purl, overrides) end -describe("golang provider :: parsing", function() +describe("golang compiler :: parsing", function() it("should parse package", function() assert.same( Result.success { @@ -26,7 +26,7 @@ describe("golang provider :: parsing", function() end) end) -describe("golang provider :: installing", function() +describe("golang compiler :: installing", function() local snapshot before_each(function() diff --git a/tests/mason-core/installer/registry/compilers/luarocks_spec.lua b/tests/mason-core/installer/compiler/compilers/luarocks_spec.lua index b8642fcf..25bcbf94 100644 --- a/tests/mason-core/installer/registry/compilers/luarocks_spec.lua +++ b/tests/mason-core/installer/compiler/compilers/luarocks_spec.lua @@ -14,7 +14,7 @@ local function purl(overrides) return vim.tbl_deep_extend("force", purl, overrides) end -describe("luarocks provider :: parsing", function() +describe("luarocks compiler :: parsing", function() it("should parse package", function() assert.same( Result.success { @@ -52,7 +52,7 @@ describe("luarocks provider :: parsing", function() end) end) -describe("luarocks provider :: installing", function() +describe("luarocks compiler :: installing", function() local snapshot before_each(function() diff --git a/tests/mason-core/installer/registry/compilers/npm_spec.lua b/tests/mason-core/installer/compiler/compilers/npm_spec.lua index 680df5bc..94d67801 100644 --- a/tests/mason-core/installer/registry/compilers/npm_spec.lua +++ b/tests/mason-core/installer/compiler/compilers/npm_spec.lua @@ -13,7 +13,7 @@ local function purl(overrides) return vim.tbl_deep_extend("force", purl, overrides) end -describe("npm provider :: parsing", function() +describe("npm compiler :: parsing", function() it("should parse package", function() assert.same( Result.success { @@ -26,7 +26,7 @@ describe("npm provider :: parsing", function() end) end) -describe("npm provider :: installing", function() +describe("npm compiler :: installing", function() local snapshot before_each(function() diff --git a/tests/mason-core/installer/registry/compilers/nuget_spec.lua b/tests/mason-core/installer/compiler/compilers/nuget_spec.lua index f514e666..973c0932 100644 --- a/tests/mason-core/installer/registry/compilers/nuget_spec.lua +++ b/tests/mason-core/installer/compiler/compilers/nuget_spec.lua @@ -13,7 +13,7 @@ local function purl(overrides) return vim.tbl_deep_extend("force", purl, overrides) end -describe("nuget provider :: parsing", function() +describe("nuget compiler :: parsing", function() it("should parse package", function() assert.same( Result.success { @@ -25,7 +25,7 @@ describe("nuget provider :: parsing", function() end) end) -describe("nuget provider :: installing", function() +describe("nuget compiler :: installing", function() local snapshot before_each(function() diff --git a/tests/mason-core/installer/registry/compilers/opam_spec.lua b/tests/mason-core/installer/compiler/compilers/opam_spec.lua index c2c7638e..7b041a9e 100644 --- a/tests/mason-core/installer/registry/compilers/opam_spec.lua +++ b/tests/mason-core/installer/compiler/compilers/opam_spec.lua @@ -13,7 +13,7 @@ local function purl(overrides) return vim.tbl_deep_extend("force", purl, overrides) end -describe("opam provider :: parsing", function() +describe("opam compiler :: parsing", function() it("should parse package", function() assert.same( Result.success { @@ -25,7 +25,7 @@ describe("opam provider :: parsing", function() end) end) -describe("opam provider :: installing", function() +describe("opam compiler :: installing", function() local snapshot before_each(function() diff --git a/tests/mason-core/installer/registry/compilers/openvsx_spec.lua b/tests/mason-core/installer/compiler/compilers/openvsx_spec.lua index d3868a69..d3868a69 100644 --- a/tests/mason-core/installer/registry/compilers/openvsx_spec.lua +++ b/tests/mason-core/installer/compiler/compilers/openvsx_spec.lua diff --git a/tests/mason-core/installer/registry/compilers/pypi_spec.lua b/tests/mason-core/installer/compiler/compilers/pypi_spec.lua index 61742b4e..7e5b8e1d 100644 --- a/tests/mason-core/installer/registry/compilers/pypi_spec.lua +++ b/tests/mason-core/installer/compiler/compilers/pypi_spec.lua @@ -14,7 +14,7 @@ local function purl(overrides) return vim.tbl_deep_extend("force", purl, overrides) end -describe("pypi provider :: parsing", function() +describe("pypi compiler :: parsing", function() it("should parse package", function() settings.set { pip = { @@ -43,7 +43,7 @@ describe("pypi provider :: parsing", function() end) end) -describe("pypi provider :: installing", function() +describe("pypi compiler :: installing", function() local snapshot before_each(function() diff --git a/tests/mason-core/installer/registry/expr_spec.lua b/tests/mason-core/installer/compiler/expr_spec.lua index 944a5983..944a5983 100644 --- a/tests/mason-core/installer/registry/expr_spec.lua +++ b/tests/mason-core/installer/compiler/expr_spec.lua diff --git a/tests/mason-core/installer/registry/link_spec.lua b/tests/mason-core/installer/compiler/link_spec.lua index 62777bc9..62777bc9 100644 --- a/tests/mason-core/installer/registry/link_spec.lua +++ b/tests/mason-core/installer/compiler/link_spec.lua diff --git a/tests/mason-core/installer/registry/util_spec.lua b/tests/mason-core/installer/compiler/util_spec.lua index be687f36..be687f36 100644 --- a/tests/mason-core/installer/registry/util_spec.lua +++ b/tests/mason-core/installer/compiler/util_spec.lua diff --git a/tests/mason-core/installer/context_spec.lua b/tests/mason-core/installer/context_spec.lua index 9c1805cb..d753c05f 100644 --- a/tests/mason-core/installer/context_spec.lua +++ b/tests/mason-core/installer/context_spec.lua @@ -104,7 +104,7 @@ cmd.exe /C echo %GREETING% %*]] assert.spy(ctx.write_shell_exec_wrapper).was_called_with( match.is_ref(ctx), "my-wrapper-script", - ("node %q"):format(path.concat { dummy:get_install_path(), js_rel_path }) + ("node %q"):format(path.concat { ctx:get_install_path(), js_rel_path }) ) end) @@ -122,7 +122,7 @@ cmd.exe /C echo %GREETING% %*]] assert.spy(ctx.write_shell_exec_wrapper).was_called_with( match.is_ref(ctx), "my-wrapper-script", - ("ruby %q"):format(path.concat { dummy:get_install_path(), js_rel_path }) + ("ruby %q"):format(path.concat { ctx:get_install_path(), js_rel_path }) ) end) @@ -157,7 +157,7 @@ cmd.exe /C echo %GREETING% %*]] assert.spy(ctx.write_shell_exec_wrapper).was_called_with( match.is_ref(ctx), "my-wrapper-script", - ("%q -m my-module"):format(path.concat { pypi.venv_path(dummy:get_install_path()), "python" }) + ("%q -m my-module"):format(path.concat { pypi.venv_path(ctx:get_install_path()), "python" }) ) end) @@ -196,7 +196,7 @@ cmd.exe /C echo %GREETING% %*]] .was_called_with( match.is_ref(ctx), "my-wrapper-script", - ("%q"):format(path.concat { dummy:get_install_path(), exec_rel_path }) + ("%q"):format(path.concat { ctx:get_install_path(), exec_rel_path }) ) end) @@ -229,7 +229,7 @@ cmd.exe /C echo %GREETING% %*]] assert.spy(ctx.write_shell_exec_wrapper).was_called_with( match.is_ref(ctx), "my-wrapper-script", - ("php %q"):format(path.concat { dummy:get_install_path(), php_rel_path }) + ("php %q"):format(path.concat { ctx:get_install_path(), php_rel_path }) ) end) diff --git a/tests/mason-core/installer/linker_spec.lua b/tests/mason-core/installer/linker_spec.lua index 9d3afeac..2177f6a3 100644 --- a/tests/mason-core/installer/linker_spec.lua +++ b/tests/mason-core/installer/linker_spec.lua @@ -50,9 +50,9 @@ describe("linker", function() fs.async.file_exists.on_call_with(ctx.location:bin "my-executable").returns(false) fs.async.file_exists.on_call_with(ctx.location:bin "another-executable").returns(false) fs.async.file_exists - .on_call_with(path.concat { dummy:get_install_path(), "nested", "path", "my-executable" }) + .on_call_with(path.concat { ctx:get_install_path(), "nested", "path", "my-executable" }) .returns(true) - fs.async.file_exists.on_call_with(path.concat { dummy:get_install_path(), "another-executable" }).returns(true) + fs.async.file_exists.on_call_with(path.concat { ctx:get_install_path(), "another-executable" }).returns(true) ctx:link_bin("my-executable", path.concat { "nested", "path", "my-executable" }) ctx:link_bin("another-executable", "another-executable") @@ -86,9 +86,9 @@ describe("linker", function() fs.async.file_exists.on_call_with(ctx.location:bin "my-executable").returns(false) fs.async.file_exists.on_call_with(ctx.location:bin "another-executable").returns(false) fs.async.file_exists - .on_call_with(path.concat { dummy:get_install_path(), "nested", "path", "my-executable" }) + .on_call_with(path.concat { ctx:get_install_path(), "nested", "path", "my-executable" }) .returns(true) - fs.async.file_exists.on_call_with(path.concat { dummy:get_install_path(), "another-executable" }).returns(true) + fs.async.file_exists.on_call_with(path.concat { ctx:get_install_path(), "another-executable" }).returns(true) ctx:link_bin("my-executable", path.concat { "nested", "path", "my-executable" }) ctx:link_bin("another-executable", "another-executable") @@ -126,9 +126,9 @@ describe("linker", function() fs.async.dir_exists.on_call_with(ctx.location:share "nested/path").returns(false) -- mock existent source files - fs.async.file_exists.on_call_with(path.concat { dummy:get_install_path(), "share-file" }).returns(true) + fs.async.file_exists.on_call_with(path.concat { ctx:get_install_path(), "share-file" }).returns(true) fs.async.file_exists - .on_call_with(path.concat { dummy:get_install_path(), "nested", "path", "to", "share-file" }) + .on_call_with(path.concat { ctx:get_install_path(), "nested", "path", "to", "share-file" }) .returns(true) ctx.links.share["nested/path/share-file"] = path.concat { "nested", "path", "to", "share-file" } @@ -171,9 +171,9 @@ describe("linker", function() fs.async.dir_exists.on_call_with(ctx.location:share "nested/path").returns(false) -- mock existent source files - fs.async.file_exists.on_call_with(path.concat { dummy:get_install_path(), "share-file" }).returns(true) + fs.async.file_exists.on_call_with(path.concat { ctx:get_install_path(), "share-file" }).returns(true) fs.async.file_exists - .on_call_with(path.concat { dummy:get_install_path(), "nested", "path", "to", "share-file" }) + .on_call_with(path.concat { ctx:get_install_path(), "nested", "path", "to", "share-file" }) .returns(true) ctx.links.share["nested/path/share-file"] = path.concat { "nested", "path", "to", "share-file" } @@ -186,9 +186,9 @@ describe("linker", function() assert.spy(fs.async.copy_file).was_called(2) assert .spy(fs.async.copy_file) - .was_called_with(path.concat { dummy:get_install_path(), "share-file" }, ctx.location:share "share-file", { excl = true }) + .was_called_with(path.concat { ctx:get_install_path(), "share-file" }, ctx.location:share "share-file", { excl = true }) assert.spy(fs.async.copy_file).was_called_with( - path.concat { dummy:get_install_path(), "nested", "path", "to", "share-file" }, + path.concat { ctx:get_install_path(), "nested", "path", "to", "share-file" }, ctx.location:share "nested/path/share-file", { excl = true } ) diff --git a/tests/mason-core/package/package_spec.lua b/tests/mason-core/package/package_spec.lua index b9b15d04..5f69ea4e 100644 --- a/tests/mason-core/package/package_spec.lua +++ b/tests/mason-core/package/package_spec.lua @@ -2,26 +2,27 @@ local Pkg = require "mason-core.package" local a = require "mason-core.async" local match = require "luassert.match" local mock = require "luassert.mock" +local receipt = require "mason-core.receipt" local registry = require "mason-registry" local spy = require "luassert.spy" local stub = require "luassert.stub" +local test_helpers = require "mason-test.helpers" -describe("package", function() +describe("Package ::", function() local snapshot before_each(function() snapshot = assert.snapshot() + local dummy = registry.get_package "dummy" + if dummy:is_installed() then + test_helpers.sync_uninstall(dummy) + end end) after_each(function() snapshot:revert() end) - before_each(function() - registry.get_package("dummy"):uninstall() - package.loaded["dummy_package"] = nil - end) - it("should parse package specifiers", function() local function parse(str) local name, version = Pkg.Parse(str) @@ -91,28 +92,27 @@ describe("package", function() it("should create new handle", function() local dummy = registry.get_package "dummy" - -- yo dawg - local handle_handler = spy.new() - dummy:once("handle", handle_handler) - local handle = dummy:new_handle() - assert.spy(handle_handler).was_called(1) - assert.spy(handle_handler).was_called_with(match.ref(handle)) + local callback = spy.new() + dummy:once("install:handle", callback) + local handle = dummy:new_install_handle() + assert.spy(callback).was_called(1) + assert.spy(callback).was_called_with(match.ref(handle)) handle:close() end) it("should not create new handle if one already exists", function() local dummy = registry.get_package "dummy" - dummy.handle = mock.new { + dummy.install_handle = mock.new { is_closed = mockx.returns(false), } local handle_handler = spy.new() - dummy:once("handle", handle_handler) + dummy:once("install:handle", handle_handler) local err = assert.has_error(function() - dummy:new_handle() + dummy:new_install_handle() end) - assert.equals("Cannot create new handle because existing handle is not closed.", err) + assert.equals("Cannot create new install handle because existing handle is not closed.", err) assert.spy(handle_handler).was_called(0) - dummy.handle = nil + dummy.install_handle = nil end) it("should successfully install package", function() @@ -135,9 +135,11 @@ describe("package", function() assert.wait(function() assert.spy(install_success_handler).was_called(1) - assert.spy(install_success_handler).was_called_with(match.is_ref(handle)) + assert.spy(install_success_handler).was_called_with(match.instanceof(receipt.InstallReceipt)) assert.spy(package_install_success_handler).was_called(1) - assert.spy(package_install_success_handler).was_called_with(match.is_ref(dummy), match.is_ref(handle)) + assert + .spy(package_install_success_handler) + .was_called_with(match.is_ref(dummy), match.instanceof(receipt.InstallReceipt)) assert.spy(package_install_failed_handler).was_called(0) assert.spy(install_failed_handler).was_called(0) end) @@ -166,11 +168,11 @@ describe("package", function() assert.wait(function() assert.spy(install_failed_handler).was_called(1) - assert.spy(install_failed_handler).was_called_with(match.is_ref(handle), "I simply refuse to be installed.") + assert.spy(install_failed_handler).was_called_with "I simply refuse to be installed." assert.spy(package_install_failed_handler).was_called(1) assert .spy(package_install_failed_handler) - .was_called_with(match.is_ref(dummy), match.is_ref(handle), "I simply refuse to be installed.") + .was_called_with(match.is_ref(dummy), "I simply refuse to be installed.") assert.spy(package_install_success_handler).was_called(0) assert.spy(install_success_handler).was_called(0) end) @@ -200,7 +202,7 @@ describe("package", function() local dummy = registry.get_package "registry" -- Move outside the main loop - a.run_blocking(function () + a.run_blocking(function() a.wait(function(resolve) local timer = vim.loop.new_timer() timer:start(0, 0, function() diff --git a/tests/mason-core/receipt_spec.lua b/tests/mason-core/receipt_spec.lua index e7fcd648..5cb01d5b 100644 --- a/tests/mason-core/receipt_spec.lua +++ b/tests/mason-core/receipt_spec.lua @@ -45,14 +45,20 @@ describe("receipt ::", function() assert.is_true(receipt:is_schema_min "1.1") end) - it("should parse 1.2 structures", function() - local receipt = InstallReceipt:new(fixture "1.2.json") + it("should parse 2.0 structures", function() + local receipt = InstallReceipt:new(fixture "2.0.json") assert.equals("angular-language-server", receipt:get_name()) - assert.equals("1.2", receipt:get_schema_version()) + assert.equals("2.0", receipt:get_schema_version()) assert.same({ type = "registry+v1", - id = "pkg:npm/%40angular/language-server@16.1.8", + id = "pkg:npm/%40angular/language-server@19.1.0", + raw = { + id = "pkg:npm/%40angular/language-server@19.1.0", + extra_packages = { + "typescript@5.4.5", + }, + }, }, receipt:get_source()) assert.same({ bin = { @@ -61,26 +67,26 @@ describe("receipt ::", function() opt = {}, share = {}, }, receipt:get_links()) - assert.is_true(receipt:is_schema_min "1.2") + assert.is_true(receipt:is_schema_min "2.0") end) describe("schema versions ::", function() it("should check minimum compatibility", function() local receipt_1_0 = InstallReceipt:new { schema_version = "1.0" } local receipt_1_1 = InstallReceipt:new { schema_version = "1.1" } - local receipt_1_2 = InstallReceipt:new { schema_version = "1.2" } + local receipt_2_0 = InstallReceipt:new { schema_version = "2.0" } assert.is_true(receipt_1_0:is_schema_min "1.0") assert.is_true(receipt_1_1:is_schema_min "1.0") - assert.is_true(receipt_1_2:is_schema_min "1.0") + assert.is_true(receipt_2_0:is_schema_min "1.0") assert.is_false(receipt_1_0:is_schema_min "1.1") assert.is_true(receipt_1_1:is_schema_min "1.1") - assert.is_true(receipt_1_2:is_schema_min "1.1") + assert.is_true(receipt_2_0:is_schema_min "1.1") assert.is_false(receipt_1_0:is_schema_min "1.2") assert.is_false(receipt_1_1:is_schema_min "1.2") - assert.is_true(receipt_1_2:is_schema_min "1.2") + assert.is_true(receipt_2_0:is_schema_min "2.0") end) end) end) diff --git a/tests/mason-core/result_spec.lua b/tests/mason-core/result_spec.lua index 227e53ae..017f8297 100644 --- a/tests/mason-core/result_spec.lua +++ b/tests/mason-core/result_spec.lua @@ -4,7 +4,7 @@ local a = require "mason-core.async" local match = require "luassert.match" local spy = require "luassert.spy" -describe("result", function() +describe("Result ::", function() it("should create success", function() local result = Result.success "Hello!" assert.is_true(result:is_success()) diff --git a/tests/mason-core/terminator_spec.lua b/tests/mason-core/terminator_spec.lua index 29a3a1dd..ce46f992 100644 --- a/tests/mason-core/terminator_spec.lua +++ b/tests/mason-core/terminator_spec.lua @@ -1,4 +1,4 @@ -local InstallHandle = require "mason-core.installer.handle" +local InstallHandle = require "mason-core.installer.InstallHandle" local _ = require "mason-core.functional" local a = require "mason-core.async" local match = require "luassert.match" @@ -7,114 +7,114 @@ local spy = require "luassert.spy" local stub = require "luassert.stub" local terminator = require "mason-core.terminator" -describe("terminator", function() - local snapshot - - before_each(function() - snapshot = assert.snapshot() - end) - - after_each(function() - -- wait for scheduled calls to expire - a.run_blocking(a.wait, vim.schedule) - snapshot:revert() - end) - - it("should terminate all active handles on nvim exit", function() - spy.on(InstallHandle, "terminate") - local dummy = registry.get_package "dummy" - local dummy2 = registry.get_package "dummy2" - for _, pkg in ipairs { dummy, dummy2 } do - stub(pkg.spec.source, "install", function() - a.sleep(10000) - end) - end - - local dummy_handle = dummy:install() - local dummy2_handle = dummy2:install() - - assert.wait(function() - assert.spy(dummy.spec.source.install).was_called() - assert.spy(dummy2.spec.source.install).was_called() - end) - - terminator.terminate(5000) - - assert.spy(InstallHandle.terminate).was_called(2) - assert.spy(InstallHandle.terminate).was_called_with(match.is_ref(dummy_handle)) - assert.spy(InstallHandle.terminate).was_called_with(match.is_ref(dummy2_handle)) - assert.wait(function() - assert.is_true(dummy_handle:is_closed()) - assert.is_true(dummy2_handle:is_closed()) - end) - end) - - it("should print warning messages", function() - spy.on(vim.api, "nvim_echo") - spy.on(vim.api, "nvim_err_writeln") - local dummy = registry.get_package "dummy" - local dummy2 = registry.get_package "dummy2" - for _, pkg in ipairs { dummy, dummy2 } do - stub(pkg.spec.source, "install", function() - a.sleep(10000) - end) - end - - local dummy_handle = dummy:install() - local dummy2_handle = dummy2:install() - - assert.wait(function() - assert.spy(dummy.spec.source.install).was_called() - assert.spy(dummy2.spec.source.install).was_called() - end) - - terminator.terminate(5000) - - assert.spy(vim.api.nvim_echo).was_called(1) - assert.spy(vim.api.nvim_echo).was_called_with({ - { - "[mason.nvim] Neovim is exiting while packages are still installing. Terminating all installations…", - "WarningMsg", - }, - }, true, {}) - - a.run_blocking(a.wait, vim.schedule) - - assert.spy(vim.api.nvim_err_writeln).was_called(1) - assert.spy(vim.api.nvim_err_writeln).was_called_with(_.dedent [[ - [mason.nvim] Neovim exited while the following packages were installing. Installation was aborted. - - dummy - - dummy2 - ]]) - assert.wait(function() - assert.is_true(dummy_handle:is_closed()) - assert.is_true(dummy2_handle:is_closed()) - end) - end) - - it("should send SIGTERM and then SIGKILL after grace period", function() - spy.on(InstallHandle, "kill") - local dummy = registry.get_package "dummy" - stub(dummy.spec.source, "install", function(ctx) - -- your signals have no power here - ctx.spawn.bash { "-c", "function noop { :; }; trap noop SIGTERM; sleep 999999;" } - end) - - local handle = dummy:install() - - assert.wait(function() - assert.spy(dummy.spec.source.install).was_called() - end) - terminator.terminate(50) - - assert.wait(function() - assert.spy(InstallHandle.kill).was_called(2) - assert.spy(InstallHandle.kill).was_called_with(match.is_ref(handle), 15) -- SIGTERM - assert.spy(InstallHandle.kill).was_called_with(match.is_ref(handle), 9) -- SIGKILL - end) - - assert.wait(function() - assert.is_true(handle:is_closed()) - end) - end) -end) +-- describe("terminator", function() +-- local snapshot +-- +-- before_each(function() +-- snapshot = assert.snapshot() +-- end) +-- +-- after_each(function() +-- -- wait for scheduled calls to expire +-- a.run_blocking(a.wait, vim.schedule) +-- snapshot:revert() +-- end) +-- +-- it("should terminate all active handles on nvim exit", function() +-- spy.on(InstallHandle, "terminate") +-- local dummy = registry.get_package "dummy" +-- local dummy2 = registry.get_package "dummy2" +-- for _, pkg in ipairs { dummy, dummy2 } do +-- stub(pkg.spec.source, "install", function() +-- a.sleep(10000) +-- end) +-- end +-- +-- local dummy_handle = dummy:install() +-- local dummy2_handle = dummy2:install() +-- +-- assert.wait(function() +-- assert.spy(dummy.spec.source.install).was_called() +-- assert.spy(dummy2.spec.source.install).was_called() +-- end) +-- +-- terminator.terminate(5000) +-- +-- assert.spy(InstallHandle.terminate).was_called(2) +-- assert.spy(InstallHandle.terminate).was_called_with(match.is_ref(dummy_handle)) +-- assert.spy(InstallHandle.terminate).was_called_with(match.is_ref(dummy2_handle)) +-- assert.wait(function() +-- assert.is_true(dummy_handle:is_closed()) +-- assert.is_true(dummy2_handle:is_closed()) +-- end) +-- end) +-- +-- it("should print warning messages", function() +-- spy.on(vim.api, "nvim_echo") +-- spy.on(vim.api, "nvim_err_writeln") +-- local dummy = registry.get_package "dummy" +-- local dummy2 = registry.get_package "dummy2" +-- for _, pkg in ipairs { dummy, dummy2 } do +-- stub(pkg.spec.source, "install", function() +-- a.sleep(10000) +-- end) +-- end +-- +-- local dummy_handle = dummy:install() +-- local dummy2_handle = dummy2:install() +-- +-- assert.wait(function() +-- assert.spy(dummy.spec.source.install).was_called() +-- assert.spy(dummy2.spec.source.install).was_called() +-- end) +-- +-- terminator.terminate(5000) +-- +-- assert.spy(vim.api.nvim_echo).was_called(1) +-- assert.spy(vim.api.nvim_echo).was_called_with({ +-- { +-- "[mason.nvim] Neovim is exiting while packages are still installing. Terminating all installations…", +-- "WarningMsg", +-- }, +-- }, true, {}) +-- +-- a.run_blocking(a.wait, vim.schedule) +-- +-- assert.spy(vim.api.nvim_err_writeln).was_called(1) +-- assert.spy(vim.api.nvim_err_writeln).was_called_with(_.dedent [[ +-- [mason.nvim] Neovim exited while the following packages were installing. Installation was aborted. +-- - dummy +-- - dummy2 +-- ]]) +-- assert.wait(function() +-- assert.is_true(dummy_handle:is_closed()) +-- assert.is_true(dummy2_handle:is_closed()) +-- end) +-- end) +-- +-- it("should send SIGTERM and then SIGKILL after grace period", function() +-- spy.on(InstallHandle, "kill") +-- local dummy = registry.get_package "dummy" +-- stub(dummy.spec.source, "install", function(ctx) +-- -- your signals have no power here +-- ctx.spawn.bash { "-c", "function noop { :; }; trap noop SIGTERM; sleep 999999;" } +-- end) +-- +-- local handle = dummy:install() +-- +-- assert.wait(function() +-- assert.spy(dummy.spec.source.install).was_called() +-- end) +-- terminator.terminate(50) +-- +-- assert.wait(function() +-- assert.spy(InstallHandle.kill).was_called(2) +-- assert.spy(InstallHandle.kill).was_called_with(match.is_ref(handle), 15) -- SIGTERM +-- assert.spy(InstallHandle.kill).was_called_with(match.is_ref(handle), 9) -- SIGKILL +-- end) +-- +-- assert.wait(function() +-- assert.is_true(handle:is_closed()) +-- end) +-- end) +-- end) diff --git a/tests/mason/api/command_spec.lua b/tests/mason/api/command_spec.lua index 6945340d..ba34cdf3 100644 --- a/tests/mason/api/command_spec.lua +++ b/tests/mason/api/command_spec.lua @@ -25,7 +25,9 @@ describe(":MasonInstall", function() spy.on(Pkg, "install") api.MasonInstall { "dummy@1.0.0", "dummy2" } assert.spy(Pkg.install).was_called(2) - assert.spy(Pkg.install).was_called_with(match.is_ref(dummy), { version = "1.0.0" }) + assert + .spy(Pkg.install) + .was_called_with(match.is_ref(dummy), { version = "1.0.0" }) assert.spy(Pkg.install).was_called_with(match.is_ref(dummy2), match.tbl_containing { version = match.is_nil() }) end) @@ -35,8 +37,12 @@ describe(":MasonInstall", function() spy.on(Pkg, "install") vim.cmd [[MasonInstall --debug dummy dummy2]] assert.spy(Pkg.install).was_called(2) - assert.spy(Pkg.install).was_called_with(match.is_ref(dummy), { version = nil, debug = true }) - assert.spy(Pkg.install).was_called_with(match.is_ref(dummy2), { version = nil, debug = true }) + assert + .spy(Pkg.install) + .was_called_with(match.is_ref(dummy), { version = nil, debug = true }) + assert + .spy(Pkg.install) + .was_called_with(match.is_ref(dummy2), { version = nil, debug = true }) end) it("should open the UI window", function() diff --git a/tests/mason/setup_spec.lua b/tests/mason/setup_spec.lua index 68871119..f3193a27 100644 --- a/tests/mason/setup_spec.lua +++ b/tests/mason/setup_spec.lua @@ -1,4 +1,4 @@ -local InstallLocation = require "mason-core.installer.location" +local InstallLocation = require "mason-core.installer.InstallLocation" local mason = require "mason" local match = require "luassert.match" local settings = require "mason.settings" |
