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/client_server.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import shutil
|
|
2
|
+
import sys
|
|
2
3
|
import tempfile
|
|
3
4
|
import threading
|
|
4
5
|
import time
|
|
@@ -9,7 +10,9 @@ from pathlib import Path
|
|
|
9
10
|
|
|
10
11
|
from fastled.compile_server import CompileServer
|
|
11
12
|
from fastled.docker_manager import DockerManager
|
|
13
|
+
from fastled.emoji_util import EMO
|
|
12
14
|
from fastled.filewatcher import DebouncedFileWatcherProcess, FileWatcherProcess
|
|
15
|
+
from fastled.find_good_connection import ConnectionResult
|
|
13
16
|
from fastled.keyboard import SpaceBarWatcher
|
|
14
17
|
from fastled.open_browser import spawn_http_server
|
|
15
18
|
from fastled.parse_args import Args
|
|
@@ -18,7 +21,6 @@ from fastled.sketch import looks_like_sketch_directory
|
|
|
18
21
|
from fastled.types import BuildMode, CompileResult, CompileServerError
|
|
19
22
|
from fastled.web_compile import (
|
|
20
23
|
SERVER_PORT,
|
|
21
|
-
ConnectionResult,
|
|
22
24
|
find_good_connection,
|
|
23
25
|
web_compile,
|
|
24
26
|
)
|
|
@@ -67,7 +69,20 @@ def TEST_BEFORE_COMPILE(url) -> None:
|
|
|
67
69
|
def _chunked_print(stdout: str) -> None:
|
|
68
70
|
lines = stdout.splitlines()
|
|
69
71
|
for line in lines:
|
|
70
|
-
|
|
72
|
+
try:
|
|
73
|
+
print(line)
|
|
74
|
+
except UnicodeEncodeError:
|
|
75
|
+
# On Windows, the console may not support Unicode characters
|
|
76
|
+
# Try to encode the line with the console's encoding and replace problematic characters
|
|
77
|
+
try:
|
|
78
|
+
console_encoding = sys.stdout.encoding or "utf-8"
|
|
79
|
+
encoded_line = line.encode(console_encoding, errors="replace").decode(
|
|
80
|
+
console_encoding
|
|
81
|
+
)
|
|
82
|
+
print(encoded_line)
|
|
83
|
+
except Exception:
|
|
84
|
+
# If all else fails, just print the line without problematic characters
|
|
85
|
+
print(line.encode("ascii", errors="replace").decode("ascii"))
|
|
71
86
|
|
|
72
87
|
|
|
73
88
|
def _run_web_compiler(
|
|
@@ -76,12 +91,26 @@ def _run_web_compiler(
|
|
|
76
91
|
build_mode: BuildMode,
|
|
77
92
|
profile: bool,
|
|
78
93
|
last_hash_value: str | None,
|
|
94
|
+
no_platformio: bool = False,
|
|
95
|
+
allow_libcompile: bool = False,
|
|
79
96
|
) -> CompileResult:
|
|
97
|
+
# Remove the import and libcompile detection logic from here
|
|
98
|
+
# since it will now be passed as a parameter
|
|
80
99
|
input_dir = Path(directory)
|
|
81
100
|
output_dir = input_dir / "fastled_js"
|
|
101
|
+
|
|
102
|
+
# Guard: libfastled compilation requires volume source mapping
|
|
103
|
+
if not allow_libcompile:
|
|
104
|
+
print(f"{EMO('⚠️', 'WARNING:')} libfastled compilation disabled.")
|
|
105
|
+
|
|
82
106
|
start = time.time()
|
|
83
107
|
web_result = web_compile(
|
|
84
|
-
directory=input_dir,
|
|
108
|
+
directory=input_dir,
|
|
109
|
+
host=host,
|
|
110
|
+
build_mode=build_mode,
|
|
111
|
+
profile=profile,
|
|
112
|
+
no_platformio=no_platformio,
|
|
113
|
+
allow_libcompile=allow_libcompile,
|
|
85
114
|
)
|
|
86
115
|
diff = time.time() - start
|
|
87
116
|
if not web_result.success:
|
|
@@ -94,14 +123,59 @@ def _run_web_compiler(
|
|
|
94
123
|
(output_dir / "index.html").write_text(error_html, encoding="utf-8")
|
|
95
124
|
return web_result
|
|
96
125
|
|
|
126
|
+
# Extract zip contents to fastled_js directory
|
|
127
|
+
extraction_start_time = time.time()
|
|
128
|
+
output_dir.mkdir(exist_ok=True)
|
|
129
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
130
|
+
temp_path = Path(temp_dir)
|
|
131
|
+
temp_zip = temp_path / "result.zip"
|
|
132
|
+
temp_zip.write_bytes(web_result.zip_bytes)
|
|
133
|
+
|
|
134
|
+
# Clear existing contents
|
|
135
|
+
shutil.rmtree(output_dir, ignore_errors=True)
|
|
136
|
+
output_dir.mkdir(exist_ok=True)
|
|
137
|
+
|
|
138
|
+
# Extract zip contents
|
|
139
|
+
shutil.unpack_archive(temp_zip, output_dir, "zip")
|
|
140
|
+
extraction_time = time.time() - extraction_start_time
|
|
141
|
+
|
|
97
142
|
def print_results() -> None:
|
|
98
143
|
hash_value = (
|
|
99
144
|
web_result.hash_value
|
|
100
145
|
if web_result.hash_value is not None
|
|
101
146
|
else "NO HASH VALUE"
|
|
102
147
|
)
|
|
148
|
+
|
|
149
|
+
# Build timing breakdown
|
|
150
|
+
timing_breakdown = f" Time: {diff:.2f} (seconds)"
|
|
151
|
+
if hasattr(web_result, "zip_time"):
|
|
152
|
+
timing_breakdown += f"\n zip creation: {web_result.zip_time:.2f}"
|
|
153
|
+
if web_result.libfastled_time > 0:
|
|
154
|
+
timing_breakdown += (
|
|
155
|
+
f"\n libfastled: {web_result.libfastled_time:.2f}"
|
|
156
|
+
)
|
|
157
|
+
timing_breakdown += (
|
|
158
|
+
f"\n sketch compile + link: {web_result.sketch_time:.2f}"
|
|
159
|
+
)
|
|
160
|
+
if hasattr(web_result, "response_processing_time"):
|
|
161
|
+
timing_breakdown += f"\n response processing: {web_result.response_processing_time:.2f}"
|
|
162
|
+
|
|
163
|
+
# Calculate any unaccounted time
|
|
164
|
+
accounted_time = (
|
|
165
|
+
web_result.zip_time
|
|
166
|
+
+ web_result.libfastled_time
|
|
167
|
+
+ web_result.sketch_time
|
|
168
|
+
+ web_result.response_processing_time
|
|
169
|
+
+ extraction_time
|
|
170
|
+
)
|
|
171
|
+
unaccounted_time = diff - accounted_time
|
|
172
|
+
if extraction_time > 0.01:
|
|
173
|
+
timing_breakdown += f"\n extraction: {extraction_time:.2f}"
|
|
174
|
+
if unaccounted_time > 0.01:
|
|
175
|
+
timing_breakdown += f"\n other overhead: {unaccounted_time:.2f}"
|
|
176
|
+
|
|
103
177
|
print(
|
|
104
|
-
f"\nWeb compilation successful\n
|
|
178
|
+
f"\nWeb compilation successful\n{timing_breakdown}\n output: {output_dir}\n hash: {hash_value}\n zip size: {len(web_result.zip_bytes)} bytes"
|
|
105
179
|
)
|
|
106
180
|
|
|
107
181
|
# now check to see if the hash value is the same as the last hash value
|
|
@@ -110,27 +184,17 @@ def _run_web_compiler(
|
|
|
110
184
|
print_results()
|
|
111
185
|
return web_result
|
|
112
186
|
|
|
113
|
-
# Extract zip contents to fastled_js directory
|
|
114
|
-
output_dir.mkdir(exist_ok=True)
|
|
115
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
116
|
-
temp_path = Path(temp_dir)
|
|
117
|
-
temp_zip = temp_path / "result.zip"
|
|
118
|
-
temp_zip.write_bytes(web_result.zip_bytes)
|
|
119
|
-
|
|
120
|
-
# Clear existing contents
|
|
121
|
-
shutil.rmtree(output_dir, ignore_errors=True)
|
|
122
|
-
output_dir.mkdir(exist_ok=True)
|
|
123
|
-
|
|
124
|
-
# Extract zip contents
|
|
125
|
-
shutil.unpack_archive(temp_zip, output_dir, "zip")
|
|
126
|
-
|
|
127
187
|
_chunked_print(web_result.stdout)
|
|
128
188
|
print_results()
|
|
129
189
|
return web_result
|
|
130
190
|
|
|
131
191
|
|
|
132
192
|
def _try_start_server_or_get_url(
|
|
133
|
-
auto_update: bool,
|
|
193
|
+
auto_update: bool,
|
|
194
|
+
args_web: str | bool,
|
|
195
|
+
localhost: bool,
|
|
196
|
+
clear: bool,
|
|
197
|
+
no_platformio: bool = False,
|
|
134
198
|
) -> tuple[str, CompileServer | None]:
|
|
135
199
|
is_local_host = localhost or (
|
|
136
200
|
isinstance(args_web, str)
|
|
@@ -140,9 +204,9 @@ def _try_start_server_or_get_url(
|
|
|
140
204
|
local_host_needs_server = False
|
|
141
205
|
if is_local_host:
|
|
142
206
|
addr = "localhost" if localhost or not isinstance(args_web, str) else args_web
|
|
143
|
-
urls = [addr]
|
|
144
207
|
if ":" not in addr:
|
|
145
|
-
|
|
208
|
+
addr = f"{addr}:{SERVER_PORT}"
|
|
209
|
+
urls = [addr]
|
|
146
210
|
|
|
147
211
|
result: ConnectionResult | None = find_good_connection(urls)
|
|
148
212
|
if result is not None:
|
|
@@ -161,7 +225,9 @@ def _try_start_server_or_get_url(
|
|
|
161
225
|
try:
|
|
162
226
|
print("No local server found, starting one...")
|
|
163
227
|
compile_server = CompileServer(
|
|
164
|
-
auto_updates=auto_update,
|
|
228
|
+
auto_updates=auto_update,
|
|
229
|
+
remove_previous=clear,
|
|
230
|
+
no_platformio=no_platformio,
|
|
165
231
|
)
|
|
166
232
|
print("Waiting for the local compiler to start...")
|
|
167
233
|
if not compile_server.ping():
|
|
@@ -177,7 +243,9 @@ def _try_start_server_or_get_url(
|
|
|
177
243
|
return (DEFAULT_URL, None)
|
|
178
244
|
|
|
179
245
|
|
|
180
|
-
def _try_make_compile_server(
|
|
246
|
+
def _try_make_compile_server(
|
|
247
|
+
clear: bool = False, no_platformio: bool = False
|
|
248
|
+
) -> CompileServer | None:
|
|
181
249
|
if not DockerManager.is_docker_installed():
|
|
182
250
|
return None
|
|
183
251
|
try:
|
|
@@ -189,7 +257,9 @@ def _try_make_compile_server(clear: bool = False) -> CompileServer | None:
|
|
|
189
257
|
free_port = find_free_port(start_port=9723, end_port=9743)
|
|
190
258
|
if free_port is None:
|
|
191
259
|
return None
|
|
192
|
-
compile_server = CompileServer(
|
|
260
|
+
compile_server = CompileServer(
|
|
261
|
+
auto_updates=False, remove_previous=clear, no_platformio=no_platformio
|
|
262
|
+
)
|
|
193
263
|
print("Waiting for the local compiler to start...")
|
|
194
264
|
if not compile_server.ping():
|
|
195
265
|
print("Failed to start local compiler.")
|
|
@@ -205,14 +275,41 @@ def _try_make_compile_server(clear: bool = False) -> CompileServer | None:
|
|
|
205
275
|
return None
|
|
206
276
|
|
|
207
277
|
|
|
208
|
-
def
|
|
278
|
+
def _background_update_docker_image() -> None:
|
|
279
|
+
"""Perform docker image update in the background silently."""
|
|
280
|
+
try:
|
|
281
|
+
# Only attempt update if Docker is installed and running
|
|
282
|
+
if not DockerManager.is_docker_installed():
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
docker_manager = DockerManager()
|
|
286
|
+
docker_running, _ = docker_manager.is_running()
|
|
287
|
+
if not docker_running:
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
# Silently update the docker image
|
|
291
|
+
docker_manager.validate_or_download_image(
|
|
292
|
+
image_name=IMAGE_NAME, tag="latest", upgrade=True
|
|
293
|
+
)
|
|
294
|
+
except KeyboardInterrupt:
|
|
295
|
+
import _thread
|
|
296
|
+
|
|
297
|
+
_thread.interrupt_main()
|
|
298
|
+
except Exception as e:
|
|
299
|
+
# Log warning but don't disrupt user experience
|
|
300
|
+
import warnings
|
|
301
|
+
|
|
302
|
+
warnings.warn(f"Background docker image update failed: {e}")
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _is_local_host(url: str) -> bool:
|
|
209
306
|
return (
|
|
210
|
-
|
|
211
|
-
or
|
|
212
|
-
or
|
|
213
|
-
or
|
|
214
|
-
or
|
|
215
|
-
or
|
|
307
|
+
url.startswith("http://localhost")
|
|
308
|
+
or url.startswith("http://127.0.0.1")
|
|
309
|
+
or url.startswith("http://0.0.0.0")
|
|
310
|
+
or url.startswith("http://[::]")
|
|
311
|
+
or url.startswith("http://[::1]")
|
|
312
|
+
or url.startswith("http://[::ffff:127.0.0.1]")
|
|
216
313
|
)
|
|
217
314
|
|
|
218
315
|
|
|
@@ -228,13 +325,19 @@ def run_client(
|
|
|
228
325
|
int | None
|
|
229
326
|
) = None, # None means auto select a free port, http_port < 0 means no server.
|
|
230
327
|
clear: bool = False,
|
|
328
|
+
no_platformio: bool = False,
|
|
329
|
+
app: bool = False, # Use app-like browser experience
|
|
330
|
+
background_update: bool = False,
|
|
331
|
+
enable_https: bool = True, # Enable HTTPS for the local server
|
|
231
332
|
) -> int:
|
|
232
333
|
has_checked_newer_version_yet = False
|
|
233
334
|
compile_server: CompileServer | None = None
|
|
234
335
|
|
|
235
336
|
if host is None:
|
|
236
337
|
# attempt to start a compile server if docker is installed.
|
|
237
|
-
compile_server = _try_make_compile_server(
|
|
338
|
+
compile_server = _try_make_compile_server(
|
|
339
|
+
clear=clear, no_platformio=no_platformio
|
|
340
|
+
)
|
|
238
341
|
if compile_server is None:
|
|
239
342
|
host = DEFAULT_URL
|
|
240
343
|
elif isinstance(host, CompileServer):
|
|
@@ -267,6 +370,11 @@ def run_client(
|
|
|
267
370
|
# Assume default port for www
|
|
268
371
|
port = 80
|
|
269
372
|
|
|
373
|
+
# Auto-detect libcompile capability on first call
|
|
374
|
+
from fastled.sketch import looks_like_fastled_repo
|
|
375
|
+
|
|
376
|
+
allow_libcompile = is_local_host and looks_like_fastled_repo(Path(".").resolve())
|
|
377
|
+
|
|
270
378
|
try:
|
|
271
379
|
|
|
272
380
|
def compile_function(
|
|
@@ -274,6 +382,8 @@ def run_client(
|
|
|
274
382
|
build_mode: BuildMode = build_mode,
|
|
275
383
|
profile: bool = profile,
|
|
276
384
|
last_hash_value: str | None = None,
|
|
385
|
+
no_platformio: bool = no_platformio,
|
|
386
|
+
allow_libcompile: bool = allow_libcompile,
|
|
277
387
|
) -> CompileResult:
|
|
278
388
|
TEST_BEFORE_COMPILE(url)
|
|
279
389
|
return _run_web_compiler(
|
|
@@ -282,6 +392,8 @@ def run_client(
|
|
|
282
392
|
build_mode=build_mode,
|
|
283
393
|
profile=profile,
|
|
284
394
|
last_hash_value=last_hash_value,
|
|
395
|
+
no_platformio=no_platformio,
|
|
396
|
+
allow_libcompile=allow_libcompile,
|
|
285
397
|
)
|
|
286
398
|
|
|
287
399
|
result: CompileResult = compile_function(last_hash_value=None)
|
|
@@ -304,13 +416,18 @@ def run_client(
|
|
|
304
416
|
port=http_port,
|
|
305
417
|
compile_server_port=port,
|
|
306
418
|
open_browser=open_web_browser,
|
|
419
|
+
app=app,
|
|
420
|
+
enable_https=enable_https,
|
|
307
421
|
)
|
|
308
422
|
else:
|
|
309
|
-
|
|
423
|
+
if result.success:
|
|
424
|
+
print("\nCompilation successful.")
|
|
425
|
+
else:
|
|
426
|
+
print("\nCompilation failed.")
|
|
310
427
|
if compile_server:
|
|
311
428
|
print("Shutting down compile server...")
|
|
312
429
|
compile_server.stop()
|
|
313
|
-
return 0
|
|
430
|
+
return 0 if result.success else 1
|
|
314
431
|
|
|
315
432
|
if not keep_running or shutdown.is_set():
|
|
316
433
|
if http_proc:
|
|
@@ -337,7 +454,15 @@ def run_client(
|
|
|
337
454
|
) -> tuple[bool, CompileResult]:
|
|
338
455
|
changed_files = debounced_sketch_watcher.get_all_changes()
|
|
339
456
|
if changed_files:
|
|
340
|
-
|
|
457
|
+
# Filter out any fastled_js changes that slipped through
|
|
458
|
+
sketch_changes = [
|
|
459
|
+
f for f in changed_files if "fastled_js" not in Path(f).parts
|
|
460
|
+
]
|
|
461
|
+
if not sketch_changes:
|
|
462
|
+
# All changes were in fastled_js, ignore them
|
|
463
|
+
return False, last_compiled_result
|
|
464
|
+
print(f"\nChanges detected in {sketch_changes}")
|
|
465
|
+
print("Compiling...")
|
|
341
466
|
last_hash_value = last_compiled_result.hash_value
|
|
342
467
|
out = compile_function(last_hash_value=last_hash_value)
|
|
343
468
|
if not out.success:
|
|
@@ -374,9 +499,20 @@ def run_client(
|
|
|
374
499
|
)
|
|
375
500
|
if has_update:
|
|
376
501
|
print(f"\n🔄 {message}")
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
502
|
+
if background_update:
|
|
503
|
+
# Start background update in a separate thread
|
|
504
|
+
update_thread = threading.Thread(
|
|
505
|
+
target=_background_update_docker_image, daemon=True
|
|
506
|
+
)
|
|
507
|
+
update_thread.start()
|
|
508
|
+
background_update = False
|
|
509
|
+
else:
|
|
510
|
+
print(
|
|
511
|
+
"Run with `fastled -u` to update the docker image to the latest version."
|
|
512
|
+
)
|
|
513
|
+
print(
|
|
514
|
+
"Or use `--background-update` to update automatically in the background after compilation."
|
|
515
|
+
)
|
|
380
516
|
except Exception as e:
|
|
381
517
|
# Don't let Docker check failures interrupt the main flow
|
|
382
518
|
warnings.warn(f"Failed to check for Docker image updates: {e}")
|
|
@@ -408,6 +544,7 @@ def run_client(
|
|
|
408
544
|
if changed_files:
|
|
409
545
|
print(f"\nChanges detected in FastLED source code: {changed_files}")
|
|
410
546
|
print("Press space bar to trigger compile.")
|
|
547
|
+
|
|
411
548
|
while True:
|
|
412
549
|
space_bar_pressed = SpaceBarWatcher.watch_space_bar_pressed(
|
|
413
550
|
timeout=1.0
|
|
@@ -421,6 +558,8 @@ def run_client(
|
|
|
421
558
|
print(
|
|
422
559
|
f"Changes detected in {file_changes}\nHit the space bar to trigger compile."
|
|
423
560
|
)
|
|
561
|
+
# Re-evaluate libcompile capability when source code changes
|
|
562
|
+
allow_libcompile = True
|
|
424
563
|
|
|
425
564
|
if space_bar_pressed or sketch_files_changed:
|
|
426
565
|
if space_bar_pressed:
|
|
@@ -430,7 +569,10 @@ def run_client(
|
|
|
430
569
|
f"Changes detected in {','.join(sketch_files_changed)}, triggering recompile..."
|
|
431
570
|
)
|
|
432
571
|
last_compiled_result = compile_function(
|
|
433
|
-
last_hash_value=None
|
|
572
|
+
last_hash_value=None, allow_libcompile=allow_libcompile
|
|
573
|
+
)
|
|
574
|
+
allow_libcompile = (
|
|
575
|
+
allow_libcompile and not last_compiled_result.success
|
|
434
576
|
)
|
|
435
577
|
print("Finished recompile.")
|
|
436
578
|
# Drain the space bar queue
|
|
@@ -456,6 +598,7 @@ def run_client_server(args: Args) -> int:
|
|
|
456
598
|
profile = bool(args.profile)
|
|
457
599
|
web: str | bool = args.web if isinstance(args.web, str) else bool(args.web)
|
|
458
600
|
auto_update = bool(args.auto_update)
|
|
601
|
+
background_update = bool(args.background_update)
|
|
459
602
|
localhost = bool(args.localhost)
|
|
460
603
|
directory = args.directory if args.directory else Path(".")
|
|
461
604
|
just_compile = bool(args.just_compile)
|
|
@@ -463,6 +606,8 @@ def run_client_server(args: Args) -> int:
|
|
|
463
606
|
force_compile = bool(args.force_compile)
|
|
464
607
|
open_web_browser = not just_compile and not interactive
|
|
465
608
|
build_mode: BuildMode = BuildMode.from_args(args)
|
|
609
|
+
no_platformio = bool(args.no_platformio)
|
|
610
|
+
app = bool(args.app)
|
|
466
611
|
|
|
467
612
|
if not force_compile and not looks_like_sketch_directory(directory):
|
|
468
613
|
# if there is only one directory in the sketch directory, use that
|
|
@@ -492,7 +637,7 @@ def run_client_server(args: Args) -> int:
|
|
|
492
637
|
compile_server: CompileServer | None = None
|
|
493
638
|
try:
|
|
494
639
|
url, compile_server = _try_start_server_or_get_url(
|
|
495
|
-
auto_update, web, localhost, args.clear
|
|
640
|
+
auto_update, web, localhost, args.clear, no_platformio
|
|
496
641
|
)
|
|
497
642
|
except KeyboardInterrupt:
|
|
498
643
|
print("\nExiting from first try...")
|
|
@@ -524,6 +669,9 @@ def run_client_server(args: Args) -> int:
|
|
|
524
669
|
build_mode=build_mode,
|
|
525
670
|
profile=profile,
|
|
526
671
|
clear=args.clear,
|
|
672
|
+
no_platformio=no_platformio,
|
|
673
|
+
app=app,
|
|
674
|
+
background_update=background_update,
|
|
527
675
|
)
|
|
528
676
|
except KeyboardInterrupt:
|
|
529
677
|
return 1
|
fastled/compile_server.py
CHANGED
|
@@ -15,6 +15,8 @@ class CompileServer:
|
|
|
15
15
|
container_name: str | None = None,
|
|
16
16
|
platform: Platform = Platform.WASM,
|
|
17
17
|
remove_previous: bool = False,
|
|
18
|
+
no_platformio: bool = False,
|
|
19
|
+
allow_libcompile: bool = True,
|
|
18
20
|
) -> None:
|
|
19
21
|
from fastled.compile_server_impl import ( # avoid circular import
|
|
20
22
|
CompileServerImpl,
|
|
@@ -29,6 +31,8 @@ class CompileServer:
|
|
|
29
31
|
mapped_dir=mapped_dir,
|
|
30
32
|
auto_start=auto_start,
|
|
31
33
|
remove_previous=remove_previous,
|
|
34
|
+
no_platformio=no_platformio,
|
|
35
|
+
allow_libcompile=allow_libcompile,
|
|
32
36
|
)
|
|
33
37
|
|
|
34
38
|
# May throw CompileServerError if server could not be started.
|
|
@@ -97,3 +101,9 @@ class CompileServer:
|
|
|
97
101
|
|
|
98
102
|
def process_running(self) -> bool:
|
|
99
103
|
return self.impl.process_running()
|
|
104
|
+
|
|
105
|
+
def get_emsdk_headers(self, filepath: Path) -> None:
|
|
106
|
+
"""Get EMSDK headers ZIP data from the server and save to filepath."""
|
|
107
|
+
if not str(filepath).endswith(".zip"):
|
|
108
|
+
raise ValueError("Filepath must end with .zip")
|
|
109
|
+
return self.impl.get_emsdk_headers(filepath)
|
fastled/compile_server_impl.py
CHANGED
|
@@ -15,6 +15,7 @@ from fastled.docker_manager import (
|
|
|
15
15
|
RunningContainer,
|
|
16
16
|
Volume,
|
|
17
17
|
)
|
|
18
|
+
from fastled.emoji_util import EMO
|
|
18
19
|
from fastled.settings import DEFAULT_CONTAINER_NAME, IMAGE_NAME, SERVER_PORT
|
|
19
20
|
from fastled.sketch import looks_like_fastled_repo
|
|
20
21
|
from fastled.types import BuildMode, CompileResult, CompileServerError
|
|
@@ -50,6 +51,8 @@ class CompileServerImpl:
|
|
|
50
51
|
auto_start: bool = True,
|
|
51
52
|
container_name: str | None = None,
|
|
52
53
|
remove_previous: bool = False,
|
|
54
|
+
no_platformio: bool = False,
|
|
55
|
+
allow_libcompile: bool = True,
|
|
53
56
|
) -> None:
|
|
54
57
|
container_name = container_name or DEFAULT_CONTAINER_NAME
|
|
55
58
|
if interactive and not mapped_dir:
|
|
@@ -68,6 +71,18 @@ class CompileServerImpl:
|
|
|
68
71
|
self.running_container: RunningContainer | None = None
|
|
69
72
|
self.auto_updates = auto_updates
|
|
70
73
|
self.remove_previous = remove_previous
|
|
74
|
+
self.no_platformio = no_platformio
|
|
75
|
+
|
|
76
|
+
# Guard: libfastled compilation requires volume source mapping
|
|
77
|
+
# If we don't have fastled_src_dir (not in FastLED repo), disable libcompile
|
|
78
|
+
if allow_libcompile and self.fastled_src_dir is None:
|
|
79
|
+
print(
|
|
80
|
+
f"{EMO('⚠️', 'WARNING:')} libfastled compilation disabled: volume source mapping not available"
|
|
81
|
+
)
|
|
82
|
+
print(" (not running in FastLED repository)")
|
|
83
|
+
allow_libcompile = False
|
|
84
|
+
|
|
85
|
+
self.allow_libcompile = allow_libcompile
|
|
71
86
|
self._port = 0 # 0 until compile server is started
|
|
72
87
|
if auto_start:
|
|
73
88
|
self.start()
|
|
@@ -105,7 +120,12 @@ class CompileServerImpl:
|
|
|
105
120
|
if not self.ping():
|
|
106
121
|
raise RuntimeError("Server is not running")
|
|
107
122
|
out: CompileResult = web_compile(
|
|
108
|
-
directory,
|
|
123
|
+
directory,
|
|
124
|
+
host=self.url(),
|
|
125
|
+
build_mode=build_mode,
|
|
126
|
+
profile=profile,
|
|
127
|
+
no_platformio=self.no_platformio,
|
|
128
|
+
allow_libcompile=self.allow_libcompile,
|
|
109
129
|
)
|
|
110
130
|
return out
|
|
111
131
|
|
|
@@ -217,6 +237,8 @@ class CompileServerImpl:
|
|
|
217
237
|
server_command = ["/bin/bash"]
|
|
218
238
|
else:
|
|
219
239
|
server_command = ["python", "/js/run.py", "server"] + SERVER_OPTIONS
|
|
240
|
+
if self.no_platformio:
|
|
241
|
+
server_command.append("--no-platformio")
|
|
220
242
|
if self.interactive:
|
|
221
243
|
print("Disabling port forwarding in interactive mode")
|
|
222
244
|
ports = {}
|
|
@@ -316,3 +338,14 @@ class CompileServerImpl:
|
|
|
316
338
|
self.docker.suspend_container(self.container_name)
|
|
317
339
|
self._port = 0
|
|
318
340
|
print("Compile server stopped")
|
|
341
|
+
|
|
342
|
+
def get_emsdk_headers(self, filepath: Path) -> None:
|
|
343
|
+
"""Get EMSDK headers ZIP data from the server and save to filepath."""
|
|
344
|
+
from fastled.util import download_emsdk_headers
|
|
345
|
+
|
|
346
|
+
if not self._port:
|
|
347
|
+
raise RuntimeError("Server has not been started yet")
|
|
348
|
+
if not self.ping():
|
|
349
|
+
raise RuntimeError("Server is not running")
|
|
350
|
+
|
|
351
|
+
download_emsdk_headers(self.url(), filepath)
|
fastled/docker_manager.py
CHANGED
|
@@ -26,7 +26,6 @@ from docker.models.images import Image
|
|
|
26
26
|
from filelock import FileLock
|
|
27
27
|
|
|
28
28
|
from fastled.print_filter import PrintFilter, PrintFilterDefault
|
|
29
|
-
from fastled.spinner import Spinner
|
|
30
29
|
|
|
31
30
|
CONFIG_DIR = Path(user_data_dir("fastled", "fastled"))
|
|
32
31
|
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
@@ -252,7 +251,34 @@ class DockerManager:
|
|
|
252
251
|
@property
|
|
253
252
|
def client(self) -> DockerClient:
|
|
254
253
|
if self._client is None:
|
|
255
|
-
|
|
254
|
+
# Retry logic for WSL startup on Windows
|
|
255
|
+
max_retries = 10
|
|
256
|
+
retry_delay = 2 # seconds
|
|
257
|
+
last_error = None
|
|
258
|
+
|
|
259
|
+
for attempt in range(max_retries):
|
|
260
|
+
try:
|
|
261
|
+
self._client = docker.from_env()
|
|
262
|
+
if attempt > 0:
|
|
263
|
+
print(
|
|
264
|
+
f"Successfully connected to Docker after {attempt + 1} attempts"
|
|
265
|
+
)
|
|
266
|
+
return self._client
|
|
267
|
+
except DockerException as e:
|
|
268
|
+
last_error = e
|
|
269
|
+
if attempt < max_retries - 1:
|
|
270
|
+
if attempt == 0:
|
|
271
|
+
print("Waiting for Docker/WSL to be ready...")
|
|
272
|
+
print(
|
|
273
|
+
f"Attempt {attempt + 1}/{max_retries} failed, retrying in {retry_delay}s..."
|
|
274
|
+
)
|
|
275
|
+
time.sleep(retry_delay)
|
|
276
|
+
else:
|
|
277
|
+
print(
|
|
278
|
+
f"Failed to connect to Docker after {max_retries} attempts"
|
|
279
|
+
)
|
|
280
|
+
raise last_error
|
|
281
|
+
assert self._client is not None
|
|
256
282
|
return self._client
|
|
257
283
|
|
|
258
284
|
@staticmethod
|
|
@@ -507,11 +533,9 @@ class DockerManager:
|
|
|
507
533
|
return False
|
|
508
534
|
|
|
509
535
|
# Quick check for latest version
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
print(f"Running command: {cmd_str}")
|
|
514
|
-
subprocess.run(cmd_list, check=True)
|
|
536
|
+
print(f"Pulling newer version of {image_name}:{tag}...")
|
|
537
|
+
cmd_list = ["docker", "pull", f"{image_name}:{tag}"]
|
|
538
|
+
subprocess.run(cmd_list, check=True)
|
|
515
539
|
print(f"Updated to newer version of {image_name}:{tag}")
|
|
516
540
|
local_image_hash = self.client.images.get(f"{image_name}:{tag}").id
|
|
517
541
|
assert local_image_hash is not None
|
|
@@ -521,12 +545,10 @@ class DockerManager:
|
|
|
521
545
|
|
|
522
546
|
except ImageNotFound:
|
|
523
547
|
print(f"Image {image_name}:{tag} not found.")
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
print(f"Running command: {cmd_str}")
|
|
529
|
-
subprocess.run(cmd_list, check=True)
|
|
548
|
+
print("Loading...")
|
|
549
|
+
# We use docker cli here because it shows the download.
|
|
550
|
+
cmd_list = ["docker", "pull", f"{image_name}:{tag}"]
|
|
551
|
+
subprocess.run(cmd_list, check=True)
|
|
530
552
|
try:
|
|
531
553
|
local_image = self.client.images.get(f"{image_name}:{tag}")
|
|
532
554
|
local_image_hash = local_image.id
|
|
@@ -980,6 +1002,7 @@ class DockerManager:
|
|
|
980
1002
|
def purge(self, image_name: str) -> None:
|
|
981
1003
|
"""
|
|
982
1004
|
Remove all containers and images associated with the given image name.
|
|
1005
|
+
Also removes FastLED containers by name pattern (including test containers).
|
|
983
1006
|
|
|
984
1007
|
Args:
|
|
985
1008
|
image_name: The name of the image to purge (without tag)
|
|
@@ -990,8 +1013,27 @@ class DockerManager:
|
|
|
990
1013
|
try:
|
|
991
1014
|
containers = self.client.containers.list(all=True)
|
|
992
1015
|
for container in containers:
|
|
1016
|
+
should_remove = False
|
|
1017
|
+
|
|
1018
|
+
# Check if container uses the specified image
|
|
993
1019
|
if any(image_name in tag for tag in container.image.tags):
|
|
994
|
-
|
|
1020
|
+
should_remove = True
|
|
1021
|
+
print(
|
|
1022
|
+
f"Removing container {container.name} (uses image {image_name})"
|
|
1023
|
+
)
|
|
1024
|
+
|
|
1025
|
+
# Also check for FastLED container name patterns (including test containers)
|
|
1026
|
+
elif any(
|
|
1027
|
+
pattern in container.name
|
|
1028
|
+
for pattern in [
|
|
1029
|
+
"fastled-wasm-container",
|
|
1030
|
+
"fastled-wasm-container-test",
|
|
1031
|
+
]
|
|
1032
|
+
):
|
|
1033
|
+
should_remove = True
|
|
1034
|
+
print(f"Removing FastLED container {container.name}")
|
|
1035
|
+
|
|
1036
|
+
if should_remove:
|
|
995
1037
|
container.remove(force=True)
|
|
996
1038
|
|
|
997
1039
|
except Exception as e:
|
fastled/emoji_util.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Emoji utility functions for handling Unicode display issues on Windows cmd.exe
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def EMO(emoji: str, fallback: str) -> str:
|
|
9
|
+
"""Get emoji with fallback for systems that don't support Unicode properly"""
|
|
10
|
+
try:
|
|
11
|
+
# Test if we can encode the emoji properly
|
|
12
|
+
emoji.encode(sys.stdout.encoding or "utf-8")
|
|
13
|
+
return emoji
|
|
14
|
+
except (UnicodeEncodeError, AttributeError):
|
|
15
|
+
return fallback
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def safe_print(text: str) -> None:
|
|
19
|
+
"""Print text safely, handling Unicode/emoji encoding issues on Windows cmd.exe"""
|
|
20
|
+
try:
|
|
21
|
+
print(text)
|
|
22
|
+
except UnicodeEncodeError:
|
|
23
|
+
# Replace problematic characters with safe alternatives
|
|
24
|
+
safe_text = text.encode(
|
|
25
|
+
sys.stdout.encoding or "utf-8", errors="replace"
|
|
26
|
+
).decode(sys.stdout.encoding or "utf-8")
|
|
27
|
+
print(safe_text)
|