diff options
Diffstat (limited to 'lua/mason-core/installer/context')
| -rw-r--r-- | lua/mason-core/installer/context/cwd.lua | 48 | ||||
| -rw-r--r-- | lua/mason-core/installer/context/fs.lua | 108 | ||||
| -rw-r--r-- | lua/mason-core/installer/context/init.lua | 261 | ||||
| -rw-r--r-- | lua/mason-core/installer/context/spawn.lua | 46 |
4 files changed, 463 insertions, 0 deletions
diff --git a/lua/mason-core/installer/context/cwd.lua b/lua/mason-core/installer/context/cwd.lua new file mode 100644 index 00000000..4f645fbb --- /dev/null +++ b/lua/mason-core/installer/context/cwd.lua @@ -0,0 +1,48 @@ +local Result = require "mason-core.result" +local fs = require "mason-core.fs" +local path = require "mason-core.path" + +---@class InstallContextCwd +---@field private location InstallLocation Defines the upper boundary for which paths are allowed as cwd. +---@field private cwd string? +local InstallContextCwd = {} +InstallContextCwd.__index = InstallContextCwd + +---@param location InstallLocation +function InstallContextCwd.new(location) + assert(location, "location not provided") + return setmetatable({ + location = location, + cwd = nil, + }, InstallContextCwd) +end + +---@param handle InstallHandle +function InstallContextCwd:initialize(handle) + return Result.try(function(try) + local staging_dir = self.location:staging(handle.package.name) + if fs.async.dir_exists(staging_dir) then + try(Result.pcall(fs.async.rmrf, staging_dir)) + end + try(Result.pcall(fs.async.mkdirp, staging_dir)) + self:set(staging_dir) + end) +end + +function InstallContextCwd:get() + assert(self.cwd ~= nil, "Tried to access cwd before it was set.") + return self.cwd +end + +---@param new_abs_cwd string +function InstallContextCwd:set(new_abs_cwd) + assert(type(new_abs_cwd) == "string", "new_cwd is not a string") + assert( + path.is_subdirectory(self.location:get_dir(), new_abs_cwd), + ("%q is not a subdirectory of %q"):format(new_abs_cwd, self.location) + ) + self.cwd = new_abs_cwd + return self +end + +return InstallContextCwd diff --git a/lua/mason-core/installer/context/fs.lua b/lua/mason-core/installer/context/fs.lua new file mode 100644 index 00000000..5c51fb56 --- /dev/null +++ b/lua/mason-core/installer/context/fs.lua @@ -0,0 +1,108 @@ +local fs = require "mason-core.fs" +local log = require "mason-core.log" +local path = require "mason-core.path" + +---@class InstallContextFs +---@field private cwd InstallContextCwd +local InstallContextFs = {} +InstallContextFs.__index = InstallContextFs + +---@param cwd InstallContextCwd +function InstallContextFs.new(cwd) + return setmetatable({ cwd = cwd }, InstallContextFs) +end + +---@async +---@param rel_path string The relative path from the current working directory to the file to append. +---@param contents string +function InstallContextFs:append_file(rel_path, contents) + return fs.async.append_file(path.concat { self.cwd:get(), rel_path }, contents) +end + +---@async +---@param rel_path string The relative path from the current working directory to the file to write. +---@param contents string +function InstallContextFs:write_file(rel_path, contents) + return fs.async.write_file(path.concat { self.cwd:get(), rel_path }, contents) +end + +---@async +---@param rel_path string The relative path from the current working directory to the file to read. +function InstallContextFs:read_file(rel_path) + return fs.async.read_file(path.concat { self.cwd:get(), rel_path }) +end + +---@async +---@param rel_path string The relative path from the current working directory. +function InstallContextFs:file_exists(rel_path) + return fs.async.file_exists(path.concat { self.cwd:get(), rel_path }) +end + +---@async +---@param rel_path string The relative path from the current working directory. +function InstallContextFs:dir_exists(rel_path) + return fs.async.dir_exists(path.concat { self.cwd:get(), rel_path }) +end + +---@async +---@param rel_path string The relative path from the current working directory. +function InstallContextFs:rmrf(rel_path) + return fs.async.rmrf(path.concat { self.cwd:get(), rel_path }) +end + +---@async +---@param rel_path string The relative path from the current working directory. +function InstallContextFs:unlink(rel_path) + return fs.async.unlink(path.concat { self.cwd:get(), rel_path }) +end + +---@async +---@param old_path string +---@param new_path string +function InstallContextFs:rename(old_path, new_path) + return fs.async.rename(path.concat { self.cwd:get(), old_path }, path.concat { self.cwd:get(), new_path }) +end + +---@async +---@param dir_path string +function InstallContextFs:mkdir(dir_path) + return fs.async.mkdir(path.concat { self.cwd:get(), dir_path }) +end + +---@async +---@param dir_path string +function InstallContextFs:mkdirp(dir_path) + return fs.async.mkdirp(path.concat { self.cwd:get(), dir_path }) +end + +---@async +---@param file_path string +function InstallContextFs:chmod_exec(file_path) + local bit = require "bit" + -- see chmod(2) + local USR_EXEC = 0x40 + local GRP_EXEC = 0x8 + local ALL_EXEC = 0x1 + local EXEC = bit.bor(USR_EXEC, GRP_EXEC, ALL_EXEC) + local fstat = self:fstat(file_path) + if bit.band(fstat.mode, EXEC) ~= EXEC then + local plus_exec = bit.bor(fstat.mode, EXEC) + log.fmt_debug("Setting exec flags on file %s %o -> %o", file_path, fstat.mode, plus_exec) + self:chmod(file_path, plus_exec) -- chmod +x + end +end + +---@async +---@param file_path string +---@param mode integer +function InstallContextFs:chmod(file_path, mode) + return fs.async.chmod(path.concat { self.cwd:get(), file_path }, mode) +end + +---@async +---@param file_path string +function InstallContextFs:fstat(file_path) + return fs.async.fstat(path.concat { self.cwd:get(), file_path }) +end + +return InstallContextFs diff --git a/lua/mason-core/installer/context/init.lua b/lua/mason-core/installer/context/init.lua new file mode 100644 index 00000000..0d178c4e --- /dev/null +++ b/lua/mason-core/installer/context/init.lua @@ -0,0 +1,261 @@ +local Result = require "mason-core.result" +local _ = require "mason-core.functional" +local fs = require "mason-core.fs" +local log = require "mason-core.log" +local path = require "mason-core.path" +local platform = require "mason-core.platform" +local receipt = require "mason-core.receipt" + +---@class InstallContext +---@field receipt InstallReceiptBuilder +---@field fs InstallContextFs +---@field spawn InstallContextSpawn +---@field handle InstallHandle +---@field package Package +---@field cwd InstallContextCwd +---@field opts PackageInstallOpts +---@field stdio_sink StdioSink +---@field links { bin: table<string, string>, share: table<string, string>, opt: table<string, string> } +local InstallContext = {} +InstallContext.__index = InstallContext + +---@param handle InstallHandle +---@param cwd InstallContextCwd +---@param spawn InstallContextSpawn +---@param fs InstallContextFs +---@param opts PackageInstallOpts +function InstallContext.new(handle, cwd, spawn, fs, opts) + return setmetatable({ + cwd = cwd, + spawn = spawn, + handle = handle, + package = handle.package, -- for convenience + fs = fs, + receipt = receipt.InstallReceiptBuilder.new(), + 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. Uninstall any existing installation + self.handle.package:uninstall() + -- 2. Prepare for renaming cwd to destination + if platform.is.unix then + -- Some Unix systems will raise an error when renaming a directory to a destination that does not already exist. + 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 + +InstallContext.CONTEXT_REQUEST = {} + +---@generic T +---@param fn fun(context: InstallContext): T +---@return T +function InstallContext:execute(fn) + local thread = coroutine.create(function(...) + -- We wrap the function to allow it to be a spy instance (in which case it's not actually a function, but a + -- callable metatable - coroutine.create strictly expects functions only) + return fn(...) + end) + local step + local ret_val + step = function(...) + local ok, result = coroutine.resume(thread, ...) + if not ok then + error(result, 0) + elseif result == InstallContext.CONTEXT_REQUEST then + step(self) + elseif coroutine.status(thread) == "suspended" then + -- yield to parent coroutine + step(coroutine.yield(result)) + else + ret_val = result + end + end + step(self) + return ret_val +end + +---@async +function InstallContext:build_receipt() + log.fmt_debug("Building receipt for %s", self.package) + return Result.pcall(function() + return self.receipt:with_name(self.package.name):with_completion_time(vim.loop.gettimeofday()):build() + end) +end + +return InstallContext diff --git a/lua/mason-core/installer/context/spawn.lua b/lua/mason-core/installer/context/spawn.lua new file mode 100644 index 00000000..6528c4b3 --- /dev/null +++ b/lua/mason-core/installer/context/spawn.lua @@ -0,0 +1,46 @@ +local spawn = require "mason-core.spawn" + +---@class InstallContextSpawn +---@field strict_mode boolean Whether spawn failures should raise an exception rather then return a Result. +---@field private cwd InstallContextCwd +---@field private handle InstallHandle +---@field [string] async fun(opts: SpawnArgs): Result +local InstallContextSpawn = {} + +---@param cwd InstallContextCwd +---@param handle InstallHandle +---@param strict_mode boolean +function InstallContextSpawn.new(cwd, handle, strict_mode) + return setmetatable({ cwd = cwd, handle = handle, strict_mode = strict_mode }, InstallContextSpawn) +end + +---@param cmd string +function InstallContextSpawn:__index(cmd) + ---@param args JobSpawnOpts + return function(args) + args.cwd = args.cwd or self.cwd:get() + args.stdio_sink = args.stdio_sink or self.handle.stdio.sink + local on_spawn = args.on_spawn + local captured_handle + args.on_spawn = function(handle, stdio, pid, ...) + captured_handle = handle + self.handle:register_spawn_handle(handle, pid, cmd, spawn._flatten_cmd_args(args)) + if on_spawn then + on_spawn(handle, stdio, pid, ...) + end + end + local function pop_spawn_stack() + if captured_handle then + self.handle:deregister_spawn_handle(captured_handle) + end + end + local result = spawn[cmd](args):on_success(pop_spawn_stack):on_failure(pop_spawn_stack) + if self.strict_mode then + return result:get_or_throw() + else + return result + end + end +end + +return InstallContextSpawn |
