lp = { name = 'LuaPack', version = '0.1', copyright = '(c) Er2 2022, Zlib License', path = {}, } -- String interpolation function lp.strf(str, env) return str:gsub('%$([%w_]+)%]?', (n) ==> if env[n] then return tostring(env[n]) end <==) .. '' end -- Helper function function lp.insPath(t) for _, v in pairs(t) do table.insert(lp.path, v) end end function lp.foreach(e, fn) for _, v in pairs(e.plug) do fn(v) end end function lp.reqf(file, opts) if opts.ext[file] then return opts.ext[file] end end function lp.req(e, opts) return lp.reqf(e.file, opts) end -- Polyfill function table.contains(t, V) for k, v in pairs(t) do if v == V then return k end end end -- Try to find file path function lp.resolve(name, dir) if path.absolute(name) then return name end name = name:gsub('%.[%w_]+$', '') for _, v in pairs(lp.path) do v = v:gsub('%?', name) v = path.join(dir, v) local f = io.open(v) if f then f:close() return v end end end -- Use this in plugins to get module function lp.get(d, e, opts) local f for _, v in pairs(e.plug) do f = lp.resolve(d, e.dir) if f then break end end if not f then print(lp.strf(' [WRN] Cannot find "$file"', {file = d})) return nil end local c = lp.reqf(f, opts) if not c then c = lp.make(f, opts, f .. '.out.lua') end if not e.req[c.i] then e.reqs = e.reqs + 1 end e.req[c.i] = true print(lp.strf(' [GOT] $file', c)) return c end -- Alias to makeEnt without entry flag function lp.make(file, opts, out) return lp.makeEnt(file, opts, out, false) end -- Make function function lp.makeEnt(file, opts, out, entry) local dir = path.abs(path.dir(file)) local ext = file:match '%.([%w_]+)$' local oext = out:match '%.([%w_]+)$' file = lp.resolve(file, dir, ext) local e = lp.reqf(file, opts) if e then return e end local f, err = io.open(file) assert(f, err) local src = f:read '*a' f:close() e = { i = #opts.ext + 1, file = file, dir = dir, ext = ext, src = src, reqs = 0, req = {}, entry = entry, plug = opts.plug, } table.insert(opts.ext, file) opts.ext[file] = e if #e.plug == 0 then error(lp.strf('File "$file" cannot be loaded. Add plugins.', e)) end lp.foreach(e, (v) ==> v.load(e, ext, oext) e.src = v.deps(e, opts) or e.src e.src = v.transform(e, opts) or e.src <==) return e end local alp = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' function lp.randgen(n) local g = '' for i = 1, n do local v = math.random(#alp) g = g.. alp:sub(v, v) end return g end function lp.save(e, out) out = lp.strf(out, { name = path.base(e.file):gsub('%.[%w_]+$', ''), hash = lp.randgen(6), }) local f, err = io.open(out, 'w') assert(f, err) f:write(e.src) f:close() return out end function lp.config(conf, dir) local cfg = { -- defaults entry = {}, output = {}, ext = {}, plug = { require 'plug.lua' {} }, } for k, v in pairs(conf) do if k == 'entry' or k == 'output' then if type(v) == 'string' then table.insert(cfg[k], path.abs(v, dir)) elseif type(v) == 'table' then for _, v in pairs(v) do table.insert(cfg[k], path.abs(tostring(v), dir)) end else error(lp.strf('Unknown type $type in "$opt"', {type = type(v), opt = k})) end elseif k == 'plug' then for _, v in pairs(v) do if type(v) == 'table' then table.insert(cfg[k], v) v.load = v.load or function(e, ext, oext) end v.deps = v.deps or function(e, opts) end v.transform = v.transform or function(e, opts) end v.postbuild = v.postbuild or function(e, opts) end else error(lp.strf('Unknown type $type in "$opt"', {type = type(v), opt = k})) end end else error(lp.strf('Unknown option "$opt"', {opt = k})) end end return cfg end function lp.build(conf, dir) local opts = lp.config(conf, dir) for k, v in pairs(opts.entry) do local out = opts.output[math.min(k, #opts.output)] print(lp.strf('[BUILD] $file => $out', {file = v, out = out})) local e = lp.makeEnt(v, opts, out, true) lp.foreach(e, function(v) v.postbuild(e, opts) end) local save = lp.save(e, out) print(lp.strf('[BUILT] $o => $r', {o = e.file, r = save})) end end function main(argv) print(lp.name ..' '.. lp.version) print(lp.copyright) print '' local int = (argv[0] or ''):match '^lua' -- started using interpreter if #argv == 0 or (#argv == 1 and int) then print(('Usage: %s config.lua'):format(int and (argv[0] ..' '.. argv[1]) or argv[0])) return -1 end local t = os.time() local ent = path.abs(int and argv[2] or argv[1]) local dir = path.dir(ent) local f, err = io.open(ent) assert(f, err) local conf = f:read '*a' f:close() f, err = load(conf, 'config') if not f then error(err) else conf = f() end lp.build(conf, dir) print(lp.strf('Built under $sec seconds.', {sec = os.time() - t})) return 0 end path = { absolute = (f) ==> f = f:sub(1, 1) return f == '/' or f == '\\' <== , relative = (f) => not path.absolute(f) , abs = (f, pwd) ==> if path.absolute(f) then return f end pwd or= os.getenv 'PWD' f = pwd .. '/'.. f f = f:gsub('(.*)[\\/]+%.%.', path.dir) f = f:gsub('%.[\\/]+', '/') f = f:gsub('[\\/]+', '/') return f <== , rel = (f) => f , base = (f) => f:match '([^/\\]*)[/\\]*$' , dir = (f) => f:match '^(.*)[\\/]+[^\\/]*[\\/]*$' , file = (f) => f:match '([^\\/]+)[\\/]*$' , join = function(base, add) if path.absolute(add) then return add end if path.relative(base) then base = path.abs(base) end local p = path.abs(base ..'/'.. add) if path.relative(p) then return add end return p end, } math.randomseed(os.time()) os.exit(main(arg))