diff options
Diffstat (limited to 'lua')
37 files changed, 646 insertions, 223 deletions
diff --git a/lua/mason-core/async/init.lua b/lua/mason-core/async/init.lua index 03963264..e3a2e850 100644 --- a/lua/mason-core/async/init.lua +++ b/lua/mason-core/async/init.lua @@ -78,7 +78,8 @@ local function new_execution_context(suspend_fn, callback, ...) if cancelled or not thread then return end - local ok, promise_or_result = co.resume(thread, ...) + local results = { co.resume(thread, ...) } + local ok, promise_or_result = results[1], results[2] if cancelled or not thread then return end @@ -88,7 +89,7 @@ local function new_execution_context(suspend_fn, callback, ...) promise_or_result(step) else -- yield to parent coroutine - step(coroutine.yield(promise_or_result)) + step(coroutine.yield(promise_or_result, unpack(results, 3))) end else callback(true, promise_or_result) diff --git a/lua/mason-core/fs.lua b/lua/mason-core/fs.lua index e7f8343f..2f620a49 100644 --- a/lua/mason-core/fs.lua +++ b/lua/mason-core/fs.lua @@ -1,5 +1,5 @@ local Path = require "mason-core.path" -local a = require "mason-core.async" +local _ = require "mason-core.functional" local log = require "mason-core.log" local settings = require "mason.settings" @@ -7,32 +7,66 @@ local function make_module(uv) local M = {} ---@param path string - function M.fstat(path) - log.trace("fs: fstat", path) - local fd = uv.fs_open(path, "r", 438) - local fstat = uv.fs_fstat(fd) - uv.fs_close(fd) - return fstat + function M.stat(path) + log.trace("fs: stat", path) + return assert(uv.fs_stat(path)) end ---@param path string function M.file_exists(path) log.trace("fs: file_exists", path) - local ok, fstat = pcall(M.fstat, path) + local ok, stat = pcall(M.stat, path) if not ok then return false end - return fstat.type == "file" + return stat.type == "file" end ---@param path string function M.dir_exists(path) log.trace("fs: dir_exists", path) - local ok, fstat = pcall(M.fstat, path) + local ok, stat = pcall(M.stat, path) if not ok then return false end - return fstat.type == "directory" + return stat.type == "directory" + end + + ---@param path string + ---@param fn fun(abs_path: string, entry: string, type: "directory" | "file") + function M.ls(path, fn) + local handle = vim.uv.fs_scandir(path) + while handle do + local entry, t = vim.uv.fs_scandir_next(handle) + if not entry then + break + end + + ---@type string + local abs_path + if vim.fn.has "win32" == 1 and path:sub(1, 4) == [[\\?\]] then + -- Extended-length paths are used, we cannot use vim.fs.joinpath. + abs_path = path .. "\\" .. entry + else + abs_path = vim.fs.joinpath(path, entry) + end + t = t or vim.uv.fs_stat(abs_path).type + + if fn(abs_path, entry, t) == false then + break + end + end + end + + ---@param path string + ---@param fn fun(abs_path: string, entry: string, type: "directory" | "file") + function M.walk(path, fn) + M.ls(path, function(abs_path, entry, type) + if type == "directory" then + M.walk(abs_path, fn) + end + fn(abs_path, entry, type) + end) end ---@param path string @@ -45,13 +79,30 @@ local function make_module(uv) ) ) log.debug("fs: rmrf", path) - if vim.in_fast_event() then - a.scheduler() - end - if vim.fn.delete(path, "rf") ~= 0 then - log.debug "fs: rmrf failed" - error(("rmrf: Could not remove directory %q."):format(path)) + if vim.fn.has "win32" == 1 then + -- Use extended-length path (ELP) on Windows. We have no easy way to check if the current system has + -- LongPathsEnabled, so we enforce extended-length paths always. + -- + -- This is currently only done in this function (rmrf) because we walk the entire file tree under `path`, + -- which may result in deeply nested file paths that exceed MAX_PATH (260 characters). Other fs operations + -- don't reach so deeply into the file tree and pose minimal risk of exceeding the MAX_PATH. + -- NOTE: When using the ELP prefix Windows doesn't normalize file paths, meaning path separators (\) need to + -- be correct. + -- + -- See https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation + local extended_length_prefix = [[\\?\]] + path = extended_length_prefix .. path:gsub("/", "\\") end + M.walk(path, function(abs_path, _, type) + if type == "directory" then + log.trace("fs: rmdir", abs_path) + vim.uv.fs_rmdir(abs_path) + else + log.trace("fs: unlink", abs_path) + vim.uv.fs_unlink(abs_path) + end + end) + M.rmdir(path) end ---@param path string @@ -69,12 +120,16 @@ local function make_module(uv) ---@param path string function M.mkdirp(path) log.debug("fs: mkdirp", path) - if vim.in_fast_event() then - a.scheduler() + local normalized_path = vim.fs.normalize(path) + local path_components = vim.split(normalized_path, "/", { plain = true }) + if vim.fn.has "win32" ~= 1 then + path_components[1] = "/" end - if vim.fn.mkdir(path, "p") ~= 1 then - log.debug "fs: mkdirp failed" - error(("mkdirp: Could not create directory %q."):format(path)) + for i = 1, #path_components, 1 do + local current_path = vim.fs.joinpath(unpack(_.take(i, path_components))) + if not M.dir_exists(current_path) then + M.mkdir(current_path) + end end end @@ -130,33 +185,13 @@ local function make_module(uv) ---@param path string: The full path to the directory to read. ---@return ReaddirEntry[] function M.readdir(path) - log.trace("fs: fs_opendir", path) - local dir = assert(vim.loop.fs_opendir(path, nil, 25)) local all_entries = {} - local exhausted = false - - repeat - local entries = uv.fs_readdir(dir) - log.trace("fs: fs_readdir", path, entries) - if entries and #entries > 0 then - for i = 1, #entries do - if entries[i].name and not entries[i].type then - -- See https://github.com/luvit/luv/issues/660 - local full_path = Path.concat { path, entries[i].name } - log.trace("fs: fs_readdir falling back to fs_stat to find type", full_path) - local stat = uv.fs_stat(full_path) - entries[i].type = stat.type - end - all_entries[#all_entries + 1] = entries[i] - end - else - log.trace("fs: fs_readdir exhausted scan", path) - exhausted = true - end - until exhausted - - uv.fs_closedir(dir) - + M.ls(path, function(_, entry, type) + all_entries[#all_entries + 1] = { + name = entry, + type = type, + } + end) return all_entries end diff --git a/lua/mason-core/installer/InstallHandle.lua b/lua/mason-core/installer/InstallHandle.lua index 3846659e..6492acd9 100644 --- a/lua/mason-core/installer/InstallHandle.lua +++ b/lua/mason-core/installer/InstallHandle.lua @@ -20,6 +20,7 @@ local uv = vim.loop ---@field pid integer ---@field cmd string ---@field args string[] +---@field firewall boolean local InstallHandleSpawnHandle = {} InstallHandleSpawnHandle.__index = InstallHandleSpawnHandle @@ -27,7 +28,8 @@ InstallHandleSpawnHandle.__index = InstallHandleSpawnHandle ---@param pid integer ---@param cmd string ---@param args string[] -function InstallHandleSpawnHandle:new(luv_handle, pid, cmd, args) +---@param firewall boolean +function InstallHandleSpawnHandle:new(luv_handle, pid, cmd, args, firewall) ---@type InstallHandleSpawnHandle local instance = {} setmetatable(instance, InstallHandleSpawnHandle) @@ -35,6 +37,7 @@ function InstallHandleSpawnHandle:new(luv_handle, pid, cmd, args) instance.pid = pid instance.cmd = cmd instance.args = args + instance.firewall = firewall return instance end @@ -73,8 +76,9 @@ end ---@param pid integer ---@param cmd string ---@param args string[] -function InstallHandle:register_spawn_handle(luv_handle, pid, cmd, args) - local spawn_handles = InstallHandleSpawnHandle:new(luv_handle, pid, cmd, args) +---@param firewall boolean +function InstallHandle:register_spawn_handle(luv_handle, pid, cmd, args, firewall) + local spawn_handles = InstallHandleSpawnHandle:new(luv_handle, pid, cmd, args, firewall) log.fmt_trace("Pushing spawn_handles stack for %s: %s (pid: %s)", self, spawn_handles, pid) self.spawn_handles[#self.spawn_handles + 1] = spawn_handles self:emit "spawn_handles:change" diff --git a/lua/mason-core/installer/InstallLocation.lua b/lua/mason-core/installer/InstallLocation.lua index 77252761..157adc8c 100644 --- a/lua/mason-core/installer/InstallLocation.lua +++ b/lua/mason-core/installer/InstallLocation.lua @@ -34,6 +34,7 @@ function InstallLocation:initialize() self:bin(), self:share(), self:package(), + self:system_package(), self:staging(), } do if not fs.sync.dir_exists(p) then @@ -63,6 +64,11 @@ function InstallLocation:package(pkg) return Path.concat { self.dir, "packages", pkg } end +---@param pkg string? +function InstallLocation:system_package(pkg) + return Path.concat { self.dir, "system_packages", pkg } +end + ---@param path string? function InstallLocation:staging(path) return Path.concat { self.dir, "staging", path } @@ -78,11 +84,6 @@ 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/InstallRunner.lua b/lua/mason-core/installer/InstallRunner.lua index 93225e11..497ac644 100644 --- a/lua/mason-core/installer/InstallRunner.lua +++ b/lua/mason-core/installer/InstallRunner.lua @@ -39,7 +39,14 @@ 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, opts) + local context = InstallContext:new(handle, opts, { + suspend = function() + self:suspend() + end, + resume = function() + self:resume() + end, + }) local tailed_output = {} @@ -71,7 +78,7 @@ function InstallRunner:execute(opts, callback) if not opts.debug and not success then -- clean up installation dir pcall(function() - fs.async.rmrf(context.cwd:get()) + fs.sync.rmrf(context.cwd:get()) end) end @@ -135,7 +142,7 @@ function InstallRunner:execute(opts, callback) 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())) + try(Result.pcall(fs.sync.write_file, handle.package:get_receipt_path(handle.location), receipt:to_json())) return { receipt = receipt, } @@ -212,6 +219,20 @@ function InstallRunner:acquire_permit() return Result.success(channel) end +function InstallRunner:suspend() + if self.global_permit then + self.global_permit:forget() + self.global_permit = nil + end +end + +---@async +function InstallRunner:resume() + self.handle:set_state "QUEUED" + self.global_permit = self.global_semaphore:acquire() + self.handle:set_state "ACTIVE" +end + ---@private function InstallRunner:release_permit() if self.global_permit then diff --git a/lua/mason-core/installer/UninstallRunner.lua b/lua/mason-core/installer/UninstallRunner.lua index 67ae285f..3429fc93 100644 --- a/lua/mason-core/installer/UninstallRunner.lua +++ b/lua/mason-core/installer/UninstallRunner.lua @@ -36,7 +36,7 @@ function UninstallRunner:execute(opts, callback) local location = self.handle.location log.fmt_info("Executing uninstaller for %s %s", pkg, opts) a.run(function() - Result.try(function(try) + return Result.try(function(try) if not opts.bypass_permit then try(self:acquire_permit()):receive() end @@ -47,7 +47,7 @@ function UninstallRunner:execute(opts, callback) else try(pkg:unlink(location)) end - fs.sync.rmrf(location:package(pkg.name)) + fs.sync.rmrf(pkg:get_install_path(location)) return receipt end):get_or_throw() end, function(success, result) diff --git a/lua/mason-core/installer/compiler/compilers/npm.lua b/lua/mason-core/installer/compiler/compilers/npm.lua index e8489fe8..cdf6a279 100644 --- a/lua/mason-core/installer/compiler/compilers/npm.lua +++ b/lua/mason-core/installer/compiler/compilers/npm.lua @@ -1,6 +1,7 @@ local Result = require "mason-core.result" local _ = require "mason-core.functional" local providers = require "mason-core.providers" +local settings = require "mason.settings" ---@param purl Purl local function purl_to_npm(purl) @@ -24,6 +25,9 @@ function M.parse(source, purl) package = purl_to_npm(purl), version = purl.version, extra_packages = source.extra_packages, + npm = { + extra_args = settings.current.npm.install_args, + }, } return Result.success(parsed_source) @@ -39,6 +43,7 @@ function M.install(ctx, source) try(npm.init()) try(npm.install(source.package, source.version, { extra_packages = source.extra_packages, + install_extra_args = source.npm.extra_args, })) end) end diff --git a/lua/mason-core/installer/context/InstallContextFs.lua b/lua/mason-core/installer/context/InstallContextFs.lua index 93379017..fd875de0 100644 --- a/lua/mason-core/installer/context/InstallContextFs.lua +++ b/lua/mason-core/installer/context/InstallContextFs.lua @@ -51,7 +51,7 @@ 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 }) + return fs.sync.rmrf(path.concat { self.cwd:get(), rel_path }) end ---@async @@ -76,7 +76,7 @@ end ---@async ---@param dir_path string function InstallContextFs:mkdirp(dir_path) - return fs.async.mkdirp(path.concat { self.cwd:get(), dir_path }) + return fs.sync.mkdirp(path.concat { self.cwd:get(), dir_path }) end ---@async @@ -88,7 +88,7 @@ function InstallContextFs:chmod_exec(file_path) local GRP_EXEC = 0x8 local ALL_EXEC = 0x1 local EXEC = bit.bor(USR_EXEC, GRP_EXEC, ALL_EXEC) - local fstat = self:fstat(file_path) + local fstat = self:stat(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) @@ -100,13 +100,13 @@ end ---@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) + return fs.sync.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 }) +function InstallContextFs:stat(file_path) + return fs.sync.stat(path.concat { self.cwd:get(), file_path }) end return InstallContextFs diff --git a/lua/mason-core/installer/context/InstallContextSpawn.lua b/lua/mason-core/installer/context/InstallContextSpawn.lua index 29e62101..25419980 100644 --- a/lua/mason-core/installer/context/InstallContextSpawn.lua +++ b/lua/mason-core/installer/context/InstallContextSpawn.lua @@ -22,7 +22,7 @@ end ---@param cmd string function InstallContextSpawn:__index(cmd) - ---@param args JobSpawnOpts + ---@param args SpawnArgs return function(args) args.cwd = args.cwd or self.cwd:get() args.stdio_sink = args.stdio_sink or self.handle.stdio_sink @@ -30,7 +30,7 @@ function InstallContextSpawn:__index(cmd) 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)) + self.handle:register_spawn_handle(handle, pid, cmd, spawn._flatten_cmd_args(args), args.firewall == true) if on_spawn then on_spawn(handle, stdio, pid, ...) end diff --git a/lua/mason-core/installer/context/init.lua b/lua/mason-core/installer/context/init.lua index ae96f986..69738911 100644 --- a/lua/mason-core/installer/context/init.lua +++ b/lua/mason-core/installer/context/init.lua @@ -21,19 +21,22 @@ local receipt = require "mason-core.receipt" ---@field cwd InstallContextCwd ---@field opts PackageInstallOpts ---@field stdio_sink StdioSink +---@field runner { suspend: fun(), resume: async fun() } ---@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) +---@param runner { suspend: fun(), resume: async fun() } +function InstallContext:new(handle, opts, runner) local cwd = InstallContextCwd:new(handle) local spawn = InstallContextSpawn:new(handle, cwd, false) local fs = InstallContextFs:new(cwd) return setmetatable({ cwd = cwd, spawn = spawn, + runner = runner, handle = handle, location = handle.location, -- for convenience package = handle.package, -- for convenience @@ -264,6 +267,7 @@ function InstallContext:link_bin(executable, rel_path) end InstallContext.CONTEXT_REQUEST = {} +InstallContext.ABORT = {} ---@generic T ---@param fn fun(context: InstallContext): T @@ -277,14 +281,17 @@ function InstallContext:execute(fn) local step local ret_val step = function(...) - local ok, result = coroutine.resume(thread, ...) + local results = { coroutine.resume(thread, ...) } + local ok, result = results[1], results[2] if not ok then error(result, 0) elseif result == InstallContext.CONTEXT_REQUEST then step(self) + elseif result == InstallContext.ABORT then + ret_val = Result.failure(results[3]) elseif coroutine.status(thread) == "suspended" then -- yield to parent coroutine - step(coroutine.yield(result)) + step(coroutine.yield(result, unpack(results, 3))) else ret_val = result end @@ -294,6 +301,31 @@ function InstallContext:execute(fn) end ---@async +---@param system_pkg SystemPackage +function InstallContext:require(system_pkg) + local result = Result.try(function(try) + if try(system_pkg:needs_install()) then + self.stdio_sink:stdout("Installing dependency " .. system_pkg.name .. ".\n") + self.runner.suspend() + try(system_pkg:install():on_failure(function() + if self.opts.force then + self.runner.resume() + end + end)) + self.runner.resume() + end + end) + if result:is_failure() then + if not self.opts.force then + self.stdio_sink:stderr "Run with :MasonInstall --force to attempt installation anyway.\n" + coroutine.yield(InstallContext.ABORT, result:err_or_nil()) + else + self.stdio_sink:stderr(result:err_or_nil() .. "\n") + end + end +end + +---@async function InstallContext:build_receipt() log.fmt_debug("Building receipt for %s", self.package) return Result.pcall(function() @@ -302,7 +334,7 @@ function InstallContext:build_receipt() end function InstallContext:get_install_path() - return self.location:package(self.package.name) + return self.package:get_install_path(self.location) end return InstallContext diff --git a/lua/mason-core/installer/linker.lua b/lua/mason-core/installer/linker.lua index a26d2592..7fff6d89 100644 --- a/lua/mason-core/installer/linker.lua +++ b/lua/mason-core/installer/linker.lua @@ -1,17 +1,17 @@ +local Path = require "mason-core.path" local Result = require "mason-core.result" local _ = require "mason-core.functional" local a = require "mason-core.async" local fs = require "mason-core.fs" local log = require "mason-core.log" -local path = require "mason-core.path" local platform = require "mason-core.platform" local M = {} ---@alias LinkContext { type: '"bin"' | '"opt"' | '"share"', prefix: fun(path: string, location: InstallLocation): string } ----@type table<'"BIN"' | '"OPT"' | '"SHARE"', LinkContext> local LinkContext = { + ---@type LinkContext BIN = { type = "bin", ---@param path string @@ -20,6 +20,7 @@ local LinkContext = { return location:bin(path) end, }, + ---@type LinkContext OPT = { type = "opt", ---@param path string @@ -28,6 +29,7 @@ local LinkContext = { return location:opt(path) end, }, + ---@type LinkContext SHARE = { type = "share", ---@param path string @@ -38,6 +40,36 @@ local LinkContext = { }, } +local SystemLinkContext = { + ---@type LinkContext + BIN = { + type = "bin", + ---@param path string + ---@param location InstallLocation + prefix = function(path, location) + return location:opt(Path.concat { "mason", "system", "bin", path }) + end, + }, + ---@type LinkContext + OPT = { + type = "opt", + ---@param path string + ---@param location InstallLocation + prefix = function(path, location) + return location:opt(Path.concat { "mason", "system", "opt", path }) + end, + }, + ---@type LinkContext + SHARE = { + type = "share", + ---@param path string + ---@param location InstallLocation + prefix = function(path, location) + return location:opt(Path.concat { "mason", "system", "share", path }) + end, + }, +} + ---@param receipt InstallReceipt ---@param link_context LinkContext ---@param location InstallLocation @@ -48,7 +80,7 @@ local function unlink(receipt, link_context, location) return end for linked_file in pairs(links) do - if receipt:get_schema_version() == "1.0" and link_context == LinkContext.BIN and platform.is.win then + if receipt:get_schema_version() == "1.0" and link_context.type == "bin" and platform.is.win then linked_file = linked_file .. ".cmd" end local share_path = link_context.prefix(linked_file, location) @@ -63,10 +95,11 @@ end ---@nodiscard function M.unlink(pkg, receipt, location) log.fmt_debug("Unlinking %s", pkg, receipt:get_links()) + local link_context = pkg.spec.system and SystemLinkContext or LinkContext return Result.try(function(try) - try(unlink(receipt, LinkContext.BIN, location)) - try(unlink(receipt, LinkContext.SHARE, location)) - try(unlink(receipt, LinkContext.OPT, location)) + try(unlink(receipt, link_context.BIN, location)) + try(unlink(receipt, link_context.SHARE, location)) + try(unlink(receipt, link_context.OPT, location)) end) end @@ -78,18 +111,18 @@ local function link(context, link_context, link_fn) log.trace("Linking", context.package, link_context.type, context.links[link_context.type]) return Result.try(function(try) for name, rel_path in pairs(context.links[link_context.type]) do - if platform.is.win and link_context == LinkContext.BIN then + if platform.is.win and link_context.type == "bin" then name = ("%s.cmd"):format(name) end local new_abs_path = link_context.prefix(name, context.location) - local target_abs_path = path.concat { context:get_install_path(), rel_path } - local target_rel_path = path.relative(new_abs_path, target_abs_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) -- 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)) + try(Result.pcall(fs.sync.mkdirp, dir)) end -- 2. Ensure source file exists and target doesn't yet exist OR if --force unlink target if it already @@ -129,8 +162,9 @@ local function copyfile(context, link_context) end ---@param context InstallContext -local function win_bin_wrapper(context) - return link(context, LinkContext.BIN, function(new_abs_path, __, target_rel_path) +---@param link_context LinkContext +local function win_bin_wrapper(context, link_context) + return link(context, link_context, function(new_abs_path, __, target_rel_path) local windows_target_rel_path = target_rel_path:gsub("/", "\\") return Result.pcall( fs.async.write_file, @@ -156,15 +190,16 @@ end ---@nodiscard function M.link(context) log.fmt_debug("Linking %s", context.package) + local link_context = context.package.spec.system and SystemLinkContext or LinkContext return Result.try(function(try) if platform.is.win then - try(win_bin_wrapper(context)) - try(copyfile(context, LinkContext.SHARE)) - try(copyfile(context, LinkContext.OPT)) + try(win_bin_wrapper(context, link_context.BIN)) + try(copyfile(context, link_context.SHARE)) + try(copyfile(context, link_context.OPT)) else - try(symlink(context, LinkContext.BIN)) - try(symlink(context, LinkContext.SHARE)) - try(symlink(context, LinkContext.OPT)) + try(symlink(context, link_context.BIN)) + try(symlink(context, link_context.SHARE)) + try(symlink(context, link_context.OPT)) end end) end diff --git a/lua/mason-core/installer/managers/cargo.lua b/lua/mason-core/installer/managers/cargo.lua index 22ec9ed6..a6116f9a 100644 --- a/lua/mason-core/installer/managers/cargo.lua +++ b/lua/mason-core/installer/managers/cargo.lua @@ -1,4 +1,5 @@ local Result = require "mason-core.result" +local SystemPackage = require "mason-core.system-package" local _ = require "mason-core.functional" local installer = require "mason-core.installer" local log = require "mason-core.log" @@ -15,6 +16,7 @@ function M.install(crate, version, opts) opts = opts or {} log.fmt_debug("cargo: install %s %s %s", crate, version, opts) local ctx = installer.context() + ctx:require(SystemPackage.sfw) ctx.stdio_sink:stdout(("Installing crate %s@%s…\n"):format(crate, version)) return ctx.spawn.cargo { "install", @@ -29,6 +31,7 @@ function M.install(crate, version, opts) opts.features and { "--features", opts.features } or vim.NIL, opts.locked and "--locked" or vim.NIL, crate, + firewall = true, } end diff --git a/lua/mason-core/installer/managers/npm.lua b/lua/mason-core/installer/managers/npm.lua index d31fe768..8a8d1582 100644 --- a/lua/mason-core/installer/managers/npm.lua +++ b/lua/mason-core/installer/managers/npm.lua @@ -1,4 +1,5 @@ local Result = require "mason-core.result" +local SystemPackage = require "mason-core.system-package" local _ = require "mason-core.functional" local installer = require "mason-core.installer" local log = require "mason-core.log" @@ -57,16 +58,19 @@ end ---@async ---@param pkg string ---@param version string ----@param opts? { extra_packages?: string[] } +---@param opts? { extra_packages?: string[], install_extra_args?: string[] } function M.install(pkg, version, opts) opts = opts or {} log.fmt_debug("npm: install %s %s %s", pkg, version, opts) local ctx = installer.context() + ctx:require(SystemPackage.sfw) ctx.stdio_sink:stdout(("Installing npm package %s@%s…\n"):format(pkg, version)) return ctx.spawn.npm { "install", ("%s@%s"):format(pkg, version), opts.extra_packages or vim.NIL, + opts.install_extra_args or vim.NIL, + firewall = true, } end diff --git a/lua/mason-core/installer/managers/nuget.lua b/lua/mason-core/installer/managers/nuget.lua index 5a4021d0..2b864163 100644 --- a/lua/mason-core/installer/managers/nuget.lua +++ b/lua/mason-core/installer/managers/nuget.lua @@ -30,7 +30,13 @@ function M.bin_path(bin) return bin end, win = function() - return ("%s.exe"):format(bin) + local ctx = installer.context() + local shim = ("%s.cmd"):format(bin) + if ctx.fs:file_exists(shim) then + return shim + else + return ("%s.exe"):format(bin) + end end, }) end diff --git a/lua/mason-core/installer/managers/powershell.lua b/lua/mason-core/installer/managers/powershell.lua index 0e7f4145..372426e8 100644 --- a/lua/mason-core/installer/managers/powershell.lua +++ b/lua/mason-core/installer/managers/powershell.lua @@ -8,7 +8,7 @@ local M = {} local PWSHOPT = { progress_preference = [[ $ProgressPreference = 'SilentlyContinue'; ]], -- https://stackoverflow.com/a/63301751 security_protocol = [[ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; ]], - error_action_preference = [[ $ErrorActionPreference = "Stop"; ]], + error_action_preference = [[ $ErrorActionPreference = 'Stop'; ]], } local powershell = _.lazy(function() diff --git a/lua/mason-core/installer/managers/pypi.lua b/lua/mason-core/installer/managers/pypi.lua index 72b1b503..a8efd0dd 100644 --- a/lua/mason-core/installer/managers/pypi.lua +++ b/lua/mason-core/installer/managers/pypi.lua @@ -1,5 +1,6 @@ local Optional = require "mason-core.optional" local Result = require "mason-core.result" +local SystemPackage = require "mason-core.system-package" local _ = require "mason-core.functional" local a = require "mason-core.async" local installer = require "mason-core.installer" @@ -66,13 +67,16 @@ local function get_versioned_candidates(supported_python_versions) end return Optional.of(executable) end, { - { semver.new "3.12.0", "python3.12" }, - { semver.new "3.11.0", "python3.11" }, - { semver.new "3.10.0", "python3.10" }, - { semver.new "3.9.0", "python3.9" }, - { semver.new "3.8.0", "python3.8" }, - { semver.new "3.7.0", "python3.7" }, + -- IMPORTANT: should be in ASCENDING order { semver.new "3.6.0", "python3.6" }, + { semver.new "3.7.0", "python3.7" }, + { semver.new "3.8.0", "python3.8" }, + { semver.new "3.9.0", "python3.9" }, + { semver.new "3.10.0", "python3.10" }, + { semver.new "3.11.0", "python3.11" }, + { semver.new "3.12.0", "python3.12" }, + { semver.new "3.13.0", "python3.13" }, + { semver.new "3.14.0", "python3.14" }, }) end @@ -170,6 +174,8 @@ end ---@param pkgs string[] ---@param extra_args? string[] local function pip_install(pkgs, extra_args) + local ctx = installer.context() + ctx:require(SystemPackage.sfw) return venv_python { "-m", "pip", @@ -179,6 +185,7 @@ local function pip_install(pkgs, extra_args) "--ignore-installed", extra_args or vim.NIL, pkgs, + firewall = true, } end diff --git a/lua/mason-core/package/AbstractPackage.lua b/lua/mason-core/package/AbstractPackage.lua index 5678f4dd..a852d350 100644 --- a/lua/mason-core/package/AbstractPackage.lua +++ b/lua/mason-core/package/AbstractPackage.lua @@ -6,6 +6,7 @@ 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 @@ -128,7 +129,7 @@ end ---@return Optional # Optional<InstallReceipt> function AbstractPackage:get_receipt(location) location = location or InstallLocation.global() - local receipt_path = location:receipt(self.name) + local receipt_path = self:get_receipt_path(location) 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)))) @@ -137,6 +138,11 @@ function AbstractPackage:get_receipt(location) end ---@param location? InstallLocation +function AbstractPackage:get_receipt_path(location) + return path.concat { self:get_install_path(location), "mason-receipt.json" } +end + +---@param location? InstallLocation ---@return boolean function AbstractPackage:is_installed(location) error "Unimplemented." @@ -174,6 +180,11 @@ function AbstractPackage:get_installed_version(location) :or_else(nil) end +---@param location? InstallLocation +function AbstractPackage:get_install_path(location) + error "Unimplemented." +end + ---@param opts? PackageInstallOpts ---@param callback? InstallRunnerCallback ---@return InstallHandle diff --git a/lua/mason-core/package/init.lua b/lua/mason-core/package/init.lua index cb4ef99e..07257d8d 100644 --- a/lua/mason-core/package/init.lua +++ b/lua/mason-core/package/init.lua @@ -76,6 +76,7 @@ Package.License = setmetatable({}, { ---@class RegistryPackageSpec ---@field schema RegistryPackageSpecSchema ---@field name string +---@field system boolean? ---@field description string ---@field homepage string ---@field licenses string[] @@ -147,9 +148,19 @@ function Package:uninstall(opts, callback) end ---@param location? InstallLocation +function Package:get_install_path(location) + location = location or InstallLocation.global() + if self.spec.system then + return location:system_package(self.name) + else + return location:package(self.name) + end +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)) + local ok, stat = pcall(vim.loop.fs_stat, self:get_install_path(location)) if not ok or not stat then return false end diff --git a/lua/mason-core/result.lua b/lua/mason-core/result.lua index e98a11b3..27c46862 100644 --- a/lua/mason-core/result.lua +++ b/lua/mason-core/result.lua @@ -189,7 +189,8 @@ function Result.try(fn) local thread = coroutine.create(fn) local step step = function(...) - local ok, result = coroutine.resume(thread, ...) + local results = { coroutine.resume(thread, ...) } + local ok, result = results[1], results[2] if not ok then return Result.failure(result) end @@ -207,7 +208,7 @@ function Result.try(fn) end else -- yield to parent coroutine - return step(coroutine.yield(result)) + return step(coroutine.yield(result, unpack(results, 3))) end end return step(coroutine.yield) diff --git a/lua/mason-core/spawn.lua b/lua/mason-core/spawn.lua index 0da67569..78be4fd7 100644 --- a/lua/mason-core/spawn.lua +++ b/lua/mason-core/spawn.lua @@ -4,6 +4,7 @@ local a = require "mason-core.async" local log = require "mason-core.log" local platform = require "mason-core.platform" local process = require "mason-core.process" +local settings = require "mason.settings" local is_not_nil = _.complement(_.equals(vim.NIL)) @@ -54,15 +55,17 @@ local function Failure(err, cmd) })) end -local get_path_from_env_list = _.compose(_.strip_prefix "PATH=", _.find_first(_.starts_with "PATH=")) +local get_path_from_env_list = + _.compose(_.if_else(_.is_nil, _.identity, _.strip_prefix "PATH="), _.find_first(_.starts_with "PATH=")) ---@class SpawnArgs ---@field with_paths string[]? Paths to add to the PATH environment variable. ----@field env table<string, string>? Example { SOME_ENV = "value", SOME_OTHER_ENV = "some_value" } ----@field env_raw string[]? Example: { "SOME_ENV=value", "SOME_OTHER_ENV=some_value" } +---@field env table<string, string>? Environment variables to merge with the current environment. Example { SOME_ENV = "value", SOME_OTHER_ENV = "some_value" } +---@field env_raw string[]? The environment to start the process with, will not merge with the current environment. Example: { "SOME_ENV=value", "SOME_OTHER_ENV=some_value" } ---@field stdio_sink StdioSink? If provided, will be used to write to stdout and stderr. ---@field cwd string? ---@field on_spawn (fun(handle: luv_handle, stdio: luv_pipe[], pid: integer))? Will be called when the process successfully spawns. +---@field firewall boolean? setmetatable(spawn, { ---@param canonical_cmd string @@ -101,6 +104,20 @@ setmetatable(spawn, { end end + if args.firewall and settings.current.firewall.enabled then + a.scheduler() + table.insert(spawn_args.args, 1, cmd) + local expanded_cmd = exepath( + "sfw", + settings.current.firewall.auto_managed and vim.fn.expand "$MASON/opt/mason/system/bin" or nil + ) + if expanded_cmd == "" then + return Failure({ stderr = "Failed to find sfw (Socket Firewall) in PATH." }, "sfw") + else + cmd = expanded_cmd + end + end + local _, exit_code, signal = a.wait(function(resolve) local handle, stdio, pid = process.spawn(cmd, spawn_args, resolve) if args.on_spawn and handle and stdio and pid then diff --git a/lua/mason-core/system-package.lua b/lua/mason-core/system-package.lua new file mode 100644 index 00000000..798db774 --- /dev/null +++ b/lua/mason-core/system-package.lua @@ -0,0 +1,96 @@ +local Result = require "mason-core.result" +local _ = require "mason-core.functional" +local a = require "mason-core.async" +local registry = require "mason-registry" +local settings = require "mason.settings" +local OneShotChannel = require("mason-core.async.control").OneShotChannel + +---@class SystemPackage +---@field name string +---@field condition? fun(): bool +local SystemPackage = {} +SystemPackage.__index = SystemPackage + +---@type table<string, OneShotChannel> +SystemPackage.channels = {} + +function SystemPackage:new(system_pkg_name) + ---@type SystemPackage + local instance = {} + setmetatable(instance, self) + instance.name = system_pkg_name + return instance +end + +function SystemPackage:conditional(fn) + self.condition = fn + return self +end + +---@async +function SystemPackage:get_package() + a.scheduler() + pcall(a.wait, registry.refresh_system) + if not registry.has_system_package(self.name) then + -- Force update to the very latest registry version + pcall(a.wait, registry.update) + end + if not registry.has_system_package(self.name) then + return Result.failure("Unable to find system package " .. self.name) + end + return Result.pcall(registry.get_system_package, self.name) +end + +---@async +---@return Result<boolean> +function SystemPackage:needs_install() + return Result.try(function(try) + if self.condition and not self.condition() then + return false + end + local pkg = try(self:get_package()) + if not pkg:is_installed() or pkg:is_installing() then + return true + end + if pkg:get_installed_version() ~= pkg:get_latest_version() then + return true + end + return false + end) +end + +---@async +function SystemPackage:await_channel() + assert(SystemPackage.channels[self.name], "Tried to await non-existing channel.") + local success, result = SystemPackage.channels[self.name]:receive() + if not success then + return Result.failure("Failed to install system package " .. self.name .. ". Error: " .. result) + end + return Result.success() +end + +---@async +---@return Result +function SystemPackage:install() + return Result.try(function(try) + local pkg = try(self:get_package()) + if not pkg:is_installing() then + local channel = OneShotChannel:new() + SystemPackage.channels[self.name] = channel + pkg:install({}, function(success, result) + channel:send(success, result) + end) + end + return self:await_channel() + end) +end + +function SystemPackage:__tostring() + return ("SystemPackage(name=%s)"):format(self.name) +end + +SystemPackage.sfw = SystemPackage:new("sfw@latest"):conditional(function() + return settings.current.firewall.enabled and settings.current.firewall.auto_managed +end) + +return SystemPackage diff --git a/lua/mason-registry/init.lua b/lua/mason-registry/init.lua index 5806c30a..167a3dfb 100644 --- a/lua/mason-registry/init.lua +++ b/lua/mason-registry/init.lua @@ -1,13 +1,19 @@ local EventEmitter = require "mason-core.EventEmitter" local InstallLocation = require "mason-core.installer.InstallLocation" +local _ = require "mason-core.functional" local log = require "mason-core.log" +local path = require "mason-core.path" +local settings = require "mason.settings" local uv = vim.loop local LazySourceCollection = require "mason-registry.sources" -- singleton local Registry = EventEmitter:new() -Registry.sources = LazySourceCollection:new() +Registry.sources = LazySourceCollection:new(path.concat { vim.fn.stdpath "cache", "mason-registry-update" }) +Registry.system_sources = + LazySourceCollection:new(path.concat { vim.fn.stdpath "cache", "mason-system-registry-update" }, true) + ---@type table<string, string[]> Registry.aliases = {} @@ -37,6 +43,26 @@ function Registry.has_package(pkg_name) return ok end +---Returns an instance of the Package class if the provided package name exists. This function errors if a package +---cannot be found. +---@param pkg_name string +---@return Package +function Registry.get_system_package(pkg_name) + for source in Registry.system_sources:iterate() do + local pkg = source:get_package(pkg_name) + if pkg then + return pkg + end + end + log.fmt_error("Cannot find system package %q.", pkg_name) + error(("Cannot find system package %q."):format(pkg_name)) +end + +function Registry.has_system_package(pkg_name) + local ok = pcall(Registry.get_system_package, pkg_name) + return ok +end + function Registry.get_installed_package_names() local fs = require "mason-core.fs" if not fs.sync.dir_exists(InstallLocation.global():package()) then @@ -103,80 +129,97 @@ function Registry.get_package_aliases(name) return Registry.aliases[name] or {} end ----@param callback? fun(success: boolean, updated_registries: RegistrySource[]) -function Registry.update(callback) - local a = require "mason-core.async" +---@async +---@param sources LazySourceCollection +local function update(sources) local installer = require "mason-registry.installer" - local noop = function() end - a.run(function() - if installer.channel then - log.debug "Registry update already in progress." - return installer.channel:receive():get_or_throw() - else - log.debug "Updating the registry." - Registry:emit("update:start", Registry.sources) - return installer - .install(Registry.sources, function(finished, all) - Registry:emit("update:progress", finished, all) - end) - :on_success(function(updated_registries) - log.fmt_debug("Successfully updated %d registries.", #updated_registries) - Registry:emit("update:success", updated_registries) - end) - :on_failure(function(errors) - log.error("Failed to update registries.", errors) - Registry:emit("update:failed", errors) - end) - :get_or_throw() - end - end, callback or noop) + if sources.install_channel then + log.debug "Registry update already in progress." + return sources.install_channel:receive():get_or_throw() + else + log.debug "Updating the registry." + Registry:emit("update:start", sources) + return installer + .install(sources, function(finished, all) + Registry:emit("update:progress", finished, all) + end) + :on_success(function(updated_registries) + log.fmt_debug("Successfully updated %d registries.", #updated_registries) + Registry:emit("update:success", updated_registries) + end) + :on_failure(function(errors) + log.error("Failed to update registries.", errors) + Registry:emit("update:failed", errors) + end) + :get_or_throw() + end end -local REGISTRY_STORE_TTL = 86400 -- 24 hrs +---@alias RegistryUpdateCallback fun(success: boolean, updated_registries: RegistrySource[]) ----@param callback? fun(success: boolean, updated_registries: RegistrySource[]) -function Registry.refresh(callback) - log.debug "Refreshing the registry." +---@param callback? RegistryUpdateCallback +function Registry.update_system(callback) local a = require "mason-core.async" - local installer = require "mason-registry.installer" + local noop = function() end + a.run(update, callback or noop, Registry.system_sources) +end + +---@param callback? RegistryUpdateCallback +function Registry.update(callback) + local a = require "mason-core.async" + local noop = function() end + a.run(update, callback or noop, Registry.sources) +end - local state = installer.get_registry_state() - if state and Registry.sources:is_all_installed() then +---@param sources LazySourceCollection +---@param callback? RegistryUpdateCallback +local function refresh(sources, callback) + if not settings.current.registry_cache.refresh then + log.debug "Not performing a registry refresh as it's disabled in settings." + if callback then + return callback(true, {}) + end + return true, {} + end + local a = require "mason-core.async" + + local state = sources:get_install_state() + if state and sources:is_all_installed() then local registry_age = os.time() - state.timestamp - if registry_age <= REGISTRY_STORE_TTL and state.checksum == Registry.sources:checksum() then + if registry_age <= settings.current.registry_cache.duration and state.checksum == sources:checksum() then log.fmt_debug( "Registry refresh is not necessary yet. Registry age=%d, checksum=%s", registry_age, state.checksum ) if callback then - callback(true, {}) + return callback(true, {}) end - return + return true, {} end end - local function async_update() - return a.wait(function(resolve, reject) - Registry.update(function(success, result) - if success then - resolve(result) - else - reject(result) - end - end) - end) - end - if not callback then -- We don't want to error in the synchronous version because of how this function is recommended to be used in -- 3rd party code. If accessing the update result is required, users are recommended to pass a callback. - pcall(a.run_blocking, async_update) + return pcall(a.run_blocking, update, sources) else - a.run(async_update, callback) + a.run(update, callback, sources) end end +---@param callback? RegistryUpdateCallback +function Registry.refresh(callback) + log.debug "Refreshing the registry." + return refresh(Registry.sources, callback) +end + +---@param callback? RegistryUpdateCallback +function Registry.refresh_system(callback) + log.debug "Refreshing the system registry." + return refresh(Registry.system_sources, callback) +end + return Registry diff --git a/lua/mason-registry/installer.lua b/lua/mason-registry/installer.lua index 05592227..b832805b 100644 --- a/lua/mason-registry/installer.lua +++ b/lua/mason-registry/installer.lua @@ -4,30 +4,19 @@ local OneShotChannel = require("mason-core.async.control").OneShotChannel local Result = require "mason-core.result" local _ = require "mason-core.functional" local fs = require "mason-core.fs" -local path = require "mason-core.path" local M = {} -local STATE_FILE = path.concat { vim.fn.stdpath "cache", "mason-registry-update" } - ---@param sources LazySourceCollection ---@param time integer local function update_registry_state(sources, time) log.trace("Updating registry state", sources, time) - local dir = vim.fn.fnamemodify(STATE_FILE, ":h") + local state_file = sources:get_state_file() + local dir = vim.fn.fnamemodify(state_file, ":h") if not fs.sync.dir_exists(dir) then fs.sync.mkdirp(dir) end - fs.sync.write_file(STATE_FILE, _.join("\n", { sources:checksum(), tostring(time) })) -end - ----@return { checksum: string, timestamp: integer }? -function M.get_registry_state() - if fs.sync.file_exists(STATE_FILE) then - local parse_state_file = - _.compose(_.evolve { timestamp = tonumber }, _.zip_table { "checksum", "timestamp" }, _.split "\n") - return parse_state_file(fs.sync.read_file(STATE_FILE)) - end + fs.sync.write_file(state_file, _.join("\n", { sources:checksum(), tostring(time) })) end ---@async @@ -36,8 +25,8 @@ end ---@return Result # Result<RegistrySource[]> function M.install(sources, on_progress) log.debug("Installing registries.", sources) - assert(not M.channel, "Cannot install when channel is active.") - M.channel = OneShotChannel:new() + assert(not sources.install_channel, "Cannot install when channel is active.") + sources.install_channel = OneShotChannel:new() local finished_registries = {} local registries = sources:to_list { include_uninstalled = true, include_synthesized = false } @@ -69,15 +58,15 @@ function M.install(sources, on_progress) if any_failed then local unwrap_failures = _.compose(_.map(Result.err_or_nil), _.filter(Result.is_failure)) local result = Result.failure(unwrap_failures(results)) - M.channel:send(result) - M.channel = nil + sources.install_channel:send(result) + sources.install_channel = nil return result else local result = Result.success(_.map(Result.get_or_nil, results)) a.scheduler() update_registry_state(sources, os.time()) - M.channel:send(result) - M.channel = nil + sources.install_channel:send(result) + sources.install_channel = nil return result end end diff --git a/lua/mason-registry/sources/file.lua b/lua/mason-registry/sources/file.lua index 663efaaa..f29cf2dc 100644 --- a/lua/mason-registry/sources/file.lua +++ b/lua/mason-registry/sources/file.lua @@ -21,12 +21,14 @@ local FileRegistrySource = {} FileRegistrySource.__index = FileRegistrySource ---@param spec FileRegistrySourceSpec -function FileRegistrySource:new(spec) +---@param system boolean +function FileRegistrySource:new(spec, system) ---@type FileRegistrySource local instance = {} setmetatable(instance, self) instance.id = spec.id instance.spec = spec + instance.system = system return instance end diff --git a/lua/mason-registry/sources/github.lua b/lua/mason-registry/sources/github.lua index 2b177bdd..73c9d285 100644 --- a/lua/mason-registry/sources/github.lua +++ b/lua/mason-registry/sources/github.lua @@ -29,7 +29,8 @@ local GitHubRegistrySource = {} GitHubRegistrySource.__index = GitHubRegistrySource ---@param spec GitHubRegistrySourceSpec -function GitHubRegistrySource:new(spec) +---@param system boolean +function GitHubRegistrySource:new(spec, system) ---@type GitHubRegistrySource local instance = {} setmetatable(instance, GitHubRegistrySource) @@ -38,6 +39,7 @@ function GitHubRegistrySource:new(spec) instance.spec = spec instance.repo = ("%s/%s"):format(spec.namespace, spec.name) instance.root_dir = root_dir + instance.system = system instance.data_file = path.concat { root_dir, "registry.json" } instance.info_file = path.concat { root_dir, "info.json" } return instance @@ -93,7 +95,7 @@ function GitHubRegistrySource:install() if not fs.async.dir_exists(self.root_dir) then log.debug("Creating registry directory", self) - try(Result.pcall(fs.async.mkdirp, self.root_dir)) + try(Result.pcall(fs.sync.mkdirp, self.root_dir)) end if version == nil then diff --git a/lua/mason-registry/sources/init.lua b/lua/mason-registry/sources/init.lua index 36e62ec5..b69fa8b1 100644 --- a/lua/mason-registry/sources/init.lua +++ b/lua/mason-registry/sources/init.lua @@ -1,7 +1,9 @@ +local _ = require "mason-core.functional" local log = require "mason-core.log" ---@class RegistrySource ---@field id string +---@field system boolean ---@field get_package fun(self: RegistrySource, pkg_name: string): Package? ---@field get_all_package_names fun(self: RegistrySource): string[] ---@field get_all_package_specs fun(self: RegistrySource): RegistryPackageSpec[] @@ -16,42 +18,46 @@ local log = require "mason-core.log" ---@class LazySource ---@field type RegistrySourceType ---@field id string ----@field init fun(id: string): RegistrySource +---@field init fun(id: string, system: boolean): RegistrySource +---@field system boolean local LazySource = {} LazySource.__index = LazySource ---@param id string -function LazySource.GitHub(id) +---@param system boolean +function LazySource.GitHub(id, system) local namespace, name = id:match "^(.+)/(.+)$" if not namespace or not name then error(("Failed to parse repository from GitHub registry: %q"):format(id), 0) end local name, version = unpack(vim.split(name, "@")) local GitHubRegistrySource = require "mason-registry.sources.github" - return GitHubRegistrySource:new { + return GitHubRegistrySource:new({ id = id, namespace = namespace, name = name, version = version, - } + }, system) end ---@param id string -function LazySource.Lua(id) +---@param system boolean +function LazySource.Lua(id, system) local LuaRegistrySource = require "mason-registry.sources.lua" - return LuaRegistrySource:new { + return LuaRegistrySource:new({ id = id, mod = id, - } + }, system) end ---@param id string -function LazySource.File(id) +---@param system boolean +function LazySource.File(id, system) local FileRegistrySource = require "mason-registry.sources.file" - return FileRegistrySource:new { + return FileRegistrySource:new({ id = id, path = id, - } + }, system) end function LazySource.Synthesized() @@ -62,17 +68,20 @@ end ---@param type RegistrySourceType ---@param id string ---@param init fun(id: string): RegistrySource -function LazySource:new(type, id, init) +---@param system boolean +function LazySource:new(type, id, init, system) + ---@type LazySource local instance = setmetatable({}, self) instance.type = type instance.id = id instance.init = init + instance.system = system return instance end function LazySource:get() if not self.instance then - self.instance = self.init(self.id) + self.instance = self.init(self.id, self.system) end return self.instance end @@ -105,43 +114,66 @@ local function split_once_left(str, char) end ---@param registry_id string -local function parse(registry_id) +---@param system boolean +local function parse(registry_id, system) local type, id = split_once_left(registry_id, ":") assert(id, ("Malformed registry %q"):format(registry_id)) if type == "github" then - return LazySource:new(type, id, LazySource.GitHub) + return LazySource:new(type, id, LazySource.GitHub, system) elseif type == "lua" then - return LazySource:new(type, id, LazySource.Lua) + return LazySource:new(type, id, LazySource.Lua, system) elseif type == "file" then - return LazySource:new(type, id, LazySource.File) + return LazySource:new(type, id, LazySource.File, system) end error(("Unknown registry type: %s"):format(type)) end ---@class LazySourceCollection +---@field state_file string +---@field system boolean? ---@field list LazySource[] ---@field synthesized LazySource +---@field install_channel OneShotChannel? local LazySourceCollection = {} LazySourceCollection.__index = LazySourceCollection ---@return LazySourceCollection -function LazySourceCollection:new() +---@param state_file string +---@param system boolean? +function LazySourceCollection:new(state_file, system) + ---@type LazySourceCollection local instance = {} setmetatable(instance, self) + instance.state_file = state_file + instance.system = system instance.list = {} instance.synthesized = LazySource:new("synthesized", "synthesized", LazySource.Synthesized) return instance end +---@return { checksum: string, timestamp: integer }? +function LazySourceCollection:get_install_state() + local fs = require "mason-core.fs" + if fs.sync.file_exists(self.state_file) then + local parse_state_file = + _.compose(_.evolve { timestamp = tonumber }, _.zip_table { "checksum", "timestamp" }, _.split "\n") + return parse_state_file(fs.sync.read_file(self.state_file)) + end +end + +function LazySourceCollection:get_state_file() + return self.state_file +end + ---@param registry string function LazySourceCollection:append(registry) - self:unique_insert(parse(registry)) + self:unique_insert(parse(registry, not not self.system)) return self end ---@param registry string function LazySourceCollection:prepend(registry) - self:unique_insert(parse(registry), 1) + self:unique_insert(parse(registry, not not self.system), 1) return self end diff --git a/lua/mason-registry/sources/lua.lua b/lua/mason-registry/sources/lua.lua index 40e728b6..273655c0 100644 --- a/lua/mason-registry/sources/lua.lua +++ b/lua/mason-registry/sources/lua.lua @@ -13,12 +13,14 @@ local LuaRegistrySource = {} LuaRegistrySource.__index = LuaRegistrySource ---@param spec LuaRegistrySourceSpec -function LuaRegistrySource:new(spec) +---@param system boolean +function LuaRegistrySource:new(spec, system) ---@type LuaRegistrySource local instance = {} setmetatable(instance, LuaRegistrySource) instance.id = spec.id instance.spec = spec + instance.system = system return instance end diff --git a/lua/mason-registry/sources/synthesized.lua b/lua/mason-registry/sources/synthesized.lua index 8cad635e..2a6cd281 100644 --- a/lua/mason-registry/sources/synthesized.lua +++ b/lua/mason-registry/sources/synthesized.lua @@ -5,6 +5,7 @@ local InstallReceipt = require("mason-core.receipt").InstallReceipt local InstallLocation = require "mason-core.installer.InstallLocation" local fs = require "mason-core.fs" local log = require "mason-core.log" +local path = require "mason-core.path" ---@class SynthesizedRegistrySource : RegistrySource ---@field buffer table<string, Package> @@ -68,14 +69,20 @@ end ---@param pkg_name string ---@return Package? function SynthesizedRegistrySource:get_package(pkg_name) - local receipt_path = InstallLocation.global():receipt(pkg_name) - if fs.sync.file_exists(receipt_path) then - local ok, receipt_json = pcall(vim.json.decode, fs.sync.read_file(receipt_path)) - if ok then - local receipt = InstallReceipt.from_json(receipt_json) - return self:load_package(pkg_name, receipt) - else - log.error("Failed to decode package receipt", pkg_name, receipt_json) + local location = InstallLocation.global() + local receipt_paths = { + path.concat { location:package(pkg_name), "mason-receipt.json" }, + path.concat { location:system_package(pkg_name), "mason-receipt.json" }, + } + for _, receipt_path in ipairs(receipt_paths) do + if fs.sync.file_exists(receipt_path) then + local ok, receipt_json = pcall(vim.json.decode, fs.sync.read_file(receipt_path)) + if ok then + local receipt = InstallReceipt.from_json(receipt_json) + return self:load_package(pkg_name, receipt) + else + log.error("Failed to decode package receipt", pkg_name, receipt_json) + end end end end diff --git a/lua/mason-registry/sources/util.lua b/lua/mason-registry/sources/util.lua index 9efa1420..e37dabfe 100644 --- a/lua/mason-registry/sources/util.lua +++ b/lua/mason-registry/sources/util.lua @@ -30,6 +30,10 @@ M.hydrate_package = _.curryN(function(registry, buffer, spec) local _ = Package.License[lang] end, spec.licenses) + if registry.system then + spec = _.assoc("system", true, spec) + end + local existing_instance = buffer[spec.name] if existing_instance then -- Apply spec to the existing Package instances. This is important as to not have lingering package instances. diff --git a/lua/mason-test/helpers.lua b/lua/mason-test/helpers.lua index 88354046..a056406e 100644 --- a/lua/mason-test/helpers.lua +++ b/lua/mason-test/helpers.lua @@ -12,7 +12,10 @@ local M = {} function M.create_context(opts) local pkg = registry.get_package(opts and opts.package or "dummy") local handle = InstallHandle:new(pkg, InstallLocation.global()) - local context = InstallContext:new(handle, opts and opts.install_opts or {}) + local context = InstallContext:new(handle, opts and opts.install_opts or {}, { + suspend = function() end, + resume = function() end, + }) context.spawn = setmetatable({}, { __index = function(s, cmd) s[cmd] = spy.new(function() diff --git a/lua/mason/health.lua b/lua/mason/health.lua index e439736a..557fdc5f 100644 --- a/lua/mason/health.lua +++ b/lua/mason/health.lua @@ -113,14 +113,15 @@ local function check_core_utils() end if platform.is.win then + local powershell = vim.fn.executable "pwsh" == 1 and "pwsh" or "powershell" check { - cmd = "pwsh", + cmd = powershell, args = { "-NoProfile", "-Command", - [[$PSVersionTable.PSVersion, $PSVersionTable.OS, $PSVersionTable.Platform -join " "]], + [[$PSVersionTable.PSVersion, $PSVersionTable.OS, $PSVersionTable.Platform -join ' ']], }, - name = "pwsh", + name = powershell, } check { cmd = "7z", args = { "--help" }, name = "7z", relaxed = true } end diff --git a/lua/mason/init.lua b/lua/mason/init.lua index 9c507f5c..043fb651 100644 --- a/lua/mason/init.lua +++ b/lua/mason/init.lua @@ -26,6 +26,9 @@ function M.setup(config) for _, registry in ipairs(settings.current.registries) do Registry.sources:append(registry) end + for _, registry in ipairs(settings.current.system_registries) do + Registry.system_sources:append(registry) + end require "mason.api.command" setup_autocmds() diff --git a/lua/mason/settings.lua b/lua/mason/settings.lua index ebff1e0b..33278bb7 100644 --- a/lua/mason/settings.lua +++ b/lua/mason/settings.lua @@ -34,6 +34,39 @@ local DEFAULT_SETTINGS = { "github:mason-org/mason-registry", }, + ---@since 2.3.0 + -- [Advanced setting] + -- The registries to source system packages from. Accepts multiple entries. Should a package with the same name exist in + -- multiple registries, the registry listed first will be used. + system_registries = { + "github:mason-org/mason-system-registry", + }, + + registry_cache = { + ---@since 2.3.0 + -- [Advanced setting] + -- Whether Mason should automatically refresh the registry when needed. If false, the registry will have to be + -- updated manually via :MasonUpdate or the :Mason UI. + refresh = true, + + ---@since 2.3.0 + -- Amount of seconds before the local registry cache is considered stale. + -- Note that this setting has no effect if refresh is set to false. + duration = 24 * 60 * 60, -- 24 hours + }, + + firewall = { + ---@since 2.3.0 + -- Whether to enable the socket.dev firewall (sfw) for supported package sources. + -- For more information, refer to https://socket.dev. + enabled = false, + + ---@since 2.3.0 + -- Whether mason.nvim should automatically install and update the Socket Firewall client. + -- If false, the sfw binary must exist in PATH if the firewall is enabled. + auto_managed = true, + }, + ---@since 1.0.0 -- The provider implementations to use for resolving supplementary package metadata (e.g., all available versions). -- Accepts multiple entries, where later entries will be used as fallback should prior providers fail. @@ -68,6 +101,15 @@ local DEFAULT_SETTINGS = { install_args = {}, }, + npm = { + ---@since 2.3.0 + -- These args will be added to `npm install` calls. Note that setting extra args might impact intended behavior + -- and is not recommended. + -- + -- Example: { "--registry", "https://registry.npmjs.org/" } + install_args = {}, + }, + ui = { ---@since 1.0.0 -- Whether to automatically check for new versions when opening the :Mason window. diff --git a/lua/mason/ui/components/help/init.lua b/lua/mason/ui/components/help/init.lua index 83e6c251..30d3abe4 100644 --- a/lua/mason/ui/components/help/init.lua +++ b/lua/mason/ui/components/help/init.lua @@ -134,10 +134,6 @@ local function GenericHelp(state) p.none "- ", p.highlight "https://github.com/mason-org/mason.nvim/blob/main/CONTRIBUTING.md", }, - { - p.none "- ", - p.highlight "https://github.com/mason-org/mason.nvim/blob/main/doc/reference.md", - }, }, }), Ui.EmptyLine(), diff --git a/lua/mason/ui/components/main/package_list.lua b/lua/mason/ui/components/main/package_list.lua index 11f0d174..67805088 100644 --- a/lua/mason/ui/components/main/package_list.lua +++ b/lua/mason/ui/components/main/package_list.lua @@ -271,6 +271,7 @@ local function InstallingPackageComponent(pkg, state) pkg_state.has_failed and p.error(settings.current.ui.icons.package_uninstalled) or p.highlight(settings.current.ui.icons.package_pending), p.none(" " .. pkg.name), + pkg_state.firewall_active and p.highlight " (firewall active)" or p.none "", current_state, pkg_state.latest_spawn and p.Comment((" $ %s"):format(pkg_state.latest_spawn)) or p.none "", }, diff --git a/lua/mason/ui/instance.lua b/lua/mason/ui/instance.lua index 476bdf8c..fc035318 100644 --- a/lua/mason/ui/instance.lua +++ b/lua/mason/ui/instance.lua @@ -46,6 +46,7 @@ end ---@field is_log_expanded boolean ---@field has_failed boolean ---@field latest_spawn string? +---@field firewall_active boolean? ---@field linked_executables table<string, string>? ---@field installed_purl string? ---@field lsp_settings_schema table? @@ -239,8 +240,13 @@ local function setup_handle(handle) local function handle_spawnhandle_change() mutate_state(function(state) + local spawn_handle = handle:peek_spawn_handle() state.packages.states[handle.package.name].latest_spawn = - handle:peek_spawn_handle():map(tostring):map(_.gsub("\n", "\\n ")):or_else(nil) + spawn_handle:map(tostring):map(_.gsub("\n", "\\n ")):or_else(nil) + if settings.current.firewall.enabled then + state.packages.states[handle.package.name].firewall_active = + spawn_handle:map(_.prop "firewall"):or_else(false) + end end) end diff --git a/lua/mason/version.lua b/lua/mason/version.lua index cf094f30..282af1aa 100644 --- a/lua/mason/version.lua +++ b/lua/mason/version.lua @@ -1,8 +1,8 @@ local M = {} -M.VERSION = "v2.2.0" -- x-release-please-version +M.VERSION = "v2.3.1" -- x-release-please-version M.MAJOR_VERSION = 2 -- x-release-please-major -M.MINOR_VERSION = 2 -- x-release-please-minor -M.PATCH_VERSION = 0 -- x-release-please-patch +M.MINOR_VERSION = 3 -- x-release-please-minor +M.PATCH_VERSION = 1 -- x-release-please-patch return M |
