codekingpro/portable-devtools
115k
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 ..ccompiler import CCompiler, CompileError, LinkError, new_compiler21from ..core import Command22from ..errors import DistutilsExecError23from ..sysconfig import customize_compiler24 25LANG_EXT = {"c": ".c", "c++": ".cxx"}26 27 28class config(Command):29 description = "prepare to build"30 31 user_options = [32 ('compiler=', None, "specify the compiler type"),33 ('cc=', None, "specify the compiler executable"),34 ('include-dirs=', 'I', "list of directories to search for header files"),35 ('define=', 'D', "C preprocessor macros to define"),36 ('undef=', 'U', "C preprocessor macros to undefine"),37 ('libraries=', 'l', "external C libraries to link with"),38 ('library-dirs=', 'L', "directories to search for external C libraries"),39 ('noisy', None, "show every action (compile, link, run, ...) taken"),40 (41 'dump-source',42 None,43 "dump generated source files before attempting to compile them",44 ),45 ]46 47 # The three standard command methods: since the "config" command48 # does nothing by default, these are empty.49 50 def initialize_options(self):51 self.compiler = None52 self.cc = None53 self.include_dirs = None54 self.libraries = None55 self.library_dirs = None56 57 # maximal output for now58 self.noisy = 159 self.dump_source = 160 61 # list of temporary files generated along-the-way that we have62 # to clean at some point63 self.temp_files = []64 65 def finalize_options(self):66 if self.include_dirs is None:67 self.include_dirs = self.distribution.include_dirs or []68 elif isinstance(self.include_dirs, str):69 self.include_dirs = self.include_dirs.split(os.pathsep)70 71 if self.libraries is None:72 self.libraries = []73 elif isinstance(self.libraries, str):74 self.libraries = [self.libraries]75 76 if self.library_dirs is None:77 self.library_dirs = []78 elif isinstance(self.library_dirs, str):79 self.library_dirs = self.library_dirs.split(os.pathsep)80 81 def run(self):82 pass83 84 # Utility methods for actual "config" commands. The interfaces are85 # loosely based on Autoconf macros of similar names. Sub-classes86 # may use these freely.87 88 def _check_compiler(self):89 """Check that 'self.compiler' really is a CCompiler object;90 if not, make it one.91 """92 if not isinstance(self.compiler, CCompiler):93 self.compiler = new_compiler(compiler=self.compiler, force=True)94 customize_compiler(self.compiler)95 if self.include_dirs:96 self.compiler.set_include_dirs(self.include_dirs)97 if self.libraries:98 self.compiler.set_libraries(self.libraries)99 if self.library_dirs:100 self.compiler.set_library_dirs(self.library_dirs)101 102 def _gen_temp_sourcefile(self, body, headers, lang):103 filename = "_configtest" + LANG_EXT[lang]104 with open(filename, "w", encoding='utf-8') as file:105 if headers:106 for header in headers:107 file.write(f"#include <{header}>\n")108 file.write("\n")109 file.write(body)110 if body[-1] != "\n":111 file.write("\n")112 return filename113 114 def _preprocess(self, body, headers, include_dirs, lang):115 src = self._gen_temp_sourcefile(body, headers, lang)116 out = "_configtest.i"117 self.temp_files.extend([src, out])118 self.compiler.preprocess(src, out, include_dirs=include_dirs)119 return (src, out)120 121 def _compile(self, body, headers, include_dirs, lang):122 src = self._gen_temp_sourcefile(body, headers, lang)123 if self.dump_source:124 dump_file(src, f"compiling '{src}':")125 (obj,) = self.compiler.object_filenames([src])126 self.temp_files.extend([src, obj])127 self.compiler.compile([src], include_dirs=include_dirs)128 return (src, obj)129 130 def _link(self, body, headers, include_dirs, libraries, library_dirs, lang):131 (src, obj) = self._compile(body, headers, include_dirs, lang)132 prog = os.path.splitext(os.path.basename(src))[0]133 self.compiler.link_executable(134 [obj],135 prog,136 libraries=libraries,137 library_dirs=library_dirs,138 target_lang=lang,139 )140 141 if self.compiler.exe_extension is not None:142 prog = prog + self.compiler.exe_extension143 self.temp_files.append(prog)144 145 return (src, obj, prog)146 147 def _clean(self, *filenames):148 if not filenames:149 filenames = self.temp_files150 self.temp_files = []151 log.info("removing: %s", ' '.join(filenames))152 for filename in filenames:153 try:154 os.remove(filename)155 except OSError:156 pass157 158 # XXX need access to the header search path and maybe default macros.159 160 def try_cpp(self, body=None, headers=None, include_dirs=None, lang="c"):161 """Construct a source file from 'body' (a string containing lines162 of C/C++ code) and 'headers' (a list of header files to include)163 and run it through the preprocessor. Return true if the164 preprocessor succeeded, false if there were any errors.165 ('body' probably isn't of much use, but what the heck.)166 """167 self._check_compiler()168 ok = True169 try:170 self._preprocess(body, headers, include_dirs, lang)171 except CompileError:172 ok = False173 174 self._clean()175 return ok176 177 def search_cpp(self, pattern, body=None, headers=None, include_dirs=None, lang="c"):178 """Construct a source file (just like 'try_cpp()'), run it through179 the preprocessor, and return true if any line of the output matches180 'pattern'. 'pattern' should either be a compiled regex object or a181 string containing a regex. If both 'body' and 'headers' are None,182 preprocesses an empty file -- which can be useful to determine the183 symbols the preprocessor and compiler set by default.184 """185 self._check_compiler()186 src, out = self._preprocess(body, headers, include_dirs, lang)187 188 if isinstance(pattern, str):189 pattern = re.compile(pattern)190 191 with open(out, encoding='utf-8') as file:192 match = any(pattern.search(line) for line in file)193 194 self._clean()195 return match196 197 def try_compile(self, body, headers=None, include_dirs=None, lang="c"):198 """Try to compile a source file built from 'body' and 'headers'.199 Return true on success, false otherwise.200 """201 self._check_compiler()202 try:203 self._compile(body, headers, include_dirs, lang)204 ok = True205 except CompileError:206 ok = False207 208 log.info(ok and "success!" or "failure.")209 self._clean()210 return ok211 212 def try_link(213 self,214 body,215 headers=None,216 include_dirs=None,217 libraries=None,218 library_dirs=None,219 lang="c",220 ):221 """Try to compile and link a source file, built from 'body' and222 'headers', to executable form. Return true on success, false223 otherwise.224 """225 self._check_compiler()226 try:227 self._link(body, headers, include_dirs, libraries, library_dirs, lang)228 ok = True229 except (CompileError, LinkError):230 ok = False231 232 log.info(ok and "success!" or "failure.")233 self._clean()234 return ok235 236 def try_run(237 self,238 body,239 headers=None,240 include_dirs=None,241 libraries=None,242 library_dirs=None,243 lang="c",244 ):245 """Try to compile, link to an executable, and run a program246 built from 'body' and 'headers'. Return true on success, false247 otherwise.248 """249 self._check_compiler()250 try:251 src, obj, exe = self._link(252 body, headers, include_dirs, libraries, library_dirs, lang253 )254 self.spawn([exe])255 ok = True256 except (CompileError, LinkError, DistutilsExecError):257 ok = False258 259 log.info(ok and "success!" or "failure.")260 self._clean()261 return ok262 263 # -- High-level methods --------------------------------------------264 # (these are the ones that are actually likely to be useful265 # when implementing a real-world config command!)266 267 def check_func(268 self,269 func,270 headers=None,271 include_dirs=None,272 libraries=None,273 library_dirs=None,274 decl=False,275 call=False,276 ):277 """Determine if function 'func' is available by constructing a278 source file that refers to 'func', and compiles and links it.279 If everything succeeds, returns true; otherwise returns false.280 281 The constructed source file starts out by including the header282 files listed in 'headers'. If 'decl' is true, it then declares283 'func' (as "int func()"); you probably shouldn't supply 'headers'284 and set 'decl' true in the same call, or you might get errors about285 a conflicting declarations for 'func'. Finally, the constructed286 'main()' function either references 'func' or (if 'call' is true)287 calls it. 'libraries' and 'library_dirs' are used when288 linking.289 """290 self._check_compiler()291 body = []292 if decl:293 body.append(f"int {func} ();")294 body.append("int main () {")295 if call:296 body.append(f" {func}();")297 else:298 body.append(f" {func};")299 body.append("}")300 body = "\n".join(body) + "\n"301 302 return self.try_link(body, headers, include_dirs, libraries, library_dirs)303 304 def check_lib(305 self,306 library,307 library_dirs=None,308 headers=None,309 include_dirs=None,310 other_libraries: Sequence[str] = [],311 ):312 """Determine if 'library' is available to be linked against,313 without actually checking that any particular symbols are provided314 by it. 'headers' will be used in constructing the source file to315 be compiled, but the only effect of this is to check if all the316 header files listed are available. Any libraries listed in317 'other_libraries' will be included in the link, in case 'library'318 has symbols that depend on other libraries.319 """320 self._check_compiler()321 return self.try_link(322 "int main (void) { }",323 headers,324 include_dirs,325 [library] + list(other_libraries),326 library_dirs,327 )328 329 def check_header(self, header, include_dirs=None, library_dirs=None, lang="c"):330 """Determine if the system header file named by 'header_file'331 exists and can be found by the preprocessor; return true if so,332 false otherwise.333 """334 return self.try_cpp(335 body="/* No body */", headers=[header], include_dirs=include_dirs336 )337 338 339def dump_file(filename, head=None):340 """Dumps a file content into log.info.341 342 If head is not None, will be dumped before the file content.343 """344 if head is None:345 log.info('%s', filename)346 else:347 log.info(head)348 log.info(pathlib.Path(filename).read_text(encoding='utf-8'))349 