Skip to content

Commit

Permalink
feat(installer): lock package installation
Browse files Browse the repository at this point in the history
  • Loading branch information
williamboman committed May 18, 2023
1 parent c8bbe05 commit 37e9c1c
Show file tree
Hide file tree
Showing 16 changed files with 183 additions and 97 deletions.
122 changes: 76 additions & 46 deletions lua/mason-core/installer/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ local sem = Semaphore.new(settings.current.max_concurrent_installers)
local M = {}

---@async
local function create_prefix_dirs()
function M.create_prefix_dirs()
return Result.try(function(try)
for _, p in ipairs {
path.install_prefix(),
Expand Down Expand Up @@ -52,11 +52,29 @@ function M.context()
return coroutine.yield(CONTEXT_REQUEST)
end

---@async
---@param ctx InstallContext
local function lock_package(ctx)
log.debug("Attempting to lock package", ctx.package)
local lockfile = path.package_lock(ctx.package.name)
if not ctx.opts.force and fs.async.file_exists(lockfile) then
log.error("Lockfile already exists.", ctx.package)
return Result.failure(
("Lockfile exists, installation is already running in another process (pid: %s). Run with :MasonInstall --force to bypass."):format(
fs.sync.read_file(lockfile)
)
)
end
a.scheduler()
fs.async.write_file(lockfile, vim.fn.getpid())
log.debug("Wrote lockfile", ctx.package)
return Result.success(lockfile)
end

---@async
---@param context InstallContext
function M.prepare_installer(context)
return Result.try(function(try)
try(create_prefix_dirs())
local package_build_prefix = path.package_build_prefix(context.package.name)
if fs.async.dir_exists(package_build_prefix) then
try(Result.pcall(fs.async.rmrf, package_build_prefix))
Expand Down Expand Up @@ -165,61 +183,73 @@ function M.execute(handle, opts)

log.fmt_info("Executing installer for %s %s", pkg, opts)

return Result.try(function(try)
-- 1. prepare directories and initialize cwd
local installer = try(M.prepare_installer(context))

-- 2. execute installer
try(run_installer(context, installer))

-- 3. promote temporary installation dir
try(Result.pcall(function()
context:promote_cwd()
end))

-- 4. link package
try(linker.link(context))

-- 5. build & write receipt
---@type InstallReceipt
local receipt = try(build_receipt(context))
try(Result.pcall(function()
receipt:write(context.cwd:get())
end))
end)
return M.create_prefix_dirs()
:and_then(function()
return lock_package(context)
end)
:and_then(function(lockfile)
local release_lock = _.partial(pcall, fs.async.unlink, lockfile)
return Result.try(function(try)
-- 1. prepare directories and initialize cwd
local installer = try(M.prepare_installer(context))

-- 2. execute installer
try(run_installer(context, installer))

-- 3. promote temporary installation dir
try(Result.pcall(function()
context:promote_cwd()
end))

-- 4. link package
try(linker.link(context))

-- 5. build & write receipt
---@type InstallReceipt
local receipt = try(build_receipt(context))
try(Result.pcall(function()
receipt:write(context.cwd:get())
end))
end)
:on_success(function()
release_lock()
if opts.debug then
context.fs:write_file("mason-debug.log", table.concat(tailed_output, ""))
end
end)
:on_failure(function()
release_lock()
if not opts.debug then
-- clean up installation dir
pcall(function()
fs.async.rmrf(context.cwd:get())
end)
else
context.fs:write_file("mason-debug.log", table.concat(tailed_output, ""))
context.stdio_sink.stdout(
("[debug] Installation directory retained at %q.\n"):format(context.cwd:get())
)
end

-- unlink linked executables (in the occasion an error occurs after linking)
build_receipt(context):on_success(function(receipt)
linker.unlink(context.package, receipt):on_failure(function(err)
log.error("Failed to unlink failed installation", err)
end)
end)
end)
end)
:on_success(function()
permit:forget()
handle:close()
log.fmt_info("Installation succeeded for %s", pkg)
if opts.debug then
context.fs:write_file("mason-debug.log", table.concat(tailed_output, ""))
end
end)
:on_failure(function(failure)
permit:forget()
log.fmt_error("Installation failed for %s error=%s", pkg, failure)
context.stdio_sink.stderr(tostring(failure))
context.stdio_sink.stderr "\n"

if not opts.debug then
-- clean up installation dir
pcall(function()
fs.async.rmrf(context.cwd:get())
end)
else
context.fs:write_file("mason-debug.log", table.concat(tailed_output, ""))
context.stdio_sink.stdout(
("[debug] Installation directory retained at %q.\n"):format(context.cwd:get())
)
end

-- unlink linked executables (in the occasion an error occurs after linking)
build_receipt(context):on_success(function(receipt)
linker.unlink(context.package, receipt):on_failure(function(err)
log.error("Failed to unlink failed installation", err)
end)
end)

if not handle:is_closed() and not handle.is_terminated then
handle:close()
end
Expand Down
5 changes: 5 additions & 0 deletions lua/mason-core/path.lua
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ function M.package_build_prefix(name)
return M.concat { M.install_prefix "staging", name }
end

---@param name string
function M.package_lock(name)
return M.package_build_prefix(("%s.lock"):format(name))
end

function M.registry_prefix()
return M.install_prefix "registries"
end
Expand Down
44 changes: 44 additions & 0 deletions tests/mason-core/installer/installer_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -167,4 +167,48 @@ describe("installer", function()
assert.equals("spawn: bash failed with exit code 42 and signal 0. ", tostring(result:err_or_nil()))
end)
)

it(
"should lock package",
async_test(function()
local handle = InstallHandleGenerator "dummy"
local callback = spy.new()
stub(handle.package.spec, "install", function()
a.sleep(3000)
end)

a.run(function()
return installer.execute(handle, { debug = true })
end, callback)

assert.wait_for(function()
assert.is_true(fs.sync.file_exists(path.package_lock "dummy"))
end)
handle:terminate()
assert.wait_for(function()
assert.spy(callback).was_called(1)
end)
assert.is_false(fs.sync.file_exists(path.package_lock "dummy"))
end)
)

it(
"should not run installer if package lock exists",
async_test(function()
local handle = InstallHandleGenerator "dummy"
local install = spy.new()
stub(handle.package.spec, "install", install)

fs.sync.write_file(path.package_lock "dummy", "dummypid")
local result = installer.execute(handle, { debug = true })
assert.is_true(fs.sync.file_exists(path.package_lock "dummy"))
fs.sync.unlink(path.package_lock "dummy")

assert.spy(install).was_not_called()
assert.equals(
"Lockfile exists, installation is already running in another process (pid: dummypid). Run with :MasonInstall --force to bypass.",
result:err_or_nil()
)
end)
)
end)
12 changes: 6 additions & 6 deletions tests/mason-core/managers/cargo_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe("cargo manager", function()
async_test(function()
local handle = InstallHandleGenerator "dummy"
local ctx = InstallContextGenerator(handle, { version = "42.13.37" })
installer.prepare_installer(ctx)
installer.prepare_installer(ctx):get_or_throw()
installer.exec_in_context(ctx, cargo.crate "my-crate")
assert.spy(ctx.spawn.cargo).was_called(1)
assert.spy(ctx.spawn.cargo).was_called_with {
Expand All @@ -39,7 +39,7 @@ describe("cargo manager", function()
async_test(function()
local handle = InstallHandleGenerator "dummy"
local ctx = InstallContextGenerator(handle)
installer.prepare_installer(ctx)
installer.prepare_installer(ctx):get_or_throw()
installer.exec_in_context(ctx, cargo.crate("my-crate", { git = { url = "https://my-crate.git" } }))
assert.spy(ctx.spawn.cargo).was_called(1)
assert.spy(ctx.spawn.cargo).was_called_with {
Expand All @@ -60,7 +60,7 @@ describe("cargo manager", function()
async_test(function()
local handle = InstallHandleGenerator "dummy"
local ctx = InstallContextGenerator(handle)
installer.prepare_installer(ctx)
installer.prepare_installer(ctx):get_or_throw()
installer.exec_in_context(ctx, cargo.crate("crate-name", { git = { url = "https://my-crate.git" } }))
assert.spy(ctx.spawn.cargo).was_called(1)
assert.spy(ctx.spawn.cargo).was_called_with {
Expand All @@ -81,7 +81,7 @@ describe("cargo manager", function()
async_test(function()
local handle = InstallHandleGenerator "dummy"
local ctx = InstallContextGenerator(handle, { version = "42.13.37" })
installer.prepare_installer(ctx)
installer.prepare_installer(ctx):get_or_throw()
installer.exec_in_context(ctx, cargo.crate("my-crate", { features = "lsp" }))
assert.spy(ctx.spawn.cargo).was_called(1)
assert.spy(ctx.spawn.cargo).was_called_with {
Expand All @@ -104,7 +104,7 @@ describe("cargo manager", function()
stub(github, "tag")
github.tag.returns { tag = "v2.1.1", with_receipt = mockx.just_runs }
local ctx = InstallContextGenerator(handle, { version = "42.13.37" })
installer.prepare_installer(ctx)
installer.prepare_installer(ctx):get_or_throw()
installer.exec_in_context(
ctx,
cargo.crate("my-crate", {
Expand Down Expand Up @@ -133,7 +133,7 @@ describe("cargo manager", function()
async_test(function()
local handle = InstallHandleGenerator "dummy"
local ctx = InstallContextGenerator(handle)
installer.prepare_installer(ctx)
installer.prepare_installer(ctx):get_or_throw()
installer.exec_in_context(ctx, cargo.crate "main-package")
assert.same({
type = "cargo",
Expand Down
4 changes: 2 additions & 2 deletions tests/mason-core/managers/composer_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe("composer manager", function()
local handle = InstallHandleGenerator "dummy"
local ctx = InstallContextGenerator(handle, { version = "42.13.37" })
ctx.fs.file_exists = spy.new(mockx.returns(false))
installer.prepare_installer(ctx)
installer.prepare_installer(ctx):get_or_throw()
installer.exec_in_context(
ctx,
composer.packages { "main-package", "supporting-package", "supporting-package2" }
Expand All @@ -41,7 +41,7 @@ describe("composer manager", function()
async_test(function()
local handle = InstallHandleGenerator "dummy"
local ctx = InstallContextGenerator(handle, { version = "42.13.37" })
installer.prepare_installer(ctx)
installer.prepare_installer(ctx):get_or_throw()
installer.exec_in_context(
ctx,
composer.packages { "main-package", "supporting-package", "supporting-package2" }
Expand Down
4 changes: 2 additions & 2 deletions tests/mason-core/managers/dotnet_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ describe("dotnet manager", function()
async_test(function()
local handle = InstallHandleGenerator "dummy"
local ctx = InstallContextGenerator(handle, { version = "42.13.37" })
installer.prepare_installer(ctx)
installer.prepare_installer(ctx):get_or_throw()
installer.exec_in_context(ctx, dotnet.package "main-package")
assert.spy(ctx.spawn.dotnet).was_called(1)
assert.spy(ctx.spawn.dotnet).was_called_with {
Expand All @@ -27,7 +27,7 @@ describe("dotnet manager", function()
async_test(function()
local handle = InstallHandleGenerator "dummy"
local ctx = InstallContextGenerator(handle, { version = "42.13.37" })
installer.prepare_installer(ctx)
installer.prepare_installer(ctx):get_or_throw()
installer.exec_in_context(ctx, dotnet.package "main-package")
assert.same({
type = "dotnet",
Expand Down
4 changes: 2 additions & 2 deletions tests/mason-core/managers/gem_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe("gem manager", function()
async_test(function()
local handle = InstallHandleGenerator "dummy"
local ctx = InstallContextGenerator(handle, { version = "42.13.37" })
installer.prepare_installer(ctx)
installer.prepare_installer(ctx):get_or_throw()
installer.exec_in_context(ctx, gem.packages { "main-package", "supporting-package", "supporting-package2" })
assert.spy(ctx.spawn.gem).was_called(1)
assert.spy(ctx.spawn.gem).was_called_with(match.tbl_containing {
Expand All @@ -39,7 +39,7 @@ describe("gem manager", function()
async_test(function()
local handle = InstallHandleGenerator "dummy"
local ctx = InstallContextGenerator(handle, { version = "42.13.37" })
installer.prepare_installer(ctx)
installer.prepare_installer(ctx):get_or_throw()
installer.exec_in_context(ctx, gem.packages { "main-package", "supporting-package", "supporting-package2" })
assert.same({
type = "gem",
Expand Down
8 changes: 4 additions & 4 deletions tests/mason-core/managers/git_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe("git manager", function()
local handle = InstallHandleGenerator "dummy"
local ctx = InstallContextGenerator(handle)
local err = assert.has_error(function()
installer.prepare_installer(ctx)
installer.prepare_installer(ctx):get_or_throw()
installer.exec_in_context(ctx, function()
git.clone {}
end)
Expand All @@ -29,7 +29,7 @@ describe("git manager", function()
async_test(function()
local handle = InstallHandleGenerator "dummy"
local ctx = InstallContextGenerator(handle)
installer.prepare_installer(ctx)
installer.prepare_installer(ctx):get_or_throw()
installer.exec_in_context(ctx, function()
git.clone { "https://github.com/williamboman/mason.nvim.git" }
end)
Expand All @@ -50,7 +50,7 @@ describe("git manager", function()
async_test(function()
local handle = InstallHandleGenerator "dummy"
local ctx = InstallContextGenerator(handle, { version = "1337" })
installer.prepare_installer(ctx)
installer.prepare_installer(ctx):get_or_throw()
installer.exec_in_context(ctx, function()
git.clone { "https://github.com/williamboman/mason.nvim.git" }
end)
Expand Down Expand Up @@ -79,7 +79,7 @@ describe("git manager", function()
async_test(function()
local handle = InstallHandleGenerator "dummy"
local ctx = InstallContextGenerator(handle)
installer.prepare_installer(ctx)
installer.prepare_installer(ctx):get_or_throw()
installer.exec_in_context(ctx, function()
git.clone({ "https://github.com/williamboman/mason.nvim.git" }).with_receipt()
end)
Expand Down
8 changes: 4 additions & 4 deletions tests/mason-core/managers/github_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe("github release file", function()
stub(providers.github, "get_latest_release")
local handle = InstallHandleGenerator "dummy"
local ctx = InstallContextGenerator(handle)
installer.prepare_installer(ctx)
installer.prepare_installer(ctx):get_or_throw()
local source = installer.exec_in_context(ctx, function()
return github.release_file {
repo = "williamboman/mason.nvim",
Expand All @@ -40,7 +40,7 @@ describe("github release file", function()
}))
local handle = InstallHandleGenerator "dummy"
local ctx = InstallContextGenerator(handle)
installer.prepare_installer(ctx)
installer.prepare_installer(ctx):get_or_throw()
local source = installer.exec_in_context(ctx, function()
return github.release_file {
repo = "williamboman/mason.nvim",
Expand Down Expand Up @@ -68,7 +68,7 @@ describe("github release version", function()
stub(providers.github, "get_latest_release")
local handle = InstallHandleGenerator "dummy"
local ctx = InstallContextGenerator(handle)
installer.prepare_installer(ctx)
installer.prepare_installer(ctx):get_or_throw()
local source = installer.exec_in_context(ctx, function()
return github.release_version {
repo = "williamboman/mason.nvim",
Expand All @@ -88,7 +88,7 @@ describe("github release version", function()
providers.github.get_latest_release.returns(Result.success { tag_name = "v42" })
local handle = InstallHandleGenerator "dummy"
local ctx = InstallContextGenerator(handle)
installer.prepare_installer(ctx)
installer.prepare_installer(ctx):get_or_throw()
local source = installer.exec_in_context(ctx, function()
return github.release_version {
repo = "williamboman/mason.nvim",
Expand Down
Loading

0 comments on commit 37e9c1c

Please sign in to comment.