neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

preprocess.lua (8358B)


      1 -- helps managing loading different headers into the LuaJIT ffi. Untested on
      2 -- windows, will probably need quite a bit of adjustment to run there.
      3 
      4 local ffi = require('ffi')
      5 local global_t = require('test.testutil')
      6 
      7 local argss_to_cmd = global_t.argss_to_cmd
      8 local repeated_read_cmd = global_t.repeated_read_cmd
      9 
     10 --- @alias Compiler {path: string[], type: string}
     11 
     12 --- @type Compiler[]
     13 local ccs = {}
     14 
     15 local env_cc = os.getenv('CC')
     16 if env_cc then
     17  table.insert(ccs, { path = { '/usr/bin/env', env_cc }, type = 'gcc' })
     18 end
     19 
     20 if ffi.os == 'Windows' then
     21  table.insert(ccs, { path = { 'cl' }, type = 'msvc' })
     22 end
     23 
     24 table.insert(ccs, { path = { '/usr/bin/env', 'cc' }, type = 'gcc' })
     25 table.insert(ccs, { path = { '/usr/bin/env', 'gcc' }, type = 'gcc' })
     26 table.insert(ccs, { path = { '/usr/bin/env', 'gcc-4.9' }, type = 'gcc' })
     27 table.insert(ccs, { path = { '/usr/bin/env', 'gcc-4.8' }, type = 'gcc' })
     28 table.insert(ccs, { path = { '/usr/bin/env', 'gcc-4.7' }, type = 'gcc' })
     29 table.insert(ccs, { path = { '/usr/bin/env', 'clang' }, type = 'clang' })
     30 table.insert(ccs, { path = { '/usr/bin/env', 'icc' }, type = 'gcc' })
     31 
     32 -- parse Makefile format dependencies into a Lua table
     33 --- @param deps string
     34 --- @return string[]
     35 local function parse_make_deps(deps)
     36  -- remove line breaks and line concatenators
     37  deps = deps:gsub('\n', ''):gsub('\\', '')
     38  -- remove the Makefile "target:" element
     39  deps = deps:gsub('.+:', '')
     40  -- remove redundant spaces
     41  deps = deps:gsub('  +', ' ')
     42 
     43  -- split according to token (space in this case)
     44  local headers = {} --- @type string[]
     45  for token in deps:gmatch('[^%s]+') do
     46    -- headers[token] = true
     47    headers[#headers + 1] = token
     48  end
     49 
     50  -- resolve path redirections (..) to normalize all paths
     51  for i, v in ipairs(headers) do
     52    -- double dots (..)
     53    headers[i] = v:gsub('/[^/%s]+/%.%.', '')
     54    -- single dot (.)
     55    headers[i] = v:gsub('%./', '')
     56  end
     57 
     58  return headers
     59 end
     60 
     61 --- will produce a string that represents a meta C header file that includes
     62 --- all the passed in headers. I.e.:
     63 ---
     64 --- headerize({"stdio.h", "math.h"}, true)
     65 --- produces:
     66 --- #include <stdio.h>
     67 --- #include <math.h>
     68 ---
     69 --- headerize({"vim_defs.h", "memory.h"}, false)
     70 --- produces:
     71 --- #include "vim_defs.h"
     72 --- #include "memory.h"
     73 --- @param headers string[]
     74 --- @param global? boolean
     75 --- @return string
     76 local function headerize(headers, global)
     77  local fmt = global and '#include <%s>' or '#include "%s"'
     78  local formatted = {} --- @type string[]
     79  for _, hdr in ipairs(headers) do
     80    formatted[#formatted + 1] = string.format(fmt, hdr)
     81  end
     82 
     83  return table.concat(formatted, '\n')
     84 end
     85 
     86 --- @class Gcc
     87 --- @field path string
     88 --- @field preprocessor_extra_flags string[]
     89 --- @field get_defines_extra_flags string[]
     90 --- @field get_declarations_extra_flags string[]
     91 local Gcc = {
     92  preprocessor_extra_flags = {},
     93  get_defines_extra_flags = { '-std=c99', '-dM', '-E' },
     94  get_declarations_extra_flags = { '-std=c99', '-P', '-E' },
     95 }
     96 
     97 --- @param name string
     98 --- @param args string[]?
     99 --- @param val string?
    100 function Gcc:define(name, args, val)
    101  local define = string.format('-D%s', name)
    102  if args then
    103    define = string.format('%s(%s)', define, table.concat(args, ','))
    104  end
    105  if val then
    106    define = string.format('%s=%s', define, val)
    107  end
    108  self.preprocessor_extra_flags[#self.preprocessor_extra_flags + 1] = define
    109 end
    110 
    111 function Gcc:undefine(name)
    112  self.preprocessor_extra_flags[#self.preprocessor_extra_flags + 1] = '-U' .. name
    113 end
    114 
    115 function Gcc:init_defines()
    116  -- preprocessor flags that will hopefully make the compiler produce C
    117  -- declarations that the LuaJIT ffi understands.
    118  self:define('aligned', { 'ARGS' }, '')
    119  self:define('__attribute__', { 'ARGS' }, '')
    120  self:define('__asm', { 'ARGS' }, '')
    121  self:define('__asm__', { 'ARGS' }, '')
    122  self:define('__inline__', nil, '')
    123  self:define('EXTERN', nil, 'extern')
    124  self:define('INIT', { '...' }, '')
    125  self:define('_GNU_SOURCE')
    126  self:define('UNIT_TESTING')
    127  self:define('UNIT_TESTING_LUA_PREPROCESSING')
    128  -- Needed for FreeBSD
    129  self:define('_Thread_local', nil, '')
    130  -- Needed for macOS Sierra
    131  self:define('_Nullable', nil, '')
    132  self:define('_Nonnull', nil, '')
    133  self:undefine('__BLOCKS__')
    134 end
    135 
    136 --- @param obj? Compiler
    137 --- @return Gcc
    138 function Gcc:new(obj)
    139  obj = obj or {}
    140  setmetatable(obj, self)
    141  self.__index = self
    142  self:init_defines()
    143  return obj
    144 end
    145 
    146 --- @param ... string
    147 function Gcc:add_to_include_path(...)
    148  local ef = self.preprocessor_extra_flags
    149  for i = 1, select('#', ...) do
    150    local path = select(i, ...)
    151    ef[#ef + 1] = '-I' .. path
    152  end
    153 end
    154 
    155 function Gcc:add_apple_sysroot(sysroot)
    156  local ef = self.preprocessor_extra_flags
    157 
    158  table.insert(ef, '-isysroot')
    159  table.insert(ef, sysroot)
    160 end
    161 
    162 -- returns a list of the headers files upon which this file relies
    163 --- @param hdr string
    164 --- @return string[]?
    165 function Gcc:dependencies(hdr)
    166  local cmd = table.concat(argss_to_cmd(self.path, { '-M', hdr }), ' ') .. ' 2>&1'
    167  local out = assert(io.popen(cmd))
    168  local deps = out:read('*a')
    169  out:close()
    170  if deps then
    171    return parse_make_deps(deps)
    172  end
    173 end
    174 
    175 --- @param defines string
    176 --- @return string
    177 function Gcc:filter_standard_defines(defines)
    178  if not self.standard_defines then
    179    local pseudoheader_fname = 'tmp_empty_pseudoheader.h'
    180    local pseudoheader_file = assert(io.open(pseudoheader_fname, 'w'))
    181    pseudoheader_file:close()
    182    local standard_defines = assert(
    183      repeated_read_cmd(
    184        self.path,
    185        self.preprocessor_extra_flags,
    186        self.get_defines_extra_flags,
    187        { pseudoheader_fname }
    188      )
    189    )
    190    os.remove(pseudoheader_fname)
    191    self.standard_defines = {} --- @type table<string,true>
    192    for line in standard_defines:gmatch('[^\n]+') do
    193      self.standard_defines[line] = true
    194    end
    195  end
    196 
    197  local ret = {} --- @type string[]
    198  for line in defines:gmatch('[^\n]+') do
    199    if not self.standard_defines[line] then
    200      ret[#ret + 1] = line
    201    end
    202  end
    203 
    204  return table.concat(ret, '\n')
    205 end
    206 
    207 --- returns a stream representing a preprocessed form of the passed-in headers.
    208 --- Don't forget to close the stream by calling the close() method on it.
    209 --- @param previous_defines string
    210 --- @param ... string
    211 --- @return string, string
    212 function Gcc:preprocess(previous_defines, ...)
    213  -- create pseudo-header
    214  local pseudoheader = headerize({ ... }, false)
    215  local pseudoheader_fname = 'tmp_pseudoheader.h'
    216  local pseudoheader_file = assert(io.open(pseudoheader_fname, 'w'))
    217  pseudoheader_file:write(previous_defines)
    218  pseudoheader_file:write('\n')
    219  pseudoheader_file:write(pseudoheader)
    220  pseudoheader_file:flush()
    221  pseudoheader_file:close()
    222 
    223  local defines = assert(
    224    repeated_read_cmd(
    225      self.path,
    226      self.preprocessor_extra_flags,
    227      self.get_defines_extra_flags,
    228      { pseudoheader_fname }
    229    )
    230  )
    231  defines = self:filter_standard_defines(defines)
    232 
    233  local declarations = assert(
    234    repeated_read_cmd(
    235      self.path,
    236      self.preprocessor_extra_flags,
    237      self.get_declarations_extra_flags,
    238      { pseudoheader_fname }
    239    )
    240  )
    241 
    242  os.remove(pseudoheader_fname)
    243 
    244  return declarations, defines
    245 end
    246 
    247 -- find the best cc. If os.exec causes problems on windows (like popping up
    248 -- a console window) we might consider using something like this:
    249 -- http://scite-ru.googlecode.com/svn/trunk/pack/tools/LuaLib/shell.html#exec
    250 --- @param compilers Compiler[]
    251 --- @return Gcc?
    252 local function find_best_cc(compilers)
    253  for _, meta in pairs(compilers) do
    254    local version = assert(io.popen(tostring(meta.path) .. ' -v 2>&1'))
    255    version:close()
    256    if version then
    257      return Gcc:new({ path = meta.path })
    258    end
    259  end
    260 end
    261 
    262 -- find the best cc. If os.exec causes problems on windows (like popping up
    263 -- a console window) we might consider using something like this:
    264 -- http://scite-ru.googlecode.com/svn/trunk/pack/tools/LuaLib/shell.html#exec
    265 local cc = assert(find_best_cc(ccs))
    266 
    267 local M = {}
    268 
    269 --- @param hdr string
    270 --- @return string[]?
    271 function M.includes(hdr)
    272  return cc:dependencies(hdr)
    273 end
    274 
    275 --- @param ... string
    276 --- @return string, string
    277 function M.preprocess(...)
    278  return cc:preprocess(...)
    279 end
    280 
    281 --- @param ... string
    282 function M.add_to_include_path(...)
    283  return cc:add_to_include_path(...)
    284 end
    285 
    286 --- @param ... string
    287 function M.add_apple_sysroot(...)
    288  return cc:add_apple_sysroot(...)
    289 end
    290 
    291 return M