cf-tool-cli 1.0.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.
- cf_tool/__init__.py +3 -0
- cf_tool/__main__.py +6 -0
- cf_tool/build.py +211 -0
- cf_tool/cli/__init__.py +117 -0
- cf_tool/cli/account.py +32 -0
- cf_tool/cli/common.py +262 -0
- cf_tool/cli/completion.py +226 -0
- cf_tool/cli/configure.py +386 -0
- cf_tool/cli/problems.py +399 -0
- cf_tool/cli/setup.py +195 -0
- cf_tool/cli/submitting.py +309 -0
- cf_tool/cli/testing.py +354 -0
- cf_tool/codeforces/__init__.py +3 -0
- cf_tool/codeforces/browser.py +346 -0
- cf_tool/codeforces/client.py +521 -0
- cf_tool/codeforces/crypto.py +41 -0
- cf_tool/codeforces/gateway.py +288 -0
- cf_tool/codeforces/http.py +213 -0
- cf_tool/codeforces/pages.py +767 -0
- cf_tool/codeforces/session.py +103 -0
- cf_tool/doctor.py +124 -0
- cf_tool/errors.py +37 -0
- cf_tool/judge.py +481 -0
- cf_tool/languages.py +1195 -0
- cf_tool/listen.py +178 -0
- cf_tool/paths.py +57 -0
- cf_tool/process.py +431 -0
- cf_tool/settings.py +386 -0
- cf_tool/stress.py +167 -0
- cf_tool/ui.py +455 -0
- cf_tool/views.py +306 -0
- cf_tool/workspace.py +777 -0
- cf_tool_cli-1.0.0.dist-info/METADATA +106 -0
- cf_tool_cli-1.0.0.dist-info/RECORD +38 -0
- cf_tool_cli-1.0.0.dist-info/WHEEL +5 -0
- cf_tool_cli-1.0.0.dist-info/entry_points.txt +2 -0
- cf_tool_cli-1.0.0.dist-info/licenses/LICENSE +48 -0
- cf_tool_cli-1.0.0.dist-info/top_level.txt +1 -0
cf_tool/languages.py
ADDED
|
@@ -0,0 +1,1195 @@
|
|
|
1
|
+
"""Languages: how each one is built and run here, and what it is submitted as.
|
|
2
|
+
|
|
3
|
+
A language is named by its file extension (cpp, py, ...). cf-tool knows seven,
|
|
4
|
+
and config.toml can change any part of one, or add one:
|
|
5
|
+
- the compiler or interpreter: found on this machine, best first, unless set
|
|
6
|
+
- the build and run commands: made for it, unless set in full
|
|
7
|
+
- the Codeforces language: the setting, else the newest of its family
|
|
8
|
+
- the starter code for new problems: the setting, else a built-in one
|
|
9
|
+
C++ is compiled with the -std of the Codeforces language it is submitted as,
|
|
10
|
+
so code that compiles here compiles there too.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
import shutil
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from datetime import datetime
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import TYPE_CHECKING, Callable, Mapping
|
|
26
|
+
|
|
27
|
+
from cf_tool import paths
|
|
28
|
+
from cf_tool.errors import CFError
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from cf_tool.settings import Settings
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ┌──────────────────────────────────────────────────────────┐
|
|
35
|
+
# │ Codeforces languages │
|
|
36
|
+
# └──────────────────────────────────────────────────────────┘
|
|
37
|
+
|
|
38
|
+
# NOTE: ids as Codeforces' submit form has them (checked against the cph
|
|
39
|
+
# \ extension's table, 2026-09); the live list replaces it once read
|
|
40
|
+
LANGS: dict[str, str] = {
|
|
41
|
+
"43": "GNU GCC C11 5.1.0",
|
|
42
|
+
"52": "Clang++17 Diagnostics",
|
|
43
|
+
"50": "GNU G++14 6.4.0",
|
|
44
|
+
"54": "GNU G++17 7.3.0",
|
|
45
|
+
"61": "GNU G++17 9.2.0 (64 bit, msys 2)",
|
|
46
|
+
"89": "GNU G++20 13.2 (64 bit, winlibs)",
|
|
47
|
+
"91": "GNU G++23 14.2 (64 bit, msys2)",
|
|
48
|
+
"59": "Microsoft Visual C++ 2017",
|
|
49
|
+
"65": "C# 8, .NET Core 3.1",
|
|
50
|
+
"79": "C# 10, .NET SDK 6.0",
|
|
51
|
+
"9": "C# Mono 6.8",
|
|
52
|
+
"28": "D DMD32 v2.105.0",
|
|
53
|
+
"32": "Go 1.22.2",
|
|
54
|
+
"12": "Haskell GHC 8.10.1",
|
|
55
|
+
"87": "Java 21 64bit",
|
|
56
|
+
"60": "Java 11.0.6",
|
|
57
|
+
"36": "Java 8 32bit",
|
|
58
|
+
"83": "Kotlin 1.7.20",
|
|
59
|
+
"88": "Kotlin 1.9.21",
|
|
60
|
+
"19": "OCaml 4.02.1",
|
|
61
|
+
"3": "Delphi 7",
|
|
62
|
+
"4": "Free Pascal 3.2.2",
|
|
63
|
+
"51": "PascalABC.NET 3.8.3",
|
|
64
|
+
"13": "Perl 5.20.1",
|
|
65
|
+
"6": "PHP 8.1.7",
|
|
66
|
+
"7": "Python 2.7.18",
|
|
67
|
+
"31": "Python 3.13.2",
|
|
68
|
+
"40": "PyPy 2.7.13 (7.3.0)",
|
|
69
|
+
"41": "PyPy 3.6.9 (7.3.0)",
|
|
70
|
+
"70": "PyPy 3.10 (7.3.15, 64bit)",
|
|
71
|
+
"67": "Ruby 3.2.2",
|
|
72
|
+
"75": "Rust 1.89.0 (2021)",
|
|
73
|
+
"98": "Rust 1.89.0 (2024)",
|
|
74
|
+
"20": "Scala 2.12.8",
|
|
75
|
+
"34": "JavaScript V8 4.8.0",
|
|
76
|
+
"55": "Node.js 15.8.0 (64bit)",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Codeforces spells one language three ways: the submit menu ("GNU G++17
|
|
80
|
+
# 7.3.0"), the /my table and the API ("C++17 (GCC 7-32)"), and older names
|
|
81
|
+
# ("GNU C++17"). Rules are tried in order on the start of a name, so the more
|
|
82
|
+
# specific ones come first: Pascal before ".NET" C#, JavaScript before Java,
|
|
83
|
+
# C# and C++ before plain C.
|
|
84
|
+
_EXT_RULES: list[tuple[re.Pattern[str], str]] = [
|
|
85
|
+
(re.compile(p, re.IGNORECASE), ext)
|
|
86
|
+
for p, ext in [
|
|
87
|
+
(r"PascalABC|Delphi|FPC|Free Pascal", "pas"),
|
|
88
|
+
(r"JavaScript|Node\.js|V8", "js"),
|
|
89
|
+
(r"Java\b", "java"),
|
|
90
|
+
(r"Kotlin", "kt"),
|
|
91
|
+
(r"(Mono |MS )?C#", "cs"),
|
|
92
|
+
(r"F#", "fs"),
|
|
93
|
+
(r"(Microsoft )?Q#", "qs"),
|
|
94
|
+
(r".*(C\+\+|G\+\+)", "cpp"), # GNU G++17, C++20 (GCC 13-64), Clang++17, MS C++
|
|
95
|
+
(r"(GNU )?(GCC )?C\d*\b", "c"), # GNU C11, GNU GCC C11 5.1.0, C11 (GCC ...)
|
|
96
|
+
(r"Python|PyPy", "py"),
|
|
97
|
+
(r"Rust", "rs"),
|
|
98
|
+
(r"Go\b", "go"),
|
|
99
|
+
(r"Haskell", "hs"),
|
|
100
|
+
(r"OCaml", "ml"),
|
|
101
|
+
(r"Scala", "scala"),
|
|
102
|
+
(r"D\b", "d"),
|
|
103
|
+
(r"Perl", "pl"),
|
|
104
|
+
(r"PHP", "php"),
|
|
105
|
+
(r"Ruby", "rb"),
|
|
106
|
+
(r"(Active)?Tcl", "tcl"),
|
|
107
|
+
(r"Befunge", "bf"),
|
|
108
|
+
(r"Pike", "pike"),
|
|
109
|
+
(r"Io\b", "io"),
|
|
110
|
+
(r"Factor", "factor"),
|
|
111
|
+
(r"(Open)?Cobol", "cbl"),
|
|
112
|
+
(r"Ada\b", "adb"),
|
|
113
|
+
(r"FALSE\b", "f"),
|
|
114
|
+
(r"Secret", "secret_171"),
|
|
115
|
+
]
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
# family display names by file extension; unknown names are "Other" (txt)
|
|
119
|
+
FAMILY_NAMES = {
|
|
120
|
+
"cpp": "C++",
|
|
121
|
+
"c": "C",
|
|
122
|
+
"py": "Python",
|
|
123
|
+
"java": "Java",
|
|
124
|
+
"kt": "Kotlin",
|
|
125
|
+
"rs": "Rust",
|
|
126
|
+
"go": "Go",
|
|
127
|
+
"cs": "C#",
|
|
128
|
+
"fs": "F#",
|
|
129
|
+
"qs": "Q#",
|
|
130
|
+
"js": "JavaScript",
|
|
131
|
+
"pas": "Pascal",
|
|
132
|
+
"hs": "Haskell",
|
|
133
|
+
"ml": "OCaml",
|
|
134
|
+
"scala": "Scala",
|
|
135
|
+
"d": "D",
|
|
136
|
+
"pl": "Perl",
|
|
137
|
+
"php": "PHP",
|
|
138
|
+
"rb": "Ruby",
|
|
139
|
+
"tcl": "Tcl",
|
|
140
|
+
"bf": "Befunge",
|
|
141
|
+
"pike": "Pike",
|
|
142
|
+
"io": "Io",
|
|
143
|
+
"factor": "Factor",
|
|
144
|
+
"cbl": "COBOL",
|
|
145
|
+
"adb": "Ada",
|
|
146
|
+
"f": "FALSE",
|
|
147
|
+
"secret_171": "Secret",
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def ext_of(lang_name: str) -> str:
|
|
152
|
+
"""Source file extension for a Codeforces language name; "txt" when unknown."""
|
|
153
|
+
name = lang_name.strip()
|
|
154
|
+
for pattern, ext in _EXT_RULES:
|
|
155
|
+
if pattern.match(name):
|
|
156
|
+
return ext
|
|
157
|
+
return "txt"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def family_name(ext: str) -> str:
|
|
161
|
+
"""Display name of the language family with this extension ("C++")."""
|
|
162
|
+
return FAMILY_NAMES.get(ext, "Other")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def newest_first_key(name: str) -> tuple:
|
|
166
|
+
"""Sort key putting the highest version numbers in a name first."""
|
|
167
|
+
return tuple(-int(n) for n in re.findall(r"\d+", name)), name
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def by_family(languages: Mapping[str, str]) -> dict[str, list[tuple[str, str]]]:
|
|
171
|
+
"""(id, name) languages grouped by file extension, each group newest first."""
|
|
172
|
+
groups: dict[str, list[tuple[str, str]]] = {}
|
|
173
|
+
for lid, name in languages.items():
|
|
174
|
+
groups.setdefault(ext_of(name), []).append((lid, name))
|
|
175
|
+
return {
|
|
176
|
+
ext: sorted(group, key=lambda item: newest_first_key(item[1]))
|
|
177
|
+
for ext, group in groups.items()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def known_languages() -> dict[str, str]:
|
|
182
|
+
"""Codeforces' languages, id -> name: the list last read from the submit
|
|
183
|
+
form, else the built-in table."""
|
|
184
|
+
try:
|
|
185
|
+
saved = json.loads((paths.STATE_DIR / "languages.json").read_text(encoding="utf-8"))
|
|
186
|
+
languages = saved["languages"]
|
|
187
|
+
if isinstance(languages, dict) and languages:
|
|
188
|
+
return {str(k): str(v) for k, v in languages.items()}
|
|
189
|
+
except (OSError, ValueError, KeyError, TypeError):
|
|
190
|
+
pass
|
|
191
|
+
return dict(LANGS)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def remember_languages(languages: Mapping[str, str]) -> None:
|
|
195
|
+
"""Keep the submit form's list for later runs (setup without logging in)."""
|
|
196
|
+
if languages:
|
|
197
|
+
data = {"fetched": time.time(), "languages": dict(languages)}
|
|
198
|
+
paths.write_atomic(paths.STATE_DIR / "languages.json", json.dumps(data, indent=1) + "\n")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# ┌──────────────────────────────────────────────────────────┐
|
|
202
|
+
# │ Compilers and interpreters │
|
|
203
|
+
# └──────────────────────────────────────────────────────────┘
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@dataclass(frozen=True)
|
|
207
|
+
class Tool:
|
|
208
|
+
"""A compiler or interpreter found here."""
|
|
209
|
+
|
|
210
|
+
program: str # the command: "g++-14"
|
|
211
|
+
label: str # shown to the user: "GCC 14 (g++-14)"
|
|
212
|
+
banner: str = "" # the first line of its version text
|
|
213
|
+
kind: str = "" # "gcc", "clang", "apple clang", "python", "pypy", ...
|
|
214
|
+
version: tuple[int, ...] = () # (14, 2, 0); () when unknown
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _version_text(where: str, flag: str) -> str:
|
|
218
|
+
"""First line of what `where flag` prints; "" when it won't run."""
|
|
219
|
+
if sys.platform == "darwin" and Path(where).parent == Path("/usr/bin"):
|
|
220
|
+
name = Path(where).name
|
|
221
|
+
# NOTE: Apple's placeholder commands open an installer dialog
|
|
222
|
+
# \ instead of running, unless their tools are installed
|
|
223
|
+
if name in _XCODE_STUBS and not _succeeds(["xcode-select", "-p"]):
|
|
224
|
+
return ""
|
|
225
|
+
if name in _JAVA_STUBS and not _succeeds(["/usr/libexec/java_home"]):
|
|
226
|
+
return ""
|
|
227
|
+
try:
|
|
228
|
+
done = subprocess.run([where, flag], capture_output=True, text=True, timeout=30)
|
|
229
|
+
except (OSError, subprocess.SubprocessError):
|
|
230
|
+
return ""
|
|
231
|
+
text = (done.stdout or done.stderr).strip() # NOTE: java and kotlinc print to stderr
|
|
232
|
+
return text.splitlines()[0] if done.returncode == 0 and text else ""
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
_XCODE_STUBS = {"cc", "c++", "gcc", "g++", "clang", "clang++", "python3"}
|
|
236
|
+
_JAVA_STUBS = {"java", "javac"}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _succeeds(argv: list[str]) -> bool:
|
|
240
|
+
try:
|
|
241
|
+
return subprocess.run(argv, capture_output=True, timeout=10).returncode == 0
|
|
242
|
+
except (OSError, subprocess.SubprocessError):
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
_banners: dict[str, str] | None = None # what tools said, kept in STATE_DIR/tools.json
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _tool_cache() -> dict[str, str]:
|
|
250
|
+
global _banners
|
|
251
|
+
if _banners is None:
|
|
252
|
+
try:
|
|
253
|
+
loaded = json.loads((paths.STATE_DIR / "tools.json").read_text(encoding="utf-8"))
|
|
254
|
+
_banners = loaded if isinstance(loaded, dict) else {}
|
|
255
|
+
except (OSError, ValueError):
|
|
256
|
+
_banners = {}
|
|
257
|
+
return _banners
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _remember(key: str, value: str) -> None:
|
|
261
|
+
cache = _tool_cache()
|
|
262
|
+
cache[key] = value
|
|
263
|
+
try:
|
|
264
|
+
paths.write_atomic(paths.STATE_DIR / "tools.json", json.dumps(cache, indent=1) + "\n")
|
|
265
|
+
except OSError:
|
|
266
|
+
pass
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _cached_version(program: str, flag: str) -> str:
|
|
270
|
+
"""_version_text of the program on PATH, remembered across runs while the
|
|
271
|
+
program file stays the same (macOS's /usr/bin stand-ins are always asked)."""
|
|
272
|
+
where = shutil.which(program)
|
|
273
|
+
if where is None:
|
|
274
|
+
return ""
|
|
275
|
+
try:
|
|
276
|
+
stat = os.stat(where)
|
|
277
|
+
except OSError:
|
|
278
|
+
return ""
|
|
279
|
+
if sys.platform == "darwin" and Path(where).parent == Path("/usr/bin"):
|
|
280
|
+
return _version_text(where, flag) # NOTE: they forward to whichever tools are selected
|
|
281
|
+
key = f"{where}|{flag}|{stat.st_mtime_ns}|{stat.st_size}"
|
|
282
|
+
cache = _tool_cache()
|
|
283
|
+
if key in cache:
|
|
284
|
+
return cache[key]
|
|
285
|
+
banner = _version_text(where, flag)
|
|
286
|
+
if banner: # NOTE: a failure isn't remembered: the tool may be installed later
|
|
287
|
+
_remember(key, banner)
|
|
288
|
+
return banner
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _accepts_std(program: str, std: str) -> bool:
|
|
292
|
+
"""Whether the C++ compiler program takes -std=std; asked once per
|
|
293
|
+
compiler version (by compiling nothing with it)."""
|
|
294
|
+
where = shutil.which(program)
|
|
295
|
+
banner = _cached_version(program, "--version") if where else ""
|
|
296
|
+
if not where or not banner:
|
|
297
|
+
return False
|
|
298
|
+
key = f"std|{banner}|{std}"
|
|
299
|
+
cache = _tool_cache()
|
|
300
|
+
if key in cache:
|
|
301
|
+
return cache[key] == "yes"
|
|
302
|
+
try:
|
|
303
|
+
done = subprocess.run(
|
|
304
|
+
[where, f"-std={std}", "-x", "c++", "-fsyntax-only", "-"],
|
|
305
|
+
stdin=subprocess.DEVNULL,
|
|
306
|
+
capture_output=True,
|
|
307
|
+
timeout=30,
|
|
308
|
+
)
|
|
309
|
+
except (OSError, subprocess.SubprocessError):
|
|
310
|
+
return False
|
|
311
|
+
_remember(key, "yes" if done.returncode == 0 else "no")
|
|
312
|
+
return done.returncode == 0
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@dataclass
|
|
316
|
+
class Machine:
|
|
317
|
+
"""What detection looks at: commands on PATH, their version text, the OS,
|
|
318
|
+
and which -std a C++ compiler takes (None: judged by its version)."""
|
|
319
|
+
|
|
320
|
+
which: Callable[[str], "str | None"] = shutil.which
|
|
321
|
+
version: Callable[[str, str], str] = _cached_version
|
|
322
|
+
platform: str = sys.platform
|
|
323
|
+
accepts: "Callable[[str, str], bool] | None" = _accepts_std
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _numbers(text: str) -> tuple[int, ...]:
|
|
327
|
+
"""The first dotted version number in text: (14, 2, 0); () when none."""
|
|
328
|
+
match = re.search(r"\d+(?:\.\d+)+", text)
|
|
329
|
+
return tuple(int(n) for n in match.group().split(".")) if match else ()
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _gcc_family(machine: Machine, names: list[str]) -> list[Tool]:
|
|
333
|
+
"""The GCC and clang compilers among names (C or C++):
|
|
334
|
+
- GCC before clang (Codeforces judges with GCC)
|
|
335
|
+
- one per compiler (macOS's g++ is clang++ again)"""
|
|
336
|
+
found: list[Tool] = []
|
|
337
|
+
seen: set[str] = set()
|
|
338
|
+
for program in names:
|
|
339
|
+
banner = machine.version(program, "--version") if machine.which(program) else ""
|
|
340
|
+
if not banner or banner in seen:
|
|
341
|
+
continue
|
|
342
|
+
seen.add(banner)
|
|
343
|
+
low = banner.lower()
|
|
344
|
+
kind = "apple clang" if "apple" in low else "clang" if "clang" in low else "gcc"
|
|
345
|
+
version = _numbers(banner)
|
|
346
|
+
title = {"apple clang": "Apple clang", "clang": "Clang", "gcc": "GCC"}[kind]
|
|
347
|
+
label = f"{title} {version[0] if version else '?'} ({program})"
|
|
348
|
+
found.append(Tool(program, label, banner, kind, version))
|
|
349
|
+
return sorted(found, key=lambda tool: tool.kind != "gcc")
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _detect_cpp(machine: Machine) -> list[Tool]:
|
|
353
|
+
names = [f"g++-{v}" for v in range(16, 10, -1)] + ["clang++", "g++"]
|
|
354
|
+
return _gcc_family(machine, names)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _detect_c(machine: Machine) -> list[Tool]:
|
|
358
|
+
names = [f"gcc-{v}" for v in range(16, 10, -1)] + ["clang", "gcc"]
|
|
359
|
+
return _gcc_family(machine, names)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _detect_python(machine: Machine) -> list[Tool]:
|
|
363
|
+
found: list[Tool] = []
|
|
364
|
+
seen: set[str] = set()
|
|
365
|
+
for program in ("python3", "python", "py", "pypy3"):
|
|
366
|
+
banner = machine.version(program, "--version") if machine.which(program) else ""
|
|
367
|
+
match = re.match(r"Python (3\.\d+)", banner)
|
|
368
|
+
if not match or banner in seen: # NOTE: python may be Python 2, or python3 again
|
|
369
|
+
continue
|
|
370
|
+
seen.add(banner)
|
|
371
|
+
kind = "pypy" if "pypy" in program else "python"
|
|
372
|
+
label = f"{'PyPy' if kind == 'pypy' else 'Python'} {match.group(1)} ({program})"
|
|
373
|
+
found.append(Tool(program, label, banner, kind, _numbers(banner)))
|
|
374
|
+
return found
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _detect_java(machine: Machine) -> list[Tool]:
|
|
378
|
+
banner = machine.version("java", "-version") if machine.which("java") else ""
|
|
379
|
+
match = re.search(r'version "(\d+)(?:\.(\d+))?', banner)
|
|
380
|
+
if not match:
|
|
381
|
+
return []
|
|
382
|
+
major = (
|
|
383
|
+
int(match.group(2) or 0) if match.group(1) == "1" else int(match.group(1))
|
|
384
|
+
) # NOTE: "1.8.0" is Java 8
|
|
385
|
+
if major < 11:
|
|
386
|
+
return [] # NOTE: running a .java file directly, whatever its class, needs 11+
|
|
387
|
+
kind = "jdk" if machine.which("javac") else "jre"
|
|
388
|
+
return [Tool("java", f"Java {major} (java)", banner, kind, (major,))]
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _detect_kotlin(machine: Machine) -> list[Tool]:
|
|
392
|
+
if not (machine.which("kotlinc") and machine.which("java")):
|
|
393
|
+
return []
|
|
394
|
+
banner = machine.version("kotlinc", "-version")
|
|
395
|
+
match = re.search(r"kotlinc-jvm (\d+\.\d+)", banner)
|
|
396
|
+
if not match:
|
|
397
|
+
return []
|
|
398
|
+
return [
|
|
399
|
+
Tool("kotlinc", f"Kotlin {match.group(1)} (kotlinc)", banner, "kotlin", _numbers(banner))
|
|
400
|
+
]
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _detect_rust(machine: Machine) -> list[Tool]:
|
|
404
|
+
banner = machine.version("rustc", "--version") if machine.which("rustc") else ""
|
|
405
|
+
match = re.search(r"rustc (\d+\.\d+)", banner)
|
|
406
|
+
if not match:
|
|
407
|
+
return []
|
|
408
|
+
return [Tool("rustc", f"Rust {match.group(1)} (rustc)", banner, "rust", _numbers(banner))]
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _detect_go(machine: Machine) -> list[Tool]:
|
|
412
|
+
banner = machine.version("go", "version") if machine.which("go") else ""
|
|
413
|
+
match = re.search(r"go(\d+\.\d+)", banner)
|
|
414
|
+
if not match:
|
|
415
|
+
return []
|
|
416
|
+
return [Tool("go", f"Go {match.group(1)} (go)", banner, "go", _numbers(match.group(1)))]
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
# ┌──────────────────────────────────────────────────────────┐
|
|
420
|
+
# │ Build and run commands │
|
|
421
|
+
# └──────────────────────────────────────────────────────────┘
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
@dataclass(frozen=True)
|
|
425
|
+
class Context:
|
|
426
|
+
"""What a language's commands depend on besides the tool."""
|
|
427
|
+
|
|
428
|
+
platform: str
|
|
429
|
+
submit_name: str = "" # the Codeforces language it is submitted as ("" unknown)
|
|
430
|
+
flags: tuple[str, ...] = () # the user's extra flags
|
|
431
|
+
source: Path | None = None # the code file, when there is one (Java's class name)
|
|
432
|
+
accepts: "Callable[[str, str], bool] | None" = None # see Machine.accepts
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
# (build command, or None when nothing is built; run command). Words may hold
|
|
436
|
+
# the placeholders {src} {exe} {dir} {name} {class} {copy}, filled in by build.py.
|
|
437
|
+
Commands = tuple["list[str] | None", "list[str]"]
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _stack_flags(platform: str) -> list[str]:
|
|
441
|
+
"""Linker flags giving a C/C++ program Codeforces' 256 MB stack (on Linux
|
|
442
|
+
a process limit does it instead, see process.py)."""
|
|
443
|
+
if platform == "darwin":
|
|
444
|
+
return ["-Wl,-stack_size,0x10000000"]
|
|
445
|
+
if platform == "win32":
|
|
446
|
+
return ["-Wl,--stack,268435456"]
|
|
447
|
+
return []
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
# each C++ standard's spellings for -std, the current one first
|
|
451
|
+
_STD_SPELLINGS = {
|
|
452
|
+
26: ("c++26", "c++2c"),
|
|
453
|
+
23: ("c++23", "c++2b"),
|
|
454
|
+
20: ("c++20", "c++2a"),
|
|
455
|
+
17: ("c++17", "c++1z"),
|
|
456
|
+
14: ("c++14", "c++1y"),
|
|
457
|
+
11: ("c++11", "c++0x"),
|
|
458
|
+
}
|
|
459
|
+
# the first major version of each compiler that knows each spelling
|
|
460
|
+
_FIRST_KNOWN = {
|
|
461
|
+
"gcc": {"c++23": 11, "c++2b": 11, "c++20": 10, "c++2a": 8, "c++17": 5, "c++1z": 5},
|
|
462
|
+
"clang": {"c++23": 17, "c++2b": 12, "c++20": 10, "c++2a": 5, "c++17": 5, "c++1z": 3},
|
|
463
|
+
"apple clang": {"c++23": 16, "c++2b": 13, "c++20": 12, "c++2a": 10, "c++17": 10, "c++1z": 6},
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def cpp_standard(
|
|
468
|
+
submit_name: str,
|
|
469
|
+
tool: Tool | None = None,
|
|
470
|
+
accepts: Callable[[str, str], bool] | None = None,
|
|
471
|
+
) -> str:
|
|
472
|
+
"""The -std value for C++ submitted as submit_name ("GNU G++20 13.2" ->
|
|
473
|
+
c++20; unknown -> C++23): the newest spelling of that standard, or of an
|
|
474
|
+
older one, that tool takes. In order:
|
|
475
|
+
- asking the compiler (accepts), when it can be asked
|
|
476
|
+
- by the compiler's version, when it is known
|
|
477
|
+
- the standard's own name"""
|
|
478
|
+
match = re.search(r"\+\+\s*(\d{2})\b", submit_name)
|
|
479
|
+
year = int(match.group(1)) if match else 23
|
|
480
|
+
year = max((y for y in _STD_SPELLINGS if y <= year), default=11)
|
|
481
|
+
spellings = [
|
|
482
|
+
s for y in sorted(_STD_SPELLINGS, reverse=True) if y <= year for s in _STD_SPELLINGS[y]
|
|
483
|
+
]
|
|
484
|
+
if tool is not None and accepts is not None:
|
|
485
|
+
for spelling in spellings:
|
|
486
|
+
if accepts(tool.program, spelling):
|
|
487
|
+
return spelling
|
|
488
|
+
first = _FIRST_KNOWN.get(tool.kind, {}) if tool is not None and tool.version else {}
|
|
489
|
+
if first:
|
|
490
|
+
major = tool.version[0] # type: ignore[union-attr]
|
|
491
|
+
for spelling in spellings:
|
|
492
|
+
if spelling in first and major >= first[spelling]:
|
|
493
|
+
return spelling
|
|
494
|
+
return spellings[0]
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _cpp_commands(tool: Tool, ctx: Context) -> Commands:
|
|
498
|
+
build = [
|
|
499
|
+
tool.program,
|
|
500
|
+
f"-std={cpp_standard(ctx.submit_name, tool, ctx.accepts)}",
|
|
501
|
+
"-O2",
|
|
502
|
+
"-Wall",
|
|
503
|
+
]
|
|
504
|
+
build += _stack_flags(ctx.platform)
|
|
505
|
+
if "clang" in tool.kind:
|
|
506
|
+
build.append(f"-I{shim_folder()}") # NOTE: clang's own library has no bits/stdc++.h
|
|
507
|
+
build += [*ctx.flags, "{src}", "-o", "{exe}"]
|
|
508
|
+
return build, ["{exe}"]
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _c_commands(tool: Tool, ctx: Context) -> Commands:
|
|
512
|
+
match = re.search(r"\bC(\d{2})\b", ctx.submit_name)
|
|
513
|
+
build = [tool.program, f"-std=c{match.group(1) if match else '11'}", "-O2", "-Wall"]
|
|
514
|
+
build += _stack_flags(ctx.platform)
|
|
515
|
+
build += [*ctx.flags, "{src}", "-o", "{exe}", "-lm"]
|
|
516
|
+
return build, ["{exe}"]
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _python_commands(tool: Tool, ctx: Context) -> Commands:
|
|
520
|
+
return None, [tool.program, *ctx.flags, "{src}"]
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
_JAVA_CLASS_RE = re.compile(r"\bpublic\s+(?:final\s+|abstract\s+)*class\s+(\w+)")
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def java_main_class(source: Path) -> str | None:
|
|
527
|
+
"""The public class of a Java file; None when it has none (or is unreadable)."""
|
|
528
|
+
try:
|
|
529
|
+
match = _JAVA_CLASS_RE.search(source.read_text(encoding="utf-8", errors="replace"))
|
|
530
|
+
except OSError:
|
|
531
|
+
return None
|
|
532
|
+
return match.group(1) if match else None
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _java_commands(tool: Tool, ctx: Context) -> Commands:
|
|
536
|
+
"""javac into the build folder, then java with the public class; without
|
|
537
|
+
javac, or no public class to name, java runs the file itself."""
|
|
538
|
+
main_class = java_main_class(ctx.source) if ctx.source is not None else None
|
|
539
|
+
if tool.kind != "jdk" or (ctx.source is not None and main_class is None):
|
|
540
|
+
return None, ["java", *ctx.flags, "{src}"]
|
|
541
|
+
# NOTE: javac wants a public class in a file of its name (a.java holds Main):
|
|
542
|
+
# \ it compiles a copy named for the class (see Setup.copy_as)
|
|
543
|
+
build = ["javac", "-encoding", "UTF-8", *ctx.flags, "-d", "{dir}", "{copy}"]
|
|
544
|
+
return build, ["java", "-cp", "{dir}", main_class or "{class}"]
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def _kotlin_commands(tool: Tool, ctx: Context) -> Commands:
|
|
548
|
+
build = [tool.program, "{src}", "-include-runtime", *ctx.flags, "-d", "{dir}/{name}.jar"]
|
|
549
|
+
return build, ["java", "-jar", "{dir}/{name}.jar"]
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _rust_commands(tool: Tool, ctx: Context) -> Commands:
|
|
553
|
+
match = re.search(r"\((20\d\d)\)", ctx.submit_name)
|
|
554
|
+
edition = match.group(1) if match else "2021"
|
|
555
|
+
if edition == "2024" and tool.version and tool.version < (1, 85):
|
|
556
|
+
edition = "2021" # NOTE: rustc learned the 2024 edition in 1.85
|
|
557
|
+
build = [tool.program, "--edition", edition, "-O", *ctx.flags, "-o", "{exe}", "{src}"]
|
|
558
|
+
return build, ["{exe}"]
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _go_commands(tool: Tool, ctx: Context) -> Commands:
|
|
562
|
+
return [tool.program, "build", *ctx.flags, "-o", "{exe}", "{src}"], ["{exe}"]
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
# ┌──────────────────────────────────────────────────────────┐
|
|
566
|
+
# │ The languages │
|
|
567
|
+
# └──────────────────────────────────────────────────────────┘
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
@dataclass(frozen=True)
|
|
571
|
+
class Language:
|
|
572
|
+
"""A language cf-tool knows: how to find its tools, how they build and
|
|
573
|
+
run a file, which Codeforces languages belong to it and which to prefer."""
|
|
574
|
+
|
|
575
|
+
key: str # "cpp": its file extension, and its table in config.toml
|
|
576
|
+
name: str # "C++"
|
|
577
|
+
extensions: tuple[str, ...] # ("cpp", "cc", "cxx")
|
|
578
|
+
detect: Callable[[Machine], list[Tool]]
|
|
579
|
+
commands: Callable[[Tool, Context], Commands]
|
|
580
|
+
starter: str # built-in starter code
|
|
581
|
+
prefer: tuple[str, ...] = () # patterns over Codeforces language names, best first
|
|
582
|
+
exclude: str = "" # a pattern of Codeforces languages never to pick
|
|
583
|
+
install: Mapping[str, str] = field(default_factory=dict) # platform -> how to get a tool
|
|
584
|
+
big_stack: bool = False # runs with Codeforces' stack (see process.py)
|
|
585
|
+
|
|
586
|
+
def install_hint(self, platform: str = sys.platform) -> str:
|
|
587
|
+
"""How to install this language's tools on platform."""
|
|
588
|
+
return self.install.get(platform) or self.install["any"]
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
CPP_STARTER = """\
|
|
592
|
+
// {{name}}
|
|
593
|
+
// {{url}}
|
|
594
|
+
#include <bits/stdc++.h>
|
|
595
|
+
using namespace std;
|
|
596
|
+
|
|
597
|
+
int main() {
|
|
598
|
+
ios::sync_with_stdio(false);
|
|
599
|
+
cin.tie(nullptr);
|
|
600
|
+
|
|
601
|
+
return 0;
|
|
602
|
+
}
|
|
603
|
+
"""
|
|
604
|
+
|
|
605
|
+
C_STARTER = """\
|
|
606
|
+
// {{name}}
|
|
607
|
+
// {{url}}
|
|
608
|
+
#include <stdio.h>
|
|
609
|
+
|
|
610
|
+
int main(void) {
|
|
611
|
+
return 0;
|
|
612
|
+
}
|
|
613
|
+
"""
|
|
614
|
+
|
|
615
|
+
PYTHON_STARTER = """\
|
|
616
|
+
# {{name}}
|
|
617
|
+
# {{url}}
|
|
618
|
+
import sys
|
|
619
|
+
|
|
620
|
+
input = sys.stdin.readline
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def main():
|
|
624
|
+
pass
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
main()
|
|
628
|
+
"""
|
|
629
|
+
|
|
630
|
+
JAVA_STARTER = """\
|
|
631
|
+
// {{name}}
|
|
632
|
+
// {{url}}
|
|
633
|
+
import java.io.*;
|
|
634
|
+
import java.util.*;
|
|
635
|
+
|
|
636
|
+
public class Main {
|
|
637
|
+
public static void main(String[] args) throws IOException {
|
|
638
|
+
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
|
|
639
|
+
PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out)));
|
|
640
|
+
|
|
641
|
+
out.flush();
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
"""
|
|
645
|
+
|
|
646
|
+
KOTLIN_STARTER = """\
|
|
647
|
+
// {{name}}
|
|
648
|
+
// {{url}}
|
|
649
|
+
import java.io.*
|
|
650
|
+
|
|
651
|
+
fun main() {
|
|
652
|
+
val input = BufferedReader(InputStreamReader(System.`in`))
|
|
653
|
+
val out = PrintWriter(BufferedWriter(OutputStreamWriter(System.out)))
|
|
654
|
+
|
|
655
|
+
out.flush()
|
|
656
|
+
}
|
|
657
|
+
"""
|
|
658
|
+
|
|
659
|
+
RUST_STARTER = """\
|
|
660
|
+
// {{name}}
|
|
661
|
+
// {{url}}
|
|
662
|
+
use std::io::{self, BufWriter, Read, Write};
|
|
663
|
+
|
|
664
|
+
fn main() {
|
|
665
|
+
let mut input = String::new();
|
|
666
|
+
io::stdin().read_to_string(&mut input).unwrap();
|
|
667
|
+
let mut tokens = input.split_ascii_whitespace();
|
|
668
|
+
let mut out = BufWriter::new(io::stdout().lock());
|
|
669
|
+
|
|
670
|
+
let _ = tokens.next();
|
|
671
|
+
out.flush().unwrap();
|
|
672
|
+
}
|
|
673
|
+
"""
|
|
674
|
+
|
|
675
|
+
GO_STARTER = """\
|
|
676
|
+
// {{name}}
|
|
677
|
+
// {{url}}
|
|
678
|
+
package main
|
|
679
|
+
|
|
680
|
+
import (
|
|
681
|
+
\t"bufio"
|
|
682
|
+
\t"fmt"
|
|
683
|
+
\t"os"
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
func main() {
|
|
687
|
+
\tin := bufio.NewReader(os.Stdin)
|
|
688
|
+
\tout := bufio.NewWriter(os.Stdout)
|
|
689
|
+
\tdefer out.Flush()
|
|
690
|
+
|
|
691
|
+
\tvar n int
|
|
692
|
+
\tfmt.Fscan(in, &n)
|
|
693
|
+
}
|
|
694
|
+
"""
|
|
695
|
+
|
|
696
|
+
LANGUAGES: dict[str, Language] = {
|
|
697
|
+
language.key: language
|
|
698
|
+
for language in (
|
|
699
|
+
Language(
|
|
700
|
+
"cpp",
|
|
701
|
+
"C++",
|
|
702
|
+
("cpp", "cc", "cxx"),
|
|
703
|
+
_detect_cpp,
|
|
704
|
+
_cpp_commands,
|
|
705
|
+
CPP_STARTER,
|
|
706
|
+
prefer=(r"G\+\+\d+.*64", r"G\+\+\d+"),
|
|
707
|
+
exclude=r"Diagnostics|MS C\+\+|Microsoft|ZIP",
|
|
708
|
+
install={
|
|
709
|
+
"darwin": "brew install gcc",
|
|
710
|
+
"win32": "install MSYS2 (msys2.org) and its mingw-w64 g++",
|
|
711
|
+
"any": "install g++, e.g. sudo apt install g++",
|
|
712
|
+
},
|
|
713
|
+
big_stack=True,
|
|
714
|
+
),
|
|
715
|
+
Language(
|
|
716
|
+
"c",
|
|
717
|
+
"C",
|
|
718
|
+
("c",),
|
|
719
|
+
_detect_c,
|
|
720
|
+
_c_commands,
|
|
721
|
+
C_STARTER,
|
|
722
|
+
prefer=(r"C\d+",),
|
|
723
|
+
install={
|
|
724
|
+
"darwin": "brew install gcc",
|
|
725
|
+
"win32": "install MSYS2 (msys2.org) and its mingw-w64 gcc",
|
|
726
|
+
"any": "install gcc, e.g. sudo apt install gcc",
|
|
727
|
+
},
|
|
728
|
+
big_stack=True,
|
|
729
|
+
),
|
|
730
|
+
Language(
|
|
731
|
+
"py",
|
|
732
|
+
"Python",
|
|
733
|
+
("py",),
|
|
734
|
+
_detect_python,
|
|
735
|
+
_python_commands,
|
|
736
|
+
PYTHON_STARTER,
|
|
737
|
+
# NOTE: Codeforces itself recommends PyPy: the same code often runs many times faster
|
|
738
|
+
prefer=(r"PyPy 3.*64", r"PyPy 3", r"Python 3"),
|
|
739
|
+
exclude=r"^(Python|PyPy) 2",
|
|
740
|
+
install={"darwin": "brew install python", "any": "install Python 3 from python.org"},
|
|
741
|
+
),
|
|
742
|
+
Language(
|
|
743
|
+
"java",
|
|
744
|
+
"Java",
|
|
745
|
+
("java",),
|
|
746
|
+
_detect_java,
|
|
747
|
+
_java_commands,
|
|
748
|
+
JAVA_STARTER,
|
|
749
|
+
prefer=(r"Java \d+.*64", r"Java \d+"),
|
|
750
|
+
install={
|
|
751
|
+
"darwin": "brew install openjdk",
|
|
752
|
+
"any": "install a JDK 17 or newer, e.g. from adoptium.net",
|
|
753
|
+
},
|
|
754
|
+
),
|
|
755
|
+
Language(
|
|
756
|
+
"kt",
|
|
757
|
+
"Kotlin",
|
|
758
|
+
("kt",),
|
|
759
|
+
_detect_kotlin,
|
|
760
|
+
_kotlin_commands,
|
|
761
|
+
KOTLIN_STARTER,
|
|
762
|
+
prefer=(r"Kotlin",),
|
|
763
|
+
install={
|
|
764
|
+
"darwin": "brew install kotlin",
|
|
765
|
+
"any": "install kotlinc (kotlinlang.org) and a JDK",
|
|
766
|
+
},
|
|
767
|
+
),
|
|
768
|
+
Language(
|
|
769
|
+
"rs",
|
|
770
|
+
"Rust",
|
|
771
|
+
("rs",),
|
|
772
|
+
_detect_rust,
|
|
773
|
+
_rust_commands,
|
|
774
|
+
RUST_STARTER,
|
|
775
|
+
prefer=(r"Rust",),
|
|
776
|
+
install={"any": "install Rust from rustup.rs"},
|
|
777
|
+
),
|
|
778
|
+
Language(
|
|
779
|
+
"go",
|
|
780
|
+
"Go",
|
|
781
|
+
("go",),
|
|
782
|
+
_detect_go,
|
|
783
|
+
_go_commands,
|
|
784
|
+
GO_STARTER,
|
|
785
|
+
prefer=(r"Go \d",),
|
|
786
|
+
install={"darwin": "brew install go", "any": "install Go from go.dev/dl"},
|
|
787
|
+
),
|
|
788
|
+
)
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
def language_key(ext_or_path: str | Path, settings: "Settings | None" = None) -> str | None:
|
|
793
|
+
"""The language a file extension (or file) belongs to: a config.toml
|
|
794
|
+
table named for exactly that extension, else the built-in language that
|
|
795
|
+
has it; None when neither does."""
|
|
796
|
+
name = Path(str(ext_or_path)).name.lower()
|
|
797
|
+
if name.startswith(".") and name.count(".") == 1:
|
|
798
|
+
ext = name[1:] # NOTE: ".py", an extension on its own
|
|
799
|
+
else:
|
|
800
|
+
ext = Path(name).suffix[1:] if "." in name else name
|
|
801
|
+
if not ext:
|
|
802
|
+
return None
|
|
803
|
+
if settings is not None and ext in settings.languages:
|
|
804
|
+
return ext
|
|
805
|
+
return next((key for key, lang in LANGUAGES.items() if ext in lang.extensions), None)
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def code_extensions(settings: "Settings | None" = None) -> set[str]:
|
|
809
|
+
"""The file extensions of code cf-tool can build and run, with a dot."""
|
|
810
|
+
found = {f".{ext}" for lang in LANGUAGES.values() for ext in lang.extensions}
|
|
811
|
+
if settings is not None:
|
|
812
|
+
found |= {f".{key}" for key in settings.languages}
|
|
813
|
+
return found
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def display_name(key: str) -> str:
|
|
817
|
+
""" "C++" for cpp; a language from config.toml goes by its family name."""
|
|
818
|
+
language = LANGUAGES.get(key)
|
|
819
|
+
if language is not None:
|
|
820
|
+
return language.name
|
|
821
|
+
name = family_name(key)
|
|
822
|
+
return name if name != "Other" else f".{key}"
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
# ┌──────────────────────────────────────────────────────────┐
|
|
826
|
+
# │ The language to submit │
|
|
827
|
+
# └──────────────────────────────────────────────────────────┘
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
def family(key: str, offered: Mapping[str, str]) -> list[tuple[str, str]]:
|
|
831
|
+
"""The (id, name) Codeforces languages of a language's family, best first:
|
|
832
|
+
- by the first of its preference patterns a name matches
|
|
833
|
+
- then newest first, by the version numbers in the name"""
|
|
834
|
+
language = LANGUAGES.get(key)
|
|
835
|
+
prefer = language.prefer if language else ()
|
|
836
|
+
exclude = language.exclude if language else ""
|
|
837
|
+
members = [
|
|
838
|
+
(lid, name)
|
|
839
|
+
for lid, name in offered.items()
|
|
840
|
+
if ext_of(name) == key and not (exclude and re.search(exclude, name))
|
|
841
|
+
]
|
|
842
|
+
|
|
843
|
+
def rank(item: tuple[str, str]):
|
|
844
|
+
name = item[1]
|
|
845
|
+
tier = next((i for i, p in enumerate(prefer) if re.search(p, name)), len(prefer))
|
|
846
|
+
return tier, *newest_first_key(name)
|
|
847
|
+
|
|
848
|
+
return sorted(members, key=rank)
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def _normal(text: str) -> str:
|
|
852
|
+
"""A language name for matching: no case, no spaces, C++ written G++."""
|
|
853
|
+
return re.sub(r"\s+", "", text.lower()).replace("c++", "g++")
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def choose_submit(key: str, spec: str, offered: Mapping[str, str]) -> tuple[str, str] | None:
|
|
857
|
+
"""The (id, name) to submit a language as, among offered:
|
|
858
|
+
- spec empty: the best of its family
|
|
859
|
+
- spec an id of its family (any id, for a language from config.toml)
|
|
860
|
+
- otherwise: the best language of the family whose name contains spec
|
|
861
|
+
("G++20"; a number like 20 too), also among the ones never picked by
|
|
862
|
+
themselves (Clang++20 Diagnostics)
|
|
863
|
+
Never another family's language: None when nothing offered fits."""
|
|
864
|
+
options = family(key, offered)
|
|
865
|
+
if not spec:
|
|
866
|
+
return options[0] if options else None
|
|
867
|
+
custom = key not in LANGUAGES # NOTE: from config.toml: its family is unknown
|
|
868
|
+
if spec.isdigit() and spec in offered and (custom or ext_of(offered[spec]) == key):
|
|
869
|
+
return spec, offered[spec]
|
|
870
|
+
wanted = _normal(spec)
|
|
871
|
+
matches = [item for item in options if wanted in _normal(item[1])]
|
|
872
|
+
if not matches and not spec.isdigit():
|
|
873
|
+
others = [
|
|
874
|
+
item
|
|
875
|
+
for item in offered.items()
|
|
876
|
+
if (custom or ext_of(item[1]) == key) and wanted in _normal(item[1])
|
|
877
|
+
]
|
|
878
|
+
matches = sorted(others, key=lambda item: newest_first_key(item[1]))
|
|
879
|
+
return matches[0] if matches else None
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
# ┌──────────────────────────────────────────────────────────┐
|
|
883
|
+
# │ Setting up │
|
|
884
|
+
# └──────────────────────────────────────────────────────────┘
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
@dataclass(frozen=True)
|
|
888
|
+
class Setup:
|
|
889
|
+
"""How a language is built, run and submitted here."""
|
|
890
|
+
|
|
891
|
+
key: str # "cpp"
|
|
892
|
+
name: str # "C++"
|
|
893
|
+
tool: str # what builds or runs it: "GCC 14 (g++-14)", or the command from config.toml
|
|
894
|
+
build: tuple[str, ...] | None # None: nothing to build
|
|
895
|
+
run: tuple[str, ...]
|
|
896
|
+
fingerprint: str = "" # the tool's version text: part of the build cache key
|
|
897
|
+
submit: tuple[str, str] | None = None # the (id, name) it is submitted as, when known
|
|
898
|
+
big_stack: bool = False # run with Codeforces' stack
|
|
899
|
+
copy_as: str = "" # the name {copy} gives the code in the build folder ("": its own)
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def setup(
|
|
903
|
+
key: str,
|
|
904
|
+
settings: "Settings",
|
|
905
|
+
*,
|
|
906
|
+
source: Path | None = None,
|
|
907
|
+
offered: Mapping[str, str] | None = None,
|
|
908
|
+
machine: Machine | None = None,
|
|
909
|
+
) -> Setup:
|
|
910
|
+
"""How to build and run a language here, and what to submit it as. Steps:
|
|
911
|
+
- the Codeforces language: the setting `submit`, else the family's best
|
|
912
|
+
- the tool: the setting `compiler`, else the best one found here
|
|
913
|
+
- the commands made for that tool and language, unless `build`/`run` are set
|
|
914
|
+
Raises CFError when no tool is found (with how to install one), or for an
|
|
915
|
+
unknown language without a `run` setting."""
|
|
916
|
+
machine = machine or Machine()
|
|
917
|
+
language = LANGUAGES.get(key)
|
|
918
|
+
conf = settings.language_settings(key)
|
|
919
|
+
offered = offered if offered is not None else known_languages()
|
|
920
|
+
submit = choose_submit(key, conf.submit, offered)
|
|
921
|
+
submit_name = submit[1] if submit else ""
|
|
922
|
+
build_words = list(conf.build) if conf.build is not None else None
|
|
923
|
+
run_words = list(conf.run) if conf.run is not None else None
|
|
924
|
+
|
|
925
|
+
if language is None:
|
|
926
|
+
if not run_words:
|
|
927
|
+
raise CFError(
|
|
928
|
+
f"cf-tool doesn't know .{key} files: give [{key}] a run command in "
|
|
929
|
+
f"{paths.config_file()}"
|
|
930
|
+
)
|
|
931
|
+
return Setup(
|
|
932
|
+
key,
|
|
933
|
+
display_name(key),
|
|
934
|
+
" ".join(run_words),
|
|
935
|
+
tuple(build_words) if build_words else None,
|
|
936
|
+
tuple(run_words),
|
|
937
|
+
fingerprint=" ".join(build_words or run_words),
|
|
938
|
+
submit=submit,
|
|
939
|
+
)
|
|
940
|
+
|
|
941
|
+
if conf.compiler:
|
|
942
|
+
tool = _configured_tool(language, conf.compiler, machine)
|
|
943
|
+
else:
|
|
944
|
+
tools = language.detect(machine)
|
|
945
|
+
if not tools:
|
|
946
|
+
raise CFError(
|
|
947
|
+
f"No {language.name} compiler or interpreter found. To get one: "
|
|
948
|
+
f"{language.install_hint(machine.platform)}"
|
|
949
|
+
)
|
|
950
|
+
tool = tools[0]
|
|
951
|
+
if key == "py" and "pypy" in submit_name.lower():
|
|
952
|
+
tool = next((t for t in tools if t.kind == "pypy"), tool) # NOTE: test as judged
|
|
953
|
+
ctx = Context(machine.platform, submit_name, conf.flags, source, machine.accepts)
|
|
954
|
+
build, run = language.commands(tool, ctx)
|
|
955
|
+
if "clang" in tool.kind and key == "cpp" and conf.build is None:
|
|
956
|
+
write_shim()
|
|
957
|
+
if conf.build is not None:
|
|
958
|
+
build = build_words or None
|
|
959
|
+
if run_words:
|
|
960
|
+
run = run_words
|
|
961
|
+
main_class = java_main_class(source) if key == "java" and source is not None else None
|
|
962
|
+
return Setup(
|
|
963
|
+
key,
|
|
964
|
+
language.name,
|
|
965
|
+
tool.label,
|
|
966
|
+
tuple(build) if build else None,
|
|
967
|
+
tuple(run),
|
|
968
|
+
fingerprint=tool.banner or tool.program,
|
|
969
|
+
submit=submit,
|
|
970
|
+
big_stack=language.big_stack,
|
|
971
|
+
copy_as=f"{main_class}.java" if main_class else "",
|
|
972
|
+
)
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
def _configured_tool(language: Language, program: str, machine: Machine) -> Tool:
|
|
976
|
+
"""The tool named by `compiler` in config.toml, as detection would see it."""
|
|
977
|
+
if not machine.which(program):
|
|
978
|
+
raise CFError(
|
|
979
|
+
f"{program}, the compiler set for [{language.key}] in {paths.config_file()}, "
|
|
980
|
+
"is not installed"
|
|
981
|
+
)
|
|
982
|
+
flag = {"java": "-version", "kt": "-version", "go": "version"}.get(language.key, "--version")
|
|
983
|
+
banner = machine.version(program, flag)
|
|
984
|
+
low = banner.lower()
|
|
985
|
+
if language.key in ("cpp", "c"):
|
|
986
|
+
kind = "apple clang" if "apple" in low else "clang" if "clang" in low else "gcc"
|
|
987
|
+
elif language.key == "py":
|
|
988
|
+
kind = "pypy" if "pypy" in low or "pypy" in program else "python"
|
|
989
|
+
elif language.key == "java":
|
|
990
|
+
kind = "jdk" if machine.which("javac") else "jre"
|
|
991
|
+
else:
|
|
992
|
+
kind = language.key
|
|
993
|
+
return Tool(program, program, banner, kind, _numbers(banner))
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
# ┌──────────────────────────────────────────────────────────┐
|
|
997
|
+
# │ Starter code │
|
|
998
|
+
# └──────────────────────────────────────────────────────────┘
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def starter(key: str, settings: "Settings", name: str = "template") -> tuple[str, str]:
|
|
1002
|
+
"""The starter code for a new file of a language, and where it came from
|
|
1003
|
+
(a path, or "built-in"). In order:
|
|
1004
|
+
- templates/<name>.<ext> in the config folder, for a name other than
|
|
1005
|
+
"template" (cf new brute)
|
|
1006
|
+
- the setting `template`
|
|
1007
|
+
- templates/template.<ext>
|
|
1008
|
+
- the built-in starter (none for a language from config.toml: "")"""
|
|
1009
|
+
from cf_tool.workspace import read_source
|
|
1010
|
+
|
|
1011
|
+
folder = paths.templates_dir()
|
|
1012
|
+
if name != "template":
|
|
1013
|
+
named = folder / f"{name}.{key}"
|
|
1014
|
+
if named.is_file():
|
|
1015
|
+
return read_source(named), str(named)
|
|
1016
|
+
conf = settings.language_settings(key)
|
|
1017
|
+
if conf.template:
|
|
1018
|
+
path = Path(conf.template).expanduser()
|
|
1019
|
+
if not path.is_file():
|
|
1020
|
+
raise CFError(
|
|
1021
|
+
f"The starter file for [{key}] in {paths.config_file()}, {path}, doesn't exist"
|
|
1022
|
+
)
|
|
1023
|
+
return read_source(path), str(path)
|
|
1024
|
+
default = folder / f"template.{key}"
|
|
1025
|
+
if default.is_file():
|
|
1026
|
+
return read_source(default), str(default)
|
|
1027
|
+
language = LANGUAGES.get(key)
|
|
1028
|
+
return (language.starter if language else ""), "built-in"
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
_PLACEHOLDER_RE = re.compile(r"\{\{\s*(\w+)\s*\}\}")
|
|
1032
|
+
# cf-tool 0.3's placeholders, still filled in
|
|
1033
|
+
_OLD_PLACEHOLDERS = {
|
|
1034
|
+
"$%U%$": "handle",
|
|
1035
|
+
"$%Y%$": "year",
|
|
1036
|
+
"$%M%$": "month",
|
|
1037
|
+
"$%D%$": "day",
|
|
1038
|
+
"$%h%$": "hour",
|
|
1039
|
+
"$%m%$": "minute",
|
|
1040
|
+
"$%s%$": "second",
|
|
1041
|
+
}
|
|
1042
|
+
_EMPTY_COMMENT_RE = re.compile(r"\s*(//|#|--|;)?\s*")
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
def render(text: str, values: Mapping[str, str], now: datetime | None = None) -> str:
|
|
1046
|
+
"""Starter code with its placeholders filled in: {{handle}} {{date}}
|
|
1047
|
+
{{time}} {{problem}} {{name}} {{url}}, and the old $%U%$ style. A comment
|
|
1048
|
+
line left empty (no problem to name, say) is dropped; an unknown
|
|
1049
|
+
placeholder stays as it is."""
|
|
1050
|
+
now = now or datetime.now()
|
|
1051
|
+
values = {
|
|
1052
|
+
"handle": "",
|
|
1053
|
+
"problem": "",
|
|
1054
|
+
"name": "",
|
|
1055
|
+
"url": "",
|
|
1056
|
+
"date": f"{now:%Y-%m-%d}",
|
|
1057
|
+
"time": f"{now:%H:%M}",
|
|
1058
|
+
"year": f"{now.year}",
|
|
1059
|
+
"month": f"{now.month:02d}",
|
|
1060
|
+
"day": f"{now.day:02d}",
|
|
1061
|
+
"hour": f"{now.hour:02d}",
|
|
1062
|
+
"minute": f"{now.minute:02d}",
|
|
1063
|
+
"second": f"{now.second:02d}",
|
|
1064
|
+
**values,
|
|
1065
|
+
}
|
|
1066
|
+
lines = []
|
|
1067
|
+
for line in text.splitlines(keepends=True):
|
|
1068
|
+
filled = _PLACEHOLDER_RE.sub(lambda m: values.get(m.group(1), m.group(0)), line)
|
|
1069
|
+
for placeholder, name in _OLD_PLACEHOLDERS.items():
|
|
1070
|
+
filled = filled.replace(placeholder, values.get(name, ""))
|
|
1071
|
+
if filled != line and _EMPTY_COMMENT_RE.fullmatch(filled.rstrip("\r\n")):
|
|
1072
|
+
continue # NOTE: a line of placeholders with nothing to fill in
|
|
1073
|
+
lines.append(filled)
|
|
1074
|
+
return "".join(lines)
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
# ┌──────────────────────────────────────────────────────────┐
|
|
1078
|
+
# │ bits/stdc++.h for clang │
|
|
1079
|
+
# └──────────────────────────────────────────────────────────┘
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
def shim_folder() -> Path:
|
|
1083
|
+
"""The include folder holding cf-tool's bits/stdc++.h for clang."""
|
|
1084
|
+
return paths.CACHE_DIR / "include"
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
def write_shim() -> Path:
|
|
1088
|
+
"""Write (or refresh) cf-tool's bits/stdc++.h; returns the header."""
|
|
1089
|
+
header = shim_folder() / "bits" / "stdc++.h"
|
|
1090
|
+
try:
|
|
1091
|
+
current = header.read_text(encoding="utf-8")
|
|
1092
|
+
except OSError:
|
|
1093
|
+
current = ""
|
|
1094
|
+
if current != STDCXX_SHIM:
|
|
1095
|
+
paths.write_atomic(header, STDCXX_SHIM)
|
|
1096
|
+
return header
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
STDCXX_SHIM = """\
|
|
1100
|
+
// bits/stdc++.h for compilers without one (clang, Apple clang): every
|
|
1101
|
+
// standard header, so `#include <bits/stdc++.h>` works as on Codeforces.
|
|
1102
|
+
// Written by cf-tool; changes here are overwritten.
|
|
1103
|
+
#ifndef CF_TOOL_BITS_STDCXX_H
|
|
1104
|
+
#define CF_TOOL_BITS_STDCXX_H
|
|
1105
|
+
|
|
1106
|
+
#include <cassert>
|
|
1107
|
+
#include <cctype>
|
|
1108
|
+
#include <cerrno>
|
|
1109
|
+
#include <cfloat>
|
|
1110
|
+
#include <cinttypes>
|
|
1111
|
+
#include <climits>
|
|
1112
|
+
#include <clocale>
|
|
1113
|
+
#include <cmath>
|
|
1114
|
+
#include <csetjmp>
|
|
1115
|
+
#include <csignal>
|
|
1116
|
+
#include <cstdarg>
|
|
1117
|
+
#include <cstddef>
|
|
1118
|
+
#include <cstdint>
|
|
1119
|
+
#include <cstdio>
|
|
1120
|
+
#include <cstdlib>
|
|
1121
|
+
#include <cstring>
|
|
1122
|
+
#include <ctime>
|
|
1123
|
+
#include <cwchar>
|
|
1124
|
+
#include <cwctype>
|
|
1125
|
+
|
|
1126
|
+
#include <algorithm>
|
|
1127
|
+
#include <array>
|
|
1128
|
+
#include <atomic>
|
|
1129
|
+
#include <bitset>
|
|
1130
|
+
#include <chrono>
|
|
1131
|
+
#include <complex>
|
|
1132
|
+
#include <condition_variable>
|
|
1133
|
+
#include <deque>
|
|
1134
|
+
#include <exception>
|
|
1135
|
+
#include <fstream>
|
|
1136
|
+
#include <functional>
|
|
1137
|
+
#include <initializer_list>
|
|
1138
|
+
#include <iomanip>
|
|
1139
|
+
#include <ios>
|
|
1140
|
+
#include <iosfwd>
|
|
1141
|
+
#include <iostream>
|
|
1142
|
+
#include <istream>
|
|
1143
|
+
#include <iterator>
|
|
1144
|
+
#include <limits>
|
|
1145
|
+
#include <list>
|
|
1146
|
+
#include <locale>
|
|
1147
|
+
#include <map>
|
|
1148
|
+
#include <memory>
|
|
1149
|
+
#include <mutex>
|
|
1150
|
+
#include <new>
|
|
1151
|
+
#include <numeric>
|
|
1152
|
+
#include <ostream>
|
|
1153
|
+
#include <queue>
|
|
1154
|
+
#include <random>
|
|
1155
|
+
#include <ratio>
|
|
1156
|
+
#include <regex>
|
|
1157
|
+
#include <set>
|
|
1158
|
+
#include <sstream>
|
|
1159
|
+
#include <stack>
|
|
1160
|
+
#include <stdexcept>
|
|
1161
|
+
#include <streambuf>
|
|
1162
|
+
#include <string>
|
|
1163
|
+
#include <system_error>
|
|
1164
|
+
#include <thread>
|
|
1165
|
+
#include <tuple>
|
|
1166
|
+
#include <type_traits>
|
|
1167
|
+
#include <typeindex>
|
|
1168
|
+
#include <typeinfo>
|
|
1169
|
+
#include <unordered_map>
|
|
1170
|
+
#include <unordered_set>
|
|
1171
|
+
#include <utility>
|
|
1172
|
+
#include <valarray>
|
|
1173
|
+
#include <vector>
|
|
1174
|
+
|
|
1175
|
+
#if __cplusplus >= 201703L
|
|
1176
|
+
#include <any>
|
|
1177
|
+
#include <charconv>
|
|
1178
|
+
#include <optional>
|
|
1179
|
+
#include <string_view>
|
|
1180
|
+
#include <variant>
|
|
1181
|
+
#endif
|
|
1182
|
+
|
|
1183
|
+
#if __cplusplus >= 202002L
|
|
1184
|
+
#include <bit>
|
|
1185
|
+
#include <compare>
|
|
1186
|
+
#include <concepts>
|
|
1187
|
+
#include <numbers>
|
|
1188
|
+
#include <span>
|
|
1189
|
+
#if __has_include(<ranges>)
|
|
1190
|
+
#include <ranges>
|
|
1191
|
+
#endif
|
|
1192
|
+
#endif
|
|
1193
|
+
|
|
1194
|
+
#endif
|
|
1195
|
+
"""
|