fastled 1.2.5__tar.gz → 1.2.7__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.
- {fastled-1.2.5 → fastled-1.2.7}/PKG-INFO +11 -1
- {fastled-1.2.5 → fastled-1.2.7}/README.md +10 -0
- fastled-1.2.7/src/fastled/__init__.py +325 -0
- {fastled-1.2.5 → fastled-1.2.7}/src/fastled/app.py +26 -0
- {fastled-1.2.5 → fastled-1.2.7}/src/fastled/docker_manager.py +81 -6
- {fastled-1.2.5 → fastled-1.2.7}/src/fastled/keyboard.py +8 -12
- {fastled-1.2.5 → fastled-1.2.7}/src/fastled/live_client.py +17 -0
- {fastled-1.2.5 → fastled-1.2.7}/src/fastled/open_browser.py +2 -1
- {fastled-1.2.5 → fastled-1.2.7}/src/fastled/parse_args.py +6 -0
- {fastled-1.2.5 → fastled-1.2.7}/src/fastled/site/build.py +46 -14
- {fastled-1.2.5 → fastled-1.2.7}/src/fastled.egg-info/PKG-INFO +11 -1
- {fastled-1.2.5 → fastled-1.2.7}/src/fastled.egg-info/SOURCES.txt +1 -0
- fastled-1.2.7/tests/test_build.py +57 -0
- {fastled-1.2.5 → fastled-1.2.7}/tests/test_ino/bad/bad.ino +9 -9
- {fastled-1.2.5 → fastled-1.2.7}/tests/test_ino/embedded/wasm.ino +9 -9
- {fastled-1.2.5 → fastled-1.2.7}/tests/test_ino/wasm/wasm.ino +9 -9
- fastled-1.2.5/src/fastled/__init__.py +0 -180
- {fastled-1.2.5 → fastled-1.2.7}/.aiderignore +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/.github/workflows/build_multi_docker_image.yml +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/.github/workflows/build_webpage.yml +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/.github/workflows/lint.yml +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/.github/workflows/publish_release.yml +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/.github/workflows/test_build_exe.yml +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/.github/workflows/test_macos.yml +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/.github/workflows/test_ubuntu.yml +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/.github/workflows/test_win.yml +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/.gitignore +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/.pylintrc +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/.vscode/launch.json +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/.vscode/settings.json +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/.vscode/tasks.json +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/LICENSE +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/MANIFEST.in +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/RELEASE.md +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/TODO.md +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/build_exe.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/build_site.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/clean +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/install +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/lint +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/pyproject.toml +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/requirements.testing.txt +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/setup.cfg +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/setup.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/src/fastled/assets/example.txt +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/src/fastled/cli.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/src/fastled/client_server.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/src/fastled/compile_server.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/src/fastled/compile_server_impl.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/src/fastled/filewatcher.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/src/fastled/paths.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/src/fastled/project_init.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/src/fastled/select_sketch_directory.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/src/fastled/settings.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/src/fastled/sketch.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/src/fastled/spinner.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/src/fastled/string_diff.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/src/fastled/test/can_run_local_docker_tests.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/src/fastled/test/examples.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/src/fastled/types.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/src/fastled/util.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/src/fastled/web_compile.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/src/fastled.egg-info/dependency_links.txt +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/src/fastled.egg-info/entry_points.txt +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/src/fastled.egg-info/requires.txt +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/src/fastled.egg-info/top_level.txt +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/test +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/tests/test_api.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/tests/test_bad_ino.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/tests/test_build_examples.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/tests/test_cli.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/tests/test_compile_server.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/tests/test_docker_linux_on_windows.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/tests/test_embedded_data.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/tests/test_examples.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/tests/test_filechanger.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/tests/test_ino/embedded/data/bigdata.dat +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/tests/test_project_init.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/tests/test_server_and_client_seperatly.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/tests/test_webcompile.py +0 -0
- {fastled-1.2.5 → fastled-1.2.7}/upload_package.sh +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: fastled
|
3
|
-
Version: 1.2.
|
3
|
+
Version: 1.2.7
|
4
4
|
Summary: FastLED Wasm Compiler
|
5
5
|
Home-page: https://github.com/zackees/fastled-wasm
|
6
6
|
Maintainer: Zachary Vorhies
|
@@ -186,6 +186,14 @@ with Api.server() as server:
|
|
186
186
|
client.stop()
|
187
187
|
```
|
188
188
|
|
189
|
+
**Build Docker Image from a local copy of the FastLED repo**
|
190
|
+
```python
|
191
|
+
from fastapi import Docker, Api
|
192
|
+
container_name: str = Docker.build_from_fastled_repo()
|
193
|
+
with Api.server(container_name=container_name) as server:
|
194
|
+
...
|
195
|
+
```
|
196
|
+
|
189
197
|
# Features
|
190
198
|
|
191
199
|
## Hot reload by default
|
@@ -266,6 +274,8 @@ A: A big chunk of space is being used by unnecessary javascript `emscripten` bun
|
|
266
274
|
|
267
275
|
# Revisions
|
268
276
|
|
277
|
+
* 1.2.7 - A bunch of fixes for MacOS and probably linux.
|
278
|
+
* 1.2.6 - Now builds image from the project root of FastLED.
|
269
279
|
* 1.1.25 - Fix up paths for `--init`
|
270
280
|
* 1.1.24 - Mac/Linux now properly responds to ctrl-c when waiting for a key event.
|
271
281
|
* 1.1.23 - Fixes missing `live-server` on platforms that don't have it already.
|
@@ -163,6 +163,14 @@ with Api.server() as server:
|
|
163
163
|
client.stop()
|
164
164
|
```
|
165
165
|
|
166
|
+
**Build Docker Image from a local copy of the FastLED repo**
|
167
|
+
```python
|
168
|
+
from fastapi import Docker, Api
|
169
|
+
container_name: str = Docker.build_from_fastled_repo()
|
170
|
+
with Api.server(container_name=container_name) as server:
|
171
|
+
...
|
172
|
+
```
|
173
|
+
|
166
174
|
# Features
|
167
175
|
|
168
176
|
## Hot reload by default
|
@@ -243,6 +251,8 @@ A: A big chunk of space is being used by unnecessary javascript `emscripten` bun
|
|
243
251
|
|
244
252
|
# Revisions
|
245
253
|
|
254
|
+
* 1.2.7 - A bunch of fixes for MacOS and probably linux.
|
255
|
+
* 1.2.6 - Now builds image from the project root of FastLED.
|
246
256
|
* 1.1.25 - Fix up paths for `--init`
|
247
257
|
* 1.1.24 - Mac/Linux now properly responds to ctrl-c when waiting for a key event.
|
248
258
|
* 1.1.23 - Fixes missing `live-server` on platforms that don't have it already.
|
@@ -0,0 +1,325 @@
|
|
1
|
+
"""FastLED Wasm Compiler package."""
|
2
|
+
|
3
|
+
# context
|
4
|
+
import subprocess
|
5
|
+
from contextlib import contextmanager
|
6
|
+
from pathlib import Path
|
7
|
+
from typing import Generator
|
8
|
+
|
9
|
+
from .compile_server import CompileServer
|
10
|
+
from .live_client import LiveClient
|
11
|
+
from .site.build import build
|
12
|
+
from .types import BuildMode, CompileResult, CompileServerError
|
13
|
+
|
14
|
+
# IMPORTANT! There's a bug in github which will REJECT any version update
|
15
|
+
# that has any other change in the repo. Please bump the version as the
|
16
|
+
# ONLY change in a commit, or else the pypi update and the release will fail.
|
17
|
+
__version__ = "1.2.7"
|
18
|
+
|
19
|
+
|
20
|
+
class Api:
|
21
|
+
@staticmethod
|
22
|
+
def get_examples(host: str | CompileServer | None = None) -> list[str]:
|
23
|
+
from fastled.project_init import get_examples
|
24
|
+
|
25
|
+
if isinstance(host, CompileServer):
|
26
|
+
host = host.url()
|
27
|
+
|
28
|
+
return get_examples(host=host)
|
29
|
+
|
30
|
+
@staticmethod
|
31
|
+
def project_init(
|
32
|
+
example=None, outputdir=None, host: str | CompileServer | None = None
|
33
|
+
) -> Path:
|
34
|
+
from fastled.project_init import project_init
|
35
|
+
|
36
|
+
if isinstance(host, CompileServer):
|
37
|
+
host = host.url()
|
38
|
+
return project_init(example, outputdir, host)
|
39
|
+
|
40
|
+
@staticmethod
|
41
|
+
def web_compile(
|
42
|
+
directory: Path | str,
|
43
|
+
host: str | CompileServer | None = None,
|
44
|
+
build_mode: BuildMode = BuildMode.QUICK,
|
45
|
+
profile: bool = False, # When true then profile information will be enabled and included in the zip.
|
46
|
+
) -> CompileResult:
|
47
|
+
from fastled.web_compile import web_compile
|
48
|
+
|
49
|
+
if isinstance(host, CompileServer):
|
50
|
+
host = host.url()
|
51
|
+
if isinstance(directory, str):
|
52
|
+
directory = Path(directory)
|
53
|
+
out: CompileResult = web_compile(
|
54
|
+
directory, host, build_mode=build_mode, profile=profile
|
55
|
+
)
|
56
|
+
return out
|
57
|
+
|
58
|
+
@staticmethod
|
59
|
+
def live_client(
|
60
|
+
sketch_directory: Path,
|
61
|
+
host: str | CompileServer | None = None,
|
62
|
+
auto_start=True,
|
63
|
+
open_web_browser=True,
|
64
|
+
keep_running=True,
|
65
|
+
build_mode=BuildMode.QUICK,
|
66
|
+
profile=False,
|
67
|
+
) -> LiveClient:
|
68
|
+
return LiveClient(
|
69
|
+
sketch_directory=sketch_directory,
|
70
|
+
host=host,
|
71
|
+
auto_start=auto_start,
|
72
|
+
open_web_browser=open_web_browser,
|
73
|
+
keep_running=keep_running,
|
74
|
+
build_mode=build_mode,
|
75
|
+
profile=profile,
|
76
|
+
)
|
77
|
+
|
78
|
+
@staticmethod
|
79
|
+
def spawn_server(
|
80
|
+
interactive=False,
|
81
|
+
auto_updates=None,
|
82
|
+
auto_start=True,
|
83
|
+
container_name: str | None = None,
|
84
|
+
) -> CompileServer:
|
85
|
+
from fastled.compile_server import CompileServer
|
86
|
+
|
87
|
+
out = CompileServer(
|
88
|
+
container_name=container_name,
|
89
|
+
interactive=interactive,
|
90
|
+
auto_updates=auto_updates,
|
91
|
+
mapped_dir=None,
|
92
|
+
auto_start=auto_start,
|
93
|
+
)
|
94
|
+
return out
|
95
|
+
|
96
|
+
@staticmethod
|
97
|
+
@contextmanager
|
98
|
+
def server(
|
99
|
+
interactive=False,
|
100
|
+
auto_updates=None,
|
101
|
+
auto_start=True,
|
102
|
+
container_name: str | None = None,
|
103
|
+
) -> Generator[CompileServer, None, None]:
|
104
|
+
server = Api.spawn_server(
|
105
|
+
interactive=interactive,
|
106
|
+
auto_updates=auto_updates,
|
107
|
+
auto_start=auto_start,
|
108
|
+
container_name=container_name,
|
109
|
+
)
|
110
|
+
try:
|
111
|
+
yield server
|
112
|
+
finally:
|
113
|
+
server.stop()
|
114
|
+
|
115
|
+
|
116
|
+
class Docker:
|
117
|
+
@staticmethod
|
118
|
+
def is_installed() -> bool:
|
119
|
+
from fastled.docker_manager import DockerManager
|
120
|
+
|
121
|
+
return DockerManager.is_docker_installed()
|
122
|
+
|
123
|
+
@staticmethod
|
124
|
+
def is_running() -> bool:
|
125
|
+
from fastled.docker_manager import DockerManager
|
126
|
+
|
127
|
+
return DockerManager.is_running()
|
128
|
+
|
129
|
+
@staticmethod
|
130
|
+
def is_container_running(container_name: str | None = None) -> bool:
|
131
|
+
# from fastled.docker import is_container_running
|
132
|
+
from fastled.docker_manager import DockerManager
|
133
|
+
from fastled.settings import CONTAINER_NAME
|
134
|
+
|
135
|
+
docker_mgr = DockerManager()
|
136
|
+
container_name = container_name or CONTAINER_NAME
|
137
|
+
return docker_mgr.is_container_running(container_name)
|
138
|
+
|
139
|
+
@staticmethod
|
140
|
+
def purge() -> None:
|
141
|
+
from fastled.docker_manager import DockerManager
|
142
|
+
from fastled.settings import IMAGE_NAME
|
143
|
+
|
144
|
+
docker_mgr = DockerManager()
|
145
|
+
docker_mgr.purge(image_name=IMAGE_NAME)
|
146
|
+
|
147
|
+
@staticmethod
|
148
|
+
def build_from_github(
|
149
|
+
url: str = "https://github.com/fastled/fastled",
|
150
|
+
output_dir: Path | str = Path(".cache/fastled"),
|
151
|
+
) -> str:
|
152
|
+
"""Build the FastLED WASM compiler Docker image from a GitHub repository.
|
153
|
+
|
154
|
+
Args:
|
155
|
+
url: GitHub repository URL (default: https://github.com/fastled/fastled)
|
156
|
+
output_dir: Directory to clone the repo into (default: .cache/fastled)
|
157
|
+
|
158
|
+
Returns:
|
159
|
+
Container name.
|
160
|
+
"""
|
161
|
+
|
162
|
+
from fastled.docker_manager import DockerManager
|
163
|
+
from fastled.settings import CONTAINER_NAME, IMAGE_NAME
|
164
|
+
|
165
|
+
if isinstance(output_dir, str):
|
166
|
+
output_dir = Path(output_dir)
|
167
|
+
|
168
|
+
# Create output directory if it doesn't exist
|
169
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
170
|
+
|
171
|
+
# Clone or update the repository
|
172
|
+
if (output_dir / ".git").exists():
|
173
|
+
print(f"Updating existing repository in {output_dir}")
|
174
|
+
# Reset local changes and move HEAD back to handle force pushes
|
175
|
+
subprocess.run(
|
176
|
+
["git", "reset", "--hard", "HEAD~10"],
|
177
|
+
cwd=output_dir,
|
178
|
+
check=True,
|
179
|
+
capture_output=True, # Suppress output of reset
|
180
|
+
)
|
181
|
+
subprocess.run(
|
182
|
+
["git", "pull", "origin", "master"], cwd=output_dir, check=True
|
183
|
+
)
|
184
|
+
else:
|
185
|
+
print(f"Cloning {url} into {output_dir}")
|
186
|
+
subprocess.run(["git", "clone", url, str(output_dir)], check=True)
|
187
|
+
|
188
|
+
dockerfile_path = (
|
189
|
+
output_dir / "src" / "platforms" / "wasm" / "compiler" / "Dockerfile"
|
190
|
+
)
|
191
|
+
|
192
|
+
if not dockerfile_path.exists():
|
193
|
+
raise FileNotFoundError(
|
194
|
+
f"Dockerfile not found at {dockerfile_path}. "
|
195
|
+
"This may not be a valid FastLED repository."
|
196
|
+
)
|
197
|
+
|
198
|
+
docker_mgr = DockerManager()
|
199
|
+
|
200
|
+
platform_tag = ""
|
201
|
+
# if "arm" in docker_mgr.architecture():
|
202
|
+
if (
|
203
|
+
"arm"
|
204
|
+
in subprocess.run(["uname", "-m"], capture_output=True).stdout.decode()
|
205
|
+
):
|
206
|
+
platform_tag = "-arm64"
|
207
|
+
|
208
|
+
# Build the image
|
209
|
+
docker_mgr.build_image(
|
210
|
+
image_name=IMAGE_NAME,
|
211
|
+
tag="main",
|
212
|
+
dockerfile_path=dockerfile_path,
|
213
|
+
build_context=output_dir,
|
214
|
+
build_args={"NO_PREWARM": "1"},
|
215
|
+
platform_tag=platform_tag,
|
216
|
+
)
|
217
|
+
|
218
|
+
# Run the container and return it
|
219
|
+
container = docker_mgr.run_container_detached(
|
220
|
+
image_name=IMAGE_NAME,
|
221
|
+
tag="main",
|
222
|
+
container_name=CONTAINER_NAME,
|
223
|
+
command=None, # Use default command from Dockerfile
|
224
|
+
volumes=None, # No volumes needed for build
|
225
|
+
ports=None, # No ports needed for build
|
226
|
+
remove_previous=True, # Remove any existing container
|
227
|
+
)
|
228
|
+
|
229
|
+
return container.name
|
230
|
+
|
231
|
+
@staticmethod
|
232
|
+
def build_from_fastled_repo(
|
233
|
+
project_root: Path | str = Path("."), platform_tag: str = ""
|
234
|
+
) -> str:
|
235
|
+
"""Build the FastLED WASM compiler Docker image, which will be tagged as "main".
|
236
|
+
|
237
|
+
Args:
|
238
|
+
project_root: Path to the FastLED project root directory
|
239
|
+
platform_tag: Optional platform tag (e.g. "-arm64" for ARM builds)
|
240
|
+
|
241
|
+
Returns:
|
242
|
+
The string name of the docker container.
|
243
|
+
"""
|
244
|
+
from fastled.docker_manager import DockerManager
|
245
|
+
from fastled.settings import CONTAINER_NAME, IMAGE_NAME
|
246
|
+
|
247
|
+
if isinstance(project_root, str):
|
248
|
+
project_root = Path(project_root)
|
249
|
+
|
250
|
+
dockerfile_path = (
|
251
|
+
project_root / "src" / "platforms" / "wasm" / "compiler" / "Dockerfile"
|
252
|
+
)
|
253
|
+
|
254
|
+
docker_mgr = DockerManager()
|
255
|
+
|
256
|
+
platform_tag = ""
|
257
|
+
# if "arm" in docker_mgr.architecture():
|
258
|
+
if (
|
259
|
+
"arm"
|
260
|
+
in subprocess.run(["uname", "-m"], capture_output=True).stdout.decode()
|
261
|
+
):
|
262
|
+
platform_tag = "-arm64"
|
263
|
+
|
264
|
+
# if image exists, remove it
|
265
|
+
docker_mgr.purge(image_name=IMAGE_NAME)
|
266
|
+
|
267
|
+
# Build the image
|
268
|
+
docker_mgr.build_image(
|
269
|
+
image_name=IMAGE_NAME,
|
270
|
+
tag="main",
|
271
|
+
dockerfile_path=dockerfile_path,
|
272
|
+
build_context=project_root,
|
273
|
+
build_args={"NO_PREWARM": "1"},
|
274
|
+
platform_tag=platform_tag,
|
275
|
+
)
|
276
|
+
|
277
|
+
# Run the container and return it
|
278
|
+
container = docker_mgr.run_container_detached(
|
279
|
+
image_name=IMAGE_NAME,
|
280
|
+
tag="main",
|
281
|
+
container_name=CONTAINER_NAME,
|
282
|
+
command=None, # Use default command from Dockerfile
|
283
|
+
volumes=None, # No volumes needed for build
|
284
|
+
ports=None, # No ports needed for build
|
285
|
+
remove_previous=True, # Remove any existing container
|
286
|
+
)
|
287
|
+
container_name = f"{container.name}"
|
288
|
+
return container_name
|
289
|
+
|
290
|
+
|
291
|
+
class Test:
|
292
|
+
__test__ = False # This prevents unittest from recognizing it as a test class.
|
293
|
+
|
294
|
+
@staticmethod
|
295
|
+
def can_run_local_docker_tests() -> bool:
|
296
|
+
from fastled.test.can_run_local_docker_tests import can_run_local_docker_tests
|
297
|
+
|
298
|
+
return can_run_local_docker_tests()
|
299
|
+
|
300
|
+
@staticmethod
|
301
|
+
def test_examples(
|
302
|
+
examples: list[str] | None = None, host: str | CompileServer | None = None
|
303
|
+
) -> dict[str, Exception]:
|
304
|
+
from fastled.test.examples import test_examples
|
305
|
+
|
306
|
+
if isinstance(host, CompileServer):
|
307
|
+
host = host.url()
|
308
|
+
|
309
|
+
return test_examples(examples=examples, host=host)
|
310
|
+
|
311
|
+
@staticmethod
|
312
|
+
def build_site(outputdir: Path, fast: bool | None = None, check: bool = True):
|
313
|
+
"""Builds the FastLED compiler site."""
|
314
|
+
build(outputdir=outputdir, fast=fast, check=check)
|
315
|
+
|
316
|
+
|
317
|
+
__all__ = [
|
318
|
+
"Api",
|
319
|
+
"Test",
|
320
|
+
"Build",
|
321
|
+
"CompileServer",
|
322
|
+
"CompileResult",
|
323
|
+
"CompileServerError",
|
324
|
+
"BuildMode",
|
325
|
+
]
|
@@ -51,6 +51,32 @@ def main() -> int:
|
|
51
51
|
print("Finished updating.")
|
52
52
|
return 0
|
53
53
|
|
54
|
+
if args.build:
|
55
|
+
try:
|
56
|
+
project_root = Path(".").absolute()
|
57
|
+
print(f"Building Docker image at {project_root}")
|
58
|
+
from fastled import Api, Docker
|
59
|
+
|
60
|
+
docker_image_name = Docker.build_from_fastled_repo(
|
61
|
+
project_root=project_root
|
62
|
+
)
|
63
|
+
print(f"Built Docker image: {docker_image_name}")
|
64
|
+
print("Running server")
|
65
|
+
with Api.server(
|
66
|
+
auto_updates=False, container_name=docker_image_name
|
67
|
+
) as server:
|
68
|
+
print(f"Server started at {server.url()}")
|
69
|
+
sketch_dir = Path("examples/wasm")
|
70
|
+
with Api.live_client(
|
71
|
+
sketch_directory=sketch_dir, host=server
|
72
|
+
) as client:
|
73
|
+
print(f"Client started at {client.url()}")
|
74
|
+
while True:
|
75
|
+
time.sleep(0.1)
|
76
|
+
except KeyboardInterrupt:
|
77
|
+
print("\nExiting from client...")
|
78
|
+
return 1
|
79
|
+
|
54
80
|
if args.server:
|
55
81
|
print("Running in server only mode.")
|
56
82
|
return run_server(args)
|
@@ -4,6 +4,7 @@ New abstraction for Docker management with improved Ctrl+C handling.
|
|
4
4
|
|
5
5
|
import _thread
|
6
6
|
import os
|
7
|
+
import platform
|
7
8
|
import subprocess
|
8
9
|
import sys
|
9
10
|
import threading
|
@@ -97,10 +98,12 @@ class RunningContainer:
|
|
97
98
|
|
98
99
|
class DockerManager:
|
99
100
|
def __init__(self) -> None:
|
101
|
+
from docker.errors import DockerException
|
102
|
+
|
100
103
|
try:
|
101
104
|
self._client: DockerClient | None = None
|
102
105
|
self.first_run = False
|
103
|
-
except
|
106
|
+
except DockerException as e:
|
104
107
|
stack = traceback.format_exc()
|
105
108
|
warnings.warn(f"Error initializing Docker client: {e}\n{stack}")
|
106
109
|
raise
|
@@ -475,7 +478,7 @@ class DockerManager:
|
|
475
478
|
print(
|
476
479
|
f"Container {container_name} did not restart within {timeout} seconds."
|
477
480
|
)
|
478
|
-
container.stop()
|
481
|
+
container.stop(timeout=0)
|
479
482
|
print(f"Container {container_name} has been stopped.")
|
480
483
|
container.start()
|
481
484
|
elif container.status == "paused":
|
@@ -501,6 +504,7 @@ class DockerManager:
|
|
501
504
|
tty=True,
|
502
505
|
volumes=volumes,
|
503
506
|
ports=ports,
|
507
|
+
remove=True,
|
504
508
|
)
|
505
509
|
return container
|
506
510
|
|
@@ -552,6 +556,8 @@ class DockerManager:
|
|
552
556
|
if isinstance(container, str):
|
553
557
|
container = self.get_container(container)
|
554
558
|
|
559
|
+
assert container is not None, "Container not found."
|
560
|
+
|
555
561
|
print(f"Attaching to container {container.name}...")
|
556
562
|
|
557
563
|
first_run = self.first_run
|
@@ -570,7 +576,11 @@ class DockerManager:
|
|
570
576
|
print(f"Could not put container {container_name} to sleep.")
|
571
577
|
return
|
572
578
|
try:
|
573
|
-
|
579
|
+
if platform.system() == "Windows":
|
580
|
+
container.pause()
|
581
|
+
else:
|
582
|
+
container.stop()
|
583
|
+
container.remove()
|
574
584
|
print(f"Container {container.name} has been suspended.")
|
575
585
|
except KeyboardInterrupt:
|
576
586
|
print(f"Container {container.name} interrupted by keyboard interrupt.")
|
@@ -583,21 +593,23 @@ class DockerManager:
|
|
583
593
|
"""
|
584
594
|
if isinstance(container, str):
|
585
595
|
container = self.get_container(container)
|
596
|
+
if not container:
|
597
|
+
print(f"Could not resume container {container}.")
|
598
|
+
return
|
586
599
|
try:
|
587
600
|
container.unpause()
|
588
601
|
print(f"Container {container.name} has been resumed.")
|
589
602
|
except Exception as e:
|
590
603
|
print(f"Failed to resume container {container.name}: {e}")
|
591
604
|
|
592
|
-
def get_container(self, container_name: str) -> Container:
|
605
|
+
def get_container(self, container_name: str) -> Container | None:
|
593
606
|
"""
|
594
607
|
Get a container by name.
|
595
608
|
"""
|
596
609
|
try:
|
597
610
|
return self.client.containers.get(container_name)
|
598
611
|
except docker.errors.NotFound:
|
599
|
-
|
600
|
-
raise
|
612
|
+
return None
|
601
613
|
|
602
614
|
def is_container_running(self, container_name: str) -> bool:
|
603
615
|
"""
|
@@ -610,6 +622,67 @@ class DockerManager:
|
|
610
622
|
print(f"Container {container_name} not found.")
|
611
623
|
return False
|
612
624
|
|
625
|
+
def build_image(
|
626
|
+
self,
|
627
|
+
image_name: str,
|
628
|
+
tag: str,
|
629
|
+
dockerfile_path: Path,
|
630
|
+
build_context: Path,
|
631
|
+
build_args: dict[str, str] | None = None,
|
632
|
+
platform_tag: str = "",
|
633
|
+
) -> None:
|
634
|
+
"""
|
635
|
+
Build a Docker image from a Dockerfile.
|
636
|
+
|
637
|
+
Args:
|
638
|
+
image_name: Name for the image
|
639
|
+
tag: Tag for the image
|
640
|
+
dockerfile_path: Path to the Dockerfile
|
641
|
+
build_context: Path to the build context directory
|
642
|
+
build_args: Optional dictionary of build arguments
|
643
|
+
platform_tag: Optional platform tag (e.g. "-arm64")
|
644
|
+
"""
|
645
|
+
if not dockerfile_path.exists():
|
646
|
+
raise FileNotFoundError(f"Dockerfile not found at {dockerfile_path}")
|
647
|
+
|
648
|
+
if not build_context.exists():
|
649
|
+
raise FileNotFoundError(
|
650
|
+
f"Build context directory not found at {build_context}"
|
651
|
+
)
|
652
|
+
|
653
|
+
print(f"Building Docker image {image_name}:{tag} from {dockerfile_path}")
|
654
|
+
|
655
|
+
# Prepare build arguments
|
656
|
+
buildargs = build_args or {}
|
657
|
+
if platform_tag:
|
658
|
+
buildargs["PLATFORM_TAG"] = platform_tag
|
659
|
+
|
660
|
+
try:
|
661
|
+
cmd_list = [
|
662
|
+
"docker",
|
663
|
+
"build",
|
664
|
+
"-t",
|
665
|
+
f"{image_name}:{tag}",
|
666
|
+
]
|
667
|
+
|
668
|
+
# Add build args
|
669
|
+
for arg_name, arg_value in buildargs.items():
|
670
|
+
cmd_list.extend(["--build-arg", f"{arg_name}={arg_value}"])
|
671
|
+
|
672
|
+
# Add dockerfile and context paths
|
673
|
+
cmd_list.extend(["-f", str(dockerfile_path), str(build_context)])
|
674
|
+
|
675
|
+
cmd_str = subprocess.list2cmdline(cmd_list)
|
676
|
+
print(f"Running command: {cmd_str}")
|
677
|
+
|
678
|
+
# Run the build command
|
679
|
+
subprocess.run(cmd_list, check=True)
|
680
|
+
print(f"Successfully built image {image_name}:{tag}")
|
681
|
+
|
682
|
+
except subprocess.CalledProcessError as e:
|
683
|
+
print(f"Error building Docker image: {e}")
|
684
|
+
raise
|
685
|
+
|
613
686
|
def purge(self, image_name: str) -> None:
|
614
687
|
"""
|
615
688
|
Remove all containers and images associated with the given image name.
|
@@ -626,11 +699,13 @@ class DockerManager:
|
|
626
699
|
if any(image_name in tag for tag in container.image.tags):
|
627
700
|
print(f"Removing container {container.name}")
|
628
701
|
container.remove(force=True)
|
702
|
+
|
629
703
|
except Exception as e:
|
630
704
|
print(f"Error removing containers: {e}")
|
631
705
|
|
632
706
|
# Remove all images with this name
|
633
707
|
try:
|
708
|
+
self.client.images.prune(filters={"dangling": False})
|
634
709
|
images = self.client.images.list()
|
635
710
|
for image in images:
|
636
711
|
if any(image_name in tag for tag in image.tags):
|
@@ -6,12 +6,10 @@ import time
|
|
6
6
|
from queue import Empty, Queue
|
7
7
|
from threading import Thread
|
8
8
|
|
9
|
-
_WHITE_SPACE =
|
9
|
+
_WHITE_SPACE = {" ", "\n", "\r"} # Including Enter key as whitespace
|
10
10
|
|
11
11
|
|
12
|
-
# Original space bar, but now also enter key.
|
13
12
|
class SpaceBarWatcher:
|
14
|
-
|
15
13
|
@classmethod
|
16
14
|
def watch_space_bar_pressed(cls, timeout: float = 0) -> bool:
|
17
15
|
watcher = cls()
|
@@ -32,9 +30,7 @@ class SpaceBarWatcher:
|
|
32
30
|
self.process.start()
|
33
31
|
|
34
32
|
def _watch_for_space(self) -> None:
|
35
|
-
# Set stdin to non-blocking mode
|
36
33
|
fd = sys.stdin.fileno()
|
37
|
-
|
38
34
|
if os.name == "nt": # Windows
|
39
35
|
import msvcrt
|
40
36
|
|
@@ -51,14 +47,13 @@ class SpaceBarWatcher:
|
|
51
47
|
char = msvcrt.getch().decode() # type: ignore
|
52
48
|
if char in _WHITE_SPACE:
|
53
49
|
self.queue.put(ord(" "))
|
54
|
-
|
55
50
|
else: # Unix-like systems
|
56
51
|
import termios
|
57
52
|
import tty
|
58
53
|
|
59
54
|
old_settings = termios.tcgetattr(fd) # type: ignore
|
60
55
|
try:
|
61
|
-
tty.
|
56
|
+
tty.setcbreak(fd) # Use cbreak mode to avoid console issues
|
62
57
|
while True:
|
63
58
|
# Check for cancel signal
|
64
59
|
try:
|
@@ -70,21 +65,22 @@ class SpaceBarWatcher:
|
|
70
65
|
# Check if there's input ready
|
71
66
|
if select.select([sys.stdin], [], [], 0.1)[0]:
|
72
67
|
char = sys.stdin.read(1)
|
73
|
-
if ord(char) == 3: #
|
68
|
+
if ord(char) == 3: # Ctrl+C
|
74
69
|
_thread.interrupt_main()
|
75
70
|
break
|
76
|
-
|
77
71
|
if char in _WHITE_SPACE:
|
78
72
|
self.queue.put(ord(" "))
|
79
73
|
finally:
|
80
|
-
termios.tcsetattr(
|
74
|
+
termios.tcsetattr(
|
75
|
+
fd, termios.TCSADRAIN, old_settings
|
76
|
+
) # Restore terminal settings
|
81
77
|
|
82
78
|
def space_bar_pressed(self) -> bool:
|
83
79
|
found = False
|
84
80
|
while not self.queue.empty():
|
85
81
|
try:
|
86
|
-
key = self.queue.get(block=False
|
87
|
-
if key == ord(" "):
|
82
|
+
key = self.queue.get(block=False)
|
83
|
+
if key == ord(" "): # Spacebar
|
88
84
|
found = True
|
89
85
|
self.queue.task_done()
|
90
86
|
except Empty:
|
@@ -45,6 +45,17 @@ class LiveClient:
|
|
45
45
|
)
|
46
46
|
return rtn
|
47
47
|
|
48
|
+
def url(self) -> str:
|
49
|
+
"""Get the URL of the server."""
|
50
|
+
if isinstance(self.host, CompileServer):
|
51
|
+
return self.host.url()
|
52
|
+
if self.host is None:
|
53
|
+
import warnings
|
54
|
+
|
55
|
+
warnings.warn("TODO: use the actual host.")
|
56
|
+
return "http://localhost:9021"
|
57
|
+
return self.host
|
58
|
+
|
48
59
|
@property
|
49
60
|
def running(self) -> bool:
|
50
61
|
return self.thread is not None and self.thread.is_alive()
|
@@ -67,3 +78,9 @@ class LiveClient:
|
|
67
78
|
"""Finalize the client."""
|
68
79
|
self.stop()
|
69
80
|
self.thread = None
|
81
|
+
|
82
|
+
def __enter__(self) -> "LiveClient":
|
83
|
+
return self
|
84
|
+
|
85
|
+
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
86
|
+
self.finalize()
|
@@ -17,7 +17,8 @@ def _open_browser_python(fastled_js: Path) -> None:
|
|
17
17
|
subprocess.run(
|
18
18
|
[sys.executable, "-m", "nodejs.npm", "install", "-g", "live-server"]
|
19
19
|
)
|
20
|
-
|
20
|
+
proc = subprocess.Popen(["cd", fastled_js, "&&", "live-server"])
|
21
|
+
proc.wait()
|
21
22
|
|
22
23
|
|
23
24
|
def _find_open_port(start_port: int) -> int:
|