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