fastled 1.2.33__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 +51 -192
- fastled/__main__.py +14 -0
- fastled/__version__.py +6 -0
- fastled/app.py +124 -27
- fastled/args.py +124 -0
- fastled/assets/localhost-key.pem +28 -0
- fastled/assets/localhost.pem +27 -0
- fastled/cli.py +10 -2
- fastled/cli_test.py +21 -0
- fastled/cli_test_interactive.py +21 -0
- fastled/client_server.py +334 -55
- fastled/compile_server.py +12 -1
- fastled/compile_server_impl.py +115 -42
- fastled/docker_manager.py +392 -69
- fastled/emoji_util.py +27 -0
- fastled/filewatcher.py +100 -8
- 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/keyboard.py +1 -0
- fastled/keyz.py +84 -0
- fastled/live_client.py +26 -1
- fastled/open_browser.py +133 -89
- fastled/parse_args.py +219 -15
- 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 -0
- fastled/project_init.py +20 -13
- fastled/select_sketch_directory.py +142 -17
- fastled/server_flask.py +487 -0
- fastled/server_start.py +21 -0
- fastled/settings.py +53 -4
- fastled/site/build.py +2 -10
- fastled/site/examples.py +10 -0
- fastled/sketch.py +129 -7
- fastled/string_diff.py +218 -9
- fastled/test/examples.py +7 -5
- fastled/types.py +22 -2
- fastled/util.py +78 -0
- fastled/version.py +41 -0
- fastled/web_compile.py +401 -218
- fastled/zip_files.py +76 -0
- {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/METADATA +533 -382
- fastled-1.4.50.dist-info/RECORD +60 -0
- {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/WHEEL +1 -1
- fastled/open_browser2.py +0 -111
- fastled-1.2.33.dist-info/RECORD +0 -33
- {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/entry_points.txt +0 -0
- {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info/licenses}/LICENSE +0 -0
- {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/top_level.txt +0 -0
fastled/web_compile.py
CHANGED
|
@@ -1,34 +1,23 @@
|
|
|
1
|
-
import _thread
|
|
2
1
|
import io
|
|
3
|
-
import json
|
|
4
2
|
import os
|
|
5
3
|
import shutil
|
|
6
4
|
import tempfile
|
|
5
|
+
import time
|
|
7
6
|
import zipfile
|
|
8
|
-
from concurrent.futures import Future, ThreadPoolExecutor, as_completed
|
|
9
|
-
from dataclasses import dataclass
|
|
10
7
|
from pathlib import Path
|
|
11
8
|
|
|
12
9
|
import httpx
|
|
13
10
|
|
|
14
|
-
from fastled.
|
|
15
|
-
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
|
|
16
15
|
from fastled.types import BuildMode, CompileResult
|
|
17
|
-
from fastled.
|
|
16
|
+
from fastled.zip_files import ZipResult, zip_files
|
|
18
17
|
|
|
19
18
|
DEFAULT_HOST = "https://fastled.onrender.com"
|
|
20
19
|
ENDPOINT_COMPILED_WASM = "compile/wasm"
|
|
21
|
-
_TIMEOUT = 60 * 4 #
|
|
22
|
-
_AUTH_TOKEN = "oBOT5jbsO4ztgrpNsQwlmFLIKB"
|
|
23
|
-
ENABLE_EMBEDDED_DATA = True
|
|
24
|
-
_EXECUTOR = ThreadPoolExecutor(max_workers=8)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
@dataclass
|
|
28
|
-
class ConnectionResult:
|
|
29
|
-
host: str
|
|
30
|
-
success: bool
|
|
31
|
-
ipv4: bool
|
|
20
|
+
_TIMEOUT = 60 * 4 # 4 mins timeout
|
|
32
21
|
|
|
33
22
|
|
|
34
23
|
def _sanitize_host(host: str) -> str:
|
|
@@ -41,116 +30,272 @@ def _sanitize_host(host: str) -> str:
|
|
|
41
30
|
return host if host.startswith("http://") else f"http://{host}"
|
|
42
31
|
|
|
43
32
|
|
|
44
|
-
def
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
transport = httpx.HTTPTransport(local_address="0.0.0.0") if use_ipv4 else None
|
|
48
|
-
try:
|
|
49
|
-
with httpx.Client(
|
|
50
|
-
timeout=_TIMEOUT,
|
|
51
|
-
transport=transport,
|
|
52
|
-
) as test_client:
|
|
53
|
-
test_response = test_client.get(
|
|
54
|
-
f"{host}/healthz", timeout=3, follow_redirects=True
|
|
55
|
-
)
|
|
56
|
-
result = ConnectionResult(host, test_response.status_code == 200, use_ipv4)
|
|
57
|
-
except KeyboardInterrupt:
|
|
58
|
-
_thread.interrupt_main()
|
|
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.
|
|
59
36
|
|
|
60
|
-
|
|
61
|
-
|
|
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
|
+
"""
|
|
42
|
+
try:
|
|
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
|
|
62
60
|
except Exception:
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
# If we can't parse the content, assume no embedded status
|
|
62
|
+
return False, None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _banner(msg: str) -> str:
|
|
66
|
+
"""
|
|
67
|
+
Create a banner for the given message.
|
|
68
|
+
Example:
|
|
69
|
+
msg = "Hello, World!"
|
|
70
|
+
print -> "#################"
|
|
71
|
+
"# Hello, World! #"
|
|
72
|
+
"#################"
|
|
73
|
+
"""
|
|
74
|
+
lines = msg.split("\n")
|
|
75
|
+
# Find the width of the widest line
|
|
76
|
+
max_width = max(len(line) for line in lines)
|
|
77
|
+
width = max_width + 4 # Add 4 for "# " and " #"
|
|
78
|
+
|
|
79
|
+
# Create the top border
|
|
80
|
+
banner = "\n" + "#" * width + "\n"
|
|
81
|
+
|
|
82
|
+
# Add each line with proper padding
|
|
83
|
+
for line in lines:
|
|
84
|
+
padding = max_width - len(line)
|
|
85
|
+
banner += f"# {line}{' ' * padding} #\n"
|
|
86
|
+
|
|
87
|
+
# Add the bottom border
|
|
88
|
+
banner += "#" * width + "\n"
|
|
89
|
+
return f"\n{banner}\n"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _print_banner(msg: str) -> None:
|
|
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
|
+
)
|
|
65
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}")
|
|
66
155
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
json_str = json.dumps({"hash": hash_txt, "size": file_size})
|
|
71
|
-
return json_str
|
|
156
|
+
connection_result = find_good_connection(urls)
|
|
157
|
+
if connection_result is None:
|
|
158
|
+
raise ConnectionError("Connection failed to all endpoints")
|
|
72
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
|
+
)
|
|
73
164
|
|
|
74
|
-
|
|
75
|
-
class ZipResult:
|
|
76
|
-
zip_bytes: bytes
|
|
77
|
-
zip_embedded_bytes: bytes | None
|
|
78
|
-
success: bool
|
|
79
|
-
error: str | None
|
|
165
|
+
archive_size = len(zip_bytes)
|
|
80
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
|
+
)
|
|
81
217
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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()
|
|
94
258
|
with zipfile.ZipFile(
|
|
95
|
-
|
|
96
|
-
) as
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
zip_embedded_buffer.getvalue() if has_embedded_zip else None
|
|
122
|
-
),
|
|
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(
|
|
123
285
|
success=True,
|
|
124
|
-
|
|
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,
|
|
125
293
|
)
|
|
126
|
-
return result
|
|
127
|
-
except Exception as e:
|
|
128
|
-
return e
|
|
129
|
-
|
|
130
294
|
|
|
131
|
-
def find_good_connection(
|
|
132
|
-
urls: list[str], filter_out_bad=True, use_ipv6: bool = True
|
|
133
|
-
) -> ConnectionResult | None:
|
|
134
|
-
futures: list[Future] = []
|
|
135
|
-
for url in urls:
|
|
136
295
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
f_v6 = _EXECUTOR.submit(_test_connection, url, use_ipv4=False)
|
|
141
|
-
futures.append(f_v6)
|
|
142
|
-
|
|
143
|
-
try:
|
|
144
|
-
# Return first successful result
|
|
145
|
-
for future in as_completed(futures):
|
|
146
|
-
result: ConnectionResult = future.result()
|
|
147
|
-
if result.success or not filter_out_bad:
|
|
148
|
-
return result
|
|
149
|
-
finally:
|
|
150
|
-
# Cancel any remaining futures
|
|
151
|
-
for future in futures:
|
|
152
|
-
future.cancel()
|
|
153
|
-
return None
|
|
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
|
|
154
299
|
|
|
155
300
|
|
|
156
301
|
def web_compile(
|
|
@@ -159,127 +304,165 @@ def web_compile(
|
|
|
159
304
|
auth_token: str | None = None,
|
|
160
305
|
build_mode: BuildMode | None = None,
|
|
161
306
|
profile: bool = False,
|
|
307
|
+
no_platformio: bool = False,
|
|
308
|
+
allow_libcompile: bool = True,
|
|
162
309
|
) -> CompileResult:
|
|
310
|
+
start_time = time.time()
|
|
163
311
|
if isinstance(directory, str):
|
|
164
312
|
directory = Path(directory)
|
|
165
313
|
host = _sanitize_host(host or DEFAULT_HOST)
|
|
166
314
|
build_mode = build_mode or BuildMode.QUICK
|
|
167
|
-
|
|
168
|
-
auth_token = auth_token or
|
|
315
|
+
_print_banner(f"Compiling on {host}")
|
|
316
|
+
auth_token = auth_token or AUTH_TOKEN
|
|
169
317
|
if not directory.exists():
|
|
170
318
|
raise FileNotFoundError(f"Directory not found: {directory}")
|
|
171
|
-
|
|
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
|
+
|
|
172
331
|
if isinstance(zip_result, Exception):
|
|
173
332
|
return CompileResult(
|
|
174
|
-
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
|
|
175
341
|
)
|
|
176
342
|
zip_bytes = zip_result.zip_bytes
|
|
177
|
-
archive_size = len(zip_bytes)
|
|
178
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
|
+
|
|
179
349
|
try:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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,
|
|
201
433
|
)
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
"authorization": auth_token,
|
|
209
|
-
"build": (
|
|
210
|
-
build_mode.value.lower()
|
|
211
|
-
if build_mode
|
|
212
|
-
else BuildMode.QUICK.value.lower()
|
|
213
|
-
),
|
|
214
|
-
"profile": "true" if profile else "false",
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
url = f"{connection_result.host}/{ENDPOINT_COMPILED_WASM}"
|
|
218
|
-
print(f"Compiling on {url} via {ipv4_stmt}. Zip size: {archive_size} bytes")
|
|
219
|
-
files = {"file": ("wasm.zip", zip_bytes, "application/x-zip-compressed")}
|
|
220
|
-
response = client.post(
|
|
221
|
-
url,
|
|
222
|
-
follow_redirects=True,
|
|
223
|
-
files=files,
|
|
224
|
-
headers=headers,
|
|
225
|
-
timeout=_TIMEOUT,
|
|
226
|
-
)
|
|
227
|
-
|
|
228
|
-
if response.status_code != 200:
|
|
229
|
-
json_response = response.json()
|
|
230
|
-
detail = json_response.get("detail", "Could not compile")
|
|
231
|
-
return CompileResult(
|
|
232
|
-
success=False, stdout=detail, hash_value=None, zip_bytes=b""
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
print(f"Response status code: {response}")
|
|
236
|
-
# Create a temporary directory to extract the zip
|
|
237
|
-
with tempfile.TemporaryDirectory() as extract_dir:
|
|
238
|
-
extract_path = Path(extract_dir)
|
|
239
|
-
|
|
240
|
-
# Write the response content to a temporary zip file
|
|
241
|
-
temp_zip = extract_path / "response.zip"
|
|
242
|
-
temp_zip.write_bytes(response.content)
|
|
243
|
-
|
|
244
|
-
# Extract the zip
|
|
245
|
-
shutil.unpack_archive(temp_zip, extract_path, "zip")
|
|
246
|
-
|
|
247
|
-
if zip_result.zip_embedded_bytes:
|
|
248
|
-
# extract the embedded bytes, which were not sent to the server
|
|
249
|
-
temp_zip.write_bytes(zip_result.zip_embedded_bytes)
|
|
250
|
-
shutil.unpack_archive(temp_zip, extract_path, "zip")
|
|
251
|
-
|
|
252
|
-
# we don't need the temp zip anymore
|
|
253
|
-
temp_zip.unlink()
|
|
254
|
-
|
|
255
|
-
# Read stdout from out.txt if it exists
|
|
256
|
-
stdout_file = extract_path / "out.txt"
|
|
257
|
-
hash_file = extract_path / "hash.txt"
|
|
258
|
-
stdout = stdout_file.read_text() if stdout_file.exists() else ""
|
|
259
|
-
hash_value = hash_file.read_text() if hash_file.exists() else None
|
|
260
|
-
|
|
261
|
-
# now rezip the extracted files since we added the embedded json files
|
|
262
|
-
out_buffer = io.BytesIO()
|
|
263
|
-
with zipfile.ZipFile(
|
|
264
|
-
out_buffer, "w", zipfile.ZIP_DEFLATED, compresslevel=9
|
|
265
|
-
) as out_zip:
|
|
266
|
-
for root, _, _files in os.walk(extract_path):
|
|
267
|
-
for file in _files:
|
|
268
|
-
file_path = Path(root) / file
|
|
269
|
-
relative_path = file_path.relative_to(extract_path)
|
|
270
|
-
out_zip.write(file_path, relative_path)
|
|
271
|
-
|
|
272
|
-
return CompileResult(
|
|
273
|
-
success=True,
|
|
274
|
-
stdout=stdout,
|
|
275
|
-
hash_value=hash_value,
|
|
276
|
-
zip_bytes=out_buffer.getvalue(),
|
|
277
|
-
)
|
|
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
|
|
438
|
+
)
|
|
439
|
+
|
|
278
440
|
except KeyboardInterrupt:
|
|
279
441
|
print("Keyboard interrupt")
|
|
280
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
|
+
)
|
|
281
457
|
except httpx.HTTPError as e:
|
|
282
458
|
print(f"Error: {e}")
|
|
283
459
|
return CompileResult(
|
|
284
|
-
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
|
|
285
468
|
)
|