Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sage
Path: blob/develop/tools/update-meson.py
7377 views
1
#!/usr/bin/env python3
2
# /// script
3
# requires-python = ">=3.12"
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
doc_sources: list[MethodNode] = []
51
52
def visit_MethodNode(self, node: MethodNode) -> None:
53
if node.name.value == "install_sources":
54
self.install_sources_calls += [node]
55
return super().visit_MethodNode(node)
56
57
def visit_AssignmentNode(self, node: AssignmentNode) -> None:
58
if node.var_name.value in ["extension_data", "extension_data_cpp"]:
59
self.extension_data += [node]
60
elif node.var_name.value == "doc_sources":
61
self.doc_sources += [node]
62
return super().visit_AssignmentNode(node)
63
64
65
# Utility function to get a list of the sources from a node
66
def arg_list_from_node(n):
67
args = []
68
if isinstance(n, FunctionNode) or isinstance(n, MethodNode):
69
args = list(n.args.arguments)
70
# if 'func_name' in n and n.func_name.value in BUILD_TARGET_FUNCTIONS:
71
# args.pop(0)
72
elif isinstance(n, ArrayNode):
73
args = n.args.arguments
74
elif isinstance(n, ArgumentNode):
75
args = n.arguments
76
return args
77
78
79
def _symbol(val: str) -> SymbolNode:
80
return SymbolNode(Token("", "", 0, 0, 0, (0, 0), val))
81
82
83
def update_python_sources(self: Rewriter, visitor: AstPython):
84
for target in visitor.install_sources_calls:
85
ignored_files = {'cmdline.py'}
86
# Generate the current source list
87
src_list: list[str] = []
88
for arg in arg_list_from_node(target):
89
if isinstance(arg, StringNode):
90
src_list += [arg.value]
91
92
folder = Path(target.filename).parent
93
python_files = sorted(
94
list(folder.glob("*.py")) + list(folder.glob('*.pxd')) + list(folder.glob('*.pyx'))
95
) # + list(folder.glob('*.pxd')) + list(folder.glob('*.h')))
96
97
to_append: list[StringNode] = []
98
for file in python_files:
99
file_name = file.name
100
if file_name in src_list or file_name in ignored_files:
101
continue
102
token = Token("string", target.filename, 0, 0, 0, None, file_name)
103
to_append += [StringNode(token)]
104
105
# Get all deleted files
106
to_remove = []
107
for src in src_list:
108
if not folder.joinpath(src).exists():
109
to_remove += [src]
110
111
if not to_append and not to_remove:
112
continue
113
114
# Update the source list
115
target.args.arguments = sorted(
116
[
117
arg
118
for arg in target.args.arguments
119
if not (isinstance(arg, StringNode) and arg.value in to_remove)
120
]
121
+ to_append,
122
key=lambda x: x.value,
123
)
124
125
# Mark the node as modified
126
if target not in self.modified_nodes:
127
self.modified_nodes += [target]
128
129
ext_data: dict[Path, list[str]] = {}
130
for target in visitor.extension_data:
131
folder = Path(target.filename).parent
132
# Generate the current source dict
133
src_list: dict[str, BaseNode] = {}
134
if isinstance(target.value, DictNode):
135
src_list.update({k.value: v for k, v in target.value.args.kwargs.items()})
136
ext_data.setdefault(folder, [])
137
ext_data[folder] += src_list.keys()
138
139
for target in visitor.extension_data:
140
if target.var_name.value != "extension_data":
141
continue
142
folder = Path(target.filename).parent
143
src_list = ext_data[folder]
144
145
cython_files = sorted(list(folder.glob("*.pyx")))
146
# Some cython files are compiled in a special way, so we don't want to add them
147
special_cython_files = {
148
"bliss.pyx",
149
"mcqd.pyx",
150
"tdlib.pyx",
151
}
152
cython_files = [x for x in cython_files if x.name not in special_cython_files]
153
# Add all cython files that are not in the source list
154
for file in cython_files:
155
file_name = file.stem
156
if file_name in src_list:
157
continue
158
token = Token("string", target.filename, 0, 0, 0, None, file_name)
159
arg = ArgumentNode(Token("", target.filename, 0, 0, 0, None, "[]"))
160
arg.append(
161
StringNode(Token("string", target.filename, 0, 0, 0, None, file.name))
162
)
163
func = FunctionNode(_symbol("files"), _symbol("("), arg, _symbol(")"))
164
target.value.args.kwargs.update({StringNode(token): func})
165
if target not in self.modified_nodes:
166
self.modified_nodes += [target]
167
168
169
def update_doc_sources(self: Rewriter, visitor: AstPython):
170
doc_sources: dict[Path, list[str]] = {}
171
ignored_files = {'bootstrap', 'Makefile', 'meson.build'}
172
ignored_folders = {'__pycache__', 'sage'}
173
for target in visitor.doc_sources:
174
folder = Path(target.filename).parent
175
# Generate the current source dict
176
src_list: list[BaseNode] = []
177
if isinstance(target.value, ArrayNode):
178
src_list.extend(target.value.args.arguments)
179
doc_sources.setdefault(folder, [])
180
doc_sources[folder] += [x.value for x in src_list]
181
182
for target in visitor.doc_sources:
183
if target.var_name.value != "doc_sources":
184
continue
185
folder = Path(target.filename).parent
186
existing_sources: list[str] = doc_sources[folder]
187
# Add all files that are not in the source list
188
for file in folder.glob("*.*"):
189
file_name: str = file.name
190
if file_name in existing_sources:
191
continue
192
if file_name in ignored_files or file.suffix == ".pyc":
193
continue
194
existing_sources.append(file_name)
195
token = Token("string", target.filename, 0, 0, 0, None, file_name)
196
target.value.args.arguments.append(StringNode(token))
197
if target not in self.modified_nodes:
198
self.modified_nodes += [target]
199
# Remove all files that are no longer existing
200
for file in existing_sources:
201
if not (folder / file).exists():
202
existing_sources.remove(file)
203
token = next((x for x in target.value.args.arguments if getattr(x, "value", None) == file), None)
204
if token is not None:
205
target.value.args.arguments.remove(token)
206
if target not in self.modified_nodes:
207
self.modified_nodes += [target]
208
209
# Add all missing meson files in the src/doc folder
210
doc_folder = Path(options.sourcedir) / "src" / "doc"
211
# Delete all totally empty folders as pre-processing step
212
for folder, dirs, files in doc_folder.walk(top_down = False):
213
if not dirs and not files:
214
folder.rmdir()
215
216
for folder, dirs, files in doc_folder.walk():
217
if folder.name in ignored_folders or folder == doc_folder:
218
continue
219
files_to_add = {}
220
for file in files:
221
if file in ignored_files or file in doc_sources.get(folder, set()):
222
continue
223
files_to_add[file] = folder
224
if files_to_add or any(dir not in ignored_folders for dir in dirs):
225
# Create meson.build file
226
meson_build = Path(folder) / "meson.build"
227
if meson_build.exists():
228
continue
229
with open(meson_build, "w", encoding="utf-8") as f:
230
if files_to_add:
231
f.write("doc_sources = [\n")
232
for file in sorted(files_to_add):
233
f.write(f" '{file}',\n")
234
f.write("]\n")
235
f.write("\n")
236
f.write("foreach file : doc_sources\n")
237
f.write(" doc_src += fs.copyfile(file)\n")
238
f.write("endforeach\n")
239
f.write("\n")
240
for dir in dirs:
241
if dir in ignored_folders:
242
continue
243
f.write(f"subdir('{dir}')\n")
244
245
def apply_changes(self: Rewriter):
246
assert all(
247
hasattr(x, "lineno") and hasattr(x, "colno") and hasattr(x, "filename")
248
for x in self.modified_nodes
249
)
250
assert all(
251
hasattr(x, "lineno") and hasattr(x, "colno") and hasattr(x, "filename")
252
for x in self.to_remove_nodes
253
)
254
assert all(
255
isinstance(x, (ArrayNode, FunctionNode, MethodNode, AssignmentNode))
256
for x in self.modified_nodes
257
)
258
assert all(
259
isinstance(x, (ArrayNode, AssignmentNode, FunctionNode))
260
for x in self.to_remove_nodes
261
)
262
# Sort based on line and column in reversed order
263
work_nodes = [{"node": x, "action": "modify"} for x in self.modified_nodes]
264
work_nodes += [{"node": x, "action": "rm"} for x in self.to_remove_nodes]
265
work_nodes = sorted(
266
work_nodes, key=lambda x: (x["node"].lineno, x["node"].colno), reverse=True
267
)
268
work_nodes += [{"node": x, "action": "add"} for x in self.to_add_nodes]
269
270
# Generating the new replacement string
271
str_list = []
272
for i in work_nodes:
273
new_data = ""
274
if i["action"] == "modify" or i["action"] == "add":
275
printer = AstPrinter()
276
i["node"].accept(printer)
277
printer.post_process()
278
new_data = printer.result.strip()
279
data = {
280
"file": i["node"].filename,
281
"str": new_data,
282
"node": i["node"],
283
"action": i["action"],
284
}
285
str_list += [data]
286
287
# Load build files
288
files = {}
289
for i in str_list:
290
if i["file"] in files:
291
continue
292
fpath = os.path.realpath(os.path.join(self.sourcedir, i["file"]))
293
fdata = ""
294
# Create an empty file if it does not exist
295
if not os.path.exists(fpath):
296
with open(fpath, "w", encoding="utf-8"):
297
pass
298
with open(fpath, encoding="utf-8") as fp:
299
fdata = fp.read()
300
301
# Generate line offsets numbers
302
m_lines = fdata.splitlines(True)
303
offset = 0
304
line_offsets = []
305
for j in m_lines:
306
line_offsets += [offset]
307
offset += len(j)
308
309
files[i["file"]] = {"path": fpath, "raw": fdata, "offsets": line_offsets}
310
311
# Replace in source code
312
def remove_node(i):
313
offsets = files[i["file"]]["offsets"]
314
raw = files[i["file"]]["raw"]
315
node = i["node"]
316
line = node.lineno - 1
317
col = node.colno
318
if isinstance(node, MethodNode):
319
# The new data contains the source object as well
320
col = node.source_object.colno
321
elif isinstance(node, AssignmentNode):
322
col = node.var_name.colno
323
start = offsets[line] + col
324
end = start
325
if isinstance(node, (ArrayNode, FunctionNode, MethodNode)):
326
end = offsets[node.end_lineno - 1] + node.end_colno
327
elif isinstance(node, AssignmentNode):
328
end = offsets[node.value.end_lineno - 1] + node.value.end_colno
329
330
# Only removal is supported for assignments
331
elif isinstance(node, AssignmentNode) and i["action"] == "rm":
332
if isinstance(node.value, (ArrayNode, FunctionNode, MethodNode)):
333
remove_node(
334
{"file": i["file"], "str": "", "node": node.value, "action": "rm"}
335
)
336
raw = files[i["file"]]["raw"]
337
while raw[end] != "=":
338
end += 1
339
end += 1 # Handle the '='
340
while raw[end] in {" ", "\n", "\t"}:
341
end += 1
342
343
files[i["file"]]["raw"] = raw[:start] + i["str"] + raw[end:]
344
345
for i in str_list:
346
if i["action"] in {"modify", "rm"}:
347
remove_node(i)
348
elif i["action"] == "add":
349
files[i["file"]]["raw"] += i["str"] + "\n"
350
351
# Write the files back
352
for key, val in files.items():
353
mlog.log("Rewriting", mlog.yellow(key))
354
with open(val["path"], "w", encoding="utf-8") as fp:
355
fp.write(val["raw"])
356
357
358
# Monkey patch the apply_changes method until https://github.com/mesonbuild/meson/pull/12899 is merged
359
Rewriter.apply_changes = apply_changes
360
# Monkey patch the update_python_sources method until this is upstreamed
361
Rewriter.process_update_python_sources = update_python_sources
362
Rewriter.process_update_doc_sources = update_doc_sources
363
364
rewriter = Rewriter(options.sourcedir)
365
visitor = AstPython()
366
rewriter.interpreter.visitors += [visitor]
367
rewriter.analyze_meson()
368
rewriter.process_update_python_sources(visitor)
369
rewriter.process_update_doc_sources(visitor)
370
rewriter.apply_changes()
371
rewriter.print_info()
372
373
# Run meson format
374
meson_format(
375
Namespace(
376
sources=[options.sourcedir],
377
inplace=True,
378
recursive=True,
379
output=None,
380
configuration=options.sourcedir / "meson.format",
381
editor_config=None,
382
source_file_path=None,
383
)
384
)
385
386