sonolus.py 0.8.0__py3-none-any.whl → 0.9.0__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.
Potentially problematic release.
This version of sonolus.py might be problematic. Click here for more details.
- sonolus/backend/ir.py +32 -2
- sonolus/backend/optimize/copy_coalesce.py +5 -1
- sonolus/backend/optimize/flow.py +14 -0
- sonolus/backend/optimize/inlining.py +1 -1
- sonolus/backend/optimize/liveness.py +6 -4
- sonolus/build/cli.py +18 -50
- sonolus/build/compile.py +42 -4
- sonolus/build/dev_server.py +222 -0
- sonolus/build/engine.py +23 -2
- sonolus/build/project.py +13 -4
- sonolus/script/array.py +2 -1
- sonolus/script/bucket.py +11 -9
- sonolus/script/debug.py +21 -6
- sonolus/script/internal/impl.py +8 -4
- sonolus/script/interval.py +17 -6
- sonolus/script/num.py +8 -6
- sonolus/script/project.py +15 -2
- sonolus/script/quad.py +2 -0
- sonolus/script/record.py +9 -0
- sonolus/script/runtime.py +3 -2
- sonolus/script/sprite.py +8 -0
- sonolus/script/vec.py +31 -14
- {sonolus_py-0.8.0.dist-info → sonolus_py-0.9.0.dist-info}/METADATA +1 -1
- {sonolus_py-0.8.0.dist-info → sonolus_py-0.9.0.dist-info}/RECORD +27 -26
- {sonolus_py-0.8.0.dist-info → sonolus_py-0.9.0.dist-info}/WHEEL +0 -0
- {sonolus_py-0.8.0.dist-info → sonolus_py-0.9.0.dist-info}/entry_points.txt +0 -0
- {sonolus_py-0.8.0.dist-info → sonolus_py-0.9.0.dist-info}/licenses/LICENSE +0 -0
sonolus/backend/ir.py
CHANGED
|
@@ -5,12 +5,25 @@ type IRExpr = IRConst | IRPureInstr | IRGet
|
|
|
5
5
|
type IRStmt = IRExpr | IRInstr | IRSet
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
_IR_CONST_CACHE_START = -5
|
|
9
|
+
_IR_CONST_CACHE_STOP = 257
|
|
10
|
+
|
|
11
|
+
|
|
8
12
|
class IRConst:
|
|
13
|
+
__slots__ = ("value",)
|
|
14
|
+
|
|
9
15
|
value: float | int
|
|
10
16
|
|
|
17
|
+
def __new__(cls, value):
|
|
18
|
+
if float(value).is_integer():
|
|
19
|
+
int_value = int(value)
|
|
20
|
+
if _IR_CONST_CACHE_START <= int_value < _IR_CONST_CACHE_STOP:
|
|
21
|
+
return _IR_CONST_CACHE[int_value - _IR_CONST_CACHE_START]
|
|
22
|
+
else:
|
|
23
|
+
return _create_raw_const(int_value)
|
|
24
|
+
return super().__new__(cls)
|
|
25
|
+
|
|
11
26
|
def __init__(self, value: float):
|
|
12
|
-
if isinstance(value, bool):
|
|
13
|
-
value = int(value)
|
|
14
27
|
self.value = value
|
|
15
28
|
|
|
16
29
|
def __repr__(self):
|
|
@@ -26,7 +39,18 @@ class IRConst:
|
|
|
26
39
|
return hash(self.value)
|
|
27
40
|
|
|
28
41
|
|
|
42
|
+
def _create_raw_const(value: float | int) -> IRConst:
|
|
43
|
+
result = object.__new__(IRConst)
|
|
44
|
+
result.value = value
|
|
45
|
+
return result
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
_IR_CONST_CACHE = tuple(_create_raw_const(i) for i in range(_IR_CONST_CACHE_START, _IR_CONST_CACHE_STOP))
|
|
49
|
+
|
|
50
|
+
|
|
29
51
|
class IRPureInstr:
|
|
52
|
+
__slots__ = ("args", "array_defs", "defs", "is_array_init", "live", "op", "uses", "visited")
|
|
53
|
+
|
|
30
54
|
op: Op
|
|
31
55
|
args: list[IRExpr]
|
|
32
56
|
|
|
@@ -49,6 +73,8 @@ class IRPureInstr:
|
|
|
49
73
|
|
|
50
74
|
|
|
51
75
|
class IRInstr:
|
|
76
|
+
__slots__ = ("args", "array_defs", "defs", "is_array_init", "live", "op", "uses", "visited")
|
|
77
|
+
|
|
52
78
|
op: Op
|
|
53
79
|
args: list[IRExpr]
|
|
54
80
|
|
|
@@ -70,6 +96,8 @@ class IRInstr:
|
|
|
70
96
|
|
|
71
97
|
|
|
72
98
|
class IRGet:
|
|
99
|
+
__slots__ = ("array_defs", "defs", "is_array_init", "live", "place", "uses", "visited")
|
|
100
|
+
|
|
73
101
|
place: Place
|
|
74
102
|
|
|
75
103
|
def __init__(self, place: Place):
|
|
@@ -89,6 +117,8 @@ class IRGet:
|
|
|
89
117
|
|
|
90
118
|
|
|
91
119
|
class IRSet:
|
|
120
|
+
__slots__ = ("array_defs", "defs", "is_array_init", "live", "place", "uses", "value", "visited")
|
|
121
|
+
|
|
92
122
|
place: Place
|
|
93
123
|
value: IRExpr | IRInstr
|
|
94
124
|
|
|
@@ -69,10 +69,14 @@ class CopyCoalesce(CompilerPass):
|
|
|
69
69
|
def get_interference(self, entry: BasicBlock) -> dict[TempBlock, set[TempBlock]]:
|
|
70
70
|
result = {}
|
|
71
71
|
for block in traverse_cfg_preorder(entry):
|
|
72
|
-
for stmt in
|
|
72
|
+
for stmt in block.statements:
|
|
73
73
|
live = {p for p in get_live(stmt) if isinstance(p, TempBlock) and p.size == 1}
|
|
74
74
|
for place in live:
|
|
75
75
|
result.setdefault(place, set()).update(live - {place})
|
|
76
|
+
if not isinstance(block.test, IRConst):
|
|
77
|
+
live = {p for p in get_live(block.test) if isinstance(p, TempBlock) and p.size == 1}
|
|
78
|
+
for place in live:
|
|
79
|
+
result.setdefault(place, set()).update(live - {place})
|
|
76
80
|
return result
|
|
77
81
|
|
|
78
82
|
def get_copies(self, entry: BasicBlock) -> dict[TempBlock, set[TempBlock]]:
|
sonolus/backend/optimize/flow.py
CHANGED
|
@@ -140,6 +140,20 @@ def cfg_to_mermaid(entry: BasicBlock):
|
|
|
140
140
|
return f"graph\n{body}"
|
|
141
141
|
|
|
142
142
|
|
|
143
|
+
def hash_cfg(entry: BasicBlock) -> int:
|
|
144
|
+
# Note: not stable between Python runs due to hash randomization
|
|
145
|
+
block_indexes = {block: i for i, block in enumerate(traverse_cfg_preorder(entry))}
|
|
146
|
+
|
|
147
|
+
h = 0
|
|
148
|
+
for block, index in block_indexes.items():
|
|
149
|
+
outgoing = [(edge.cond, block_indexes.get(edge.dst, -1)) for edge in block.outgoing]
|
|
150
|
+
h = hash(
|
|
151
|
+
(h, index, tuple(block.statements), block.test, tuple(sorted(outgoing, key=lambda x: (x[0] is None, x[0]))))
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
return h
|
|
155
|
+
|
|
156
|
+
|
|
143
157
|
def cfg_to_text(entry: BasicBlock) -> str:
|
|
144
158
|
def indent(iterable, prefix=" "):
|
|
145
159
|
for line in iterable:
|
|
@@ -50,7 +50,7 @@ class InlineVars(CompilerPass):
|
|
|
50
50
|
# Update the definition if it's a Get from another SSAPlace until we reach a definition that is not a Get
|
|
51
51
|
while defn and isinstance(defn, IRGet) and isinstance(defn.place, SSAPlace):
|
|
52
52
|
canonical_definitions[p] = defn
|
|
53
|
-
defn = definitions.get(defn.place
|
|
53
|
+
defn = definitions.get(defn.place) # Can be None if it's a phi
|
|
54
54
|
canonical_defn = canonical_definitions[p]
|
|
55
55
|
if (
|
|
56
56
|
use_counts.get(p, 0) > 0
|
|
@@ -31,8 +31,9 @@ class LivenessAnalysis(CompilerPass):
|
|
|
31
31
|
statement.defs = self.get_defs(statement)
|
|
32
32
|
statement.is_array_init = False # True if this may be the first assignment to an array
|
|
33
33
|
statement.array_defs = self.get_array_defs(statement)
|
|
34
|
-
block.test
|
|
35
|
-
|
|
34
|
+
if not isinstance(block.test, IRConst):
|
|
35
|
+
block.test.live = set()
|
|
36
|
+
block.test.uses = self.get_uses(block.test, set())
|
|
36
37
|
self.preprocess_arrays(entry)
|
|
37
38
|
|
|
38
39
|
def process(self, entry: BasicBlock):
|
|
@@ -72,8 +73,9 @@ class LivenessAnalysis(CompilerPass):
|
|
|
72
73
|
for place in block.live_out
|
|
73
74
|
if not (isinstance(place, TempBlock) and place.size > 1 and place not in block.array_defs_out)
|
|
74
75
|
}
|
|
75
|
-
block.test
|
|
76
|
-
|
|
76
|
+
if not isinstance(block.test, IRConst):
|
|
77
|
+
block.test.live.update(live)
|
|
78
|
+
live.update(block.test.uses)
|
|
77
79
|
for statement in reversed(block.statements):
|
|
78
80
|
statement.live.update(live)
|
|
79
81
|
if self.can_skip(statement, live):
|
sonolus/build/cli.py
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import argparse
|
|
2
|
-
import contextlib
|
|
3
|
-
import http.server
|
|
4
2
|
import importlib
|
|
5
3
|
import json
|
|
6
4
|
import shutil
|
|
7
|
-
import socket
|
|
8
|
-
import socketserver
|
|
9
5
|
import sys
|
|
10
6
|
from pathlib import Path
|
|
11
7
|
from time import perf_counter
|
|
8
|
+
from types import ModuleType
|
|
12
9
|
|
|
13
10
|
from sonolus.backend.excepthook import print_simple_traceback
|
|
14
11
|
from sonolus.backend.optimize.optimize import FAST_PASSES, MINIMAL_PASSES, STANDARD_PASSES
|
|
12
|
+
from sonolus.build.compile import CompileCache
|
|
13
|
+
from sonolus.build.dev_server import run_server
|
|
15
14
|
from sonolus.build.engine import no_gil, package_engine, validate_engine
|
|
16
15
|
from sonolus.build.level import package_level_data
|
|
17
16
|
from sonolus.build.project import build_project_to_collection, get_project_schema
|
|
@@ -35,8 +34,10 @@ def find_default_module() -> str | None:
|
|
|
35
34
|
return potential_modules[0] if len(potential_modules) == 1 else None
|
|
36
35
|
|
|
37
36
|
|
|
38
|
-
def import_project(module_path: str) -> Project | None:
|
|
37
|
+
def import_project(module_path: str) -> tuple[Project, ModuleType, set[str]] | tuple[None, None, None]:
|
|
39
38
|
try:
|
|
39
|
+
initial_modules = set(sys.modules)
|
|
40
|
+
|
|
40
41
|
current_dir = Path.cwd()
|
|
41
42
|
if current_dir not in sys.path:
|
|
42
43
|
sys.path.insert(0, str(current_dir))
|
|
@@ -44,8 +45,8 @@ def import_project(module_path: str) -> Project | None:
|
|
|
44
45
|
project = None
|
|
45
46
|
|
|
46
47
|
try:
|
|
47
|
-
|
|
48
|
-
project = getattr(
|
|
48
|
+
project_module = importlib.import_module(module_path)
|
|
49
|
+
project = getattr(project_module, "project", None)
|
|
49
50
|
except ImportError as e:
|
|
50
51
|
if not str(e).endswith(f"'{module_path}'"):
|
|
51
52
|
# It's an error from the module itself
|
|
@@ -61,9 +62,9 @@ def import_project(module_path: str) -> Project | None:
|
|
|
61
62
|
|
|
62
63
|
if project is None:
|
|
63
64
|
print(f"Error: No Project instance found in module {module_path} or {module_path}.project")
|
|
64
|
-
return None
|
|
65
|
+
return None, None, None
|
|
65
66
|
|
|
66
|
-
return project
|
|
67
|
+
return project, project_module, initial_modules
|
|
67
68
|
except Exception as e:
|
|
68
69
|
print(f"Error: Failed to import project: {e}")
|
|
69
70
|
raise e from None
|
|
@@ -87,51 +88,15 @@ def validate_project(project: Project, config: BuildConfig):
|
|
|
87
88
|
validate_engine(project.engine.data, config)
|
|
88
89
|
|
|
89
90
|
|
|
90
|
-
def build_collection(project: Project, build_dir: Path, config: BuildConfig | None):
|
|
91
|
+
def build_collection(project: Project, build_dir: Path, config: BuildConfig | None, cache: CompileCache | None = None):
|
|
91
92
|
site_dir = build_dir / "site"
|
|
92
93
|
shutil.rmtree(site_dir, ignore_errors=True)
|
|
93
94
|
site_dir.mkdir(parents=True, exist_ok=True)
|
|
94
95
|
|
|
95
|
-
collection = build_project_to_collection(project, config)
|
|
96
|
+
collection = build_project_to_collection(project, config, cache=cache)
|
|
96
97
|
collection.write(site_dir)
|
|
97
98
|
|
|
98
99
|
|
|
99
|
-
def get_local_ips():
|
|
100
|
-
hostname = socket.gethostname()
|
|
101
|
-
local_ips = []
|
|
102
|
-
|
|
103
|
-
with contextlib.suppress(socket.gaierror):
|
|
104
|
-
local_ips.append(socket.gethostbyname(socket.getfqdn()))
|
|
105
|
-
|
|
106
|
-
try:
|
|
107
|
-
for info in socket.getaddrinfo(hostname, None):
|
|
108
|
-
ip = info[4][0]
|
|
109
|
-
if not ip.startswith("127.") and ":" not in ip:
|
|
110
|
-
local_ips.append(ip)
|
|
111
|
-
except socket.gaierror:
|
|
112
|
-
pass
|
|
113
|
-
|
|
114
|
-
return sorted(set(local_ips))
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def run_server(base_dir: Path, port: int = 8000):
|
|
118
|
-
class DirectoryHandler(http.server.SimpleHTTPRequestHandler):
|
|
119
|
-
def __init__(self, *args, **kwargs):
|
|
120
|
-
super().__init__(*args, directory=str(base_dir), **kwargs)
|
|
121
|
-
|
|
122
|
-
with socketserver.TCPServer(("", port), DirectoryHandler) as httpd:
|
|
123
|
-
local_ips = get_local_ips()
|
|
124
|
-
print(f"Server started on port {port}")
|
|
125
|
-
print("Available on:")
|
|
126
|
-
for ip in local_ips:
|
|
127
|
-
print(f" http://{ip}:{port}")
|
|
128
|
-
try:
|
|
129
|
-
httpd.serve_forever()
|
|
130
|
-
except KeyboardInterrupt:
|
|
131
|
-
print("\nStopping server...")
|
|
132
|
-
httpd.shutdown()
|
|
133
|
-
|
|
134
|
-
|
|
135
100
|
def get_config(args: argparse.Namespace) -> BuildConfig:
|
|
136
101
|
if hasattr(args, "optimize_minimal") and args.optimize_minimal:
|
|
137
102
|
optimization_passes = MINIMAL_PASSES
|
|
@@ -236,7 +201,7 @@ def main():
|
|
|
236
201
|
print("Python JIT is enabled")
|
|
237
202
|
|
|
238
203
|
start_time = perf_counter()
|
|
239
|
-
project = import_project(args.module)
|
|
204
|
+
project, project_module, core_module_names = import_project(args.module)
|
|
240
205
|
end_time = perf_counter()
|
|
241
206
|
if project is None:
|
|
242
207
|
sys.exit(1)
|
|
@@ -254,10 +219,13 @@ def main():
|
|
|
254
219
|
build_dir = Path(args.build_dir)
|
|
255
220
|
start_time = perf_counter()
|
|
256
221
|
config = get_config(args)
|
|
257
|
-
|
|
222
|
+
cache = CompileCache()
|
|
223
|
+
build_collection(project, build_dir, config, cache=cache)
|
|
258
224
|
end_time = perf_counter()
|
|
259
225
|
print(f"Build finished in {end_time - start_time:.2f}s")
|
|
260
|
-
run_server(
|
|
226
|
+
run_server(
|
|
227
|
+
build_dir / "site", args.port, project_module.__name__, core_module_names, build_dir, config, cache
|
|
228
|
+
)
|
|
261
229
|
elif args.command == "schema":
|
|
262
230
|
print(json.dumps(get_project_schema(project), indent=2))
|
|
263
231
|
elif args.command == "check":
|
sonolus/build/compile.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
from collections.abc import Callable, Sequence
|
|
2
2
|
from concurrent.futures import Executor, Future
|
|
3
|
+
from threading import Event, Lock
|
|
3
4
|
|
|
4
5
|
from sonolus.backend.finalize import cfg_to_engine_node
|
|
5
6
|
from sonolus.backend.ir import IRConst, IRInstr
|
|
6
7
|
from sonolus.backend.mode import Mode
|
|
8
|
+
from sonolus.backend.node import EngineNode
|
|
7
9
|
from sonolus.backend.ops import Op
|
|
8
|
-
from sonolus.backend.optimize.flow import BasicBlock
|
|
10
|
+
from sonolus.backend.optimize.flow import BasicBlock, hash_cfg
|
|
9
11
|
from sonolus.backend.optimize.optimize import STANDARD_PASSES
|
|
10
12
|
from sonolus.backend.optimize.passes import CompilerPass, OptimizerConfig, run_passes
|
|
11
13
|
from sonolus.backend.visitor import compile_and_call_at_definition
|
|
@@ -25,6 +27,31 @@ from sonolus.script.internal.error import CompilationError
|
|
|
25
27
|
from sonolus.script.num import _is_num
|
|
26
28
|
|
|
27
29
|
|
|
30
|
+
class CompileCache:
|
|
31
|
+
def __init__(self):
|
|
32
|
+
self._cache = {}
|
|
33
|
+
self._lock = Lock()
|
|
34
|
+
self._event = Event()
|
|
35
|
+
|
|
36
|
+
def get(self, entry_hash: int) -> EngineNode | None:
|
|
37
|
+
with self._lock:
|
|
38
|
+
if entry_hash not in self._cache:
|
|
39
|
+
self._cache[entry_hash] = None # Mark as being compiled
|
|
40
|
+
return None
|
|
41
|
+
while True:
|
|
42
|
+
with self._lock:
|
|
43
|
+
node = self._cache[entry_hash]
|
|
44
|
+
if node is not None:
|
|
45
|
+
return node
|
|
46
|
+
self._event.wait()
|
|
47
|
+
|
|
48
|
+
def set(self, entry_hash: int, node: EngineNode) -> None:
|
|
49
|
+
with self._lock:
|
|
50
|
+
self._cache[entry_hash] = node
|
|
51
|
+
self._event.set()
|
|
52
|
+
self._event.clear()
|
|
53
|
+
|
|
54
|
+
|
|
28
55
|
def compile_mode(
|
|
29
56
|
mode: Mode,
|
|
30
57
|
rom: ReadOnlyMemory,
|
|
@@ -33,6 +60,7 @@ def compile_mode(
|
|
|
33
60
|
passes: Sequence[CompilerPass] | None = None,
|
|
34
61
|
thread_pool: Executor | None = None,
|
|
35
62
|
validate_only: bool = False,
|
|
63
|
+
cache: CompileCache | None = None,
|
|
36
64
|
) -> dict:
|
|
37
65
|
if passes is None:
|
|
38
66
|
passes = STANDARD_PASSES
|
|
@@ -63,9 +91,19 @@ def compile_mode(
|
|
|
63
91
|
return cb_info.name, {"index": 0, "order": cb_order}
|
|
64
92
|
else:
|
|
65
93
|
return cb_info.name, 0
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
94
|
+
|
|
95
|
+
if cache is not None:
|
|
96
|
+
cfg_hash = hash_cfg(cfg)
|
|
97
|
+
node = cache.get(cfg_hash)
|
|
98
|
+
if node is None:
|
|
99
|
+
optimized_cfg = run_passes(cfg, passes, OptimizerConfig(mode=mode, callback=cb_info.name))
|
|
100
|
+
node = cfg_to_engine_node(optimized_cfg)
|
|
101
|
+
cache.set(cfg_hash, node)
|
|
102
|
+
node_index = nodes.add(node)
|
|
103
|
+
else:
|
|
104
|
+
optimized_cfg = run_passes(cfg, passes, OptimizerConfig(mode=mode, callback=cb_info.name))
|
|
105
|
+
node = cfg_to_engine_node(optimized_cfg)
|
|
106
|
+
node_index = nodes.add(node)
|
|
69
107
|
|
|
70
108
|
if arch is not None:
|
|
71
109
|
cb_order = getattr(cb, "_callback_order_", 0)
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import contextlib
|
|
5
|
+
import http.server
|
|
6
|
+
import importlib
|
|
7
|
+
import queue
|
|
8
|
+
import shlex
|
|
9
|
+
import socket
|
|
10
|
+
import socketserver
|
|
11
|
+
import sys
|
|
12
|
+
import textwrap
|
|
13
|
+
import threading
|
|
14
|
+
import traceback
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from time import perf_counter
|
|
18
|
+
from typing import TYPE_CHECKING, Protocol
|
|
19
|
+
|
|
20
|
+
from sonolus.backend.excepthook import print_simple_traceback
|
|
21
|
+
from sonolus.backend.utils import get_function, get_functions, get_tree_from_file
|
|
22
|
+
from sonolus.build.compile import CompileCache
|
|
23
|
+
from sonolus.script.internal.error import CompilationError
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from sonolus.script.project import BuildConfig
|
|
27
|
+
|
|
28
|
+
HELP_TEXT = """
|
|
29
|
+
[r]ebuild
|
|
30
|
+
[q]uit
|
|
31
|
+
""".strip()
|
|
32
|
+
|
|
33
|
+
HELP_TEXT = textwrap.dedent(HELP_TEXT)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Command(Protocol):
|
|
37
|
+
def execute(
|
|
38
|
+
self,
|
|
39
|
+
project_module_name: str,
|
|
40
|
+
core_module_names: set[str],
|
|
41
|
+
build_dir: Path,
|
|
42
|
+
config: BuildConfig,
|
|
43
|
+
cache: CompileCache,
|
|
44
|
+
) -> None: ...
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class RebuildCommand:
|
|
49
|
+
def execute(
|
|
50
|
+
self,
|
|
51
|
+
project_module_name: str,
|
|
52
|
+
core_module_names: set[str],
|
|
53
|
+
build_dir: Path,
|
|
54
|
+
config: BuildConfig,
|
|
55
|
+
cache: CompileCache,
|
|
56
|
+
):
|
|
57
|
+
from sonolus.build.cli import build_collection
|
|
58
|
+
|
|
59
|
+
for module_name in tuple(sys.modules):
|
|
60
|
+
if module_name not in core_module_names:
|
|
61
|
+
del sys.modules[module_name]
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
project_module = importlib.import_module(project_module_name)
|
|
65
|
+
except Exception:
|
|
66
|
+
print(traceback.format_exc())
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
get_function.cache_clear()
|
|
70
|
+
get_tree_from_file.cache_clear()
|
|
71
|
+
get_functions.cache_clear()
|
|
72
|
+
print("Rebuilding...")
|
|
73
|
+
try:
|
|
74
|
+
start_time = perf_counter()
|
|
75
|
+
build_collection(project_module.project, build_dir, config, cache=cache)
|
|
76
|
+
end_time = perf_counter()
|
|
77
|
+
print(f"Rebuild completed in {end_time - start_time:.2f} seconds")
|
|
78
|
+
except CompilationError:
|
|
79
|
+
exc_info = sys.exc_info()
|
|
80
|
+
print_simple_traceback(*exc_info)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class ExitCommand:
|
|
85
|
+
def execute(
|
|
86
|
+
self,
|
|
87
|
+
project_module_name: str,
|
|
88
|
+
core_module_names: set[str],
|
|
89
|
+
build_dir: Path,
|
|
90
|
+
config: BuildConfig,
|
|
91
|
+
cache: CompileCache,
|
|
92
|
+
):
|
|
93
|
+
print("Exiting...")
|
|
94
|
+
sys.exit(0)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def parse_dev_command(command_line: str) -> Command | None:
|
|
98
|
+
parser = argparse.ArgumentParser(prog="", add_help=False, exit_on_error=False)
|
|
99
|
+
subparsers = parser.add_subparsers(dest="cmd")
|
|
100
|
+
|
|
101
|
+
subparsers.add_parser("rebuild", aliases=["r"])
|
|
102
|
+
subparsers.add_parser("quit", aliases=["q"])
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
args = parser.parse_args(shlex.split(command_line))
|
|
106
|
+
if args.cmd in {"rebuild", "r"}:
|
|
107
|
+
return RebuildCommand()
|
|
108
|
+
elif args.cmd in {"quit", "q"}:
|
|
109
|
+
return ExitCommand()
|
|
110
|
+
return None
|
|
111
|
+
except argparse.ArgumentError:
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def command_input_thread(command_queue: queue.Queue, stop_event: threading.Event, prompt_event: threading.Event):
|
|
116
|
+
print(f"\nAvailable commands:\n{HELP_TEXT}")
|
|
117
|
+
|
|
118
|
+
while not stop_event.is_set():
|
|
119
|
+
try:
|
|
120
|
+
prompt_event.wait()
|
|
121
|
+
prompt_event.clear()
|
|
122
|
+
|
|
123
|
+
if stop_event.is_set():
|
|
124
|
+
break
|
|
125
|
+
|
|
126
|
+
print("\n> ", end="", flush=True)
|
|
127
|
+
command_line = input()
|
|
128
|
+
if command_line.strip():
|
|
129
|
+
cmd = parse_dev_command(command_line.strip())
|
|
130
|
+
if cmd:
|
|
131
|
+
command_queue.put(cmd)
|
|
132
|
+
if isinstance(cmd, ExitCommand):
|
|
133
|
+
break
|
|
134
|
+
else:
|
|
135
|
+
print(f"Unknown command. Available commands:\n{HELP_TEXT}")
|
|
136
|
+
# Show prompt again
|
|
137
|
+
prompt_event.set()
|
|
138
|
+
except EOFError:
|
|
139
|
+
break
|
|
140
|
+
except Exception as e:
|
|
141
|
+
print(f"Error reading command: {e}")
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def get_local_ips():
|
|
146
|
+
hostname = socket.gethostname()
|
|
147
|
+
local_ips = []
|
|
148
|
+
|
|
149
|
+
with contextlib.suppress(socket.gaierror):
|
|
150
|
+
local_ips.append(socket.gethostbyname(socket.getfqdn()))
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
for info in socket.getaddrinfo(hostname, None):
|
|
154
|
+
ip = info[4][0]
|
|
155
|
+
if not ip.startswith("127.") and ":" not in ip:
|
|
156
|
+
local_ips.append(ip)
|
|
157
|
+
except socket.gaierror:
|
|
158
|
+
pass
|
|
159
|
+
|
|
160
|
+
return sorted(set(local_ips))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def run_server(
|
|
164
|
+
base_dir: Path,
|
|
165
|
+
port: int,
|
|
166
|
+
project_module_name: str | None,
|
|
167
|
+
core_module_names: set[str] | None,
|
|
168
|
+
build_dir: Path,
|
|
169
|
+
config: BuildConfig,
|
|
170
|
+
cache: CompileCache,
|
|
171
|
+
):
|
|
172
|
+
interactive = project_module_name is not None and core_module_names is not None
|
|
173
|
+
|
|
174
|
+
class DirectoryHandler(http.server.SimpleHTTPRequestHandler):
|
|
175
|
+
def __init__(self, *args, **kwargs):
|
|
176
|
+
super().__init__(*args, directory=str(base_dir), **kwargs)
|
|
177
|
+
|
|
178
|
+
def log_message(self, fmt, *args):
|
|
179
|
+
sys.stdout.write("\r\033[K") # Clear line
|
|
180
|
+
sys.stdout.write(f"{self.address_string()} - - [{self.log_date_time_string()}] {fmt % args}\n")
|
|
181
|
+
if interactive:
|
|
182
|
+
sys.stdout.write("> ")
|
|
183
|
+
sys.stdout.flush()
|
|
184
|
+
|
|
185
|
+
with socketserver.TCPServer(("", port), DirectoryHandler) as httpd:
|
|
186
|
+
local_ips = get_local_ips()
|
|
187
|
+
print(f"Server started on port {port}")
|
|
188
|
+
print("Available on:")
|
|
189
|
+
for ip in local_ips:
|
|
190
|
+
print(f" http://{ip}:{port}")
|
|
191
|
+
|
|
192
|
+
if interactive:
|
|
193
|
+
threading.Thread(target=httpd.serve_forever, daemon=True).start()
|
|
194
|
+
|
|
195
|
+
command_queue = queue.Queue()
|
|
196
|
+
stop_event = threading.Event()
|
|
197
|
+
prompt_event = threading.Event()
|
|
198
|
+
input_thread = threading.Thread(
|
|
199
|
+
target=command_input_thread, args=(command_queue, stop_event, prompt_event), daemon=True
|
|
200
|
+
)
|
|
201
|
+
input_thread.start()
|
|
202
|
+
|
|
203
|
+
prompt_event.set()
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
while True:
|
|
207
|
+
try:
|
|
208
|
+
cmd = command_queue.get(timeout=0.5)
|
|
209
|
+
cmd.execute(project_module_name, core_module_names, build_dir, config, cache)
|
|
210
|
+
prompt_event.set()
|
|
211
|
+
except queue.Empty:
|
|
212
|
+
continue
|
|
213
|
+
except KeyboardInterrupt:
|
|
214
|
+
print("\nStopping server...")
|
|
215
|
+
sys.exit(0)
|
|
216
|
+
finally:
|
|
217
|
+
httpd.shutdown()
|
|
218
|
+
stop_event.set()
|
|
219
|
+
prompt_event.set()
|
|
220
|
+
input_thread.join()
|
|
221
|
+
else:
|
|
222
|
+
httpd.serve_forever()
|