aboutsummaryrefslogtreecommitdiffstats
path: root/lua/mason-core
diff options
context:
space:
mode:
Diffstat (limited to 'lua/mason-core')
-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.lua12
-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.lua58
-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.lua21
-rw-r--r--lua/mason-core/package/init.lua13
-rw-r--r--lua/mason-core/receipt.lua12
-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
22 files changed, 422 insertions, 126 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 d8b8941f..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
@@ -43,7 +46,7 @@ function InstallHandleSpawnHandle:__tostring()
end
---@class InstallHandle : EventEmitter
----@field package AbstractPackage
+---@field public package AbstractPackage
---@field state InstallHandleState
---@field stdio_sink BufferedSink
---@field is_terminated boolean
@@ -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 e0150e18..69738911 100644
--- a/lua/mason-core/installer/context/init.lua
+++ b/lua/mason-core/installer/context/init.lua
@@ -17,23 +17,26 @@ local receipt = require "mason-core.receipt"
---@field location InstallLocation
---@field spawn InstallContextSpawn
---@field handle InstallHandle
----@field package AbstractPackage
+---@field public package AbstractPackage
---@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
@@ -86,19 +89,19 @@ function InstallContext:promote_cwd()
-- 2. Prepare for renaming cwd to destination
if platform.is.unix then
-- Some Unix systems will raise an error when renaming a directory to a destination that does not already exist.
- fs.async.mkdir(install_path)
+ fs.sync.mkdir(install_path)
end
- -- 3. Update cwd
- self.cwd:set(install_path)
- -- 4. Move the cwd to the final installation directory
- local rename_success, rename_err = pcall(fs.async.rename, cwd, install_path)
+ -- 3. Move the cwd to the final installation directory
+ local rename_success, rename_err = pcall(fs.sync.rename, cwd, install_path)
if not rename_success then
-- On some file systems, we cannot create the directory before renaming. Therefore, remove it and then rename.
log.trace("Call to uv_fs_rename() while promoting cwd failed.", rename_err)
- fs.async.rmdir(install_path)
- assert(fs.async.dir_exists(cwd), "Current working directory no longer exists after retrying uv_fs_rename().")
- fs.async.rename(cwd, install_path)
+ fs.sync.rmdir(install_path)
+ assert(fs.sync.dir_exists(cwd), "Current working directory no longer exists after retrying uv_fs_rename().")
+ fs.sync.rename(cwd, install_path)
end
+ -- 4. Update cwd
+ self.cwd:set(install_path)
end
---@param rel_path string The relative path from the current working directory to change cwd to. Will only restore to the initial cwd after execution of fn (if provided).
@@ -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 d0fde00d..a852d350 100644
--- a/lua/mason-core/package/AbstractPackage.lua
+++ b/lua/mason-core/package/AbstractPackage.lua
@@ -129,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))))
@@ -138,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."
@@ -166,20 +171,20 @@ end
---@return string?
function AbstractPackage:get_installed_version(location)
return self:get_receipt(location)
- :and_then(
+ :map(
---@param receipt InstallReceipt
function(receipt)
- local source = receipt:get_source()
- if source.id then
- return Purl.parse(source.id):map(_.prop "version"):ok()
- else
- return Optional.empty()
- end
+ return receipt:get_installed_package_version()
end
)
: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/receipt.lua b/lua/mason-core/receipt.lua
index 42a7e882..bdf96254 100644
--- a/lua/mason-core/receipt.lua
+++ b/lua/mason-core/receipt.lua
@@ -1,3 +1,7 @@
+local Optional = require "mason-core.optional"
+local Purl = require "mason-core.purl"
+local _ = require "mason-core.functional"
+
local M = {}
---@alias InstallReceiptSchemaVersion
@@ -41,6 +45,14 @@ function InstallReceipt:get_name()
return self.name
end
+---@return string?
+function InstallReceipt:get_installed_package_version()
+ local source = self:get_source()
+ if source.id then
+ return Purl.parse(source.id):map(_.prop "version"):get_or_nil()
+ end
+end
+
function InstallReceipt:get_schema_version()
return self.schema_version
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