diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a6ab75..cee000d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.3.0] - 2026-04-11 + +### Added +- `scripts/install-neovim-src.sh`: standalone script to build the latest + stable neovim from source; intended for systems where glibc < 2.32 prevents + running prebuilt GitHub release binaries (e.g. Ubuntu 20.04); supports + `NEOVIM_TAG` (pin version) and `NEOVIM_PREFIX` (install path) env vars + +### Fixed +- `modules/neovim.sh`: detect glibc version *before* downloading — saves + ~100 MB download on incompatible systems; fall back to v0.9.5 tarball + (`nvim-linux64.tar.gz`, built on Ubuntu 18.04 CI, glibc 2.17+ baseline) + when system glibc < 2.32; verify binary executes before declaring success; + clean up any broken binary left by a prior failed install when the glibc + fallback triggers; ARM64 systems with glibc < 2.32 fall back to apt +- `nvim/init.lua`: gate `nvim-treesitter`, `nvim-lspconfig`, + `nvim-treesitter-context`, `mini.ai`, and `mini.bracketed` behind + `cond = vim.fn.has('nvim-0.10') == 1` — these plugins use nvim 0.10+ APIs + (`vim.fs.joinpath`, `LspRequest` event) or declare nvim < 0.10 soft- + deprecated; nvim 0.9.5 now starts cleanly with regex highlighting and no LSP +- `nvim/init.lua`: use the correct post-2024 nvim-treesitter install API + (`require('nvim-treesitter').install({...})`) — the old + `require('nvim-treesitter.install').ensure_installed` no longer exists after + the nvim-treesitter refactor + ## [1.2.5] - 2026-04-02 ### Added @@ -307,7 +332,8 @@ Complete overhaul of the dotfiles infrastructure: modular profiles, Neovim, CI, ### Added - Initial dotfiles: Zsh (oh-my-zsh + fzf), Tmux, Vim, and monolithic `install.sh` -[Unreleased]: https://github.com/YASoftwareDev/dotfiles/compare/v1.2.5...HEAD +[Unreleased]: https://github.com/YASoftwareDev/dotfiles/compare/v1.3.0...HEAD +[1.3.0]: https://github.com/YASoftwareDev/dotfiles/compare/v1.2.5...v1.3.0 [1.2.5]: https://github.com/YASoftwareDev/dotfiles/compare/v1.2.4...v1.2.5 [1.2.4]: https://github.com/YASoftwareDev/dotfiles/compare/v1.2.3...v1.2.4 [1.2.3]: https://github.com/YASoftwareDev/dotfiles/compare/v1.2.2...v1.2.3 diff --git a/VERSION b/VERSION index c813fe1..f0bb29e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.5 +1.3.0 diff --git a/modules/neovim.sh b/modules/neovim.sh index 97f4e6e..474652c 100755 --- a/modules/neovim.sh +++ b/modules/neovim.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash # Neovim: install latest stable release from GitHub (prebuilt tarball). -# Falls back to apt if GitHub is unreachable or arch is unsupported. +# Falls back to apt if GitHub is unreachable, arch is unsupported, or the +# prebuilt binary is incompatible with the system's glibc. # Idempotent: skips install when the installed version already matches latest. # Probe ~/.local/bin/nvim and ~/bin/nvim for older copies that would shadow @@ -92,11 +93,12 @@ install_neovim() { local prefix if $CAN_SUDO; then prefix="/usr/local"; else prefix="$HOME/.local"; fi - # Skip if already at latest version + # Skip if already at latest version AND binary actually runs. + # A version-matching but glibc-broken binary must not be skipped. if has nvim; then local current current=$(nvim --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) - if [ "$current" = "$latest" ]; then + if [ "$current" = "$latest" ] && "$prefix/bin/nvim" --version >/dev/null 2>&1; then log_ok "neovim $latest_tag already installed — skipping" # Shadow check still needed: `nvim` above may have resolved to a # user-local copy (e.g. ~/.local/bin/nvim) that shadows an existing @@ -110,6 +112,29 @@ install_neovim() { log_info "neovim: installing $latest_tag → $prefix" fi + # Early glibc check — avoids a needless ~100 MB download on old systems. + # Prebuilt binaries since v0.10.0 require glibc ≥ 2.32 (Ubuntu 22.04+). + # Detect before downloading; if too old, go straight to the legacy binary. + local glibc_ver + glibc_ver=$(ldd --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+$' || echo "0.0") + if _ver_older_than "$glibc_ver" "2.32"; then + log_warn "neovim: system glibc $glibc_ver < 2.32 — latest prebuilt incompatible" + # Remove any broken binary left by a prior failed install. + if [ -f "$prefix/bin/nvim" ] && ! "$prefix/bin/nvim" --version >/dev/null 2>&1; then + rm -f "$prefix/bin/nvim" + log_info "neovim: removed incompatible binary from $prefix/bin/nvim" + fi + # Keep any already-working compatible binary (e.g. v0.9.5 from a prior run). + if "$prefix/bin/nvim" --version >/dev/null 2>&1; then + local legacy_cur + legacy_cur=$("$prefix/bin/nvim" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) + log_ok "neovim $legacy_cur already installed (glibc-compatible) — skipping" + return + fi + [ "$arch" = "x86_64" ] && _neovim_legacy_binary "$prefix" || _neovim_apt + return + fi + if ! $CAN_SUDO; then mkdir -p "$prefix"; fi local tmp @@ -149,9 +174,48 @@ install_neovim() { if $CAN_SUDO; then _nvim_warn_shadows /usr/local/bin/nvim; fi } +# Download the last neovim release compatible with glibc < 2.32. +# v0.9.5 was built on Ubuntu 18.04 CI (glibc 2.17 baseline) and runs on any +# glibc ≥ 2.17. Asset name changed to nvim-linux-x86_64 at v0.10.0; v0.9.x +# used nvim-linux64. Only x86_64 is handled — ARM64 falls back to apt. +_neovim_legacy_binary() { + local prefix="$1" + local tag="v0.9.5" + local url="https://github.com/neovim/neovim/releases/download/${tag}/nvim-linux64.tar.gz" + log_info "neovim: downloading legacy $tag (glibc 2.17+ compatible)" + + local tmp + tmp=$(mktemp -d) + # shellcheck disable=SC2064 + trap "rm -rf '$tmp'" RETURN + + if has curl; then + curl -sfL "$url" | tar -xz -C "$tmp" || { log_warn "neovim: legacy download failed — falling back to apt"; _neovim_apt; return; } + else + wget -qO- "$url" | tar -xz -C "$tmp" || { log_warn "neovim: legacy download failed — falling back to apt"; _neovim_apt; return; } + fi + + local extracted="$tmp/nvim-linux64" + if [ ! -d "$extracted" ] || ! "$extracted/bin/nvim" --version >/dev/null 2>&1; then + log_warn "neovim: legacy binary not usable — falling back to apt" + _neovim_apt + return + fi + + if $CAN_SUDO; then + [ -n "${SUDO:-}" ] && sudo -v 2>/dev/null || true + $SUDO cp -r "$extracted"/. "$prefix/" + else + cp -r "$extracted"/. "$prefix/" + fi + log_ok "neovim legacy $tag installed → $prefix ($($prefix/bin/nvim --version 2>/dev/null | head -1))" +} + _neovim_apt() { if ! $CAN_SUDO; then log_warn "neovim: no sudo — cannot install via apt" + log_warn " Prebuilt GitHub binaries require glibc ≥ 2.32 (Ubuntu 22.04+)" + log_warn " Options: upgrade OS, or build neovim from source" return fi apt_install neovim diff --git a/nvim/.config/nvim/init.lua b/nvim/.config/nvim/init.lua index 540cd90..c3f4ff8 100644 --- a/nvim/.config/nvim/init.lua +++ b/nvim/.config/nvim/init.lua @@ -73,10 +73,13 @@ require('lazy').setup({ opts = { style = 'palenight' } }, -- darker/dark/palenight/oceanic -- ── LSP ────────────────────────────────────────────────────────────────── - -- blink.cmp is a dependency so capabilities are available in config + -- nvim-lspconfig ≥ 2024-12 requires nvim 0.10 at the plugin level (not just + -- API level), so gate the entire block. On nvim 0.9 the editor still works + -- fully; only LSP/completion is absent. { 'neovim/nvim-lspconfig', - lazy = false, + cond = vim.fn.has('nvim-0.10') == 1, + lazy = false, dependencies = { 'williamboman/mason.nvim', 'williamboman/mason-lspconfig.nvim', @@ -84,14 +87,13 @@ require('lazy').setup({ }, config = function() require('mason').setup() - require('mason-lspconfig').setup({ - ensure_installed = { 'pyright', 'clangd', 'bashls', 'lua_ls' }, - automatic_enable = false, -- we call vim.lsp.enable() ourselves below - }) - local capabilities = require('blink.cmp').get_lsp_capabilities() + -- blink.cmp requires nvim ≥ 0.10; fall back to plain capabilities on older. + local capabilities = vim.fn.has('nvim-0.10') == 1 + and require('blink.cmp').get_lsp_capabilities() + or vim.lsp.protocol.make_client_capabilities() - -- Single LspAttach autocmd covers all servers — no per-server on_attach needed + -- Single LspAttach autocmd covers all servers — no per-server on_attach needed. vim.api.nvim_create_autocmd('LspAttach', { group = vim.api.nvim_create_augroup('UserLspAttach', { clear = true }), callback = function(event) @@ -101,16 +103,22 @@ require('lazy').setup({ local map = function(key, fn, desc) vim.keymap.set('n', key, fn, vim.tbl_extend('force', opts, { desc = desc })) end - map('gd', vim.lsp.buf.definition, 'Go to definition') - map('gD', vim.lsp.buf.declaration, 'Go to declaration') - map('gr', vim.lsp.buf.references, 'References') - map('gi', vim.lsp.buf.implementation, 'Go to implementation') - map('K', vim.lsp.buf.hover, 'Hover docs') - map('rn', vim.lsp.buf.rename, 'Rename symbol') + map('gd', vim.lsp.buf.definition, 'Go to definition') + map('gD', vim.lsp.buf.declaration, 'Go to declaration') + map('gr', vim.lsp.buf.references, 'References') + map('gi', vim.lsp.buf.implementation, 'Go to implementation') + map('K', vim.lsp.buf.hover, 'Hover docs') + map('rn', vim.lsp.buf.rename, 'Rename symbol') map('ca', vim.lsp.buf.code_action, 'Code action') - map('d', vim.diagnostic.open_float, 'Show diagnostics') - map('[d', function() vim.diagnostic.jump({ count = -1 }) end, 'Prev diagnostic') - map(']d', function() vim.diagnostic.jump({ count = 1 }) end, 'Next diagnostic') + map('d', vim.diagnostic.open_float, 'Show diagnostics') + -- vim.diagnostic.jump() was added in nvim 0.10 + if vim.fn.has('nvim-0.10') == 1 then + map('[d', function() vim.diagnostic.jump({ count = -1 }) end, 'Prev diagnostic') + map(']d', function() vim.diagnostic.jump({ count = 1 }) end, 'Next diagnostic') + else + map('[d', vim.diagnostic.goto_prev, 'Prev diagnostic') + map(']d', vim.diagnostic.goto_next, 'Next diagnostic') + end -- LSP word highlight — replaces vim-illuminate (semantic, not regex) -- Use a buffer-keyed augroup so multiple servers attaching to the same @@ -131,34 +139,44 @@ require('lazy').setup({ end, }) - vim.lsp.config('pyright', { - capabilities = capabilities, - settings = { - python = { - analysis = { - useLibraryCodeForTypes = true, -- infer types from lib source when stubs absent + -- Server configs defined once; registration method differs by nvim version. + -- nvim 0.11+: vim.lsp.config/enable (new built-in API, no lspconfig on_attach) + -- nvim 0.9–0.10: lspconfig.server.setup() (classic API) + local servers = { + pyright = { + capabilities = capabilities, + settings = { python = { analysis = { useLibraryCodeForTypes = true } } }, + }, + clangd = { capabilities = capabilities }, + bashls = { capabilities = capabilities }, + lua_ls = { + capabilities = capabilities, + settings = { + Lua = { + runtime = { version = 'LuaJIT' }, + workspace = { library = { vim.env.VIMRUNTIME }, checkThirdParty = false }, + diagnostics = { globals = { 'vim' } }, }, }, }, - }) - for _, server in ipairs({ 'clangd', 'bashls' }) do - vim.lsp.config(server, { capabilities = capabilities }) + } + + if vim.fn.has('nvim-0.11') == 1 then + require('mason-lspconfig').setup({ + ensure_installed = vim.tbl_keys(servers), + automatic_enable = false, -- we call vim.lsp.enable() below + }) + for name, cfg in pairs(servers) do vim.lsp.config(name, cfg) end + vim.lsp.enable(vim.tbl_keys(servers)) + else + require('mason-lspconfig').setup({ ensure_installed = vim.tbl_keys(servers) }) + local lspconfig = require('lspconfig') + for name, cfg in pairs(servers) do lspconfig[name].setup(cfg) end end - vim.lsp.config('lua_ls', { - capabilities = capabilities, - settings = { - Lua = { - runtime = { version = 'LuaJIT' }, - workspace = { library = { vim.env.VIMRUNTIME }, checkThirdParty = false }, - diagnostics = { globals = { 'vim' } }, - }, - }, - }) - vim.lsp.enable({ 'pyright', 'clangd', 'bashls', 'lua_ls' }) vim.diagnostic.config({ severity_sort = true, - float = { border = 'rounded', source = true }, -- show server name per diagnostic + float = { border = 'rounded', source = true }, }) end, }, @@ -166,6 +184,7 @@ require('lazy').setup({ -- ── Completion ─────────────────────────────────────────────────────────── { 'saghen/blink.cmp', + cond = vim.fn.has('nvim-0.10') == 1, -- uses vim.snippet built-in (nvim 0.10+) version = '*', -- use release tags (pre-built Rust binary) dependencies = { 'rafamadriz/friendly-snippets' }, config = function() @@ -201,18 +220,27 @@ require('lazy').setup({ -- ── Treesitter ─────────────────────────────────────────────────────────── { 'nvim-treesitter/nvim-treesitter', + cond = vim.fn.has('nvim-0.10') == 1, -- uses vim.fs.joinpath (nvim 0.10+) lazy = false, build = ':TSUpdate', dependencies = { 'nvim-treesitter/nvim-treesitter-textobjects', { 'nvim-treesitter/nvim-treesitter-context', + cond = vim.fn.has('nvim-0.10') == 1, -- uses LspRequest event (nvim 0.10+) config = function() require('treesitter-context').setup({ max_lines = 3 }) end, }, }, config = function() + -- Ensure parsers are present on fresh installs (async, no-op if already installed). + require('nvim-treesitter').install({ + 'bash', 'c', 'cpp', 'css', 'go', 'html', 'javascript', + 'json', 'lua', 'markdown', 'python', 'rust', 'toml', + 'typescript', 'vim', 'yaml', + }) + -- Highlighting: built-in vim.treesitter, enabled per filetype vim.api.nvim_create_autocmd('FileType', { group = vim.api.nvim_create_augroup('UserTreesitter', { clear = true }), @@ -439,6 +467,7 @@ require('lazy').setup({ { 'andymass/vim-matchup', event = 'BufReadPost' }, { 'echasnovski/mini.ai', + cond = vim.fn.has('nvim-0.10') == 1, event = 'VeryLazy', config = function() require('mini.ai').setup({ @@ -471,6 +500,7 @@ require('lazy').setup({ }, { 'echasnovski/mini.bracketed', + cond = vim.fn.has('nvim-0.10') == 1, event = 'VeryLazy', config = function() require('mini.bracketed').setup({ diff --git a/scripts/install-neovim-src.sh b/scripts/install-neovim-src.sh new file mode 100755 index 0000000..515f974 --- /dev/null +++ b/scripts/install-neovim-src.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# Build and install the latest stable neovim from source. +# Required on systems where glibc < 2.32 (e.g. Ubuntu 20.04) prevents +# running the prebuilt GitHub release binaries. +# +# Usage: bash scripts/install-neovim-src.sh +# NEOVIM_TAG=v0.12.1 bash scripts/install-neovim-src.sh # pin version +# NEOVIM_PREFIX=~/.local bash scripts/install-neovim-src.sh # nosudo install +set -euo pipefail + +# ── Preflight ───────────────────────────────────────────────────────────────── +if ! sudo -n true 2>/dev/null && ! sudo -v 2>/dev/null; then + echo "ERROR: sudo is required to install build deps and copy files to /usr/local" >&2 + exit 1 +fi +echo "Note: this build downloads ~200 MB of dependencies and takes 5–15 minutes." + +PREFIX="${NEOVIM_PREFIX:-/usr/local}" + +# ── Build dependencies ──────────────────────────────────────────────────────── +echo "── Installing build dependencies ──" +sudo apt-get install -y \ + git ninja-build gettext cmake unzip curl build-essential + +# ── Resolve tag ────────────────────────────────────────────────────────────── +if [ -z "${NEOVIM_TAG:-}" ]; then + NEOVIM_TAG=$(curl -sfL \ + https://api.github.com/repos/neovim/neovim/releases/latest \ + | grep -o '"tag_name": *"[^"]*"' \ + | grep -o 'v[^"]*') +fi +[ -n "$NEOVIM_TAG" ] || { echo "ERROR: could not resolve latest neovim tag" >&2; exit 1; } +echo "── Building neovim $NEOVIM_TAG → $PREFIX ──" + +# ── Clone ───────────────────────────────────────────────────────────────────── +TMP=$(mktemp -d) +# shellcheck disable=SC2064 +trap "rm -rf '$TMP'" EXIT + +git clone --branch "$NEOVIM_TAG" --depth 1 \ + https://github.com/neovim/neovim.git "$TMP/neovim" + +cd "$TMP/neovim" + +# ── Build ───────────────────────────────────────────────────────────────────── +cmake -B build \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX="$PREFIX" + +cmake --build build -j"$(nproc)" + +# ── Install ─────────────────────────────────────────────────────────────────── +sudo cmake --install build + +VER=$("$PREFIX/bin/nvim" --version 2>/dev/null | head -1) || VER="(unknown)" +echo "✓ $VER installed → $PREFIX"