From 55bdeb56b8ba6a78968de8d924301efe7260f9db Mon Sep 17 00:00:00 2001 From: Mark Jansen Date: Fri, 29 Mar 2024 00:28:11 +0100 Subject: [PATCH] Add win32/win64 option to precompile bytecode --- README.md | 1 + how_to_x.md | 9 +++++++++ makelove/bytecode.py | 31 +++++++++++++++++++++++++++++++ makelove/config.py | 2 ++ makelove/love-luac/conf.lua | 5 +++++ makelove/love-luac/main.lua | 26 ++++++++++++++++++++++++++ makelove/windows.py | 16 ++++++++++++++++ makelove_full.toml | 5 +++++ 8 files changed, 95 insertions(+) create mode 100644 makelove/bytecode.py create mode 100644 makelove/love-luac/conf.lua create mode 100644 makelove/love-luac/main.lua diff --git a/README.md b/README.md index 11e439e..362e8e6 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ A packaging tool for [löve](https://love2d.org) games * Proper handling of shared libraries (both Lua modules and FFI)! * Packaging of those binaries in archives, including extra files * Versioned builds +* Precompile lua scripts to bytecode (win32 and win64 and only on Windows) * Control and customization along the way: - Configure which targets to build - Which files to include in the .love with a list of include/exclude patterns diff --git a/how_to_x.md b/how_to_x.md index 389ac56..3769a9d 100644 --- a/how_to_x.md +++ b/how_to_x.md @@ -35,3 +35,12 @@ And finally build without prebuild, but with postbuild ``` makelove -d prebuild -v 1.2.3 appimage (--resume) ``` + +## Compile lua scripts to bytecode +Use the option `compile_lua = true` in `makelove.toml` in the `win32` or `win64` sections. +The created .exe will now contain bytecode instead of scripts. + +To check if a script is compiled as bytecode, you can add this code to your game: +```lua +is_compiled = (love.filesystem.read('main.lua', 1) == '\x1b' and true or false) +``` diff --git a/makelove/bytecode.py b/makelove/bytecode.py new file mode 100644 index 0000000..fcd708c --- /dev/null +++ b/makelove/bytecode.py @@ -0,0 +1,31 @@ +import os +import subprocess +import zipfile +import base64 + +def compile_file(love_binary, data, filename): + # Compile a single .lua file to bytecode using love. + # To support this, we have a folder 'love-luac' that will be recognized as 'game' by love + compiler = os.path.join(os.path.dirname(__file__), 'love-luac') + compile_args = [love_binary, compiler] + proc = subprocess.Popen(compile_args, stdin=subprocess.PIPE, stdout=subprocess.PIPE) + # (Binary) data is base64 encoded to work around the fact that lua opens stdin/stdout in text mode on windows, + # which would break the bytecode + out = proc.communicate(input=base64.b64encode(data)) + if proc.returncode == 0: + return base64.b64decode(out[0]) + print('Failed to compile {}: {}'.format(filename, out[0])) + return data + + +def create_compiled_lovezip(love_binary, love_file_path_in, love_file_path_out): + # Create a new zip with all .lua files converted to bytecode + with zipfile.ZipFile(love_file_path_in, 'r') as zip_in: + with zipfile.ZipFile(love_file_path_out, 'w') as zip_out: + zip_out.comment = zip_in.comment # preserve the comment + for item in zip_in.infolist(): + data = zip_in.read(item.filename) + if item.filename.endswith('.lua'): + data = compile_file(love_binary, data, item.filename) + zip_out.writestr(item, data) + diff --git a/makelove/config.py b/makelove/config.py index f10468b..0366426 100644 --- a/makelove/config.py +++ b/makelove/config.py @@ -70,6 +70,7 @@ "love_binaries": val.Path(), "shared_libraries": val.List(val.Path()), "artifacts": val.ValueOrList(val.Choice("directory", "archive")), + "compile_lua": val.Bool(), } ), "win64": val.Section( @@ -77,6 +78,7 @@ "love_binaries": val.Path(), "shared_libraries": val.List(val.Path()), "artifacts": val.ValueOrList(val.Choice("directory", "archive")), + "compile_lua": val.Bool(), } ), "linux": val.Section( diff --git a/makelove/love-luac/conf.lua b/makelove/love-luac/conf.lua new file mode 100644 index 0000000..4dd0507 --- /dev/null +++ b/makelove/love-luac/conf.lua @@ -0,0 +1,5 @@ +-- love configuration handler to suppress the window popping up. + +function love.conf(t) + t.window = nil +end diff --git a/makelove/love-luac/main.lua b/makelove/love-luac/main.lua new file mode 100644 index 0000000..45acb0d --- /dev/null +++ b/makelove/love-luac/main.lua @@ -0,0 +1,26 @@ +--[[ + Handler used to convert lua code to bytecode. + This is setup as a 'game', so that the love binary that we are appending to can run this. +]] + +function love.load() + -- Read input from stdin + -- This is base64 encoded to ensure no translation occurs on windows + local source = love.data.decode("string", "base64", io.read()) + -- Load the data + local lua_data = assert(load(source)) + -- Convert to bytecode, strip debug symbols + local stripped = assert(string.dump(lua_data, true)) + -- Encode the bytecode to base64 again + local encoded = love.data.encode('string', 'base64', stripped, 0) + -- Send it back + io.write(encoded) + -- Great success + os.exit(0, true) +end + +function love.errorhandler(msg) + msg = tostring(msg) + print(msg) + os.exit(1, true) +end diff --git a/makelove/windows.py b/makelove/windows.py index 1ae4177..106ac63 100644 --- a/makelove/windows.py +++ b/makelove/windows.py @@ -11,6 +11,7 @@ from .util import get_default_love_binary_dir, get_download_url, tmpfile, eprint from .config import should_build_artifact +from .bytecode import create_compiled_lovezip def common_prefix(l): @@ -217,12 +218,27 @@ def build_windows(config, version, target, target_directory, love_file_path): ) print("If you are using a POSIX-compliant system, try installing WINE.") + temp_love_archive = None + if config[target].get("compile_lua", False): + if not sys.platform.startswith("win32"): + # TODO: For x64 targets we could try to grab the win64 binary? + sys.exit("Cannot compile lua on this platform, please run me on windows") + original_love_file = love_file_path + temp_love_archive = dest("compiled.love") + create_compiled_lovezip(src("love_orig.exe"), original_love_file, temp_love_archive) + # Fuse with the generated archive instead the original one + love_file_path = temp_love_archive + with open(target_exe_path, "wb") as fused: with open(src("love.exe"), "rb") as loveExe: with open(love_file_path, "rb") as loveZip: fused.write(loveExe.read()) fused.write(loveZip.read()) + # Did we create a temp file? + if temp_love_archive: + os.remove(temp_love_archive) + copy("license.txt") for f in os.listdir(love_binaries): if f.endswith(".dll"): diff --git a/makelove_full.toml b/makelove_full.toml index aa0c3d5..7a97bb3 100644 --- a/makelove_full.toml +++ b/makelove_full.toml @@ -114,6 +114,11 @@ shared_libraries = [ # after the .zip has been built. artifacts = "archive" +# Whether or not to compile lua code to bytecode. +# If this option is set to true, the build needs to run on windows, because love.exe for windows +# is used to compile the lua files. +compile_lua = false + # The values above for the target win32 can also be set for the win64 target [macos]