fastled 1.3.30__py3-none-any.whl → 1.4.50__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastled/__init__.py +30 -2
- fastled/__main__.py +14 -0
- fastled/__version__.py +1 -1
- fastled/app.py +51 -2
- fastled/args.py +33 -0
- fastled/client_server.py +188 -40
- fastled/compile_server.py +10 -0
- fastled/compile_server_impl.py +34 -1
- fastled/docker_manager.py +56 -14
- fastled/emoji_util.py +27 -0
- fastled/filewatcher.py +6 -3
- fastled/find_good_connection.py +105 -0
- fastled/header_dump.py +63 -0
- fastled/install/__init__.py +1 -0
- fastled/install/examples_manager.py +62 -0
- fastled/install/extension_manager.py +113 -0
- fastled/install/main.py +156 -0
- fastled/install/project_detection.py +167 -0
- fastled/install/test_install.py +373 -0
- fastled/install/vscode_config.py +344 -0
- fastled/interruptible_http.py +148 -0
- fastled/live_client.py +21 -1
- fastled/open_browser.py +84 -16
- fastled/parse_args.py +110 -9
- fastled/playwright/chrome_extension_downloader.py +207 -0
- fastled/playwright/playwright_browser.py +773 -0
- fastled/playwright/resize_tracking.py +127 -0
- fastled/print_filter.py +52 -52
- fastled/project_init.py +20 -13
- fastled/select_sketch_directory.py +142 -19
- fastled/server_flask.py +37 -1
- fastled/settings.py +47 -3
- fastled/sketch.py +121 -4
- fastled/string_diff.py +162 -26
- fastled/test/examples.py +7 -5
- fastled/types.py +4 -0
- fastled/util.py +34 -0
- fastled/version.py +41 -41
- fastled/web_compile.py +379 -236
- fastled/zip_files.py +76 -0
- {fastled-1.3.30.dist-info → fastled-1.4.50.dist-info}/METADATA +533 -508
- fastled-1.4.50.dist-info/RECORD +60 -0
- fastled-1.3.30.dist-info/RECORD +0 -44
- {fastled-1.3.30.dist-info → fastled-1.4.50.dist-info}/WHEEL +0 -0
- {fastled-1.3.30.dist-info → fastled-1.4.50.dist-info}/entry_points.txt +0 -0
- {fastled-1.3.30.dist-info → fastled-1.4.50.dist-info}/licenses/LICENSE +0 -0
- {fastled-1.3.30.dist-info → fastled-1.4.50.dist-info}/top_level.txt +0 -0
fastled/open_browser.py
CHANGED
|
@@ -6,8 +6,27 @@ import weakref
|
|
|
6
6
|
from multiprocessing import Process
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
|
+
from fastled.playwright.playwright_browser import open_with_playwright
|
|
9
10
|
from fastled.server_flask import run_flask_in_thread
|
|
10
11
|
|
|
12
|
+
# Global reference to keep Playwright browser alive
|
|
13
|
+
_playwright_browser_proxy = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def cleanup_playwright_browser() -> None:
|
|
17
|
+
"""Clean up the Playwright browser on exit."""
|
|
18
|
+
try:
|
|
19
|
+
global _playwright_browser_proxy
|
|
20
|
+
if _playwright_browser_proxy:
|
|
21
|
+
_playwright_browser_proxy.close()
|
|
22
|
+
_playwright_browser_proxy = None
|
|
23
|
+
except Exception:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Register cleanup function
|
|
28
|
+
atexit.register(cleanup_playwright_browser)
|
|
29
|
+
|
|
11
30
|
DEFAULT_PORT = 8089 # different than live version.
|
|
12
31
|
PYTHON_EXE = sys.executable
|
|
13
32
|
|
|
@@ -38,9 +57,15 @@ def is_port_free(port: int) -> bool:
|
|
|
38
57
|
import httpx
|
|
39
58
|
|
|
40
59
|
try:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
60
|
+
# Try HTTPS first, then fall back to HTTP
|
|
61
|
+
try:
|
|
62
|
+
response = httpx.get(f"https://localhost:{port}", timeout=1, verify=False)
|
|
63
|
+
response.raise_for_status()
|
|
64
|
+
return False
|
|
65
|
+
except (httpx.HTTPError, httpx.ConnectError):
|
|
66
|
+
response = httpx.get(f"http://localhost:{port}", timeout=1)
|
|
67
|
+
response.raise_for_status()
|
|
68
|
+
return False
|
|
44
69
|
except (httpx.HTTPError, httpx.ConnectError):
|
|
45
70
|
return True
|
|
46
71
|
|
|
@@ -56,16 +81,21 @@ def find_free_port(start_port: int) -> int:
|
|
|
56
81
|
raise ValueError("Could not find a free port")
|
|
57
82
|
|
|
58
83
|
|
|
59
|
-
def wait_for_server(port: int, timeout: int = 10) -> None:
|
|
84
|
+
def wait_for_server(port: int, timeout: int = 10, enable_https: bool = True) -> None:
|
|
60
85
|
"""Wait for the server to start."""
|
|
61
86
|
from httpx import get
|
|
62
87
|
|
|
63
88
|
future_time = time.time() + timeout
|
|
89
|
+
protocol = "https" if enable_https else "http"
|
|
64
90
|
while future_time > time.time():
|
|
65
91
|
try:
|
|
66
|
-
|
|
92
|
+
# Try the specified protocol (HTTPS with SSL verification disabled for self-signed certs)
|
|
93
|
+
url = f"{protocol}://localhost:{port}"
|
|
67
94
|
# print(f"Waiting for server to start at {url}")
|
|
68
|
-
|
|
95
|
+
verify = (
|
|
96
|
+
False if enable_https else True
|
|
97
|
+
) # Only disable SSL verification for HTTPS
|
|
98
|
+
response = get(url, timeout=1, verify=verify)
|
|
69
99
|
if response.status_code == 200:
|
|
70
100
|
return
|
|
71
101
|
except Exception:
|
|
@@ -78,6 +108,8 @@ def spawn_http_server(
|
|
|
78
108
|
compile_server_port: int,
|
|
79
109
|
port: int | None = None,
|
|
80
110
|
open_browser: bool = True,
|
|
111
|
+
app: bool = False,
|
|
112
|
+
enable_https: bool = True,
|
|
81
113
|
) -> Process:
|
|
82
114
|
|
|
83
115
|
if port is not None and not is_port_free(port):
|
|
@@ -86,6 +118,17 @@ def spawn_http_server(
|
|
|
86
118
|
offset = random.randint(0, 100)
|
|
87
119
|
port = find_free_port(DEFAULT_PORT + offset)
|
|
88
120
|
|
|
121
|
+
# Get SSL certificate paths from the fastled assets directory if HTTPS is enabled
|
|
122
|
+
certfile: Path | None = None
|
|
123
|
+
keyfile: Path | None = None
|
|
124
|
+
|
|
125
|
+
if enable_https:
|
|
126
|
+
import fastled
|
|
127
|
+
|
|
128
|
+
assets_dir = Path(fastled.__file__).parent / "assets"
|
|
129
|
+
certfile = assets_dir / "localhost.pem"
|
|
130
|
+
keyfile = assets_dir / "localhost-key.pem"
|
|
131
|
+
|
|
89
132
|
# port: int,
|
|
90
133
|
# cwd: Path,
|
|
91
134
|
# compile_server_port: int,
|
|
@@ -94,7 +137,7 @@ def spawn_http_server(
|
|
|
94
137
|
|
|
95
138
|
proc = Process(
|
|
96
139
|
target=run_flask_in_thread,
|
|
97
|
-
args=(port, fastled_js, compile_server_port),
|
|
140
|
+
args=(port, fastled_js, compile_server_port, certfile, keyfile),
|
|
98
141
|
daemon=True,
|
|
99
142
|
)
|
|
100
143
|
add_cleanup(proc)
|
|
@@ -103,16 +146,41 @@ def spawn_http_server(
|
|
|
103
146
|
# Add to cleanup set with weak reference
|
|
104
147
|
add_cleanup(proc)
|
|
105
148
|
|
|
106
|
-
wait_for_server(port)
|
|
149
|
+
wait_for_server(port, enable_https=enable_https)
|
|
107
150
|
if open_browser:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
151
|
+
protocol = "https" if enable_https else "http"
|
|
152
|
+
url = f"{protocol}://localhost:{port}"
|
|
153
|
+
should_use_playwright = app
|
|
154
|
+
|
|
155
|
+
if should_use_playwright:
|
|
156
|
+
if app:
|
|
157
|
+
# For --app mode, try to install browsers if needed
|
|
158
|
+
from fastled.playwright.playwright_browser import (
|
|
159
|
+
install_playwright_browsers,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
install_playwright_browsers()
|
|
163
|
+
|
|
164
|
+
print(f"Opening FastLED sketch in Playwright browser: {url}")
|
|
165
|
+
print(
|
|
166
|
+
"Auto-resize enabled: Browser window will automatically adjust to content size"
|
|
167
|
+
)
|
|
168
|
+
print(
|
|
169
|
+
"🔧 C++ DevTools Support extension will be loaded for DWARF debugging"
|
|
170
|
+
)
|
|
171
|
+
global _playwright_browser_proxy
|
|
172
|
+
_playwright_browser_proxy = open_with_playwright(
|
|
173
|
+
url, enable_extensions=True
|
|
174
|
+
)
|
|
175
|
+
else:
|
|
176
|
+
print(f"Opening browser to {url}")
|
|
177
|
+
import webbrowser
|
|
178
|
+
|
|
179
|
+
webbrowser.open(
|
|
180
|
+
url=url,
|
|
181
|
+
new=1,
|
|
182
|
+
autoraise=True,
|
|
183
|
+
)
|
|
116
184
|
return proc
|
|
117
185
|
|
|
118
186
|
|
fastled/parse_args.py
CHANGED
|
@@ -8,6 +8,7 @@ from fastled.project_init import project_init
|
|
|
8
8
|
from fastled.select_sketch_directory import select_sketch_directory
|
|
9
9
|
from fastled.settings import DEFAULT_URL, IMAGE_NAME
|
|
10
10
|
from fastled.sketch import (
|
|
11
|
+
find_sketch_by_partial_name,
|
|
11
12
|
find_sketch_directories,
|
|
12
13
|
looks_like_fastled_repo,
|
|
13
14
|
looks_like_sketch_directory,
|
|
@@ -30,16 +31,20 @@ FastLED WASM Compiler - Useful options:
|
|
|
30
31
|
--init [example] Initialize one of the top tier WASM examples
|
|
31
32
|
--web [url] Use web compiler
|
|
32
33
|
--server Run the compiler server
|
|
34
|
+
--no-platformio Bypass PlatformIO constraints using local Docker compilation
|
|
33
35
|
--quick Build in quick mode (default)
|
|
34
36
|
--profile Enable profiling the C++ build system
|
|
35
37
|
--update Update the docker image for the wasm compiler
|
|
38
|
+
--background-update Update the docker image in the background after compilation
|
|
36
39
|
--purge Remove all FastLED containers and images
|
|
40
|
+
--emsdk-headers <path> Export EMSDK headers ZIP to specified path
|
|
37
41
|
--version Show version information
|
|
38
42
|
--help Show detailed help
|
|
39
43
|
Examples:
|
|
40
44
|
fastled (will auto detect the sketch directory and prompt you)
|
|
41
45
|
fastled my_sketch
|
|
42
46
|
fastled my_sketch --web (compiles using the web compiler only)
|
|
47
|
+
fastled my_sketch --background-update (compiles and updates docker image in background)
|
|
43
48
|
fastled --init Blink (initializes a new sketch directory with the Blink example)
|
|
44
49
|
fastled --server (runs the compiler server in the current directory)
|
|
45
50
|
|
|
@@ -81,8 +86,8 @@ def parse_args() -> Args:
|
|
|
81
86
|
parser.add_argument(
|
|
82
87
|
"--ram-disk-size",
|
|
83
88
|
type=str,
|
|
84
|
-
default="
|
|
85
|
-
help="
|
|
89
|
+
default="1gb",
|
|
90
|
+
help="Size of the RAM disk for compilation (e.g., '1gb', '512mb')",
|
|
86
91
|
)
|
|
87
92
|
parser.add_argument(
|
|
88
93
|
"--web",
|
|
@@ -112,7 +117,17 @@ def parse_args() -> Args:
|
|
|
112
117
|
parser.add_argument(
|
|
113
118
|
"--no-auto-updates",
|
|
114
119
|
action="store_true",
|
|
115
|
-
help="Disable automatic updates of the wasm compiler image when using docker.",
|
|
120
|
+
help="Disable automatic updates of the wasm compiler image when using docker. (Default: False)",
|
|
121
|
+
)
|
|
122
|
+
parser.add_argument(
|
|
123
|
+
"--no-platformio",
|
|
124
|
+
action="store_true",
|
|
125
|
+
help="Bypass PlatformIO constraints by using local Docker compilation with custom build environment",
|
|
126
|
+
)
|
|
127
|
+
parser.add_argument(
|
|
128
|
+
"--app",
|
|
129
|
+
action="store_true",
|
|
130
|
+
help="Use Playwright app-like browser experience (will download browsers if needed)",
|
|
116
131
|
)
|
|
117
132
|
parser.add_argument(
|
|
118
133
|
"-u",
|
|
@@ -121,6 +136,11 @@ def parse_args() -> Args:
|
|
|
121
136
|
action="store_true",
|
|
122
137
|
help="Update the wasm compiler (if necessary) before running",
|
|
123
138
|
)
|
|
139
|
+
parser.add_argument(
|
|
140
|
+
"--background-update",
|
|
141
|
+
action="store_true",
|
|
142
|
+
help="Update the docker image in the background after compilation (user doesn't have to wait)",
|
|
143
|
+
)
|
|
124
144
|
parser.add_argument(
|
|
125
145
|
"--localhost",
|
|
126
146
|
"--local",
|
|
@@ -152,6 +172,30 @@ def parse_args() -> Args:
|
|
|
152
172
|
help="Remove all FastLED containers and images",
|
|
153
173
|
)
|
|
154
174
|
|
|
175
|
+
parser.add_argument(
|
|
176
|
+
"--install",
|
|
177
|
+
action="store_true",
|
|
178
|
+
help="Install FastLED development environment with VSCode configuration and Auto Debug extension",
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
parser.add_argument(
|
|
182
|
+
"--dry-run",
|
|
183
|
+
action="store_true",
|
|
184
|
+
help="Run in dry-run mode (simulate actions without making changes)",
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
parser.add_argument(
|
|
188
|
+
"--no-interactive",
|
|
189
|
+
action="store_true",
|
|
190
|
+
help="Run in non-interactive mode (fail instead of prompting for input)",
|
|
191
|
+
)
|
|
192
|
+
parser.add_argument(
|
|
193
|
+
"--emsdk-headers",
|
|
194
|
+
type=str,
|
|
195
|
+
default=None,
|
|
196
|
+
help="Export EMSDK headers ZIP to specified path",
|
|
197
|
+
)
|
|
198
|
+
|
|
155
199
|
build_mode = parser.add_mutually_exclusive_group()
|
|
156
200
|
build_mode.add_argument("--debug", action="store_true", help="Build in debug mode")
|
|
157
201
|
build_mode.add_argument(
|
|
@@ -167,6 +211,38 @@ def parse_args() -> Args:
|
|
|
167
211
|
|
|
168
212
|
args = parser.parse_args()
|
|
169
213
|
|
|
214
|
+
# Handle --emsdk-headers early before other processing
|
|
215
|
+
if args.emsdk_headers:
|
|
216
|
+
from fastled.header_dump import dump_emsdk_headers
|
|
217
|
+
|
|
218
|
+
out_path = args.emsdk_headers
|
|
219
|
+
dump_emsdk_headers(out_path)
|
|
220
|
+
sys.exit(0)
|
|
221
|
+
|
|
222
|
+
# Auto-enable app mode if debug is used and Playwright cache exists
|
|
223
|
+
if args.debug and not args.app:
|
|
224
|
+
playwright_dir = Path.home() / ".fastled" / "playwright"
|
|
225
|
+
if playwright_dir.exists() and any(playwright_dir.iterdir()):
|
|
226
|
+
from fastled.emoji_util import EMO
|
|
227
|
+
|
|
228
|
+
print(
|
|
229
|
+
f"{EMO('⚠️', 'WARNING:')} Debug mode detected with Playwright installed - automatically enabling app mode"
|
|
230
|
+
)
|
|
231
|
+
args.app = True
|
|
232
|
+
elif not args.no_interactive:
|
|
233
|
+
# Prompt user to install Playwright only if not in no-interactive mode
|
|
234
|
+
answer = (
|
|
235
|
+
input("Would you like to install the FastLED debugger? [y/n] ")
|
|
236
|
+
.strip()
|
|
237
|
+
.lower()
|
|
238
|
+
)
|
|
239
|
+
if answer in ["y", "yes"]:
|
|
240
|
+
print(
|
|
241
|
+
"📦 To install Playwright, run: pip install playwright && python -m playwright install"
|
|
242
|
+
)
|
|
243
|
+
print("Then run your command again with --app flag")
|
|
244
|
+
sys.exit(0)
|
|
245
|
+
|
|
170
246
|
# TODO: propagate the library.
|
|
171
247
|
# from fastled.docker_manager import force_remove_previous
|
|
172
248
|
|
|
@@ -186,6 +262,11 @@ def parse_args() -> Args:
|
|
|
186
262
|
# print(msg)
|
|
187
263
|
# set_ramdisk_size(args.ram_disk_size)
|
|
188
264
|
|
|
265
|
+
# Handle --install early before other processing
|
|
266
|
+
if args.install:
|
|
267
|
+
# Don't process other arguments when --install is used
|
|
268
|
+
return Args.from_namespace(args)
|
|
269
|
+
|
|
189
270
|
if args.purge:
|
|
190
271
|
from fastled.docker_manager import DockerManager
|
|
191
272
|
|
|
@@ -268,6 +349,12 @@ def parse_args() -> Args:
|
|
|
268
349
|
if cwd_is_fastled and not args.web and not args.server:
|
|
269
350
|
print("Forcing --local mode because we are in the FastLED repo")
|
|
270
351
|
args.localhost = True
|
|
352
|
+
if args.no_platformio:
|
|
353
|
+
print(
|
|
354
|
+
"--no-platformio mode enabled: forcing local Docker compilation to bypass PlatformIO constraints"
|
|
355
|
+
)
|
|
356
|
+
args.localhost = True
|
|
357
|
+
args.web = None # Clear web flag to ensure local compilation
|
|
271
358
|
if args.localhost:
|
|
272
359
|
args.web = "localhost"
|
|
273
360
|
if args.interactive and not args.server:
|
|
@@ -282,7 +369,7 @@ def parse_args() -> Args:
|
|
|
282
369
|
print("Searching for sketch directories...")
|
|
283
370
|
sketch_directories = find_sketch_directories(maybe_sketch_dir)
|
|
284
371
|
selected_dir = select_sketch_directory(
|
|
285
|
-
sketch_directories, cwd_is_fastled
|
|
372
|
+
sketch_directories, cwd_is_fastled, is_followup=True
|
|
286
373
|
)
|
|
287
374
|
if selected_dir:
|
|
288
375
|
print(f"Using sketch directory: {selected_dir}")
|
|
@@ -292,10 +379,24 @@ def parse_args() -> Args:
|
|
|
292
379
|
"\nYou either need to specify a sketch directory or run in --server mode."
|
|
293
380
|
)
|
|
294
381
|
sys.exit(1)
|
|
295
|
-
elif args.directory is not None
|
|
296
|
-
|
|
297
|
-
if
|
|
298
|
-
|
|
299
|
-
|
|
382
|
+
elif args.directory is not None:
|
|
383
|
+
# Check if directory is a file path
|
|
384
|
+
if os.path.isfile(args.directory):
|
|
385
|
+
dir_path = Path(args.directory).parent
|
|
386
|
+
if looks_like_sketch_directory(dir_path):
|
|
387
|
+
print(f"Using sketch directory: {dir_path}")
|
|
388
|
+
args.directory = str(dir_path)
|
|
389
|
+
# Check if directory exists as a path
|
|
390
|
+
elif not os.path.exists(args.directory):
|
|
391
|
+
# Directory doesn't exist - try partial name matching
|
|
392
|
+
try:
|
|
393
|
+
matched_dir = find_sketch_by_partial_name(args.directory)
|
|
394
|
+
print(
|
|
395
|
+
f"Matched '{args.directory}' to sketch directory: {matched_dir}"
|
|
396
|
+
)
|
|
397
|
+
args.directory = str(matched_dir)
|
|
398
|
+
except ValueError as e:
|
|
399
|
+
print(f"Error: {e}")
|
|
400
|
+
sys.exit(1)
|
|
300
401
|
|
|
301
402
|
return Args.from_namespace(args)
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Chrome extension downloader utility for FastLED WASM compiler.
|
|
3
|
+
|
|
4
|
+
This module provides functionality to download Chrome extensions from the
|
|
5
|
+
Chrome Web Store and prepare them for use with Playwright browser.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import shutil
|
|
11
|
+
import tempfile
|
|
12
|
+
import warnings
|
|
13
|
+
import zipfile
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ChromeExtensionDownloader:
|
|
20
|
+
"""Downloads Chrome extensions from the Chrome Web Store."""
|
|
21
|
+
|
|
22
|
+
# Chrome Web Store CRX download URL
|
|
23
|
+
CRX_URL = "https://clients2.google.com/service/update2/crx?response=redirect&prodversion=114.0&acceptformat=crx2,crx3&x=id%3D{extension_id}%26uc"
|
|
24
|
+
|
|
25
|
+
# Modern user agent string
|
|
26
|
+
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36"
|
|
27
|
+
|
|
28
|
+
def __init__(self, cache_dir: Path | None = None):
|
|
29
|
+
"""Initialize the Chrome extension downloader.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
cache_dir: Directory to store downloaded extensions.
|
|
33
|
+
Defaults to ~/.fastled/chrome-extensions
|
|
34
|
+
"""
|
|
35
|
+
if cache_dir is None:
|
|
36
|
+
cache_dir = Path.home() / ".fastled" / "chrome-extensions"
|
|
37
|
+
|
|
38
|
+
self.cache_dir = Path(cache_dir)
|
|
39
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
|
|
41
|
+
self.headers = {
|
|
42
|
+
"User-Agent": self.USER_AGENT,
|
|
43
|
+
"Referer": "https://chrome.google.com",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
def extract_extension_id(self, url: str) -> str:
|
|
47
|
+
"""Extract extension ID from Chrome Web Store URL.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
url: Chrome Web Store URL
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Extension ID string
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
ValueError: If URL is not a valid Chrome Web Store URL
|
|
57
|
+
"""
|
|
58
|
+
# Match new Chrome Web Store URLs (chromewebstore.google.com)
|
|
59
|
+
new_pattern = r"chromewebstore\.google\.com/detail/[^/]+/([a-z]{32})"
|
|
60
|
+
match = re.search(new_pattern, url)
|
|
61
|
+
|
|
62
|
+
if match:
|
|
63
|
+
return match.group(1)
|
|
64
|
+
|
|
65
|
+
# Match old Chrome Web Store URLs (chrome.google.com/webstore)
|
|
66
|
+
old_pattern = r"chrome\.google\.com/webstore/detail/[^/]+/([a-z]{32})"
|
|
67
|
+
match = re.search(old_pattern, url)
|
|
68
|
+
|
|
69
|
+
if match:
|
|
70
|
+
return match.group(1)
|
|
71
|
+
|
|
72
|
+
# Try direct extension ID
|
|
73
|
+
if re.match(r"^[a-z]{32}$", url):
|
|
74
|
+
return url
|
|
75
|
+
|
|
76
|
+
raise ValueError(f"Invalid Chrome Web Store URL or extension ID: {url}")
|
|
77
|
+
|
|
78
|
+
def download_crx(self, extension_id: str) -> bytes:
|
|
79
|
+
"""Download CRX file from Chrome Web Store.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
extension_id: Chrome extension ID
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
CRX file content as bytes
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
httpx.RequestError: If download fails
|
|
89
|
+
"""
|
|
90
|
+
download_url = self.CRX_URL.format(extension_id=extension_id)
|
|
91
|
+
|
|
92
|
+
with httpx.Client(follow_redirects=True) as client:
|
|
93
|
+
response = client.get(download_url, headers=self.headers)
|
|
94
|
+
response.raise_for_status()
|
|
95
|
+
|
|
96
|
+
return response.content
|
|
97
|
+
|
|
98
|
+
def extract_crx_to_directory(self, crx_content: bytes, extract_dir: Path) -> None:
|
|
99
|
+
"""Extract CRX file content to a directory.
|
|
100
|
+
|
|
101
|
+
CRX files are essentially ZIP files with a header that needs to be removed.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
crx_content: CRX file content as bytes
|
|
105
|
+
extract_dir: Directory to extract the extension to
|
|
106
|
+
"""
|
|
107
|
+
# CRX files have a header before the ZIP content
|
|
108
|
+
# We need to find the ZIP header (starts with 'PK')
|
|
109
|
+
zip_start = crx_content.find(b"PK\x03\x04")
|
|
110
|
+
if zip_start == -1:
|
|
111
|
+
zip_start = crx_content.find(b"PK\x05\x06") # Empty ZIP
|
|
112
|
+
|
|
113
|
+
if zip_start == -1:
|
|
114
|
+
raise ValueError("Could not find ZIP header in CRX file")
|
|
115
|
+
|
|
116
|
+
# Extract the ZIP portion
|
|
117
|
+
zip_content = crx_content[zip_start:]
|
|
118
|
+
|
|
119
|
+
# Create temporary file to extract from
|
|
120
|
+
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as temp_zip:
|
|
121
|
+
temp_zip.write(zip_content)
|
|
122
|
+
temp_zip_path = temp_zip.name
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
# Extract the ZIP file
|
|
126
|
+
extract_dir.mkdir(parents=True, exist_ok=True)
|
|
127
|
+
with zipfile.ZipFile(temp_zip_path, "r") as zip_ref:
|
|
128
|
+
zip_ref.extractall(extract_dir)
|
|
129
|
+
finally:
|
|
130
|
+
# Clean up temporary file
|
|
131
|
+
os.unlink(temp_zip_path)
|
|
132
|
+
|
|
133
|
+
def get_extension_path(
|
|
134
|
+
self, url_or_id: str, extension_name: str | None = None
|
|
135
|
+
) -> Path:
|
|
136
|
+
"""Download and extract Chrome extension, returning the path to the extracted directory.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
url_or_id: Chrome Web Store URL or extension ID
|
|
140
|
+
extension_name: Optional name for the extension directory
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Path to the extracted extension directory
|
|
144
|
+
"""
|
|
145
|
+
extension_id = self.extract_extension_id(url_or_id)
|
|
146
|
+
|
|
147
|
+
if extension_name is None:
|
|
148
|
+
extension_name = extension_id
|
|
149
|
+
|
|
150
|
+
extension_dir = self.cache_dir / extension_name
|
|
151
|
+
|
|
152
|
+
# Check if extension is already downloaded and extracted
|
|
153
|
+
if extension_dir.exists() and (extension_dir / "manifest.json").exists():
|
|
154
|
+
print(f"✅ Chrome extension already cached: {extension_dir}")
|
|
155
|
+
return extension_dir
|
|
156
|
+
|
|
157
|
+
print(f"🔽 Downloading Chrome extension {extension_id}...")
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
# Download the CRX file
|
|
161
|
+
crx_content = self.download_crx(extension_id)
|
|
162
|
+
|
|
163
|
+
# Clean up existing directory if it exists
|
|
164
|
+
if extension_dir.exists():
|
|
165
|
+
shutil.rmtree(extension_dir)
|
|
166
|
+
|
|
167
|
+
# Extract the CRX file
|
|
168
|
+
self.extract_crx_to_directory(crx_content, extension_dir)
|
|
169
|
+
|
|
170
|
+
# Verify extraction worked
|
|
171
|
+
if not (extension_dir / "manifest.json").exists():
|
|
172
|
+
raise ValueError("Extension extraction failed - no manifest.json found")
|
|
173
|
+
|
|
174
|
+
print(f"✅ Chrome extension downloaded and extracted: {extension_dir}")
|
|
175
|
+
return extension_dir
|
|
176
|
+
|
|
177
|
+
except Exception as e:
|
|
178
|
+
warnings.warn(f"Failed to download Chrome extension {extension_id}: {e}")
|
|
179
|
+
if extension_dir.exists():
|
|
180
|
+
shutil.rmtree(extension_dir)
|
|
181
|
+
raise
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def download_cpp_devtools_extension() -> Path | None:
|
|
185
|
+
"""Download the C++ DevTools Support (DWARF) extension.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Path to the extracted extension directory, or None if download failed
|
|
189
|
+
"""
|
|
190
|
+
# C++ DevTools Support (DWARF) extension
|
|
191
|
+
extension_url = "https://chromewebstore.google.com/detail/cc++-devtools-support-dwa/pdcpmagijalfljmkmjngeonclgbbannb"
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
downloader = ChromeExtensionDownloader()
|
|
195
|
+
return downloader.get_extension_path(extension_url, "cpp-devtools-support")
|
|
196
|
+
except Exception as e:
|
|
197
|
+
warnings.warn(f"Failed to download C++ DevTools Support extension: {e}")
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
if __name__ == "__main__":
|
|
202
|
+
# Test the downloader with the C++ DevTools Support extension
|
|
203
|
+
extension_path = download_cpp_devtools_extension()
|
|
204
|
+
if extension_path:
|
|
205
|
+
print(f"Extension downloaded to: {extension_path}")
|
|
206
|
+
else:
|
|
207
|
+
print("Failed to download extension")
|