xref: /petsc/src/binding/petsc4py/conf/confpetsc.py (revision bcd4bb4a4158aa96f212e9537e87b40407faf83e)
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            dry_run=self.dry_run,
733        )
734
735    def get_config_data(self, arch_list):
736        DESTDIR = self.DESTDIR
737        template = (
738            '\n'.join(
739                [
740                    'PETSC_DIR  = %(PETSC_DIR)s',
741                    'PETSC_ARCH = %(PETSC_ARCH)s',
742                ]
743            )
744            + '\n'
745        )
746        variables = {
747            'PETSC_DIR': strip_prefix(DESTDIR, self.petsc_dir),
748            'PETSC_ARCH': os.path.pathsep.join(arch_list),
749        }
750        return template, variables
751
752    def copy_extensions_to_source(self):
753        build_py = self.get_finalized_command('build_py')
754        for ext in self.extensions:
755            inp_file, reg_file = self._get_inplace_equivalent(build_py, ext)
756
757            arch_list = ['']
758            if isinstance(ext, Extension) and self.petsc_arch:
759                arch_list = self.petsc_arch[:]
760
761            file_pairs = []
762            inp_head, inp_tail = os.path.split(inp_file)
763            reg_head, reg_tail = os.path.split(reg_file)
764            for arch in arch_list:
765                inp_file = os.path.join(inp_head, arch, inp_tail)
766                reg_file = os.path.join(reg_head, arch, reg_tail)
767                file_pairs.append((inp_file, reg_file))
768
769            for inp_file, reg_file in file_pairs:
770                if os.path.exists(reg_file) or not ext.optional:
771                    dest_dir, _ = os.path.split(inp_file)
772                    self.mkpath(dest_dir)
773                    self.copy_file(reg_file, inp_file, level=self.verbose)
774
775    def get_outputs(self):
776        self.check_extensions_list(self.extensions)
777        outputs = []
778        for ext in self.extensions:
779            fullname = self.get_ext_fullname(ext.name)
780            filename = self.get_ext_filename(fullname)
781            if isinstance(ext, Extension) and self.petsc_arch:
782                head, tail = os.path.split(filename)
783                for arch in self.petsc_arch:
784                    outfile = os.path.join(self.build_lib, head, arch, tail)
785                    outputs.append(outfile)
786            else:
787                outfile = os.path.join(self.build_lib, filename)
788                outputs.append(outfile)
789
790        pkgname = self.distribution.get_name()
791        modname = self.extensions[0].name.split(".")[-1]
792        outputs.append(os.path.join(self.build_lib, pkgname, f"{modname}.pyi"))
793        return list(set(outputs))
794
795    def get_source_files(self):
796        orig = log.set_threshold(log.WARN)
797        try:
798            return super().get_source_files()
799        finally:
800            log.set_threshold(orig)
801
802
803class install(_install):
804    def initialize_options(self):
805        with warnings.catch_warnings():
806            if setuptools:
807                if hasattr(setuptools, 'SetuptoolsDeprecationWarning'):
808                    category = setuptools.SetuptoolsDeprecationWarning
809                    warnings.simplefilter('ignore', category)
810            _install.initialize_options(self)
811        self.old_and_unmanageable = True
812
813
814cmdclass_list = [
815    config,
816    build,
817    build_src,
818    build_ext,
819    install,
820]
821
822# --------------------------------------------------------------------
823
824
825def setup(**attrs):
826    cmdclass = attrs.setdefault('cmdclass', {})
827    for cmd in cmdclass_list:
828        cmdclass.setdefault(cmd.__name__, cmd)
829    build_src.sources = attrs.pop('cython_sources', None)
830    use_setup_requires = False  # handle Cython requirement ourselves
831    if setuptools and build_src.sources and use_setup_requires:
832        version = cython_req()
833        if not cython_chk(version, verbose=False):
834            reqs = attrs.setdefault('setup_requires', [])
835            reqs += ['Cython>=' + version]
836    return _setup(**attrs)
837
838
839# --------------------------------------------------------------------
840
841if setuptools:
842    try:
843        from setuptools.command import egg_info as mod_egg_info
844
845        _FileList = mod_egg_info.FileList
846
847        class FileList(_FileList):
848            def process_template_line(self, line):
849                level = log.set_threshold(log.ERROR)
850                try:
851                    _FileList.process_template_line(self, line)
852                finally:
853                    log.set_threshold(level)
854
855        mod_egg_info.FileList = FileList
856    except (ImportError, AttributeError):
857        pass
858
859# --------------------------------------------------------------------
860
861
862def append(seq, item):
863    if item not in seq:
864        seq.append(item)
865
866
867def append_dict(conf, dct):
868    for key, values in dct.items():
869        if key in conf:
870            for value in values:
871                if value not in conf[key]:
872                    conf[key].append(value)
873
874
875def unique(seq):
876    res = []
877    for item in seq:
878        if item not in res:
879            res.append(item)
880    return res
881
882
883def flaglist(flags):
884    conf = {
885        'define_macros': [],
886        'undef_macros': [],
887        'include_dirs': [],
888        'libraries': [],
889        'library_dirs': [],
890        'runtime_library_dirs': [],
891        'extra_compile_args': [],
892        'extra_link_args': [],
893    }
894
895    if isinstance(flags, str):
896        flags = flags.split()
897
898    switch = '-Wl,'
899    newflags = []
900    linkopts = []
901    for f in flags:
902        if f.startswith(switch):
903            if len(f) > 4:
904                append(linkopts, f[4:])
905        else:
906            append(newflags, f)
907    if linkopts:
908        newflags.append(switch + ','.join(linkopts))
909    flags = newflags
910
911    append_next_word = None
912
913    for word in flags:
914        if append_next_word is not None:
915            append(append_next_word, word)
916            append_next_word = None
917            continue
918
919        switch, value = word[0:2], word[2:]
920
921        if switch == '-I':
922            append(conf['include_dirs'], value)
923        elif switch == '-D':
924            try:
925                idx = value.index('=')
926                macro = (value[:idx], value[idx + 1 :])
927            except ValueError:
928                macro = (value, None)
929            append(conf['define_macros'], macro)
930        elif switch == '-U':
931            append(conf['undef_macros'], value)
932        elif switch == '-l':
933            append(conf['libraries'], value)
934        elif switch == '-L':
935            append(conf['library_dirs'], value)
936        elif switch == '-R':
937            append(conf['runtime_library_dirs'], value)
938        elif word.startswith('-Wl'):
939            linkopts = word.split(',')
940            append_dict(conf, flaglist(linkopts[1:]))
941        elif word == '-rpath':
942            append_next_word = conf['runtime_library_dirs']
943        elif word == '-Xlinker':
944            append_next_word = conf['extra_link_args']
945        else:
946            # log.warn("unrecognized flag '%s'" % word)
947            pass
948    return conf
949
950
951def prepend_to_flags(path, flags):
952    """Prepend a path to compiler flags with absolute paths"""
953    if not path:
954        return flags
955
956    def append_path(m):
957        switch = m.group(1)
958        open_quote = m.group(4)
959        old_path = m.group(5)
960        close_quote = m.group(6)
961        if os.path.isabs(old_path):
962            moded_path = os.path.normpath(path + os.path.sep + old_path)
963            return switch + open_quote + moded_path + close_quote
964        return m.group(0)
965
966    return re.sub(r'((^|\s+)(-I|-L))(\s*["\']?)(\S+)(["\']?)', append_path, flags)
967
968
969def strip_prefix(prefix, string):
970    if not prefix:
971        return string
972    return re.sub(r'^' + prefix, '', string)
973
974
975# --------------------------------------------------------------------
976
977# Regexes needed for parsing Makefile-like syntaxes
978_variable_rx = re.compile(r'([a-zA-Z][a-zA-Z0-9_]+)\s*=\s*(.*)')
979_findvar1_rx = re.compile(r'\$\(([A-Za-z][A-Za-z0-9_]*)\)')
980_findvar2_rx = re.compile(r'\${([A-Za-z][A-Za-z0-9_]*)}')
981
982
983def makefile(fileobj, dct=None):
984    """Parse a Makefile-style file.
985
986    A dictionary containing name/value pairs is returned.  If an
987    optional dictionary is passed in as the second argument, it is
988    used instead of a new dictionary.
989    """
990    fp = TextFile(file=fileobj, strip_comments=1, skip_blanks=1, join_lines=1)
991
992    if dct is None:
993        dct = {}
994    done = {}
995    notdone = {}
996
997    while 1:
998        line = fp.readline()
999        if line is None:  # eof
1000            break
1001        m = _variable_rx.match(line)
1002        if m:
1003            n, v = m.group(1, 2)
1004            v = str.strip(v)
1005            if '$' in v:
1006                notdone[n] = v
1007            else:
1008                try:
1009                    v = int(v)
1010                except ValueError:
1011                    pass
1012                done[n] = v
1013                try:
1014                    del notdone[n]
1015                except KeyError:
1016                    pass
1017    fp.close()
1018
1019    # do variable interpolation here
1020    while notdone:
1021        for name in list(notdone.keys()):
1022            value = notdone[name]
1023            m = _findvar1_rx.search(value) or _findvar2_rx.search(value)
1024            if m:
1025                n = m.group(1)
1026                found = True
1027                if n in done:
1028                    item = str(done[n])
1029                elif n in notdone:
1030                    # get it on a subsequent round
1031                    found = False
1032                else:
1033                    done[n] = item = ''
1034                if found:
1035                    after = value[m.end() :]
1036                    value = value[: m.start()] + item + after
1037                    if '$' in after:
1038                        notdone[name] = value
1039                    else:
1040                        try:
1041                            value = int(value)
1042                        except ValueError:
1043                            done[name] = str.strip(value)
1044                        else:
1045                            done[name] = value
1046                        del notdone[name]
1047            else:
1048                # bogus variable reference;
1049                # just drop it since we can't deal
1050                del notdone[name]
1051    # save the results in the global dictionary
1052    dct.update(done)
1053    return dct
1054
1055
1056# --------------------------------------------------------------------
1057