summaryrefslogtreecommitdiffstats
path: root/lua
diff options
context:
space:
mode:
Diffstat (limited to 'lua')
-rw-r--r--lua/mason-core/async/init.lua5
-rw-r--r--lua/mason-core/fs.lua131
-rw-r--r--lua/mason-core/installer/InstallHandle.lua10
-rw-r--r--lua/mason-core/installer/InstallLocation.lua11
-rw-r--r--lua/mason-core/installer/InstallRunner.lua27
-rw-r--r--lua/mason-core/installer/UninstallRunner.lua4
-rw-r--r--lua/mason-core/installer/compiler/compilers/npm.lua5
-rw-r--r--lua/mason-core/installer/context/InstallContextFs.lua12
-rw-r--r--lua/mason-core/installer/context/InstallContextSpawn.lua4
-rw-r--r--lua/mason-core/installer/context/init.lua40
-rw-r--r--lua/mason-core/installer/linker.lua71
-rw-r--r--lua/mason-core/installer/managers/cargo.lua3
-rw-r--r--lua/mason-core/installer/managers/npm.lua6
-rw-r--r--lua/mason-core/installer/managers/nuget.lua8
-rw-r--r--lua/mason-core/installer/managers/powershell.lua2
-rw-r--r--lua/mason-core/installer/managers/pypi.lua19
-rw-r--r--lua/mason-core/package/AbstractPackage.lua13
-rw-r--r--lua/mason-core/package/init.lua13
-rw-r--r--lua/mason-core/result.lua5
-rw-r--r--lua/mason-core/spawn.lua23
-rw-r--r--lua/mason-core/system-package.lua96
-rw-r--r--lua/mason-registry/init.lua145
-rw-r--r--lua/mason-registry/installer.lua29
-rw-r--r--lua/mason-registry/sources/file.lua4
-rw-r--r--lua/mason-registry/sources/github.lua6
-rw-r--r--lua/mason-registry/sources/init.lua70
-rw-r--r--lua/mason-registry/sources/lua.lua4
-rw-r--r--lua/mason-registry/sources/synthesized.lua23
-rw-r--r--lua/mason-registry/sources/util.lua4
-rw-r--r--lua/mason-test/helpers.lua5
-rw-r--r--lua/mason/health.lua7
-rw-r--r--lua/mason/init.lua3
-rw-r--r--lua/mason/settings.lua42
-rw-r--r--lua/mason/ui/components/help/init.lua4
-rw-r--r--lua/mason/ui/components/main/package_list.lua1
-rw-r--r--lua/mason/ui/instance.lua8
-rw-r--r--lua/mason/version.lua6
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