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

1import os 

2import pathlib 

3import platform 

4import shlex 

5import shutil 

6from logging import getLogger 

7from typing import Any 

8 

9from pydantic import BaseModel 

10 

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) 

18 

19from . import special_comments 

20from .cplusplus_bundle import Bundler 

21 

22# ruff: noqa: N803 

23 

24logger = getLogger(__name__) 

25 

26 

27class OjVerifyCPlusPlusConfigEnv(BaseModel): 

28 CXX: str 

29 CXXFLAGS: list[str] | None = None 

30 

31 

32class OjVerifyCPlusPlusConfig(OjVerifyLanguageConfig): 

33 environments: list[OjVerifyCPlusPlusConfigEnv] | None = None 

34 

35 

36class CPlusPlusLanguageEnvironment(LanguageEnvironment): 

37 cxx: pathlib.Path 

38 cxx_flags: list[str] 

39 

40 def __init__(self, *, CXX: pathlib.Path, CXXFLAGS: list[str]): 

41 self.cxx = CXX 

42 self.cxx_flags = CXXFLAGS 

43 

44 @property 

45 def name(self) -> str: 

46 return self.cxx.name 

47 

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 ] 

60 

61 def get_execute_command( 

62 self, path: pathlib.Path, *, basedir: pathlib.Path, tempdir: pathlib.Path 

63 ) -> str: 

64 return str(tempdir / "a.out") 

65 

66 def is_clang(self) -> bool: 

67 return "clang++" in self.cxx.name 

68 

69 def is_gcc(self) -> bool: 

70 return not self.is_clang() and "g++" in self.cxx.name 

71 

72 

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:]] 

98 

99 

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 

115 

116 

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" 

124 

125 

126class CPlusPlusLanguage(Language): 

127 config: OjVerifyCPlusPlusConfig 

128 

129 def __init__(self, *, config: OjVerifyCPlusPlusConfig | None): 

130 self.config = config or OjVerifyCPlusPlusConfig() 

131 

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") 

143 

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 

150 

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 ) 

161 

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 ) 

173 

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 ) 

184 

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 

188 

189 def list_attributes( 

190 self, path: pathlib.Path, *, basedir: pathlib.Path 

191 ) -> dict[str, Any]: 

192 attributes: dict[str, Any] = {} 

193 

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 ) 

208 

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] = "" 

213 

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] = "" 

230 

231 attributes.setdefault("links", []) 

232 attributes["links"].extend(special_comments.list_embedded_urls(path)) 

233 return attributes 

234 

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 ) 

244 

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() 

251 

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