Coverage for src / competitive_verifier / oj / verify / languages / cplusplus.py: 80%
136 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-03-05 16:00 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-03-05 16:00 +0000
1import os
2import pathlib
3import platform
4import shlex
5import shutil
6from logging import getLogger
7from typing import Any
9from pydantic import BaseModel
11from competitive_verifier.exec import command_stdout
12from competitive_verifier.log import GitHubMessageParams
13from competitive_verifier.oj.verify.models import (
14 Language,
15 LanguageEnvironment,
16 OjVerifyLanguageConfig,
17)
19from . import special_comments
20from .cplusplus_bundle import Bundler
22# ruff: noqa: N803
24logger = getLogger(__name__)
27class OjVerifyCPlusPlusConfigEnv(BaseModel):
28 CXX: str
29 CXXFLAGS: list[str] | None = None
32class OjVerifyCPlusPlusConfig(OjVerifyLanguageConfig):
33 environments: list[OjVerifyCPlusPlusConfigEnv] | None = None
36class CPlusPlusLanguageEnvironment(LanguageEnvironment):
37 cxx: pathlib.Path
38 cxx_flags: list[str]
40 def __init__(self, *, CXX: pathlib.Path, CXXFLAGS: list[str]):
41 self.cxx = CXX
42 self.cxx_flags = CXXFLAGS
44 @property
45 def name(self) -> str:
46 return self.cxx.name
48 def get_compile_command(
49 self, path: pathlib.Path, *, basedir: pathlib.Path, tempdir: pathlib.Path
50 ) -> list[str]:
51 return [
52 str(self.cxx),
53 *self.cxx_flags,
54 "-I",
55 str(basedir),
56 "-o",
57 str(tempdir / "a.out"),
58 str(path),
59 ]
61 def get_execute_command(
62 self, path: pathlib.Path, *, basedir: pathlib.Path, tempdir: pathlib.Path
63 ) -> str:
64 return str(tempdir / "a.out")
66 def is_clang(self) -> bool:
67 return "clang++" in self.cxx.name
69 def is_gcc(self) -> bool:
70 return not self.is_clang() and "g++" in self.cxx.name
73def _cplusplus_list_depending_files(
74 path: pathlib.Path,
75 *,
76 CXX: pathlib.Path,
77 CXXFLAGS: list[str],
78) -> list[pathlib.Path]:
79 is_windows = platform.uname().system == "Windows"
80 command = [str(CXX), *CXXFLAGS, "-MM", str(path)]
81 try:
82 data = command_stdout(command)
83 except Exception:
84 logger.exception(
85 "failed to analyze dependencies with %s: %s (hint: Please check #include directives of the file and its dependencies."
86 " The paths must exist, must not contain '\\', and must be case-sensitive.)",
87 CXX,
88 path,
89 exc_info=False,
90 )
91 raise
92 logger.debug("dependencies of %s: %r", path, data)
93 makefile_rule = shlex.split(
94 data.strip().replace("\\\n", "").replace("\\\r\n", ""),
95 posix=not is_windows,
96 )
97 return [pathlib.Path(path).resolve() for path in makefile_rule[1:]]
100def _cplusplus_list_defined_macros(
101 path: pathlib.Path, *, CXX: pathlib.Path, CXXFLAGS: list[str]
102) -> dict[str, str]:
103 command = [str(CXX), *CXXFLAGS, "-dM", "-E", str(path)]
104 data = command_stdout(command)
105 define: dict[str, str] = {}
106 for line in data.splitlines():
107 assert line.startswith("#define ")
108 a, _, b = line[len("#define ") :].partition(" ")
109 if (b.startswith('"') and b.endswith('"')) or (
110 b.startswith("'") and b.endswith("'")
111 ):
112 b = b[1:-1]
113 define[a] = b
114 return define
117_NOT_SPECIAL_COMMENTS = "*NOT_SPECIAL_COMMENTS*"
118_PROBLEM = "PROBLEM"
119_IGNORE = "IGNORE"
120_IGNORE_IF_CLANG = "IGNORE_IF_CLANG"
121_IGNORE_IF_GCC = "IGNORE_IF_GCC"
122_ERROR = "ERROR"
123_STANDALONE = "STANDALONE"
126class CPlusPlusLanguage(Language):
127 config: OjVerifyCPlusPlusConfig
129 def __init__(self, *, config: OjVerifyCPlusPlusConfig | None):
130 self.config = config or OjVerifyCPlusPlusConfig()
132 def _list_environments(self) -> list[CPlusPlusLanguageEnvironment]:
133 default_CXXFLAGS = ["--std=c++17", "-O2", "-Wall", "-g"] # noqa: N806
134 if platform.system() == "Windows" or "CYGWIN" in platform.system(): 134 ↛ 135line 134 didn't jump to line 135 because the condition on line 134 was never true
135 default_CXXFLAGS.append("-Wl,-stack,0x10000000")
136 if platform.system() == "Darwin": 136 ↛ 137line 136 didn't jump to line 137 because the condition on line 136 was never true
137 default_CXXFLAGS.append("-Wl,-stack_size,0x10000000")
138 if ( 138 ↛ 142line 138 didn't jump to line 142 because the condition on line 138 was never true
139 platform.uname().system == "Linux"
140 and "Microsoft" in platform.uname().release
141 ):
142 default_CXXFLAGS.append("-fsplit-stack")
144 if "CXXFLAGS" in os.environ and not self.config.environments: 144 ↛ 145line 144 didn't jump to line 145 because the condition on line 144 was never true
145 logger.warning(
146 "Usage of $CXXFLAGS envvar to specify options is deprecated and will be removed soon",
147 extra={"github": GitHubMessageParams()},
148 )
149 default_CXXFLAGS = shlex.split(os.environ["CXXFLAGS"]) # noqa: N806
151 envs: list[CPlusPlusLanguageEnvironment] = []
152 if self.config.environments:
153 # configured: use specified CXX & CXXFLAGS
154 envs.extend(
155 CPlusPlusLanguageEnvironment(
156 CXX=pathlib.Path(env.CXX),
157 CXXFLAGS=env.CXXFLAGS or default_CXXFLAGS,
158 )
159 for env in self.config.environments
160 )
162 elif "CXX" in os.environ: 162 ↛ 164line 162 didn't jump to line 164 because the condition on line 162 was never true
163 # old-style: 以前は $CXX を使ってたけど設定ファイルに移行したい
164 logger.warning(
165 "Usage of $CXX envvar to restrict compilers is deprecated and will be removed soon",
166 extra={"github": GitHubMessageParams()},
167 )
168 envs.append(
169 CPlusPlusLanguageEnvironment(
170 CXX=pathlib.Path(os.environ["CXX"]), CXXFLAGS=default_CXXFLAGS
171 )
172 )
174 else:
175 # default: use found compilers
176 for name in ("g++", "clang++"):
177 path = shutil.which(name)
178 if path is not None: 178 ↛ 176line 178 didn't jump to line 176 because the condition on line 178 was always true
179 envs.append(
180 CPlusPlusLanguageEnvironment(
181 CXX=pathlib.Path(path), CXXFLAGS=default_CXXFLAGS
182 )
183 )
185 if not envs: 185 ↛ 186line 185 didn't jump to line 186 because the condition on line 185 was never true
186 raise RuntimeError("No C++ compilers found")
187 return envs
189 def list_attributes(
190 self, path: pathlib.Path, *, basedir: pathlib.Path
191 ) -> dict[str, Any]:
192 attributes: dict[str, Any] = {}
194 comments = special_comments.list_special_comments(path.resolve())
195 if comments: 195 ↛ 196line 195 didn't jump to line 196 because the condition on line 195 was never true
196 attributes.update(comments)
197 else:
198 # use old-style if special comments not found
199 # #define PROBLEM "https://..." の形式は複数 environments との相性がよくない。あと遅い
200 attributes[_NOT_SPECIAL_COMMENTS] = ""
201 all_ignored = True
202 for env in self._list_environments():
203 macros = _cplusplus_list_defined_macros(
204 path.resolve(),
205 CXX=env.cxx,
206 CXXFLAGS=[*env.cxx_flags, "-I", str(basedir)],
207 )
209 # convert macros to attributes
210 if _IGNORE not in macros: 210 ↛ 222line 210 didn't jump to line 222 because the condition on line 210 was always true
211 if _STANDALONE in macros:
212 attributes[_STANDALONE] = ""
214 for key in [_PROBLEM, _ERROR]:
215 if all_ignored:
216 # the first non-ignored environment
217 if key in macros:
218 attributes[key] = macros[key]
219 else:
220 assert attributes.get(key) == macros.get(key)
221 all_ignored = False
222 elif env.is_gcc():
223 attributes[_IGNORE_IF_GCC] = ""
224 elif env.is_clang():
225 attributes[_IGNORE_IF_CLANG] = ""
226 else:
227 attributes[_IGNORE] = ""
228 if all_ignored: 228 ↛ 229line 228 didn't jump to line 229 because the condition on line 228 was never true
229 attributes[_IGNORE] = ""
231 attributes.setdefault("links", [])
232 attributes["links"].extend(special_comments.list_embedded_urls(path))
233 return attributes
235 def list_dependencies(
236 self, path: pathlib.Path, *, basedir: pathlib.Path
237 ) -> list[pathlib.Path]:
238 env = self._list_environments()[0]
239 return _cplusplus_list_depending_files(
240 path.resolve(),
241 CXX=env.cxx,
242 CXXFLAGS=[*env.cxx_flags, "-I", str(basedir)],
243 )
245 def bundle(self, path: pathlib.Path, *, basedir: pathlib.Path) -> bytes | None:
246 include_paths: list[pathlib.Path] = [basedir]
247 assert isinstance(include_paths, list)
248 bundler = Bundler(iquotes=include_paths)
249 bundler.update(path)
250 return bundler.get()
252 def list_environments(
253 self, path: pathlib.Path, *, basedir: pathlib.Path
254 ) -> list[CPlusPlusLanguageEnvironment]:
255 attributes = self.list_attributes(path, basedir=basedir)
256 envs: list[CPlusPlusLanguageEnvironment] = []
257 for env in self._list_environments():
258 if env.is_gcc() and _IGNORE_IF_GCC in attributes: 258 ↛ 259line 258 didn't jump to line 259 because the condition on line 258 was never true
259 continue
260 if env.is_clang() and _IGNORE_IF_CLANG in attributes: 260 ↛ 261line 260 didn't jump to line 261 because the condition on line 260 was never true
261 continue
262 envs.append(env)
263 return envs