1""" Modified version of build_clib that handles fortran source files.
2"""
3import os
4from glob import glob
5import shutil
6from distutils.command.build_clib import build_clib as old_build_clib
7from distutils.errors import DistutilsSetupError, DistutilsError, \
8    DistutilsFileError
9
10from numpy.distutils import log
11from distutils.dep_util import newer_group
12from numpy.distutils.misc_util import (
13    filter_sources, get_lib_source_files, get_numpy_include_dirs,
14    has_cxx_sources, has_f_sources, is_sequence
15)
16from numpy.distutils.ccompiler_opt import new_ccompiler_opt
17
18# Fix Python distutils bug sf #1718574:
19_l = old_build_clib.user_options
20for _i in range(len(_l)):
21    if _l[_i][0] in ['build-clib', 'build-temp']:
22        _l[_i] = (_l[_i][0] + '=',) + _l[_i][1:]
23#
24
25
26class build_clib(old_build_clib):
27
28    description = "build C/C++/F libraries used by Python extensions"
29
30    user_options = old_build_clib.user_options + [
31        ('fcompiler=', None,
32         "specify the Fortran compiler type"),
33        ('inplace', 'i', 'Build in-place'),
34        ('parallel=', 'j',
35         "number of parallel jobs"),
36        ('warn-error', None,
37         "turn all warnings into errors (-Werror)"),
38        ('cpu-baseline=', None,
39         "specify a list of enabled baseline CPU optimizations"),
40        ('cpu-dispatch=', None,
41         "specify a list of dispatched CPU optimizations"),
42        ('disable-optimization', None,
43         "disable CPU optimized code(dispatch,simd,fast...)"),
44    ]
45
46    boolean_options = old_build_clib.boolean_options + \
47    ['inplace', 'warn-error', 'disable-optimization']
48
49    def initialize_options(self):
50        old_build_clib.initialize_options(self)
51        self.fcompiler = None
52        self.inplace = 0
53        self.parallel = None
54        self.warn_error = None
55        self.cpu_baseline = None
56        self.cpu_dispatch = None
57        self.disable_optimization = None
58
59
60    def finalize_options(self):
61        if self.parallel:
62            try:
63                self.parallel = int(self.parallel)
64            except ValueError as e:
65                raise ValueError("--parallel/-j argument must be an integer") from e
66        old_build_clib.finalize_options(self)
67        self.set_undefined_options('build',
68                                        ('parallel', 'parallel'),
69                                        ('warn_error', 'warn_error'),
70                                        ('cpu_baseline', 'cpu_baseline'),
71                                        ('cpu_dispatch', 'cpu_dispatch'),
72                                        ('disable_optimization', 'disable_optimization')
73                                  )
74
75    def have_f_sources(self):
76        for (lib_name, build_info) in self.libraries:
77            if has_f_sources(build_info.get('sources', [])):
78                return True
79        return False
80
81    def have_cxx_sources(self):
82        for (lib_name, build_info) in self.libraries:
83            if has_cxx_sources(build_info.get('sources', [])):
84                return True
85        return False
86
87    def run(self):
88        if not self.libraries:
89            return
90
91        # Make sure that library sources are complete.
92        languages = []
93
94        # Make sure that extension sources are complete.
95        self.run_command('build_src')
96
97        for (lib_name, build_info) in self.libraries:
98            l = build_info.get('language', None)
99            if l and l not in languages:
100                languages.append(l)
101
102        from distutils.ccompiler import new_compiler
103        self.compiler = new_compiler(compiler=self.compiler,
104                                     dry_run=self.dry_run,
105                                     force=self.force)
106        self.compiler.customize(self.distribution,
107                                need_cxx=self.have_cxx_sources())
108
109        if self.warn_error:
110            self.compiler.compiler.append('-Werror')
111            self.compiler.compiler_so.append('-Werror')
112
113        libraries = self.libraries
114        self.libraries = None
115        self.compiler.customize_cmd(self)
116        self.libraries = libraries
117
118        self.compiler.show_customization()
119
120        if not self.disable_optimization:
121            dispatch_hpath = os.path.join("numpy", "distutils", "include", "npy_cpu_dispatch_config.h")
122            dispatch_hpath = os.path.join(self.get_finalized_command("build_src").build_src, dispatch_hpath)
123            opt_cache_path = os.path.abspath(
124                os.path.join(self.build_temp, 'ccompiler_opt_cache_clib.py')
125            )
126            self.compiler_opt = new_ccompiler_opt(
127                compiler=self.compiler, dispatch_hpath=dispatch_hpath,
128                cpu_baseline=self.cpu_baseline, cpu_dispatch=self.cpu_dispatch,
129                cache_path=opt_cache_path
130            )
131            if not self.compiler_opt.is_cached():
132                log.info("Detected changes on compiler optimizations, force rebuilding")
133                self.force = True
134
135            import atexit
136            def report():
137                log.info("\n########### CLIB COMPILER OPTIMIZATION ###########")
138                log.info(self.compiler_opt.report(full=True))
139
140            atexit.register(report)
141
142        if self.have_f_sources():
143            from numpy.distutils.fcompiler import new_fcompiler
144            self._f_compiler = new_fcompiler(compiler=self.fcompiler,
145                                             verbose=self.verbose,
146                                             dry_run=self.dry_run,
147                                             force=self.force,
148                                             requiref90='f90' in languages,
149                                             c_compiler=self.compiler)
150            if self._f_compiler is not None:
151                self._f_compiler.customize(self.distribution)
152
153                libraries = self.libraries
154                self.libraries = None
155                self._f_compiler.customize_cmd(self)
156                self.libraries = libraries
157
158                self._f_compiler.show_customization()
159        else:
160            self._f_compiler = None
161
162        self.build_libraries(self.libraries)
163
164        if self.inplace:
165            for l in self.distribution.installed_libraries:
166                libname = self.compiler.library_filename(l.name)
167                source = os.path.join(self.build_clib, libname)
168                target = os.path.join(l.target_dir, libname)
169                self.mkpath(l.target_dir)
170                shutil.copy(source, target)
171
172    def get_source_files(self):
173        self.check_library_list(self.libraries)
174        filenames = []
175        for lib in self.libraries:
176            filenames.extend(get_lib_source_files(lib))
177        return filenames
178
179    def build_libraries(self, libraries):
180        for (lib_name, build_info) in libraries:
181            self.build_a_library(build_info, lib_name, libraries)
182
183    def build_a_library(self, build_info, lib_name, libraries):
184        # default compilers
185        compiler = self.compiler
186        fcompiler = self._f_compiler
187
188        sources = build_info.get('sources')
189        if sources is None or not is_sequence(sources):
190            raise DistutilsSetupError(("in 'libraries' option (library '%s'), " +
191                                       "'sources' must be present and must be " +
192                                       "a list of source filenames") % lib_name)
193        sources = list(sources)
194
195        c_sources, cxx_sources, f_sources, fmodule_sources \
196            = filter_sources(sources)
197        requiref90 = not not fmodule_sources or \
198            build_info.get('language', 'c') == 'f90'
199
200        # save source type information so that build_ext can use it.
201        source_languages = []
202        if c_sources:
203            source_languages.append('c')
204        if cxx_sources:
205            source_languages.append('c++')
206        if requiref90:
207            source_languages.append('f90')
208        elif f_sources:
209            source_languages.append('f77')
210        build_info['source_languages'] = source_languages
211
212        lib_file = compiler.library_filename(lib_name,
213                                             output_dir=self.build_clib)
214        depends = sources + build_info.get('depends', [])
215        if not (self.force or newer_group(depends, lib_file, 'newer')):
216            log.debug("skipping '%s' library (up-to-date)", lib_name)
217            return
218        else:
219            log.info("building '%s' library", lib_name)
220
221        config_fc = build_info.get('config_fc', {})
222        if fcompiler is not None and config_fc:
223            log.info('using additional config_fc from setup script '
224                     'for fortran compiler: %s'
225                     % (config_fc,))
226            from numpy.distutils.fcompiler import new_fcompiler
227            fcompiler = new_fcompiler(compiler=fcompiler.compiler_type,
228                                      verbose=self.verbose,
229                                      dry_run=self.dry_run,
230                                      force=self.force,
231                                      requiref90=requiref90,
232                                      c_compiler=self.compiler)
233            if fcompiler is not None:
234                dist = self.distribution
235                base_config_fc = dist.get_option_dict('config_fc').copy()
236                base_config_fc.update(config_fc)
237                fcompiler.customize(base_config_fc)
238
239        # check availability of Fortran compilers
240        if (f_sources or fmodule_sources) and fcompiler is None:
241            raise DistutilsError("library %s has Fortran sources"
242                                 " but no Fortran compiler found" % (lib_name))
243
244        if fcompiler is not None:
245            fcompiler.extra_f77_compile_args = build_info.get(
246                'extra_f77_compile_args') or []
247            fcompiler.extra_f90_compile_args = build_info.get(
248                'extra_f90_compile_args') or []
249
250        macros = build_info.get('macros')
251        if macros is None:
252            macros = []
253        include_dirs = build_info.get('include_dirs')
254        if include_dirs is None:
255            include_dirs = []
256        extra_postargs = build_info.get('extra_compiler_args') or []
257
258        include_dirs.extend(get_numpy_include_dirs())
259        # where compiled F90 module files are:
260        module_dirs = build_info.get('module_dirs') or []
261        module_build_dir = os.path.dirname(lib_file)
262        if requiref90:
263            self.mkpath(module_build_dir)
264
265        if compiler.compiler_type == 'msvc':
266            # this hack works around the msvc compiler attributes
267            # problem, msvc uses its own convention :(
268            c_sources += cxx_sources
269            cxx_sources = []
270
271        # filtering C dispatch-table sources when optimization is not disabled,
272        # otherwise treated as normal sources.
273        copt_c_sources = []
274        copt_baseline_flags = []
275        copt_macros = []
276        if not self.disable_optimization:
277            bsrc_dir = self.get_finalized_command("build_src").build_src
278            dispatch_hpath = os.path.join("numpy", "distutils", "include")
279            dispatch_hpath = os.path.join(bsrc_dir, dispatch_hpath)
280            include_dirs.append(dispatch_hpath)
281
282            copt_build_src = None if self.inplace else bsrc_dir
283            copt_c_sources = [
284                c_sources.pop(c_sources.index(src))
285                for src in c_sources[:] if src.endswith(".dispatch.c")
286            ]
287            copt_baseline_flags = self.compiler_opt.cpu_baseline_flags()
288        else:
289            copt_macros.append(("NPY_DISABLE_OPTIMIZATION", 1))
290
291        objects = []
292        if copt_c_sources:
293            log.info("compiling C dispatch-able sources")
294            objects += self.compiler_opt.try_dispatch(copt_c_sources,
295                                                      output_dir=self.build_temp,
296                                                      src_dir=copt_build_src,
297                                                      macros=macros + copt_macros,
298                                                      include_dirs=include_dirs,
299                                                      debug=self.debug,
300                                                      extra_postargs=extra_postargs)
301
302        if c_sources:
303            log.info("compiling C sources")
304            objects += compiler.compile(c_sources,
305                                        output_dir=self.build_temp,
306                                        macros=macros + copt_macros,
307                                        include_dirs=include_dirs,
308                                        debug=self.debug,
309                                        extra_postargs=extra_postargs + copt_baseline_flags)
310
311        if cxx_sources:
312            log.info("compiling C++ sources")
313            cxx_compiler = compiler.cxx_compiler()
314            cxx_objects = cxx_compiler.compile(cxx_sources,
315                                               output_dir=self.build_temp,
316                                               macros=macros + copt_macros,
317                                               include_dirs=include_dirs,
318                                               debug=self.debug,
319                                               extra_postargs=extra_postargs + copt_baseline_flags)
320            objects.extend(cxx_objects)
321
322        if f_sources or fmodule_sources:
323            extra_postargs = []
324            f_objects = []
325
326            if requiref90:
327                if fcompiler.module_dir_switch is None:
328                    existing_modules = glob('*.mod')
329                extra_postargs += fcompiler.module_options(
330                    module_dirs, module_build_dir)
331
332            if fmodule_sources:
333                log.info("compiling Fortran 90 module sources")
334                f_objects += fcompiler.compile(fmodule_sources,
335                                               output_dir=self.build_temp,
336                                               macros=macros,
337                                               include_dirs=include_dirs,
338                                               debug=self.debug,
339                                               extra_postargs=extra_postargs)
340
341            if requiref90 and self._f_compiler.module_dir_switch is None:
342                # move new compiled F90 module files to module_build_dir
343                for f in glob('*.mod'):
344                    if f in existing_modules:
345                        continue
346                    t = os.path.join(module_build_dir, f)
347                    if os.path.abspath(f) == os.path.abspath(t):
348                        continue
349                    if os.path.isfile(t):
350                        os.remove(t)
351                    try:
352                        self.move_file(f, module_build_dir)
353                    except DistutilsFileError:
354                        log.warn('failed to move %r to %r'
355                                 % (f, module_build_dir))
356
357            if f_sources:
358                log.info("compiling Fortran sources")
359                f_objects += fcompiler.compile(f_sources,
360                                               output_dir=self.build_temp,
361                                               macros=macros,
362                                               include_dirs=include_dirs,
363                                               debug=self.debug,
364                                               extra_postargs=extra_postargs)
365        else:
366            f_objects = []
367
368        if f_objects and not fcompiler.can_ccompiler_link(compiler):
369            # Default linker cannot link Fortran object files, and results
370            # need to be wrapped later. Instead of creating a real static
371            # library, just keep track of the object files.
372            listfn = os.path.join(self.build_clib,
373                                  lib_name + '.fobjects')
374            with open(listfn, 'w') as f:
375                f.write("\n".join(os.path.abspath(obj) for obj in f_objects))
376
377            listfn = os.path.join(self.build_clib,
378                                  lib_name + '.cobjects')
379            with open(listfn, 'w') as f:
380                f.write("\n".join(os.path.abspath(obj) for obj in objects))
381
382            # create empty "library" file for dependency tracking
383            lib_fname = os.path.join(self.build_clib,
384                                     lib_name + compiler.static_lib_extension)
385            with open(lib_fname, 'wb') as f:
386                pass
387        else:
388            # assume that default linker is suitable for
389            # linking Fortran object files
390            objects.extend(f_objects)
391            compiler.create_static_lib(objects, lib_name,
392                                       output_dir=self.build_clib,
393                                       debug=self.debug)
394
395        # fix library dependencies
396        clib_libraries = build_info.get('libraries', [])
397        for lname, binfo in libraries:
398            if lname in clib_libraries:
399                clib_libraries.extend(binfo.get('libraries', []))
400        if clib_libraries:
401            build_info['libraries'] = clib_libraries
402