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