aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorWilliam Boman <william@redwill.se>2023-04-23 22:25:51 +0200
committerGitHub <noreply@github.com>2023-04-23 22:25:51 +0200
commitb03c163f12684c21f43b98208ada78145763c6f1 (patch)
treef2875067c3ddcd3eda47c7161725c9ab5807f84b
parentfix(expr): fix strip_{prefix,suffix} (#1249) (diff)
downloadmason-b03c163f12684c21f43b98208ada78145763c6f1.tar
mason-b03c163f12684c21f43b98208ada78145763c6f1.tar.gz
mason-b03c163f12684c21f43b98208ada78145763c6f1.tar.bz2
mason-b03c163f12684c21f43b98208ada78145763c6f1.tar.lz
mason-b03c163f12684c21f43b98208ada78145763c6f1.tar.xz
mason-b03c163f12684c21f43b98208ada78145763c6f1.tar.zst
mason-b03c163f12684c21f43b98208ada78145763c6f1.zip
fix(linker): don't symlink on Windows (#1253)
1) FAT (FAT32, exFAT, etc.) file systems doesn't support symlinks. 2) You need administrator access in order to create symlinks. This was working for me locally because I'm running a NTFS fs with "Developer Mode" enabled in Windows 10, which bypasses administrator requirement for mklink. Instead we uv_fs_rename() links. This should effectively be the same, but will require special handling down the road if/when for example multiple package versions can be installed at the same time. When unlinking a package the renamed files should me moved back into the package installation directory, instead of simply being unlinked (i.e. removed). Closes #1251.
-rw-r--r--lua/mason-core/installer/linker.lua40
-rw-r--r--tests/mason-core/installer/linker_spec.lua56
2 files changed, 79 insertions, 17 deletions
diff --git a/lua/mason-core/installer/linker.lua b/lua/mason-core/installer/linker.lua
index 98d1d30f..c9a87027 100644
--- a/lua/mason-core/installer/linker.lua
+++ b/lua/mason-core/installer/linker.lua
@@ -50,7 +50,7 @@ end
---@async
---@param context InstallContext
---@param link_context LinkContext
----@param link_fn async fun(dest: string, target: string): Result
+---@param link_fn async fun(new_abs_path: string, target_abs_path: string): Result
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)
@@ -58,13 +58,13 @@ local function link(context, link_context, link_fn)
if platform.is.win and link_context == LinkContext.BIN then
name = ("%s.cmd"):format(name)
end
- local dest_abs_path = link_context.prefix(name)
+ local new_abs_path = link_context.prefix(name)
local target_abs_path = path.concat { context.package:get_install_path(), rel_path }
do
-- 1. Ensure destination directory exists
a.scheduler()
- local dir = vim.fn.fnamemodify(dest_abs_path, ":h")
+ local dir = vim.fn.fnamemodify(new_abs_path, ":h")
if not fs.async.dir_exists(dir) then
try(Result.pcall(fs.async.mkdirp, dir))
end
@@ -74,11 +74,11 @@ local function link(context, link_context, link_fn)
-- 2. Ensure source file exists and target doesn't yet exist OR if --force unlink target if it already
-- exists.
if context.opts.force then
- if fs.async.file_exists(dest_abs_path) then
- try(Result.pcall(fs.async.unlink, dest_abs_path))
+ if fs.async.file_exists(new_abs_path) then
+ try(Result.pcall(fs.async.unlink, new_abs_path))
end
- elseif fs.async.file_exists(dest_abs_path) then
- return Result.failure(("%q is already linked."):format(dest_abs_path, name))
+ elseif fs.async.file_exists(new_abs_path) then
+ return Result.failure(("%q is already linked."):format(new_abs_path, name))
end
if not fs.async.file_exists(target_abs_path) then
return Result.failure(("Link target %q does not exist."):format(target_abs_path))
@@ -86,7 +86,7 @@ local function link(context, link_context, link_fn)
end
-- 3. Execute link.
- try(link_fn(dest_abs_path, target_abs_path))
+ try(link_fn(new_abs_path, target_abs_path))
context.receipt:with_link(link_context.type, name, rel_path)
end
end)
@@ -95,17 +95,25 @@ end
---@param context InstallContext
---@param link_context LinkContext
local function symlink(context, link_context)
- return link(context, link_context, function(target, dest)
- return Result.pcall(fs.async.symlink, dest, target)
+ return link(context, link_context, function(new_abs_path, target_abs_path)
+ return Result.pcall(fs.async.symlink, target_abs_path, new_abs_path)
+ end)
+end
+
+---@param context InstallContext
+---@param link_context LinkContext
+local function rename(context, link_context)
+ return link(context, link_context, function(new_abs_path, target_abs_path)
+ return Result.pcall(fs.async.rename, target_abs_path, new_abs_path)
end)
end
---@param context InstallContext
local function win_bin_wrapper(context)
- return link(context, LinkContext.BIN, function(dest, target)
+ return link(context, LinkContext.BIN, function(new_abs_path, target_abs_path)
return Result.pcall(
fs.async.write_file,
- dest,
+ new_abs_path,
_.dedent(([[
@ECHO off
GOTO start
@@ -117,7 +125,7 @@ local function win_bin_wrapper(context)
CALL :find_dp0
endLocal & goto #_undefined_# 2>NUL || title %%COMSPEC%% & "%s" %%*
- ]]):format(target))
+ ]]):format(target_abs_path))
)
end)
end
@@ -130,11 +138,13 @@ function M.link(context)
return Result.try(function(try)
if platform.is.win then
try(win_bin_wrapper(context))
+ try(rename(context, LinkContext.SHARE))
+ try(rename(context, LinkContext.OPT))
else
try(symlink(context, LinkContext.BIN))
+ try(symlink(context, LinkContext.SHARE))
+ try(symlink(context, LinkContext.OPT))
end
- try(symlink(context, LinkContext.SHARE))
- try(symlink(context, LinkContext.OPT))
end)
end
diff --git a/tests/mason-core/installer/linker_spec.lua b/tests/mason-core/installer/linker_spec.lua
index ac66b212..0a2b62ab 100644
--- a/tests/mason-core/installer/linker_spec.lua
+++ b/tests/mason-core/installer/linker_spec.lua
@@ -21,7 +21,7 @@ describe("linker", function()
local platform
before_each(function()
- package.loaded["mason-core.installer.platform"] = nil
+ package.loaded["mason-core.platform"] = nil
package.loaded["mason-core.installer.linker"] = nil
platform = require "mason-core.platform"
linker = require "mason-core.installer.linker"
@@ -131,7 +131,10 @@ describe("linker", function()
local ctx = InstallContextGenerator(handle)
ctx.links.share["nested/path/share-file"] = path.concat { "nested", "path", "to", "share-file" }
ctx.links.share["share-file"] = "share-file"
- assert.is_true(linker.link(ctx):is_success())
+
+ local result = linker.link(ctx)
+
+ assert.is_true(result:is_success())
assert.spy(fs.async.write_file).was_called(0)
assert.spy(fs.async.symlink).was_called(2)
@@ -148,4 +151,53 @@ describe("linker", function()
assert.spy(fs.async.mkdirp).was_called_with(path.share_prefix "nested/path")
end)
)
+
+ it("should rename share files on Windows", function()
+ platform.is.darwin = false
+ platform.is.mac = false
+ platform.is.linux = false
+ platform.is.unix = false
+ platform.is.win = true
+
+ local dummy = registry.get_package "dummy"
+ stub(fs.async, "mkdirp")
+ stub(fs.async, "dir_exists")
+ stub(fs.async, "file_exists")
+ stub(fs.async, "rename")
+
+ -- mock non-existent dest files
+ fs.async.file_exists.on_call_with(path.share_prefix "share-file").returns(false)
+ fs.async.file_exists.on_call_with(path.share_prefix(path.concat { "nested", "share-file" })).returns(false)
+
+ fs.async.dir_exists.on_call_with(path.share_prefix()).returns(false)
+ fs.async.dir_exists.on_call_with(path.share_prefix "nested/path").returns(false)
+
+ -- mock existent source files
+ fs.async.file_exists.on_call_with(path.concat { dummy:get_install_path(), "share-file" }).returns(true)
+ fs.async.file_exists
+ .on_call_with(path.concat { dummy:get_install_path(), "nested", "path", "to", "share-file" })
+ .returns(true)
+
+ local handle = InstallHandleGenerator "dummy"
+ local ctx = InstallContextGenerator(handle)
+ ctx.links.share["nested/path/share-file"] = path.concat { "nested", "path", "to", "share-file" }
+ ctx.links.share["share-file"] = "share-file"
+
+ local result = linker.link(ctx)
+
+ assert.is_true(result:is_success())
+
+ assert.spy(fs.async.rename).was_called(2)
+ assert
+ .spy(fs.async.rename)
+ .was_called_with(path.concat { dummy:get_install_path(), "share-file" }, path.share_prefix "share-file")
+ assert.spy(fs.async.rename).was_called_with(
+ path.concat { dummy:get_install_path(), "nested", "path", "to", "share-file" },
+ path.share_prefix "nested/path/share-file"
+ )
+
+ assert.spy(fs.async.mkdirp).was_called(2)
+ assert.spy(fs.async.mkdirp).was_called_with(path.share_prefix())
+ assert.spy(fs.async.mkdirp).was_called_with(path.share_prefix "nested/path")
+ end)
end)