Skip to content

Commit

Permalink
feat(cargo): improve handling of git-based crates
Browse files Browse the repository at this point in the history
This is all pretty overkill, especially considering the small amount of
packages based on git-based crates.
  • Loading branch information
williamboman committed Oct 5, 2022
1 parent 45606b0 commit 1b8d00b
Show file tree
Hide file tree
Showing 11 changed files with 369 additions and 86 deletions.
167 changes: 118 additions & 49 deletions lua/mason-core/managers/cargo/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ local a = require "mason-core.async"
local Optional = require "mason-core.optional"
local installer = require "mason-core.installer"
local client = require "mason-core.managers.cargo.client"
local github = require "mason-core.managers.github"
local github_client = require "mason-core.managers.github.client"
local _ = require "mason-core.functional"

local get_bin_path = _.compose(path.concat, function(executable)
Expand All @@ -23,46 +25,61 @@ local M = {}

---@async
---@param crate string The crate to install.
---@param opts {git: boolean | string, features: string?, bin: string[]? }?
---@param opts { git: { url: string, tag: boolean? }, features: string?, bin: string[]? }?
function M.crate(crate, opts)
return function()
M.install(crate, opts).with_receipt()
if opts and opts.git and opts.git.tag then
local ctx = installer.context()
local repo = assert(opts.git.url:match "^https://github%.com/(.+)$", "git url needs to be github.com")
local source = github.tag { repo = repo }
source.with_receipt()
ctx.requested_version = Optional.of(source.tag)
M.install(crate, opts)
else
M.install(crate, opts).with_receipt()
end
end
end

---@async
---@param crate string The crate to install.
---@param opts {git: boolean | string, features: string?, bin: string[]? }?
---@param opts { git: { url: string, tag: boolean? }, features: string?, bin: string[]? }?
function M.install(crate, opts)
local ctx = installer.context()
opts = opts or {}
ctx.requested_version:if_present(function()
assert(not opts.git, "Providing a version when installing a git crate is not allowed.")
end)

---@type string | string[]
local final_crate = crate
local version

if opts.git then
final_crate = { "--git" }
if type(opts.git) == "string" then
table.insert(final_crate, opts.git)
if opts.git.tag then
assert(ctx.requested_version:is_present(), "version is required when installing tagged git crate.")
end
table.insert(final_crate, crate)
version = ctx.requested_version
:map(function(version)
if opts.git.tag then
return { "--tag", version }
else
return { "--rev", version }
end
end)
:or_else(vim.NIL)
else
version = ctx.requested_version
:map(function(version)
return { "--version", version }
end)
:or_else(vim.NIL)
end

ctx.spawn.cargo {
"install",
"--root",
".",
"--locked",
ctx.requested_version
:map(function(version)
return { "--version", version }
end)
:or_else(vim.NIL),
version,
opts.git and { "--git", opts.git.url } or vim.NIL,
opts.features and { "--features", opts.features } or vim.NIL,
final_crate,
crate,
}

if opts.bin then
Expand All @@ -76,42 +93,39 @@ function M.install(crate, opts)
}
end

---@alias InstalledCrate { name: string, version: string, github_ref: { owner: string, repo: string, ref: string }? }

---@param line string
---@return InstalledCrate? crate
local function parse_installed_crate(line)
local name, version, context = line:match "^(.+)%s+v([^%s:]+) ?(.*):$"
if context then
local owner, repo, ref = context:match "^%(https://github%.com/(.+)/([^?]+).*#(.+)%)$"
if ref then
return { name = name, version = ref, github_ref = { owner = owner, repo = repo, ref = ref } }
end
end
if name and version then
return { name = name, version = version }
end
end

---@param output string The `cargo install --list` output.
---@return table<string, string> # Key is the crate name, value is its version.
---@return table<string, InstalledCrate> # Key is the crate name, value is its version.
function M.parse_installed_crates(output)
local installed_crates = {}
for _, line in ipairs(vim.split(output, "\n")) do
local name, version = line:match "^(.+)%s+v([.%S]+)[%s:]"
if name and version then
installed_crates[name] = version
local installed_crate = parse_installed_crate(line)
if installed_crate then
installed_crates[installed_crate.name] = installed_crate
end
end
return installed_crates
end

---@async
---@param receipt InstallReceipt<InstallReceiptPackageSource>
---@param install_dir string
function M.check_outdated_primary_package(receipt, install_dir)
return M.get_installed_primary_package_version(receipt, install_dir):map_catching(function(installed_version)
---@type CrateResponse
local crate_response = client.fetch_crate(receipt.primary_source.package):get_or_throw()
if installed_version ~= crate_response.crate.max_stable_version then
return {
name = receipt.primary_source.package,
current_version = installed_version,
latest_version = crate_response.crate.max_stable_version,
}
else
error "Primary package is not outdated."
end
end)
end

---@async
---@param receipt InstallReceipt<InstallReceiptPackageSource>
---@param install_dir string
function M.get_installed_primary_package_version(receipt, install_dir)
local function get_installed_crates(install_dir)
return spawn
.cargo({
"install",
Expand All @@ -121,13 +135,68 @@ function M.get_installed_primary_package_version(receipt, install_dir)
cwd = install_dir,
})
:map_catching(function(result)
local installed_crates = M.parse_installed_crates(result.stdout)
if vim.in_fast_event() then
a.scheduler() -- needed because vim.fn.* call
end
local pkg = vim.fn.fnamemodify(receipt.primary_source.package, ":t")
return Optional.of_nilable(installed_crates[pkg]):or_else_throw "Failed to find cargo package version."
return M.parse_installed_crates(result.stdout)
end)
end

---@async
---@param receipt InstallReceipt<InstallReceiptPackageSource>
---@param install_dir string
function M.check_outdated_primary_package(receipt, install_dir)
if vim.in_fast_event() then
a.scheduler()
end
local crate_name = vim.fn.fnamemodify(receipt.primary_source.package, ":t")
return get_installed_crates(install_dir)
:ok()
:map(_.prop(crate_name))
:map(
---@param installed_crate InstalledCrate
function(installed_crate)
if installed_crate.github_ref then
---@type GitHubCommit
local latest_commit = github_client
.fetch_commits(
("%s/%s"):format(installed_crate.github_ref.owner, installed_crate.github_ref.repo),
{ page = 1, per_page = 1 }
)
:get_or_throw("Failed to fetch latest commits.")[1]
if not vim.startswith(latest_commit.sha, installed_crate.github_ref.ref) then
return {
name = receipt.primary_source.package,
current_version = installed_crate.github_ref.ref,
latest_version = latest_commit.sha,
}
end
else
---@type CrateResponse
local crate_response = client.fetch_crate(crate_name):get_or_throw()
if installed_crate.version ~= crate_response.crate.max_stable_version then
return {
name = receipt.primary_source.package,
current_version = installed_crate.version,
latest_version = crate_response.crate.max_stable_version,
}
end
end
end
)
:ok_or(_.always "Primary package is not outdated.")
end

---@async
---@param receipt InstallReceipt<InstallReceiptPackageSource>
---@param install_dir string
function M.get_installed_primary_package_version(receipt, install_dir)
return get_installed_crates(install_dir):map(function(pkgs)
if vim.in_fast_event() then
a.scheduler()
end
local pkg = vim.fn.fnamemodify(receipt.primary_source.package, ":t")
return Optional.of_nilable(pkgs[pkg])
:map(_.prop "version")
:or_else_throw "Failed to find cargo package version."
end)
end

return M
26 changes: 25 additions & 1 deletion lua/mason-core/managers/github/client.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,16 @@ local M = {}
---@alias GitHubReleaseAsset {url: string, id: integer, name: string, browser_download_url: string, created_at: string, updated_at: string, size: integer, download_count: integer}
---@alias GitHubRelease {tag_name: string, prerelease: boolean, draft: boolean, assets:GitHubReleaseAsset[]}
---@alias GitHubTag {name: string}
---@alias GitHubCommit {sha: string}

---@param path string
---@param opts { params: table<string, any>? }?
---@return Result # JSON decoded response.
local function api_call(path)
local function api_call(path, opts)
if opts and opts.params then
local params = _.join("&", _.map(_.join "=", _.sort_by(_.head, _.to_pairs(opts.params))))
path = ("%s?%s"):format(path, params)
end
return spawn
.gh({ "api", path, env = { CLICOLOR_FORCE = 0 } })
:map(_.prop "stdout")
Expand All @@ -21,6 +27,8 @@ local function api_call(path)
:map_catching(vim.json.decode)
end

M.api_call = api_call

---@async
---@param repo string The GitHub repo ("username/repo").
---@return Result # Result<GitHubRelease[]>
Expand Down Expand Up @@ -105,6 +113,22 @@ function M.fetch_latest_tag(repo)
:map(_.prop "tag")
end

---@async
---@param repo string The GitHub repo ("username/repo").
---@param opts { page: integer?, per_page: integer? }?
---@return Result # Result<GitHubCommit[]>
function M.fetch_commits(repo, opts)
local path = ("repos/%s/commits"):format(repo)
return api_call(path, {
params = {
page = opts and opts.page or 1,
per_page = opts and opts.per_page or 30,
},
}):map_err(function()
return ("Failed to fetch commits for GitHub repository %s."):format(repo)
end)
end

---@alias GitHubRateLimit {limit: integer, remaining: integer, reset: integer, used: integer}
---@alias GitHubRateLimitResponse {resources: { core: GitHubRateLimit }}

Expand Down
10 changes: 10 additions & 0 deletions lua/mason-core/optional.lua
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,14 @@ function Optional:is_present()
return self._value ~= nil
end

---@param err fun(): any
function Optional:ok_or(err)
local Result = require "mason-core.result"
if self:is_present() then
return Result.success(self:get())
else
return Result.failure(err())
end
end

return Optional
9 changes: 9 additions & 0 deletions lua/mason-core/result.lua
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,15 @@ function Result:on_success(fn)
return self
end

function Result:ok()
local Optional = require "mason-core.optional"
if self:is_success() then
return Optional.of(self.value)
else
return Optional.empty()
end
end

---@param fn fun(): any
---@return Result
function Result.run_catching(fn)
Expand Down
7 changes: 5 additions & 2 deletions lua/mason-registry/flux-lsp/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ return Pkg.new {
homepage = "https://github.com/influxdata/flux-lsp",
languages = { Pkg.Lang.Flux },
categories = { Pkg.Cat.LSP },
install = cargo.crate("https://github.com/influxdata/flux-lsp", {
git = true,
install = cargo.crate("flux-lsp", {
git = {
url = "https://github.com/influxdata/flux-lsp",
tag = true,
},
bin = { "flux-lsp" },
}),
}
4 changes: 3 additions & 1 deletion lua/mason-registry/move-analyzer/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ return Pkg.new {
languages = { Pkg.Lang.Move },
categories = { Pkg.Cat.LSP },
install = cargo.crate("move-analyzer", {
git = "https://github.com/move-language/move",
git = {
url = "https://github.com/move-language/move",
},
bin = { "move-analyzer" },
}),
}
9 changes: 5 additions & 4 deletions lua/mason-registry/wgsl-analyzer/init.lua
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
local Pkg = require "mason-core.package"
local cargo = require "mason-core.managers.cargo"

local github_url = "https://github.com/wgsl-analyzer/wgsl-analyzer"

return Pkg.new {
name = "wgsl-analyzer",
desc = [[A language server implementation for the WGSL shading language]],
homepage = github_url,
homepage = "https://github.com/wgsl-analyzer/wgsl-analyzer",
languages = { Pkg.Lang.WGSL },
categories = { Pkg.Cat.LSP },
install = cargo.crate("wgsl_analyzer", {
git = github_url,
git = {
url = "https://github.com/wgsl-analyzer/wgsl-analyzer",
tag = true,
},
bin = { "wgsl_analyzer" },
}),
}
Loading

0 comments on commit 1b8d00b

Please sign in to comment.