fastled 1.2.96__py3-none-any.whl → 1.2.98__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 +1 -1
- fastled/client_server.py +513 -513
- fastled/compile_server_impl.py +355 -355
- fastled/docker_manager.py +987 -987
- fastled/open_browser.py +137 -137
- fastled/print_filter.py +190 -190
- fastled/project_init.py +129 -129
- fastled/server_flask.py +2 -8
- fastled/site/build.py +449 -449
- fastled/string_diff.py +82 -82
- {fastled-1.2.96.dist-info → fastled-1.2.98.dist-info}/METADATA +471 -471
- {fastled-1.2.96.dist-info → fastled-1.2.98.dist-info}/RECORD +16 -16
- {fastled-1.2.96.dist-info → fastled-1.2.98.dist-info}/WHEEL +0 -0
- {fastled-1.2.96.dist-info → fastled-1.2.98.dist-info}/entry_points.txt +0 -0
- {fastled-1.2.96.dist-info → fastled-1.2.98.dist-info}/licenses/LICENSE +0 -0
- {fastled-1.2.96.dist-info → fastled-1.2.98.dist-info}/top_level.txt +0 -0
fastled/open_browser.py
CHANGED
@@ -1,137 +1,137 @@
|
|
1
|
-
import atexit
|
2
|
-
import random
|
3
|
-
import sys
|
4
|
-
import time
|
5
|
-
import weakref
|
6
|
-
from multiprocessing import Process
|
7
|
-
from pathlib import Path
|
8
|
-
|
9
|
-
from fastled.server_flask import run_flask_in_thread
|
10
|
-
|
11
|
-
DEFAULT_PORT = 8089 # different than live version.
|
12
|
-
PYTHON_EXE = sys.executable
|
13
|
-
|
14
|
-
# Use a weak reference set to track processes without preventing garbage collection
|
15
|
-
_WEAK_CLEANUP_SET = weakref.WeakSet()
|
16
|
-
|
17
|
-
|
18
|
-
def add_cleanup(proc: Process) -> None:
|
19
|
-
"""Add a process to the cleanup list using weak references"""
|
20
|
-
_WEAK_CLEANUP_SET.add(proc)
|
21
|
-
|
22
|
-
# Register a cleanup function that checks if the process is still alive
|
23
|
-
def cleanup_if_alive():
|
24
|
-
if proc.is_alive():
|
25
|
-
try:
|
26
|
-
proc.terminate()
|
27
|
-
proc.join(timeout=1.0)
|
28
|
-
if proc.is_alive():
|
29
|
-
proc.kill()
|
30
|
-
except Exception:
|
31
|
-
pass
|
32
|
-
|
33
|
-
atexit.register(cleanup_if_alive)
|
34
|
-
|
35
|
-
|
36
|
-
def is_port_free(port: int) -> bool:
|
37
|
-
"""Check if a port is free"""
|
38
|
-
import httpx
|
39
|
-
|
40
|
-
try:
|
41
|
-
response = httpx.get(f"http://localhost:{port}", timeout=1)
|
42
|
-
response.raise_for_status()
|
43
|
-
return False
|
44
|
-
except (httpx.HTTPError, httpx.ConnectError):
|
45
|
-
return True
|
46
|
-
|
47
|
-
|
48
|
-
def find_free_port(start_port: int) -> int:
|
49
|
-
"""Find a free port starting at start_port"""
|
50
|
-
for port in range(start_port, start_port + 100, 2):
|
51
|
-
if is_port_free(port):
|
52
|
-
print(f"Found free port: {port}")
|
53
|
-
return port
|
54
|
-
else:
|
55
|
-
print(f"Port {port} is in use, finding next")
|
56
|
-
raise ValueError("Could not find a free port")
|
57
|
-
|
58
|
-
|
59
|
-
def wait_for_server(port: int, timeout: int = 10) -> None:
|
60
|
-
"""Wait for the server to start."""
|
61
|
-
from httpx import get
|
62
|
-
|
63
|
-
future_time = time.time() + timeout
|
64
|
-
while future_time > time.time():
|
65
|
-
try:
|
66
|
-
url = f"http://localhost:{port}"
|
67
|
-
# print(f"Waiting for server to start at {url}")
|
68
|
-
response = get(url, timeout=1)
|
69
|
-
if response.status_code == 200:
|
70
|
-
return
|
71
|
-
except Exception:
|
72
|
-
continue
|
73
|
-
raise TimeoutError("Could not connect to server")
|
74
|
-
|
75
|
-
|
76
|
-
def spawn_http_server(
|
77
|
-
fastled_js: Path,
|
78
|
-
compile_server_port: int,
|
79
|
-
port: int | None = None,
|
80
|
-
open_browser: bool = True,
|
81
|
-
) -> Process:
|
82
|
-
|
83
|
-
if port is not None and not is_port_free(port):
|
84
|
-
raise ValueError(f"Port {port} was specified but in use")
|
85
|
-
if port is None:
|
86
|
-
offset = random.randint(0, 100)
|
87
|
-
port = find_free_port(DEFAULT_PORT + offset)
|
88
|
-
|
89
|
-
# port: int,
|
90
|
-
# cwd: Path,
|
91
|
-
# compile_server_port: int,
|
92
|
-
# certfile: Path | None = None,
|
93
|
-
# keyfile: Path | None = None,
|
94
|
-
|
95
|
-
proc = Process(
|
96
|
-
target=run_flask_in_thread,
|
97
|
-
args=(port, fastled_js, compile_server_port),
|
98
|
-
daemon=True,
|
99
|
-
)
|
100
|
-
add_cleanup(proc)
|
101
|
-
proc.start()
|
102
|
-
|
103
|
-
# Add to cleanup set with weak reference
|
104
|
-
add_cleanup(proc)
|
105
|
-
|
106
|
-
wait_for_server(port)
|
107
|
-
if open_browser:
|
108
|
-
print(f"Opening browser to http://localhost:{port}")
|
109
|
-
import webbrowser
|
110
|
-
|
111
|
-
webbrowser.open(
|
112
|
-
url=f"http://localhost:{port}",
|
113
|
-
new=1,
|
114
|
-
autoraise=True,
|
115
|
-
)
|
116
|
-
return proc
|
117
|
-
|
118
|
-
|
119
|
-
if __name__ == "__main__":
|
120
|
-
import argparse
|
121
|
-
|
122
|
-
parser = argparse.ArgumentParser(
|
123
|
-
description="Open a browser to the fastled_js directory"
|
124
|
-
)
|
125
|
-
parser.add_argument(
|
126
|
-
"fastled_js", type=Path, help="Path to the fastled_js directory"
|
127
|
-
)
|
128
|
-
parser.add_argument(
|
129
|
-
"--port",
|
130
|
-
type=int,
|
131
|
-
default=DEFAULT_PORT,
|
132
|
-
help=f"Port to run the server on (default: {DEFAULT_PORT})",
|
133
|
-
)
|
134
|
-
args = parser.parse_args()
|
135
|
-
|
136
|
-
proc = spawn_http_server(args.fastled_js, args.port, open_browser=True)
|
137
|
-
proc.join()
|
1
|
+
import atexit
|
2
|
+
import random
|
3
|
+
import sys
|
4
|
+
import time
|
5
|
+
import weakref
|
6
|
+
from multiprocessing import Process
|
7
|
+
from pathlib import Path
|
8
|
+
|
9
|
+
from fastled.server_flask import run_flask_in_thread
|
10
|
+
|
11
|
+
DEFAULT_PORT = 8089 # different than live version.
|
12
|
+
PYTHON_EXE = sys.executable
|
13
|
+
|
14
|
+
# Use a weak reference set to track processes without preventing garbage collection
|
15
|
+
_WEAK_CLEANUP_SET = weakref.WeakSet()
|
16
|
+
|
17
|
+
|
18
|
+
def add_cleanup(proc: Process) -> None:
|
19
|
+
"""Add a process to the cleanup list using weak references"""
|
20
|
+
_WEAK_CLEANUP_SET.add(proc)
|
21
|
+
|
22
|
+
# Register a cleanup function that checks if the process is still alive
|
23
|
+
def cleanup_if_alive():
|
24
|
+
if proc.is_alive():
|
25
|
+
try:
|
26
|
+
proc.terminate()
|
27
|
+
proc.join(timeout=1.0)
|
28
|
+
if proc.is_alive():
|
29
|
+
proc.kill()
|
30
|
+
except Exception:
|
31
|
+
pass
|
32
|
+
|
33
|
+
atexit.register(cleanup_if_alive)
|
34
|
+
|
35
|
+
|
36
|
+
def is_port_free(port: int) -> bool:
|
37
|
+
"""Check if a port is free"""
|
38
|
+
import httpx
|
39
|
+
|
40
|
+
try:
|
41
|
+
response = httpx.get(f"http://localhost:{port}", timeout=1)
|
42
|
+
response.raise_for_status()
|
43
|
+
return False
|
44
|
+
except (httpx.HTTPError, httpx.ConnectError):
|
45
|
+
return True
|
46
|
+
|
47
|
+
|
48
|
+
def find_free_port(start_port: int) -> int:
|
49
|
+
"""Find a free port starting at start_port"""
|
50
|
+
for port in range(start_port, start_port + 100, 2):
|
51
|
+
if is_port_free(port):
|
52
|
+
print(f"Found free port: {port}")
|
53
|
+
return port
|
54
|
+
else:
|
55
|
+
print(f"Port {port} is in use, finding next")
|
56
|
+
raise ValueError("Could not find a free port")
|
57
|
+
|
58
|
+
|
59
|
+
def wait_for_server(port: int, timeout: int = 10) -> None:
|
60
|
+
"""Wait for the server to start."""
|
61
|
+
from httpx import get
|
62
|
+
|
63
|
+
future_time = time.time() + timeout
|
64
|
+
while future_time > time.time():
|
65
|
+
try:
|
66
|
+
url = f"http://localhost:{port}"
|
67
|
+
# print(f"Waiting for server to start at {url}")
|
68
|
+
response = get(url, timeout=1)
|
69
|
+
if response.status_code == 200:
|
70
|
+
return
|
71
|
+
except Exception:
|
72
|
+
continue
|
73
|
+
raise TimeoutError("Could not connect to server")
|
74
|
+
|
75
|
+
|
76
|
+
def spawn_http_server(
|
77
|
+
fastled_js: Path,
|
78
|
+
compile_server_port: int,
|
79
|
+
port: int | None = None,
|
80
|
+
open_browser: bool = True,
|
81
|
+
) -> Process:
|
82
|
+
|
83
|
+
if port is not None and not is_port_free(port):
|
84
|
+
raise ValueError(f"Port {port} was specified but in use")
|
85
|
+
if port is None:
|
86
|
+
offset = random.randint(0, 100)
|
87
|
+
port = find_free_port(DEFAULT_PORT + offset)
|
88
|
+
|
89
|
+
# port: int,
|
90
|
+
# cwd: Path,
|
91
|
+
# compile_server_port: int,
|
92
|
+
# certfile: Path | None = None,
|
93
|
+
# keyfile: Path | None = None,
|
94
|
+
|
95
|
+
proc = Process(
|
96
|
+
target=run_flask_in_thread,
|
97
|
+
args=(port, fastled_js, compile_server_port),
|
98
|
+
daemon=True,
|
99
|
+
)
|
100
|
+
add_cleanup(proc)
|
101
|
+
proc.start()
|
102
|
+
|
103
|
+
# Add to cleanup set with weak reference
|
104
|
+
add_cleanup(proc)
|
105
|
+
|
106
|
+
wait_for_server(port)
|
107
|
+
if open_browser:
|
108
|
+
print(f"Opening browser to http://localhost:{port}")
|
109
|
+
import webbrowser
|
110
|
+
|
111
|
+
webbrowser.open(
|
112
|
+
url=f"http://localhost:{port}",
|
113
|
+
new=1,
|
114
|
+
autoraise=True,
|
115
|
+
)
|
116
|
+
return proc
|
117
|
+
|
118
|
+
|
119
|
+
if __name__ == "__main__":
|
120
|
+
import argparse
|
121
|
+
|
122
|
+
parser = argparse.ArgumentParser(
|
123
|
+
description="Open a browser to the fastled_js directory"
|
124
|
+
)
|
125
|
+
parser.add_argument(
|
126
|
+
"fastled_js", type=Path, help="Path to the fastled_js directory"
|
127
|
+
)
|
128
|
+
parser.add_argument(
|
129
|
+
"--port",
|
130
|
+
type=int,
|
131
|
+
default=DEFAULT_PORT,
|
132
|
+
help=f"Port to run the server on (default: {DEFAULT_PORT})",
|
133
|
+
)
|
134
|
+
args = parser.parse_args()
|
135
|
+
|
136
|
+
proc = spawn_http_server(args.fastled_js, args.port, open_browser=True)
|
137
|
+
proc.join()
|
fastled/print_filter.py
CHANGED
@@ -1,190 +1,190 @@
|
|
1
|
-
import re
|
2
|
-
import zlib
|
3
|
-
from abc import ABC, abstractmethod
|
4
|
-
from dataclasses import dataclass
|
5
|
-
from enum import Enum
|
6
|
-
|
7
|
-
|
8
|
-
class PrintFilter(ABC):
|
9
|
-
"""Abstract base class for filtering text output."""
|
10
|
-
|
11
|
-
def __init__(self, echo: bool = True) -> None:
|
12
|
-
self.echo = echo
|
13
|
-
|
14
|
-
@abstractmethod
|
15
|
-
def filter(self, text: str) -> str:
|
16
|
-
"""Filter the text according to implementation-specific rules."""
|
17
|
-
pass
|
18
|
-
|
19
|
-
def print(self, text: str | bytes) -> str:
|
20
|
-
"""Prints the text to the console after filtering."""
|
21
|
-
if isinstance(text, bytes):
|
22
|
-
text = text.decode("utf-8")
|
23
|
-
text = self.filter(text)
|
24
|
-
if self.echo:
|
25
|
-
print(text, end="")
|
26
|
-
return text
|
27
|
-
|
28
|
-
|
29
|
-
def _handle_ino_cpp(line: str) -> str:
|
30
|
-
if ".ino.cpp" in line[0:30]:
|
31
|
-
# Extract the filename without path and extension
|
32
|
-
match = re.search(r"src/([^/]+)\.ino\.cpp", line)
|
33
|
-
if match:
|
34
|
-
filename = match.group(1)
|
35
|
-
# Replace with examples/Filename/Filename.ino format
|
36
|
-
line = line.replace(
|
37
|
-
f"src/{filename}.ino.cpp", f"examples/{filename}/{filename}.ino"
|
38
|
-
)
|
39
|
-
else:
|
40
|
-
# Fall back to simple extension replacement if regex doesn't match
|
41
|
-
line = line.replace(".ino.cpp", ".ino")
|
42
|
-
return line
|
43
|
-
|
44
|
-
|
45
|
-
def _handle_fastled_src(line: str) -> str:
|
46
|
-
return line.replace("fastled/src", "src")
|
47
|
-
|
48
|
-
|
49
|
-
class PrintFilterDefault(PrintFilter):
|
50
|
-
"""Provides default filtering for FastLED output."""
|
51
|
-
|
52
|
-
def filter(self, text: str) -> str:
|
53
|
-
return text
|
54
|
-
|
55
|
-
|
56
|
-
class PrintFilterFastled(PrintFilter):
|
57
|
-
"""Provides filtering for FastLED output so that source files match up with local names."""
|
58
|
-
|
59
|
-
def __init__(self, echo: bool = True) -> None:
|
60
|
-
super().__init__(echo)
|
61
|
-
self.build_started = False
|
62
|
-
# self.compile_link_active = False
|
63
|
-
# self.compile_link_filter:
|
64
|
-
|
65
|
-
def filter(self, text: str) -> str:
|
66
|
-
lines = text.splitlines()
|
67
|
-
out: list[str] = []
|
68
|
-
for line in lines:
|
69
|
-
## DEBUG DO NOT SUBMIT
|
70
|
-
# print(line)
|
71
|
-
if "# WASM is building" in line:
|
72
|
-
self.build_started = True
|
73
|
-
line = _handle_fastled_src(
|
74
|
-
line
|
75
|
-
) # Always convert fastled/src to src for file matchups.
|
76
|
-
if self.build_started or " error: " in line:
|
77
|
-
line = _handle_ino_cpp(line)
|
78
|
-
out.append(line)
|
79
|
-
text = "\n".join(out)
|
80
|
-
return text
|
81
|
-
|
82
|
-
|
83
|
-
class CompileOrLink(Enum):
|
84
|
-
COMPILE = "compile"
|
85
|
-
LINK = "link"
|
86
|
-
|
87
|
-
|
88
|
-
@dataclass
|
89
|
-
class BuildArtifact:
|
90
|
-
timestamp: float
|
91
|
-
input_artifact: str | None
|
92
|
-
output_artifact: str | None
|
93
|
-
build_flags: str
|
94
|
-
compile_or_link: CompileOrLink
|
95
|
-
hash: int
|
96
|
-
|
97
|
-
def __str__(self) -> str:
|
98
|
-
return f"{self.timestamp} {self.output_artifact} {self.build_flags} {self.compile_or_link} {self.hash}"
|
99
|
-
|
100
|
-
@staticmethod
|
101
|
-
def parse(input_str: str) -> "BuildArtifact | None":
|
102
|
-
"""
|
103
|
-
Parse a single build-log line of the form:
|
104
|
-
"<timestamp> ... <some .cpp or .h file> ... <flags>"
|
105
|
-
|
106
|
-
Returns a BuildArtifact, or None if parsing failed.
|
107
|
-
"""
|
108
|
-
return _parse(input_str)
|
109
|
-
|
110
|
-
|
111
|
-
class TokenFilter(ABC):
|
112
|
-
@abstractmethod
|
113
|
-
def extract(self, tokens: list[str]) -> str | None:
|
114
|
-
"""
|
115
|
-
Scan `tokens`, remove any tokens this filter is responsible for,
|
116
|
-
and return the extracted string (or None if not found/invalid).
|
117
|
-
"""
|
118
|
-
...
|
119
|
-
|
120
|
-
|
121
|
-
class TimestampFilter(TokenFilter):
|
122
|
-
def extract(self, tokens: list[str]) -> str | None:
|
123
|
-
if not tokens:
|
124
|
-
return None
|
125
|
-
candidate = tokens[0]
|
126
|
-
try:
|
127
|
-
_ = float(candidate)
|
128
|
-
return tokens.pop(0)
|
129
|
-
except ValueError:
|
130
|
-
return None
|
131
|
-
|
132
|
-
|
133
|
-
class InputArtifactFilter(TokenFilter):
|
134
|
-
def extract(self, tokens: list[str]) -> str | None:
|
135
|
-
for i, tok in enumerate(tokens):
|
136
|
-
if tok.endswith(".cpp") or tok.endswith(".h"):
|
137
|
-
return tokens.pop(i)
|
138
|
-
return None
|
139
|
-
|
140
|
-
|
141
|
-
class OutputArtifactFilter(TokenFilter):
|
142
|
-
def extract(self, tokens: list[str]) -> str | None:
|
143
|
-
for i, tok in enumerate(tokens):
|
144
|
-
if tok == "-o" and i + 1 < len(tokens):
|
145
|
-
tokens.pop(i) # drop '-o'
|
146
|
-
return tokens.pop(i) # drop & return artifact
|
147
|
-
return None
|
148
|
-
|
149
|
-
|
150
|
-
class ActionFilter(TokenFilter):
|
151
|
-
def extract(self, tokens: list[str]) -> str | None:
|
152
|
-
if "-c" in tokens:
|
153
|
-
return CompileOrLink.COMPILE.value
|
154
|
-
return CompileOrLink.LINK.value
|
155
|
-
|
156
|
-
|
157
|
-
def _parse(line: str) -> BuildArtifact | None:
|
158
|
-
tokens = line.strip().split()
|
159
|
-
if not tokens:
|
160
|
-
return None
|
161
|
-
|
162
|
-
# instantiate in the order we need them
|
163
|
-
filters: list[TokenFilter] = [
|
164
|
-
TimestampFilter(),
|
165
|
-
InputArtifactFilter(),
|
166
|
-
OutputArtifactFilter(),
|
167
|
-
ActionFilter(),
|
168
|
-
]
|
169
|
-
|
170
|
-
# apply each filter
|
171
|
-
raw_ts = filters[0].extract(tokens)
|
172
|
-
raw_in = filters[1].extract(tokens)
|
173
|
-
raw_out = filters[2].extract(tokens)
|
174
|
-
raw_act = filters[3].extract(tokens)
|
175
|
-
|
176
|
-
if raw_ts is None or raw_in is None or raw_act is None:
|
177
|
-
return None
|
178
|
-
|
179
|
-
# the rest of `tokens` are the flags
|
180
|
-
flags_str = " ".join(tokens)
|
181
|
-
h = zlib.adler32(flags_str.encode("utf-8"))
|
182
|
-
|
183
|
-
return BuildArtifact(
|
184
|
-
timestamp=float(raw_ts),
|
185
|
-
input_artifact=raw_in,
|
186
|
-
output_artifact=raw_out,
|
187
|
-
build_flags=flags_str,
|
188
|
-
compile_or_link=CompileOrLink(raw_act),
|
189
|
-
hash=h,
|
190
|
-
)
|
1
|
+
import re
|
2
|
+
import zlib
|
3
|
+
from abc import ABC, abstractmethod
|
4
|
+
from dataclasses import dataclass
|
5
|
+
from enum import Enum
|
6
|
+
|
7
|
+
|
8
|
+
class PrintFilter(ABC):
|
9
|
+
"""Abstract base class for filtering text output."""
|
10
|
+
|
11
|
+
def __init__(self, echo: bool = True) -> None:
|
12
|
+
self.echo = echo
|
13
|
+
|
14
|
+
@abstractmethod
|
15
|
+
def filter(self, text: str) -> str:
|
16
|
+
"""Filter the text according to implementation-specific rules."""
|
17
|
+
pass
|
18
|
+
|
19
|
+
def print(self, text: str | bytes) -> str:
|
20
|
+
"""Prints the text to the console after filtering."""
|
21
|
+
if isinstance(text, bytes):
|
22
|
+
text = text.decode("utf-8")
|
23
|
+
text = self.filter(text)
|
24
|
+
if self.echo:
|
25
|
+
print(text, end="")
|
26
|
+
return text
|
27
|
+
|
28
|
+
|
29
|
+
def _handle_ino_cpp(line: str) -> str:
|
30
|
+
if ".ino.cpp" in line[0:30]:
|
31
|
+
# Extract the filename without path and extension
|
32
|
+
match = re.search(r"src/([^/]+)\.ino\.cpp", line)
|
33
|
+
if match:
|
34
|
+
filename = match.group(1)
|
35
|
+
# Replace with examples/Filename/Filename.ino format
|
36
|
+
line = line.replace(
|
37
|
+
f"src/{filename}.ino.cpp", f"examples/{filename}/{filename}.ino"
|
38
|
+
)
|
39
|
+
else:
|
40
|
+
# Fall back to simple extension replacement if regex doesn't match
|
41
|
+
line = line.replace(".ino.cpp", ".ino")
|
42
|
+
return line
|
43
|
+
|
44
|
+
|
45
|
+
def _handle_fastled_src(line: str) -> str:
|
46
|
+
return line.replace("fastled/src", "src")
|
47
|
+
|
48
|
+
|
49
|
+
class PrintFilterDefault(PrintFilter):
|
50
|
+
"""Provides default filtering for FastLED output."""
|
51
|
+
|
52
|
+
def filter(self, text: str) -> str:
|
53
|
+
return text
|
54
|
+
|
55
|
+
|
56
|
+
class PrintFilterFastled(PrintFilter):
|
57
|
+
"""Provides filtering for FastLED output so that source files match up with local names."""
|
58
|
+
|
59
|
+
def __init__(self, echo: bool = True) -> None:
|
60
|
+
super().__init__(echo)
|
61
|
+
self.build_started = False
|
62
|
+
# self.compile_link_active = False
|
63
|
+
# self.compile_link_filter:
|
64
|
+
|
65
|
+
def filter(self, text: str) -> str:
|
66
|
+
lines = text.splitlines()
|
67
|
+
out: list[str] = []
|
68
|
+
for line in lines:
|
69
|
+
## DEBUG DO NOT SUBMIT
|
70
|
+
# print(line)
|
71
|
+
if "# WASM is building" in line:
|
72
|
+
self.build_started = True
|
73
|
+
line = _handle_fastled_src(
|
74
|
+
line
|
75
|
+
) # Always convert fastled/src to src for file matchups.
|
76
|
+
if self.build_started or " error: " in line:
|
77
|
+
line = _handle_ino_cpp(line)
|
78
|
+
out.append(line)
|
79
|
+
text = "\n".join(out)
|
80
|
+
return text
|
81
|
+
|
82
|
+
|
83
|
+
class CompileOrLink(Enum):
|
84
|
+
COMPILE = "compile"
|
85
|
+
LINK = "link"
|
86
|
+
|
87
|
+
|
88
|
+
@dataclass
|
89
|
+
class BuildArtifact:
|
90
|
+
timestamp: float
|
91
|
+
input_artifact: str | None
|
92
|
+
output_artifact: str | None
|
93
|
+
build_flags: str
|
94
|
+
compile_or_link: CompileOrLink
|
95
|
+
hash: int
|
96
|
+
|
97
|
+
def __str__(self) -> str:
|
98
|
+
return f"{self.timestamp} {self.output_artifact} {self.build_flags} {self.compile_or_link} {self.hash}"
|
99
|
+
|
100
|
+
@staticmethod
|
101
|
+
def parse(input_str: str) -> "BuildArtifact | None":
|
102
|
+
"""
|
103
|
+
Parse a single build-log line of the form:
|
104
|
+
"<timestamp> ... <some .cpp or .h file> ... <flags>"
|
105
|
+
|
106
|
+
Returns a BuildArtifact, or None if parsing failed.
|
107
|
+
"""
|
108
|
+
return _parse(input_str)
|
109
|
+
|
110
|
+
|
111
|
+
class TokenFilter(ABC):
|
112
|
+
@abstractmethod
|
113
|
+
def extract(self, tokens: list[str]) -> str | None:
|
114
|
+
"""
|
115
|
+
Scan `tokens`, remove any tokens this filter is responsible for,
|
116
|
+
and return the extracted string (or None if not found/invalid).
|
117
|
+
"""
|
118
|
+
...
|
119
|
+
|
120
|
+
|
121
|
+
class TimestampFilter(TokenFilter):
|
122
|
+
def extract(self, tokens: list[str]) -> str | None:
|
123
|
+
if not tokens:
|
124
|
+
return None
|
125
|
+
candidate = tokens[0]
|
126
|
+
try:
|
127
|
+
_ = float(candidate)
|
128
|
+
return tokens.pop(0)
|
129
|
+
except ValueError:
|
130
|
+
return None
|
131
|
+
|
132
|
+
|
133
|
+
class InputArtifactFilter(TokenFilter):
|
134
|
+
def extract(self, tokens: list[str]) -> str | None:
|
135
|
+
for i, tok in enumerate(tokens):
|
136
|
+
if tok.endswith(".cpp") or tok.endswith(".h"):
|
137
|
+
return tokens.pop(i)
|
138
|
+
return None
|
139
|
+
|
140
|
+
|
141
|
+
class OutputArtifactFilter(TokenFilter):
|
142
|
+
def extract(self, tokens: list[str]) -> str | None:
|
143
|
+
for i, tok in enumerate(tokens):
|
144
|
+
if tok == "-o" and i + 1 < len(tokens):
|
145
|
+
tokens.pop(i) # drop '-o'
|
146
|
+
return tokens.pop(i) # drop & return artifact
|
147
|
+
return None
|
148
|
+
|
149
|
+
|
150
|
+
class ActionFilter(TokenFilter):
|
151
|
+
def extract(self, tokens: list[str]) -> str | None:
|
152
|
+
if "-c" in tokens:
|
153
|
+
return CompileOrLink.COMPILE.value
|
154
|
+
return CompileOrLink.LINK.value
|
155
|
+
|
156
|
+
|
157
|
+
def _parse(line: str) -> BuildArtifact | None:
|
158
|
+
tokens = line.strip().split()
|
159
|
+
if not tokens:
|
160
|
+
return None
|
161
|
+
|
162
|
+
# instantiate in the order we need them
|
163
|
+
filters: list[TokenFilter] = [
|
164
|
+
TimestampFilter(),
|
165
|
+
InputArtifactFilter(),
|
166
|
+
OutputArtifactFilter(),
|
167
|
+
ActionFilter(),
|
168
|
+
]
|
169
|
+
|
170
|
+
# apply each filter
|
171
|
+
raw_ts = filters[0].extract(tokens)
|
172
|
+
raw_in = filters[1].extract(tokens)
|
173
|
+
raw_out = filters[2].extract(tokens)
|
174
|
+
raw_act = filters[3].extract(tokens)
|
175
|
+
|
176
|
+
if raw_ts is None or raw_in is None or raw_act is None:
|
177
|
+
return None
|
178
|
+
|
179
|
+
# the rest of `tokens` are the flags
|
180
|
+
flags_str = " ".join(tokens)
|
181
|
+
h = zlib.adler32(flags_str.encode("utf-8"))
|
182
|
+
|
183
|
+
return BuildArtifact(
|
184
|
+
timestamp=float(raw_ts),
|
185
|
+
input_artifact=raw_in,
|
186
|
+
output_artifact=raw_out,
|
187
|
+
build_flags=flags_str,
|
188
|
+
compile_or_link=CompileOrLink(raw_act),
|
189
|
+
hash=h,
|
190
|
+
)
|