codekingpro/portable-devtools
114k
1"""distutils.command.config2 3Implements the Distutils 'config' command, a (mostly) empty command class4that exists mainly to be sub-classed by specific module distributions and5applications. The idea is that while every "config" command is different,6at least they're all named the same, and users always see "config" in the7list of standard commands. Also, this is a good place to put common8configure-like tasks: "try to compile this C code", or "figure out where9this header file lives".10"""11 12from __future__ import annotations13 14import os15import pathlib16import re17from collections.abc import Sequence18from distutils._log import log19 20from ..core import Command21from ..errors import DistutilsExecError22from ..sysconfig import customize_compiler23 24LANG_EXT = {"c": ".c", "c++": ".cxx"}25 26 27class config(Command):28 description = "prepare to build"29 30 user_options = [31 ('compiler=', None, "specify the compiler type"),32 ('cc=', None, "specify the compiler executable"),33 ('include-dirs=', 'I', "list of directories to search for header files"),34 ('define=', 'D', "C preprocessor macros to define"),35 ('undef=', 'U', "C preprocessor macros to undefine"),36 ('libraries=', 'l', "external C libraries to link with"),37 ('library-dirs=', 'L', "directories to search for external C libraries"),38 ('noisy', None, "show every action (compile, link, run, ...) taken"),39 (40 'dump-source',41 None,42 "dump generated source files before attempting to compile them",43 ),44 ]45 46 # The three standard command methods: since the "config" command47 # does nothing by default, these are empty.48 49 def initialize_options(self):50 self.compiler = None51 self.cc = None52 self.include_dirs = None53 self.libraries = None54 self.library_dirs = None55 56 # maximal output for now57 self.noisy = 158 self.dump_source = 159 60 # list of temporary files generated along-the-way that we have61 # to clean at some point62 self.temp_files = []63 64 def finalize_options(self):65 if self.include_dirs is None:66 self.include_dirs = self.distribution.include_dirs or []67 elif isinstance(self.include_dirs, str):68 self.include_dirs = self.include_dirs.split(os.pathsep)69 70 if self.libraries is None:71 self.libraries = []72 elif isinstance(self.libraries, str):73 self.libraries = [self.libraries]74 75 if self.library_dirs is None:76 self.library_dirs = []77 elif isinstance(self.library_dirs, str):78 self.library_dirs = self.library_dirs.split(os.pathsep)79 80 def run(self):81 pass82 83 # Utility methods for actual "config" commands. The interfaces are84 # loosely based on Autoconf macros of similar names. Sub-classes85 # may use these freely.86 87 def _check_compiler(self):88 """Check that 'self.compiler' really is a CCompiler object;89 if not, make it one.90 """91 # We do this late, and only on-demand, because this is an expensive92 # import.93 from ..ccompiler import CCompiler, new_compiler94 95 if not isinstance(self.compiler, CCompiler):96 self.compiler = new_compiler(97 compiler=self.compiler, dry_run=self.dry_run, force=198 )99 customize_compiler(self.compiler)100 if self.include_dirs:101 self.compiler.set_include_dirs(self.include_dirs)102 if self.libraries:103 self.compiler.set_libraries(self.libraries)104 if self.library_dirs:105 self.compiler.set_library_dirs(self.library_dirs)106 107 def _gen_temp_sourcefile(self, body, headers, lang):108 filename = "_configtest" + LANG_EXT[lang]109 with open(filename, "w", encoding='utf-8') as file:110 if headers:111 for header in headers:112 file.write("#include <%s>\n" % header)113 file.write("\n")114 file.write(body)115 if body[-1] != "\n":116 file.write("\n")117 return filename118 119 def _preprocess(self, body, headers, include_dirs, lang):120 src = self._gen_temp_sourcefile(body, headers, lang)121 out = "_configtest.i"122 self.temp_files.extend([src, out])123 self.compiler.preprocess(src, out, include_dirs=include_dirs)124 return (src, out)125 126 def _compile(self, body, headers, include_dirs, lang):127 src = self._gen_temp_sourcefile(body, headers, lang)128 if self.dump_source:129 dump_file(src, "compiling '%s':" % src)130 (obj,) = self.compiler.object_filenames([src])131 self.temp_files.extend([src, obj])132 self.compiler.compile([src], include_dirs=include_dirs)133 return (src, obj)134 135 def _link(self, body, headers, include_dirs, libraries, library_dirs, lang):136 (src, obj) = self._compile(body, headers, include_dirs, lang)137 prog = os.path.splitext(os.path.basename(src))[0]138 self.compiler.link_executable(139 [obj],140 prog,141 libraries=libraries,142 library_dirs=library_dirs,143 target_lang=lang,144 )145 146 if self.compiler.exe_extension is not None:147 prog = prog + self.compiler.exe_extension148 self.temp_files.append(prog)149 150 return (src, obj, prog)151 152 def _clean(self, *filenames):153 if not filenames:154 filenames = self.temp_files155 self.temp_files = []156 log.info("removing: %s", ' '.join(filenames))157 for filename in filenames:158 try:159 os.remove(filename)160 except OSError:161 pass162 163 # XXX these ignore the dry-run flag: what to do, what to do? even if164 # you want a dry-run build, you still need some sort of configuration165 # info. My inclination is to make it up to the real config command to166 # consult 'dry_run', and assume a default (minimal) configuration if167 # true. The problem with trying to do it here is that you'd have to168 # return either true or false from all the 'try' methods, neither of169 # which is correct.170 171 # XXX need access to the header search path and maybe default macros.172 173 def try_cpp(self, body=None, headers=None, include_dirs=None, lang="c"):174 """Construct a source file from 'body' (a string containing lines175 of C/C++ code) and 'headers' (a list of header files to include)176 and run it through the preprocessor. Return true if the177 preprocessor succeeded, false if there were any errors.178 ('body' probably isn't of much use, but what the heck.)179 """180 from ..ccompiler import CompileError181 182 self._check_compiler()183 ok = True184 try:185 self._preprocess(body, headers, include_dirs, lang)186 except CompileError:187 ok = False188 189 self._clean()190 return ok191 192 def search_cpp(self, pattern, body=None, headers=None, include_dirs=None, lang="c"):193 """Construct a source file (just like 'try_cpp()'), run it through194 the preprocessor, and return true if any line of the output matches195 'pattern'. 'pattern' should either be a compiled regex object or a196 string containing a regex. If both 'body' and 'headers' are None,197 preprocesses an empty file -- which can be useful to determine the198 symbols the preprocessor and compiler set by default.199 """200 self._check_compiler()201 src, out = self._preprocess(body, headers, include_dirs, lang)202 203 if isinstance(pattern, str):204 pattern = re.compile(pattern)205 206 with open(out, encoding='utf-8') as file:207 match = any(pattern.search(line) for line in file)208 209 self._clean()210 return match211 212 def try_compile(self, body, headers=None, include_dirs=None, lang="c"):213 """Try to compile a source file built from 'body' and 'headers'.214 Return true on success, false otherwise.215 """216 from ..ccompiler import CompileError217 218 self._check_compiler()219 try:220 self._compile(body, headers, include_dirs, lang)221 ok = True222 except CompileError:223 ok = False224 225 log.info(ok and "success!" or "failure.")226 self._clean()227 return ok228 229 def try_link(230 self,231 body,232 headers=None,233 include_dirs=None,234 libraries=None,235 library_dirs=None,236 lang="c",237 ):238 """Try to compile and link a source file, built from 'body' and239 'headers', to executable form. Return true on success, false240 otherwise.241 """242 from ..ccompiler import CompileError, LinkError243 244 self._check_compiler()245 try:246 self._link(body, headers, include_dirs, libraries, library_dirs, lang)247 ok = True248 except (CompileError, LinkError):249 ok = False250 251 log.info(ok and "success!" or "failure.")252 self._clean()253 return ok254 255 def try_run(256 self,257 body,258 headers=None,259 include_dirs=None,260 libraries=None,261 library_dirs=None,262 lang="c",263 ):264 """Try to compile, link to an executable, and run a program265 built from 'body' and 'headers'. Return true on success, false266 otherwise.267 """268 from ..ccompiler import CompileError, LinkError269 270 self._check_compiler()271 try:272 src, obj, exe = self._link(273 body, headers, include_dirs, libraries, library_dirs, lang274 )275 self.spawn([exe])276 ok = True277 except (CompileError, LinkError, DistutilsExecError):278 ok = False279 280 log.info(ok and "success!" or "failure.")281 self._clean()282 return ok283 284 # -- High-level methods --------------------------------------------285 # (these are the ones that are actually likely to be useful286 # when implementing a real-world config command!)287 288 def check_func(289 self,290 func,291 headers=None,292 include_dirs=None,293 libraries=None,294 library_dirs=None,295 decl=0,296 call=0,297 ):298 """Determine if function 'func' is available by constructing a299 source file that refers to 'func', and compiles and links it.300 If everything succeeds, returns true; otherwise returns false.301 302 The constructed source file starts out by including the header303 files listed in 'headers'. If 'decl' is true, it then declares304 'func' (as "int func()"); you probably shouldn't supply 'headers'305 and set 'decl' true in the same call, or you might get errors about306 a conflicting declarations for 'func'. Finally, the constructed307 'main()' function either references 'func' or (if 'call' is true)308 calls it. 'libraries' and 'library_dirs' are used when309 linking.310 """311 self._check_compiler()312 body = []313 if decl:314 body.append("int %s ();" % func)315 body.append("int main () {")316 if call:317 body.append(" %s();" % func)318 else:319 body.append(" %s;" % func)320 body.append("}")321 body = "\n".join(body) + "\n"322 323 return self.try_link(body, headers, include_dirs, libraries, library_dirs)324 325 def check_lib(326 self,327 library,328 library_dirs=None,329 headers=None,330 include_dirs=None,331 other_libraries: Sequence[str] = [],332 ):333 """Determine if 'library' is available to be linked against,334 without actually checking that any particular symbols are provided335 by it. 'headers' will be used in constructing the source file to336 be compiled, but the only effect of this is to check if all the337 header files listed are available. Any libraries listed in338 'other_libraries' will be included in the link, in case 'library'339 has symbols that depend on other libraries.340 """341 self._check_compiler()342 return self.try_link(343 "int main (void) { }",344 headers,345 include_dirs,346 [library] + list(other_libraries),347 library_dirs,348 )349 350 def check_header(self, header, include_dirs=None, library_dirs=None, lang="c"):351 """Determine if the system header file named by 'header_file'352 exists and can be found by the preprocessor; return true if so,353 false otherwise.354 """355 return self.try_cpp(356 body="/* No body */", headers=[header], include_dirs=include_dirs357 )358 359 360def dump_file(filename, head=None):361 """Dumps a file content into log.info.362 363 If head is not None, will be dumped before the file content.364 """365 if head is None:366 log.info('%s', filename)367 else:368 log.info(head)369 log.info(pathlib.Path(filename).read_text(encoding='utf-8'))370 