1import re 2import os 3import sys 4import warnings 5import platform 6import tempfile 7import hashlib 8import base64 9import subprocess 10from subprocess import Popen, PIPE, STDOUT 11from numpy.distutils.exec_command import filepath_from_subprocess_output 12from numpy.distutils.fcompiler import FCompiler 13from distutils.version import LooseVersion 14 15compilers = ['GnuFCompiler', 'Gnu95FCompiler'] 16 17TARGET_R = re.compile(r"Target: ([a-zA-Z0-9_\-]*)") 18 19# XXX: handle cross compilation 20 21 22def is_win64(): 23 return sys.platform == "win32" and platform.architecture()[0] == "64bit" 24 25 26class GnuFCompiler(FCompiler): 27 compiler_type = 'gnu' 28 compiler_aliases = ('g77', ) 29 description = 'GNU Fortran 77 compiler' 30 31 def gnu_version_match(self, version_string): 32 """Handle the different versions of GNU fortran compilers""" 33 # Strip warning(s) that may be emitted by gfortran 34 while version_string.startswith('gfortran: warning'): 35 version_string =\ 36 version_string[version_string.find('\n') + 1:].strip() 37 38 # Gfortran versions from after 2010 will output a simple string 39 # (usually "x.y", "x.y.z" or "x.y.z-q") for ``-dumpversion``; older 40 # gfortrans may still return long version strings (``-dumpversion`` was 41 # an alias for ``--version``) 42 if len(version_string) <= 20: 43 # Try to find a valid version string 44 m = re.search(r'([0-9.]+)', version_string) 45 if m: 46 # g77 provides a longer version string that starts with GNU 47 # Fortran 48 if version_string.startswith('GNU Fortran'): 49 return ('g77', m.group(1)) 50 51 # gfortran only outputs a version string such as #.#.#, so check 52 # if the match is at the start of the string 53 elif m.start() == 0: 54 return ('gfortran', m.group(1)) 55 else: 56 # Output probably from --version, try harder: 57 m = re.search(r'GNU Fortran\s+95.*?([0-9-.]+)', version_string) 58 if m: 59 return ('gfortran', m.group(1)) 60 m = re.search( 61 r'GNU Fortran.*?\-?([0-9-.]+\.[0-9-.]+)', version_string) 62 if m: 63 v = m.group(1) 64 if v.startswith('0') or v.startswith('2') or v.startswith('3'): 65 # the '0' is for early g77's 66 return ('g77', v) 67 else: 68 # at some point in the 4.x series, the ' 95' was dropped 69 # from the version string 70 return ('gfortran', v) 71 72 # If still nothing, raise an error to make the problem easy to find. 73 err = 'A valid Fortran version was not found in this string:\n' 74 raise ValueError(err + version_string) 75 76 def version_match(self, version_string): 77 v = self.gnu_version_match(version_string) 78 if not v or v[0] != 'g77': 79 return None 80 return v[1] 81 82 possible_executables = ['%%FC%%', 'g77', 'f77'] 83 executables = { 84 'version_cmd' : [None, "-dumpversion"], 85 'compiler_f77' : [None, "-g", "-Wall", "-fno-second-underscore"], 86 'compiler_f90' : None, # Use --fcompiler=gnu95 for f90 codes 87 'compiler_fix' : None, 88 'linker_so' : [None, "-g", "-Wall"], 89 'archiver' : ["ar", "-cr"], 90 'ranlib' : ["ranlib"], 91 'linker_exe' : [None, "-g", "-Wall"] 92 } 93 module_dir_switch = None 94 module_include_switch = None 95 96 # Cygwin: f771: warning: -fPIC ignored for target (all code is 97 # position independent) 98 if os.name != 'nt' and sys.platform != 'cygwin': 99 pic_flags = ['-fPIC'] 100 101 # use -mno-cygwin for g77 when Python is not Cygwin-Python 102 if sys.platform == 'win32': 103 for key in ['version_cmd', 'compiler_f77', 'linker_so', 'linker_exe']: 104 executables[key].append('-mno-cygwin') 105 106 g2c = '%%FC%%' 107 suggested_f90_compiler = 'gnu95' 108 109 def get_flags_linker_so(self): 110 opt = self.linker_so[1:] 111 if 'FFLAGS' in os.environ: 112 opt.append(os.environ['FFLAGS']) 113 if sys.platform == 'darwin': 114 target = os.environ.get('MACOSX_DEPLOYMENT_TARGET', None) 115 # If MACOSX_DEPLOYMENT_TARGET is set, we simply trust the value 116 # and leave it alone. But, distutils will complain if the 117 # environment's value is different from the one in the Python 118 # Makefile used to build Python. We let disutils handle this 119 # error checking. 120 if not target: 121 # If MACOSX_DEPLOYMENT_TARGET is not set in the environment, 122 # we try to get it first from sysconfig and then 123 # fall back to setting it to 10.9 This is a reasonable default 124 # even when using the official Python dist and those derived 125 # from it. 126 import sysconfig 127 target = sysconfig.get_config_var('MACOSX_DEPLOYMENT_TARGET') 128 if not target: 129 target = '10.9' 130 s = f'Env. variable MACOSX_DEPLOYMENT_TARGET set to {target}' 131 warnings.warn(s, stacklevel=2) 132 os.environ['MACOSX_DEPLOYMENT_TARGET'] = str(target) 133 opt.extend(['-undefined', 'dynamic_lookup', '-bundle']) 134 else: 135 opt.append("-shared") 136 if sys.platform.startswith('sunos'): 137 # SunOS often has dynamically loaded symbols defined in the 138 # static library libg2c.a The linker doesn't like this. To 139 # ignore the problem, use the -mimpure-text flag. It isn't 140 # the safest thing, but seems to work. 'man gcc' says: 141 # ".. Instead of using -mimpure-text, you should compile all 142 # source code with -fpic or -fPIC." 143 opt.append('-mimpure-text') 144 return opt 145 146 def get_libgcc_dir(self): 147 try: 148 output = subprocess.check_output(self.compiler_f77 + 149 ['-print-libgcc-file-name']) 150 except (OSError, subprocess.CalledProcessError): 151 pass 152 else: 153 output = filepath_from_subprocess_output(output) 154 return os.path.dirname(output) 155 return None 156 157 def get_libgfortran_dir(self): 158 if sys.platform[:5] == 'linux': 159 libgfortran_name = 'libgfortran.so' 160 elif sys.platform == 'darwin': 161 libgfortran_name = 'libgfortran.dylib' 162 else: 163 libgfortran_name = None 164 165 libgfortran_dir = None 166 if libgfortran_name: 167 find_lib_arg = ['-print-file-name={0}'.format(libgfortran_name)] 168 try: 169 output = subprocess.check_output( 170 self.compiler_f77 + find_lib_arg) 171 except (OSError, subprocess.CalledProcessError): 172 pass 173 else: 174 output = filepath_from_subprocess_output(output) 175 libgfortran_dir = os.path.dirname(output) 176 return libgfortran_dir 177 178 def get_library_dirs(self): 179 opt = [] 180 if sys.platform[:5] != 'linux': 181 d = self.get_libgcc_dir() 182 if d: 183 # if windows and not cygwin, libg2c lies in a different folder 184 if sys.platform == 'win32' and not d.startswith('/usr/lib'): 185 d = os.path.normpath(d) 186 path = os.path.join(d, "lib%s.a" % self.g2c) 187 if not os.path.exists(path): 188 root = os.path.join(d, *((os.pardir, ) * 4)) 189 d2 = os.path.abspath(os.path.join(root, 'lib')) 190 path = os.path.join(d2, "lib%s.a" % self.g2c) 191 if os.path.exists(path): 192 opt.append(d2) 193 opt.append(d) 194 # For Macports / Linux, libgfortran and libgcc are not co-located 195 lib_gfortran_dir = self.get_libgfortran_dir() 196 if lib_gfortran_dir: 197 opt.append(lib_gfortran_dir) 198 return opt 199 200 def get_libraries(self): 201 opt = [] 202 d = self.get_libgcc_dir() 203 if d is not None: 204 g2c = self.g2c + '-pic' 205 f = self.static_lib_format % (g2c, self.static_lib_extension) 206 if not os.path.isfile(os.path.join(d, f)): 207 g2c = self.g2c 208 else: 209 g2c = self.g2c 210 211 if g2c is not None: 212 opt.append(g2c) 213 c_compiler = self.c_compiler 214 if sys.platform == 'win32' and c_compiler and \ 215 c_compiler.compiler_type == 'msvc': 216 opt.append('gcc') 217 if sys.platform == 'darwin': 218 opt.append('cc_dynamic') 219 return opt 220 221 def get_flags_debug(self): 222 return ['-g'] 223 224 def get_flags_opt(self): 225 v = self.get_version() 226 if v and v <= '3.3.3': 227 # With this compiler version building Fortran BLAS/LAPACK 228 # with -O3 caused failures in lib.lapack heevr,syevr tests. 229 opt = ['-O2'] 230 else: 231 opt = ['-O3'] 232 opt.append('-funroll-loops') 233 return opt 234 235 def _c_arch_flags(self): 236 """ Return detected arch flags from CFLAGS """ 237 import sysconfig 238 try: 239 cflags = sysconfig.get_config_vars()['CFLAGS'] 240 except KeyError: 241 return [] 242 arch_re = re.compile(r"-arch\s+(\w+)") 243 arch_flags = [] 244 for arch in arch_re.findall(cflags): 245 arch_flags += ['-arch', arch] 246 return arch_flags 247 248 def get_flags_arch(self): 249 return [] 250 251 def runtime_library_dir_option(self, dir): 252 if sys.platform == 'win32': 253 # Linux/Solaris/Unix support RPATH, Windows does not 254 raise NotImplementedError 255 256 # TODO: could use -Xlinker here, if it's supported 257 assert "," not in dir 258 259 if sys.platform == 'darwin': 260 return f'-Wl,-rpath,{dir}' 261 elif sys.platform[:3] == 'aix': 262 # AIX RPATH is called LIBPATH 263 return f'-Wl,-blibpath:{dir}' 264 else: 265 return f'-Wl,-rpath={dir}' 266 267 268class Gnu95FCompiler(GnuFCompiler): 269 compiler_type = 'gnu95' 270 compiler_aliases = ('gfortran', ) 271 description = 'GNU Fortran 95 compiler' 272 273 def version_match(self, version_string): 274 v = self.gnu_version_match(version_string) 275 if not v or v[0] != 'gfortran': 276 return None 277 v = v[1] 278 if LooseVersion(v) >= "4": 279 # gcc-4 series releases do not support -mno-cygwin option 280 pass 281 else: 282 # use -mno-cygwin flag for gfortran when Python is not 283 # Cygwin-Python 284 if sys.platform == 'win32': 285 for key in [ 286 'version_cmd', 'compiler_f77', 'compiler_f90', 287 'compiler_fix', 'linker_so', 'linker_exe' 288 ]: 289 self.executables[key].append('-mno-cygwin') 290 return v 291 292 possible_executables = ['%%FC%%', 'gfortran', 'f95'] 293 executables = { 294 'version_cmd' : ["<F90>", "-dumpversion"], 295 'compiler_f77' : [None, "-Wall", "-g", "-ffixed-form", 296 "-fno-second-underscore"], 297 'compiler_f90' : [None, "-Wall", "-g", 298 "-fno-second-underscore"], 299 'compiler_fix' : [None, "-Wall", "-g","-ffixed-form", 300 "-fno-second-underscore"], 301 'linker_so' : ["<F90>", "-Wall", "-g"], 302 'archiver' : ["ar", "-cr"], 303 'ranlib' : ["ranlib"], 304 'linker_exe' : [None, "-Wall"] 305 } 306 307 module_dir_switch = '-J' 308 module_include_switch = '-I' 309 310 if sys.platform[:3] == 'aix': 311 executables['linker_so'].append('-lpthread') 312 if platform.architecture()[0][:2] == '64': 313 for key in ['compiler_f77', 'compiler_f90','compiler_fix','linker_so', 'linker_exe']: 314 executables[key].append('-maix64') 315 316 g2c = 'gfortran' 317 318 def _universal_flags(self, cmd): 319 """Return a list of -arch flags for every supported architecture.""" 320 if not sys.platform == 'darwin': 321 return [] 322 arch_flags = [] 323 # get arches the C compiler gets. 324 c_archs = self._c_arch_flags() 325 if "i386" in c_archs: 326 c_archs[c_archs.index("i386")] = "i686" 327 # check the arches the Fortran compiler supports, and compare with 328 # arch flags from C compiler 329 for arch in ["ppc", "i686", "x86_64", "ppc64"]: 330 if _can_target(cmd, arch) and arch in c_archs: 331 arch_flags.extend(["-arch", arch]) 332 return arch_flags 333 334 def get_flags(self): 335 flags = GnuFCompiler.get_flags(self) 336 arch_flags = self._universal_flags(self.compiler_f90) 337 if arch_flags: 338 flags[:0] = arch_flags 339 return flags 340 341 def get_flags_linker_so(self): 342 flags = GnuFCompiler.get_flags_linker_so(self) 343 arch_flags = self._universal_flags(self.linker_so) 344 if arch_flags: 345 flags[:0] = arch_flags 346 return flags 347 348 def get_library_dirs(self): 349 opt = GnuFCompiler.get_library_dirs(self) 350 if sys.platform == 'win32': 351 c_compiler = self.c_compiler 352 if c_compiler and c_compiler.compiler_type == "msvc": 353 target = self.get_target() 354 if target: 355 d = os.path.normpath(self.get_libgcc_dir()) 356 root = os.path.join(d, *((os.pardir, ) * 4)) 357 path = os.path.join(root, "lib") 358 mingwdir = os.path.normpath(path) 359 if os.path.exists(os.path.join(mingwdir, "libmingwex.a")): 360 opt.append(mingwdir) 361 # For Macports / Linux, libgfortran and libgcc are not co-located 362 lib_gfortran_dir = self.get_libgfortran_dir() 363 if lib_gfortran_dir: 364 opt.append(lib_gfortran_dir) 365 return opt 366 367 def get_libraries(self): 368 opt = GnuFCompiler.get_libraries(self) 369 if sys.platform == 'darwin': 370 opt.remove('cc_dynamic') 371 if sys.platform == 'win32': 372 c_compiler = self.c_compiler 373 if c_compiler and c_compiler.compiler_type == "msvc": 374 if "gcc" in opt: 375 i = opt.index("gcc") 376 opt.insert(i + 1, "mingwex") 377 opt.insert(i + 1, "mingw32") 378 c_compiler = self.c_compiler 379 if c_compiler and c_compiler.compiler_type == "msvc": 380 return [] 381 else: 382 pass 383 return opt 384 385 def get_target(self): 386 try: 387 output = subprocess.check_output(self.compiler_f77 + ['-v']) 388 except (OSError, subprocess.CalledProcessError): 389 pass 390 else: 391 output = filepath_from_subprocess_output(output) 392 m = TARGET_R.search(output) 393 if m: 394 return m.group(1) 395 return "" 396 397 def _hash_files(self, filenames): 398 h = hashlib.sha1() 399 for fn in filenames: 400 with open(fn, 'rb') as f: 401 while True: 402 block = f.read(131072) 403 if not block: 404 break 405 h.update(block) 406 text = base64.b32encode(h.digest()) 407 text = text.decode('ascii') 408 return text.rstrip('=') 409 410 def _link_wrapper_lib(self, objects, output_dir, extra_dll_dir, 411 chained_dlls, is_archive): 412 """Create a wrapper shared library for the given objects 413 414 Return an MSVC-compatible lib 415 """ 416 417 c_compiler = self.c_compiler 418 if c_compiler.compiler_type != "msvc": 419 raise ValueError("This method only supports MSVC") 420 421 object_hash = self._hash_files(list(objects) + list(chained_dlls)) 422 423 if is_win64(): 424 tag = 'win_amd64' 425 else: 426 tag = 'win32' 427 428 basename = 'lib' + os.path.splitext( 429 os.path.basename(objects[0]))[0][:8] 430 root_name = basename + '.' + object_hash + '.gfortran-' + tag 431 dll_name = root_name + '.dll' 432 def_name = root_name + '.def' 433 lib_name = root_name + '.lib' 434 dll_path = os.path.join(extra_dll_dir, dll_name) 435 def_path = os.path.join(output_dir, def_name) 436 lib_path = os.path.join(output_dir, lib_name) 437 438 if os.path.isfile(lib_path): 439 # Nothing to do 440 return lib_path, dll_path 441 442 if is_archive: 443 objects = (["-Wl,--whole-archive"] + list(objects) + 444 ["-Wl,--no-whole-archive"]) 445 self.link_shared_object( 446 objects, 447 dll_name, 448 output_dir=extra_dll_dir, 449 extra_postargs=list(chained_dlls) + [ 450 '-Wl,--allow-multiple-definition', 451 '-Wl,--output-def,' + def_path, 452 '-Wl,--export-all-symbols', 453 '-Wl,--enable-auto-import', 454 '-static', 455 '-mlong-double-64', 456 ]) 457 458 # No PowerPC! 459 if is_win64(): 460 specifier = '/MACHINE:X64' 461 else: 462 specifier = '/MACHINE:X86' 463 464 # MSVC specific code 465 lib_args = ['/def:' + def_path, '/OUT:' + lib_path, specifier] 466 if not c_compiler.initialized: 467 c_compiler.initialize() 468 c_compiler.spawn([c_compiler.lib] + lib_args) 469 470 return lib_path, dll_path 471 472 def can_ccompiler_link(self, compiler): 473 # MSVC cannot link objects compiled by GNU fortran 474 return compiler.compiler_type not in ("msvc", ) 475 476 def wrap_unlinkable_objects(self, objects, output_dir, extra_dll_dir): 477 """ 478 Convert a set of object files that are not compatible with the default 479 linker, to a file that is compatible. 480 """ 481 if self.c_compiler.compiler_type == "msvc": 482 # Compile a DLL and return the lib for the DLL as 483 # the object. Also keep track of previous DLLs that 484 # we have compiled so that we can link against them. 485 486 # If there are .a archives, assume they are self-contained 487 # static libraries, and build separate DLLs for each 488 archives = [] 489 plain_objects = [] 490 for obj in objects: 491 if obj.lower().endswith('.a'): 492 archives.append(obj) 493 else: 494 plain_objects.append(obj) 495 496 chained_libs = [] 497 chained_dlls = [] 498 for archive in archives[::-1]: 499 lib, dll = self._link_wrapper_lib( 500 [archive], 501 output_dir, 502 extra_dll_dir, 503 chained_dlls=chained_dlls, 504 is_archive=True) 505 chained_libs.insert(0, lib) 506 chained_dlls.insert(0, dll) 507 508 if not plain_objects: 509 return chained_libs 510 511 lib, dll = self._link_wrapper_lib( 512 plain_objects, 513 output_dir, 514 extra_dll_dir, 515 chained_dlls=chained_dlls, 516 is_archive=False) 517 return [lib] + chained_libs 518 else: 519 raise ValueError("Unsupported C compiler") 520 521 522def _can_target(cmd, arch): 523 """Return true if the architecture supports the -arch flag""" 524 newcmd = cmd[:] 525 fid, filename = tempfile.mkstemp(suffix=".f") 526 os.close(fid) 527 try: 528 d = os.path.dirname(filename) 529 output = os.path.splitext(filename)[0] + ".o" 530 try: 531 newcmd.extend(["-arch", arch, "-c", filename]) 532 p = Popen(newcmd, stderr=STDOUT, stdout=PIPE, cwd=d) 533 p.communicate() 534 return p.returncode == 0 535 finally: 536 if os.path.exists(output): 537 os.remove(output) 538 finally: 539 os.remove(filename) 540 return False 541 542 543if __name__ == '__main__': 544 from distutils import log 545 from numpy.distutils import customized_fcompiler 546 log.set_verbosity(2) 547 548 print(customized_fcompiler('gnu').get_version()) 549 try: 550 print(customized_fcompiler('g95').get_version()) 551 except Exception as e: 552 print(e) 553