-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbuild_helpers.py
More file actions
166 lines (141 loc) · 5.35 KB
/
build_helpers.py
File metadata and controls
166 lines (141 loc) · 5.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
#!/usr/bin/env python3
"""Pure helpers for assembling the single-file `cgr.py` artifact."""
from __future__ import annotations
import ast
import hashlib
import re
from pathlib import Path
MODULE_ORDER = [
"common.py",
"lexer.py",
"ast_nodes.py",
"parser_cg.py",
"parser_cgr.py",
"repo.py",
"resolver.py",
"executor.py",
"state.py",
"commands.py",
"dot.py",
"visualize.py",
"serve.py",
"cli.py",
]
SHEBANG = "#!/usr/bin/env python3\n"
SHIPPING_DOCSTRING = '''"""
cgr (CommandGraph) v3 — DSL with nested dependencies, reusable templates, and
a copy-on-read repository.
Usage:
cgr plan FILE [--repo DIR] [-v]
cgr apply FILE [--repo DIR] [--dry-run] [--parallel N]
cgr validate FILE [--repo DIR] [-q] [--json]
cgr dot FILE [--repo DIR]
cgr visualize FILE [--repo DIR] [-o FILE.html] [--state .state/FILE.state]
cgr state show|test|reset|set|drop|diff FILE [STEP|FILE2] [VALUE]
cgr report FILE [--format table|json|csv] [-o FILE] [--keys K1,K2]
cgr repo index [--repo DIR]
cgr serve [FILE] [--port 8420] [--no-open]
cgr version
Requires: Python 3.9+. No external dependencies.
"""\n'''
VISUALIZER_MARKER = "GENERATED: VISUALIZER TEMPLATE"
IDE_MARKER = "GENERATED: EMBEDDED IDE HTML"
INTERNAL_IMPORT_RE = re.compile(
r"^(from cgr_src(?:\.| import)|import cgr_src(?:\.|$)).*$",
re.MULTILINE,
)
DUNDER_ALL_RE = re.compile(
r"\n__all__ = \[\n"
r"(?: .*\n)+?"
r"\]\n?",
re.MULTILINE,
)
def _repo_root() -> Path:
return Path(__file__).resolve().parent
def _replace_marked_section(content: str, marker: str, replacement: str) -> str:
begin = f"# BEGIN {marker}"
end = f"# END {marker}"
start = content.find(begin)
if start < 0:
raise ValueError(f"Missing marker start: {begin}")
finish = content.find(end, start)
if finish < 0:
raise ValueError(f"Missing marker end: {end}")
finish += len(end)
if finish < len(content) and content[finish] == "\n":
finish += 1
return content[:start] + replacement.rstrip() + "\n" + content[finish:]
def _load_visualizer_function(path: Path) -> str:
source = path.read_text()
module = ast.parse(source, filename=str(path))
lines = source.splitlines()
for node in module.body:
if isinstance(node, ast.FunctionDef) and node.name == "generate_html":
fn_lines = lines[node.lineno - 1 : node.end_lineno]
fn_source = "\n".join(fn_lines) + "\n"
return fn_source.replace("def generate_html(", "def _build_html(", 1)
raise ValueError(f"generate_html() not found in {path}")
def strip_module(path: str) -> str:
"""Strip docstring, internal imports, __all__, and __future__ from a module."""
source = (_repo_root() / path).read_text()
tree = ast.parse(source)
body = source
if (
tree.body
and isinstance(tree.body[0], ast.Expr)
and isinstance(getattr(tree.body[0], "value", None), ast.Constant)
and isinstance(tree.body[0].value.value, str)
):
lines = source.splitlines(keepends=True)
body = "".join(lines[tree.body[0].end_lineno :])
body = INTERNAL_IMPORT_RE.sub("", body)
body = DUNDER_ALL_RE.sub("\n", body)
body = re.sub(r"^(from __future__ import annotations\n)+", "", body, count=1)
return body.strip("\n") + "\n"
def source_hash(source_root: str) -> str:
"""SHA-256 hash of all source modules in topological order."""
root = _repo_root() / source_root
digest = hashlib.sha256()
for name in MODULE_ORDER:
digest.update((root / name).read_bytes())
return digest.hexdigest()[:16]
def assemble_header(source_root: str) -> str:
"""Generate the shipping header: shebang, docstring, future import, hash."""
return (
SHEBANG
+ SHIPPING_DOCSTRING
+ "from __future__ import annotations\n"
+ f"# Built from source hash: {source_hash(source_root)}\n"
)
def inject_visualizer(content: str, viz_path: str) -> str:
"""Inject the generated visualizer function into the marked section."""
path = _repo_root() / viz_path
replacement = "\n".join(
[
f"# BEGIN {VISUALIZER_MARKER}",
f"# Generated by build_helpers.py from {path.name}. Do not edit by hand.",
_load_visualizer_function(path).rstrip(),
f"# END {VISUALIZER_MARKER}",
]
)
return _replace_marked_section(content, VISUALIZER_MARKER, replacement)
def inject_ide(content: str, ide_path: str) -> str:
"""Inject embedded IDE HTML into the marked section."""
path = _repo_root() / ide_path
replacement = "\n".join(
[
f"# BEGIN {IDE_MARKER}",
f"# Generated by build_helpers.py from {path.name}. Do not edit by hand.",
f"_EMBEDDED_IDE_HTML = {path.read_text()!r}",
f"# END {IDE_MARKER}",
]
)
return _replace_marked_section(content, IDE_MARKER, replacement)
def full_build() -> str:
"""Run the complete assembly pipeline and return the artifact content."""
source_root = "cgr_src"
sections = [strip_module(f"{source_root}/{name}") for name in MODULE_ORDER]
content = assemble_header(source_root) + "\n".join(s.rstrip("\n") for s in sections) + "\n"
content = inject_visualizer(content, "visualize_template.py")
content = inject_ide(content, "ide.html")
return content