1b279d4d10
e2e Tests / e2e (macos-latest) (push) Waiting to run
e2e Tests / e2e (windows-latest) (push) Waiting to run
Nix CI / check (push) Waiting to run
Python CI / Python bindings (macos-latest) (push) Waiting to run
Python CI / Python bindings (windows-latest) (push) Waiting to run
Build & Publish / Build Neovim aarch64-apple-darwin (push) Waiting to run
Build & Publish / Build Neovim aarch64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build Neovim x86_64-apple-darwin (push) Waiting to run
Build & Publish / Build Neovim x86_64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build C FFI aarch64-apple-darwin (push) Waiting to run
Build & Publish / Build C FFI aarch64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build C FFI x86_64-apple-darwin (push) Waiting to run
Build & Publish / Build C FFI x86_64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build MCP x86_64-apple-darwin (push) Waiting to run
Build & Publish / Build MCP x86_64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build Python wheels aarch64 (macos-latest) (push) Waiting to run
Build & Publish / Build Python wheels x86_64 (macos-latest) (push) Waiting to run
Build & Publish / Build Python wheels x86_64 (windows-latest) (push) Waiting to run
Build & Publish / Release (push) Blocked by required conditions
Build & Publish / Publish Python wheels to PyPI (push) Blocked by required conditions
Build & Publish / Publish Rust crates (push) Blocked by required conditions
Build & Publish / Publish npm packages (push) Blocked by required conditions
Rust CI / Test (macos-latest) (push) Waiting to run
Rust CI / Test (windows-latest) (push) Waiting to run
Rust CI / Fuzz Tests (macos-latest) (push) Waiting to run
Rust CI / Fuzz Tests (windows-latest) (push) Waiting to run
Build & Publish / Build MCP aarch64-apple-darwin (push) Waiting to run
Build & Publish / Build MCP aarch64-pc-windows-msvc (push) Waiting to run
Lua CI / lua-language-server type check (push) Failing after 1s
Lua CI / luacheck lint (push) Failing after 1s
Python CI / Python bindings (ubuntu-latest) (push) Failing after 1s
Build & Publish / Build Neovim aarch64-linux-android (push) Failing after 3s
Build & Publish / Build Neovim aarch64-unknown-linux-gnu (push) Failing after 0s
Build & Publish / Build Neovim aarch64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build Neovim x86_64-unknown-linux-gnu (push) Failing after 1s
Build & Publish / Build Neovim x86_64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build C FFI aarch64-linux-android (push) Failing after 1s
Build & Publish / Build C FFI aarch64-unknown-linux-gnu (push) Failing after 1s
Build & Publish / Build C FFI aarch64-unknown-linux-musl (push) Failing after 0s
Build & Publish / Build C FFI x86_64-unknown-linux-gnu (push) Failing after 1s
Build & Publish / Build C FFI x86_64-unknown-linux-musl (push) Failing after 3s
Build & Publish / Build MCP aarch64-unknown-linux-gnu (push) Failing after 1s
Build & Publish / Build MCP aarch64-unknown-linux-musl (push) Failing after 0s
Build & Publish / Build MCP x86_64-unknown-linux-gnu (push) Failing after 2s
Rust CI / Fuzz Tests (ubuntu-latest) (push) Failing after 1s
Rust CI / Build i686-unknown-linux-gnu (push) Failing after 1s
Rust CI / cargo fmt (push) Failing after 1s
Build & Publish / Build MCP x86_64-unknown-linux-musl (push) Failing after 4s
Build & Publish / Build Python wheels aarch64 (ubuntu-latest) (push) Failing after 1s
Build & Publish / Build Python wheels x86_64 (ubuntu-latest) (push) Failing after 1s
Build & Publish / Build Python sdist (push) Failing after 0s
Rust CI / Test (ubuntu-latest) (push) Failing after 1s
Rust CI / cargo clippy (push) Failing after 1s
Spelling / Spell Check with Typos (push) Failing after 1s
Stylua / Check lua files using Stylua (push) Failing after 1s
e2e Tests / e2e (ubuntu-latest) (push) Failing after 4s
e2e Tests / e2e (alpine-musl) (push) Successful in 58m54s
290 lines
9.4 KiB
Lua
290 lines
9.4 KiB
Lua
local M = {}
|
|
local system = require('fff.utils.system')
|
|
local fs_utils = require('fff.utils.fs')
|
|
local fff_version = require('fff.utils.version')
|
|
|
|
local GITHUB_REPO = 'dmtrKovalenko/fff.nvim'
|
|
|
|
local function get_binary_dir(plugin_dir) return plugin_dir .. '/../target/release' end
|
|
|
|
local function get_binary_path(plugin_dir)
|
|
local binary_dir = get_binary_dir(plugin_dir)
|
|
local extension = system.get_lib_extension()
|
|
return binary_dir .. '/libfff_nvim.' .. extension
|
|
end
|
|
|
|
local function binary_exists(plugin_dir)
|
|
local binary_path = get_binary_path(plugin_dir)
|
|
local stat = vim.uv.fs_stat(binary_path)
|
|
if stat and stat.type == 'file' then return true end
|
|
|
|
-- On Windows the rename over a loaded DLL fails, so a verified binary may be
|
|
-- left at binary_path .. '.tmp'. Promote it now that the old session is gone.
|
|
local tmp_path = binary_path .. '.tmp'
|
|
local tmp_stat = vim.uv.fs_stat(tmp_path)
|
|
if tmp_stat and tmp_stat.type == 'file' then
|
|
-- Verify the .tmp is a valid library before promoting it, in case the
|
|
-- process was killed between the loadlib check and the rename attempt
|
|
-- during a previous download, leaving a corrupt or partial .tmp on disk.
|
|
local loader = package.loadlib(tmp_path, 'luaopen_fff_nvim')
|
|
if not loader then
|
|
vim.uv.fs_unlink(tmp_path)
|
|
return false
|
|
end
|
|
local ok = vim.uv.fs_rename(tmp_path, binary_path)
|
|
return ok ~= nil
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
local function download_file(url, output_path, opts, callback)
|
|
opts = opts or {}
|
|
|
|
local dir = vim.fn.fnamemodify(output_path, ':h')
|
|
fs_utils.mkdir_recursive(dir, function(mkdir_ok, mkdir_err)
|
|
if not mkdir_ok then
|
|
callback(false, mkdir_err)
|
|
return
|
|
end
|
|
|
|
local curl_args = {
|
|
'curl',
|
|
'--fail',
|
|
'--location',
|
|
'--silent',
|
|
'--show-error',
|
|
'--output',
|
|
output_path,
|
|
}
|
|
|
|
if opts.proxy then
|
|
table.insert(curl_args, '--proxy')
|
|
table.insert(curl_args, opts.proxy)
|
|
end
|
|
|
|
if opts.extra_curl_args then
|
|
for _, arg in ipairs(opts.extra_curl_args) do
|
|
table.insert(curl_args, arg)
|
|
end
|
|
end
|
|
|
|
table.insert(curl_args, url)
|
|
vim.system(curl_args, {}, function(result)
|
|
if result.code ~= 0 then
|
|
callback(false, 'Failed to download: ' .. (result.stderr or 'unknown error'))
|
|
return
|
|
end
|
|
callback(true, nil)
|
|
end)
|
|
end)
|
|
end
|
|
|
|
local function download_from_github(version, binary_path, opts, callback)
|
|
opts = opts or {}
|
|
|
|
local triple = system.get_triple()
|
|
local extension = system.get_lib_extension()
|
|
local binary_name = triple .. '.' .. extension
|
|
local url = string.format('https://github.com/%s/releases/download/%s/%s', GITHUB_REPO, version, binary_name)
|
|
|
|
vim.schedule(function()
|
|
vim.notify(string.format('Downloading fff.nvim binary for ' .. version), vim.log.levels.INFO)
|
|
vim.notify(string.format('Do not open fff until you see a success notification.'), vim.log.levels.WARN)
|
|
end)
|
|
|
|
-- Download to a temp path first so we can validate before replacing the live binary.
|
|
-- If we wrote directly to binary_path and the current process already has the old
|
|
-- library loaded, package.loadlib() on the same path returns the *cached* handle —
|
|
-- meaning a truncated download would pass validation silently.
|
|
-- Using a distinct temp path forces dlopen to load the new file for real.
|
|
local tmp_path = binary_path .. '.tmp'
|
|
|
|
download_file(url, tmp_path, {
|
|
proxy = opts.proxy,
|
|
extra_curl_args = opts.extra_curl_args,
|
|
}, function(success, err)
|
|
if not success then
|
|
vim.uv.fs_unlink(tmp_path)
|
|
callback(false, err)
|
|
return
|
|
end
|
|
|
|
vim.schedule(function()
|
|
-- Validate the downloaded binary by actually loading it (temp path is not yet
|
|
-- loaded by this process, so dlopen loads the new file for real and catches
|
|
-- truncated or corrupt downloads).
|
|
-- Note: package.loadlib returns (nil, error_string) on failure rather than throwing.
|
|
local loader, load_err = package.loadlib(tmp_path, 'luaopen_fff_nvim')
|
|
|
|
if not loader then
|
|
vim.uv.fs_unlink(tmp_path)
|
|
callback(false, 'Downloaded binary is not valid: ' .. (load_err or 'unknown error'))
|
|
return
|
|
end
|
|
|
|
-- Atomically replace the live binary only after successful validation.
|
|
-- On Windows the old .dll may be locked by the current process, so rename can
|
|
-- fail if fff is already loaded. In that case, leave the verified .tmp on disk
|
|
-- so the next Neovim start can pick it up automatically.
|
|
local rename_ok, rename_err = vim.uv.fs_rename(tmp_path, binary_path)
|
|
if not rename_ok then
|
|
if vim.uv.os_uname().sysname:lower():match('windows') then
|
|
vim.notify(
|
|
'fff.nvim binary downloaded to '
|
|
.. tmp_path
|
|
.. '.\nThe live binary is locked by the current session — please restart Neovim to apply the update.',
|
|
vim.log.levels.WARN
|
|
)
|
|
callback(true, nil)
|
|
else
|
|
vim.uv.fs_unlink(tmp_path)
|
|
callback(false, 'Failed to install binary: ' .. (rename_err or 'unknown error'))
|
|
end
|
|
return
|
|
end
|
|
|
|
vim.notify('fff.nvim binary downloaded successfully!', vim.log.levels.INFO)
|
|
callback(true, nil)
|
|
end)
|
|
end)
|
|
end
|
|
|
|
function M.ensure_downloaded(opts, callback)
|
|
opts = opts or {}
|
|
local plugin_dir = vim.fn.fnamemodify(debug.getinfo(1, 'S').source:sub(2), ':h:h')
|
|
|
|
if binary_exists(plugin_dir) and not opts.force then
|
|
callback(true, nil)
|
|
return
|
|
end
|
|
|
|
local function on_release_tag(release_tag)
|
|
if not release_tag then
|
|
callback(false, 'Could not determine target version')
|
|
return
|
|
end
|
|
|
|
local binary_path = get_binary_path(plugin_dir)
|
|
download_from_github(release_tag, binary_path, opts, callback)
|
|
end
|
|
|
|
if opts.version then
|
|
on_release_tag(opts.version)
|
|
else
|
|
-- plugin_dir is <repo>/lua; parent is the repo root
|
|
local repo_root = vim.fn.fnamemodify(plugin_dir, ':h')
|
|
|
|
-- 1. Try reading the CI-created tag on HEAD (no version computation)
|
|
local tag = fff_version.current_release_tag(repo_root)
|
|
if tag then
|
|
on_release_tag(tag)
|
|
return
|
|
end
|
|
|
|
-- 2. No local tag — construct the nightly version (bumps patch so
|
|
-- the prerelease is higher than Cargo.toml base in semver)
|
|
local info, err = fff_version.resolve(repo_root)
|
|
if info then
|
|
on_release_tag(info.release_tag)
|
|
return
|
|
end
|
|
|
|
callback(false, err or 'Could not determine target version')
|
|
end
|
|
end
|
|
|
|
function M.download_binary(callback)
|
|
M.ensure_downloaded({ force = true }, function(success, err)
|
|
if not success then
|
|
if callback then
|
|
callback(false, err)
|
|
else
|
|
vim.schedule(
|
|
function()
|
|
vim.notify('Failed to download fff.nvim binary: ' .. (err or 'unknown error'), vim.log.levels.ERROR)
|
|
end
|
|
)
|
|
end
|
|
return
|
|
end
|
|
if callback then callback(true, nil) end
|
|
end)
|
|
end
|
|
|
|
function M.build_binary(callback)
|
|
local plugin_dir = vim.fn.fnamemodify(debug.getinfo(1, 'S').source:sub(2), ':h:h')
|
|
local has_rustup = vim.fn.executable('rustup') == 1
|
|
if not has_rustup then
|
|
callback(
|
|
false,
|
|
'rustup is not found. It is required to build the fff.nvim binary. Install it from https://rustup.rs/'
|
|
)
|
|
return
|
|
end
|
|
|
|
vim.system({ 'cargo', 'build', '--release' }, { cwd = plugin_dir }, function(result)
|
|
if result.code ~= 0 then
|
|
callback(false, 'Failed to build rust binary: ' .. (result.stderr or 'unknown error'))
|
|
return
|
|
end
|
|
callback(true, nil)
|
|
end)
|
|
end
|
|
|
|
function M.download_or_build_binary()
|
|
local done = false
|
|
local fatal_error = nil
|
|
|
|
M.ensure_downloaded({ force = true }, function(download_success, download_error)
|
|
if download_success then
|
|
done = true
|
|
return
|
|
end
|
|
|
|
vim.schedule(
|
|
function()
|
|
vim.notify(
|
|
'Error downloading binary: ' .. (download_error or 'unknown error') .. '\nTrying cargo build --release\n',
|
|
vim.log.levels.WARN
|
|
)
|
|
end
|
|
)
|
|
|
|
M.build_binary(function(build_success, build_error)
|
|
if not build_success then
|
|
fatal_error = 'Failed to build fff.nvim binary. Build error: ' .. (build_error or 'unknown error')
|
|
else
|
|
vim.schedule(function() vim.notify('fff.nvim binary built successfully!', vim.log.levels.INFO) end)
|
|
end
|
|
done = true
|
|
end)
|
|
end)
|
|
|
|
-- Block the caller (and keep the Neovim event loop alive) until the entire
|
|
-- download-or-build chain finishes. This is critical for lazy.nvim build
|
|
-- hooks: lazy returns from the hook immediately after this function returns,
|
|
-- and if Neovim exits before the final rename(tmp → libfff_nvim.{dylib,so,dll})
|
|
-- executes, the binary is never written to disk. vim.wait pumps the event
|
|
-- loop so all vim.system / vim.schedule callbacks can fire.
|
|
local timeout_ms = 1000 * 60 * 2 -- 2 minutes
|
|
local ok, wait_err = vim.wait(timeout_ms, function() return done end, 100)
|
|
if not ok and wait_err == -2 then error('fff.nvim: download_or_build_binary timed out') end
|
|
|
|
if fatal_error then error(fatal_error) end
|
|
end
|
|
|
|
function M.get_binary_path()
|
|
local plugin_dir = vim.fn.fnamemodify(debug.getinfo(1, 'S').source:sub(2), ':h:h')
|
|
return get_binary_path(plugin_dir)
|
|
end
|
|
|
|
function M.get_binary_cpath_component()
|
|
local plugin_dir = vim.fn.fnamemodify(debug.getinfo(1, 'S').source:sub(2), ':h:h')
|
|
local binary_dir = get_binary_dir(plugin_dir)
|
|
local extension = system.get_lib_extension()
|
|
return binary_dir .. '/lib?.' .. extension
|
|
end
|
|
|
|
return M
|