CI: minified builds (ImGui.min.luau, ImGui-nodemo.min.luau, imgui_love.min.lua, imgui_love-nodemo.min.lua) via tools/minify.py; checks + release assets; run.py MINIFY=1

This commit is contained in:
4DBug
2026-10-14 10:23:55 -05:00
parent 1431d2e19d
commit e502866075
3 changed files with 129 additions and 2 deletions
+20 -2
View File
@@ -36,6 +36,11 @@ jobs:
python3 tools/bundle.py --no-demo dist/roblox-nodemo # ImGui.luau (library only)
python3 tools/bundle.py --target love dist/love # imgui_love.lua (library + demo)
python3 tools/bundle.py --target love --no-demo dist/love-nodemo
# minified variants (comments/whitespace stripped, same code): tools/minify.py
python3 tools/minify.py dist/roblox/ImGui.luau dist/ImGui.min.luau
python3 tools/minify.py dist/roblox-nodemo/ImGui.luau dist/ImGui-nodemo.min.luau
python3 tools/minify.py dist/love/imgui_love.lua dist/imgui_love.min.lua
python3 tools/minify.py dist/love-nodemo/imgui_love.lua dist/imgui_love-nodemo.min.lua
- name: Check
run: |
@@ -45,12 +50,23 @@ jobs:
sed 's|require(script:WaitForChild("Impl"))|nil|' dist/roblox/ImGuiTyped.luau > /tmp/ImGuiTyped.luau && luau-analyze /tmp/ImGuiTyped.luau
luajit -bl dist/love/imgui_love.lua > /dev/null
luajit -bl dist/love-nodemo/imgui_love.lua > /dev/null
python3 tools/minify.py --test
luau-compile --null dist/ImGui.min.luau
luau-compile --null dist/ImGui-nodemo.min.luau
luajit -bl dist/imgui_love.min.lua > /dev/null
luajit -bl dist/imgui_love-nodemo.min.lua > /dev/null
python3 tools/test/run.py --main tools/test/scripts/t_readme.luau --frames 30 --quiet --roblox --timeout 300 | tee /tmp/r.txt
grep -q "README OK" /tmp/r.txt
python3 tools/test/run.py --main tools/test/scripts/t_cached_window.luau --frames 130 --quiet --roblox --timeout 300 | tee /tmp/c.txt
grep -q "CACHED OK" /tmp/c.txt
MINIFY=1 python3 tools/test/run.py --main tools/test/scripts/t_readme.luau --frames 30 --quiet --roblox --timeout 300 | tee /tmp/rm.txt
grep -q "README OK" /tmp/rm.txt
python3 tools/bundle.py --target love >/dev/null
luajit tools/test/love/t_drawhash.lua 60 Widgets,Basic
luajit tools/test/love/t_drawhash.lua 60 Widgets,Basic | tee /tmp/h.txt
cp build/imgui_love.lua /tmp/imgui_love.lua && cp dist/imgui_love.min.lua build/imgui_love.lua
luajit tools/test/love/t_drawhash.lua 60 Widgets,Basic | tee /tmp/hm.txt
cp /tmp/imgui_love.lua build/imgui_love.lua
diff /tmp/h.txt /tmp/hm.txt # minified LÖVE bundle draws exactly the same
luajit tools/test/love/t_examples.lua
- name: Package
@@ -80,6 +96,8 @@ jobs:
dist/ImGui-nodemo.luau
dist/imgui_love.lua
dist/imgui_love-nodemo.lua
dist/*.min.luau
dist/*.min.lua
dist/*.zip
generate_release_notes: true
@@ -105,4 +123,4 @@ jobs:
gh release delete latest --yes --cleanup-tag || true
gh release create latest --latest --title "Latest build (${GITHUB_SHA::7})" \
--notes "Automatic build of ${GITHUB_SHA} on ${GITHUB_REF_NAME}. Tag a commit v* for a versioned release." \
dist/ImGui.luau dist/ImGui-nodemo.luau dist/imgui_love.lua dist/imgui_love-nodemo.lua dist/*.zip
dist/ImGui.luau dist/ImGui-nodemo.luau dist/imgui_love.lua dist/imgui_love-nodemo.lua dist/*.min.luau dist/*.min.lua dist/*.zip
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
# Minifies a Lua/Luau file: drops comments, indentation, blank lines and spaces that don't separate tokens.
# Kept: strings (verbatim), "--!" directive lines (--!optimize, --!native, ...), @native attributes, and one newline
# wherever the source had one (so no ambiguous-call issues, and error line numbers stay roughly meaningful).
# python3 tools/minify.py IN OUT # write minified IN to OUT
# python3 tools/minify.py --same-code F.. # check F.. have the same code tokens as `git show HEAD:F` (comment-only edits)
# python3 tools/minify.py --same-code-as OLD NEW # same check between two files
# python3 tools/minify.py --test # self-check
import re, subprocess, sys
from functools import lru_cache
TOKEN = re.compile(r"""
(?P<ws>[ \t\r\n\f\v]+)
| (?P<lcom>--\[(?P<ceq>=*)\[.*?\](?P=ceq)\])
| (?P<com>--[^\n]*)
| (?P<lstr>\[(?P<seq>=*)\[.*?\](?P=seq)\])
| (?P<str>"(?:\\.|\\\n|[^"\\\n])*"|'(?:\\.|\\\n|[^'\\\n])*'|`(?:\\.|\\\n|[^`\\])*`)
| (?P<num>0[xX][0-9a-fA-F_]*(?:\.[0-9a-fA-F_]*)?(?:[pP][+-]?\d+)?[uUlLi]*|0[bB][01_]+|(?:\d[\d_]*(?:\.[\d_]*)?|\.\d[\d_]*)(?:[eE][+-]?\d+)?[uUlLi]*)
| (?P<name>@?[A-Za-z_]\w*)
| (?P<op>\.\.\.|\.\.=|//=|\.\.|==|~=|<=|>=|//|::|->|[-+*/%^]=|.)
""", re.S | re.X)
def tokenize(src):
"""[(kind, text, newline_before)]; comments/whitespace folded into newline_before ("--!" lines kept as 'dir')."""
out, nl, pos = [], False, 0
while pos < len(src):
m = TOKEN.match(src, pos)
kind, text = m.lastgroup, m.group(0)
if kind in ("ceq", "seq"): kind = "lcom" if text.startswith("--") else "lstr"
pos = m.end()
if kind == "ws" or kind == "lcom" or (kind == "com" and not text.startswith("--!")):
nl = nl or "\n" in text
continue
if kind == "com": kind = "dir"
if kind == "lstr" or kind == "str": kind = "str"
out.append((kind, text, nl))
nl = False
return out
@lru_cache(maxsize=None)
def glue_ok(a, b):
"""True if `a..b` with no space still tokenizes as exactly a, b."""
if re.match(r"\w", a[-1]) and re.match(r"\w", b[0]): return False # real lexers read `0then`/`x1` as one token
return [t for _, t, _ in tokenize(a + b)] == [a, b]
def minify(src):
toks, out = tokenize(src), []
prev = None
for kind, text, nl in toks:
if prev is not None:
if nl or prev[0] == "dir" or kind == "dir": out.append("\n")
elif kind == "str" or prev[0] == "str": # strings are self-delimiting; only `[` + `[[` would fuse
if kind == "str" and text[0] == "[" and prev[1].endswith("["): out.append(" ")
elif not glue_ok(prev[1], text):
out.append(" ")
out.append(text)
prev = (kind, text)
return "".join(out) + "\n"
def code(src):
return [(k, t) for k, t, _ in tokenize(src)]
def test():
cases = {
"local a = 1 -- c\nlocal b = a - -1\n": "local a=1\nlocal b=a- -1\n",
"x = 1 .. 2": "x=1 ..2\n",
"x = t[ [[s]] ]": "x=t[ [[s]]]\n",
"x = t[ [==[s]==] ]": "x=t[ [==[s]==]]\n",
"--!optimize 2\n--[[ long\ncomment ]] local s = 'a -- b' .. \"c\\\"d\"": "--!optimize 2\nlocal s='a -- b'..\"c\\\"d\"\n",
"@native\nfunction f(...) return ... end": "@native\nfunction f(...)return...end\n",
"a = b == c and d ~= e or f <= g": "a=b==c and d~=e or f<=g\n",
"x = 0x10 + 1e-5 + .5": "x=0x10+1e-5+.5\n",
"f(a)\n\n\n(g)(b)": "f(a)\n(g)(b)\n",
"s = [[a\n-- not a comment\n]]": "s=[[a\n-- not a comment\n]]\n",
"x = a / / b": "x=a/ /b\n",
"x = a . . b": "x=a. .b\n",
"if a == 0 then return 1 end": "if a==0 then return 1 end\n",
"x = 1 .. y": "x=1 ..y\n",
}
for src, want in cases.items():
got = minify(src)
assert got == want, (src, got, want)
assert code(minify(src)) == code(src), src
print("minify OK")
if __name__ == "__main__":
if sys.argv[1] == "--test":
test()
elif sys.argv[1] in ("--same-code", "--same-code-as"):
bad = 0
pairs = [(sys.argv[2], sys.argv[3])] if sys.argv[1] == "--same-code-as" else [(None, f) for f in sys.argv[2:]]
for ref, f in pairs:
old = open(ref).read() if ref else subprocess.run(["git", "show", "HEAD:" + f], capture_output=True, text=True, check=True).stdout
a, b = code(old), code(open(f).read())
if a != b:
bad += 1
i = next((i for i, (x, y) in enumerate(zip(a, b)) if x != y), min(len(a), len(b)))
print("%s: CODE CHANGED at token %d: %r -> %r" % (f, i, a[i:i + 6], b[i:i + 6]))
else:
print("%s: same code" % f)
sys.exit(1 if bad else 0)
else:
src = open(sys.argv[1], encoding="utf-8").read()
dst = minify(src)
assert code(dst) == code(src), "minify changed code tokens"
open(sys.argv[2], "w", encoding="utf-8").write(dst)
print("wrote %s (%d -> %d bytes, %.0f%%)" % (sys.argv[2], len(src.encode()), len(dst.encode()), 100.0 * len(dst) / len(src)))
+2
View File
@@ -16,6 +16,8 @@ G = "game, workspace, Instance, Enum, Vector2, Vector3, UDim2, Color3, Content,
def build_run(args, flags):
subprocess.run([sys.executable, os.path.join(ROOT, "tools", "bundle.py"), TMP] + (["--no-demo"] if os.environ.get("NO_DEMO") else []), check=True, stdout=subprocess.DEVNULL)
if os.environ.get("MINIFY"): # test the minified bundle instead (error line numbers won't map back to sources)
subprocess.run([sys.executable, os.path.join(ROOT, "tools", "minify.py"), os.path.join(TMP, "ImGui.luau"), os.path.join(TMP, "ImGui.luau")], check=True, stdout=subprocess.DEVNULL)
out = ["--" if args.roblox else "--!native", "--!optimize 2", flags, "local SOURCE_FNS = {}"]
def add(key, path):
s = "\n".join(l for l in open(path).read().split("\n") if not l.startswith("--!"))