repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
neovim/puk.nu
Raw# an out-of-band plugin manager for neovim.
# cannot do a lot, compared to "normal" neovim plugin managers, but:
# - does not incur runtime costs on neovim
# - uses built-in vim packages
let tar_command = if $nu.os-info.name == 'windows' {
[$env.SystemRoot System32 tar.exe] | path join
} else { 'tar' }
def require-zig [] {
if (which zig | is-empty) {
error make { msg: 'puk requires Zig in PATH.' }
}
}
def refuse-running-neovim [] {
let processes = ps | where { |process|
let name = $process.name | path basename | str lowercase
$name in [ 'nvim' 'nvim.exe' ]
}
if not ($processes | is-empty) {
let pids = $processes.pid | each { into string } | str join ', '
[
$"Error: Neovim is running, PIDs: ($pids). Plugin installation stopped."
'Why: updating Treesitter parsers while Neovim holds parser DLLs can leave stale files and break health checks.'
'Action: close every Neovim instance, then rerun `just puk`.'
] | str join (char nl) | print --stderr
exit 1
}
}
def main [] {
require-zig
refuse-running-neovim
let paths_result = (nvim --headless --clean --cmd 'lua io.stdout:write(vim.json.encode({ data = vim.fn.stdpath("data"), cache = vim.fn.stdpath("cache") }))' +q | complete)
if $paths_result.exit_code != 0 {
error make { msg: $"Neovim path lookup failed: ($paths_result.stderr)" }
}
let paths = $paths_result.stdout | from json
let data_path = $paths.data
let cache_path = $paths.cache
if not ($cache_path | path exists) {
print $"creating cache path: ($cache_path)"
mkdir $cache_path
}
let plugins_path = [ $data_path site pack puk opt ] | path join
let site_path = [ $data_path site ] | path join
let base = $env.PWD
let plugin_specs = (open ($base | path join puk.plugins.nuon) | collect)
let parser_specs = (open ($base | path join puk.parsers.nuon) | collect)
if (not ($parser_specs | is-empty)) and ((which tree-sitter | is-empty)) {
print "puk.parsers.nuon declares parsers but the tree-sitter CLI is not in PATH."
print "Install tree-sitter-cli (>= 0.26) first."
exit 1
}
let all_specs = ($plugin_specs | append $parser_specs)
let total = $all_specs | length
print $"Installing plugins and parsers from puk.*.nuon. total: ($total)"
let pending_hooks = $plugin_specs | where { |spec|
($spec.post_install? | default null) != null and not (plugin-is-installed $spec $plugins_path)
}
let plugin_errors = $plugin_specs | par-each { install-spec $in $plugins_path $site_path $cache_path } | flatten
let hook_errors = $pending_hooks | where { |spec| $spec.name not-in $plugin_errors.name } | each { |spec|
finish-plugin $spec $plugins_path
} | flatten
let parser_errors = $parser_specs | each { install-spec $in $plugins_path $site_path $cache_path } | flatten
let errors = ($plugin_errors | append $hook_errors | append $parser_errors)
print 'Cleaning plugins, that are not part of puk.plugins.nuon.'
clean-plugins $plugins_path $plugin_specs
print 'Cleaning stale cache archives.'
clean-cache $cache_path $all_specs
# plugins need to be packadded before helptags can be generated
print 'Generating helptags.'
mut packs = $plugin_specs | par-each { | spec |
$"packadd ($spec.name)"
} | str join "\n"
nvim -u NONE --headless --cmd $packs --cmd 'helptags ALL' +q
if not ($errors | is-empty) {
let failed = $errors | length
print $"\n($failed)/($total) plugins FAILED:"
$errors | each { |e| print $" - ($e.name): ($e.error)" }
exit 1
}
}
# check online for fresh commits. Does not actually mutate or update anything.
# updated plugin specs will be written to a new file.
def 'main update' [] {
require-zig
update-file puk.plugins.nuon
update-file puk.parsers.nuon
}
# check one spec nuon file for fresh upstream commits; writes updated specs to a new file.
def update-file [
path: string # nuon file with plugin or parser specs
] {
print $"Checking for fresh commits of specs in ($path)..."
let specs = open $path
let updated = $specs |
update commit {
|spec|
let fresh_commit = find-latest-commit $spec
if ($fresh_commit != $spec.commit) {
print $"Found fresh commit for ($spec.name): ($fresh_commit)!"
$fresh_commit
} else {
$spec.commit
}
}
if not ($specs == $updated) {
let target = ($path | str replace '.nuon' '.updated.nuon')
$updated |
to nuon --indent 2 |
save --force $target
print $"Written updated specs into ($target)!"
} else {
print "Nothing to do."
}
}
# deletes all existing plugins, that are NOT in the specs!
def clean-plugins [
plugins_path: path # folder to install plugins into
plugin_specs: list<any> # all known plugins
] {
ls $plugins_path | each {
| row |
let existing_plugin_name = $row.name | path basename
if ( $plugin_specs | where name == $existing_plugin_name | is-empty) {
print $"Found plugin, that is not part of the specs: ($existing_plugin_name)"
rm --trash --recursive $row.name
}
}
}
# deletes all cache archives, that are NOT matching any spec's name+commit!
def clean-cache [
cache_path: path # folder containing cached archives
plugin_specs: list<any> # all known plugins
] {
let expected = $plugin_specs | each { |spec| (make-archive-path $spec $cache_path | path basename) }
try {
glob $"($cache_path | str replace --all '\' '/')/*.tar.gz" | each { |path|
let name = $path | path basename
if ($expected | where $it == $name | is-empty) {
print $"Removing stale cache: ($name)"
rm $path
}
}
} catch {}
}
# given a plugin spec, and a plugins base install directory, this method generates
# a new path, that points to the location the plugin should be installed into.
def make-plugin-path [
spec: record # a plugin to install
plugins_path: path # folder to install plugins into
] { [$plugins_path $spec.name] | path join }
# given a path to an installed plugin, this method generates a path to a file holding
# the commit of the plugin. even though git is not really involved in installing
# plugins here, we use the commit as a kind of version number and lock file in one.
def make-commit-file-path [
plugin_path: path # path to an installed plugin
] { [$plugin_path .commit.puk] | path join }
def plugin-is-installed [
spec: record # a plugin to install
plugins_path: path # folder to install plugins into
] {
let predicted_plugin_path = make-plugin-path $spec $plugins_path
if not ( $predicted_plugin_path | path exists ) {
return false
}
let commit_file_path = make-commit-file-path $predicted_plugin_path
if not ($commit_file_path | path exists) {
return false
}
let commit = open --raw $commit_file_path | str trim
return ($commit == $spec.commit)
}
def find-latest-commit [
spec: record # a plugin to install
] {
let feed = generate-commit-feed-url $spec
let commit = match $spec.source {
github => {
try {
http get --max-time 5sec $feed |
lines |
parse --regex "Commit/(?<commit>[a-f0-9]+)" |
first |
get commit
} catch {
print $"Timed out or failed fetching ($spec.name), keeping current commit."
$spec.commit
}
},
codeberg => { 'TODO' },
srht => { 'TODO' },
_ => { error make {
msg: 'unknown plugin source given'
label: {
text: 'dunno this one'
span: (metadata $spec.source).span
}
}
}
}
$commit
}
def generate-commit-feed-url [
spec: record # a plugin to install
] {
let url_format = match $spec.source {
github => { 'https://github.com/{repo}/commits/{branch}.atom' },
codeberg => { 'TODO' },
srht => { 'TODO' },
_ => { error make {
msg: 'unknown plugin source given'
label: {
text: 'dunno this one'
span: (metadata $spec.source).span
}
}
}
}
$spec | format pattern $url_format
}
def generate-download-url [
spec: record # a plugin to install
] {
let url_format = match $spec.source {
github => { 'https://github.com/{repo}/archive/{commit}.tar.gz' },
codeberg => { 'https://codeberg.org/{repo}/archive/{commit}.tar.gz' },
srht => { 'https://git.sr.ht/{repo}/archive/{commit}.tar.gz' },
_ => { error make {
msg: 'unknown plugin source given'
label: {
text: 'dunno this one'
span: (metadata $spec.source).span
}
}
}
}
$spec | format pattern $url_format
}
def make-archive-path [
spec: record # a plugin to install
cache_path: path # folder to cache downloaded archives into
] {
[ $cache_path $"($spec.name)-($spec.commit).tar.gz" ] | path join
}
def download-plugin [
spec: record # a plugin to install
cache_path: path # folder to cache downloaded archives into
] {
let archive_path = make-archive-path $spec $cache_path
if ($archive_path | path exists) {
let checked = (run-external $tar_command '--list' '--file' $archive_path | complete)
if $checked.exit_code == 0 {
print $"($spec.name) already cached in ($archive_path)"
return $archive_path
}
rm $archive_path
}
let url = generate-download-url $spec
let partial_path = mktemp --tmpdir-path $cache_path 'puk-download.XXXXXX'
try {
try {
http get --raw --max-time 5sec $url | save --force $partial_path
} catch {
print $"Download of ($spec.name) failed, retrying..."
http get --raw --max-time 10sec $url | save --force $partial_path
}
let checked = (run-external $tar_command '--list' '--file' $partial_path | complete)
if $checked.exit_code != 0 {
error make { msg: $"($spec.name): invalid archive: ($checked.stderr)" }
}
mv $partial_path $archive_path
$archive_path
} finally {
if ($partial_path | path exists) { rm $partial_path }
}
}
def tree-hash [dir: path] {
glob $"($dir | str replace --all '\' '/')/**/*" | where { |f| ($f | path type) == file } | each { |f|
{ rel: ($f | path relative-to $dir), h: (open --raw $f | hash sha256) }
}
}
def diff-hashes [base: path, hashes: list, label: string, name: string] {
let now = (tree-hash $base)
$hashes | each { |x|
let y = ($now | where rel == $x.rel)
if ($y | is-empty) { $"VERIFY ($name) ($label): missing ($x.rel)" } else if ($y | first | get h) != $x.h { $"VERIFY ($name) ($label): content ($x.rel)" } else { '' }
} | where { |s| $s != '' }
}
def unpack-plugin-archive [
spec: record # a plugin to install
archive_path: path # path to a plugin archive
] {
let tmp_path = mktemp --directory
try {
let extracted = (run-external $tar_command '--extract' '--file' $archive_path '--directory' $tmp_path '--strip-components=1' | complete)
if $extracted.exit_code != 0 {
error make { msg: $"($spec.name): extraction failed: ($extracted.stderr)" }
}
if $spec.build != null {
print $"Build step for plugin ($spec.name) detected! Executing now..."
run-nu-in-dir $tmp_path $spec.build
}
$tmp_path
} catch { |e|
rm --recursive $tmp_path
error make { msg: $e.msg }
}
}
let allowed_shared_lib_type = match $nu.os-info.name {
windows => 'dll',
macos => 'dylib',
_ => 'so',
}
let base_filetypes = [ lua vim scm txt $allowed_shared_lib_type ]
let exclusions = [
**/tests/**,
**/test/**,
**/perf/**,
**/scripts/**,
**/fixtures/**,
**/scrapers/**,
**/spec/**,
**/target/release/deps/**
]
def install-plugin [
spec: record # a plugin to install
folder_path: path # unpacked path to plugin folder
plugins_path: path # folder to install plugins into
] {
let plugin_path = make-plugin-path $spec $plugins_path
let tmp_path = [$plugins_path $".tmp.($spec.name)"] | path join
try { rm --recursive $tmp_path }
mkdir $tmp_path
let filetypes = if ($spec.extra_extensions? | default null) != null {
$base_filetypes | append $spec.extra_extensions
} else {
$base_filetypes
}
let copied = (glob $"($folder_path | str replace --all '\' '/')/**/*.{($filetypes | str join ',')}" --exclude $exclusions | each { |f| { rel: ($f | path relative-to $folder_path), h: (open --raw $f | hash sha256) } })
$copied | each { | entry |
let rebased_parent = [$tmp_path ($entry.rel | path dirname)] | path join
mkdir $rebased_parent
cp ([$folder_path $entry.rel] | path join) $rebased_parent
}
let copy_problems = (diff-hashes $tmp_path $copied 'copy' $spec.name)
if not ($copy_problems | is-empty) {
error make { msg: $"($spec.name) copy verification failed: ($copy_problems | str join ', ')" }
}
try { rm --trash --recursive $plugin_path }
mv $tmp_path $plugin_path
let move_problems = (diff-hashes $plugin_path $copied 'move' $spec.name)
if not ($move_problems | is-empty) {
rm --trash --recursive $plugin_path
error make { msg: $"($spec.name) move verification failed: ($move_problems | str join ', ')" }
}
$plugin_path
}
def run-nu-in-dir [
dir: path
command: string
] {
cd $dir
let result = (run-external $nu.current-exe '--no-config-file' '--commands' $command | complete)
print --no-newline $result.stdout
print --stderr --no-newline $result.stderr
if $result.exit_code != 0 {
error make { msg: $"Build failed with exit code ($result.exit_code)" }
}
}
def install-spec-parser [
spec: record # a parser to install
site_path: path # nvim site dir (parsers/queries install root)
cache_path: path # folder to cache downloaded archives into
] {
let parser_so = [$site_path parser $"($spec.name).so"] | path join
let pin_path = [$site_path parser $".($spec.name).commit.puk"] | path join
let queries_target = [$site_path queries $spec.name] | path join
let has_queries = ($spec.queries? | default null) != null
let queries_ready = if $has_queries {
(glob $"($queries_target | str replace --all '\' '/')/*.scm" | length) > 0
} else { true }
if ($parser_so | path type) == file and $queries_ready and ($pin_path | path type) == file and ((open --raw $pin_path | str trim) == $spec.commit) {
return []
}
try {
print $"Downloading parser ($spec.name)..."
let archive_path = download-plugin $spec $cache_path
print $"Unpacking parser ($spec.name)..."
let unpacked_path = unpack-plugin-archive { name: $spec.name, build: null } $archive_path
try {
let root = if ($spec.location? | default null) != null { [$unpacked_path $spec.location] | path join } else { $unpacked_path }
let source_ready = if $spec.generate {
($root | path join src grammar.json | path exists) or ($root | path join grammar.js | path exists)
} else { $root | path join src parser.c | path exists }
if not $source_ready {
error make { msg: $"($spec.name): tarball is missing parser source or grammar" }
}
if $spec.generate {
print $"Generating parser ($spec.name)..."
let grammar = if ($root | path join src grammar.json | path exists) {
$root | path join src grammar.json
} else {
$root | path join grammar.js
}
let gen = (tree-sitter generate $grammar '--output' ($root | path join src) | complete)
if $gen.exit_code != 0 {
error make { msg: $"($spec.name): tree-sitter generate failed: ($gen.stderr)" }
}
}
print $"Compiling parser ($spec.name)..."
let so_tmp = ($root | path join $"($spec.name).so")
let compiler_env = {
CC: 'zig cc',
CRATE_CC_NO_DEFAULTS: '1',
CFLAGS: (if $nu.os-info.name == 'windows' { $"-target ($nu.os-info.arch)-windows-gnu -O2" } else { '-O2' })
}
let built = with-env $compiler_env {
tree-sitter build '--output' $so_tmp $root | complete
}
if $built.exit_code != 0 {
error make { msg: $"($spec.name): tree-sitter build failed: ($built.stderr)" }
}
if not ($so_tmp | path exists) {
error make { msg: $"($spec.name): compile produced no .so" }
}
let staged_queries = [$unpacked_path '.puk-queries'] | path join
if $has_queries {
let queries = glob $"([$root $spec.queries] | path join | str replace --all '\' '/')/*.scm"
if ($queries | is-empty) {
error make { msg: $"($spec.name): no .scm queries found" }
}
mkdir $staged_queries
$queries | each { |f| cp $f $staged_queries } | ignore
}
mkdir ($site_path | path join parser)
let old_parser = [$unpacked_path '.puk-old-parser'] | path join
let old_queries = [$unpacked_path '.puk-old-queries'] | path join
let old_pin = if ($pin_path | path type) == file { open --raw $pin_path } else { null }
if ($parser_so | path exists) { cp $parser_so $old_parser }
mut queries_replaced = false
let failure = try {
if $has_queries {
mkdir ($queries_target | path dirname)
if ($queries_target | path exists) { mv $queries_target $old_queries }
$queries_replaced = true
mv $staged_queries $queries_target
}
cp --force $so_tmp $parser_so
$spec.commit | save --force $pin_path
null
} catch { |e| $e }
if $failure != null {
if ($old_parser | path exists) {
cp --force $old_parser $parser_so
} else if ($parser_so | path exists) { rm $parser_so }
if $queries_replaced {
if ($queries_target | path exists) { rm --recursive $queries_target }
if ($old_queries | path exists) { mv $old_queries $queries_target }
}
if $old_pin != null {
$old_pin | save --force $pin_path
} else if ($pin_path | path type) == file { rm $pin_path }
error make { msg: $failure.msg }
}
print $"Parser ($spec.name) installed into ($parser_so)!"
[]
} finally {
rm --recursive $unpacked_path
}
} catch {
|e|
print $"FAILED parser ($spec.name): ($e.msg)"
return [{ name: $spec.name, error: $e.msg }]
}
}
def install-spec [
spec: record # a plugin or parser to install
plugins_path: path # folder to install plugins into
site_path: path # nvim site dir (parser install root)
cache_path: path # folder to cache downloaded archives into
] {
if ($spec.generate? != null) {
return (install-spec-parser $spec $site_path $cache_path)
}
if (plugin-is-installed $spec $plugins_path) {
return []
}
try {
print $"Downloading ($spec.name)..."
let archive_path = download-plugin $spec $cache_path
print $"Unpacking ($spec.name)..."
let unpacked_path = unpack-plugin-archive $spec $archive_path
try {
print $"Installing ($spec.name)..."
let installed_plugin_path = install-plugin $spec $unpacked_path $plugins_path
if ($spec.post_install? | default null) == null {
$spec.commit | save --force (make-commit-file-path $installed_plugin_path)
}
print $"Plugin ($spec.name) installed into ($installed_plugin_path)!"
[]
} finally {
rm --recursive $unpacked_path
}
} catch {
|e|
print $"FAILED ($spec.name): ($e.msg)"
return [{ name: $spec.name, error: $e.msg }]
}
}
def finish-plugin [spec: record, plugins_path: path] {
try {
print $"Running post-install for ($spec.name)..."
let commands = $spec.post_install | each { |cmd| $cmd | to json --raw } | str join ', '
let script = 'lua for _, command in ipairs({%s}) do local ok, err = pcall(vim.cmd, command); if not ok then vim.api.nvim_err_writeln(tostring(err)); vim.cmd.cquit(1) end end' | str replace '%s' $commands
let result = (nvim --headless -c $script +qa | complete)
print --no-newline $result.stdout
print --stderr --no-newline $result.stderr
if $result.exit_code != 0 {
error make { msg: $"($spec.name) post-install failed with exit code ($result.exit_code)" }
}
$spec.commit | save --force (make-commit-file-path (make-plugin-path $spec $plugins_path))
[]
} catch { |e|
[{ name: $spec.name, error: $e.msg }]
}
}