fix(install): prefer the musl asset on NixOS and other non-FHS hosts
CI / editor tests (push) Successful in 1m28s
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:
@@ -88,6 +88,13 @@ a Rust toolchain (1.83+ stable). You can run install manually any time with
|
|||||||
`:NuwikiInstall` (works in both Vim and Neovim) instead of relying on a
|
`:NuwikiInstall` (works in both Vim and Neovim) instead of relying on a
|
||||||
plugin-manager build hook.
|
plugin-manager build hook.
|
||||||
|
|
||||||
|
On Linux the installer picks the musl (statically linked) asset when the
|
||||||
|
host is musl-based or is not an FHS distribution — NixOS and Guix System
|
||||||
|
keep glibc in the store, so a generic `-gnu` binary cannot find its dynamic
|
||||||
|
loader there. Elsewhere it prefers the `-gnu` asset and falls back to musl
|
||||||
|
if the downloaded binary turns out not to run. Set `g:nuwiki_ls_target` to
|
||||||
|
a release triple (e.g. `x86_64-unknown-linux-musl`) to force the choice.
|
||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
- **Neovim 0.11+** — uses the built-in LSP client; no extra plugin.
|
- **Neovim 0.11+** — uses the built-in LSP client; no extra plugin.
|
||||||
@@ -462,6 +469,7 @@ For users configuring without Lua.
|
|||||||
| `g:nuwiki_no_folding` | `0` | `1` skips foldexpr setup |
|
| `g:nuwiki_no_folding` | `0` | `1` skips foldexpr setup |
|
||||||
| `g:nuwiki_mouse_mappings` | `0` | `1` enables mouse maps |
|
| `g:nuwiki_mouse_mappings` | `0` | `1` enables mouse maps |
|
||||||
| `g:nuwiki_binary_path` | _(auto)_ | path to a prebuilt `nuwiki-ls` binary, bypassing the bundled one |
|
| `g:nuwiki_binary_path` | _(auto)_ | path to a prebuilt `nuwiki-ls` binary, bypassing the bundled one |
|
||||||
|
| `g:nuwiki_ls_target` | _(auto)_ | force a release target triple at install time, e.g. `x86_64-unknown-linux-musl` |
|
||||||
| `g:nuwiki_no_calendar` | `0` | `1` opts out of calendar-vim integration |
|
| `g:nuwiki_no_calendar` | `0` | `1` opts out of calendar-vim integration |
|
||||||
| `g:nuwiki_auto_chdir` | `0` | `1` `:lcd` into the wiki root when a wiki buffer becomes current |
|
| `g:nuwiki_auto_chdir` | `0` | `1` `:lcd` into the wiki root when a wiki buffer becomes current |
|
||||||
| `g:nuwiki_auto_header` | `0` | `1` insert a level-1 header from the filename on new wiki pages |
|
| `g:nuwiki_auto_header` | `0` | `1` insert a level-1 header from the filename on new wiki pages |
|
||||||
|
|||||||
@@ -62,6 +62,14 @@ Highlights:
|
|||||||
Install via your plugin manager; the build hook downloads a pre-built
|
Install via your plugin manager; the build hook downloads a pre-built
|
||||||
`nuwiki-ls` binary into the plugin's `bin/` directory.
|
`nuwiki-ls` binary into the plugin's `bin/` directory.
|
||||||
|
|
||||||
|
On Linux the installer picks the musl (statically linked) asset when the
|
||||||
|
host is musl-based or is not an FHS distribution — NixOS and Guix System
|
||||||
|
keep glibc in the store, so a generic `-gnu` binary cannot find its dynamic
|
||||||
|
loader there. Elsewhere it prefers the `-gnu` asset and falls back to musl
|
||||||
|
if the downloaded binary turns out not to run. Set `g:nuwiki_ls_target`
|
||||||
|
*g:nuwiki_ls_target*
|
||||||
|
to a release triple (e.g. `x86_64-unknown-linux-musl`) to force the choice.
|
||||||
|
|
||||||
lazy.nvim: >
|
lazy.nvim: >
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
+85
-19
@@ -3,8 +3,11 @@
|
|||||||
-- Strategy:
|
-- Strategy:
|
||||||
-- 1. Look for `bin/nuwiki-ls[.exe]` next to the plugin.
|
-- 1. Look for `bin/nuwiki-ls[.exe]` next to the plugin.
|
||||||
-- 2. If missing, download a release asset for the current target from
|
-- 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.
|
-- 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 = {}
|
local M = {}
|
||||||
|
|
||||||
@@ -25,23 +28,80 @@ function M.is_installed()
|
|||||||
return vim.fn.executable(M.expected_path()) == 1
|
return vim.fn.executable(M.expected_path()) == 1
|
||||||
end
|
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
|
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
|
elseif vim.fn.has('mac') == 1 then
|
||||||
if vim.fn.has('arm64') == 1 or jit and jit.arch == 'arm64' then
|
if vim.fn.has('arm64') == 1 or jit and jit.arch == 'arm64' then
|
||||||
return 'aarch64-apple-darwin'
|
return { 'aarch64-apple-darwin' }
|
||||||
end
|
end
|
||||||
return 'x86_64-apple-darwin'
|
return { 'x86_64-apple-darwin' }
|
||||||
elseif vim.fn.has('linux') == 1 then
|
elseif vim.fn.has('linux') == 1 then
|
||||||
local uname = vim.uv and vim.uv.os_uname() or vim.loop.os_uname()
|
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)
|
local arch = (uname.machine == 'aarch64' or uname.machine == 'arm64')
|
||||||
if uname.machine == 'aarch64' or uname.machine == 'arm64' then
|
and 'aarch64' or 'x86_64'
|
||||||
return musl and 'aarch64-unknown-linux-musl' or 'aarch64-unknown-linux-gnu'
|
local musl = arch .. '-unknown-linux-musl'
|
||||||
|
if is_musl(arch) or is_non_fhs_linux() then
|
||||||
|
return { musl }
|
||||||
end
|
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
|
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
|
end
|
||||||
|
|
||||||
local function build_from_source(dest)
|
local function build_from_source(dest)
|
||||||
@@ -80,11 +140,7 @@ local function build_from_source(dest)
|
|||||||
return vim.fn.executable(dest) == 1
|
return vim.fn.executable(dest) == 1
|
||||||
end
|
end
|
||||||
|
|
||||||
local function download_release(dest)
|
local function download_release(dest, target)
|
||||||
local target = target_triple()
|
|
||||||
if not target then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
if vim.fn.executable('curl') ~= 1 or vim.fn.executable('tar') ~= 1 then
|
if vim.fn.executable('curl') ~= 1 or vim.fn.executable('tar') ~= 1 then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
@@ -103,7 +159,14 @@ local function download_release(dest)
|
|||||||
if vim.v.shell_error ~= 0 then
|
if vim.v.shell_error ~= 0 then
|
||||||
return false
|
return false
|
||||||
end
|
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 })
|
vim.fn.system({ 'chmod', '+x', dest })
|
||||||
return vim.fn.executable(dest) == 1
|
return vim.fn.executable(dest) == 1
|
||||||
end
|
end
|
||||||
@@ -112,9 +175,12 @@ function M.install()
|
|||||||
local dest = M.expected_path()
|
local dest = M.expected_path()
|
||||||
vim.fn.mkdir(vim.fn.fnamemodify(dest, ':h'), 'p')
|
vim.fn.mkdir(vim.fn.fnamemodify(dest, ':h'), 'p')
|
||||||
|
|
||||||
if download_release(dest) then
|
for _, target in ipairs(M.targets()) do
|
||||||
vim.notify('nuwiki: installed ' .. dest, vim.log.levels.INFO)
|
if download_release(dest, target) then
|
||||||
return true
|
vim.notify('nuwiki: installed ' .. dest .. ' (' .. target .. ')',
|
||||||
|
vim.log.levels.INFO)
|
||||||
|
return true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
vim.notify('nuwiki: download failed, falling back to source build',
|
vim.notify('nuwiki: download failed, falling back to source build',
|
||||||
vim.log.levels.WARN)
|
vim.log.levels.WARN)
|
||||||
|
|||||||
+84
-27
@@ -20,24 +20,71 @@ let s:rs_repo = fnamemodify(s:plugin_dir, ':h:h') . '/nuwiki-rs'
|
|||||||
|
|
||||||
call mkdir(s:bin_dir, 'p')
|
call mkdir(s:bin_dir, 'p')
|
||||||
|
|
||||||
function! s:target() abort
|
function! s:is_musl(arch) abort
|
||||||
if has('win32')
|
if filereadable('/lib/ld-musl-' . a:arch . '.so.1')
|
||||||
return 'x86_64-pc-windows-msvc'
|
return 1
|
||||||
elseif has('mac')
|
|
||||||
if has('arm64')
|
|
||||||
return 'aarch64-apple-darwin'
|
|
||||||
endif
|
|
||||||
return 'x86_64-apple-darwin'
|
|
||||||
elseif has('linux')
|
|
||||||
let l:ldd = system('ldd --version 2>&1')
|
|
||||||
let l:musl = stridx(l:ldd, 'musl') >= 0
|
|
||||||
let l:uname_m = trim(system('uname -m'))
|
|
||||||
if l:uname_m ==# 'aarch64' || l:uname_m ==# 'arm64'
|
|
||||||
return l:musl ? 'aarch64-unknown-linux-musl' : 'aarch64-unknown-linux-gnu'
|
|
||||||
endif
|
|
||||||
return l:musl ? 'x86_64-unknown-linux-musl' : 'x86_64-unknown-linux-gnu'
|
|
||||||
endif
|
endif
|
||||||
return ''
|
if !executable('ldd')
|
||||||
|
return 0
|
||||||
|
endif
|
||||||
|
" musl's ldd writes its banner to stderr.
|
||||||
|
return stridx(system('ldd --version 2>&1'), 'musl') >= 0
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
" 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.
|
||||||
|
function! s:is_non_fhs_linux() abort
|
||||||
|
if filereadable('/etc/NIXOS')
|
||||||
|
return 1
|
||||||
|
endif
|
||||||
|
if !filereadable('/etc/os-release')
|
||||||
|
return 0
|
||||||
|
endif
|
||||||
|
for l:line in readfile('/etc/os-release')
|
||||||
|
let l:id = matchstr(l:line, '^ID=\s*"\?\zs[[:alnum:]._-]\+')
|
||||||
|
if l:id ==# 'nixos' || l:id ==# 'guix'
|
||||||
|
return 1
|
||||||
|
endif
|
||||||
|
endfor
|
||||||
|
return 0
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
" 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.
|
||||||
|
function! s:targets() abort
|
||||||
|
if get(g:, 'nuwiki_ls_target', '') !=# ''
|
||||||
|
return [g:nuwiki_ls_target]
|
||||||
|
endif
|
||||||
|
if has('win32')
|
||||||
|
return ['x86_64-pc-windows-msvc']
|
||||||
|
elseif has('mac')
|
||||||
|
return [has('arm64') ? 'aarch64-apple-darwin' : 'x86_64-apple-darwin']
|
||||||
|
elseif has('linux')
|
||||||
|
let l:uname_m = trim(system('uname -m'))
|
||||||
|
let l:arch = (l:uname_m ==# 'aarch64' || l:uname_m ==# 'arm64')
|
||||||
|
\ ? 'aarch64' : 'x86_64'
|
||||||
|
let l:musl = l:arch . '-unknown-linux-musl'
|
||||||
|
if s:is_musl(l:arch) || s:is_non_fhs_linux()
|
||||||
|
return [l:musl]
|
||||||
|
endif
|
||||||
|
" Static musl runs anywhere, so keep it as a last resort on glibc hosts
|
||||||
|
" too (old glibc, exotic loader layouts).
|
||||||
|
return [l:arch . '-unknown-linux-gnu', l:musl]
|
||||||
|
endif
|
||||||
|
return []
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
" 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.
|
||||||
|
function! s:can_exec(path) abort
|
||||||
|
call system(shellescape(a:path) . ' --version')
|
||||||
|
return v:shell_error != 126 && v:shell_error != 127
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
function! s:build_from_source() abort
|
function! s:build_from_source() abort
|
||||||
@@ -65,16 +112,12 @@ function! s:build_from_source() abort
|
|||||||
return executable(s:bin)
|
return executable(s:bin)
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
function! s:download_release() abort
|
function! s:download_release(target) abort
|
||||||
let l:target = s:target()
|
|
||||||
if empty(l:target)
|
|
||||||
return 0
|
|
||||||
endif
|
|
||||||
if !executable('curl') || !executable('tar')
|
if !executable('curl') || !executable('tar')
|
||||||
return 0
|
return 0
|
||||||
endif
|
endif
|
||||||
let l:url = 'https://code.gfran.co/gffranco/nuwiki-rs/releases/download/latest/nuwiki-ls-'
|
let l:url = 'https://code.gfran.co/gffranco/nuwiki-rs/releases/download/latest/nuwiki-ls-'
|
||||||
\ . l:target . '.tar.gz'
|
\ . a:target . '.tar.gz'
|
||||||
let l:archive = tempname() . '.tar.gz'
|
let l:archive = tempname() . '.tar.gz'
|
||||||
call system('curl -fsSL -o ' . shellescape(l:archive) . ' ' . shellescape(l:url))
|
call system('curl -fsSL -o ' . shellescape(l:archive) . ' ' . shellescape(l:url))
|
||||||
if v:shell_error != 0
|
if v:shell_error != 0
|
||||||
@@ -86,14 +129,28 @@ function! s:download_release() abort
|
|||||||
if v:shell_error != 0
|
if v:shell_error != 0
|
||||||
return 0
|
return 0
|
||||||
endif
|
endif
|
||||||
call system('cp ' . shellescape(l:extract . '/nuwiki-ls' . s:suffix) . ' ' . shellescape(s:bin))
|
" Vet the staged copy before it becomes the installed one, so a binary this
|
||||||
|
" host cannot exec never lands at s:bin for the LSP client to pick up.
|
||||||
|
let l:staged = l:extract . '/nuwiki-ls' . s:suffix
|
||||||
|
call system('chmod +x ' . shellescape(l:staged))
|
||||||
|
if !executable(l:staged) || !s:can_exec(l:staged)
|
||||||
|
return 0
|
||||||
|
endif
|
||||||
|
call system('cp ' . shellescape(l:staged) . ' ' . shellescape(s:bin))
|
||||||
call system('chmod +x ' . shellescape(s:bin))
|
call system('chmod +x ' . shellescape(s:bin))
|
||||||
return executable(s:bin)
|
return executable(s:bin)
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
if s:download_release()
|
let s:installed = 0
|
||||||
echom 'nuwiki: installed ' . s:bin
|
for s:t in s:targets()
|
||||||
else
|
if s:download_release(s:t)
|
||||||
|
echom 'nuwiki: installed ' . s:bin . ' (' . s:t . ')'
|
||||||
|
let s:installed = 1
|
||||||
|
break
|
||||||
|
endif
|
||||||
|
endfor
|
||||||
|
|
||||||
|
if !s:installed
|
||||||
echohl WarningMsg | echom 'nuwiki: download failed, falling back to source build' | echohl None
|
echohl WarningMsg | echom 'nuwiki: download failed, falling back to source build' | echohl None
|
||||||
if !s:build_from_source()
|
if !s:build_from_source()
|
||||||
echohl ErrorMsg | echom 'nuwiki: install failed' | echohl None
|
echohl ErrorMsg | echom 'nuwiki: install failed' | echohl None
|
||||||
|
|||||||
Reference in New Issue
Block a user