preprocess.lua 8.0 KB

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