buildroot.lua 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. --- Module implementing the LuaRocks "buildroot" command.
  2. local buildroot = {}
  3. local dir = require("luarocks.dir")
  4. local fs = require("luarocks.fs")
  5. local util = require("luarocks.util")
  6. local queries = require("luarocks.queries")
  7. local search = require("luarocks.search")
  8. local download = require("luarocks.download")
  9. local fetch = require("luarocks.fetch")
  10. function buildroot.add_to_parser(parser)
  11. local cmd = parser:command("buildroot", [[
  12. This addon generates Buildroot package files of a rock.
  13. First argument is the name of a rock, the second argument is optional
  14. and needed when Buildroot uses another name (usually prefixed by lua-).
  15. Files are generated with the source content of the rock and more
  16. especially the rockspec. So, the rock is downloaded and unpacked.
  17. ]], util.see_also())
  18. :summary("generate buildroot package files of a rock.")
  19. cmd:argument("rockname", "the name of a rock to be fetched and unpacked.")
  20. cmd:argument("brname", "the name used by Buildroot.")
  21. :args("?")
  22. end
  23. local function brname (name)
  24. return name:upper():gsub('-', '_')
  25. end
  26. local function brlicense (license)
  27. if license:match('MIT/X') then
  28. return 'MIT'
  29. end
  30. return license
  31. end
  32. local function wrap (txt, max)
  33. local lines = {}
  34. local line = ''
  35. for word in txt:gmatch('(%S+)') do
  36. if line:len() + word:len() > max - 1 then
  37. lines[#lines+1] = line
  38. line = ''
  39. end
  40. if line == '' then
  41. line = word
  42. else
  43. line = line .. ' ' .. word
  44. end
  45. end
  46. lines[#lines+1] = line
  47. return lines
  48. end
  49. local function has_c_files (rockspec)
  50. for _, mod in pairs(rockspec.build.modules or {}) do
  51. if type(mod) == 'string' then
  52. if mod:match'%.c$' then
  53. return true
  54. end
  55. elseif type(mod) == 'table' then
  56. local sources = mod.sources
  57. if type(sources) == 'string' and sources:match'%.c$' then
  58. return true
  59. end
  60. for _, src in ipairs(sources or mod) do
  61. if src:match'%.c$' then
  62. return true
  63. end
  64. end
  65. end
  66. end
  67. return false
  68. end
  69. local function get_main_modules (rockspec)
  70. local t = {}
  71. for name in pairs(rockspec.build.modules or {}) do
  72. if not name:match('%.') then
  73. t[#t+1] = name
  74. end
  75. end
  76. if #t == 0 then
  77. for name in pairs(rockspec.build.modules or {}) do
  78. t[#t+1] = name
  79. end
  80. end
  81. if #t == 0 then
  82. t[#t+1] = rockspec.package:gsub('%-', '')
  83. end
  84. table.sort(t)
  85. return t
  86. end
  87. local function get_external_dependencies (rockspec)
  88. local t = {}
  89. for k in pairs(rockspec.external_dependencies or {}) do
  90. k = k:lower()
  91. if fs.is_dir('package/' .. k) then
  92. t[#t+1] = k
  93. else
  94. t[#t+1] = 'lib' .. k
  95. if not fs.is_dir('package/lib' .. k) then
  96. util.printout('unkwown external dependency: ' .. k)
  97. end
  98. end
  99. end
  100. table.sort(t)
  101. return t
  102. end
  103. local function get_dependencies (rockspec)
  104. local t = {}
  105. for i = 1, #rockspec.dependencies do
  106. local dep = tostring(rockspec.dependencies[i]):match('^(%S+)')
  107. if dep ~= 'lua' then
  108. dep = dep:gsub('_', '-')
  109. if fs.is_dir('package/lua-' .. dep) then
  110. t[#t+1] = 'lua-' .. dep
  111. else
  112. t[#t+1] = dep
  113. if not fs.is_dir('package/' .. dep) then
  114. util.printout('unkwown dependency: ' .. dep)
  115. end
  116. end
  117. end
  118. end
  119. table.sort(t)
  120. return t
  121. end
  122. function get_digest (file)
  123. local absname = fs.absolute_name(file)
  124. local pipe = io.popen('sha256sum ' .. fs.Q(absname))
  125. local line = pipe:read('*l')
  126. pipe:close()
  127. local computed = line and line:match('(' .. ('%x'):rep(64) .. ')')
  128. if computed then
  129. return computed
  130. else
  131. return nil, "Failed to compute SHA256 hash for file " .. absname
  132. end
  133. end
  134. local function generate_config (rockspec, lcname)
  135. local ucname = brname(lcname)
  136. local only_luajit = rockspec.package:match('^lj')
  137. local summary = rockspec.description.summary
  138. if not summary then
  139. summary = '???'
  140. elseif not summary:match('%.%s*$') then
  141. summary = summary:gsub('%s*$', '.')
  142. end
  143. local homepage = rockspec.description.homepage or '???'
  144. local external_dependencies = get_external_dependencies(rockspec)
  145. local dependencies = get_dependencies(rockspec)
  146. local fname = 'package/' .. lcname .. '/Config.in'
  147. local f = assert(io.open(fname, 'w'))
  148. util.printout('write ' .. fname)
  149. f:write('config BR2_PACKAGE_' .. ucname .. '\n')
  150. f:write('\tbool "' .. lcname .. '"\n')
  151. if only_luajit then
  152. f:write('\tdepends on BR2_PACKAGE_LUAJIT\n')
  153. end
  154. for i = 1, #external_dependencies do
  155. f:write('\tselect BR2_PACKAGE_' .. brname(external_dependencies[i]) .. '\n')
  156. end
  157. for i = 1, #dependencies do
  158. f:write('\tselect BR2_PACKAGE_' .. brname(dependencies[i]) .. ' # runtime\n')
  159. end
  160. f:write('\thelp\n')
  161. f:write('\t ' .. table.concat(wrap(summary, 62), '\n\t ') .. '\n')
  162. f:write('\n\t ' .. homepage .. '\n')
  163. if only_luajit then
  164. f:write('\ncomment "' .. lcname .. ' needs LuaJIT"\n')
  165. f:write('\tdepends on !BR2_PACKAGE_LUAJIT\n')
  166. end
  167. f:close()
  168. end
  169. local function generate_mk (rockspec, lcname, licenses)
  170. local function escape (s)
  171. return s:gsub('-', '%%-'):gsub('%.', '%%.')
  172. end
  173. local ucname = brname(lcname)
  174. local need_name_upstream = false
  175. local need_version_upstream = false
  176. local name_upstream = rockspec.package
  177. local version = rockspec.version
  178. local version_upstream = version:match('^([^-]+)-')
  179. local revision = version:match('-(%d+)$')
  180. local license = rockspec.description.license
  181. local subdir = rockspec.source.dir
  182. if subdir then
  183. local root = subdir:match('^(.-)-' .. escape(version) .. '$')
  184. if root then
  185. subdir = root .. '-$(' .. ucname .. '_VERSION)'
  186. end
  187. root = subdir:match('^(.--[Vv])' .. escape(version_upstream) .. '$')
  188. if root then
  189. need_version_upstream = true
  190. subdir = root .. '$(' .. ucname .. '_VERSION_UPSTREAM)'
  191. end
  192. root = subdir:match('^(.-)-' .. escape(version_upstream) .. '$')
  193. if root then
  194. if root == lcname then
  195. subdir = nil
  196. elseif root == name_upstream then
  197. subdir = nil
  198. need_name_upstream = true
  199. else
  200. need_version_upstream = true
  201. subdir = root .. '-$(' .. ucname .. '_VERSION_UPSTREAM)'
  202. end
  203. end
  204. end
  205. local external_dependencies = get_external_dependencies(rockspec)
  206. local fname = 'package/' .. lcname .. '/' .. lcname .. '.mk'
  207. local f = assert(io.open(fname, 'w'))
  208. util.printout('write ' .. fname)
  209. f:write('################################################################################\n')
  210. f:write('#\n')
  211. f:write('# ' .. lcname .. '\n')
  212. f:write('#\n')
  213. f:write('################################################################################\n')
  214. f:write('\n')
  215. if need_version_upstream then
  216. f:write(ucname .. '_VERSION_UPSTREAM = ' .. version_upstream .. '\n')
  217. f:write(ucname .. '_VERSION = $(' .. ucname .. '_VERSION_UPSTREAM)-' .. revision .. '\n')
  218. else
  219. f:write(ucname .. '_VERSION = ' .. version .. '\n')
  220. end
  221. if lcname ~= name_upstream:lower() or need_name_upstream then
  222. f:write(ucname .. '_NAME_UPSTREAM = ' .. name_upstream .. '\n')
  223. end
  224. if subdir then
  225. f:write(ucname .. '_SUBDIR = ' .. subdir .. '\n')
  226. end
  227. if license then
  228. f:write(ucname .. '_LICENSE = ' .. brlicense(license) .. '\n')
  229. end
  230. if #licenses == 1 then
  231. f:write(ucname .. '_LICENSE_FILES = $(' .. ucname .. '_SUBDIR)/' .. licenses[1] .. '\n')
  232. elseif #licenses > 1 then
  233. f:write(ucname .. '_LICENSE_FILES =')
  234. for i = 1, #licenses do
  235. local file = licenses[i]
  236. f:write(' \\\n\t$(' .. ucname .. '_SUBDIR)/' .. file)
  237. end
  238. f:write('\n')
  239. end
  240. if #external_dependencies > 0 then
  241. f:write(ucname .. '_DEPENDENCIES = ' .. table.concat(external_dependencies, ' ') .. '\n')
  242. end
  243. f:write('\n$(eval $(luarocks-package))\n')
  244. f:close()
  245. end
  246. local function generate_hash (rockspec, lcname, rock_file, licenses, digest)
  247. local subdir = rockspec.source.dir
  248. local fname = 'package/' .. lcname .. '/' .. lcname .. '.hash'
  249. local f = assert(io.open(fname, 'w'))
  250. util.printout('write ' .. fname)
  251. f:write('# computed by luarocks/buildroot\n')
  252. f:write('sha256 ' .. digest[rock_file] .. ' ' .. rock_file .. '\n')
  253. for i = 1, #licenses do
  254. local file = licenses[i]
  255. f:write('sha256 ' .. digest[file] .. ' ' .. subdir .. '/' .. file .. '\n')
  256. end
  257. f:close()
  258. end
  259. local function generate_test (rockspec, lcname)
  260. local ucname = brname(lcname)
  261. local classname = rockspec.package:gsub('%-', ''):gsub('%.', '')
  262. classname = classname:sub(1, 1):upper() .. classname:sub(2)
  263. local modnames = get_main_modules(rockspec)
  264. local fname = 'support/testing/tests/package/test_' .. ucname:lower() .. '.py'
  265. local f = assert(io.open(fname, 'w'))
  266. util.printout('write ' .. fname)
  267. f:write('from tests.package.test_lua import TestLuaBase\n')
  268. f:write('\n')
  269. f:write('\n')
  270. f:write('class TestLua' .. classname .. '(TestLuaBase):\n')
  271. f:write(' config = TestLuaBase.config + \\\n')
  272. f:write(' """\n')
  273. f:write(' BR2_PACKAGE_LUA=y\n')
  274. f:write(' BR2_PACKAGE_' .. ucname .. '=y\n')
  275. f:write(' """\n')
  276. f:write('\n')
  277. f:write(' def test_run(self):\n')
  278. f:write(' self.login()\n')
  279. for i = 1, #modnames do
  280. f:write(' self.module_test("' .. modnames[i] .. '")\n')
  281. end
  282. f:write('\n')
  283. f:write('\n')
  284. f:write('class TestLuajit' .. classname .. '(TestLuaBase):\n')
  285. f:write(' config = TestLuaBase.config + \\\n')
  286. f:write(' """\n')
  287. f:write(' BR2_PACKAGE_LUAJIT=y\n')
  288. f:write(' BR2_PACKAGE_' .. ucname .. '=y\n')
  289. f:write(' """\n')
  290. f:write('\n')
  291. f:write(' def test_run(self):\n')
  292. f:write(' self.login()\n')
  293. for i = 1, #modnames do
  294. f:write(' self.module_test("' .. modnames[i] .. '")\n')
  295. end
  296. f:close()
  297. end
  298. --- Driver function for the "buildroot" command.
  299. -- @return boolean: true if successful
  300. function buildroot.command(args)
  301. local rockname = assert(args.rockname)
  302. local fsname = args.brname or rockname
  303. local query = queries.new(rockname:lower(), nil, nil, false, 'src')
  304. local url, err = search.find_suitable_rock(query)
  305. if not url then
  306. return nil, "Could not find a result named " .. tostring(query) .. ": " .. err
  307. end
  308. local rock_file = dir.base_name(url)
  309. local temp_dir, err = fs.make_temp_dir(rockname)
  310. if not temp_dir then
  311. return nil, "Failed creating temporary dir: " .. err
  312. end
  313. local ok, err = fs.change_dir(temp_dir)
  314. if not ok then return nil, err end
  315. ok = fs.download(url, rock_file, true)
  316. if not ok then
  317. return nil, "Failed downloading " .. url
  318. end
  319. local digest = {}
  320. digest[rock_file], err = get_digest(rock_file)
  321. if not digest[rock_file] then return nil, err end
  322. ok, err = fs.unzip(rock_file)
  323. if not ok then return nil, err end
  324. local rockspec_file = rock_file:gsub('%.src%.rock$', '.rockspec')
  325. local rockspec, err = fetch.load_rockspec(rockspec_file)
  326. if not rockspec then
  327. return nil, "Error loading rockspec: " .. err
  328. end
  329. if rockspec.source.file then
  330. ok, err = fs.unpack_archive(rockspec.source.file)
  331. if not ok then return nil, err end
  332. end
  333. if rockspec.source.dir ~= '.' then
  334. fs.copy(rockspec.local_abs_filename, rockspec.source.dir, 'read')
  335. end
  336. local build_type = rockspec.build.type
  337. if build_type ~= 'none' and build_type ~= 'builtin' and build_type ~= 'module' then
  338. util.printout('[' .. rockspec.package .. "] build_type '" .. build_type .. "' not supported")
  339. end
  340. local licenses = {}
  341. ok, err = fs.change_dir(rockspec.source.dir)
  342. if not ok then return nil, err end
  343. local files = fs.find()
  344. for i = 1, #files do
  345. local v = files[i]
  346. if v == 'COPYING'
  347. or v == 'COPYRIGHT'
  348. or v:match('^LICENSE') then
  349. licenses[#licenses+1] = v
  350. digest[v], err = get_digest(v)
  351. if not digest[v] then return nil, err end
  352. end
  353. end
  354. if #licenses == 0 then
  355. for i = 1, #files do
  356. local v = files[i]
  357. if v:match('^doc/LICENSE')
  358. or v:match('^doc/license')
  359. or v:match('^doc/us/license') then
  360. licenses[#licenses+1] = v
  361. digest[v], err = get_digest(v)
  362. if not digest[v] then return nil, err end
  363. end
  364. end
  365. end
  366. fs.pop_dir()
  367. table.sort(licenses)
  368. fs.pop_dir()
  369. ok, err = fs.make_dir('package/' .. fsname:lower())
  370. if not ok then return nil, err end
  371. generate_config(rockspec, fsname:lower())
  372. generate_mk(rockspec, fsname:lower(), licenses)
  373. generate_hash(rockspec, fsname:lower(), rock_file, licenses, digest)
  374. if has_c_files(rockspec) then
  375. ok, err = fs.make_dir('support/testing/tests/package')
  376. if not ok then return nil, err end
  377. generate_test(rockspec, fsname:lower())
  378. end
  379. return true
  380. end
  381. return buildroot