feat!: use tree-sitter build

This commit is contained in:
Christian Clason 2024-04-22 19:56:30 +02:00
parent c17de56890
commit 214cfcf851
13 changed files with 443 additions and 898 deletions

View file

@ -9,9 +9,6 @@ error('Cannot require a meta file')
---Commit hash of parser to download (compatible with queries)
---@field revision string
---
---Files to include when compiling (`src/parser.c` and optionally `src/scanner.c')
---@field files string[]
---
---Branch of parser repo to download (if not default branch)
---@field branch? string
---

View file

@ -104,15 +104,6 @@ function M.get_available(tier)
languages
)
end
if vim.fn.executable('tree-sitter') == 0 then
languages = vim.tbl_filter(
--- @param p string
function(p)
return parsers[p].install_info ~= nil and not parsers[p].install_info.generate
end,
languages
)
end
return languages
end

View file

@ -1,4 +1,3 @@
local install = require('nvim-treesitter.install')
local parsers = require('nvim-treesitter.parsers')
local config = require('nvim-treesitter.config')
local util = require('nvim-treesitter.util')
@ -8,82 +7,80 @@ local health = vim.health
local M = {}
local NVIM_TREESITTER_MINIMUM_ABI = 13
local TREE_SITTER_MIN_VER = { 0, 22, 6 }
---@return string|nil
local function ts_cli_version()
if vim.fn.executable('tree-sitter') == 1 then
local result = assert(vim.system({ 'tree-sitter', '-V' }):wait().stdout)
return vim.split(result, '\n')[1]:match('[^tree%psitter ].*')
---@param name string
---@return table?
local function check_exe(name)
if vim.fn.executable(name) == 1 then
local path = vim.fn.exepath(name)
local out = vim.trim(vim.fn.system({ name, '--version' }))
local version = vim.version.parse(out)
return { path = path, version = version, out = out }
end
end
local function install_health()
health.start('Requirements')
if vim.fn.has('nvim-0.10') ~= 1 then
health.error('Nvim-treesitter requires Neovim Nightly')
do -- nvim check
if vim.fn.has('nvim-0.10') ~= 1 then
health.error('Nvim-treesitter requires the latest Neovim nightly')
end
if vim.treesitter.language_version then
if vim.treesitter.language_version >= NVIM_TREESITTER_MINIMUM_ABI then
health.ok(
'Neovim was compiled with tree-sitter runtime ABI version '
.. vim.treesitter.language_version
.. ' (required >='
.. NVIM_TREESITTER_MINIMUM_ABI
.. ').'
)
else
health.error(
'Neovim was compiled with tree-sitter runtime ABI version '
.. vim.treesitter.language_version
.. '.\n'
.. 'nvim-treesitter expects at least ABI version '
.. NVIM_TREESITTER_MINIMUM_ABI
.. '\n'
.. 'Please make sure that Neovim is linked against a recent tree-sitter library when building'
.. ' or raise an issue at your Neovim packager. Parsers must be compatible with runtime ABI.'
)
end
end
end
if vim.fn.executable('tree-sitter') == 0 then
health.warn(
'`tree-sitter` executable not found (parser generator, only needed for :TSInstallFromGrammar,'
.. ' not required for :TSInstall)'
)
else
health.ok(
'`tree-sitter` found '
.. (ts_cli_version() or '(unknown version)')
.. ' (only needed for `:TSInstallFromGrammar`)'
)
end
if vim.fn.executable('git') == 0 then
health.warn(
'`git` executable not found.',
'Install it with your package manager and check that your `$PATH` is set correctly.'
)
else
health.ok('`git` executable found.')
end
local cc = install.select_executable(install.compilers)
if not cc then
health.error('`cc` executable not found.', {
'Check that any of '
.. table.concat(install.compilers, ', ')
.. ' is in your $PATH'
.. ' or set `$CC` or `require"nvim-treesitter.install".compilers` explicitly.',
})
else
local version = assert(vim.system({ cc, cc == 'cl' and '' or '--version' }):wait().stdout)
health.ok(
'`'
.. cc
.. '` executable found, selected from: '
.. table.concat(install.compilers, ', ')
.. (version and ('\nVersion: ' .. version) or '')
)
end
if vim.treesitter.language_version then
if vim.treesitter.language_version >= NVIM_TREESITTER_MINIMUM_ABI then
health.ok(
'Neovim was compiled with tree-sitter runtime ABI version '
.. vim.treesitter.language_version
.. ' (required >='
.. NVIM_TREESITTER_MINIMUM_ABI
.. ').'
)
do -- treesitter check
local ts = check_exe('tree-sitter')
if ts then
if vim.version.ge(ts.version, TREE_SITTER_MIN_VER) then
health.ok(string.format('tree-sitter %s (%s)', ts.version, ts.path))
else
health.error(
string.format('tree-sitter CLI v%d.%d.%d is required', unpack(TREE_SITTER_MIN_VER))
)
end
else
health.error(
'Neovim was compiled with tree-sitter runtime ABI version '
.. vim.treesitter.language_version
.. '.\n'
.. 'nvim-treesitter expects at least ABI version '
.. NVIM_TREESITTER_MINIMUM_ABI
.. '\n'
.. 'Please make sure that Neovim is linked against a recent tree-sitter library when building'
.. ' or raise an issue at your Neovim packager. Parsers must be compatible with runtime ABI.'
)
health.error('tree-sitter CLI not found')
end
end
do -- curl+tar or git check
local curl = check_exe('curl')
local tar = check_exe('tar')
if curl and tar and vim.uv.os_uname().sysname ~= 'Windows_NT' then
health.ok(string.format('tar %s (%s)', tar.version, tar.path))
health.ok(string.format('curl %s (%s)\n%s', curl.version, curl.path, curl.out))
else
local git = check_exe('git')
if git then
health.ok(string.format('git %s (%s)', git.version, git.path))
else
health.error('Either curl and tar or git must be installed and on `$PATH`')
end
end
end

View file

@ -23,19 +23,8 @@ local uv_symlink = a.wrap(uv.fs_symlink, 4)
--- @type fun(path: string): string?
local uv_unlink = a.wrap(uv.fs_unlink, 2)
local M = {}
local max_jobs = 10
local iswin = uv.os_uname().sysname == 'Windows_NT'
local ismac = uv.os_uname().sysname == 'Darwin'
--- @diagnostic disable-next-line:missing-parameter
M.compilers = { 'cc', 'gcc', 'clang', 'cl', 'zig' }
if uv.os_getenv('CC') then
table.insert(M.compilers, 1, uv.os_getenv('CC'))
end
local function system(cmd, opts)
log.trace('running job: (cwd=%s) %s', opts.cwd, table.concat(cmd, ' '))
local r = a.wrap(vim.system, 3)(cmd, opts) --[[@as vim.SystemCompleted]]
@ -50,6 +39,10 @@ local function system(cmd, opts)
return r
end
local iswin = uv.os_uname().sysname == 'Windows_NT'
local M = {}
---
--- PARSER INFO
---
@ -99,18 +92,6 @@ end
--- PARSER MANAGEMENT FUNCTIONS
---
local function istring(c)
return type(c) == 'string'
end
local function cc_err()
log.error(
'No C compiler found! "'
.. table.concat(vim.tbl_filter(istring, M.compilers), '", "')
.. '" are not executable.'
)
end
--- @param x string
--- @return boolean
local function executable(x)
@ -122,10 +103,6 @@ end
--- @param compile_location string
--- @return string? err
local function do_generate(logger, repo, compile_location)
if not executable('tree-sitter') then
return logger:error('tree-sitter CLI not found: `tree-sitter` is not executable')
end
logger:info(
string.format(
'Generating parser.c from %s...',
@ -134,7 +111,7 @@ local function do_generate(logger, repo, compile_location)
)
local r = system({
fn.exepath('tree-sitter'),
'tree-sitter',
'generate',
'--no-bindings',
'--abi',
@ -232,10 +209,6 @@ end
---@param project_dir string
---@return string? err
local function do_download_git(logger, repo, project_name, cache_dir, revision, project_dir)
if not executable('git') then
return logger:error('git not found!')
end
logger:info('Downloading ' .. project_name .. '...')
local r = system({
@ -266,86 +239,6 @@ local function do_download_git(logger, repo, project_name, cache_dir, revision,
end
end
--- @type table<string,table<string,boolean>>
local cc_args_cache = vim.defaulttable()
--- @param cc string
--- @param arg string
--- @return boolean
local function test_cc_arg(cc, arg)
if cc_args_cache[cc][arg] == nil then
cc_args_cache[cc][arg] = system({ cc, '-xc', '-', arg }, {
stdin = 'int main(void) { return 0; }',
}).code == 0
end
return cc_args_cache[cc][arg]
end
---@param executables string[]
---@return string?
function M.select_executable(executables)
return vim.tbl_filter(executable, executables)[1]
end
-- Returns the compiler arguments based on the compiler and OS
---@param repo InstallInfo
---@param compiler string
---@return string[]
local function select_compiler_args(repo, compiler)
if compiler:find('cl$') or compiler:find('cl.exe$') then
return {
'/Fe:',
'parser.so',
'/Isrc',
repo.files,
'-Os',
'/utf-8',
'/LD',
}
end
if compiler:find('zig$') or compiler:find('zig.exe$') then
return {
'cc',
'-o',
'parser.so',
repo.files,
'-lc',
'-Isrc',
'-shared',
'-Os',
}
end
local args = {
'-o',
'parser.so',
'-I./src',
repo.files,
'-Os',
ismac and '-bundle' or '-shared',
}
--- @param arg string
local function add_cc_arg(arg)
if test_cc_arg(compiler, arg) then
args[#args + 1] = arg
end
end
if not iswin then
add_cc_arg('-Wall')
add_cc_arg('-Wextra')
add_cc_arg('-fPIC')
-- Make sure we don't compile in any unresolved symbols, otherwise nvim will
-- just exit (not even crash)
add_cc_arg('-Werror=implicit-function-declaration')
end
return args
end
---@param repo InstallInfo
---@return boolean
local function can_download_tar(repo)
@ -355,21 +248,40 @@ local function can_download_tar(repo)
return can_use_tar and (is_github or is_gitlab) and not iswin
end
-- Returns the compile command based on the OS and user options
---@param logger Logger
---@param repo InstallInfo
---@param cc string
---@param compile_location string
---@return string? err
local function do_compile(logger, repo, cc, compile_location)
local args = vim.iter(select_compiler_args(repo, cc)):flatten():totable()
local cmd = vim.list_extend({ cc }, args)
local function do_compile(logger, compile_location)
logger:info(string.format('Compiling parser'))
logger:info('Compiling parser')
local r = system(cmd, { cwd = compile_location })
local r = system({
'tree-sitter',
'build',
'-o',
'parser.so',
}, { cwd = compile_location })
if r.code > 0 then
return logger:error('Error during compilation: %s', r.stderr)
return logger:error('Error during "tree-sitter build": %s', r.stderr)
end
end
---@param logger Logger
---@param compile_location string
---@param target_location string
---@return string? err
local function do_install(logger, compile_location, target_location)
logger:info(string.format('Installing parser'))
if iswin then -- why can't you just be normal?!
local tempfile = target_location .. tostring(uv.hrtime())
uv_rename(target_location, tempfile) -- parser may be in use: rename...
uv_unlink(tempfile) -- ...and mark for garbage collection
end
local err = uv_copyfile(compile_location, target_location)
a.main()
if err then
return logger:error('Error during parser installation: %s', err)
end
end
@ -383,12 +295,6 @@ local function install_lang0(lang, cache_dir, install_dir, generate)
local repo = get_parser_install_info(lang)
if repo then
local cc = M.select_executable(M.compilers)
if not cc then
cc_err()
return
end
local project_name = 'tree-sitter-' .. lang
local revision = repo.revision
@ -414,7 +320,7 @@ local function install_lang0(lang, cache_dir, install_dir, generate)
compile_location = fs.joinpath(compile_location, repo.location)
end
do
do -- generate parser from grammar
if repo.generate or generate then
local err = do_generate(logger, repo, compile_location)
if err then
@ -423,24 +329,25 @@ local function install_lang0(lang, cache_dir, install_dir, generate)
end
end
do
local err = do_compile(logger, repo, cc, compile_location)
do -- compile parser
local err = do_compile(logger, compile_location)
if err then
return err
end
end
local parser_lib_name = fs.joinpath(install_dir, lang) .. '.so'
do -- install parser
local parser_lib_name = fs.joinpath(compile_location, 'parser.so')
local install_location = fs.joinpath(install_dir, lang) .. '.so'
local err = do_install(logger, parser_lib_name, install_location)
if err then
return err
end
local err = uv_copyfile(fs.joinpath(compile_location, 'parser.so'), parser_lib_name)
a.main()
if err then
return logger:error(err)
local revfile = fs.joinpath(config.get_install_dir('parser-info') or '', lang .. '.revision')
util.write_file(revfile, revision or '')
end
local revfile = fs.joinpath(config.get_install_dir('parser-info') or '', lang .. '.revision')
util.write_file(revfile, revision or '')
if not repo.path then
util.delete(fs.joinpath(cache_dir, project_name))
end

File diff suppressed because it is too large Load diff