fastled 1.2.68__tar.gz → 1.2.74__tar.gz

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.
Files changed (129) hide show
  1. {fastled-1.2.68 → fastled-1.2.74}/Dockerfile +2 -2
  2. {fastled-1.2.68 → fastled-1.2.74}/PKG-INFO +1 -1
  3. {fastled-1.2.68 → fastled-1.2.74}/compiler/compile.py +76 -32
  4. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/__init__.py +1 -1
  5. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/app.py +5 -0
  6. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/compile_server_impl.py +26 -16
  7. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/docker_manager.py +142 -17
  8. fastled-1.2.74/src/fastled/keyz.py +84 -0
  9. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/open_browser.py +3 -13
  10. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/parse_args.py +2 -1
  11. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/string_diff.py +22 -0
  12. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/util.py +9 -0
  13. {fastled-1.2.68 → fastled-1.2.74}/src/fastled.egg-info/PKG-INFO +1 -1
  14. fastled-1.2.68/src/fastled/keyz.py +0 -31
  15. {fastled-1.2.68 → fastled-1.2.74}/.aiderignore +0 -0
  16. {fastled-1.2.68 → fastled-1.2.74}/.dockerignore +0 -0
  17. {fastled-1.2.68 → fastled-1.2.74}/.github/workflows/build_multi_docker_image.yml +0 -0
  18. {fastled-1.2.68 → fastled-1.2.74}/.github/workflows/build_webpage.yml +0 -0
  19. {fastled-1.2.68 → fastled-1.2.74}/.github/workflows/lint.yml +0 -0
  20. {fastled-1.2.68 → fastled-1.2.74}/.github/workflows/publish_release.yml +0 -0
  21. {fastled-1.2.68 → fastled-1.2.74}/.github/workflows/template_build_docker_image.yml +0 -0
  22. {fastled-1.2.68 → fastled-1.2.74}/.github/workflows/test_build_exe.yml +0 -0
  23. {fastled-1.2.68 → fastled-1.2.74}/.github/workflows/test_macos.yml +0 -0
  24. {fastled-1.2.68 → fastled-1.2.74}/.github/workflows/test_ubuntu.yml +0 -0
  25. {fastled-1.2.68 → fastled-1.2.74}/.github/workflows/test_win.yml +0 -0
  26. {fastled-1.2.68 → fastled-1.2.74}/.gitignore +0 -0
  27. {fastled-1.2.68 → fastled-1.2.74}/.pylintrc +0 -0
  28. {fastled-1.2.68 → fastled-1.2.74}/.vscode/launch.json +0 -0
  29. {fastled-1.2.68 → fastled-1.2.74}/.vscode/settings.json +0 -0
  30. {fastled-1.2.68 → fastled-1.2.74}/.vscode/tasks.json +0 -0
  31. {fastled-1.2.68 → fastled-1.2.74}/LICENSE +0 -0
  32. {fastled-1.2.68 → fastled-1.2.74}/MANIFEST.in +0 -0
  33. {fastled-1.2.68 → fastled-1.2.74}/README.md +0 -0
  34. {fastled-1.2.68 → fastled-1.2.74}/RELEASE.md +0 -0
  35. {fastled-1.2.68 → fastled-1.2.74}/TODO.md +0 -0
  36. {fastled-1.2.68 → fastled-1.2.74}/build_exe.py +0 -0
  37. {fastled-1.2.68 → fastled-1.2.74}/build_site.py +0 -0
  38. {fastled-1.2.68 → fastled-1.2.74}/clean +0 -0
  39. {fastled-1.2.68 → fastled-1.2.74}/compiler/CMakeLists.txt +0 -0
  40. {fastled-1.2.68 → fastled-1.2.74}/compiler/__init__.py +0 -0
  41. {fastled-1.2.68 → fastled-1.2.74}/compiler/arduino-pre-process.sh +0 -0
  42. {fastled-1.2.68 → fastled-1.2.74}/compiler/build.sh +0 -0
  43. {fastled-1.2.68 → fastled-1.2.74}/compiler/build_archive.sh +0 -0
  44. {fastled-1.2.68 → fastled-1.2.74}/compiler/build_fast.sh +0 -0
  45. {fastled-1.2.68 → fastled-1.2.74}/compiler/code_sync.py +0 -0
  46. {fastled-1.2.68 → fastled-1.2.74}/compiler/compile_lock.py +0 -0
  47. {fastled-1.2.68 → fastled-1.2.74}/compiler/entrypoint.sh +0 -0
  48. {fastled-1.2.68 → fastled-1.2.74}/compiler/extra/100dots.html +0 -0
  49. {fastled-1.2.68 → fastled-1.2.74}/compiler/extra/demo_threejs.html +0 -0
  50. {fastled-1.2.68 → fastled-1.2.74}/compiler/extra/micdemo.html +0 -0
  51. {fastled-1.2.68 → fastled-1.2.74}/compiler/extra/mp3upload.html +0 -0
  52. {fastled-1.2.68 → fastled-1.2.74}/compiler/extra/webgl_postprocessing_unreal_bloom.html +0 -0
  53. {fastled-1.2.68 → fastled-1.2.74}/compiler/final_prewarm.sh +0 -0
  54. {fastled-1.2.68 → fastled-1.2.74}/compiler/init_runtime.py +0 -0
  55. {fastled-1.2.68 → fastled-1.2.74}/compiler/install-arduino-cli.sh +0 -0
  56. {fastled-1.2.68 → fastled-1.2.74}/compiler/libcompile/CMakeLists.txt +0 -0
  57. {fastled-1.2.68 → fastled-1.2.74}/compiler/paths.py +0 -0
  58. {fastled-1.2.68 → fastled-1.2.74}/compiler/pre-process.sh +0 -0
  59. {fastled-1.2.68 → fastled-1.2.74}/compiler/prewarm.sh +0 -0
  60. {fastled-1.2.68 → fastled-1.2.74}/compiler/process-ino.py +0 -0
  61. {fastled-1.2.68 → fastled-1.2.74}/compiler/process_extended.py +0 -0
  62. {fastled-1.2.68 → fastled-1.2.74}/compiler/pyproject.toml +0 -0
  63. {fastled-1.2.68 → fastled-1.2.74}/compiler/run.py +0 -0
  64. {fastled-1.2.68 → fastled-1.2.74}/compiler/server.py +0 -0
  65. {fastled-1.2.68 → fastled-1.2.74}/compiler/sketch_hasher.py +0 -0
  66. {fastled-1.2.68 → fastled-1.2.74}/compiler/wasm_compiler_flags.py +0 -0
  67. {fastled-1.2.68 → fastled-1.2.74}/docker-compose.yml +0 -0
  68. {fastled-1.2.68 → fastled-1.2.74}/install +0 -0
  69. {fastled-1.2.68 → fastled-1.2.74}/install_linux.sh +0 -0
  70. {fastled-1.2.68 → fastled-1.2.74}/lint +0 -0
  71. {fastled-1.2.68 → fastled-1.2.74}/pyproject.toml +0 -0
  72. {fastled-1.2.68 → fastled-1.2.74}/requirements.testing.txt +0 -0
  73. {fastled-1.2.68 → fastled-1.2.74}/setup.cfg +0 -0
  74. {fastled-1.2.68 → fastled-1.2.74}/setup.py +0 -0
  75. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/assets/example.txt +0 -0
  76. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/assets/localhost-key.pem +0 -0
  77. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/assets/localhost.pem +0 -0
  78. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/cli.py +0 -0
  79. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/cli_test.py +0 -0
  80. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/client_server.py +0 -0
  81. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/compile_server.py +0 -0
  82. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/filewatcher.py +0 -0
  83. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/interactive_srcs.py +0 -0
  84. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/keyboard.py +0 -0
  85. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/live_client.py +0 -0
  86. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/paths.py +0 -0
  87. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/project_init.py +0 -0
  88. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/select_sketch_directory.py +0 -0
  89. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/server_fastapi.py +0 -0
  90. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/server_fastapi_cli.py +0 -0
  91. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/server_flask.py +0 -0
  92. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/server_start.py +0 -0
  93. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/settings.py +0 -0
  94. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/site/build.py +0 -0
  95. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/site/examples.py +0 -0
  96. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/sketch.py +0 -0
  97. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/spinner.py +0 -0
  98. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/test/can_run_local_docker_tests.py +0 -0
  99. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/test/examples.py +0 -0
  100. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/types.py +0 -0
  101. {fastled-1.2.68 → fastled-1.2.74}/src/fastled/web_compile.py +0 -0
  102. {fastled-1.2.68 → fastled-1.2.74}/src/fastled.egg-info/SOURCES.txt +0 -0
  103. {fastled-1.2.68 → fastled-1.2.74}/src/fastled.egg-info/dependency_links.txt +0 -0
  104. {fastled-1.2.68 → fastled-1.2.74}/src/fastled.egg-info/entry_points.txt +0 -0
  105. {fastled-1.2.68 → fastled-1.2.74}/src/fastled.egg-info/requires.txt +0 -0
  106. {fastled-1.2.68 → fastled-1.2.74}/src/fastled.egg-info/top_level.txt +0 -0
  107. {fastled-1.2.68 → fastled-1.2.74}/test +0 -0
  108. {fastled-1.2.68 → fastled-1.2.74}/tests/integration/test_build_examples.py +0 -0
  109. {fastled-1.2.68 → fastled-1.2.74}/tests/integration/test_examples.py +0 -0
  110. {fastled-1.2.68 → fastled-1.2.74}/tests/unit/html/index.html +0 -0
  111. {fastled-1.2.68 → fastled-1.2.74}/tests/unit/test_api.py +0 -0
  112. {fastled-1.2.68 → fastled-1.2.74}/tests/unit/test_bad_ino.py +0 -0
  113. {fastled-1.2.68 → fastled-1.2.74}/tests/unit/test_cli.py +0 -0
  114. {fastled-1.2.68 → fastled-1.2.74}/tests/unit/test_compile_server.py +0 -0
  115. {fastled-1.2.68 → fastled-1.2.74}/tests/unit/test_docker_linux_on_windows.py +0 -0
  116. {fastled-1.2.68 → fastled-1.2.74}/tests/unit/test_embedded_data.py +0 -0
  117. {fastled-1.2.68 → fastled-1.2.74}/tests/unit/test_filechanger.py +0 -0
  118. {fastled-1.2.68 → fastled-1.2.74}/tests/unit/test_http_server.py +0 -0
  119. {fastled-1.2.68 → fastled-1.2.74}/tests/unit/test_ino/bad/bad.ino +0 -0
  120. {fastled-1.2.68 → fastled-1.2.74}/tests/unit/test_ino/bad_platformio/bad_platformio.ino +0 -0
  121. {fastled-1.2.68 → fastled-1.2.74}/tests/unit/test_ino/bad_platformio/platformio.ini +0 -0
  122. {fastled-1.2.68 → fastled-1.2.74}/tests/unit/test_ino/embedded/data/bigdata.dat +0 -0
  123. {fastled-1.2.68 → fastled-1.2.74}/tests/unit/test_ino/embedded/wasm.ino +0 -0
  124. {fastled-1.2.68 → fastled-1.2.74}/tests/unit/test_ino/wasm/wasm.ino +0 -0
  125. {fastled-1.2.68 → fastled-1.2.74}/tests/unit/test_project_init.py +0 -0
  126. {fastled-1.2.68 → fastled-1.2.74}/tests/unit/test_server_and_client_seperatly.py +0 -0
  127. {fastled-1.2.68 → fastled-1.2.74}/tests/unit/test_string_diff.py +0 -0
  128. {fastled-1.2.68 → fastled-1.2.74}/tests/unit/test_webcompile.py +0 -0
  129. {fastled-1.2.68 → fastled-1.2.74}/upload_package.sh +0 -0
@@ -41,8 +41,8 @@
41
41
 
42
42
  # This will be set to arm64 to support MacOS M1+ devices (and Linux-based arm64 devices)
43
43
  ARG PLATFORM_TAG=""
44
- # ARG EMSDK_VERSION_TAG="4.0.4"
45
- ARG EMSDK_VERSION_TAG="3.1.70"
44
+ ARG EMSDK_VERSION_TAG="4.0.8"
45
+ # ARG EMSDK_VERSION_TAG="3.1.70"
46
46
 
47
47
  # Use only Emscripten base image
48
48
  FROM emscripten/emsdk:${EMSDK_VERSION_TAG}${PLATFORM_TAG}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastled
3
- Version: 1.2.68
3
+ Version: 1.2.74
4
4
  Summary: FastLED Wasm Compiler
5
5
  Home-page: https://github.com/zackees/fastled-wasm
6
6
  Maintainer: Zachary Vorhies
@@ -89,6 +89,33 @@ def copy_files(src_dir: Path, js_src: Path) -> None:
89
89
  shutil.copy2(item, js_src / item.name)
90
90
 
91
91
 
92
+ def _banner(msg: str) -> str:
93
+ """
94
+ Create a banner for the given message.
95
+ Example:
96
+ msg = "Hello, World!"
97
+ print -> "#################"
98
+ "# Hello, World! #"
99
+ "#################"
100
+ """
101
+ lines = msg.split("\n")
102
+ # Find the width of the widest line
103
+ max_width = max(len(line) for line in lines)
104
+ width = max_width + 4 # Add 4 for "# " and " #"
105
+
106
+ # Create the top border
107
+ banner = "\n" + "#" * width + "\n"
108
+
109
+ # Add each line with proper padding
110
+ for line in lines:
111
+ padding = max_width - len(line)
112
+ banner += f"# {line}{' ' * padding} #\n"
113
+
114
+ # Add the bottom border
115
+ banner += "#" * width + "\n"
116
+ return banner
117
+
118
+
92
119
  def compile(
93
120
  compiler_root: Path, build_mode: BuildMode, auto_clean: bool, no_platformio: bool
94
121
  ) -> int:
@@ -96,7 +123,7 @@ def compile(
96
123
  max_attempts = 1
97
124
  env = os.environ.copy()
98
125
  env["BUILD_MODE"] = build_mode.name
99
- print(f"Build mode: {build_mode.name}")
126
+ print(_banner(f"WASM is building in mode: {build_mode.name}"))
100
127
  cmd_list: list[str] = []
101
128
  if no_platformio:
102
129
  # execute build_archive.syh
@@ -113,6 +140,7 @@ def compile(
113
140
  cmd_list.append("-v")
114
141
 
115
142
  def _open_process(cmd_list: list[str] = cmd_list) -> subprocess.Popen:
143
+ print(_banner("Running command:\n " + subprocess.list2cmdline(cmd_list)))
116
144
  out = subprocess.Popen(
117
145
  cmd_list,
118
146
  cwd=compiler_root,
@@ -138,7 +166,7 @@ def compile(
138
166
  relative_output = _make_timestamps_relative("\n".join(output_lines))
139
167
  print(relative_output)
140
168
  if process.returncode == 0:
141
- print(f"Compilation successful on attempt {attempt}")
169
+ print(_banner(f"Compilation successful on attempt {attempt}"))
142
170
  return 0
143
171
  else:
144
172
  raise subprocess.CalledProcessError(process.returncode, ["pio", "run"])
@@ -199,7 +227,7 @@ def transform_to_cpp(src_dir: Path) -> None:
199
227
  def insert_headers(
200
228
  src_dir: Path, exclusion_folders: List[Path], file_extensions: List[str]
201
229
  ) -> None:
202
- print("Inserting headers in source files...")
230
+ print(_banner("Inserting headers in source files..."))
203
231
  for file in src_dir.rglob("*"):
204
232
  if (
205
233
  file.suffix in file_extensions
@@ -213,7 +241,7 @@ def process_ino_files(src_dir: Path) -> None:
213
241
  transform_to_cpp(src_dir)
214
242
  exclusion_folders: List[Path] = []
215
243
  insert_headers(src_dir, exclusion_folders, _FILE_EXTENSIONS)
216
- print("Transform to cpp and insert header operations completed.")
244
+ print(_banner("Transform to cpp and insert header operations completed."))
217
245
 
218
246
 
219
247
  def _make_timestamps_relative(stdout: str) -> str:
@@ -381,7 +409,7 @@ def process_compile(
381
409
  if rtn != 0:
382
410
  print("Compilation failed.")
383
411
  raise RuntimeError("Compilation failed.")
384
- print("Compilation successful.")
412
+ print(_banner("Compilation successful."))
385
413
 
386
414
 
387
415
  def cleanup(args: Args, js_src: Path) -> None:
@@ -422,9 +450,15 @@ def run(args: Args) -> int:
422
450
  print(f"Using mapped directory: {args.mapped_dir}")
423
451
 
424
452
  if args.profile:
425
- print("Enabling profiling for compilation.")
453
+ print(_banner("Enabling profiling for compilation."))
426
454
  # Profile linking
427
455
  os.environ["EMPROFILE"] = "2"
456
+ else:
457
+ print(
458
+ _banner(
459
+ "Build process profiling is disabled\nuse --profile to get metrics on how long the build process took."
460
+ )
461
+ )
428
462
 
429
463
  try:
430
464
 
@@ -477,6 +511,8 @@ def run(args: Args) -> int:
477
511
  return 1
478
512
 
479
513
  def _get_build_dir_platformio() -> Path:
514
+ # First assert there is only one build artifact directory.
515
+ # The name is dynamic: it's your sketch folder name.
480
516
  build_dirs = [d for d in PIO_BUILD_DIR.iterdir() if d.is_dir()]
481
517
  if len(build_dirs) != 1:
482
518
  raise RuntimeError(
@@ -493,16 +529,17 @@ def run(args: Args) -> int:
493
529
  else:
494
530
  build_dir = _get_build_dir_platformio()
495
531
 
496
- print("Copying output files...")
532
+ print(_banner("Copying output files..."))
497
533
  out_dir: Path = src_dir / _FASTLED_OUTPUT_DIR_NAME
498
534
  out_dir.mkdir(parents=True, exist_ok=True)
499
535
 
500
- for file in ["fastled.js", "fastled.wasm"]:
501
- _src = build_dir / file
502
- _dst = out_dir / file
503
- print(f"Copying {_src} to {_dst}")
504
- shutil.copy2(_src, _dst)
536
+ # Copy all fastled.* build artifacts
537
+ for file_path in build_dir.glob("fastled.*"):
538
+ _dst = out_dir / file_path.name
539
+ print(f"Copying {file_path} to {_dst}")
540
+ shutil.copy2(file_path, _dst)
505
541
 
542
+ # Copy static files.
506
543
  print(f"Copying {_INDEX_HTML_SRC} to output directory")
507
544
  shutil.copy2(_INDEX_HTML_SRC, out_dir / "index.html")
508
545
  print(f"Copying {_INDEX_CSS_SRC} to output directory")
@@ -510,23 +547,30 @@ def run(args: Args) -> int:
510
547
 
511
548
  # copy all js files in _FASTLED_COMPILER_DIR to output directory
512
549
  Path(out_dir / "modules").mkdir(parents=True, exist_ok=True)
513
- for _file in _FASTLED_MODULES_DIR.iterdir():
514
- if _file.suffix == ".js":
515
- print(f"Copying {_file} to output directory")
516
- shutil.copy2(_file, out_dir / "modules" / _file.name)
517
-
518
- fastled_js_mem = build_dir / "fastled.js.mem"
519
- fastled_wasm_map = build_dir / "fastled.wasm.map"
520
- fastled_js_symbols = build_dir / "fastled.js.symbols"
521
- if fastled_js_mem.exists():
522
- print(f"Copying {fastled_js_mem} to output directory")
523
- shutil.copy2(fastled_js_mem, out_dir / fastled_js_mem.name)
524
- if fastled_wasm_map.exists():
525
- print(f"Copying {fastled_wasm_map} to output directory")
526
- shutil.copy2(fastled_wasm_map, out_dir / fastled_wasm_map.name)
527
- if fastled_js_symbols.exists():
528
- print(f"Copying {fastled_js_symbols} to output directory")
529
- shutil.copy2(fastled_js_symbols, out_dir / fastled_js_symbols.name)
550
+
551
+ # Recursively copy all non-hidden files and directories
552
+ print(f"Copying files from {_FASTLED_MODULES_DIR} to {out_dir / 'modules'}")
553
+ shutil.copytree(
554
+ src=_FASTLED_MODULES_DIR,
555
+ dst=out_dir / "modules",
556
+ dirs_exist_ok=True,
557
+ ignore=shutil.ignore_patterns(".*"),
558
+ ) # Ignore hidden files
559
+
560
+ # Now long needed since now we do glob copy.
561
+ # fastled_js_mem = build_dir / "fastled.js.mem"
562
+ # fastled_wasm_map = build_dir / "fastled.wasm.map"
563
+ # fastled_js_symbols = build_dir / "fastled.js.symbols"
564
+ # if fastled_js_mem.exists():
565
+ # print(f"Copying {fastled_js_mem} to output directory")
566
+ # shutil.copy2(fastled_js_mem, out_dir / fastled_js_mem.name)
567
+ # if fastled_wasm_map.exists():
568
+ # print(f"Copying {fastled_wasm_map} to output directory")
569
+ # shutil.copy2(fastled_wasm_map, out_dir / fastled_wasm_map.name)
570
+ # if fastled_js_symbols.exists():
571
+ # print(f"Copying {fastled_js_symbols} to output directory")
572
+ # shutil.copy2(fastled_js_symbols, out_dir / fastled_js_symbols.name)
573
+
530
574
  print("Copying index.js to output directory")
531
575
  shutil.copy2(_INDEX_JS_SRC, out_dir / "index.js")
532
576
  optional_input_data_dir = src_dir / "data"
@@ -546,7 +590,7 @@ def run(args: Args) -> int:
546
590
  if _file.is_file(): # Only copy files, not directories
547
591
  filename: str = _file.name
548
592
  if filename.endswith(".embedded.json"):
549
- print("Embedding data file")
593
+ print(_banner("Embedding data file"))
550
594
  filename_no_embedded = filename.replace(
551
595
  ".embedded.json", ""
552
596
  )
@@ -577,13 +621,13 @@ def run(args: Args) -> int:
577
621
  )
578
622
 
579
623
  # Write manifest file even if empty
580
- print("Writing manifest files.json")
624
+ print(_banner("Writing manifest files.json"))
581
625
  manifest_json_str = json.dumps(manifest, indent=2, sort_keys=True)
582
626
  with open(out_dir / "files.json", "w") as f:
583
627
  f.write(manifest_json_str)
584
628
  cleanup(args, SKETCH_SRC)
585
629
 
586
- print("Compilation process completed successfully")
630
+ print(_banner("Compilation process completed successfully"))
587
631
  return 0
588
632
 
589
633
  except Exception as e:
@@ -13,7 +13,7 @@ from .types import BuildMode, CompileResult, CompileServerError
13
13
  # IMPORTANT! There's a bug in github which will REJECT any version update
14
14
  # that has any other change in the repo. Please bump the version as the
15
15
  # ONLY change in a commit, or else the pypi update and the release will fail.
16
- __version__ = "1.2.68"
16
+ __version__ = "1.2.74"
17
17
 
18
18
  DOCKER_FILE = (
19
19
  "https://raw.githubusercontent.com/zackees/fastled-wasm/refs/heads/main/Dockerfile"
@@ -43,6 +43,8 @@ def run_server(args: Args) -> int:
43
43
 
44
44
 
45
45
  def main() -> int:
46
+ from fastled import __version__
47
+
46
48
  args = parse_args()
47
49
  interactive: bool = args.interactive
48
50
  has_server = args.server
@@ -52,6 +54,9 @@ def main() -> int:
52
54
  # directory: Path | None = Path(args.directory).absolute() if args.directory else None
53
55
  directory: Path | None = Path(args.directory) if args.directory else None
54
56
 
57
+ # now it is safe to print out the version
58
+ print(f"FastLED version: {__version__}")
59
+
55
60
  if update:
56
61
  # Force auto_update to ensure update check happens
57
62
  compile_server = CompileServer(interactive=False, auto_updates=True)
@@ -13,6 +13,7 @@ from fastled.docker_manager import (
13
13
  Container,
14
14
  DockerManager,
15
15
  RunningContainer,
16
+ Volume,
16
17
  )
17
18
  from fastled.interactive_srcs import INTERACTIVE_SOURCES
18
19
  from fastled.settings import DEFAULT_CONTAINER_NAME, IMAGE_NAME, SERVER_PORT
@@ -209,27 +210,32 @@ class CompileServerImpl:
209
210
  else:
210
211
  server_command = ["python", "/js/run.py", "server"] + SERVER_OPTIONS
211
212
  ports = {80: port}
212
- volumes = None
213
+ volumes = []
213
214
  if self.fastled_src_dir:
214
215
  print(
215
216
  f"Mounting FastLED source directory {self.fastled_src_dir} into container /host/fastled/src"
216
217
  )
217
- volumes = {
218
- str(self.fastled_src_dir): {"bind": "/host/fastled/src", "mode": "ro"}
219
- }
218
+ volumes.append(
219
+ Volume(
220
+ host_path=str(self.fastled_src_dir),
221
+ container_path="/host/fastled/src",
222
+ mode="ro",
223
+ )
224
+ )
220
225
  if self.interactive:
221
226
  # add the mapped directory to the container
222
227
  print(f"Mounting {self.mapped_dir} into container /mapped")
223
- # volumes = {str(self.mapped_dir): {"bind": "/mapped", "mode": "rw"}}
224
- # add it
225
228
  assert self.mapped_dir is not None
226
229
  dir_name = self.mapped_dir.name
227
230
  if not volumes:
228
- volumes = {}
229
- volumes[str(self.mapped_dir)] = {
230
- "bind": f"/mapped/{dir_name}",
231
- "mode": "rw",
232
- }
231
+ volumes = []
232
+ volumes.append(
233
+ Volume(
234
+ host_path=str(self.mapped_dir),
235
+ container_path=f"/mapped/{dir_name}",
236
+ mode="rw",
237
+ )
238
+ )
233
239
  if self.fastled_src_dir is not None:
234
240
  # to allow for interactive compilation
235
241
  interactive_sources = list(INTERACTIVE_SOURCES)
@@ -237,11 +243,13 @@ class CompileServerImpl:
237
243
  src_path = Path(src).absolute()
238
244
  if src_path.exists():
239
245
  print(f"Mounting {src} into container")
240
- src_str = str(src_path)
241
- volumes[src_str] = {
242
- "bind": f"/js/fastled/{src}",
243
- "mode": "rw",
244
- }
246
+ volumes.append(
247
+ Volume(
248
+ host_path=str(src_path),
249
+ container_path=f"/js/fastled/{src}",
250
+ mode="rw",
251
+ )
252
+ )
245
253
  else:
246
254
  print(f"Could not find {src}")
247
255
 
@@ -277,6 +285,8 @@ class CompileServerImpl:
277
285
  return self.docker.is_container_running(self.container_name)
278
286
 
279
287
  def stop(self) -> None:
288
+ if self.docker.is_suspended:
289
+ return
280
290
  if self.running_container:
281
291
  self.running_container.detach()
282
292
  self.running_container = None
@@ -3,6 +3,7 @@ New abstraction for Docker management with improved Ctrl+C handling.
3
3
  """
4
4
 
5
5
  import _thread
6
+ import json
6
7
  import os
7
8
  import platform
8
9
  import subprocess
@@ -11,6 +12,7 @@ import threading
11
12
  import time
12
13
  import traceback
13
14
  import warnings
15
+ from dataclasses import dataclass
14
16
  from datetime import datetime, timezone
15
17
  from pathlib import Path
16
18
 
@@ -63,8 +65,69 @@ def get_lock(image_name: str) -> FileLock:
63
65
  return out
64
66
 
65
67
 
68
+ @dataclass
69
+ class Volume:
70
+ """
71
+ Represents a Docker volume mapping between host and container.
72
+
73
+ Attributes:
74
+ host_path: Path on the host system (e.g., "C:\\Users\\username\\project")
75
+ container_path: Path inside the container (e.g., "/app/data")
76
+ mode: Access mode, "rw" for read-write or "ro" for read-only
77
+ """
78
+
79
+ host_path: str
80
+ container_path: str
81
+ mode: str = "rw"
82
+
83
+ def to_dict(self) -> dict[str, dict[str, str]]:
84
+ """Convert the Volume object to the format expected by Docker API."""
85
+ return {self.host_path: {"bind": self.container_path, "mode": self.mode}}
86
+
87
+ @classmethod
88
+ def from_dict(cls, volume_dict: dict[str, dict[str, str]]) -> list["Volume"]:
89
+ """Create Volume objects from a Docker volume dictionary."""
90
+ volumes = []
91
+ for host_path, config in volume_dict.items():
92
+ volumes.append(
93
+ cls(
94
+ host_path=host_path,
95
+ container_path=config["bind"],
96
+ mode=config.get("mode", "rw"),
97
+ )
98
+ )
99
+ return volumes
100
+
101
+
102
+ class PrintFilter:
103
+ """Provides filtering for text output so that source files match up with local names."""
104
+
105
+ def __init__(self) -> None:
106
+ pass
107
+
108
+ def _remove_ino_cpp(self, text: str) -> str:
109
+ return text.replace(".ino.cpp", ".ino")
110
+
111
+ def _filter_all(self, text: str) -> str:
112
+ text = self._remove_ino_cpp(text)
113
+ return text
114
+
115
+ def print(self, text: str | bytes) -> None:
116
+ """Prints the text to the console."""
117
+ if isinstance(text, bytes):
118
+ text = text.decode("utf-8")
119
+ text = self._filter_all(text)
120
+ print(text, end="")
121
+
122
+
66
123
  class RunningContainer:
67
- def __init__(self, container, first_run=False):
124
+ def __init__(
125
+ self,
126
+ container: Container,
127
+ first_run: bool = False,
128
+ filter: PrintFilter | None = None,
129
+ ) -> None:
130
+ self.filter = filter or PrintFilter()
68
131
  self.container = container
69
132
  self.first_run = first_run
70
133
  self.running = True
@@ -81,7 +144,8 @@ class RunningContainer:
81
144
  for log in self.container.logs(
82
145
  follow=False, since=from_date, until=to_date, stream=True
83
146
  ):
84
- print(log.decode("utf-8"), end="")
147
+ # print(log.decode("utf-8"), end="")
148
+ self.filter.print(log)
85
149
  time.sleep(0.1)
86
150
  from_date = to_date
87
151
  to_date = _utc_now_no_tz()
@@ -104,10 +168,46 @@ class RunningContainer:
104
168
  self.detach()
105
169
 
106
170
 
171
+ def _hack_to_fix_mac(volumes: list[Volume] | None) -> list[Volume] | None:
172
+ """Fixes the volume mounts on MacOS by removing the mode."""
173
+ if volumes is None:
174
+ return None
175
+ if sys.platform != "darwin":
176
+ # Only macos needs hacking.
177
+ return volumes
178
+
179
+ volumes = volumes.copy()
180
+ # Work around a Docker bug on MacOS where the expected network socket to the
181
+ # the host is not mounted correctly. This was actually fixed in recent versions
182
+ # of docker client but there is a large chunk of Docker clients out there with
183
+ # this bug in it.
184
+ #
185
+ # This hack is done by mounting the socket directly to the container.
186
+ # This socket talks to the docker daemon on the host.
187
+ #
188
+ # Found here.
189
+ # https://github.com/docker/docker-py/issues/3069#issuecomment-1316778735
190
+ # if it exists already then return the input
191
+ for volume in volumes:
192
+ if volume.host_path == "/var/run/docker.sock":
193
+ return volumes
194
+ # ok it doesn't exist, so add it
195
+ volumes.append(
196
+ Volume(
197
+ host_path="/var/run/docker.sock",
198
+ container_path="/var/run/docker.sock",
199
+ mode="rw",
200
+ )
201
+ )
202
+ return volumes
203
+
204
+
107
205
  class DockerManager:
108
206
  def __init__(self) -> None:
109
207
  from docker.errors import DockerException
110
208
 
209
+ self.is_suspended: bool = False
210
+
111
211
  try:
112
212
  self._client: DockerClient | None = None
113
213
  self.first_run = False
@@ -314,8 +414,6 @@ class DockerManager:
314
414
 
315
415
  # Quick check for latest version
316
416
  with Spinner(f"Pulling newer version of {image_name}:{tag}..."):
317
- # This needs to be swapped out using the the command line interface AI!
318
- # _ = self.client.images.pull(image_name, tag=tag)
319
417
  cmd_list = ["docker", "pull", f"{image_name}:{tag}"]
320
418
  cmd_str = subprocess.list2cmdline(cmd_list)
321
419
  print(f"Running command: {cmd_str}")
@@ -355,8 +453,8 @@ class DockerManager:
355
453
  self,
356
454
  container: Container,
357
455
  command: str | None,
358
- volumes: dict | None,
359
- ports: dict | None,
456
+ volumes_dict: dict[str, dict[str, str]] | None,
457
+ ports: dict[int, int] | None,
360
458
  ) -> bool:
361
459
  """Compare if existing container has matching configuration"""
362
460
  try:
@@ -386,7 +484,7 @@ class DockerManager:
386
484
  return False
387
485
 
388
486
  # Check volumes if specified
389
- if volumes:
487
+ if volumes_dict:
390
488
  container_mounts = (
391
489
  {
392
490
  m["Source"]: {"bind": m["Destination"], "mode": m["Mode"]}
@@ -396,7 +494,7 @@ class DockerManager:
396
494
  else {}
397
495
  )
398
496
 
399
- for host_dir, mount in volumes.items():
497
+ for host_dir, mount in volumes_dict.items():
400
498
  if host_dir not in container_mounts:
401
499
  print(f"Volume {host_dir} not found in container mounts.")
402
500
  return False
@@ -446,7 +544,7 @@ class DockerManager:
446
544
  tag: str,
447
545
  container_name: str,
448
546
  command: str | None = None,
449
- volumes: dict[str, dict[str, str]] | None = None,
547
+ volumes: list[Volume] | None = None,
450
548
  ports: dict[int, int] | None = None,
451
549
  remove_previous: bool = False,
452
550
  ) -> Container:
@@ -455,11 +553,23 @@ class DockerManager:
455
553
  If it exists with different config, remove and recreate it.
456
554
 
457
555
  Args:
458
- volumes: Dict mapping host paths to dicts with 'bind' and 'mode' keys
459
- Example: {'/host/path': {'bind': '/container/path', 'mode': 'rw'}}
556
+ volumes: List of Volume objects for container volume mappings
460
557
  ports: Dict mapping host ports to container ports
461
558
  Example: {8080: 80} maps host port 8080 to container port 80
462
559
  """
560
+ volumes = _hack_to_fix_mac(volumes)
561
+ # Convert volumes to the format expected by Docker API
562
+ volumes_dict = None
563
+ if volumes is not None:
564
+ volumes_dict = {}
565
+ for volume in volumes:
566
+ volumes_dict.update(volume.to_dict())
567
+
568
+ # Serialize the volumes to a json string
569
+ if volumes_dict:
570
+ volumes_str = json.dumps(volumes_dict)
571
+ print(f"Volumes: {volumes_str}")
572
+ print("Done")
463
573
  image_name = f"{image_name}:{tag}"
464
574
  try:
465
575
  container: Container = self.client.containers.get(container_name)
@@ -469,7 +579,9 @@ class DockerManager:
469
579
  container.remove(force=True)
470
580
  raise NotFound("Container removed due to remove_previous")
471
581
  # Check if configuration matches
472
- elif not self._container_configs_match(container, command, volumes, ports):
582
+ elif not self._container_configs_match(
583
+ container, command, volumes_dict, ports
584
+ ):
473
585
  print(
474
586
  f"Container {container_name} exists but with different configuration. Removing and recreating..."
475
587
  )
@@ -517,7 +629,7 @@ class DockerManager:
517
629
  name=container_name,
518
630
  detach=True,
519
631
  tty=True,
520
- volumes=volumes,
632
+ volumes=volumes_dict,
521
633
  ports=ports, # type: ignore
522
634
  remove=True,
523
635
  )
@@ -529,9 +641,16 @@ class DockerManager:
529
641
  tag: str,
530
642
  container_name: str,
531
643
  command: str | None = None,
532
- volumes: dict[str, dict[str, str]] | None = None,
644
+ volumes: list[Volume] | None = None,
533
645
  ports: dict[int, int] | None = None,
534
646
  ) -> None:
647
+ # Convert volumes to the format expected by Docker API
648
+ volumes = _hack_to_fix_mac(volumes)
649
+ volumes_dict = None
650
+ if volumes is not None:
651
+ volumes_dict = {}
652
+ for volume in volumes:
653
+ volumes_dict.update(volume.to_dict())
535
654
  # Remove existing container
536
655
  try:
537
656
  container: Container = self.client.containers.get(container_name)
@@ -548,9 +667,13 @@ class DockerManager:
548
667
  "--name",
549
668
  container_name,
550
669
  ]
551
- if volumes:
552
- for host_dir, mount in volumes.items():
553
- docker_command.extend(["-v", f"{host_dir}:{mount['bind']}"])
670
+ if volumes_dict:
671
+ for host_dir, mount in volumes_dict.items():
672
+ docker_volume_arg = [
673
+ "-v",
674
+ f"{host_dir}:{mount['bind']}:{mount['mode']}",
675
+ ]
676
+ docker_command.extend(docker_volume_arg)
554
677
  if ports:
555
678
  for host_port, container_port in ports.items():
556
679
  docker_command.extend(["-p", f"{host_port}:{container_port}"])
@@ -591,6 +714,8 @@ class DockerManager:
591
714
  """
592
715
  Suspend (pause) the container.
593
716
  """
717
+ if self.is_suspended:
718
+ return
594
719
  if isinstance(container, str):
595
720
  container_name = container
596
721
  # container = self.get_container(container)