Repository navigation
Expand file tree
/
Copy pathbootstrap.lua
More file actions
215 lines (194 loc) · 7.56 KB
/
Copy pathbootstrap.lua
File metadata and controls
215 lines (194 loc) · 7.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
-- Headless provisioning for this Neovim config.
--
-- Run through install-deps.sh, or directly:
-- nvim --headless -c "lua dofile('bootstrap.lua')"
--
-- Everything Neovim installs at startup (vim.pack clones, lazy.nvim installs,
-- mason tools, tree-sitter parsers) is asynchronous, so a plain `nvim
-- --headless +qa` exits mid-install and leaves the machine half-provisioned.
-- This script drives each of those to completion, verifies the result on disk,
-- and exits non-zero if anything is still missing.
--
-- It must be sourced during startup (via -c) so its MasonToolsUpdateCompleted
-- autocmd is registered before mason-tool-installer's deferred run fires.
local deps = require("config.deps")
local MASON_TIMEOUT_MS = 15 * 60 * 1000
local PARSER_TIMEOUT_MS = 5 * 60 * 1000
local POLL_MS = 200
local failures = {}
local function log(fmt, ...)
io.stdout:write(string.format(fmt, ...) .. "\n")
io.stdout:flush()
end
local function fail(fmt, ...)
local msg = string.format(fmt, ...)
table.insert(failures, msg)
log(" ✗ %s", msg)
end
local function section(name)
log("\n==> %s", name)
end
-- ─── mason ───────────────────────────────────────────────────────────────────
-- Registered first, before anything blocks, so the startup-triggered run is
-- caught rather than raced.
local mason_done = false
vim.api.nvim_create_autocmd("User", {
pattern = "MasonToolsUpdateCompleted",
once = true,
callback = function()
mason_done = true
end,
})
-- ─── binaries ────────────────────────────────────────────────────────────────
local function check_binaries()
section("External binaries")
for _, bin in ipairs(deps.binaries) do
if vim.fn.executable(bin.cmd) == 1 then
log(" ✓ %s", bin.cmd)
elseif bin.required then
fail("%s is missing (%s)", bin.cmd, bin.why)
else
log(" ! %s is missing (%s)", bin.cmd, bin.why)
end
end
end
-- ─── lazy.nvim ───────────────────────────────────────────────────────────────
local function install_lazy_plugins()
section("lazy.nvim plugins")
local lazy = require("lazy")
-- install() clones anything missing and runs its build hook; wait = true
-- blocks until the whole task queue drains.
lazy.install({ wait = true, show = false })
for _, plugin in pairs(lazy.plugins()) do
if not plugin._.installed then
fail("plugin %s failed to install", plugin.name)
end
end
log(" ✓ %d plugins present", #lazy.plugins())
end
-- ─── magenta.nvim ────────────────────────────────────────────────────────────
-- Its build hook is `npm run build`, which needs node on Neovim's PATH. When
-- Neovim is launched outside a login shell (nvm not sourced) the hook fails
-- silently and the plugin falls back to source mode, which then dies on a
-- missing @magenta/core. Build it here where the shell environment is known.
local function build_magenta()
section("magenta.nvim bundle")
local root = vim.fs.joinpath(vim.fn.stdpath("data"), "lazy", "magenta.nvim")
if not vim.uv.fs_stat(root) then
log(" ! magenta.nvim is not installed, skipping")
return
end
if vim.uv.fs_stat(vim.fs.joinpath(root, "dist", "magenta.mjs")) then
log(" ✓ dist/magenta.mjs already built")
return
end
if vim.fn.executable("npm") == 0 then
fail("npm is missing, cannot build magenta.nvim")
return
end
log(" … running npm run build (this takes a minute)")
local res = vim.system({ "npm", "run", "build" }, { cwd = root, text = true }):wait(10 * 60 * 1000)
if res.code ~= 0 or not vim.uv.fs_stat(vim.fs.joinpath(root, "dist", "magenta.mjs")) then
fail("magenta.nvim build failed:\n%s", (res.stderr or ""):sub(-800))
return
end
log(" ✓ dist/magenta.mjs built")
end
-- ─── tree-sitter parsers ─────────────────────────────────────────────────────
local function install_parsers()
section("tree-sitter parsers")
if vim.fn.executable("tree-sitter") == 0 then
fail("tree-sitter CLI is missing, no parser can be built")
return
end
local ok, installer = pcall(require, "tree-sitter-manager.installer")
if not ok then
fail("tree-sitter-manager is not installed")
return
end
local util = require("tree-sitter-manager.util")
-- pack.lua fires these asynchronously at startup; let those land first so we
-- do not clone the same parser twice.
for _, lang in ipairs(deps.treesitter) do
vim.wait(PARSER_TIMEOUT_MS, function()
return vim.uv.fs_stat(util.ppath(lang)) ~= nil
end, POLL_MS)
end
-- Anything still missing gets one deterministic serial retry.
for _, lang in ipairs(deps.treesitter) do
if vim.uv.fs_stat(util.ppath(lang)) then
log(" ✓ %s", lang)
else
log(" … installing %s", lang)
local finished, succeeded = false, false
installer.install(lang, function(result)
succeeded, finished = result, true
end)
vim.wait(PARSER_TIMEOUT_MS, function()
return finished
end, POLL_MS)
if succeeded and vim.uv.fs_stat(util.ppath(lang)) then
log(" ✓ %s", lang)
else
fail("parser %s failed to build", lang)
end
end
end
end
-- ─── mason tools ─────────────────────────────────────────────────────────────
local function install_mason_tools()
section("mason tools")
local ok, registry = pcall(require, "mason-registry")
if not ok then
fail("mason-registry is not available")
return
end
if not vim.wait(MASON_TIMEOUT_MS, function()
return mason_done
end, POLL_MS) then
log(" ! mason-tool-installer did not report completion, verifying on disk")
end
for _, name in ipairs(deps.mason) do
local got, pkg = pcall(registry.get_package, name)
if not got then
fail("unknown mason package: %s", name)
elseif pkg:is_installed() then
log(" ✓ %s", name)
else
log(" … installing %s", name)
local finished = false
local done = function()
finished = true
end
-- mason v1 returns a handle to subscribe to; v2 takes a callback.
local handle = pkg:install({}, done)
if type(handle) == "table" and handle.once then
handle:once("closed", done)
end
vim.wait(MASON_TIMEOUT_MS, function()
return finished
end, POLL_MS)
if pkg:is_installed() then
log(" ✓ %s", name)
else
fail("mason package %s failed to install", name)
end
end
end
end
-- ─── run ─────────────────────────────────────────────────────────────────────
check_binaries()
install_lazy_plugins()
build_magenta()
install_parsers()
install_mason_tools()
section("Result")
if #failures > 0 then
log("%d problem(s):", #failures)
for _, msg in ipairs(failures) do
log(" - %s", msg)
end
vim.cmd("cquit 1")
end
log(" ✓ everything installed")
vim.cmd("qall!")