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