aboutsummaryrefslogtreecommitdiffstats
path: root/lua/mason-core/installer/context.lua
diff options
context:
space:
mode:
authorWilliam Boman <william@redwill.se>2023-10-11 16:31:50 +0200
committerWilliam Boman <william@redwill.se>2025-02-19 09:22:40 +0100
commit047ec18da56ad8f331e5c6bc7417dc5a9a6e71cc (patch)
treec50c22cd05d3605fc5a1e8eb902ffeb11e339697 /lua/mason-core/installer/context.lua
parentrefactor(receipt): change receipt structure and remove old builder APIs (#1521) (diff)
downloadmason-047ec18da56ad8f331e5c6bc7417dc5a9a6e71cc.tar
mason-047ec18da56ad8f331e5c6bc7417dc5a9a6e71cc.tar.gz
mason-047ec18da56ad8f331e5c6bc7417dc5a9a6e71cc.tar.bz2
mason-047ec18da56ad8f331e5c6bc7417dc5a9a6e71cc.tar.lz
mason-047ec18da56ad8f331e5c6bc7417dc5a9a6e71cc.tar.xz
mason-047ec18da56ad8f331e5c6bc7417dc5a9a6e71cc.tar.zst
mason-047ec18da56ad8f331e5c6bc7417dc5a9a6e71cc.zip
refactor!: refactor installer internals and add new Package class methods (#1523)
This contains the following changes: 1) `Package:install()` now accepts a second, optional, callback argument which is called when installation finishes (successfully or not). 2) Adds a `Package:is_installing()` method. This contains the following breaking changes: 1) `Package:install()` will now error when called while an installation is already ongoing. Use the new `Package:is_installing()` method to check whether an installation is already running. This also refactors large portions of the tests by removing test globals, removing async_test, and adding the `mason-test` Lua module instead. Test helpers via globals are problematic to work with due to not being detected through tools like the Lua language server without additional configuration. This has been replaced with a Lua module `mason-test`. `async_test` has also been removed in favour of explicitly making use of the `mason-core.async` API. These changes stands for a significant portion of the diff.
Diffstat (limited to 'lua/mason-core/installer/context.lua')
-rw-r--r--lua/mason-core/installer/context.lua399
1 files changed, 0 insertions, 399 deletions
diff --git a/lua/mason-core/installer/context.lua b/lua/mason-core/installer/context.lua
deleted file mode 100644
index a991cd9f..00000000
--- a/lua/mason-core/installer/context.lua
+++ /dev/null
@@ -1,399 +0,0 @@
-local Optional = require "mason-core.optional"
-local _ = require "mason-core.functional"
-local fs = require "mason-core.fs"
-local log = require "mason-core.log"
-local path = require "mason-core.path"
-local platform = require "mason-core.platform"
-local receipt = require "mason-core.receipt"
-local spawn = require "mason-core.spawn"
-
----@class ContextualSpawn
----@field strict_mode boolean Whether spawn failures should raise an exception rather then return a Result.
----@field cwd CwdManager
----@field handle InstallHandle
----@field [string] async fun(opts: SpawnArgs): Result
-local ContextualSpawn = {}
-
----@param cwd CwdManager
----@param handle InstallHandle
----@param strict_mode boolean
-function ContextualSpawn.new(cwd, handle, strict_mode)
- return setmetatable({ cwd = cwd, handle = handle, strict_mode = strict_mode }, ContextualSpawn)
-end
-
----@param cmd string
-function ContextualSpawn:__index(cmd)
- ---@param args JobSpawnOpts
- return function(args)
- args.cwd = args.cwd or self.cwd:get()
- args.stdio_sink = args.stdio_sink or self.handle.stdio.sink
- local on_spawn = args.on_spawn
- local captured_handle
- args.on_spawn = function(handle, stdio, pid, ...)
- captured_handle = handle
- self.handle:register_spawn_handle(handle, pid, cmd, spawn._flatten_cmd_args(args))
- if on_spawn then
- on_spawn(handle, stdio, pid, ...)
- end
- end
- local function pop_spawn_stack()
- if captured_handle then
- self.handle:deregister_spawn_handle(captured_handle)
- end
- end
- local result = spawn[cmd](args):on_success(pop_spawn_stack):on_failure(pop_spawn_stack)
- if self.strict_mode then
- return result:get_or_throw()
- else
- return result
- end
- end
-end
-
----@class ContextualFs
----@field private cwd CwdManager
-local ContextualFs = {}
-ContextualFs.__index = ContextualFs
-
----@param cwd CwdManager
-function ContextualFs.new(cwd)
- return setmetatable({ cwd = cwd }, ContextualFs)
-end
-
----@async
----@param rel_path string The relative path from the current working directory to the file to append.
----@param contents string
-function ContextualFs:append_file(rel_path, contents)
- return fs.async.append_file(path.concat { self.cwd:get(), rel_path }, contents)
-end
-
----@async
----@param rel_path string The relative path from the current working directory to the file to write.
----@param contents string
-function ContextualFs:write_file(rel_path, contents)
- return fs.async.write_file(path.concat { self.cwd:get(), rel_path }, contents)
-end
-
----@async
----@param rel_path string The relative path from the current working directory to the file to read.
-function ContextualFs:read_file(rel_path)
- return fs.async.read_file(path.concat { self.cwd:get(), rel_path })
-end
-
----@async
----@param rel_path string The relative path from the current working directory.
-function ContextualFs:file_exists(rel_path)
- return fs.async.file_exists(path.concat { self.cwd:get(), rel_path })
-end
-
----@async
----@param rel_path string The relative path from the current working directory.
-function ContextualFs:dir_exists(rel_path)
- return fs.async.dir_exists(path.concat { self.cwd:get(), rel_path })
-end
-
----@async
----@param rel_path string The relative path from the current working directory.
-function ContextualFs:rmrf(rel_path)
- return fs.async.rmrf(path.concat { self.cwd:get(), rel_path })
-end
-
----@async
----@param rel_path string The relative path from the current working directory.
-function ContextualFs:unlink(rel_path)
- return fs.async.unlink(path.concat { self.cwd:get(), rel_path })
-end
-
----@async
----@param old_path string
----@param new_path string
-function ContextualFs:rename(old_path, new_path)
- return fs.async.rename(path.concat { self.cwd:get(), old_path }, path.concat { self.cwd:get(), new_path })
-end
-
----@async
----@param dir_path string
-function ContextualFs:mkdir(dir_path)
- return fs.async.mkdir(path.concat { self.cwd:get(), dir_path })
-end
-
----@async
----@param dir_path string
-function ContextualFs:mkdirp(dir_path)
- return fs.async.mkdirp(path.concat { self.cwd:get(), dir_path })
-end
-
----@async
----@param file_path string
-function ContextualFs:chmod_exec(file_path)
- local bit = require "bit"
- -- see chmod(2)
- local USR_EXEC = 0x40
- local GRP_EXEC = 0x8
- local ALL_EXEC = 0x1
- local EXEC = bit.bor(USR_EXEC, GRP_EXEC, ALL_EXEC)
- local fstat = self:fstat(file_path)
- if bit.band(fstat.mode, EXEC) ~= EXEC then
- local plus_exec = bit.bor(fstat.mode, EXEC)
- log.fmt_debug("Setting exec flags on file %s %o -> %o", file_path, fstat.mode, plus_exec)
- self:chmod(file_path, plus_exec) -- chmod +x
- end
-end
-
----@async
----@param file_path string
----@param mode integer
-function ContextualFs:chmod(file_path, mode)
- return fs.async.chmod(path.concat { self.cwd:get(), file_path }, mode)
-end
-
----@async
----@param file_path string
-function ContextualFs:fstat(file_path)
- return fs.async.fstat(path.concat { self.cwd:get(), file_path })
-end
-
----@class CwdManager
----@field private install_prefix string Defines the upper boundary for which paths are allowed as cwd.
----@field private cwd string
-local CwdManager = {}
-CwdManager.__index = CwdManager
-
-function CwdManager.new(install_prefix)
- assert(type(install_prefix) == "string", "install_prefix not provided")
- return setmetatable({
- install_prefix = install_prefix,
- cwd = nil,
- }, CwdManager)
-end
-
-function CwdManager:get()
- assert(self.cwd ~= nil, "Tried to access cwd before it was set.")
- return self.cwd
-end
-
----@param new_cwd string
-function CwdManager:set(new_cwd)
- assert(type(new_cwd) == "string", "new_cwd is not a string")
- assert(
- path.is_subdirectory(self.install_prefix, new_cwd),
- ("%q is not a subdirectory of %q"):format(new_cwd, self.install_prefix)
- )
- self.cwd = new_cwd
-end
-
----@class InstallContext
----@field public receipt InstallReceiptBuilder
----@field public requested_version Optional
----@field public fs ContextualFs
----@field public spawn ContextualSpawn
----@field public handle InstallHandle
----@field public package Package
----@field public cwd CwdManager
----@field public opts PackageInstallOpts
----@field public stdio_sink StdioSink
----@field links { bin: table<string, string>, share: table<string, string>, opt: table<string, string> }
-local InstallContext = {}
-InstallContext.__index = InstallContext
-
----@param handle InstallHandle
----@param opts PackageInstallOpts
-function InstallContext.new(handle, opts)
- local cwd_manager = CwdManager.new(path.install_prefix())
- return setmetatable({
- cwd = cwd_manager,
- spawn = ContextualSpawn.new(cwd_manager, handle, false),
- handle = handle,
- package = handle.package, -- for convenience
- fs = ContextualFs.new(cwd_manager),
- receipt = receipt.InstallReceiptBuilder.new(),
- requested_version = Optional.of_nilable(opts.version),
- stdio_sink = handle.stdio.sink,
- links = {
- bin = {},
- share = {},
- opt = {},
- },
- opts = opts,
- }, InstallContext)
-end
-
----@async
-function InstallContext:promote_cwd()
- local cwd = self.cwd:get()
- local install_path = self.package:get_install_path()
- if install_path == cwd then
- log.fmt_debug("cwd %s is already promoted (at %s)", cwd, install_path)
- return
- end
- log.fmt_debug("Promoting cwd %s to %s", cwd, install_path)
- -- 1. Unlink any existing installation
- self.handle.package:unlink()
- -- 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
- self.cwd:set(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).
----@param fn async (fun(): any)? The function to run in the context of the given path.
-function InstallContext:chdir(rel_path, fn)
- local old_cwd = self.cwd:get()
- self.cwd:set(path.concat { old_cwd, rel_path })
- if fn then
- local ok, result = pcall(fn)
- self.cwd:set(old_cwd)
- if not ok then
- error(result, 0)
- end
- return result
- end
-end
-
----@param new_executable_rel_path string Relative path to the executable file to create.
----@param script_rel_path string Relative path to the Node.js script.
-function InstallContext:write_node_exec_wrapper(new_executable_rel_path, script_rel_path)
- if not self.fs:file_exists(script_rel_path) then
- error(("Cannot write Node exec wrapper for path %q as it doesn't exist."):format(script_rel_path), 0)
- end
- return self:write_shell_exec_wrapper(
- new_executable_rel_path,
- ("node %q"):format(path.concat {
- self.package:get_install_path(),
- script_rel_path,
- })
- )
-end
-
----@param new_executable_rel_path string Relative path to the executable file to create.
----@param script_rel_path string Relative path to the Node.js script.
-function InstallContext:write_ruby_exec_wrapper(new_executable_rel_path, script_rel_path)
- if not self.fs:file_exists(script_rel_path) then
- error(("Cannot write Ruby exec wrapper for path %q as it doesn't exist."):format(script_rel_path), 0)
- end
- return self:write_shell_exec_wrapper(
- new_executable_rel_path,
- ("ruby %q"):format(path.concat {
- self.package:get_install_path(),
- script_rel_path,
- })
- )
-end
-
----@param new_executable_rel_path string Relative path to the executable file to create.
----@param script_rel_path string Relative path to the PHP script.
-function InstallContext:write_php_exec_wrapper(new_executable_rel_path, script_rel_path)
- if not self.fs:file_exists(script_rel_path) then
- error(("Cannot write PHP exec wrapper for path %q as it doesn't exist."):format(script_rel_path), 0)
- end
- return self:write_shell_exec_wrapper(
- new_executable_rel_path,
- ("php %q"):format(path.concat {
- self.package:get_install_path(),
- script_rel_path,
- })
- )
-end
-
----@param new_executable_rel_path string Relative path to the executable file to create.
----@param module string The python module to call.
-function InstallContext:write_pyvenv_exec_wrapper(new_executable_rel_path, module)
- local pypi = require "mason-core.installer.managers.pypi"
- local module_exists, module_err = pcall(function()
- local result =
- self.spawn.python { "-c", ("import %s"):format(module), with_paths = { pypi.venv_path(self.cwd:get()) } }
- if not self.spawn.strict_mode then
- result:get_or_throw()
- end
- end)
- if not module_exists then
- log.fmt_error("Failed to find module %q for package %q. %s", module, self.package, module_err)
- error(("Cannot write Python exec wrapper for module %q as it doesn't exist."):format(module), 0)
- end
- return self:write_shell_exec_wrapper(
- new_executable_rel_path,
- ("%q -m %s"):format(
- path.concat {
- pypi.venv_path(self.package:get_install_path()),
- "python",
- },
- module
- )
- )
-end
-
----@param new_executable_rel_path string Relative path to the executable file to create.
----@param target_executable_rel_path string
-function InstallContext:write_exec_wrapper(new_executable_rel_path, target_executable_rel_path)
- if not self.fs:file_exists(target_executable_rel_path) then
- error(("Cannot write exec wrapper for path %q as it doesn't exist."):format(target_executable_rel_path), 0)
- end
- if platform.is.unix then
- self.fs:chmod_exec(target_executable_rel_path)
- end
- return self:write_shell_exec_wrapper(
- new_executable_rel_path,
- ("%q"):format(path.concat {
- self.package:get_install_path(),
- target_executable_rel_path,
- })
- )
-end
-
-local BASH_TEMPLATE = _.dedent [[
-#!/usr/bin/env bash
-%s
-exec %s "$@"
-]]
-
-local BATCH_TEMPLATE = _.dedent [[
-@ECHO off
-%s
-%s %%*
-]]
-
----@param new_executable_rel_path string Relative path to the executable file to create.
----@param command string The shell command to run.
----@param env table<string, string>?
----@return string # The created executable filename.
-function InstallContext:write_shell_exec_wrapper(new_executable_rel_path, command, env)
- if self.fs:file_exists(new_executable_rel_path) or self.fs:dir_exists(new_executable_rel_path) then
- error(("Cannot write exec wrapper to %q because the file already exists."):format(new_executable_rel_path), 0)
- end
- return platform.when {
- unix = function()
- local formatted_envs = _.map(function(pair)
- local var, value = pair[1], pair[2]
- return ("export %s=%q"):format(var, value)
- end, _.to_pairs(env or {}))
-
- self.fs:write_file(new_executable_rel_path, BASH_TEMPLATE:format(_.join("\n", formatted_envs), command))
- self.fs:chmod_exec(new_executable_rel_path)
- return new_executable_rel_path
- end,
- win = function()
- local executable_file = ("%s.cmd"):format(new_executable_rel_path)
- local formatted_envs = _.map(function(pair)
- local var, value = pair[1], pair[2]
- return ("SET %s=%s"):format(var, value)
- end, _.to_pairs(env or {}))
-
- self.fs:write_file(executable_file, BATCH_TEMPLATE:format(_.join("\n", formatted_envs), command))
- return executable_file
- end,
- }
-end
-
----@param executable string
----@param rel_path string
-function InstallContext:link_bin(executable, rel_path)
- self.links.bin[executable] = rel_path
- return self
-end
-
-return InstallContext