fastled 1.3.30__py3-none-any.whl → 1.4.50__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastled/__init__.py +30 -2
- fastled/__main__.py +14 -0
- fastled/__version__.py +1 -1
- fastled/app.py +51 -2
- fastled/args.py +33 -0
- fastled/client_server.py +188 -40
- fastled/compile_server.py +10 -0
- fastled/compile_server_impl.py +34 -1
- fastled/docker_manager.py +56 -14
- fastled/emoji_util.py +27 -0
- fastled/filewatcher.py +6 -3
- fastled/find_good_connection.py +105 -0
- fastled/header_dump.py +63 -0
- fastled/install/__init__.py +1 -0
- fastled/install/examples_manager.py +62 -0
- fastled/install/extension_manager.py +113 -0
- fastled/install/main.py +156 -0
- fastled/install/project_detection.py +167 -0
- fastled/install/test_install.py +373 -0
- fastled/install/vscode_config.py +344 -0
- fastled/interruptible_http.py +148 -0
- fastled/live_client.py +21 -1
- fastled/open_browser.py +84 -16
- fastled/parse_args.py +110 -9
- fastled/playwright/chrome_extension_downloader.py +207 -0
- fastled/playwright/playwright_browser.py +773 -0
- fastled/playwright/resize_tracking.py +127 -0
- fastled/print_filter.py +52 -52
- fastled/project_init.py +20 -13
- fastled/select_sketch_directory.py +142 -19
- fastled/server_flask.py +37 -1
- fastled/settings.py +47 -3
- fastled/sketch.py +121 -4
- fastled/string_diff.py +162 -26
- fastled/test/examples.py +7 -5
- fastled/types.py +4 -0
- fastled/util.py +34 -0
- fastled/version.py +41 -41
- fastled/web_compile.py +379 -236
- fastled/zip_files.py +76 -0
- {fastled-1.3.30.dist-info → fastled-1.4.50.dist-info}/METADATA +533 -508
- fastled-1.4.50.dist-info/RECORD +60 -0
- fastled-1.3.30.dist-info/RECORD +0 -44
- {fastled-1.3.30.dist-info → fastled-1.4.50.dist-info}/WHEEL +0 -0
- {fastled-1.3.30.dist-info → fastled-1.4.50.dist-info}/entry_points.txt +0 -0
- {fastled-1.3.30.dist-info → fastled-1.4.50.dist-info}/licenses/LICENSE +0 -0
- {fastled-1.3.30.dist-info → fastled-1.4.50.dist-info}/top_level.txt +0 -0
fastled/web_compile.py
CHANGED
|
@@ -1,35 +1,23 @@
|
|
|
1
|
-
import _thread
|
|
2
1
|
import io
|
|
3
|
-
import json
|
|
4
2
|
import os
|
|
5
3
|
import shutil
|
|
6
4
|
import tempfile
|
|
7
5
|
import time
|
|
8
6
|
import zipfile
|
|
9
|
-
from concurrent.futures import Future, ThreadPoolExecutor, as_completed
|
|
10
|
-
from dataclasses import dataclass
|
|
11
7
|
from pathlib import Path
|
|
12
8
|
|
|
13
9
|
import httpx
|
|
14
10
|
|
|
15
|
-
from fastled.
|
|
16
|
-
from fastled.
|
|
11
|
+
from fastled.emoji_util import EMO, safe_print
|
|
12
|
+
from fastled.find_good_connection import find_good_connection
|
|
13
|
+
from fastled.interruptible_http import make_interruptible_post_request
|
|
14
|
+
from fastled.settings import AUTH_TOKEN, SERVER_PORT
|
|
17
15
|
from fastled.types import BuildMode, CompileResult
|
|
18
|
-
from fastled.
|
|
16
|
+
from fastled.zip_files import ZipResult, zip_files
|
|
19
17
|
|
|
20
18
|
DEFAULT_HOST = "https://fastled.onrender.com"
|
|
21
19
|
ENDPOINT_COMPILED_WASM = "compile/wasm"
|
|
22
|
-
_TIMEOUT = 60 * 4 #
|
|
23
|
-
_AUTH_TOKEN = "oBOT5jbsO4ztgrpNsQwlmFLIKB"
|
|
24
|
-
ENABLE_EMBEDDED_DATA = True
|
|
25
|
-
_EXECUTOR = ThreadPoolExecutor(max_workers=8)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
@dataclass
|
|
29
|
-
class ConnectionResult:
|
|
30
|
-
host: str
|
|
31
|
-
success: bool
|
|
32
|
-
ipv4: bool
|
|
20
|
+
_TIMEOUT = 60 * 4 # 4 mins timeout
|
|
33
21
|
|
|
34
22
|
|
|
35
23
|
def _sanitize_host(host: str) -> str:
|
|
@@ -42,120 +30,36 @@ def _sanitize_host(host: str) -> str:
|
|
|
42
30
|
return host if host.startswith("http://") else f"http://{host}"
|
|
43
31
|
|
|
44
32
|
|
|
45
|
-
def
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
transport = httpx.HTTPTransport(local_address="0.0.0.0") if use_ipv4 else None
|
|
49
|
-
result: ConnectionResult | None = None
|
|
50
|
-
try:
|
|
51
|
-
with httpx.Client(
|
|
52
|
-
timeout=_TIMEOUT,
|
|
53
|
-
transport=transport,
|
|
54
|
-
) as test_client:
|
|
55
|
-
test_response = test_client.get(
|
|
56
|
-
f"{host}/healthz", timeout=3, follow_redirects=True
|
|
57
|
-
)
|
|
58
|
-
result = ConnectionResult(host, test_response.status_code == 200, use_ipv4)
|
|
59
|
-
except KeyboardInterrupt:
|
|
60
|
-
_thread.interrupt_main()
|
|
61
|
-
result = ConnectionResult(host, False, use_ipv4)
|
|
62
|
-
except TimeoutError:
|
|
63
|
-
result = ConnectionResult(host, False, use_ipv4)
|
|
64
|
-
except Exception:
|
|
65
|
-
result = ConnectionResult(host, False, use_ipv4)
|
|
66
|
-
return result
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def _file_info(file_path: Path) -> str:
|
|
70
|
-
hash_txt = hash_file(file_path)
|
|
71
|
-
file_size = file_path.stat().st_size
|
|
72
|
-
json_str = json.dumps({"hash": hash_txt, "size": file_size})
|
|
73
|
-
return json_str
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
@dataclass
|
|
77
|
-
class ZipResult:
|
|
78
|
-
zip_bytes: bytes
|
|
79
|
-
zip_embedded_bytes: bytes | None
|
|
80
|
-
success: bool
|
|
81
|
-
error: str | None
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def zip_files(directory: Path, build_mode: BuildMode) -> ZipResult | Exception:
|
|
85
|
-
print("Zipping files...")
|
|
86
|
-
try:
|
|
87
|
-
files = get_sketch_files(directory)
|
|
88
|
-
if not files:
|
|
89
|
-
raise FileNotFoundError(f"No files found in {directory}")
|
|
90
|
-
for f in files:
|
|
91
|
-
print(f"Adding file: {f}")
|
|
92
|
-
# Create in-memory zip file
|
|
93
|
-
has_embedded_zip = False
|
|
94
|
-
zip_embedded_buffer = io.BytesIO()
|
|
95
|
-
zip_buffer = io.BytesIO()
|
|
96
|
-
with zipfile.ZipFile(
|
|
97
|
-
zip_embedded_buffer, "w", zipfile.ZIP_DEFLATED, compresslevel=9
|
|
98
|
-
) as emebedded_zip_file:
|
|
99
|
-
with zipfile.ZipFile(
|
|
100
|
-
zip_buffer, "w", zipfile.ZIP_DEFLATED, compresslevel=9
|
|
101
|
-
) as zip_file:
|
|
102
|
-
for file_path in files:
|
|
103
|
-
if "fastled_js" in str(file_path):
|
|
104
|
-
# These can be huge, don't send the output files back to the server!
|
|
105
|
-
continue
|
|
106
|
-
relative_path = file_path.relative_to(directory)
|
|
107
|
-
achive_path = str(Path("wasm") / relative_path)
|
|
108
|
-
if str(relative_path).startswith("data") and ENABLE_EMBEDDED_DATA:
|
|
109
|
-
_file_info_str = _file_info(file_path)
|
|
110
|
-
zip_file.writestr(
|
|
111
|
-
achive_path + ".embedded.json", _file_info_str
|
|
112
|
-
)
|
|
113
|
-
emebedded_zip_file.write(file_path, relative_path)
|
|
114
|
-
has_embedded_zip = True
|
|
115
|
-
else:
|
|
116
|
-
zip_file.write(file_path, achive_path)
|
|
117
|
-
# write build mode into the file as build.txt so that sketches are fingerprinted
|
|
118
|
-
# based on the build mode. Otherwise the same sketch with different build modes
|
|
119
|
-
# will have the same fingerprint.
|
|
120
|
-
zip_file.writestr(
|
|
121
|
-
str(Path("wasm") / "build_mode.txt"), build_mode.value
|
|
122
|
-
)
|
|
123
|
-
result = ZipResult(
|
|
124
|
-
zip_bytes=zip_buffer.getvalue(),
|
|
125
|
-
zip_embedded_bytes=(
|
|
126
|
-
zip_embedded_buffer.getvalue() if has_embedded_zip else None
|
|
127
|
-
),
|
|
128
|
-
success=True,
|
|
129
|
-
error=None,
|
|
130
|
-
)
|
|
131
|
-
return result
|
|
132
|
-
except Exception as e:
|
|
133
|
-
return e
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def find_good_connection(
|
|
137
|
-
urls: list[str], filter_out_bad=True, use_ipv6: bool = True
|
|
138
|
-
) -> ConnectionResult | None:
|
|
139
|
-
futures: list[Future] = []
|
|
140
|
-
for url in urls:
|
|
141
|
-
|
|
142
|
-
f = _EXECUTOR.submit(_test_connection, url, use_ipv4=True)
|
|
143
|
-
futures.append(f)
|
|
144
|
-
if use_ipv6 and "localhost" not in url:
|
|
145
|
-
f_v6 = _EXECUTOR.submit(_test_connection, url, use_ipv4=False)
|
|
146
|
-
futures.append(f_v6)
|
|
33
|
+
def _check_embedded_http_status(response_content: bytes) -> tuple[bool, int | None]:
|
|
34
|
+
"""
|
|
35
|
+
Check if the response content has an embedded HTTP status at the end.
|
|
147
36
|
|
|
37
|
+
Returns:
|
|
38
|
+
tuple: (has_embedded_status, status_code)
|
|
39
|
+
has_embedded_status is True if an embedded status was found
|
|
40
|
+
status_code is the embedded status code or None if not found
|
|
41
|
+
"""
|
|
148
42
|
try:
|
|
149
|
-
#
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
43
|
+
# Convert bytes to string for parsing
|
|
44
|
+
content_str = response_content.decode("utf-8", errors="ignore")
|
|
45
|
+
|
|
46
|
+
# Look for HTTP_STATUS: at the end of the content
|
|
47
|
+
lines = content_str.strip().split("\n")
|
|
48
|
+
if lines:
|
|
49
|
+
last_line = lines[-1].strip()
|
|
50
|
+
if "HTTP_STATUS:" in last_line:
|
|
51
|
+
try:
|
|
52
|
+
right = last_line.split("HTTP_STATUS:")[-1].strip()
|
|
53
|
+
status_code = int(right)
|
|
54
|
+
return True, status_code
|
|
55
|
+
except (ValueError, IndexError):
|
|
56
|
+
# Malformed status line
|
|
57
|
+
return True, None
|
|
58
|
+
|
|
59
|
+
return False, None
|
|
60
|
+
except Exception:
|
|
61
|
+
# If we can't parse the content, assume no embedded status
|
|
62
|
+
return False, None
|
|
159
63
|
|
|
160
64
|
|
|
161
65
|
def _banner(msg: str) -> str:
|
|
@@ -186,7 +90,212 @@ def _banner(msg: str) -> str:
|
|
|
186
90
|
|
|
187
91
|
|
|
188
92
|
def _print_banner(msg: str) -> None:
|
|
189
|
-
|
|
93
|
+
safe_print(_banner(msg))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _compile_libfastled(
|
|
97
|
+
host: str,
|
|
98
|
+
auth_token: str,
|
|
99
|
+
build_mode: BuildMode,
|
|
100
|
+
) -> httpx.Response:
|
|
101
|
+
"""Compile the FastLED library separately."""
|
|
102
|
+
host = _sanitize_host(host)
|
|
103
|
+
urls = [host]
|
|
104
|
+
domain = host.split("://")[-1]
|
|
105
|
+
if ":" not in domain:
|
|
106
|
+
urls.append(f"{host}:{SERVER_PORT}")
|
|
107
|
+
|
|
108
|
+
connection_result = find_good_connection(urls)
|
|
109
|
+
if connection_result is None:
|
|
110
|
+
raise ConnectionError(
|
|
111
|
+
"Connection failed to all endpoints for libfastled compilation"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
ipv4_stmt = "IPv4" if connection_result.ipv4 else "IPv6"
|
|
115
|
+
transport = (
|
|
116
|
+
httpx.HTTPTransport(local_address="0.0.0.0") if connection_result.ipv4 else None
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
headers = {
|
|
120
|
+
"accept": "application/json",
|
|
121
|
+
"authorization": auth_token,
|
|
122
|
+
"build": build_mode.value.lower(),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
url = f"{connection_result.host}/compile/libfastled"
|
|
126
|
+
print(f"Compiling libfastled on {url} via {ipv4_stmt}")
|
|
127
|
+
|
|
128
|
+
# Use interruptible HTTP request
|
|
129
|
+
response = make_interruptible_post_request(
|
|
130
|
+
url=url,
|
|
131
|
+
files={}, # No files for libfastled compilation
|
|
132
|
+
headers=headers,
|
|
133
|
+
transport=transport,
|
|
134
|
+
timeout=_TIMEOUT * 2, # Give more time for library compilation
|
|
135
|
+
follow_redirects=False,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return response
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _send_compile_request(
|
|
142
|
+
host: str,
|
|
143
|
+
zip_bytes: bytes,
|
|
144
|
+
auth_token: str,
|
|
145
|
+
build_mode: BuildMode,
|
|
146
|
+
profile: bool,
|
|
147
|
+
no_platformio: bool,
|
|
148
|
+
) -> httpx.Response:
|
|
149
|
+
"""Send the compile request to the server and return the response."""
|
|
150
|
+
host = _sanitize_host(host)
|
|
151
|
+
urls = [host]
|
|
152
|
+
domain = host.split("://")[-1]
|
|
153
|
+
if ":" not in domain:
|
|
154
|
+
urls.append(f"{host}:{SERVER_PORT}")
|
|
155
|
+
|
|
156
|
+
connection_result = find_good_connection(urls)
|
|
157
|
+
if connection_result is None:
|
|
158
|
+
raise ConnectionError("Connection failed to all endpoints")
|
|
159
|
+
|
|
160
|
+
ipv4_stmt = "IPv4" if connection_result.ipv4 else "IPv6"
|
|
161
|
+
transport = (
|
|
162
|
+
httpx.HTTPTransport(local_address="0.0.0.0") if connection_result.ipv4 else None
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
archive_size = len(zip_bytes)
|
|
166
|
+
|
|
167
|
+
headers = {
|
|
168
|
+
"accept": "application/json",
|
|
169
|
+
"authorization": auth_token,
|
|
170
|
+
"build": (
|
|
171
|
+
build_mode.value.lower() if build_mode else BuildMode.QUICK.value.lower()
|
|
172
|
+
),
|
|
173
|
+
"profile": "true" if profile else "false",
|
|
174
|
+
"no-platformio": "true" if no_platformio else "false",
|
|
175
|
+
"allow-libcompile": "false", # Always false since we handle it manually
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
url = f"{connection_result.host}/{ENDPOINT_COMPILED_WASM}"
|
|
179
|
+
print(f"Compiling sketch on {url} via {ipv4_stmt}. Zip size: {archive_size} bytes")
|
|
180
|
+
files = {"file": ("wasm.zip", zip_bytes, "application/x-zip-compressed")}
|
|
181
|
+
|
|
182
|
+
# Use interruptible HTTP request
|
|
183
|
+
response = make_interruptible_post_request(
|
|
184
|
+
url=url,
|
|
185
|
+
files=files,
|
|
186
|
+
headers=headers,
|
|
187
|
+
transport=transport,
|
|
188
|
+
timeout=_TIMEOUT,
|
|
189
|
+
follow_redirects=True,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return response
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _process_compile_response(
|
|
196
|
+
response: httpx.Response,
|
|
197
|
+
zip_result: ZipResult,
|
|
198
|
+
start_time: float,
|
|
199
|
+
zip_time: float,
|
|
200
|
+
libfastled_time: float,
|
|
201
|
+
sketch_time: float,
|
|
202
|
+
) -> CompileResult:
|
|
203
|
+
"""Process the compile response and return the final result."""
|
|
204
|
+
if response.status_code != 200:
|
|
205
|
+
json_response = response.json()
|
|
206
|
+
detail = json_response.get("detail", "Could not compile")
|
|
207
|
+
return CompileResult(
|
|
208
|
+
success=False,
|
|
209
|
+
stdout=detail,
|
|
210
|
+
hash_value=None,
|
|
211
|
+
zip_bytes=b"",
|
|
212
|
+
zip_time=zip_time,
|
|
213
|
+
libfastled_time=libfastled_time,
|
|
214
|
+
sketch_time=sketch_time,
|
|
215
|
+
response_processing_time=0.0, # No response processing in error case
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
print(f"Response status code: {response}")
|
|
219
|
+
|
|
220
|
+
# Time the response processing
|
|
221
|
+
response_processing_start = time.time()
|
|
222
|
+
|
|
223
|
+
# Create a temporary directory to extract the zip
|
|
224
|
+
with tempfile.TemporaryDirectory() as extract_dir:
|
|
225
|
+
extract_path = Path(extract_dir)
|
|
226
|
+
|
|
227
|
+
# Write the response content to a temporary zip file
|
|
228
|
+
temp_zip = extract_path / "response.zip"
|
|
229
|
+
temp_zip.write_bytes(response.content)
|
|
230
|
+
|
|
231
|
+
# Extract the zip
|
|
232
|
+
shutil.unpack_archive(temp_zip, extract_path, "zip")
|
|
233
|
+
|
|
234
|
+
if zip_result.zip_embedded_bytes:
|
|
235
|
+
# extract the embedded bytes, which were not sent to the server
|
|
236
|
+
temp_zip.write_bytes(zip_result.zip_embedded_bytes)
|
|
237
|
+
shutil.unpack_archive(temp_zip, extract_path, "zip")
|
|
238
|
+
|
|
239
|
+
# we don't need the temp zip anymore
|
|
240
|
+
temp_zip.unlink()
|
|
241
|
+
|
|
242
|
+
# Read stdout from out.txt if it exists
|
|
243
|
+
stdout_file = extract_path / "out.txt"
|
|
244
|
+
hash_file = extract_path / "hash.txt"
|
|
245
|
+
stdout = (
|
|
246
|
+
stdout_file.read_text(encoding="utf-8", errors="replace")
|
|
247
|
+
if stdout_file.exists()
|
|
248
|
+
else ""
|
|
249
|
+
)
|
|
250
|
+
hash_value = (
|
|
251
|
+
hash_file.read_text(encoding="utf-8", errors="replace")
|
|
252
|
+
if hash_file.exists()
|
|
253
|
+
else None
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# now rezip the extracted files since we added the embedded json files
|
|
257
|
+
out_buffer = io.BytesIO()
|
|
258
|
+
with zipfile.ZipFile(
|
|
259
|
+
out_buffer, "w", zipfile.ZIP_DEFLATED, compresslevel=9
|
|
260
|
+
) as out_zip:
|
|
261
|
+
for root, _, _files in os.walk(extract_path):
|
|
262
|
+
for file in _files:
|
|
263
|
+
file_path = Path(root) / file
|
|
264
|
+
relative_path = file_path.relative_to(extract_path)
|
|
265
|
+
out_zip.write(file_path, relative_path)
|
|
266
|
+
|
|
267
|
+
response_processing_time = time.time() - response_processing_start
|
|
268
|
+
diff_time = time.time() - start_time
|
|
269
|
+
|
|
270
|
+
# Create detailed timing breakdown
|
|
271
|
+
unaccounted_time = diff_time - (
|
|
272
|
+
zip_time + libfastled_time + sketch_time + response_processing_time
|
|
273
|
+
)
|
|
274
|
+
msg = f"Compilation success, took {diff_time:.2f} seconds\n"
|
|
275
|
+
msg += f" zip creation: {zip_time:.2f}\n"
|
|
276
|
+
if libfastled_time > 0:
|
|
277
|
+
msg += f" libfastled: {libfastled_time:.2f}\n"
|
|
278
|
+
msg += f" sketch compile + link: {sketch_time:.2f}\n"
|
|
279
|
+
msg += f" response processing: {response_processing_time:.2f}\n"
|
|
280
|
+
if unaccounted_time > 0.01: # Only show if significant
|
|
281
|
+
msg += f" other overhead: {unaccounted_time:.2f}"
|
|
282
|
+
|
|
283
|
+
_print_banner(msg)
|
|
284
|
+
return CompileResult(
|
|
285
|
+
success=True,
|
|
286
|
+
stdout=stdout,
|
|
287
|
+
hash_value=hash_value,
|
|
288
|
+
zip_bytes=out_buffer.getvalue(),
|
|
289
|
+
zip_time=zip_time,
|
|
290
|
+
libfastled_time=libfastled_time,
|
|
291
|
+
sketch_time=sketch_time,
|
|
292
|
+
response_processing_time=response_processing_time,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _libcompiled_is_allowed(host: str) -> bool:
|
|
297
|
+
"""Check if libcompiled is allowed for the given host."""
|
|
298
|
+
return "localhost" in host or "127.0.0.1" in host or "0.0.0.0" in host
|
|
190
299
|
|
|
191
300
|
|
|
192
301
|
def web_compile(
|
|
@@ -195,6 +304,8 @@ def web_compile(
|
|
|
195
304
|
auth_token: str | None = None,
|
|
196
305
|
build_mode: BuildMode | None = None,
|
|
197
306
|
profile: bool = False,
|
|
307
|
+
no_platformio: bool = False,
|
|
308
|
+
allow_libcompile: bool = True,
|
|
198
309
|
) -> CompileResult:
|
|
199
310
|
start_time = time.time()
|
|
200
311
|
if isinstance(directory, str):
|
|
@@ -202,124 +313,156 @@ def web_compile(
|
|
|
202
313
|
host = _sanitize_host(host or DEFAULT_HOST)
|
|
203
314
|
build_mode = build_mode or BuildMode.QUICK
|
|
204
315
|
_print_banner(f"Compiling on {host}")
|
|
205
|
-
auth_token = auth_token or
|
|
316
|
+
auth_token = auth_token or AUTH_TOKEN
|
|
206
317
|
if not directory.exists():
|
|
207
318
|
raise FileNotFoundError(f"Directory not found: {directory}")
|
|
208
|
-
|
|
319
|
+
|
|
320
|
+
if allow_libcompile and not _libcompiled_is_allowed(host):
|
|
321
|
+
print(
|
|
322
|
+
f"{EMO('🚫', '[ERROR]')} libcompile is not allowed for host {host}, disabling."
|
|
323
|
+
)
|
|
324
|
+
allow_libcompile = False
|
|
325
|
+
|
|
326
|
+
# Time the zip creation
|
|
327
|
+
zip_start_time = time.time()
|
|
328
|
+
zip_result: ZipResult | Exception = zip_files(directory, build_mode=build_mode)
|
|
329
|
+
zip_time = time.time() - zip_start_time
|
|
330
|
+
|
|
209
331
|
if isinstance(zip_result, Exception):
|
|
210
332
|
return CompileResult(
|
|
211
|
-
success=False,
|
|
333
|
+
success=False,
|
|
334
|
+
stdout=str(zip_result),
|
|
335
|
+
hash_value=None,
|
|
336
|
+
zip_bytes=b"",
|
|
337
|
+
zip_time=zip_time,
|
|
338
|
+
libfastled_time=0.0, # No libfastled compilation in zip error case
|
|
339
|
+
sketch_time=0.0, # No sketch compilation in zip error case
|
|
340
|
+
response_processing_time=0.0, # No response processing in zip error case
|
|
212
341
|
)
|
|
213
342
|
zip_bytes = zip_result.zip_bytes
|
|
214
|
-
archive_size = len(zip_bytes)
|
|
215
343
|
print(f"Web compiling on {host}...")
|
|
344
|
+
|
|
345
|
+
# Track timing for each step
|
|
346
|
+
libfastled_time = 0.0
|
|
347
|
+
sketch_time = 0.0
|
|
348
|
+
|
|
216
349
|
try:
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
350
|
+
# Step 1: Compile libfastled if requested
|
|
351
|
+
if allow_libcompile:
|
|
352
|
+
print("Step 1: Compiling libfastled...")
|
|
353
|
+
libfastled_start_time = time.time()
|
|
354
|
+
try:
|
|
355
|
+
libfastled_response = _compile_libfastled(host, auth_token, build_mode)
|
|
356
|
+
|
|
357
|
+
# Check HTTP response status first
|
|
358
|
+
if libfastled_response.status_code != 200:
|
|
359
|
+
msg = f"Error: libfastled compilation failed with HTTP status {libfastled_response.status_code}"
|
|
360
|
+
|
|
361
|
+
# Error out here, this is a critical error
|
|
362
|
+
stdout = libfastled_response.content
|
|
363
|
+
if stdout is not None:
|
|
364
|
+
stdout = stdout.decode("utf-8", errors="replace") + "\n" + msg
|
|
365
|
+
else:
|
|
366
|
+
stdout = msg
|
|
367
|
+
return CompileResult(
|
|
368
|
+
success=False,
|
|
369
|
+
stdout=stdout,
|
|
370
|
+
hash_value=None,
|
|
371
|
+
zip_bytes=b"",
|
|
372
|
+
zip_time=zip_time,
|
|
373
|
+
libfastled_time=libfastled_time,
|
|
374
|
+
sketch_time=0.0, # No sketch compilation when libfastled fails
|
|
375
|
+
response_processing_time=0.0, # No response processing when libfastled fails
|
|
376
|
+
)
|
|
377
|
+
else:
|
|
378
|
+
# Check for embedded HTTP status in response content
|
|
379
|
+
has_embedded_status, embedded_status = _check_embedded_http_status(
|
|
380
|
+
libfastled_response.content
|
|
381
|
+
)
|
|
382
|
+
if has_embedded_status:
|
|
383
|
+
if embedded_status is not None and embedded_status != 200:
|
|
384
|
+
msg = f"Warning: libfastled compilation failed with embedded status {embedded_status}"
|
|
385
|
+
# this is a critical error
|
|
386
|
+
stdout = libfastled_response.content
|
|
387
|
+
if stdout is not None:
|
|
388
|
+
stdout = (
|
|
389
|
+
stdout.decode("utf-8", errors="replace")
|
|
390
|
+
+ "\n"
|
|
391
|
+
+ msg
|
|
392
|
+
)
|
|
393
|
+
else:
|
|
394
|
+
stdout = msg
|
|
395
|
+
return CompileResult(
|
|
396
|
+
success=False,
|
|
397
|
+
stdout=stdout,
|
|
398
|
+
hash_value=None,
|
|
399
|
+
zip_bytes=b"",
|
|
400
|
+
zip_time=zip_time,
|
|
401
|
+
libfastled_time=libfastled_time,
|
|
402
|
+
sketch_time=0.0, # No sketch compilation when libfastled fails
|
|
403
|
+
response_processing_time=0.0, # No response processing when libfastled fails
|
|
404
|
+
)
|
|
405
|
+
# Continue with sketch compilation even if libfastled fails
|
|
406
|
+
elif embedded_status is None:
|
|
407
|
+
print(
|
|
408
|
+
"Warning: libfastled compilation returned malformed embedded status"
|
|
409
|
+
)
|
|
410
|
+
# Continue with sketch compilation even if libfastled fails
|
|
411
|
+
else:
|
|
412
|
+
print("✅ libfastled compilation successful")
|
|
413
|
+
else:
|
|
414
|
+
print("✅ libfastled compilation successful")
|
|
415
|
+
libfastled_time = time.time() - libfastled_start_time
|
|
416
|
+
except Exception as e:
|
|
417
|
+
libfastled_time = time.time() - libfastled_start_time
|
|
418
|
+
print(f"Warning: libfastled compilation failed: {e}")
|
|
419
|
+
# Continue with sketch compilation even if libfastled fails
|
|
420
|
+
else:
|
|
421
|
+
print("Step 1 (skipped): Compiling libfastled")
|
|
422
|
+
|
|
423
|
+
# Step 2: Compile the sketch
|
|
424
|
+
print("Step 2: Compiling sketch...")
|
|
425
|
+
sketch_start_time = time.time()
|
|
426
|
+
response = _send_compile_request(
|
|
427
|
+
host,
|
|
428
|
+
zip_bytes,
|
|
429
|
+
auth_token,
|
|
430
|
+
build_mode,
|
|
431
|
+
profile,
|
|
432
|
+
no_platformio,
|
|
433
|
+
)
|
|
434
|
+
sketch_time = time.time() - sketch_start_time
|
|
435
|
+
|
|
436
|
+
return _process_compile_response(
|
|
437
|
+
response, zip_result, start_time, zip_time, libfastled_time, sketch_time
|
|
238
438
|
)
|
|
239
|
-
|
|
240
|
-
transport=transport,
|
|
241
|
-
timeout=_TIMEOUT,
|
|
242
|
-
) as client:
|
|
243
|
-
headers = {
|
|
244
|
-
"accept": "application/json",
|
|
245
|
-
"authorization": auth_token,
|
|
246
|
-
"build": (
|
|
247
|
-
build_mode.value.lower()
|
|
248
|
-
if build_mode
|
|
249
|
-
else BuildMode.QUICK.value.lower()
|
|
250
|
-
),
|
|
251
|
-
"profile": "true" if profile else "false",
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
url = f"{connection_result.host}/{ENDPOINT_COMPILED_WASM}"
|
|
255
|
-
print(f"Compiling on {url} via {ipv4_stmt}. Zip size: {archive_size} bytes")
|
|
256
|
-
files = {"file": ("wasm.zip", zip_bytes, "application/x-zip-compressed")}
|
|
257
|
-
response = client.post(
|
|
258
|
-
url,
|
|
259
|
-
follow_redirects=True,
|
|
260
|
-
files=files,
|
|
261
|
-
headers=headers,
|
|
262
|
-
timeout=_TIMEOUT,
|
|
263
|
-
)
|
|
264
|
-
|
|
265
|
-
if response.status_code != 200:
|
|
266
|
-
json_response = response.json()
|
|
267
|
-
detail = json_response.get("detail", "Could not compile")
|
|
268
|
-
return CompileResult(
|
|
269
|
-
success=False, stdout=detail, hash_value=None, zip_bytes=b""
|
|
270
|
-
)
|
|
271
|
-
|
|
272
|
-
print(f"Response status code: {response}")
|
|
273
|
-
# Create a temporary directory to extract the zip
|
|
274
|
-
with tempfile.TemporaryDirectory() as extract_dir:
|
|
275
|
-
extract_path = Path(extract_dir)
|
|
276
|
-
|
|
277
|
-
# Write the response content to a temporary zip file
|
|
278
|
-
temp_zip = extract_path / "response.zip"
|
|
279
|
-
temp_zip.write_bytes(response.content)
|
|
280
|
-
|
|
281
|
-
# Extract the zip
|
|
282
|
-
shutil.unpack_archive(temp_zip, extract_path, "zip")
|
|
283
|
-
|
|
284
|
-
if zip_result.zip_embedded_bytes:
|
|
285
|
-
# extract the embedded bytes, which were not sent to the server
|
|
286
|
-
temp_zip.write_bytes(zip_result.zip_embedded_bytes)
|
|
287
|
-
shutil.unpack_archive(temp_zip, extract_path, "zip")
|
|
288
|
-
|
|
289
|
-
# we don't need the temp zip anymore
|
|
290
|
-
temp_zip.unlink()
|
|
291
|
-
|
|
292
|
-
# Read stdout from out.txt if it exists
|
|
293
|
-
stdout_file = extract_path / "out.txt"
|
|
294
|
-
hash_file = extract_path / "hash.txt"
|
|
295
|
-
stdout = stdout_file.read_text() if stdout_file.exists() else ""
|
|
296
|
-
hash_value = hash_file.read_text() if hash_file.exists() else None
|
|
297
|
-
|
|
298
|
-
# now rezip the extracted files since we added the embedded json files
|
|
299
|
-
out_buffer = io.BytesIO()
|
|
300
|
-
with zipfile.ZipFile(
|
|
301
|
-
out_buffer, "w", zipfile.ZIP_DEFLATED, compresslevel=9
|
|
302
|
-
) as out_zip:
|
|
303
|
-
for root, _, _files in os.walk(extract_path):
|
|
304
|
-
for file in _files:
|
|
305
|
-
file_path = Path(root) / file
|
|
306
|
-
relative_path = file_path.relative_to(extract_path)
|
|
307
|
-
out_zip.write(file_path, relative_path)
|
|
308
|
-
|
|
309
|
-
diff_time = time.time() - start_time
|
|
310
|
-
msg = f"Compilation success, took {diff_time:.2f} seconds"
|
|
311
|
-
_print_banner(msg)
|
|
312
|
-
return CompileResult(
|
|
313
|
-
success=True,
|
|
314
|
-
stdout=stdout,
|
|
315
|
-
hash_value=hash_value,
|
|
316
|
-
zip_bytes=out_buffer.getvalue(),
|
|
317
|
-
)
|
|
439
|
+
|
|
318
440
|
except KeyboardInterrupt:
|
|
319
441
|
print("Keyboard interrupt")
|
|
320
442
|
raise
|
|
443
|
+
except (ConnectionError, httpx.RemoteProtocolError, httpx.RequestError) as e:
|
|
444
|
+
# Handle connection and server disconnection issues
|
|
445
|
+
error_msg = f"Server connection error: {e}"
|
|
446
|
+
_print_banner(error_msg)
|
|
447
|
+
return CompileResult(
|
|
448
|
+
success=False,
|
|
449
|
+
stdout=error_msg,
|
|
450
|
+
hash_value=None,
|
|
451
|
+
zip_bytes=b"",
|
|
452
|
+
zip_time=zip_time,
|
|
453
|
+
libfastled_time=libfastled_time,
|
|
454
|
+
sketch_time=sketch_time,
|
|
455
|
+
response_processing_time=0.0,
|
|
456
|
+
)
|
|
321
457
|
except httpx.HTTPError as e:
|
|
322
458
|
print(f"Error: {e}")
|
|
323
459
|
return CompileResult(
|
|
324
|
-
success=False,
|
|
460
|
+
success=False,
|
|
461
|
+
stdout=str(e),
|
|
462
|
+
hash_value=None,
|
|
463
|
+
zip_bytes=b"",
|
|
464
|
+
zip_time=zip_time,
|
|
465
|
+
libfastled_time=libfastled_time,
|
|
466
|
+
sketch_time=sketch_time,
|
|
467
|
+
response_processing_time=0.0, # No response processing in HTTP error case
|
|
325
468
|
)
|