fastled 1.1.0__py2.py3-none-any.whl → 1.1.3__py2.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/app.py CHANGED
@@ -16,7 +16,7 @@ from dataclasses import dataclass
16
16
  from pathlib import Path
17
17
 
18
18
  from fastled.build_mode import BuildMode, get_build_mode
19
- from fastled.compile_server import CompileServer
19
+ from fastled.compile_server import CompileServer, looks_like_fastled_repo
20
20
  from fastled.docker_manager import DockerManager
21
21
  from fastled.filewatcher import FileChangedNotifier
22
22
  from fastled.open_browser import open_browser_thread
@@ -81,6 +81,12 @@ def parse_args() -> argparse.Namespace:
81
81
  nargs="+",
82
82
  help="Additional patterns to exclude from file watching (Not available with --web)",
83
83
  )
84
+ parser.add_argument(
85
+ "-i",
86
+ "--interactive",
87
+ action="store_true",
88
+ help="Run in interactive mode (Not available with --web)",
89
+ )
84
90
  parser.add_argument(
85
91
  "--profile",
86
92
  action="store_true",
@@ -97,6 +103,11 @@ def parse_args() -> argparse.Namespace:
97
103
  build_mode.add_argument(
98
104
  "--release", action="store_true", help="Build in release mode"
99
105
  )
106
+ build_mode.add_argument(
107
+ "--server",
108
+ action="store_true",
109
+ help="Run the server in the current directory, volume mapping fastled if we are in the repo",
110
+ )
100
111
 
101
112
  build_mode.add_argument(
102
113
  "--force-compile",
@@ -148,9 +159,7 @@ def run_web_compiler(
148
159
 
149
160
  # now check to see if the hash value is the same as the last hash value
150
161
  if last_hash_value is not None and last_hash_value == web_result.hash_value:
151
- print(
152
- "\nNo significant source code changes detected and data was the same, skipping recompilation."
153
- )
162
+ print("\nSkipping redeploy: No significant changes found.")
154
163
  print_results()
155
164
  return CompiledResult(
156
165
  success=True, fastled_js=str(output_dir), hash_value=web_result.hash_value
@@ -200,7 +209,24 @@ def _try_start_server_or_get_url(args: argparse.Namespace) -> str | CompileServe
200
209
  return DEFAULT_URL
201
210
 
202
211
 
212
+ def _lots_and_lots_of_files(directory: Path) -> bool:
213
+ count = 0
214
+ for root, dirs, files in os.walk(directory):
215
+ count += len(files)
216
+ if count > 100:
217
+ return True
218
+ return False
219
+
220
+
203
221
  def _looks_like_sketch_directory(directory: Path) -> bool:
222
+ if looks_like_fastled_repo(directory):
223
+ print("Directory looks like the FastLED repo")
224
+ return False
225
+
226
+ if _lots_and_lots_of_files(directory):
227
+ print("Too many files in the directory, bailing out")
228
+ return False
229
+
204
230
  # walk the path and if there are over 30 files, return False
205
231
  # at the root of the directory there should either be an ino file or a src directory
206
232
  # or some cpp files
@@ -217,11 +243,10 @@ def _looks_like_sketch_directory(directory: Path) -> bool:
217
243
  return False
218
244
 
219
245
 
220
- def main() -> int:
221
- args = parse_args()
246
+ def run_client(args: argparse.Namespace) -> int:
247
+ compile_server: CompileServer | None = None
222
248
  open_web_browser = not args.just_compile
223
249
  profile = args.profile
224
-
225
250
  if not args.force_compile and not _looks_like_sketch_directory(
226
251
  Path(args.directory)
227
252
  ):
@@ -237,16 +262,16 @@ def main() -> int:
237
262
  )
238
263
  args.web = True
239
264
 
240
- compile_server: CompileServer | None = None
241
265
  url: str
242
-
243
266
  try:
244
267
  try:
245
268
  url_or_server: str | CompileServer = _try_start_server_or_get_url(args)
246
269
  if isinstance(url_or_server, str):
270
+ print(f"Found URL: {url_or_server}")
247
271
  url = url_or_server
248
272
  else:
249
273
  compile_server = url_or_server
274
+ print(f"Server started at {compile_server.url()}")
250
275
  url = compile_server.url()
251
276
  except KeyboardInterrupt:
252
277
  print("\nExiting from first try...")
@@ -343,9 +368,49 @@ def main() -> int:
343
368
  browser_proc.kill()
344
369
 
345
370
 
371
+ def run_server(args: argparse.Namespace) -> int:
372
+ interactive = args.interactive
373
+ compile_server = CompileServer(
374
+ disable_auto_clean=args.no_auto_clean, interactive=interactive
375
+ )
376
+ print(f"Server started at {compile_server.url()}")
377
+ compile_server.start()
378
+ compile_server.wait_for_startup()
379
+ try:
380
+ while True:
381
+ if not compile_server.proceess_running():
382
+ print("Server process is not running. Exiting...")
383
+ return 1
384
+ time.sleep(1)
385
+ except KeyboardInterrupt:
386
+ print("\nExiting from server...")
387
+ return 1
388
+ finally:
389
+ compile_server.stop()
390
+ return 0
391
+
392
+
393
+ def main() -> int:
394
+ args = parse_args()
395
+ target_dir = Path(args.directory)
396
+ cwd_is_target_dir = target_dir == Path(os.getcwd())
397
+ force_server = cwd_is_target_dir and looks_like_fastled_repo(target_dir)
398
+ auto_server = (args.server or args.interactive or cwd_is_target_dir) and (
399
+ not args.web and not args.just_compile
400
+ )
401
+ if auto_server or force_server:
402
+ print("Running in server only mode.")
403
+ return run_server(args)
404
+ else:
405
+ print("Running in client/server mode.")
406
+ return run_client(args)
407
+
408
+
346
409
  if __name__ == "__main__":
347
410
  try:
348
411
  sys.argv.append("examples/wasm")
412
+ sys.argv.append("-w")
413
+ sys.argv.append("localhost")
349
414
  sys.exit(main())
350
415
  except KeyboardInterrupt:
351
416
  print("\nExiting from main...")
fastled/compile_server.py CHANGED
@@ -2,6 +2,7 @@ import socket
2
2
  import subprocess
3
3
  import threading
4
4
  import time
5
+ from pathlib import Path
5
6
  from typing import Optional
6
7
 
7
8
  import httpx
@@ -10,10 +11,10 @@ from fastled.docker_manager import DockerManager
10
11
 
11
12
  _DEFAULT_CONTAINER_NAME = "fastled-wasm-compiler"
12
13
 
13
- _DEFAULT_START_PORT = 9021
14
+ SERVER_PORT = 9021
14
15
 
15
16
 
16
- def _find_available_port(start_port: int = _DEFAULT_START_PORT) -> int:
17
+ def find_available_port(start_port: int = SERVER_PORT) -> int:
17
18
  """Find an available port starting from the given port."""
18
19
  port = start_port
19
20
  end_port = start_port + 1000
@@ -26,16 +27,38 @@ def _find_available_port(start_port: int = _DEFAULT_START_PORT) -> int:
26
27
  raise RuntimeError("No available ports found")
27
28
 
28
29
 
30
+ def looks_like_fastled_repo(directory: Path) -> bool:
31
+ libprops = directory / "library.properties"
32
+ if not libprops.exists():
33
+ return False
34
+ txt = libprops.read_text()
35
+ return "FastLED" in txt
36
+
37
+
29
38
  class CompileServer:
30
39
  def __init__(
31
- self, container_name=_DEFAULT_CONTAINER_NAME, disable_auto_clean: bool = False
40
+ self,
41
+ container_name=_DEFAULT_CONTAINER_NAME,
42
+ disable_auto_clean: bool = False,
43
+ interactive: bool = False,
32
44
  ) -> None:
45
+
46
+ cwd = Path(".").resolve()
47
+ fastled_src_dir: Path | None = None
48
+ if looks_like_fastled_repo(cwd):
49
+ print(
50
+ "Looks like a FastLED repo, using it as the source directory and mapping it into the server."
51
+ )
52
+ fastled_src_dir = cwd / "src"
53
+
33
54
  self.container_name = container_name
34
55
  self.disable_auto_clean = disable_auto_clean
35
56
  self.docker = DockerManager(container_name=container_name)
36
57
  self.running = False
37
58
  self.thread: Optional[threading.Thread] = None
38
59
  self.running_process: subprocess.Popen | None = None
60
+ self.fastled_src_dir: Path | None = fastled_src_dir
61
+ self.interactive = interactive
39
62
  self._port = self.start()
40
63
 
41
64
  def port(self) -> int:
@@ -54,7 +77,9 @@ class CompileServer:
54
77
  # use httpx to ping the server
55
78
  # if successful, return True
56
79
  try:
57
- response = httpx.get(f"http://localhost:{self._port}")
80
+ response = httpx.get(
81
+ f"http://localhost:{self._port}", follow_redirects=True
82
+ )
58
83
  if response.status_code < 400:
59
84
  return True
60
85
  except KeyboardInterrupt:
@@ -98,7 +123,7 @@ class CompileServer:
98
123
  except Exception as e:
99
124
  print(f"Warning: Failed to remove existing container: {e}")
100
125
 
101
- print("Ensuring Docker image exists")
126
+ print("Ensuring Docker image exists at latest version")
102
127
  if not self.docker.ensure_image_exists():
103
128
  print("Failed to ensure Docker image exists.")
104
129
  raise RuntimeError("Failed to ensure Docker image exists")
@@ -109,14 +134,35 @@ class CompileServer:
109
134
  # subprocess.run(["docker", "rmi", "fastled-wasm"], capture_output=True)
110
135
  # print("All clean")
111
136
 
112
- port = _find_available_port()
137
+ port = find_available_port()
138
+ print(f"Found an available port: {port}")
113
139
  # server_command = ["python", "/js/run.py", "server", "--allow-shutdown"]
114
- server_command = ["python", "/js/run.py", "server"]
140
+ if self.interactive:
141
+ server_command = ["/bin/bash"]
142
+ else:
143
+ server_command = ["python", "/js/run.py", "server"]
115
144
  if self.disable_auto_clean:
116
145
  server_command.append("--disable-auto-clean")
117
146
  print(f"Started Docker container with command: {server_command}")
118
147
  ports = {port: 80}
119
- self.running_process = self.docker.run_container(server_command, ports=ports)
148
+ volumes = None
149
+ if self.fastled_src_dir:
150
+ print(
151
+ f"Mounting FastLED source directory {self.fastled_src_dir} into container /js/fastled/src"
152
+ )
153
+ volumes = {
154
+ str(self.fastled_src_dir): {"bind": "/js/fastled/src", "mode": "rw"}
155
+ }
156
+ # no auto-update because the source directory is mapped in.
157
+ server_command.append("--no-auto-update") # stop git repo updates.
158
+ if not self.interactive:
159
+ server_command.append(
160
+ "--no-sketch-cache"
161
+ ) # Remove sketch cache which assumes src is static.
162
+ self.running_process = self.docker.run_container(
163
+ server_command, ports=ports, volumes=volumes
164
+ )
165
+ print("Compile server starting")
120
166
  time.sleep(3)
121
167
  if self.running_process.poll() is not None:
122
168
  print("Server failed to start")
@@ -124,38 +170,47 @@ class CompileServer:
124
170
  raise RuntimeError("Server failed to start")
125
171
  self.thread = threading.Thread(target=self._server_loop, daemon=True)
126
172
  self.thread.start()
127
- print("Compile server started")
173
+
128
174
  return port
129
175
 
176
+ def proceess_running(self) -> bool:
177
+ if self.running_process is None:
178
+ return False
179
+ return self.running_process.poll() is None
180
+
130
181
  def stop(self) -> None:
131
182
  print(f"Stopping server on port {self._port}")
132
183
  # attempt to send a shutdown signal to the server
133
- # httpx.get(f"http://localhost:{self._port}/shutdown", timeout=2)
134
- if self.running_process:
135
- try:
136
- # Stop the Docker container
137
- cp: subprocess.CompletedProcess
138
- cp = subprocess.run(
139
- ["docker", "stop", self.container_name],
140
- capture_output=True,
141
- text=True,
142
- check=True,
143
- )
144
- if cp.returncode != 0:
145
- print(f"Failed to stop Docker container: {cp.stderr}")
146
-
147
- cp = subprocess.run(
148
- ["docker", "rm", self.container_name],
149
- capture_output=True,
150
- text=True,
151
- check=True,
152
- )
153
- if cp.returncode != 0:
154
- print(f"Failed to remove Docker container: {cp.stderr}")
184
+ try:
185
+ httpx.get(f"http://localhost:{self._port}/shutdown", timeout=2)
186
+ # except Exception:
187
+ except Exception as e:
188
+ print(f"Failed to send shutdown signal: {e}")
189
+ pass
190
+ try:
191
+ # Stop the Docker container
192
+ cp: subprocess.CompletedProcess
193
+ cp = subprocess.run(
194
+ ["docker", "stop", self.container_name],
195
+ capture_output=True,
196
+ text=True,
197
+ check=True,
198
+ )
199
+ if cp.returncode != 0:
200
+ print(f"Failed to stop Docker container: {cp.stderr}")
201
+
202
+ cp = subprocess.run(
203
+ ["docker", "rm", self.container_name],
204
+ capture_output=True,
205
+ text=True,
206
+ check=True,
207
+ )
208
+ if cp.returncode != 0:
209
+ print(f"Failed to remove Docker container: {cp.stderr}")
155
210
 
156
- # Close the stdout pipe
157
- if self.running_process.stdout:
158
- self.running_process.stdout.close()
211
+ # Close the stdout pipe
212
+ if self.running_process and self.running_process.stdout:
213
+ self.running_process.stdout.close()
159
214
 
160
215
  # Wait for the process to fully terminate with a timeout
161
216
  self.running_process.wait(timeout=10)
@@ -167,17 +222,19 @@ class CompileServer:
167
222
  f"Server stopped with return code {self.running_process.returncode}"
168
223
  )
169
224
 
170
- except subprocess.TimeoutExpired:
171
- # Force kill if it doesn't stop gracefully
225
+ except subprocess.TimeoutExpired:
226
+ # Force kill if it doesn't stop gracefully
227
+ if self.running_process:
172
228
  self.running_process.kill()
173
229
  self.running_process.wait()
174
- except KeyboardInterrupt:
230
+ except KeyboardInterrupt:
231
+ if self.running_process:
175
232
  self.running_process.kill()
176
233
  self.running_process.wait()
177
- except Exception as e:
178
- print(f"Error stopping Docker container: {e}")
179
- finally:
180
- self.running_process = None
234
+ except Exception as e:
235
+ print(f"Error stopping Docker container: {e}")
236
+ finally:
237
+ self.running_process = None
181
238
  # Signal the server thread to stop
182
239
  self.running = False
183
240
  if self.thread:
fastled/docker_manager.py CHANGED
@@ -210,15 +210,15 @@ class DockerManager:
210
210
  def run_container(
211
211
  self,
212
212
  cmd: list[str],
213
- volumes: dict[str, str] | None = None,
213
+ volumes: dict[str, dict[str, str]] | None = None,
214
214
  ports: dict[int, int] | None = None,
215
215
  ) -> subprocess.Popen:
216
216
  """Run the Docker container with the specified volume.
217
217
 
218
218
  Args:
219
- volume_path: Path to the volume to mount
220
- base_name: Base name for the mounted volume
221
- build_mode: Build mode (DEBUG, QUICK, or RELEASE)
219
+ cmd: Command to run in the container
220
+ volumes: Dict mapping host paths to dicts with 'bind' and 'mode' keys
221
+ ports: Dict mapping host ports to container ports
222
222
  """
223
223
  volumes = volumes or {}
224
224
  ports = ports or {}
@@ -237,8 +237,10 @@ class DockerManager:
237
237
  for host_port, container_port in ports.items():
238
238
  docker_command.extend(["-p", f"{host_port}:{container_port}"])
239
239
  if volumes:
240
- for host_path, container_path in volumes.items():
241
- docker_command.extend(["-v", f"{host_path}:{container_path}"])
240
+ for host_path, mount_spec in volumes.items():
241
+ docker_command.extend(
242
+ ["-v", f"{host_path}:{mount_spec['bind']}:{mount_spec['mode']}"]
243
+ )
242
244
 
243
245
  docker_command.extend(
244
246
  [
fastled/web_compile.py CHANGED
@@ -1,173 +1,220 @@
1
- import shutil
2
- import tempfile
3
- from dataclasses import dataclass
4
- from pathlib import Path
5
-
6
- import httpx
7
-
8
- from fastled.build_mode import BuildMode
9
-
10
- DEFAULT_HOST = "https://fastled.onrender.com"
11
- ENDPOINT_COMPILED_WASM = "compile/wasm"
12
- _TIMEOUT = 60 * 4 # 2 mins timeout
13
- _AUTH_TOKEN = "oBOT5jbsO4ztgrpNsQwlmFLIKB"
14
-
15
-
16
- @dataclass
17
- class WebCompileResult:
18
- success: bool
19
- stdout: str
20
- hash_value: str | None
21
- zip_bytes: bytes
22
-
23
- def __bool__(self) -> bool:
24
- return self.success
25
-
26
-
27
- def _sanitize_host(host: str) -> str:
28
- if host.startswith("http"):
29
- return host
30
- is_local_host = "localhost" in host or "127.0.0.1" in host or "0.0.0.0" in host
31
- use_https = not is_local_host
32
- if use_https:
33
- return host if host.startswith("https://") else f"https://{host}"
34
- return host if host.startswith("http://") else f"http://{host}"
35
-
36
-
37
- _CONNECTION_ERROR_MAP: dict[str, bool] = {}
38
-
39
-
40
- def web_compile(
41
- directory: Path,
42
- host: str | None = None,
43
- auth_token: str | None = None,
44
- build_mode: BuildMode | None = None,
45
- profile: bool = False,
46
- ) -> WebCompileResult:
47
- host = _sanitize_host(host or DEFAULT_HOST)
48
- auth_token = auth_token or _AUTH_TOKEN
49
- # zip up the files
50
- print("Zipping files...")
51
-
52
- # Create a temporary zip file
53
- with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_zip:
54
- # Create temporary directory for organizing files
55
- with tempfile.TemporaryDirectory() as temp_dir:
56
- # Create wasm subdirectory
57
- wasm_dir = Path(temp_dir) / "wasm"
58
-
59
- # Copy all files from source to wasm subdirectory, excluding fastled_js
60
- def ignore_fastled_js(dir: str, files: list[str]) -> list[str]:
61
- if "fastled_js" in dir:
62
- return files
63
- if dir.startswith("."):
64
- return files
65
- return []
66
-
67
- shutil.copytree(directory, wasm_dir, ignore=ignore_fastled_js)
68
- # Create zip archive from the temp directory
69
- shutil.make_archive(tmp_zip.name[:-4], "zip", temp_dir)
70
-
71
- print(f"Web compiling on {host}...")
72
-
73
- try:
74
- with open(tmp_zip.name, "rb") as zip_file:
75
- files = {"file": ("wasm.zip", zip_file, "application/x-zip-compressed")}
76
-
77
- tested = host in _CONNECTION_ERROR_MAP
78
- if not tested:
79
- test_url = f"{host}/healthz"
80
- print(f"Testing connection to {test_url}")
81
- timeout = 10
82
- with httpx.Client(
83
- transport=httpx.HTTPTransport(local_address="0.0.0.0"),
84
- timeout=timeout,
85
- ) as test_client:
86
- test_response = test_client.get(test_url, timeout=timeout)
87
- if test_response.status_code != 200:
88
- print(f"Connection to {test_url} failed")
89
- _CONNECTION_ERROR_MAP[host] = True
90
- return WebCompileResult(
91
- success=False,
92
- stdout="Connection failed",
93
- hash_value=None,
94
- zip_bytes=b"",
95
- )
96
- _CONNECTION_ERROR_MAP[host] = False
97
-
98
- ok = not _CONNECTION_ERROR_MAP[host]
99
- if not ok:
100
- return WebCompileResult(
101
- success=False,
102
- stdout="Connection failed",
103
- hash_value=None,
104
- zip_bytes=b"",
105
- )
106
- print(f"Connection to {host} successful")
107
- with httpx.Client(
108
- transport=httpx.HTTPTransport(local_address="0.0.0.0"), # forces IPv4
109
- timeout=_TIMEOUT,
110
- ) as client:
111
- url = f"{host}/{ENDPOINT_COMPILED_WASM}"
112
- headers = {
113
- "accept": "application/json",
114
- "authorization": auth_token,
115
- "build": (
116
- build_mode.value.lower()
117
- if build_mode
118
- else BuildMode.QUICK.value.lower()
119
- ),
120
- "profile": "true" if profile else "false",
121
- }
122
- print(f"Compiling on {url}")
123
- response = client.post(
124
- url,
125
- files=files,
126
- headers=headers,
127
- timeout=_TIMEOUT,
128
- )
129
-
130
- if response.status_code != 200:
131
- json_response = response.json()
132
- detail = json_response.get("detail", "Could not compile")
133
- return WebCompileResult(
134
- success=False, stdout=detail, hash_value=None, zip_bytes=b""
135
- )
136
-
137
- print(f"Response status code: {response}")
138
- # Create a temporary directory to extract the zip
139
- with tempfile.TemporaryDirectory() as extract_dir:
140
- extract_path = Path(extract_dir)
141
-
142
- # Write the response content to a temporary zip file
143
- temp_zip = extract_path / "response.zip"
144
- temp_zip.write_bytes(response.content)
145
-
146
- # Extract the zip
147
- shutil.unpack_archive(temp_zip, extract_path, "zip")
148
-
149
- # Read stdout from out.txt if it exists
150
- stdout_file = extract_path / "out.txt"
151
- hash_file = extract_path / "hash.txt"
152
- stdout = stdout_file.read_text() if stdout_file.exists() else ""
153
- hash_value = hash_file.read_text() if hash_file.exists() else None
154
-
155
- return WebCompileResult(
156
- success=True,
157
- stdout=stdout,
158
- hash_value=hash_value,
159
- zip_bytes=response.content,
160
- )
161
- except KeyboardInterrupt:
162
- print("Keyboard interrupt")
163
- raise
164
- except httpx.HTTPError as e:
165
- print(f"Error: {e}")
166
- return WebCompileResult(
167
- success=False, stdout=str(e), hash_value=None, zip_bytes=b""
168
- )
169
- finally:
170
- try:
171
- Path(tmp_zip.name).unlink()
172
- except PermissionError:
173
- print("Warning: Could not delete temporary zip file")
1
+ import shutil
2
+ import tempfile
3
+ from concurrent.futures import ThreadPoolExecutor, as_completed
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+ import httpx
8
+
9
+ from fastled.build_mode import BuildMode
10
+ from fastled.compile_server import SERVER_PORT
11
+
12
+ DEFAULT_HOST = "https://fastled.onrender.com"
13
+ ENDPOINT_COMPILED_WASM = "compile/wasm"
14
+ _TIMEOUT = 60 * 4 # 2 mins timeout
15
+ _AUTH_TOKEN = "oBOT5jbsO4ztgrpNsQwlmFLIKB"
16
+
17
+
18
+ @dataclass
19
+ class TestConnectionResult:
20
+ host: str
21
+ success: bool
22
+ ipv4: bool
23
+
24
+
25
+ @dataclass
26
+ class WebCompileResult:
27
+ success: bool
28
+ stdout: str
29
+ hash_value: str | None
30
+ zip_bytes: bytes
31
+
32
+ def __bool__(self) -> bool:
33
+ return self.success
34
+
35
+
36
+ def _sanitize_host(host: str) -> str:
37
+ if host.startswith("http"):
38
+ return host
39
+ is_local_host = "localhost" in host or "127.0.0.1" in host or "0.0.0.0" in host
40
+ use_https = not is_local_host
41
+ if use_https:
42
+ return host if host.startswith("https://") else f"https://{host}"
43
+ return host if host.startswith("http://") else f"http://{host}"
44
+
45
+
46
+ _CONNECTION_ERROR_MAP: dict[str, TestConnectionResult] = {}
47
+
48
+
49
+ def _test_connection(host: str, use_ipv4: bool) -> TestConnectionResult:
50
+ key = f"{host}-{use_ipv4}"
51
+ maybe_result: TestConnectionResult | None = _CONNECTION_ERROR_MAP.get(key)
52
+ if maybe_result is not None:
53
+ return maybe_result
54
+ transport = httpx.HTTPTransport(local_address="0.0.0.0") if use_ipv4 else None
55
+ try:
56
+ with httpx.Client(
57
+ timeout=_TIMEOUT,
58
+ transport=transport,
59
+ ) as test_client:
60
+ test_response = test_client.get(
61
+ f"{host}/healthz", timeout=_TIMEOUT, follow_redirects=True
62
+ )
63
+ result = TestConnectionResult(
64
+ host, test_response.status_code == 200, use_ipv4
65
+ )
66
+ _CONNECTION_ERROR_MAP[key] = result
67
+ except Exception:
68
+ result = TestConnectionResult(host, False, use_ipv4)
69
+ _CONNECTION_ERROR_MAP[key] = result
70
+ return result
71
+
72
+
73
+ def web_compile(
74
+ directory: Path,
75
+ host: str | None = None,
76
+ auth_token: str | None = None,
77
+ build_mode: BuildMode | None = None,
78
+ profile: bool = False,
79
+ ) -> WebCompileResult:
80
+ host = _sanitize_host(host or DEFAULT_HOST)
81
+ print("Compiling on", host)
82
+ auth_token = auth_token or _AUTH_TOKEN
83
+ # zip up the files
84
+ print("Zipping files...")
85
+
86
+ # Create a temporary zip file
87
+ with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_zip:
88
+ # Create temporary directory for organizing files
89
+ with tempfile.TemporaryDirectory() as temp_dir:
90
+ # Create wasm subdirectory
91
+ wasm_dir = Path(temp_dir) / "wasm"
92
+
93
+ # Copy all files from source to wasm subdirectory, excluding fastled_js
94
+ def ignore_fastled_js(dir: str, files: list[str]) -> list[str]:
95
+ if "fastled_js" in dir:
96
+ return files
97
+ if dir.startswith("."):
98
+ return files
99
+ return []
100
+
101
+ shutil.copytree(directory, wasm_dir, ignore=ignore_fastled_js)
102
+ # Create zip archive from the temp directory
103
+ shutil.make_archive(tmp_zip.name[:-4], "zip", temp_dir)
104
+
105
+ print(f"Web compiling on {host}...")
106
+
107
+ try:
108
+ with open(tmp_zip.name, "rb") as zip_file:
109
+ files = {"file": ("wasm.zip", zip_file, "application/x-zip-compressed")}
110
+ urls = [host]
111
+ domain = host.split("://")[-1]
112
+ if ":" not in domain:
113
+ urls.append(f"{host}:{SERVER_PORT}")
114
+ test_connection_result: TestConnectionResult | None = None
115
+
116
+ with ThreadPoolExecutor(max_workers=len(urls)) as executor:
117
+ futures: list = []
118
+ ip_versions = [True, False] if "localhost" not in host else [True]
119
+ for ipv4 in ip_versions:
120
+ for url in urls:
121
+ f = executor.submit(_test_connection, url, ipv4)
122
+ futures.append(f)
123
+
124
+ succeeded = False
125
+ for future in as_completed(futures):
126
+ result: TestConnectionResult = future.result()
127
+
128
+ if result.success:
129
+ print(f"Connection successful to {result.host}")
130
+ succeeded = True
131
+ # host = test_url
132
+ test_connection_result = result
133
+ break
134
+ else:
135
+ print(f"Ignoring {result.host} due to connection failure")
136
+
137
+ if not succeeded:
138
+ print("Connection failed to all endpoints")
139
+ return WebCompileResult(
140
+ success=False,
141
+ stdout="Connection failed",
142
+ hash_value=None,
143
+ zip_bytes=b"",
144
+ )
145
+ assert test_connection_result is not None
146
+ ipv4_stmt = "IPv4" if test_connection_result.ipv4 else "IPv6"
147
+ transport = (
148
+ httpx.HTTPTransport(local_address="0.0.0.0")
149
+ if test_connection_result.ipv4
150
+ else None
151
+ )
152
+ with httpx.Client(
153
+ transport=transport,
154
+ timeout=_TIMEOUT,
155
+ ) as client:
156
+ headers = {
157
+ "accept": "application/json",
158
+ "authorization": auth_token,
159
+ "build": (
160
+ build_mode.value.lower()
161
+ if build_mode
162
+ else BuildMode.QUICK.value.lower()
163
+ ),
164
+ "profile": "true" if profile else "false",
165
+ }
166
+
167
+ url = f"{test_connection_result.host}/{ENDPOINT_COMPILED_WASM}"
168
+ print(f"Compiling on {url} via {ipv4_stmt}")
169
+ response = client.post(
170
+ url,
171
+ follow_redirects=True,
172
+ files=files,
173
+ headers=headers,
174
+ timeout=_TIMEOUT,
175
+ )
176
+
177
+ if response.status_code != 200:
178
+ json_response = response.json()
179
+ detail = json_response.get("detail", "Could not compile")
180
+ return WebCompileResult(
181
+ success=False, stdout=detail, hash_value=None, zip_bytes=b""
182
+ )
183
+
184
+ print(f"Response status code: {response}")
185
+ # Create a temporary directory to extract the zip
186
+ with tempfile.TemporaryDirectory() as extract_dir:
187
+ extract_path = Path(extract_dir)
188
+
189
+ # Write the response content to a temporary zip file
190
+ temp_zip = extract_path / "response.zip"
191
+ temp_zip.write_bytes(response.content)
192
+
193
+ # Extract the zip
194
+ shutil.unpack_archive(temp_zip, extract_path, "zip")
195
+
196
+ # Read stdout from out.txt if it exists
197
+ stdout_file = extract_path / "out.txt"
198
+ hash_file = extract_path / "hash.txt"
199
+ stdout = stdout_file.read_text() if stdout_file.exists() else ""
200
+ hash_value = hash_file.read_text() if hash_file.exists() else None
201
+
202
+ return WebCompileResult(
203
+ success=True,
204
+ stdout=stdout,
205
+ hash_value=hash_value,
206
+ zip_bytes=response.content,
207
+ )
208
+ except KeyboardInterrupt:
209
+ print("Keyboard interrupt")
210
+ raise
211
+ except httpx.HTTPError as e:
212
+ print(f"Error: {e}")
213
+ return WebCompileResult(
214
+ success=False, stdout=str(e), hash_value=None, zip_bytes=b""
215
+ )
216
+ finally:
217
+ try:
218
+ Path(tmp_zip.name).unlink()
219
+ except PermissionError:
220
+ print("Warning: Could not delete temporary zip file")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastled
3
- Version: 1.1.0
3
+ Version: 1.1.3
4
4
  Summary: FastLED Wasm Compiler
5
5
  Home-page: https://github.com/zackees/fastled-wasm
6
6
  Maintainer: Zachary Vorhies
@@ -28,7 +28,6 @@ Compiles an Arduino/Platformio sketch into a wasm binary that can be run directl
28
28
  [![Win_Tests](https://github.com/zackees/fastled-wasm/actions/workflows/test_win.yml/badge.svg)](https://github.com/zackees/fastled-wasm/actions/workflows/test_win.yml)
29
29
 
30
30
 
31
-
32
31
  # About
33
32
 
34
33
  This python app will compile your FastLED style sketches into html/js/wasm output that runs directly in the browser.
@@ -92,6 +91,9 @@ provide shims for most of the common api points.
92
91
 
93
92
  # Revisions
94
93
 
94
+ * 1.1.3 - Live editing of *.h and *.cpp files is now possible. Sketch cache will be disabled in this mode.
95
+ * 1.1.2 - `--server` will now volume map fastled src directory if it detects this. This was also implemented on the docker side.
96
+ * 1.1.1 - `--interactive` is now supported to debug the container. Volume maps and better compatibilty with ipv4/v6 by concurrent connection finding.
95
97
  * 1.1.0 - Use `fastled` as the command for the wasm compiler.
96
98
  * 1.0.17 - Pulls updates when necessary. Removed dependency on keyring.
97
99
  * 1.0.16 - `fastled-wasm` package name has been changed to `fled`
@@ -1,18 +1,18 @@
1
1
  fastled/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- fastled/app.py,sha256=N4a-XXteQMTmODxRmEuicUuXKyfP9OS6rGxT36aVAWs,11380
2
+ fastled/app.py,sha256=duSkE31cggy363rFZRHjKIzc0ijCA_TmmPXOLAj-jEM,13560
3
3
  fastled/build_mode.py,sha256=joMwsV4K1y_LijT4gEAcjx69RZBoe_KmFmHZdPYbL_4,631
4
4
  fastled/check_cpp_syntax.py,sha256=YxRJm7cFPv4bdhL1v_KOkBz8RL86ihayoJYvclr69ms,1024
5
5
  fastled/cli.py,sha256=CNR_pQR0sNVPNuv8e_nmm-0PI8sU-eUBUgnWgWkzW9c,237
6
- fastled/compile_server.py,sha256=_tpHpOgxcizgb9Lis8RVTDsLKLtCaaH3upVH65z0XhA,7994
7
- fastled/docker_manager.py,sha256=eEc12bmQEER7xf-7ehfYJgQua9cDB1IlLVuuw_9eflI,8934
6
+ fastled/compile_server.py,sha256=QqxTWYmC_Fej-hTNRjfa8qjyGusLY6L858zwTciC9AM,9903
7
+ fastled/docker_manager.py,sha256=WcOKa3EpIPAjICPfTL87CUYuAmX9KYT6L_Hcqbj95eE,9028
8
8
  fastled/filewatcher.py,sha256=SHKx9Dnt4EJiT-iPYakdPZBRIL1gsJGN9tY8FJW2myU,5079
9
9
  fastled/open_browser.py,sha256=-VhpGmydwLCcXmrDD2esMEdJPZYcoX2Mt73eb88Nna0,1392
10
10
  fastled/paths.py,sha256=VsPmgu0lNSCFOoEC0BsTYzDygXqy15AHUfN-tTuzDZA,99
11
- fastled/web_compile.py,sha256=xHy4ndEy5Xr5-WCINFrgvPgfk7XPm5_MhveiOElJKb0,6372
11
+ fastled/web_compile.py,sha256=rCMClX8yUtZlAzrgFB-r5Pdt7l9TJWhHXe-u99Hmdvw,8257
12
12
  fastled/assets/example.txt,sha256=lTBovRjiz0_TgtAtbA1C5hNi2ffbqnNPqkKg6UiKCT8,54
13
- fastled-1.1.0.dist-info/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
14
- fastled-1.1.0.dist-info/METADATA,sha256=C5bl7UjYlsgO2a-Xf6qi9-Dwcg6ctrYWlhZMB86GSSs,5294
15
- fastled-1.1.0.dist-info/WHEEL,sha256=0VNUDWQJzfRahYI3neAhz2UVbRCtztpN5dPHAGvmGXc,109
16
- fastled-1.1.0.dist-info/entry_points.txt,sha256=RCwmzCSOS4-C2i9EziANq7Z2Zb4KFnEMR1FQC0bBwAw,101
17
- fastled-1.1.0.dist-info/top_level.txt,sha256=xfG6Z_ol9V5YmBROkZq2QTRwjbS2ouCUxaTJsOwfkOo,14
18
- fastled-1.1.0.dist-info/RECORD,,
13
+ fastled-1.1.3.dist-info/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
14
+ fastled-1.1.3.dist-info/METADATA,sha256=C2OkZwJd57L96Gauxz_CZKMmk7bmNlv_VhvghbFdMD8,5687
15
+ fastled-1.1.3.dist-info/WHEEL,sha256=0VNUDWQJzfRahYI3neAhz2UVbRCtztpN5dPHAGvmGXc,109
16
+ fastled-1.1.3.dist-info/entry_points.txt,sha256=RCwmzCSOS4-C2i9EziANq7Z2Zb4KFnEMR1FQC0bBwAw,101
17
+ fastled-1.1.3.dist-info/top_level.txt,sha256=xfG6Z_ol9V5YmBROkZq2QTRwjbS2ouCUxaTJsOwfkOo,14
18
+ fastled-1.1.3.dist-info/RECORD,,