neovim/test/unit/preprocess.lua

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

269 lines
8.2 KiB
Lua
Raw Normal View History

-- helps managing loading different headers into the LuaJIT ffi. Untested on
-- windows, will probably need quite a bit of adjustment to run there.
local ffi = require("ffi")
local global_helpers = require('test.helpers')
local argss_to_cmd = global_helpers.argss_to_cmd
local repeated_read_cmd = global_helpers.repeated_read_cmd
2023-04-03 04:01:23 -07:00
--- @alias Compiler {path: string[], type: string}
--- @type Compiler[]
local ccs = {}
local env_cc = os.getenv("CC")
if env_cc then
table.insert(ccs, {path = {"/usr/bin/env", env_cc}, type = "gcc"})
end
if ffi.os == "Windows" then
table.insert(ccs, {path = {"cl"}, type = "msvc"})
end
table.insert(ccs, {path = {"/usr/bin/env", "cc"}, type = "gcc"})
table.insert(ccs, {path = {"/usr/bin/env", "gcc"}, type = "gcc"})
table.insert(ccs, {path = {"/usr/bin/env", "gcc-4.9"}, type = "gcc"})
table.insert(ccs, {path = {"/usr/bin/env", "gcc-4.8"}, type = "gcc"})
table.insert(ccs, {path = {"/usr/bin/env", "gcc-4.7"}, type = "gcc"})
table.insert(ccs, {path = {"/usr/bin/env", "clang"}, type = "clang"})
table.insert(ccs, {path = {"/usr/bin/env", "icc"}, type = "gcc"})
-- parse Makefile format dependencies into a Lua table
2023-04-03 04:01:23 -07:00
--- @param deps string
--- @return string[]
local function parse_make_deps(deps)
-- remove line breaks and line concatenators
deps = deps:gsub("\n", ""):gsub("\\", "")
-- remove the Makefile "target:" element
deps = deps:gsub(".+:", "")
-- remove redundant spaces
deps = deps:gsub(" +", " ")
-- split according to token (space in this case)
2023-04-03 04:01:23 -07:00
local headers = {} --- @type string[]
for token in deps:gmatch("[^%s]+") do
-- headers[token] = true
headers[#headers + 1] = token
end
-- resolve path redirections (..) to normalize all paths
for i, v in ipairs(headers) do
-- double dots (..)
headers[i] = v:gsub("/[^/%s]+/%.%.", "")
-- single dot (.)
headers[i] = v:gsub("%./", "")
end
return headers
end
2023-04-03 04:01:23 -07:00
--- will produce a string that represents a meta C header file that includes
--- all the passed in headers. I.e.:
---
--- headerize({"stdio.h", "math.h"}, true)
--- produces:
--- #include <stdio.h>
--- #include <math.h>
---
--- headerize({"vim.h", "memory.h"}, false)
--- produces:
--- #include "vim.h"
--- #include "memory.h"
--- @param headers string[]
--- @param global? boolean
--- @return string
local function headerize(headers, global)
2023-04-03 04:01:23 -07:00
local fmt = global and '#include <%s>' or '#include "%s"'
local formatted = {} --- @type string[]
2017-01-03 07:37:18 -07:00
for _, hdr in ipairs(headers) do
2023-04-03 04:01:23 -07:00
formatted[#formatted + 1] = string.format(fmt, hdr)
end
return table.concat(formatted, "\n")
end
2023-04-03 04:01:23 -07:00
--- @class Gcc
--- @field path string
--- @field preprocessor_extra_flags string[]
--- @field get_defines_extra_flags string[]
--- @field get_declarations_extra_flags string[]
local Gcc = {
preprocessor_extra_flags = {},
get_defines_extra_flags = {'-std=c99', '-dM', '-E'},
get_declarations_extra_flags = {'-std=c99', '-P', '-E'},
}
2023-04-03 04:01:23 -07:00
--- @param name string
--- @param args string[]?
--- @param val string?
function Gcc:define(name, args, val)
2023-04-03 04:01:23 -07:00
local define = string.format('-D%s', name)
if args then
define = string.format('%s(%s)', define, table.concat(args, ','))
end
2023-04-03 04:01:23 -07:00
if val then
define = string.format('%s=%s', define, val)
end
self.preprocessor_extra_flags[#self.preprocessor_extra_flags + 1] = define
end
function Gcc:undefine(name)
2023-04-03 04:01:23 -07:00
self.preprocessor_extra_flags[#self.preprocessor_extra_flags + 1] = '-U' .. name
end
2017-01-03 07:37:18 -07:00
function Gcc:init_defines()
-- preprocessor flags that will hopefully make the compiler produce C
-- declarations that the LuaJIT ffi understands.
self:define('aligned', {'ARGS'}, '')
self:define('__attribute__', {'ARGS'}, '')
self:define('__asm', {'ARGS'}, '')
self:define('__asm__', {'ARGS'}, '')
self:define('__inline__', nil, '')
self:define('EXTERN', nil, 'extern')
self:define('INIT', {'...'}, '')
self:define('_GNU_SOURCE')
self:define('INCLUDE_GENERATED_DECLARATIONS')
2017-01-07 09:07:32 -07:00
self:define('UNIT_TESTING')
self:define('UNIT_TESTING_LUA_PREPROCESSING')
-- Needed for FreeBSD
self:define('_Thread_local', nil, '')
-- Needed for macOS Sierra
self:define('_Nullable', nil, '')
self:define('_Nonnull', nil, '')
self:undefine('__BLOCKS__')
end
2023-04-03 04:01:23 -07:00
--- @param obj? Compiler
--- @return Gcc
function Gcc:new(obj)
obj = obj or {}
setmetatable(obj, self)
self.__index = self
self:init_defines()
return obj
end
2023-04-03 04:01:23 -07:00
--- @param ... string
function Gcc:add_to_include_path(...)
for i = 1, select('#', ...) do
local path = select(i, ...)
local ef = self.preprocessor_extra_flags
ef[#ef + 1] = '-I' .. path
end
end
-- returns a list of the headers files upon which this file relies
2023-04-03 04:01:23 -07:00
--- @param hdr string
--- @return string[]?
function Gcc:dependencies(hdr)
2023-04-03 04:01:23 -07:00
--- @type string
local cmd = argss_to_cmd(self.path, {'-M', hdr}) .. ' 2>&1'
2023-04-03 04:01:23 -07:00
local out = assert(io.popen(cmd))
local deps = out:read("*a")
out:close()
if deps then
return parse_make_deps(deps)
end
end
2023-04-03 04:01:23 -07:00
--- @param defines string
--- @return string
function Gcc:filter_standard_defines(defines)
if not self.standard_defines then
local pseudoheader_fname = 'tmp_empty_pseudoheader.h'
2023-04-03 04:01:23 -07:00
local pseudoheader_file = assert(io.open(pseudoheader_fname, 'w'))
pseudoheader_file:close()
2023-04-03 04:01:23 -07:00
local standard_defines = assert(repeated_read_cmd(self.path,
self.preprocessor_extra_flags,
self.get_defines_extra_flags,
{pseudoheader_fname}))
os.remove(pseudoheader_fname)
2023-04-03 04:01:23 -07:00
self.standard_defines = {} --- @type table<string,true>
for line in standard_defines:gmatch('[^\n]+') do
self.standard_defines[line] = true
end
end
2023-04-03 04:01:23 -07:00
local ret = {} --- @type string[]
for line in defines:gmatch('[^\n]+') do
if not self.standard_defines[line] then
ret[#ret + 1] = line
end
end
2023-04-03 04:01:23 -07:00
return table.concat(ret, "\n")
end
2023-04-03 04:01:23 -07:00
--- returns a stream representing a preprocessed form of the passed-in headers.
--- Don't forget to close the stream by calling the close() method on it.
--- @param previous_defines string
--- @param ... string
--- @return string, string
function Gcc:preprocess(previous_defines, ...)
-- create pseudo-header
local pseudoheader = headerize({...}, false)
local pseudoheader_fname = 'tmp_pseudoheader.h'
2023-04-03 04:01:23 -07:00
local pseudoheader_file = assert(io.open(pseudoheader_fname, 'w'))
pseudoheader_file:write(previous_defines)
pseudoheader_file:write("\n")
pseudoheader_file:write(pseudoheader)
pseudoheader_file:flush()
pseudoheader_file:close()
2023-04-03 04:01:23 -07:00
local defines = assert(repeated_read_cmd(self.path, self.preprocessor_extra_flags,
self.get_defines_extra_flags,
{pseudoheader_fname}))
defines = self:filter_standard_defines(defines)
2023-04-03 04:01:23 -07:00
local declarations = assert(repeated_read_cmd(self.path,
self.preprocessor_extra_flags,
self.get_declarations_extra_flags,
{pseudoheader_fname}))
os.remove(pseudoheader_fname)
return declarations, defines
end
-- find the best cc. If os.exec causes problems on windows (like popping up
-- a console window) we might consider using something like this:
-- http://scite-ru.googlecode.com/svn/trunk/pack/tools/LuaLib/shell.html#exec
2023-04-03 04:01:23 -07:00
--- @param compilers Compiler[]
--- @return Gcc?
local function find_best_cc(compilers)
for _, meta in pairs(compilers) do
2023-04-03 04:01:23 -07:00
local version = assert(io.popen(tostring(meta.path) .. " -v 2>&1"))
version:close()
if version then
2023-04-03 04:01:23 -07:00
return Gcc:new({path = meta.path})
end
end
end
-- find the best cc. If os.exec causes problems on windows (like popping up
-- a console window) we might consider using something like this:
-- http://scite-ru.googlecode.com/svn/trunk/pack/tools/LuaLib/shell.html#exec
2023-04-03 04:01:23 -07:00
local cc = assert(find_best_cc(ccs))
local M = {}
--- @param hdr string
--- @return string[]?
function M.includes(hdr)
return cc:dependencies(hdr)
end
--- @param ... string
--- @return string, string
function M.preprocess(...)
return cc:preprocess(...)
end
--- @param ... string
function M.add_to_include_path(...)
return cc:add_to_include_path(...)
end
return M