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

1import os 

2import pathlib 

3import platform 

4import shlex 

5import shutil 

6from logging import getLogger 

7from typing import Any 

8 

9from pydantic import BaseModel, Field 

10 

11from competitive_verifier.exec import command_stdout 

12from competitive_verifier.log import GitHubMessageParams 

13 

14from . import special_comments 

15from .base import Language, LanguageEnvironment, OjVerifyLanguageConfig 

16from .cplusplus_bundle import Bundler 

17 

18# ruff: noqa: N803 

19 

20logger = getLogger(__name__) 

21 

22 

23class OjVerifyCPlusPlusConfigEnv(BaseModel): 

24 CXX: str 

25 CXXFLAGS: list[str] | None = None 

26 

27 

28class OjVerifyCPlusPlusConfig(OjVerifyLanguageConfig): 

29 read_macros: bool = True 

30 environments: list[OjVerifyCPlusPlusConfigEnv] | None = None 

31 

32 

33class CPlusPlusLanguageEnvironment(LanguageEnvironment): 

34 cxx: pathlib.Path 

35 cxx_flags: list[str] 

36 

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

38 self.cxx = CXX 

39 self.cxx_flags = CXXFLAGS 

40 

41 @property 

42 def name(self) -> str: 

43 return self.cxx.name 

44 

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 ] 

57 

58 def get_execute_command( 

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

60 ) -> str: 

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

62 

63 def is_clang(self) -> bool: 

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

65 

66 def is_gcc(self) -> bool: 

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

68 

69 

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

95 

96 

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 

112 

113 

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" 

121 

122 

123class CPlusPlusLanguage(Language): 

124 config: OjVerifyCPlusPlusConfig = Field(default_factory=OjVerifyCPlusPlusConfig) 

125 

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

137 

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 

144 

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 ) 

155 

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 ) 

167 

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 ) 

178 

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 

182 

183 def list_attributes( 

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

185 ) -> dict[str, Any]: 

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

187 

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 ) 

202 

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

207 

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

224 

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

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

227 return attributes 

228 

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 ) 

238 

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

245 

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