diff --git a/esp32/scripts/build_app.py b/esp32/scripts/build_app.py index 712edbc..61969c4 100644 --- a/esp32/scripts/build_app.py +++ b/esp32/scripts/build_app.py @@ -7,12 +7,14 @@ # Copyright (C) 2018 - 2023 rjwats # Copyright (C) 2023 theelims # Copyright (C) 2023 Maxtrium B.V. [ code available under dual license ] +# Copyright (C) 2024 runeharlyk # # All Rights Reserved. This software may be modified and distributed under # the terms of the LGPL v3 license. See the LICENSE file for details. from pathlib import Path -from shutil import copyfileobj +from shutil import copytree, rmtree, copyfileobj +from os.path import exists, getmtime import os import gzip import mimetypes @@ -20,31 +22,35 @@ import glob from datetime import datetime Import("env") -buildFlags = env.ParseFlags(env["BUILD_FLAGS"]) -project_dir = env["PROJECT_DIR"] -app_dir = project_dir + "/../app2" -output_file = project_dir + "/lib/ESP32-sveltekit/WWWData.h" -source_www_dir = app_dir + "/src" -build_www_dir = app_dir + "/build" -www_dir = "www" +project_dir = env["PROJECT_DIR"] +buildFlags = env.ParseFlags(env["BUILD_FLAGS"]) + +interface_dir = project_dir + "/../app" +output_file = project_dir + "/lib/ESP32-Sveltekit/WWWData.h" +source_www_dir = interface_dir + "/src" +build_dir = interface_dir + "/build" +filesystem_dir = project_dir + "/data/www" + def find_latest_timestamp_for_app(): - return max((os.path.getmtime(f) for f in glob.glob(f'{source_www_dir}/**/*', recursive=True))) + return max( + (getmtime(f) for f in glob.glob(f"{source_www_dir}/**/*", recursive=True)) + ) + def should_regenerate_output_file(): - if not flag_exists("EMBED_WWW") or not os.path.exists(output_file): + if not flag_exists("EMBED_WWW") or not exists(output_file): return True last_source_change = find_latest_timestamp_for_app() - last_build = os.path.getmtime(output_file) + last_build = getmtime(output_file) + + print( + f"Newest file: {datetime.fromtimestamp(last_source_change)}, output file: {datetime.fromtimestamp(last_build)}" + ) + + return last_build < last_source_change - print(f'Newest file: {datetime.fromtimestamp(last_source_change)}, output file: {datetime.fromtimestamp(last_build)}') - - if last_build < last_source_change: - print("Svelte files updated. Regenerating.") - return True - print("Output file up-to-date.") - return False def gzip_file(file): with open(file, 'rb') as f_in: @@ -56,58 +62,104 @@ def flag_exists(flag): for define in buildFlags.get("CPPDEFINES"): if (define == flag or (isinstance(define, list) and define[0] == flag)): return True + return False -def build_progmem(): - mimetypes.init() - with open(output_file, "w") as progmem: - progmem.write("#include \n") - progmem.write("#include \n\n") - assetMap = {} +def get_package_manager(): + if exists(os.path.join(interface_dir, "pnpm-lock.yaml")): + return "pnpm" + if exists(os.path.join(interface_dir, "yarn.lock")): + return "yarn" + if exists(os.path.join(interface_dir, "package-lock.json")): + return "npm" - for idx, path in enumerate(Path(www_dir).rglob("*.*")): - asset_path = path.relative_to(www_dir).as_posix() - filetype, asset_mime = path.suffix.lstrip('.'), mimetypes.guess_type(asset_path)[0] or 'application/octet-stream' - print(f"Converting {asset_path}") - asset_var = f'WEB_ASSET_DATA_{idx}' - - progmem.write(f'// {asset_path}\n') - progmem.write(f'const uint8_t {asset_var}[] = {{\n ') - file_data = gzip.compress(path.read_bytes()) - - for i, byte in enumerate(file_data): - if i and not (i % 16): - progmem.write('\n\t') - progmem.write(f"0x{byte:02X},") - - progmem.write('\n};\n\n') - assetMap[asset_path] = { "name": asset_var, "mime": asset_mime, "size": len(file_data) } - - progmem.write('typedef std::function RouteRegistrationHandler;\n\n') - progmem.write('class WWWData {\n') - progmem.write('\tpublic:\n') - progmem.write('\t\tstatic void registerRoutes(RouteRegistrationHandler handler) {\n') - - for asset_path, asset in assetMap.items(): - progmem.write(f'\t\t\thandler("/{asset_path}", "{asset["mime"]}", {asset["name"]}, {asset["size"]});\n') - - progmem.write('\t\t}\n') - progmem.write('};\n\n') def build_webapp(): - os.chdir("../app2") - print("Building app with pnpm") - env.Execute("pnpm install") - env.Execute("pnpm run build") - os.chdir("../esp32") + if package_manager := get_package_manager(): + print(f"Building interface with {package_manager}") + os.chdir(interface_dir) + env.Execute(f"{package_manager} install") + env.Execute(f"{package_manager} run build") + os.chdir("..") + else: + raise Exception( + "No lock-file found. Please install dependencies for interface (eg. npm install)" + ) + def embed_webapp(): if flag_exists("EMBED_WWW"): print("Converting interface to PROGMEM") build_progmem() return + add_app_to_filesystem() + + +def build_progmem(): + mimetypes.init() + with open(output_file, "w") as progmem: + progmem.write("#include \n") + progmem.write("#include \n") + + assetMap = {} + + for idx, path in enumerate(Path(build_dir).rglob("*.*")): + asset_path = path.relative_to(build_dir).as_posix() + asset_mime = ( + mimetypes.guess_type(asset_path)[0] or "application/octet-stream" + ) + print(f"Converting {asset_path}") + + asset_var = f"ESP_SVELTEKIT_DATA_{idx}" + progmem.write(f"// {asset_path}\n") + progmem.write(f"const uint8_t {asset_var}[] = {{\n\t") + file_data = gzip.compress(path.read_bytes()) + + for i, byte in enumerate(file_data): + if i and not (i % 16): + progmem.write("\n\t") + progmem.write(f"0x{byte:02X},") + + progmem.write("\n};\n\n") + assetMap[asset_path] = { + "name": asset_var, + "mime": asset_mime, + "size": len(file_data), + } + + progmem.write( + "typedef std::function RouteRegistrationHandler;\n\n" + ) + progmem.write("class WWWData {\n") + progmem.write("\tpublic:\n") + progmem.write( + "\t\tstatic void registerRoutes(RouteRegistrationHandler handler) {\n" + ) + + for asset_path, asset in assetMap.items(): + progmem.write( + f'\t\t\thandler("/{asset_path}", "{asset["mime"]}", {asset["name"]}, {asset["size"]});\n' + ) + + progmem.write("\t\t}\n") + progmem.write("};\n\n") + + +def add_app_to_filesystem(): + build_path = Path(build_dir) + www_path = Path(filesystem_dir) + if www_path.exists() and www_path.is_dir(): + rmtree(www_path) + print("Copying and compress interface to data directory") + copytree(build_path, www_path) + for current_path, _, files in os.walk(www_path): + for file in files: + gzip_file(os.path.join(current_path, file)) + print("Build LittleFS file system image and upload to ESP32") + env.Execute("pio run --target uploadfs") + print("running: build_interface.py") -if (should_regenerate_output_file()): - build_webapp() -embed_webapp() \ No newline at end of file +if should_regenerate_output_file(): + build_webapp() + embed_webapp()