Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sage
Path: blob/develop/tools/update-meson.py
4013 views
1
#!/usr/bin/env python3
2
# /// script
3
# requires-python = ">=3.11"
4
# dependencies = [
5
# "meson",
6
# ]
7
# ///
8
# See README.md for more details
9
10
import argparse
11
import os
12
from argparse import Namespace
13
from pathlib import Path
14
15
from mesonbuild import mlog
16
from mesonbuild.ast import (
17
AstPrinter,
18
AstVisitor,
19
)
20
from mesonbuild.ast.interpreter import MethodNode
21
from mesonbuild.mformat import (
22
run as meson_format,
23
)
24
from mesonbuild.mparser import (
25
AssignmentNode,
26
BaseNode,
27
DictNode,
28
SymbolNode,
29
)
30
from mesonbuild.rewriter import (
31
ArgumentNode,
32
ArrayNode,
33
FunctionNode,
34
Rewriter,
35
StringNode,
36
Token,
37
)
38
39
# Get target directory from command line arguments
40
parser = argparse.ArgumentParser()
41
parser.add_argument(
42
"sourcedir", help="Source directory", nargs="?", default=".", type=Path
43
)
44
options = parser.parse_args()
45
46
47
class AstPython(AstVisitor):
48
install_sources_calls: list[MethodNode] = []
49
extension_data: list[AssignmentNode] = []
50
51
def visit_MethodNode(self, node: MethodNode) -> None:
52
if node.name.value == "install_sources":
53
self.install_sources_calls += [node]
54
return super().visit_MethodNode(node)
55
56
def visit_AssignmentNode(self, node: AssignmentNode) -> None:
57
if node.var_name.value in ["extension_data", "extension_data_cpp"]:
58
self.extension_data += [node]
59
return super().visit_AssignmentNode(node)
60
61
62
# Utility function to get a list of the sources from a node
63
def arg_list_from_node(n):
64
args = []
65
if isinstance(n, FunctionNode) or isinstance(n, MethodNode):
66
args = list(n.args.arguments)
67
# if 'func_name' in n and n.func_name.value in BUILD_TARGET_FUNCTIONS:
68
# args.pop(0)
69
elif isinstance(n, ArrayNode):
70
args = n.args.arguments
71
elif isinstance(n, ArgumentNode):
72
args = n.arguments
73
return args
74
75
76
def _symbol(val: str) -> SymbolNode:
77
return SymbolNode(Token("", "", 0, 0, 0, (0, 0), val))
78
79
80
def update_python_sources(self: Rewriter, visitor: AstPython):
81
for target in visitor.install_sources_calls:
82
ignored_files = {'cmdline.py'}
83
# Generate the current source list
84
src_list: list[str] = []
85
for arg in arg_list_from_node(target):
86
if isinstance(arg, StringNode):
87
src_list += [arg.value]
88
89
folder = Path(target.filename).parent
90
python_files = sorted(
91
list(folder.glob("*.py")) + list(folder.glob('*.pxd'))
92
) # + list(folder.glob('*.pxd')) + list(folder.glob('*.h')))
93
94
to_append: list[StringNode] = []
95
for file in python_files:
96
file_name = file.name
97
if file_name in src_list or file_name in ignored_files:
98
continue
99
token = Token("string", target.filename, 0, 0, 0, None, file_name)
100
to_append += [StringNode(token)]
101
102
# Get all deleted files
103
to_remove = []
104
for src in src_list:
105
if not folder.joinpath(src).exists():
106
to_remove += [src]
107
108
if not to_append and not to_remove:
109
continue
110
111
# Update the source list
112
target.args.arguments = sorted(
113
[
114
arg
115
for arg in target.args.arguments
116
if not (isinstance(arg, StringNode) and arg.value in to_remove)
117
]
118
+ to_append,
119
key=lambda x: x.value,
120
)
121
122
# Mark the node as modified
123
if target not in self.modified_nodes:
124
self.modified_nodes += [target]
125
126
ext_data: dict[Path, list[str]] = {}
127
for target in visitor.extension_data:
128
folder = Path(target.filename).parent
129
# Generate the current source dict
130
src_list: dict[str, BaseNode] = {}
131
if isinstance(target.value, DictNode):
132
src_list.update({k.value: v for k, v in target.value.args.kwargs.items()})
133
ext_data.setdefault(folder, [])
134
ext_data[folder] += src_list.keys()
135
136
for target in visitor.extension_data:
137
if target.var_name.value != "extension_data":
138
continue
139
folder = Path(target.filename).parent
140
src_list = ext_data[folder]
141
142
cython_files = sorted(list(folder.glob("*.pyx")))
143
# Some cython files are compiled in a special way, so we don't want to add them
144
special_cython_files = {
145
"bliss.pyx",
146
"mcqd.pyx",
147
"tdlib.pyx",
148
}
149
cython_files = [x for x in cython_files if x.name not in special_cython_files]
150
# Add all cython files that are not in the source list
151
for file in cython_files:
152
file_name = file.stem
153
if file_name in src_list:
154
continue
155
token = Token("string", target.filename, 0, 0, 0, None, file_name)
156
arg = ArgumentNode(Token("", target.filename, 0, 0, 0, None, "[]"))
157
arg.append(
158
StringNode(Token("string", target.filename, 0, 0, 0, None, file.name))
159
)
160
func = FunctionNode(_symbol("files"), _symbol("("), arg, _symbol(")"))
161
target.value.args.kwargs.update({StringNode(token): func})
162
if target not in self.modified_nodes:
163
self.modified_nodes += [target]
164
165
166
def apply_changes(self: Rewriter):
167
assert all(
168
hasattr(x, "lineno") and hasattr(x, "colno") and hasattr(x, "filename")
169
for x in self.modified_nodes
170
)
171
assert all(
172
hasattr(x, "lineno") and hasattr(x, "colno") and hasattr(x, "filename")
173
for x in self.to_remove_nodes
174
)
175
assert all(
176
isinstance(x, (ArrayNode, FunctionNode, MethodNode, AssignmentNode))
177
for x in self.modified_nodes
178
)
179
assert all(
180
isinstance(x, (ArrayNode, AssignmentNode, FunctionNode))
181
for x in self.to_remove_nodes
182
)
183
# Sort based on line and column in reversed order
184
work_nodes = [{"node": x, "action": "modify"} for x in self.modified_nodes]
185
work_nodes += [{"node": x, "action": "rm"} for x in self.to_remove_nodes]
186
work_nodes = sorted(
187
work_nodes, key=lambda x: (x["node"].lineno, x["node"].colno), reverse=True
188
)
189
work_nodes += [{"node": x, "action": "add"} for x in self.to_add_nodes]
190
191
# Generating the new replacement string
192
str_list = []
193
for i in work_nodes:
194
new_data = ""
195
if i["action"] == "modify" or i["action"] == "add":
196
printer = AstPrinter()
197
i["node"].accept(printer)
198
printer.post_process()
199
new_data = printer.result.strip()
200
data = {
201
"file": i["node"].filename,
202
"str": new_data,
203
"node": i["node"],
204
"action": i["action"],
205
}
206
str_list += [data]
207
208
# Load build files
209
files = {}
210
for i in str_list:
211
if i["file"] in files:
212
continue
213
fpath = os.path.realpath(os.path.join(self.sourcedir, i["file"]))
214
fdata = ""
215
# Create an empty file if it does not exist
216
if not os.path.exists(fpath):
217
with open(fpath, "w", encoding="utf-8"):
218
pass
219
with open(fpath, encoding="utf-8") as fp:
220
fdata = fp.read()
221
222
# Generate line offsets numbers
223
m_lines = fdata.splitlines(True)
224
offset = 0
225
line_offsets = []
226
for j in m_lines:
227
line_offsets += [offset]
228
offset += len(j)
229
230
files[i["file"]] = {"path": fpath, "raw": fdata, "offsets": line_offsets}
231
232
# Replace in source code
233
def remove_node(i):
234
offsets = files[i["file"]]["offsets"]
235
raw = files[i["file"]]["raw"]
236
node = i["node"]
237
line = node.lineno - 1
238
col = node.colno
239
if isinstance(node, MethodNode):
240
# The new data contains the source object as well
241
col = node.source_object.colno
242
elif isinstance(node, AssignmentNode):
243
col = node.var_name.colno
244
start = offsets[line] + col
245
end = start
246
if isinstance(node, (ArrayNode, FunctionNode, MethodNode)):
247
end = offsets[node.end_lineno - 1] + node.end_colno
248
elif isinstance(node, AssignmentNode):
249
end = offsets[node.value.end_lineno - 1] + node.value.end_colno
250
251
# Only removal is supported for assignments
252
elif isinstance(node, AssignmentNode) and i["action"] == "rm":
253
if isinstance(node.value, (ArrayNode, FunctionNode, MethodNode)):
254
remove_node(
255
{"file": i["file"], "str": "", "node": node.value, "action": "rm"}
256
)
257
raw = files[i["file"]]["raw"]
258
while raw[end] != "=":
259
end += 1
260
end += 1 # Handle the '='
261
while raw[end] in {" ", "\n", "\t"}:
262
end += 1
263
264
files[i["file"]]["raw"] = raw[:start] + i["str"] + raw[end:]
265
266
for i in str_list:
267
if i["action"] in {"modify", "rm"}:
268
remove_node(i)
269
elif i["action"] == "add":
270
files[i["file"]]["raw"] += i["str"] + "\n"
271
272
# Write the files back
273
for key, val in files.items():
274
mlog.log("Rewriting", mlog.yellow(key))
275
with open(val["path"], "w", encoding="utf-8") as fp:
276
fp.write(val["raw"])
277
278
279
# Monkey patch the apply_changes method until https://github.com/mesonbuild/meson/pull/12899 is merged
280
Rewriter.apply_changes = apply_changes
281
# Monkey patch the update_python_sources method until this is upstreamed
282
Rewriter.process_update_python_sources = update_python_sources
283
284
rewriter = Rewriter(options.sourcedir)
285
visitor = AstPython()
286
rewriter.interpreter.visitors += [visitor]
287
rewriter.analyze_meson()
288
rewriter.process_update_python_sources(visitor)
289
rewriter.apply_changes()
290
rewriter.print_info()
291
292
# Run meson format
293
meson_format(
294
Namespace(
295
sources=[options.sourcedir],
296
inplace=True,
297
recursive=True,
298
output=None,
299
configuration=options.sourcedir / "meson.format",
300
editor_config=None,
301
)
302
)
303
304