Finish lowering memory use, R1

Since this is after the technical "release", version numbers have been bumped to 1.

Changes before this commit for R1:
 Kernel memory usage reduction schemes, with some security fixes.
 Still need to deal w/ proxies (see later)
Changes in this commit:
 Some various little things in apps
 CLAW inet actually works now on 192K
 sys-icecap no longer uses the event/neoux combination,
  and now handles Everest disappearance as a mass-close,
  but still handles Everest not being around on window create.
 So it still handles every situation that matters.
 neoux no longer handles everest crash protection.
 Security policy and filedialog obviously don't use neoux anymore.
 Kernel now only guarantees parsing, not event-loop, by executeAsync
 This is safer and allows app-launcher to get rid of NeoUX by
  any means necessary.
 wrapMeta cache now exists, and proxies get wrapMeta'd to deal with
  various low-priority security shenanigans.
 This is a *stopgap* until I work out how to force OCEmu to give me
  totally accurate boot-time memory figures, so I can create the
  ultimate lowmem proxy. I'm calling it "puppet". FG knows why.
This commit is contained in:
20kdc 2018-03-30 12:36:48 +01:00
parent fcabf5b853
commit 7bde8fee55
12 changed files with 361 additions and 229 deletions

8
.gitignore vendored
View File

@ -12,6 +12,14 @@ work/*/
work/*/*
work/*/*/
work/*/*/*
repobuild
repobuild/
repobuild/*
repobuild/*/
repobuild/*/*
repobuild/*/*/
repobuild/*/*/*
inst.lua
com2/code.tar.bd
upldr.sh

17
clawmerge.lua Normal file
View File

@ -0,0 +1,17 @@
local merges = {...}
neo = {
wrapMeta = function (x)
return x
end
}
local serial = loadfile("code/libs/serial.lua")()
local repo = {}
for _, v in ipairs(merges) do
local f = io.open(v, "rb")
local fd = f:read("*a")
f:close()
for k, v in pairs(serial.deserialize(fd)) do
repo[k] = v
end
end
io.write(serial.serialize(repo))

View File

@ -20,7 +20,7 @@ if primaryINet then primaryINet = primaryINet.list()() end
local function yielder()
-- slightly dangerous, but what can we do?
event.sleepTo(os.uptime() + 0.05)
pcall(event.sleepTo, os.uptime() + 0.05)
end
local function download(url, cb)
@ -28,16 +28,12 @@ local function download(url, cb)
local req, err = primaryINet.request(source .. url)
if not req then
cb(nil)
return nil, tostring(err)
end
local ok, err = req.finishConnect()
if not req.finishConnect() then
req.close()
cb(nil)
return nil, tostring(err)
return nil, "dlR/" .. tostring(err)
end
-- OpenComputers#535
req.finishConnect()
while true do
local n, n2 = req.read()
local n, n2 = req.read(neo.readBufSize)
local o, r = cb(n)
if not o then
req.close()

View File

@ -4,8 +4,7 @@
-- app-taskmgr: Task manager
-- a-hello : simple test program for Everest.
local everest = neo.requestAccess("x.neo.pub.window")
if not everest then error("no everest") return end
local everest = neo.requireAccess("x.neo.pub.window", "main window")
local kill = neo.requestAccess("k.kill")
@ -13,7 +12,6 @@ local sW, sH = 20, 8
local headlines = 2
local window = everest(sW, sH)
if not window then error("no window") end
local lastIdleTimeTime = os.uptime()
local lastIdleTime = neo.totalIdleTime()

View File

@ -4,11 +4,6 @@
-- s-icecap : Responsible for x.neo.pub API, crash dialogs, and security policy that isn't "sys- has ALL access, anything else has none"
-- In general, this is what userspace will be interacting with in some way or another to get stuff done
local event = require("event")(neo)
local neoux, err = require("neoux")
if not neoux then error(err) end -- This app is basically neoux's testcase
neoux = neoux(event, neo)
local settings = neo.requireAccess("x.neo.sys.manage", "security sysconf access")
local fs = neo.requireAccess("c.filesystem", "file managers")
@ -17,6 +12,72 @@ local donkonitDFProvider = neo.requireAccess("r.neo.pub.base", "creating basic N
local targsDH = {} -- data disposal
local todo = {}
local onEverest = {}
local everestWindows = {}
local nexus
local function resumeWF(...)
local ok, e = coroutine.resume(...)
if not ok then
e = tostring(e)
neo.emergency(e)
nexus.startDialog(e, "ice")
end
return ok
end
nexus = {
createNexusThread = function (f, ...)
local t = coroutine.create(f)
if not resumeWF(t, ...) then return end
local early = neo.requestAccess("x.neo.pub.window")
if early then
onEverest[#onEverest] = nil
resumeWF(t, early)
end
return function ()
for k, v in ipairs(onEverest) do
if v == t then
table.remove(onEverest, k)
return
end
end
end
end,
create = function (w, h, t)
local thr = coroutine.running()
table.insert(onEverest, thr)
local everest = coroutine.yield()
local dw = everest(w, h, title)
everestWindows[dw.id] = thr
return dw
end,
startDialog = function (tx, ti)
local fmt = require("fmttext")
local txl = fmt.fmtText(unicode.safeTextFormat(tx), 40)
fmt = nil
nexus.createNexusThread(function ()
local w = nexus.create(40, #txl, ti)
while true do
local ev, a = coroutine.yield()
if ev == "line" then
w.span(1, a, txl[a], 0xFFFFFF, 0)
elseif ev == "close" then
w.close()
return
end
end
end)
end,
close = function (wnd)
wnd.close()
everestWindows[wnd.id] = nil
end
}
donkonitDFProvider(function (pkg, pid, sendSig)
local prefixNS = "data/" .. pkg
local prefixWS = "data/" .. pkg .. "/"
@ -32,9 +93,10 @@ donkonitDFProvider(function (pkg, pid, sendSig)
-- Not hooked into the event API, so can't safely interfere
-- Thus, this is async and uses a return event.
local tag = {}
event.runAt(0, function ()
neo.scheduleTimer(0)
table.insert(todo, function ()
-- sys-filedialog is yet another "library to control memory usage".
local closer = require("sys-filedialog")(event, neoux, function (res) openHandles[tag] = nil sendSig("filedialog", tag, res) end, fs, pkg, forWrite)
local closer = require("sys-filedialog")(event, nexus, function (res) openHandles[tag] = nil sendSig("filedialog", tag, res) end, fs, pkg, forWrite)
openHandles[tag] = closer
end)
return tag
@ -122,9 +184,10 @@ rootAccess.securityPolicy = function (pid, proc, perm, req)
req(def)
return
end
-- Push to ICECAP thread to avoid deadlock on neoux b/c wrong event-pull context
event.runAt(0, function ()
local ok, err = pcall(secpol, neoux, settings, proc.pkg, pid, perm, req)
-- Push to ICECAP thread to avoid deadlock b/c wrong event-pull context
neo.scheduleTimer(0)
table.insert(todo, function ()
local ok, err = pcall(secpol, nexus, settings, proc.pkg, pid, perm, req)
if not ok then
neo.emergency("Used fallback policy because of run-err: " .. err)
req(def)
@ -132,19 +195,41 @@ rootAccess.securityPolicy = function (pid, proc, perm, req)
end)
end
event.listen("k.procdie", function (evt, pkg, pid, reason)
if targsDH[pid] then
targsDH[pid]()
end
targsDH[pid] = nil
if reason then
-- Process death logging in console (for lifecycle dbg)
-- neo.emergency(n[2])
-- neo.emergency(n[4])
neoux.startDialog(string.format("%s/%i died:\n%s", pkg, pid, reason), "error")
end
end)
while true do
event.pull()
local ev = {coroutine.yield()}
if ev[1] == "k.procdie" then
local _, pkg, pid, reason = table.unpack(ev)
if targsDH[pid] then
targsDH[pid]()
end
targsDH[pid] = nil
if reason then
nexus.startDialog(string.format("%s/%i died:\n%s", pkg, pid, reason), "error")
end
elseif ev[1] == "k.timer" then
local nt = todo
todo = {}
for _, v in ipairs(nt) do
local ok, e = pcall(v)
if not ok then
nexus.startDialog(tostring(e), "terr")
end
end
elseif ev[1] == "k.registration" then
if ev[2] == "x.neo.pub.window" then
local nt = onEverest
onEverest = {}
for _, v in ipairs(nt) do
coroutine.resume(v, neo.requestAccess("x.neo.pub.window"))
end
end
elseif ev[1] == "x.neo.pub.window" then
local v = everestWindows[ev[2]]
if v then
resumeWF(v, table.unpack(ev, 3))
if coroutine.status(v) == "dead" then
everestWindows[ev[2]] = nil
end
end
end
end

View File

@ -1,7 +1,7 @@
return {
["neo"] = {
desc = "KittenOS NEO Kernel & Base Libs",
v = 0,
v = 1,
deps = {
},
dirs = {
@ -14,6 +14,7 @@ return {
"apps/sys-glacier.lua",
"libs/event.lua",
"libs/serial.lua",
"libs/fmttext.lua",
"libs/neoux.lua",
"libs/braille.lua",
"libs/sys-filewrap.lua"
@ -62,7 +63,7 @@ return {
},
["neo-icecap"] = {
desc = "KittenOS NEO / Icecap",
v = 0,
v = 1,
deps = {
"neo"
},
@ -79,20 +80,19 @@ return {
},
["neo-secpolicy"] = {
desc = "KittenOS NEO / Secpolicy",
v = 0,
v = 1,
deps = {
},
dirs = {
"libs"
},
files = {
"libs/sys-secpolicy.lua",
"libs/sys-criticals.lua"
"libs/sys-secpolicy.lua"
}
},
["neo-coreapps"] = {
desc = "KittenOS NEO Core Apps",
v = 0,
v = 1,
deps = {
"neo"
},
@ -136,7 +136,7 @@ return {
},
["app-claw"] = {
desc = "KittenOS NEO Package Manager",
v = 0,
v = 1,
deps = {
"neo"
},

View File

@ -101,12 +101,23 @@ end
uniqueNEOProtectionObject = {}
wrapMetaCache = {}
setmetatable(wrapMetaCache, {__mode = "v"})
function wrapMeta(t)
if type(t) == "table" then
if wrapMetaCache[t] then
return wrapMetaCache[t]
end
local t2 = {}
wrapMetaCache[t] = t2
setmetatable(t2, {
__index = function (a, k) return wrapMeta(t[k]) end,
__newindex = error,
-- WTF
__call = function (_, ...)
return t(...)
end,
__pairs = function (a)
return function (x, key)
local k, v = next(t, k)
@ -392,8 +403,8 @@ function retrieveAccess(perm, pkg, pid)
local temporary = nil
local t = perm:sub(3)
if t == "filesystem" then
primary = primaryDisk
temporary = component.proxy(computer.tmpAddress())
primary = wrapMeta(primaryDisk)
temporary = wrapMeta(component.proxy(computer.tmpAddress()))
end
return {
list = function ()
@ -401,7 +412,7 @@ function retrieveAccess(perm, pkg, pid)
return function ()
local ii = i()
if not ii then return nil end
return component.proxy(ii)
return wrapMeta(component.proxy(ii))
end
end,
primary = primary,
@ -556,10 +567,7 @@ function start(pkg, ...)
-- Note the target process doesn't get the procnew (the dist occurs before it's creation)
pcall(distEvent, nil, "k.procnew", pkg, pid)
processes[pid] = proc
-- For processes waiting on others, this at least tries to guarantee some safety.
if not pcall(execEvent, pid, ...) then
return nil, "neocore"
end
table.insert(timers, {0, execEvent, pid, ...})
return pid
end

67
code/libs/fmttext.lua Normal file
View File

@ -0,0 +1,67 @@
local fmt
fmt = {
pad = function (t, len, centre, cut)
local l = unicode.len(t)
local add = len - l
if add > 0 then
if centre then
t = (" "):rep(math.floor(add / 2)) .. t .. (" "):rep(math.ceil(add / 2))
else
t = t .. (" "):rep(add)
end
end
if cut then
t = unicode.sub(t, 1, len)
end
return t
end,
fmtText = function (text, w)
local nl = text:find("\n")
if nl then
local base = text:sub(1, nl - 1)
local ext = text:sub(nl + 1)
local baseT = fmt.fmtText(base, w)
local extT = fmt.fmtText(ext, w)
for _, v in ipairs(extT) do
table.insert(baseT, v)
end
return baseT
end
if unicode.len(text) > w then
local lastSpace
for i = 1, w do
if unicode.sub(text, i, i) == " " then
-- Check this isn't an inserted space (unicode safe text format)
local ok = true
if i > 1 then
if unicode.charWidth(unicode.sub(text, i - 1, i - 1)) ~= 1 then
ok = false
end
end
if ok then
lastSpace = i
end
end
end
local baseText, extText
if not lastSpace then
-- Break at a 1-earlier boundary
local wEffect = w
if unicode.charWidth(unicode.sub(text, w, w)) ~= 1 then
-- Guaranteed to be safe, so
wEffect = wEffect - 1
end
baseText = unicode.sub(text, 1, wEffect)
extText = unicode.sub(text, wEffect + 1)
else
baseText = unicode.sub(text, 1, lastSpace - 1)
extText = unicode.sub(text, lastSpace + 1)
end
local full = fmt.fmtText(extText, w)
table.insert(full, 1, fmt.pad(baseText, w))
return full
end
return {fmt.pad(text, w)}
end
}
return neo.wrapMeta(fmt)

View File

@ -16,57 +16,12 @@
-- Global forces reference. Otherwise, nasty duplication happens.
newNeoux = function (event, neo)
-- this is why neo access is 'needed'
local function retrieveIcecap()
return neo.requestAccess("x.neo.pub.base")
end
local function retrieveEverest()
return neo.requestAccess("x.neo.pub.window")
end
-- id -> {lclEv, w, h, title, callback}
local windows = {}
-- id -> callback
local lclEvToW = {}
retrieveEverest()
local function everestDied()
for _, v in pairs(windows) do
v[1] = nil
end
lclEvToW = {}
end
local function pushWindowToEverest(k)
local everest = retrieveEverest()
if not everest then
everestDied()
return
end
local v = windows[k]
local r, res = pcall(everest, v[2], v[3], v[4])
if not r then
everestDied()
return
else
-- res is the window!
lclEvToW[res.id] = k
windows[k][1] = res
end
end
event.listen("k.registration", function (_, xe)
if #windows > 0 then
if xe == "x.neo.pub.window" then
for k, v in pairs(windows) do
pushWindowToEverest(k)
end
end
end
end)
event.listen("k.deregistration", function (_, xe)
if xe == "x.neo.pub.window" then
everestDied()
end
end)
local everest = neo.requireAccess("x.neo.pub.window", "windowing")
event.listen("x.neo.pub.window", function (_, window, tp, ...)
if lclEvToW[window] then
windows[lclEvToW[window]][5](tp, ...)
lclEvToW[window](tp, ...)
end
end)
local neoux = {}
@ -80,7 +35,7 @@ newNeoux = function (event, neo)
rtt = rt
end
end
local tag = retrieveIcecap().showFileDialogAsync(forWrite)
local tag = neo.requestAccess("x.neo.pub.base").showFileDialogAsync(forWrite)
local f
f = function (_, fd, tg, re)
if fd == "filedialog" then
@ -99,117 +54,40 @@ newNeoux = function (event, neo)
-- Creates a wrapper around a window.
neoux.create = function (w, h, title, callback)
local window = {}
local windowCore = {nil, w, h, title, function (...) callback(window, ...) end}
local k = #windows + 1
table.insert(windows, windowCore)
pushWindowToEverest(k)
local windowCore = everest(w, h, title)
-- res is the window!
lclEvToW[windowCore.id] = function (...) callback(window, ...) end
-- API convenience: args compatible with .create
window.reset = function (nw, nh, _, cb)
callback = cb
if nw or nh then
windowCore[2] = nw
windowCore[3] = nh
end
if windowCore[1] then
windowCore[1].setSize(windowCore[2], windowCore[3])
end
w = nw or w
h = nh or h
windowCore.setSize(w, h)
end
window.getSize = function ()
return windowCore[2], windowCore[3]
return w, h
end
window.getDepth = function ()
if windowCore[1] then
return windowCore[1].getDepth()
end
return 1
end
window.setSize = function (w, h)
windowCore[2] = w
windowCore[3] = h
if windowCore[1] then
windowCore[1].setSize(w, h)
end
end
window.span = function (x, y, text, bg, fg)
if windowCore[1] then
pcall(windowCore[1].span, x, y, text, bg, fg)
end
window.getDepth = windowCore.getDepth
window.setSize = function (nw, nh)
w = nw
h = nh
windowCore.setSize(w, h)
end
window.span = windowCore.span
window.close = function ()
if windowCore[1] then
windowCore[1].close()
lclEvToW[windowCore[1].id] = nil
windowCore[1] = nil
end
windows[k] = nil
windowCore.close()
lclEvToW[windowCore.id] = nil
windowCore = nil
end
return window
end
-- Padding function
neoux.pad = function (t, len, centre, cut)
local l = unicode.len(t)
local add = len - l
if add > 0 then
if centre then
t = (" "):rep(math.floor(add / 2)) .. t .. (" "):rep(math.ceil(add / 2))
else
t = t .. (" "):rep(add)
end
end
if cut then
t = unicode.sub(t, 1, len)
end
return t
end
neoux.pad = require("fmttext").pad
-- Text dialog formatting function.
-- Assumes you've run unicode.safeTextFormat if need be
neoux.fmtText = function (text, w)
local nl = text:find("\n")
if nl then
local base = text:sub(1, nl - 1)
local ext = text:sub(nl + 1)
local baseT = neoux.fmtText(base, w)
local extT = neoux.fmtText(ext, w)
for _, v in ipairs(extT) do
table.insert(baseT, v)
end
return baseT
end
if unicode.len(text) > w then
local lastSpace
for i = 1, w do
if unicode.sub(text, i, i) == " " then
-- Check this isn't an inserted space (unicode safe text format)
local ok = true
if i > 1 then
if unicode.charWidth(unicode.sub(text, i - 1, i - 1)) ~= 1 then
ok = false
end
end
if ok then
lastSpace = i
end
end
end
local baseText, extText
if not lastSpace then
-- Break at a 1-earlier boundary
local wEffect = w
if unicode.charWidth(unicode.sub(text, w, w)) ~= 1 then
-- Guaranteed to be safe, so
wEffect = wEffect - 1
end
baseText = unicode.sub(text, 1, wEffect)
extText = unicode.sub(text, wEffect + 1)
else
baseText = unicode.sub(text, 1, lastSpace - 1)
extText = unicode.sub(text, lastSpace + 1)
end
local full = neoux.fmtText(extText, w)
table.insert(full, 1, neoux.pad(baseText, w))
return full
end
return {neoux.pad(text, w)}
neoux.fmtText = function (...)
local fmt = require("fmttext")
return fmt.fmtText(...)
end
-- UI FRAMEWORK --
neoux.tcwindow = function (w, h, controls, closing, bg, fg, selIndex)

View File

@ -2,8 +2,8 @@
-- No warranty is provided, implied or otherwise.
-- just don't bother with proper indent here
return function (event, neoux, retFunc, fs, pkg, mode)
return function (event, nexus, retFunc, fs, pkg, mode)
local fmt = require("fmttext")
local class = "manage"
if mode ~= nil then
if mode then
@ -23,7 +23,7 @@ local function cb(...)
name = "F.M. Error",
list = function ()
local l = {}
for k, v in ipairs(neoux.fmtText(unicode.safeTextFormat(e), 25)) do
for k, v in ipairs(fmt.fmtText(unicode.safeTextFormat(e), 25)) do
l[k] = {v, function () return true end}
end
return l
@ -64,7 +64,7 @@ local function prepareNodeI(node)
if sel then
colB, colA = 0xFFFFFF, 0
end
wnd.span(1, a, neoux.pad(unicode.safeTextFormat(text), w, cen, true), colA, colB)
wnd.span(1, a, fmt.pad(unicode.safeTextFormat(text), w, cen, true), colA, colB)
end
local function flush(wnd)
for i = 1, h do
@ -108,7 +108,7 @@ local function prepareNodeI(node)
end
if aResult then
retFunc(res)
wnd.close()
nexus.close(wnd)
else
prepareNode(res)
end
@ -148,13 +148,13 @@ local function prepareNodeI(node)
end
if evt == "close" then
retFunc(nil)
wnd.close()
nexus.close(wnd)
end
end
end
local text = class .. " " .. pkg
local window = neoux.create(25, 10, text, cb)
local window
function prepareNode(node)
local w, h, c = prepareNodeI(node)
@ -162,10 +162,24 @@ function prepareNode(node)
window.setSize(w, h)
end
prepareNode(require("sys-filevfs")(fs, mode))
local closer = nexus.createNexusThread(function ()
window = nexus.create(25, 10, text)
prepareNode(require("sys-filevfs")(fs, mode))
while window do
cb(window, coroutine.yield())
end
end)
if not closer then
retFunc()
return
end
return function ()
retFunc()
window.close()
closer()
if window then
nexus.close(window)
window = nil
end
end
-- end bad indent

View File

@ -51,39 +51,92 @@ local actualPolicy = function (pkg, pid, perm)
return "ask"
end
return function (neoux, settings, pkg, pid, perm, rsp)
return function (nexus, settings, pkg, pid, perm, rsp)
local res = actualPolicy(pkg, pid, perm)
if res == "ask" and settings then
res = settings.getSetting("perm|" .. pkg .. "|" .. perm) or "ask"
end
if res == "ask" and neoux then
local fmt = neoux.fmtText(unicode.safeTextFormat(string.format("%s/%i wants:\n%s\nAllow this?", pkg, pid, perm)), 20)
local always = "Always"
local yes = "Yes"
local no = "No"
local totalW = (#yes) + (#always) + (#no) + 8
neoux.create(20, #fmt + 2, "security", neoux.tcwindow(20, #fmt + 3, {
neoux.tcbutton(1, #fmt + 2, no, function (w)
if res == "ask" and nexus then
local totalW = 3 + 6 + 2 + 8
local fmt = require("fmttext").fmtText(unicode.safeTextFormat(string.format("%s/%i wants:\n%s\nAllow this?\n\n", pkg, pid, perm)), totalW)
local buttons = {
{"<No>", function (w)
rsp(false)
w.close()
end),
neoux.tcbutton(totalW - ((#yes) + 1), #fmt + 2, yes, function (w)
rsp(true)
w.close()
end),
neoux.tcbutton((#yes) + 3, #fmt + 2, always, function (w)
nexus.close(w)
end},
{"<Always>", function (w)
if settings then
settings.setSetting("perm|" .. pkg .. "|" .. perm, "allow")
end
rsp(true)
w.close()
end),
neoux.tchdivider(1, #fmt + 1, 21),
neoux.tcrawview(1, 1, fmt),
}, function (w)
rsp(false)
w.close()
end, 0xFFFFFF, 0))
nexus.close(w)
end},
{"<Yes>", function (w)
rsp(true)
nexus.close(w)
end}
}
nexus.createNexusThread(function ()
local window = nexus.create(totalW, #fmt, "security")
local cButton = 0
local ev, a, b, c
while true do
if not ev then
ev, a, b, c = coroutine.yield()
end
if ev == "line" or ev == "touch" then
local cor = b
if ev == "line" then
cor = a
if fmt[a] then
window.span(1, a, fmt[a], 0xFFFFFF, 0)
end
end
if cor == #fmt then
local x = 1
for k, v in ipairs(buttons) do
if ev == "line" then
if k ~= cButton + 1 then
window.span(x, a, v[1], 0xFFFFFF, 0)
else
window.span(x, a, v[1], 0, 0xFFFFFF)
end
elseif a >= x and a < (x + #v[1]) then
cButton = k - 1
ev = "key"
a = 32
b = 0
c = true
break
end
x = x + #v[1] + 1
end
end
elseif ev == "close" then
rsp(false)
nexus.close(window)
return
end
if ev == "key" then
if c and (a == 9 or b == 205) then
cButton = (cButton + 1) % #buttons
ev = "line"
a = #fmt
elseif c and b == 203 then
cButton = (cButton - 1) % #buttons
ev = "line"
a = #fmt
elseif c and (a == 13 or a == 32) then
buttons[cButton + 1][2](window)
ev = nil
else
ev = nil
end
else
ev = nil
end
end
end)
else
rsp(res == "allow")
end

View File

@ -7,3 +7,11 @@ rm code.tar
# Hey, look behind you, there's nothing to see here.
# ... ok, are they seriously all named "Mann"?
tar --owner=gray:0 --group=mann:0 -cf code.tar code
lua heroes.lua `wc -c code.tar` > inst.lua
stat repobuild/data/app-claw/local.lua && rm -rf repobuild
mkdir repobuild
cp -r code/* repobuild/
cp -r repository/* repobuild/
cp inst.lua repobuild/
lua clawmerge.lua repository/data/app-claw/local.lua code/data/app-claw/local.lua > repobuild/data/app-claw/local.lua