fix(install): prefer the musl asset on NixOS and other non-FHS hosts
CI / editor tests (push) Successful in 1m28s

NixOS keeps glibc in the store, so a generic `-gnu` release binary cannot
find its dynamic loader. The old detection missed this twice over: `ldd
--version` reports plain GNU libc there, and `/lib64/ld-linux-x86-64.so.2`
does exist — as a stub that only prints an error and exits 127. The
installer downloaded the gnu asset, `executable()` returned 1, and the
binary could never start.

Detect non-FHS distributions (`/etc/NIXOS`, `ID=nixos`/`ID=guix` in
os-release) and pick the statically linked musl asset there. Turn the
single target triple into an ordered candidate list so glibc hosts fall
back to musl too, and verify the staged binary actually execs — exit 126
or 127 means the kernel or loader refused it — before it lands in `bin/`.

Applied to both installers (Lua and the Vim build hook, which the shell
test harnesses source). Adds `g:nuwiki_ls_target` to force a triple.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012qPCSWXxATFvjEpVxQMhG9
This commit is contained in:
2026-08-07 14:58:48 -03:00
parent 5375e30bda
commit 95c054ef80
4 changed files with 185 additions and 46 deletions
+85 -19
View File
@@ -3,8 +3,11 @@
-- Strategy:
-- 1. Look for `bin/nuwiki-ls[.exe]` next to the plugin.
-- 2. If missing, download a release asset for the current target from
-- nuwiki-rs.
-- nuwiki-rs, trying each candidate triple in turn and rejecting one the
-- host cannot actually exec.
-- 3. On failure, fall back to cloning nuwiki-rs and building with cargo.
--
-- Set `vim.g.nuwiki_ls_target` to force a specific release triple.
local M = {}
@@ -25,23 +28,80 @@ function M.is_installed()
return vim.fn.executable(M.expected_path()) == 1
end
local function target_triple()
local function is_musl(arch)
local uv = vim.uv or vim.loop
if uv.fs_stat('/lib/ld-musl-' .. arch .. '.so.1') then
return true
end
if vim.fn.executable('ldd') ~= 1 then
return false
end
-- musl's ldd writes its banner to stderr.
return vim.fn.system('ldd --version 2>&1'):lower():find('musl', 1, true) ~= nil
end
-- NixOS and Guix System are not FHS distributions: glibc lives in the store,
-- so the `/lib64/ld-linux-*.so.2` a generic `-gnu` binary is linked against is
-- either absent or — on NixOS — a stub that only prints an error and exits
-- 127. `ldd --version` still reports GNU libc there, so libc detection alone
-- picks the wrong asset. The musl builds are statically linked and need no
-- loader at all, so they run on these systems unchanged.
local function is_non_fhs_linux()
local uv = vim.uv or vim.loop
if uv.fs_stat('/etc/NIXOS') then
return true
end
local ok, lines = pcall(vim.fn.readfile, '/etc/os-release')
if not ok then
return false
end
for _, line in ipairs(lines) do
local id = line:match('^ID=%s*"?([%w%-%._]+)')
if id == 'nixos' or id == 'guix' then
return true
end
end
return false
end
-- Release triples to try, best first. More than one entry means the later
-- ones are fallbacks for a host that turns out not to be able to run the
-- first (see can_exec below).
function M.targets()
local override = vim.g.nuwiki_ls_target
if type(override) == 'string' and override ~= '' then
return { override }
end
if vim.fn.has('win32') == 1 then
return 'x86_64-pc-windows-msvc'
return { 'x86_64-pc-windows-msvc' }
elseif vim.fn.has('mac') == 1 then
if vim.fn.has('arm64') == 1 or jit and jit.arch == 'arm64' then
return 'aarch64-apple-darwin'
return { 'aarch64-apple-darwin' }
end
return 'x86_64-apple-darwin'
return { 'x86_64-apple-darwin' }
elseif vim.fn.has('linux') == 1 then
local uname = vim.uv and vim.uv.os_uname() or vim.loop.os_uname()
local musl = (vim.fn.system('ldd --version 2>&1'):find('musl') ~= nil)
if uname.machine == 'aarch64' or uname.machine == 'arm64' then
return musl and 'aarch64-unknown-linux-musl' or 'aarch64-unknown-linux-gnu'
local arch = (uname.machine == 'aarch64' or uname.machine == 'arm64')
and 'aarch64' or 'x86_64'
local musl = arch .. '-unknown-linux-musl'
if is_musl(arch) or is_non_fhs_linux() then
return { musl }
end
return musl and 'x86_64-unknown-linux-musl' or 'x86_64-unknown-linux-gnu'
-- Static musl runs anywhere, so keep it as a last resort on glibc hosts
-- too (old glibc, exotic loader layouts).
return { arch .. '-unknown-linux-gnu', musl }
end
return nil
return {}
end
-- Can this host actually start the binary? Exit codes 126 and 127 mean the
-- kernel or the dynamic loader refused to exec it (missing/stub loader, wrong
-- architecture); any other code means it ran and exited on its own terms.
-- system() gives the child an empty stdin, so the LSP loop sees EOF and stops.
local function can_exec(path)
vim.fn.system({ path, '--version' })
local code = vim.v.shell_error
return code ~= 126 and code ~= 127
end
local function build_from_source(dest)
@@ -80,11 +140,7 @@ local function build_from_source(dest)
return vim.fn.executable(dest) == 1
end
local function download_release(dest)
local target = target_triple()
if not target then
return false
end
local function download_release(dest, target)
if vim.fn.executable('curl') ~= 1 or vim.fn.executable('tar') ~= 1 then
return false
end
@@ -103,7 +159,14 @@ local function download_release(dest)
if vim.v.shell_error ~= 0 then
return false
end
vim.fn.system({ 'cp', extract .. '/nuwiki-ls' .. exe_suffix(), dest })
-- Vet the staged copy before it becomes the installed one, so a binary this
-- host cannot exec never lands at `dest` for the LSP client to pick up.
local staged = extract .. '/nuwiki-ls' .. exe_suffix()
vim.fn.system({ 'chmod', '+x', staged })
if vim.fn.executable(staged) ~= 1 or not can_exec(staged) then
return false
end
vim.fn.system({ 'cp', staged, dest })
vim.fn.system({ 'chmod', '+x', dest })
return vim.fn.executable(dest) == 1
end
@@ -112,9 +175,12 @@ function M.install()
local dest = M.expected_path()
vim.fn.mkdir(vim.fn.fnamemodify(dest, ':h'), 'p')
if download_release(dest) then
vim.notify('nuwiki: installed ' .. dest, vim.log.levels.INFO)
return true
for _, target in ipairs(M.targets()) do
if download_release(dest, target) then
vim.notify('nuwiki: installed ' .. dest .. ' (' .. target .. ')',
vim.log.levels.INFO)
return true
end
end
vim.notify('nuwiki: download failed, falling back to source build',
vim.log.levels.WARN)