xref: /petsc/src/binding/petsc4py/conf/confpetsc.py (revision d756bedd70a89ca052be956bccd75c5761cb2ab4)
1# --------------------------------------------------------------------
2
3from pathlib import Path
4import re
5import os
6import subprocess
7import sys
8import glob
9import copy
10import warnings
11from distutils import log
12from distutils import sysconfig
13from distutils.util import execute
14from distutils.util import split_quoted
15from distutils.errors import DistutilsError
16from distutils.text_file import TextFile
17
18
19try:
20    from cStringIO import StringIO
21except ImportError:
22    from io import StringIO
23
24try:
25    import setuptools
26except ImportError:
27    setuptools = None
28
29if setuptools:
30    from setuptools import setup as _setup
31    from setuptools import Extension as _Extension
32    from setuptools import Command
33else:
34    from distutils.core import setup as _setup
35    from distutils.core import Extension as _Extension
36    from distutils.core import Command
37
38
39def import_command(cmd):
40    try:
41        from importlib import import_module
42    except ImportError:
43
44        def import_module(n):
45            return __import__(n, fromlist=[None])
46
47    try:
48        if not setuptools:
49            raise ImportError
50        mod = import_module('setuptools.command.' + cmd)
51        return getattr(mod, cmd)
52    except ImportError:
53        mod = import_module('distutils.command.' + cmd)
54        return getattr(mod, cmd)
55
56
57_config = import_command('config')
58_build = import_command('build')
59_build_ext = import_command('build_ext')
60_install = import_command('install')
61
62try:
63    from setuptools import modified
64except ImportError:
65    try:
66        from setuptools import dep_util as modified
67    except ImportError:
68        from distutils import dep_util as modified
69
70try:
71    from packaging.version import Version
72except ImportError:
73    try:
74        from setuptools.extern.packaging.version import Version
75    except ImportError:
76        from distutils.version import StrictVersion as Version
77
78# --------------------------------------------------------------------
79
80# Cython
81
82CYTHON = '3.0.0'
83
84
85def cython_req():
86    return CYTHON
87
88
89def cython_chk(VERSION, verbose=True):
90    #
91    def warn(message):
92        if not verbose:
93            return
94        ruler, ws, nl = '*' * 80, ' ', '\n'
95        pyexe = sys.executable
96        advise = '$ %s -m pip install --upgrade cython' % pyexe
97
98        def printer(*s):
99            sys.stderr.write(' '.join(s) + '\n')
100
101        printer(ruler, nl)
102        printer(ws, message, nl)
103        printer(ws, ws, advise, nl)
104        printer(ruler)
105
106    #
107    try:
108        import Cython
109    except ImportError:
110        warn('You need Cython to generate C source files.')
111        return False
112    #
113    CYTHON_VERSION = Cython.__version__
114    m = re.match(r'(\d+\.\d+(?:\.\d+)?).*', CYTHON_VERSION)
115    if not m:
116        warn(f'Cannot parse Cython version string {CYTHON_VERSION!r}')
117        return False
118    REQUIRED = Version(VERSION)
119    PROVIDED = Version(m.groups()[0])
120    if PROVIDED < REQUIRED:
121        warn(f'You need Cython >= {VERSION} (you have version {CYTHON_VERSION})')
122        return False
123    #
124    if verbose:
125        log.info('using Cython %s' % CYTHON_VERSION)
126    return True
127
128
129def cython_run(
130    source,
131    target=None,
132    depends=(),
133    includes=(),
134    workdir=None,
135    force=False,
136    VERSION='0.0',
137):
138    if target is None:
139        target = os.path.splitext(source)[0] + '.c'
140    cwd = os.getcwd()
141    try:
142        if workdir:
143            os.chdir(workdir)
144        alldeps = [source]
145        for dep in depends:
146            alldeps += glob.glob(dep)
147        if not (force or modified.newer_group(alldeps, target)):
148            log.debug("skipping '%s' -> '%s' (up-to-date)", source, target)
149            return
150    finally:
151        os.chdir(cwd)
152    require = 'Cython >= %s' % VERSION
153    if setuptools and not cython_chk(VERSION, verbose=False):
154        if sys.modules.get('Cython'):
155            removed = getattr(sys.modules['Cython'], '__version__', '')
156            log.info('removing Cython %s from sys.modules' % removed)
157            pkgname = re.compile(r'cython(\.|$)', re.IGNORECASE)
158            for modname in list(sys.modules.keys()):
159                if pkgname.match(modname):
160                    del sys.modules[modname]
161        try:
162            install_setup_requires = setuptools._install_setup_requires
163            with warnings.catch_warnings():
164                if hasattr(setuptools, 'SetuptoolsDeprecationWarning'):
165                    category = setuptools.SetuptoolsDeprecationWarning
166                    warnings.simplefilter('ignore', category)
167                log.info("fetching build requirement '%s'" % require)
168                install_setup_requires({'setup_requires': [require]})
169        except Exception:
170            log.info("failed to fetch build requirement '%s'" % require)
171    if not cython_chk(VERSION):
172        raise DistutilsError("unsatisfied build requirement '%s'" % require)
173    #
174    log.info("cythonizing '%s' -> '%s'", source, target)
175    from cythonize import cythonize
176
177    args = []
178    if workdir:
179        args += ['--working', workdir]
180    args += [source]
181    if target:
182        args += ['--output-file', target]
183    err = cythonize(args)
184    if err:
185        raise DistutilsError(f"Cython failure: '{source}' -> '{target}'")
186
187
188# --------------------------------------------------------------------
189
190
191def fix_config_vars(names, values):
192    values = list(values)
193    if 'CONDA_BUILD' in os.environ:
194        return values
195    if sys.platform == 'darwin':
196        if 'ARCHFLAGS' in os.environ:
197            ARCHFLAGS = os.environ['ARCHFLAGS']
198            for i, flag in enumerate(list(values)):
199                flag, count = re.subn(r'-arch\s+\w+', ' ', str(flag))
200                if count and ARCHFLAGS:
201                    flag = flag + ' ' + ARCHFLAGS
202                values[i] = flag
203        if 'SDKROOT' in os.environ:
204            SDKROOT = os.environ['SDKROOT']
205            for i, flag in enumerate(list(values)):
206                flag, count = re.subn(r'-isysroot [^ \t]*', ' ', str(flag))
207                if count and SDKROOT:
208                    flag = flag + ' ' + '-isysroot ' + SDKROOT
209                values[i] = flag
210    return values
211
212
213def get_config_vars(*names):
214    # Core Python configuration
215    values = sysconfig.get_config_vars(*names)
216    # Do any distutils flags fixup right now
217    return fix_config_vars(names, values)
218
219
220# --------------------------------------------------------------------
221
222
223class PetscConfig:
224    def __init__(self, petsc_dir, petsc_arch, dest_dir=None):
225        if dest_dir is None:
226            dest_dir = os.environ.get('DESTDIR')
227        self.configdict = {}
228        if not petsc_dir:
229            raise DistutilsError('PETSc not found')
230        if not os.path.isdir(petsc_dir):
231            raise DistutilsError('invalid PETSC_DIR: %s' % petsc_dir)
232        self.version = self._get_petsc_version(petsc_dir)
233        self.configdict = self._get_petsc_config(petsc_dir, petsc_arch)
234        self.PETSC_DIR = self['PETSC_DIR']
235        self.PETSC_ARCH = self['PETSC_ARCH']
236        self.DESTDIR = dest_dir
237        language_map = {'CONLY': 'c', 'CXXONLY': 'c++'}
238        self.language = language_map[self['PETSC_LANGUAGE']]
239
240    def __getitem__(self, item):
241        return self.configdict[item]
242
243    def get(self, item, default=None):
244        return self.configdict.get(item, default)
245
246    def configure(self, extension, compiler=None):
247        self.configure_extension(extension)
248        if compiler is not None:
249            self.configure_compiler(compiler)
250
251    def _get_petsc_version(self, petsc_dir):
252        import re
253
254        version_re = {
255            'major': re.compile(r'#define\s+PETSC_VERSION_MAJOR\s+(\d+)'),
256            'minor': re.compile(r'#define\s+PETSC_VERSION_MINOR\s+(\d+)'),
257            'micro': re.compile(r'#define\s+PETSC_VERSION_SUBMINOR\s+(\d+)'),
258            'release': re.compile(r'#define\s+PETSC_VERSION_RELEASE\s+(-*\d+)'),
259        }
260        petscversion_h = os.path.join(petsc_dir, 'include', 'petscversion.h')
261        with open(petscversion_h, 'rt') as f:
262            data = f.read()
263        major = int(version_re['major'].search(data).groups()[0])
264        minor = int(version_re['minor'].search(data).groups()[0])
265        micro = int(version_re['micro'].search(data).groups()[0])
266        release = int(version_re['release'].search(data).groups()[0])
267        return (major, minor, micro), (release == 1)
268
269    def _get_petsc_config(self, petsc_dir, petsc_arch):
270        from os.path import join, isdir, exists
271
272        PETSC_DIR = petsc_dir
273        PETSC_ARCH = petsc_arch
274        #
275        confdir = join('lib', 'petsc', 'conf')
276        if not (PETSC_ARCH and isdir(join(PETSC_DIR, PETSC_ARCH))):
277            petscvars = join(PETSC_DIR, confdir, 'petscvariables')
278            PETSC_ARCH = makefile(open(petscvars, 'rt')).get('PETSC_ARCH')
279        if not (PETSC_ARCH and isdir(join(PETSC_DIR, PETSC_ARCH))):
280            PETSC_ARCH = ''
281        #
282        variables = join(PETSC_DIR, confdir, 'variables')
283        if not exists(variables):
284            variables = join(PETSC_DIR, PETSC_ARCH, confdir, 'variables')
285        petscvariables = join(PETSC_DIR, PETSC_ARCH, confdir, 'petscvariables')
286        #
287        with open(variables) as f:
288            contents = f.read()
289        with open(petscvariables) as f:
290            contents += f.read()
291        #
292        confstr = 'PETSC_DIR  = %s\n' % PETSC_DIR
293        confstr += 'PETSC_ARCH = %s\n' % PETSC_ARCH
294        confstr += contents
295        return makefile(StringIO(confstr))
296
297    def _configure_ext(self, ext, dct, append=False):
298        extdict = ext.__dict__
299        for key, values in dct.items():
300            if key in extdict:
301                for value in values:
302                    if value not in extdict[key]:
303                        if not append:
304                            extdict[key].insert(0, value)
305                        else:
306                            extdict[key].append(value)
307
308    def configure_extension(self, extension):
309        # includes and libraries
310        # paths in PETSc config files point to final installation location, but
311        # we might be building against PETSc in staging location (DESTDIR) when
312        # DESTDIR is set, so append DESTDIR (if nonempty) to those paths
313        petsc_inc = flaglist(prepend_to_flags(self.DESTDIR, self['PETSC_CC_INCLUDES']))
314        lib_flags = prepend_to_flags(
315            self.DESTDIR,
316            '-L{} {}'.format(self['PETSC_LIB_DIR'], self['PETSC_LIB_BASIC']),
317        )
318        petsc_lib = flaglist(lib_flags)
319        # runtime_library_dirs is not supported on Windows
320        if sys.platform != 'win32':
321            # if DESTDIR is set, then we're building against PETSc in a staging
322            # directory, but rpath needs to point to final install directory.
323            rpath = [strip_prefix(self.DESTDIR, self['PETSC_LIB_DIR'])]
324            if sys.modules.get('petsc') is not None:
325                if sys.platform == 'darwin':
326                    rpath = ['@loader_path/../../petsc/lib']
327                else:
328                    rpath = ['$ORIGIN/../../petsc/lib']
329            petsc_lib['runtime_library_dirs'].extend(rpath)
330        # Link in extra libraries on static builds
331        if self['BUILDSHAREDLIB'] != 'yes':
332            petsc_ext_lib = split_quoted(self['PETSC_EXTERNAL_LIB_BASIC'])
333            petsc_lib['extra_link_args'].extend(petsc_ext_lib)
334        self._configure_ext(extension, petsc_inc, append=True)
335        self._configure_ext(extension, petsc_lib)
336
337    def configure_compiler(self, compiler):
338        if compiler.compiler_type != 'unix':
339            return
340        getenv = os.environ.get
341        # distutils C/C++ compiler
342        (cc, cflags, ccshared, cxx) = get_config_vars('CC', 'CFLAGS', 'CCSHARED', 'CXX')
343        ccshared = getenv('CCSHARED', ccshared or '')
344        cflags = getenv('CFLAGS', cflags or '')
345        cflags = cflags.replace('-Wstrict-prototypes', '')
346        # distutils linker
347        (ldflags, ldshared, so_ext) = get_config_vars('LDFLAGS', 'LDSHARED', 'SO')
348        ld = cc
349        ldshared = getenv('LDSHARED', ldshared)
350        ldflags = getenv('LDFLAGS', cflags + ' ' + (ldflags or ''))
351        ldcmd = split_quoted(ld) + split_quoted(ldflags)
352        ldshared = [
353            flg
354            for flg in split_quoted(ldshared)
355            if flg not in ldcmd and (flg.find('/lib/spack/env') < 0) and (flg.find('/libexec/spack/') < 0)
356        ]
357        ldshared = str.join(' ', ldshared)
358
359        #
360        def get_flags(cmd):
361            if not cmd:
362                return ''
363            cmd = split_quoted(cmd)
364            if os.path.basename(cmd[0]) == 'xcrun':
365                del cmd[0]
366                while True:
367                    if cmd[0] == '-sdk':
368                        del cmd[0:2]
369                        continue
370                    if cmd[0] == '-log':
371                        del cmd[0]
372                        continue
373                    break
374            return ' '.join(cmd[1:])
375
376        # PETSc C compiler
377        PCC = self['PCC']
378        PCC_FLAGS = get_flags(cc) + ' ' + self['PCC_FLAGS']
379        PCC_FLAGS = PCC_FLAGS.replace('-fvisibility=hidden', '')
380        PCC_FLAGS = PCC_FLAGS.replace('-Wpedantic', '-Wno-pedantic')
381        PCC_FLAGS = PCC_FLAGS.replace('-Wextra-semi-stmt', '-Wno-extra-semi-stmt')
382        PCC = getenv('PCC', PCC) + ' ' + getenv('PCCFLAGS', PCC_FLAGS)
383        PCC_SHARED = str.join(' ', (PCC, ccshared, cflags))
384        # PETSc C++ compiler
385        PCXX = PCC if self.language == 'c++' else self.get('CXX', cxx)
386        # PETSc linker
387        PLD = self['PCC_LINKER']
388        PLD_FLAGS = get_flags(ld) + ' ' + self['PCC_LINKER_FLAGS']
389        PLD_FLAGS = PLD_FLAGS.replace('-fvisibility=hidden', '')
390        PLD = getenv('PLD', PLD) + ' ' + getenv('PLDFLAGS', PLD_FLAGS)
391        PLD_SHARED = str.join(' ', (PLD, ldshared, ldflags))
392        #
393        compiler.set_executables(
394            compiler=PCC,
395            compiler_cxx=PCXX,
396            linker_exe=PLD,
397            compiler_so=PCC_SHARED,
398            linker_so=PLD_SHARED,
399        )
400        compiler.shared_lib_extension = so_ext
401
402    def log_info(self):
403        PETSC_DIR = self['PETSC_DIR']
404        PETSC_ARCH = self['PETSC_ARCH']
405        version = '.'.join([str(i) for i in self.version[0]])
406        release = ('development', 'release')[self.version[1]]
407        version_info = version + ' ' + release
408        integer_size = '%s-bit' % self['PETSC_INDEX_SIZE']
409        scalar_type = self['PETSC_SCALAR']
410        precision = self['PETSC_PRECISION']
411        language = self['PETSC_LANGUAGE']
412        compiler = self['PCC']
413        linker = self['PCC_LINKER']
414        log.info('PETSC_DIR:    %s' % PETSC_DIR)
415        log.info('PETSC_ARCH:   %s' % PETSC_ARCH)
416        log.info('version:      %s' % version_info)
417        log.info('integer-size: %s' % integer_size)
418        log.info('scalar-type:  %s' % scalar_type)
419        log.info('precision:    %s' % precision)
420        log.info('language:     %s' % language)
421        log.info('compiler:     %s' % compiler)
422        log.info('linker:       %s' % linker)
423
424
425# --------------------------------------------------------------------
426
427
428class Extension(_Extension):
429    pass
430
431
432# --------------------------------------------------------------------
433
434cmd_petsc_opts = [
435    ('petsc-dir=', None, 'define PETSC_DIR, overriding environmental variables'),
436    ('petsc-arch=', None, 'define PETSC_ARCH, overriding environmental variables'),
437]
438
439
440class config(_config):
441    Configure = PetscConfig
442
443    user_options = _config.user_options + cmd_petsc_opts
444
445    def initialize_options(self):
446        _config.initialize_options(self)
447        self.petsc_dir = None
448        self.petsc_arch = None
449
450    def get_config_arch(self, arch):
451        return config.Configure(self.petsc_dir, arch)
452
453    def run(self):
454        _config.run(self)
455        self.petsc_dir = config.get_petsc_dir(self.petsc_dir)
456        if self.petsc_dir is None:
457            return
458        petsc_arch = config.get_petsc_arch(self.petsc_dir, self.petsc_arch)
459        log.info('-' * 70)
460        log.info('PETSC_DIR:   %s' % self.petsc_dir)
461        arch_list = petsc_arch
462        if not arch_list:
463            arch_list = [None]
464        for arch in arch_list:
465            conf = self.get_config_arch(arch)
466            archname = conf.PETSC_ARCH or conf['PETSC_ARCH']
467            scalar_type = conf['PETSC_SCALAR']
468            precision = conf['PETSC_PRECISION']
469            language = conf['PETSC_LANGUAGE']
470            compiler = conf['PCC']
471            linker = conf['PCC_LINKER']
472            log.info('-' * 70)
473            log.info('PETSC_ARCH:  %s' % archname)
474            log.info(' * scalar-type: %s' % scalar_type)
475            log.info(' * precision:   %s' % precision)
476            log.info(' * language:    %s' % language)
477            log.info(' * compiler:    %s' % compiler)
478            log.info(' * linker:      %s' % linker)
479        log.info('-' * 70)
480
481    # @staticmethod
482    def get_petsc_dir(petsc_dir):
483        if not petsc_dir:
484            return None
485        petsc_dir = os.path.expandvars(petsc_dir)
486        if not petsc_dir or '$PETSC_DIR' in petsc_dir:
487            try:
488                import petsc
489
490                petsc_dir = petsc.get_petsc_dir()
491            except ImportError:
492                log.warn('PETSC_DIR not specified')
493                return None
494        petsc_dir = os.path.expanduser(petsc_dir)
495        petsc_dir = os.path.abspath(petsc_dir)
496        return config.chk_petsc_dir(petsc_dir)
497
498    get_petsc_dir = staticmethod(get_petsc_dir)
499
500    # @staticmethod
501    def chk_petsc_dir(petsc_dir):
502        if not os.path.isdir(petsc_dir):
503            log.error('invalid PETSC_DIR: %s (ignored)' % petsc_dir)
504            return None
505        return petsc_dir
506
507    chk_petsc_dir = staticmethod(chk_petsc_dir)
508
509    # @staticmethod
510    def get_petsc_arch(petsc_dir, petsc_arch):
511        if not petsc_dir:
512            return None
513        petsc_arch = os.path.expandvars(petsc_arch)
514        if not petsc_arch or '$PETSC_ARCH' in petsc_arch:
515            petsc_arch = ''
516            petsc_conf = os.path.join(petsc_dir, 'lib', 'petsc', 'conf')
517            if os.path.isdir(petsc_conf):
518                petscvariables = os.path.join(petsc_conf, 'petscvariables')
519                if os.path.exists(petscvariables):
520                    conf = makefile(open(petscvariables, 'rt'))
521                    petsc_arch = conf.get('PETSC_ARCH', '')
522        petsc_arch = petsc_arch.split(os.pathsep)
523        petsc_arch = unique(petsc_arch)
524        petsc_arch = [arch for arch in petsc_arch if arch]
525        return config.chk_petsc_arch(petsc_dir, petsc_arch)
526
527    get_petsc_arch = staticmethod(get_petsc_arch)
528
529    # @staticmethod
530    def chk_petsc_arch(petsc_dir, petsc_arch):
531        valid_archs = []
532        for arch in petsc_arch:
533            arch_path = os.path.join(petsc_dir, arch)
534            if os.path.isdir(arch_path):
535                valid_archs.append(arch)
536            else:
537                log.warn('invalid PETSC_ARCH: %s (ignored)' % arch)
538        return valid_archs
539
540    chk_petsc_arch = staticmethod(chk_petsc_arch)
541
542
543class build(_build):
544    user_options = _build.user_options
545    user_options += [
546        (
547            'inplace',
548            'i',
549            'ignore build-lib and put compiled extensions into the source '
550            'directory alongside your pure Python modules',
551        )
552    ]
553    user_options += cmd_petsc_opts
554
555    boolean_options = _build.boolean_options
556    boolean_options += ['inplace']
557
558    def initialize_options(self):
559        _build.initialize_options(self)
560        self.inplace = None
561        self.petsc_dir = None
562        self.petsc_arch = None
563
564    def finalize_options(self):
565        _build.finalize_options(self)
566        if self.inplace is None:
567            self.inplace = False
568        self.set_undefined_options(
569            'config', ('petsc_dir', 'petsc_dir'), ('petsc_arch', 'petsc_arch')
570        )
571        self.petsc_dir = config.get_petsc_dir(self.petsc_dir)
572        self.petsc_arch = config.get_petsc_arch(self.petsc_dir, self.petsc_arch)
573
574    sub_commands = [('build_src', lambda *args: True)] + _build.sub_commands
575
576
577class build_src(Command):
578    description = 'build C sources from Cython files'
579
580    user_options = [
581        ('force', 'f', 'forcibly build everything (ignore file timestamps)'),
582    ]
583
584    boolean_options = ['force']
585
586    def initialize_options(self):
587        self.force = False
588
589    def finalize_options(self):
590        self.set_undefined_options(
591            'build',
592            ('force', 'force'),
593        )
594
595    def run(self):
596        sources = getattr(self, 'sources', [])
597        for source in sources:
598            cython_run(force=self.force, VERSION=cython_req(), **source)
599
600
601class build_ext(_build_ext):
602    user_options = _build_ext.user_options + cmd_petsc_opts
603
604    def initialize_options(self):
605        _build_ext.initialize_options(self)
606        self.inplace = None
607        self.petsc_dir = None
608        self.petsc_arch = None
609        self._outputs = []
610
611    def finalize_options(self):
612        _build_ext.finalize_options(self)
613        self.set_undefined_options('build', ('inplace', 'inplace'))
614        self.set_undefined_options(
615            'build', ('petsc_dir', 'petsc_dir'), ('petsc_arch', 'petsc_arch')
616        )
617
618    def _copy_ext(self, ext):
619        extclass = ext.__class__
620        fullname = self.get_ext_fullname(ext.name)
621        modpath = str.split(fullname, '.')
622        pkgpath = os.path.join('', *modpath[0:-1])
623        name = modpath[-1]
624        sources = list(ext.sources)
625        newext = extclass(name, sources)
626        newext.__dict__.update(copy.deepcopy(ext.__dict__))
627        newext.name = name
628        return pkgpath, newext
629
630    def _build_ext_arch(self, ext, pkgpath, arch):
631        build_temp = self.build_temp
632        build_lib = self.build_lib
633        try:
634            self.build_temp = os.path.join(build_temp, arch)
635            self.build_lib = os.path.join(build_lib, pkgpath, arch)
636            _build_ext.build_extension(self, ext)
637        finally:
638            self.build_temp = build_temp
639            self.build_lib = build_lib
640
641    def get_config_arch(self, arch):
642        return config.Configure(self.petsc_dir, arch)
643
644    def build_extension(self, ext):
645        if not isinstance(ext, Extension):
646            return _build_ext.build_extension(self, ext)
647        petsc_arch = self.petsc_arch
648        if not petsc_arch:
649            petsc_arch = [None]
650        for arch in petsc_arch:
651            config = self.get_config_arch(arch)
652            ARCH = arch or config['PETSC_ARCH']
653            if ARCH not in self.PETSC_ARCH_LIST:
654                self.PETSC_ARCH_LIST.append(ARCH)
655            self.DESTDIR = config.DESTDIR
656            ext.language = config.language
657            config.log_info()
658            pkgpath, newext = self._copy_ext(ext)
659            config.configure(newext, self.compiler)
660            self._build_ext_arch(newext, pkgpath, ARCH)
661        return None
662
663    def run(self):
664        self.build_sources()
665        _build_ext.run(self)
666        self.build_stubs()
667
668    def build_sources(self):
669        if 'build_src' in self.distribution.cmdclass:
670            self.run_command('build_src')
671
672    def build_stubs(self):
673        pkgname = self.distribution.get_name()
674        modname = self.extensions[0].name.split(".")[-1]
675        srcdir = Path(__file__).parent.parent / 'src' / pkgname
676        blddir = Path(self.build_lib) / pkgname
677
678        alldeps = glob.glob(str(blddir / 'lib' / '*' / f'{modname}.*'))
679        target =  srcdir / f'{modname}.pyi'
680        if not (self.force or modified.newer_group(alldeps, target)):
681            log.debug(f"skipping '{modname}.*.so' -> '{target}' (up-to-date)")
682            return
683
684        env = os.environ.copy()
685        python_path = env.get('PYTHONPATH', "")
686        if python_path != "":
687            python_path += ":"
688        python_path += self.build_lib
689        env['PYTHONPATH'] = python_path
690        env.pop('PETSC_ARCH', None)
691
692        stubgen = Path(__file__).parent / 'stubgen.py'
693        rc = subprocess.call([sys.executable, stubgen], env=env) # noqa S603
694        if rc != 0:
695            log.warn("Stubs could not be generated.")
696            return
697
698        self.copy_file(
699            srcdir / f'{modname}.pyi',
700            blddir / f'{modname}.pyi',
701            level=self.verbose,
702        )
703
704    def build_extensions(self, *args, **kargs):
705        self.PETSC_ARCH_LIST = []
706        _build_ext.build_extensions(self, *args, **kargs)
707        if not self.PETSC_ARCH_LIST:
708            return
709        self.build_configuration(self.PETSC_ARCH_LIST)
710
711    def build_configuration(self, arch_list):
712        #
713        template, variables = self.get_config_data(arch_list)
714        config_data = template % variables
715        #
716        build_lib = self.build_lib
717        dist_name = self.distribution.get_name()
718        config_file = os.path.join(
719            build_lib, dist_name, 'lib', dist_name.replace('4py', '') + '.cfg'
720        )
721
722        #
723        def write_file(filename, data):
724            with open(filename, 'w') as fh:
725                fh.write(config_data)
726
727        execute(
728            write_file,
729            (config_file, config_data),
730            msg='writing %s' % config_file,
731            verbose=self.verbose,
732        )
733
734    def get_config_data(self, arch_list):
735        DESTDIR = self.DESTDIR
736        template = (
737            '\n'.join(
738                [
739                    'PETSC_DIR  = %(PETSC_DIR)s',
740                    'PETSC_ARCH = %(PETSC_ARCH)s',
741                ]
742            )
743            + '\n'
744        )
745        variables = {
746            'PETSC_DIR': strip_prefix(DESTDIR, self.petsc_dir),
747            'PETSC_ARCH': os.path.pathsep.join(arch_list),
748        }
749        return template, variables
750
751    def copy_extensions_to_source(self):
752        build_py = self.get_finalized_command('build_py')
753        for ext in self.extensions:
754            inp_file, reg_file = self._get_inplace_equivalent(build_py, ext)
755
756            arch_list = ['']
757            if isinstance(ext, Extension) and self.petsc_arch:
758                arch_list = self.petsc_arch[:]
759
760            file_pairs = []
761            inp_head, inp_tail = os.path.split(inp_file)
762            reg_head, reg_tail = os.path.split(reg_file)
763            for arch in arch_list:
764                inp_file = os.path.join(inp_head, arch, inp_tail)
765                reg_file = os.path.join(reg_head, arch, reg_tail)
766                file_pairs.append((inp_file, reg_file))
767
768            for inp_file, reg_file in file_pairs:
769                if os.path.exists(reg_file) or not ext.optional:
770                    dest_dir, _ = os.path.split(inp_file)
771                    self.mkpath(dest_dir)
772                    self.copy_file(reg_file, inp_file, level=self.verbose)
773
774    def get_outputs(self):
775        self.check_extensions_list(self.extensions)
776        outputs = []
777        for ext in self.extensions:
778            fullname = self.get_ext_fullname(ext.name)
779            filename = self.get_ext_filename(fullname)
780            if isinstance(ext, Extension) and self.petsc_arch:
781                head, tail = os.path.split(filename)
782                for arch in self.petsc_arch:
783                    outfile = os.path.join(self.build_lib, head, arch, tail)
784                    outputs.append(outfile)
785            else:
786                outfile = os.path.join(self.build_lib, filename)
787                outputs.append(outfile)
788
789        pkgname = self.distribution.get_name()
790        modname = self.extensions[0].name.split(".")[-1]
791        outputs.append(os.path.join(self.build_lib, pkgname, f"{modname}.pyi"))
792        return list(set(outputs))
793
794    def get_source_files(self):
795        orig = log.set_threshold(log.WARN)
796        try:
797            return super().get_source_files()
798        finally:
799            log.set_threshold(orig)
800
801
802class install(_install):
803    def initialize_options(self):
804        with warnings.catch_warnings():
805            if setuptools:
806                if hasattr(setuptools, 'SetuptoolsDeprecationWarning'):
807                    category = setuptools.SetuptoolsDeprecationWarning
808                    warnings.simplefilter('ignore', category)
809            _install.initialize_options(self)
810        self.old_and_unmanageable = True
811
812
813cmdclass_list = [
814    config,
815    build,
816    build_src,
817    build_ext,
818    install,
819]
820
821# --------------------------------------------------------------------
822
823
824def setup(**attrs):
825    cmdclass = attrs.setdefault('cmdclass', {})
826    for cmd in cmdclass_list:
827        cmdclass.setdefault(cmd.__name__, cmd)
828    build_src.sources = attrs.pop('cython_sources', None)
829    use_setup_requires = False  # handle Cython requirement ourselves
830    if setuptools and build_src.sources and use_setup_requires:
831        version = cython_req()
832        if not cython_chk(version, verbose=False):
833            reqs = attrs.setdefault('setup_requires', [])
834            reqs += ['Cython>=' + version]
835    return _setup(**attrs)
836
837
838# --------------------------------------------------------------------
839
840if setuptools:
841    try:
842        from setuptools.command import egg_info as mod_egg_info
843
844        _FileList = mod_egg_info.FileList
845
846        class FileList(_FileList):
847            def process_template_line(self, line):
848                level = log.set_threshold(log.ERROR)
849                try:
850                    _FileList.process_template_line(self, line)
851                finally:
852                    log.set_threshold(level)
853
854        mod_egg_info.FileList = FileList
855    except (ImportError, AttributeError):
856        pass
857
858# --------------------------------------------------------------------
859
860
861def append(seq, item):
862    if item not in seq:
863        seq.append(item)
864
865
866def append_dict(conf, dct):
867    for key, values in dct.items():
868        if key in conf:
869            for value in values:
870                if value not in conf[key]:
871                    conf[key].append(value)
872
873
874def unique(seq):
875    res = []
876    for item in seq:
877        if item not in res:
878            res.append(item)
879    return res
880
881
882def flaglist(flags):
883    conf = {
884        'define_macros': [],
885        'undef_macros': [],
886        'include_dirs': [],
887        'libraries': [],
888        'library_dirs': [],
889        'runtime_library_dirs': [],
890        'extra_compile_args': [],
891        'extra_link_args': [],
892    }
893
894    if isinstance(flags, str):
895        flags = flags.split()
896
897    switch = '-Wl,'
898    newflags = []
899    linkopts = []
900    for f in flags:
901        if f.startswith(switch):
902            if len(f) > 4:
903                append(linkopts, f[4:])
904        else:
905            append(newflags, f)
906    if linkopts:
907        newflags.append(switch + ','.join(linkopts))
908    flags = newflags
909
910    append_next_word = None
911
912    for word in flags:
913        if append_next_word is not None:
914            append(append_next_word, word)
915            append_next_word = None
916            continue
917
918        switch, value = word[0:2], word[2:]
919
920        if switch == '-I':
921            append(conf['include_dirs'], value)
922        elif switch == '-D':
923            try:
924                idx = value.index('=')
925                macro = (value[:idx], value[idx + 1 :])
926            except ValueError:
927                macro = (value, None)
928            append(conf['define_macros'], macro)
929        elif switch == '-U':
930            append(conf['undef_macros'], value)
931        elif switch == '-l':
932            append(conf['libraries'], value)
933        elif switch == '-L':
934            append(conf['library_dirs'], value)
935        elif switch == '-R':
936            append(conf['runtime_library_dirs'], value)
937        elif word.startswith('-Wl'):
938            linkopts = word.split(',')
939            append_dict(conf, flaglist(linkopts[1:]))
940        elif word == '-rpath':
941            append_next_word = conf['runtime_library_dirs']
942        elif word == '-Xlinker':
943            append_next_word = conf['extra_link_args']
944        else:
945            # log.warn("unrecognized flag '%s'" % word)
946            pass
947    return conf
948
949
950def prepend_to_flags(path, flags):
951    """Prepend a path to compiler flags with absolute paths"""
952    if not path:
953        return flags
954
955    def append_path(m):
956        switch = m.group(1)
957        open_quote = m.group(4)
958        old_path = m.group(5)
959        close_quote = m.group(6)
960        if os.path.isabs(old_path):
961            moded_path = os.path.normpath(path + os.path.sep + old_path)
962            return switch + open_quote + moded_path + close_quote
963        return m.group(0)
964
965    return re.sub(r'((^|\s+)(-I|-L))(\s*["\']?)(\S+)(["\']?)', append_path, flags)
966
967
968def strip_prefix(prefix, string):
969    if not prefix:
970        return string
971    return re.sub(r'^' + prefix, '', string)
972
973
974# --------------------------------------------------------------------
975
976# Regexes needed for parsing Makefile-like syntaxes
977_variable_rx = re.compile(r'([a-zA-Z][a-zA-Z0-9_]+)\s*=\s*(.*)')
978_findvar1_rx = re.compile(r'\$\(([A-Za-z][A-Za-z0-9_]*)\)')
979_findvar2_rx = re.compile(r'\${([A-Za-z][A-Za-z0-9_]*)}')
980
981
982def makefile(fileobj, dct=None):
983    """Parse a Makefile-style file.
984
985    A dictionary containing name/value pairs is returned.  If an
986    optional dictionary is passed in as the second argument, it is
987    used instead of a new dictionary.
988    """
989    fp = TextFile(file=fileobj, strip_comments=1, skip_blanks=1, join_lines=1)
990
991    if dct is None:
992        dct = {}
993    done = {}
994    notdone = {}
995
996    while 1:
997        line = fp.readline()
998        if line is None:  # eof
999            break
1000        m = _variable_rx.match(line)
1001        if m:
1002            n, v = m.group(1, 2)
1003            v = str.strip(v)
1004            if '$' in v:
1005                notdone[n] = v
1006            else:
1007                try:
1008                    v = int(v)
1009                except ValueError:
1010                    pass
1011                done[n] = v
1012                try:
1013                    del notdone[n]
1014                except KeyError:
1015                    pass
1016    fp.close()
1017
1018    # do variable interpolation here
1019    while notdone:
1020        for name in list(notdone.keys()):
1021            value = notdone[name]
1022            m = _findvar1_rx.search(value) or _findvar2_rx.search(value)
1023            if m:
1024                n = m.group(1)
1025                found = True
1026                if n in done:
1027                    item = str(done[n])
1028                elif n in notdone:
1029                    # get it on a subsequent round
1030                    found = False
1031                else:
1032                    done[n] = item = ''
1033                if found:
1034                    after = value[m.end() :]
1035                    value = value[: m.start()] + item + after
1036                    if '$' in after:
1037                        notdone[name] = value
1038                    else:
1039                        try:
1040                            value = int(value)
1041                        except ValueError:
1042                            done[name] = str.strip(value)
1043                        else:
1044                            done[name] = value
1045                        del notdone[name]
1046            else:
1047                # bogus variable reference;
1048                # just drop it since we can't deal
1049                del notdone[name]
1050    # save the results in the global dictionary
1051    dct.update(done)
1052    return dct
1053
1054
1055# --------------------------------------------------------------------
1056