aboutsummaryrefslogtreecommitdiffstats
path: root/lua/mason-core/managers/cargo/init.lua
blob: 07883cbf997eab3e687d6a68281f4047dd356b1a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
local path = require "mason-core.path"
local platform = require "mason-core.platform"
local spawn = require "mason-core.spawn"
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)
    return _.append(executable, { "bin" })
end, _.if_else(_.always(platform.is.win), _.format "%s.exe", _.identity))

---@param crate string
local function with_receipt(crate)
    return function()
        local ctx = installer.context()
        ctx.receipt:with_primary_source(ctx.receipt.cargo(crate))
    end
end

local M = {}

---@async
---@param crate string The crate to install.
---@param opts { git: { url: string, tag: boolean? }, features: string?, bin: string[]? }?
function M.crate(crate, opts)
    return function()
        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: { url: string, tag: boolean? }, features: string?, bin: string[]? }?
function M.install(crate, opts)
    local ctx = installer.context()
    opts = opts or {}

    local version

    if opts.git then
        if opts.git.tag then
            assert(ctx.requested_version:is_present(), "version is required when installing tagged git crate.")
        end
        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",
        version,
        opts.git and { "--git", opts.git.url } or vim.NIL,
        opts.features and { "--features", opts.features } or vim.NIL,
        crate,
    }

    if opts.bin then
        _.each(function(bin)
            ctx:link_bin(bin, get_bin_path(bin))
        end, opts.bin)
    end

    return {
        with_receipt = with_receipt(crate),
    }
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, 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 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 install_dir string
---@return Result # Result<table<string, InstalledCrate>>
local function get_installed_crates(install_dir)
    return spawn
        .cargo({
            "install",
            "--list",
            "--root",
            ".",
            cwd = install_dir,
        })
        :map_catching(function(result)
            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)
    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(_.prop "version")
        :ok_or(_.always "Failed to find cargo package version.")
end

return M