|
7 | 7 | # Copyright (C) 2018 - 2023 rjwats
|
8 | 8 | # Copyright (C) 2023 theelims
|
9 | 9 | # Copyright (C) 2023 Maxtrium B.V. [ code available under dual license ]
|
| 10 | +# Copyright (C) 2024 runeharlyk |
10 | 11 | #
|
11 | 12 | # All Rights Reserved. This software may be modified and distributed under
|
12 | 13 | # the terms of the LGPL v3 license. See the LICENSE file for details.
|
13 | 14 |
|
14 | 15 | from pathlib import Path
|
15 | 16 | from shutil import copytree, rmtree, copyfileobj
|
16 |
| -from subprocess import check_output, Popen, PIPE, STDOUT, CalledProcessError |
17 |
| -from os.path import exists |
18 |
| -from typing import Final |
| 17 | +from os.path import exists, getmtime |
19 | 18 | import os
|
20 | 19 | import gzip
|
21 | 20 | import mimetypes
|
|
24 | 23 |
|
25 | 24 | Import("env")
|
26 | 25 |
|
27 |
| -# print("Current build environment:") |
28 |
| -# print(env.ParseFlags(env["BUILD_FLAGS"]).get("CPPDEFINES")) |
| 26 | +project_dir = env["PROJECT_DIR"] |
| 27 | +buildFlags = env.ParseFlags(env["BUILD_FLAGS"]) |
29 | 28 |
|
30 |
| -# print("Current CLI targets", COMMAND_LINE_TARGETS) |
31 |
| -# print("Current Build targets", BUILD_TARGETS) |
| 29 | +interface_dir = project_dir + "/interface" |
| 30 | +output_file = project_dir + "/lib/framework/WWWData.h" |
| 31 | +source_www_dir = interface_dir + "/src" |
| 32 | +build_dir = interface_dir + "/build" |
| 33 | +filesystem_dir = project_dir + "/data/www" |
32 | 34 |
|
33 |
| -OUTPUTFILE: Final[str] = env["PROJECT_DIR"] + "/lib/framework/WWWData.h" |
34 |
| -SOURCEWWWDIR: Final[str] = env["PROJECT_DIR"] + "/interface/src" |
35 | 35 |
|
36 |
| -def OutputFileExits(): |
37 |
| - return os.path.exists(OUTPUTFILE) |
| 36 | +def find_latest_timestamp_for_app(): |
| 37 | + return max( |
| 38 | + (getmtime(f) for f in glob.glob(f"{source_www_dir}/**/*", recursive=True)) |
| 39 | + ) |
38 | 40 |
|
39 |
| -def findLastestTimeStampWWWInterface(): |
40 |
| - list_of_files = glob.glob(SOURCEWWWDIR+'/**/*', recursive=True) |
41 |
| - # print(list_of_files) |
42 |
| - latest_file = max(list_of_files, key=os.path.getmtime) |
43 |
| - # print(latest_file) |
44 | 41 |
|
45 |
| - return os.path.getmtime(latest_file) |
| 42 | +def should_regenerate_output_file(): |
| 43 | + if not flag_exists("EMBED_WWW") or not exists(output_file): |
| 44 | + return True |
| 45 | + last_source_change = find_latest_timestamp_for_app() |
| 46 | + last_build = getmtime(output_file) |
46 | 47 |
|
47 |
| -def findTimestampOutputFile(): |
48 |
| - return os.path.getmtime(OUTPUTFILE) |
| 48 | + print( |
| 49 | + f"Newest file: {datetime.fromtimestamp(last_source_change)}, output file: {datetime.fromtimestamp(last_build)}" |
| 50 | + ) |
49 | 51 |
|
50 |
| -def needtoRegenerateOutputFile(): |
51 |
| - if not flagExists("EMBED_WWW"): |
52 |
| - return True |
53 |
| - else: |
54 |
| - if (OutputFileExits()): |
55 |
| - latestWWWInterface = findLastestTimeStampWWWInterface() |
56 |
| - timestampOutputFile = findTimestampOutputFile() |
57 |
| - |
58 |
| - # print timestamp of newest file in interface directory and timestamp of outputfile nicely formatted as time |
59 |
| - print(f'Newest interface file: {datetime.fromtimestamp(latestWWWInterface):%Y-%m-%d %H:%M:%S}, WWW Outputfile: {datetime.fromtimestamp(timestampOutputFile):%Y-%m-%d %H:%M:%S}') |
60 |
| - # print(f'Newest interface file: {latestWWWInterface:.2f}, WWW Outputfile: {timestampOutputFile:.2f}') |
61 |
| - |
62 |
| - sourceEdited=( timestampOutputFile < latestWWWInterface ) |
63 |
| - if (sourceEdited): |
64 |
| - print("Svelte source files are updated. Need to regenerate.") |
65 |
| - return True |
66 |
| - else: |
67 |
| - print("Current outputfile is O.K. No need to regenerate.") |
68 |
| - return False |
69 |
| - |
70 |
| - else: |
71 |
| - print("WWW outputfile does not exists. Need to regenerate.") |
72 |
| - return True |
| 52 | + return last_build < last_source_change |
73 | 53 |
|
74 |
| -def gzipFile(file): |
| 54 | + |
| 55 | +def gzip_file(file): |
75 | 56 | with open(file, 'rb') as f_in:
|
76 | 57 | with gzip.open(file + '.gz', 'wb') as f_out:
|
77 | 58 | copyfileobj(f_in, f_out)
|
78 | 59 | os.remove(file)
|
79 | 60 |
|
80 |
| -def flagExists(flag): |
81 |
| - buildFlags = env.ParseFlags(env["BUILD_FLAGS"]) |
| 61 | + |
| 62 | +def flag_exists(flag): |
82 | 63 | for define in buildFlags.get("CPPDEFINES"):
|
83 | 64 | if (define == flag or (isinstance(define, list) and define[0] == flag)):
|
84 | 65 | return True
|
| 66 | + return False |
85 | 67 |
|
86 |
| -def buildProgMem(): |
87 |
| - mimetypes.init() |
88 |
| - progmem = open(OUTPUTFILE,"w") |
89 |
| - |
90 |
| - progmem.write("#include <functional>\n") |
91 |
| - progmem.write("#include <Arduino.h>\n") |
92 |
| - |
93 |
| - |
94 |
| - progmemCounter = 0 |
95 |
| - |
96 |
| - assetMap = {} |
97 |
| - |
98 |
| - # Iterate over all files in the build directory |
99 |
| - for path in Path("build").rglob("*.*"): |
100 |
| - asset_path = path.relative_to("build").as_posix() |
101 |
| - print("Converting " + str(asset_path)) |
102 |
| - |
103 |
| - asset_var = 'ESP_SVELTEKIT_DATA_' + str(progmemCounter) |
104 |
| - asset_mime = mimetypes.types_map['.' + asset_path.split('.')[-1]] |
105 |
| - |
106 |
| - progmem.write('// ' + str(asset_path) + '\n') |
107 |
| - progmem.write('const uint8_t ' + asset_var + '[] = {\n ') |
108 |
| - progmemCounter += 1 |
109 |
| - |
110 |
| - # Open path as binary file, compress and read into byte array |
111 |
| - size = 0 |
112 |
| - with open(path, "rb") as f: |
113 |
| - zipBuffer = gzip.compress(f.read()) |
114 |
| - for byte in zipBuffer: |
115 |
| - if not (size % 16): |
116 |
| - progmem.write('\n ') |
117 |
| - |
118 |
| - progmem.write(f"0x{byte:02X}" + ',') |
119 |
| - size += 1 |
120 |
| - |
121 |
| - progmem.write('\n};\n\n') |
122 |
| - assetMap[asset_path] = { "name": asset_var, "mime": asset_mime, "size": size } |
123 |
| - |
124 |
| - progmem.write('typedef std::function<void(const String& uri, const String& contentType, const uint8_t * content, size_t len)> RouteRegistrationHandler;\n\n') |
125 |
| - progmem.write('class WWWData {\n') |
126 |
| - progmem.write(' public:\n') |
127 |
| - progmem.write(' static void registerRoutes(RouteRegistrationHandler handler) {\n') |
128 |
| - |
129 |
| - for asset_path, asset in assetMap.items(): |
130 |
| - progmem.write(' handler("/' + str(asset_path) + '", "' + asset['mime'] + '", ' + asset['name'] + ', ' + str(asset['size']) + ');\n') |
131 |
| - |
132 |
| - progmem.write(' }\n') |
133 |
| - progmem.write('};\n') |
134 |
| - |
135 |
| - progmem.write('\n') |
136 |
| - |
137 |
| - |
138 |
| -def buildWeb(): |
139 |
| - os.chdir("interface") |
140 |
| - print("Building interface with npm") |
141 |
| - try: |
142 |
| - env.Execute("npm install") |
143 |
| - env.Execute("npm run build") |
144 |
| - buildPath = Path("build") |
145 |
| - wwwPath = Path("../data/www") |
146 |
| - if not flagExists("EMBED_WWW"): |
147 |
| - if wwwPath.exists() and wwwPath.is_dir(): |
148 |
| - rmtree(wwwPath) |
149 |
| - print("Copying and compress interface to data directory") |
150 |
| - copytree(buildPath, wwwPath) |
151 |
| - for currentpath, folders, files in os.walk(wwwPath): |
152 |
| - for file in files: |
153 |
| - gzipFile(os.path.join(currentpath, file)) |
154 |
| - else: |
155 |
| - print("Converting interface to PROGMEM") |
156 |
| - buildProgMem() |
157 |
| - |
158 |
| - finally: |
159 |
| - os.chdir("..") |
160 |
| - if not flagExists("EMBED_WWW"): |
161 |
| - print("Build LittleFS file system image and upload to ESP32") |
162 |
| - env.Execute("pio run --target uploadfs") |
163 | 68 |
|
164 |
| -print("running: build_interface.py") |
| 69 | +def get_package_manager(): |
| 70 | + if exists(os.path.join(interface_dir, "pnpm-lock.yaml")): |
| 71 | + return "pnpm" |
| 72 | + return "npm" |
165 | 73 |
|
166 |
| -# Dump global construction environment (for debug purpose) |
167 |
| -#print(env.Dump()) |
168 | 74 |
|
169 |
| -# Dump project construction environment (for debug purpose) |
170 |
| -#print(projenv.Dump()) |
| 75 | +def build_webapp(): |
| 76 | + package_manager = get_package_manager() |
| 77 | + print(f"Building interface with {package_manager}") |
| 78 | + os.chdir(interface_dir) |
| 79 | + env.Execute(f"{package_manager} install") |
| 80 | + env.Execute(f"{package_manager} run build") |
| 81 | + os.chdir("..") |
171 | 82 |
|
172 |
| -if (needtoRegenerateOutputFile()): |
173 |
| - buildWeb() |
174 | 83 |
|
175 |
| -#env.AddPreAction("${BUILD_DIR}/src/HTTPServer.o", buildWebInterface) |
| 84 | +def embed_webapp(): |
| 85 | + if flag_exists("EMBED_WWW"): |
| 86 | + print("Converting interface to PROGMEM") |
| 87 | + build_progmem() |
| 88 | + return |
| 89 | + add_app_to_filesystem() |
176 | 90 |
|
177 |
| -# if ("upload" in BUILD_TARGETS): |
178 |
| -# print(BUILD_TARGETS) |
179 |
| -# if (needtoRegenerateOutputFile()): |
180 |
| -# buildWeb() |
181 |
| -# else: |
182 |
| -# print("Skipping build interface step for target(s): " + ", ".join(BUILD_TARGETS)) |
183 | 91 |
|
| 92 | +def build_progmem(): |
| 93 | + mimetypes.init() |
| 94 | + with open(output_file, "w") as progmem: |
| 95 | + progmem.write("#include <functional>\n") |
| 96 | + progmem.write("#include <Arduino.h>\n") |
| 97 | + |
| 98 | + assetMap = {} |
| 99 | + |
| 100 | + for idx, path in enumerate(Path(build_dir).rglob("*.*")): |
| 101 | + asset_path = path.relative_to(build_dir).as_posix() |
| 102 | + asset_mime = ( |
| 103 | + mimetypes.guess_type(asset_path)[0] or "application/octet-stream" |
| 104 | + ) |
| 105 | + print(f"Converting {asset_path}") |
| 106 | + |
| 107 | + asset_var = f"ESP_SVELTEKIT_DATA_{idx}" |
| 108 | + progmem.write(f"// {asset_path}\n") |
| 109 | + progmem.write(f"const uint8_t {asset_var}[] = {{\n ") |
| 110 | + file_data = gzip.compress(path.read_bytes()) |
| 111 | + |
| 112 | + for i, byte in enumerate(file_data): |
| 113 | + if i and not (i % 16): |
| 114 | + progmem.write("\n\t") |
| 115 | + progmem.write(f"0x{byte:02X},") |
| 116 | + |
| 117 | + progmem.write("\n};\n\n") |
| 118 | + assetMap[asset_path] = { |
| 119 | + "name": asset_var, |
| 120 | + "mime": asset_mime, |
| 121 | + "size": len(file_data), |
| 122 | + } |
| 123 | + |
| 124 | + progmem.write( |
| 125 | + "typedef std::function<void(const String& uri, const String& contentType, const uint8_t * content, size_t len)> RouteRegistrationHandler;\n\n" |
| 126 | + ) |
| 127 | + progmem.write("class WWWData {\n") |
| 128 | + progmem.write("\tpublic:\n") |
| 129 | + progmem.write( |
| 130 | + "\t\tstatic void registerRoutes(RouteRegistrationHandler handler) {\n" |
| 131 | + ) |
| 132 | + |
| 133 | + for asset_path, asset in assetMap.items(): |
| 134 | + progmem.write( |
| 135 | + f'\t\t\thandler("/{asset_path}", "{asset["mime"]}", {asset["name"]}, {asset["size"]});\n' |
| 136 | + ) |
| 137 | + |
| 138 | + progmem.write("\t\t}\n") |
| 139 | + progmem.write("};\n\n") |
| 140 | + |
| 141 | + |
| 142 | +def add_app_to_filesystem(): |
| 143 | + build_path = Path(build_dir) |
| 144 | + www_path = Path(filesystem_dir) |
| 145 | + if www_path.exists() and www_path.is_dir(): |
| 146 | + rmtree(www_path) |
| 147 | + print("Copying and compress interface to data directory") |
| 148 | + copytree(build_path, www_path) |
| 149 | + for current_path, _, files in os.walk(www_path): |
| 150 | + for file in files: |
| 151 | + gzip_file(os.path.join(current_path, file)) |
| 152 | + print("Build LittleFS file system image and upload to ESP32") |
| 153 | + env.Execute("pio run --target uploadfs") |
184 | 154 |
|
| 155 | + |
| 156 | +print("running: build_interface.py") |
| 157 | +if should_regenerate_output_file(): |
| 158 | + build_webapp() |
| 159 | + embed_webapp() |
0 commit comments