|
| 1 | +--- Install the native module for a plugin-manager build step: download a |
| 2 | +--- prebuilt binary matching the installed release tag, and fall back to building |
| 3 | +--- from source when no matching prebuilt exists (e.g. tracking a branch) or the |
| 4 | +--- download fails. |
| 5 | +--- |
| 6 | +--- Use as a lazy.nvim build step: |
| 7 | +--- build = function() require("lockfile.download").download_or_build() end |
| 8 | +--- |
| 9 | +--- Prebuilt binaries are published per release tag (see .github/workflows). A |
| 10 | +--- prebuilt is only used when the checked-out HEAD is exactly at a release tag, |
| 11 | +--- so the binary always matches the source it ships with; otherwise the module |
| 12 | +--- is built from source. |
| 13 | + |
| 14 | +local M = {} |
| 15 | + |
| 16 | +--- GitHub "owner/repo" that publishes the release binaries. |
| 17 | +local REPO = "willothy/lockfile.nvim" |
| 18 | + |
| 19 | +--- Absolute path to the plugin root (the directory containing `lua/`). |
| 20 | +---@return string |
| 21 | +local function plugin_root() |
| 22 | + -- download.lua -> lockfile/ -> lua/ -> <root> |
| 23 | + return vim.fn.fnamemodify(debug.getinfo(1, "S").source:sub(2), ":h:h:h") |
| 24 | +end |
| 25 | + |
| 26 | +--- Whether the host is Windows. |
| 27 | +---@return boolean |
| 28 | +local function is_windows() |
| 29 | + return vim.fn.has("win32") == 1 |
| 30 | +end |
| 31 | + |
| 32 | +--- Extension of the loadable module file on this platform ("so" | "dll"). |
| 33 | +---@return string |
| 34 | +local function lib_extension() |
| 35 | + return is_windows() and "dll" or "so" |
| 36 | +end |
| 37 | + |
| 38 | +--- The Rust target triple for the current platform, or nil if unsupported. |
| 39 | +---@return string? |
| 40 | +local function target_triple() |
| 41 | + local uname = vim.uv.os_uname() |
| 42 | + local sysname = uname.sysname:lower() |
| 43 | + local machine = uname.machine:lower() |
| 44 | + |
| 45 | + local arch |
| 46 | + if machine == "x86_64" or machine == "amd64" then |
| 47 | + arch = "x86_64" |
| 48 | + elseif machine == "aarch64" or machine == "arm64" then |
| 49 | + arch = "aarch64" |
| 50 | + else |
| 51 | + return nil |
| 52 | + end |
| 53 | + |
| 54 | + if sysname:find("darwin", 1, true) then |
| 55 | + return arch .. "-apple-darwin" |
| 56 | + elseif sysname:find("windows", 1, true) or sysname:find("mingw", 1, true) then |
| 57 | + return arch .. "-pc-windows-msvc" |
| 58 | + elseif sysname:find("linux", 1, true) then |
| 59 | + -- Distinguish musl from glibc; the binaries are not interchangeable. |
| 60 | + local libc = "gnu" |
| 61 | + local ldd = vim.system({ "ldd", "--version" }):wait() |
| 62 | + local out = ((ldd.stdout or "") .. (ldd.stderr or "")):lower() |
| 63 | + if out:find("musl", 1, true) then |
| 64 | + libc = "musl" |
| 65 | + end |
| 66 | + return arch .. "-unknown-linux-" .. libc |
| 67 | + end |
| 68 | + return nil |
| 69 | +end |
| 70 | + |
| 71 | +--- Path where the loadable module must live for `native.lua` to find it. |
| 72 | +---@param root string |
| 73 | +---@return string |
| 74 | +local function installed_path(root) |
| 75 | + return root .. "/lua/lockfile_native." .. lib_extension() |
| 76 | +end |
| 77 | + |
| 78 | +--- Path of the freshly built library for a host `cargo build --release`. |
| 79 | +---@param root string |
| 80 | +---@return string |
| 81 | +local function built_path(root) |
| 82 | + local base = root .. "/target/release/" |
| 83 | + if is_windows() then |
| 84 | + return base .. "lockfile_native.dll" |
| 85 | + elseif vim.uv.os_uname().sysname:lower():find("darwin", 1, true) then |
| 86 | + return base .. "liblockfile_native.dylib" |
| 87 | + end |
| 88 | + return base .. "liblockfile_native.so" |
| 89 | +end |
| 90 | + |
| 91 | +--- The release tag at the current HEAD, or nil if HEAD is not exactly at a tag. |
| 92 | +---@param root string |
| 93 | +---@return string? |
| 94 | +local function release_tag(root) |
| 95 | + local res = vim.system({ "git", "-C", root, "describe", "--tags", "--exact-match", "HEAD" }):wait() |
| 96 | + if res.code == 0 then |
| 97 | + local tag = vim.trim(res.stdout or "") |
| 98 | + if tag ~= "" then |
| 99 | + return tag |
| 100 | + end |
| 101 | + end |
| 102 | + return nil |
| 103 | +end |
| 104 | + |
| 105 | +--- Download `url` to `out` with curl. Returns ok, error message. |
| 106 | +---@param url string |
| 107 | +---@param out string |
| 108 | +---@return boolean ok |
| 109 | +---@return string? err |
| 110 | +local function curl(url, out) |
| 111 | + if vim.fn.executable("curl") == 0 then |
| 112 | + return false, "curl is not available" |
| 113 | + end |
| 114 | + local res = vim.system({ |
| 115 | + "curl", |
| 116 | + "--fail", |
| 117 | + "--location", |
| 118 | + "--silent", |
| 119 | + "--show-error", |
| 120 | + "--output", |
| 121 | + out, |
| 122 | + url, |
| 123 | + }):wait() |
| 124 | + if res.code ~= 0 then |
| 125 | + return false, vim.trim(res.stderr or "download failed") |
| 126 | + end |
| 127 | + return true, nil |
| 128 | +end |
| 129 | + |
| 130 | +--- Try to download and install a prebuilt binary for `tag`. Returns ok, err. |
| 131 | +---@param root string |
| 132 | +---@param tag string |
| 133 | +---@param triple string |
| 134 | +---@return boolean ok |
| 135 | +---@return string? err |
| 136 | +local function install_prebuilt(root, tag, triple) |
| 137 | + local asset = ("lockfile_native-%s.%s"):format(triple, lib_extension()) |
| 138 | + local url = ("https://github.com/%s/releases/download/%s/%s"):format(REPO, tag, asset) |
| 139 | + local dest = installed_path(root) |
| 140 | + local tmp = dest .. ".tmp" |
| 141 | + |
| 142 | + local ok, err = curl(url, tmp) |
| 143 | + if not ok then |
| 144 | + return false, err |
| 145 | + end |
| 146 | + |
| 147 | + -- Validate the download actually loads before swapping it in. |
| 148 | + local loader = package.loadlib(tmp, "luaopen_lockfile_native") |
| 149 | + if not loader then |
| 150 | + pcall(vim.uv.fs_unlink, tmp) |
| 151 | + return false, "downloaded binary failed to load" |
| 152 | + end |
| 153 | + |
| 154 | + local renamed, rename_err = vim.uv.fs_rename(tmp, dest) |
| 155 | + if not renamed then |
| 156 | + if is_windows() then |
| 157 | + -- The live DLL may be locked by this session; leave the validated .tmp |
| 158 | + -- for the next restart to promote. |
| 159 | + vim.notify( |
| 160 | + "lockfile.nvim: downloaded binary saved to " .. tmp .. "; restart Neovim to apply.", |
| 161 | + vim.log.levels.WARN |
| 162 | + ) |
| 163 | + return true, nil |
| 164 | + end |
| 165 | + pcall(vim.uv.fs_unlink, tmp) |
| 166 | + return false, "failed to install binary: " .. tostring(rename_err) |
| 167 | + end |
| 168 | + return true, nil |
| 169 | +end |
| 170 | + |
| 171 | +--- Build the native module from source and copy it into place. |
| 172 | +---@param root string? |
| 173 | +function M.build(root) |
| 174 | + root = root or plugin_root() |
| 175 | + if vim.fn.executable("cargo") == 0 then |
| 176 | + error( |
| 177 | + "lockfile.nvim: no prebuilt binary for this platform and `cargo` was not found " |
| 178 | + .. "to build from source. Install Rust from https://rustup.rs/", |
| 179 | + 0 |
| 180 | + ) |
| 181 | + end |
| 182 | + |
| 183 | + local res = vim.system({ "cargo", "build", "--release" }, { cwd = root }):wait() |
| 184 | + if res.code ~= 0 then |
| 185 | + error("lockfile.nvim: `cargo build --release` failed:\n" .. (res.stderr or ""), 0) |
| 186 | + end |
| 187 | + |
| 188 | + local dest = installed_path(root) |
| 189 | + local ok, err = vim.uv.fs_copyfile(built_path(root), dest) |
| 190 | + if not ok then |
| 191 | + error("lockfile.nvim: failed to copy built binary to " .. dest .. ": " .. tostring(err), 0) |
| 192 | + end |
| 193 | + vim.notify("lockfile.nvim: built native module from source.") |
| 194 | +end |
| 195 | + |
| 196 | +--- Install the native module: prefer a prebuilt binary matching the installed |
| 197 | +--- release tag, otherwise build from source. |
| 198 | +function M.download_or_build() |
| 199 | + local root = plugin_root() |
| 200 | + local tag = release_tag(root) |
| 201 | + local triple = target_triple() |
| 202 | + |
| 203 | + if tag and triple then |
| 204 | + local ok, err = install_prebuilt(root, tag, triple) |
| 205 | + if ok then |
| 206 | + vim.notify(("lockfile.nvim: installed prebuilt binary for %s (%s)."):format(triple, tag)) |
| 207 | + return |
| 208 | + end |
| 209 | + vim.notify( |
| 210 | + ("lockfile.nvim: prebuilt unavailable (%s); building from source."):format(err or "unknown"), |
| 211 | + vim.log.levels.INFO |
| 212 | + ) |
| 213 | + end |
| 214 | + |
| 215 | + M.build(root) |
| 216 | +end |
| 217 | + |
| 218 | +return M |
0 commit comments