Dependencies.py 37.9 KB
Newer Older
1
from __future__ import absolute_import, print_function
2

3
import cython
4
from .. import __version__
5

Robert Bradshaw's avatar
Robert Bradshaw committed
6
import re, os, sys, time
Robert Bradshaw's avatar
Robert Bradshaw committed
7
from glob import iglob
Stefan Behnel's avatar
Stefan Behnel committed
8

9 10
try:
    import gzip
Stefan Behnel's avatar
Stefan Behnel committed
11
    gzip_open = gzip.open
12 13 14 15
    gzip_ext = '.gz'
except ImportError:
    gzip_open = open
    gzip_ext = ''
16
import shutil
Robert Bradshaw's avatar
Robert Bradshaw committed
17
import subprocess
18 19 20 21 22

try:
    import hashlib
except ImportError:
    import md5 as hashlib
Robert Bradshaw's avatar
Robert Bradshaw committed
23

24 25 26 27 28
try:
    from io import open as io_open
except ImportError:
    from codecs import open as io_open

29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
try:
    from os.path import relpath as _relpath
except ImportError:
    # Py<2.6
    def _relpath(path, start=os.path.curdir):
        if not path:
            raise ValueError("no path specified")
        start_list = os.path.abspath(start).split(os.path.sep)
        path_list = os.path.abspath(path).split(os.path.sep)
        i = len(os.path.commonprefix([start_list, path_list]))
        rel_list = [os.path.pardir] * (len(start_list)-i) + path_list[i:]
        if not rel_list:
            return os.path.curdir
        return os.path.join(*rel_list)

Robert Bradshaw's avatar
Robert Bradshaw committed
44

45 46
from distutils.extension import Extension

47
from .. import Utils
48
from ..Utils import (cached_function, cached_method, path_exists,
Robert Bradshaw's avatar
Robert Bradshaw committed
49
    safe_makedirs, copy_file_to_dir_if_newer, is_package_dir)
50
from ..Compiler.Main import Context, CompilationOptions, default_options
51

52
join_path = cached_function(os.path.join)
Robert Bradshaw's avatar
Robert Bradshaw committed
53
copy_once_if_newer = cached_function(copy_file_to_dir_if_newer)
54
safe_makedirs_once = cached_function(safe_makedirs)
55 56 57 58 59 60 61

if sys.version_info[0] < 3:
    # stupid Py2 distutils enforces str type in list of sources
    _fs_encoding = sys.getfilesystemencoding()
    if _fs_encoding is None:
        _fs_encoding = sys.getdefaultencoding()
    def encode_filename_in_py2(filename):
62
        if not isinstance(filename, bytes):
63 64 65 66 67
            return filename.encode(_fs_encoding)
        return filename
else:
    def encode_filename_in_py2(filename):
        return filename
Robert Bradshaw's avatar
Robert Bradshaw committed
68
    basestring = str
69

Stefan Behnel's avatar
Stefan Behnel committed
70

71
def extended_iglob(pattern):
72 73 74 75 76 77 78 79
    if '{' in pattern:
        m = re.match('(.*){([^}]+)}(.*)', pattern)
        if m:
            before, switch, after = m.groups()
            for case in switch.split(','):
                for path in extended_iglob(before + case + after):
                    yield path
            return
80 81 82
    if '**/' in pattern:
        seen = set()
        first, rest = pattern.split('**/', 1)
83
        if first:
84
            first = iglob(first+'/')
85 86 87
        else:
            first = ['']
        for root in first:
88
            for path in extended_iglob(join_path(root, rest)):
89 90 91
                if path not in seen:
                    seen.add(path)
                    yield path
92
            for path in extended_iglob(join_path(root, '*', '**/' + rest)):
93 94 95 96
                if path not in seen:
                    seen.add(path)
                    yield path
    else:
97
        for path in iglob(pattern):
98
            yield path
99

100 101 102 103 104 105 106

def nonempty(it, error_msg="expected non-empty iterator"):
    empty = True
    for value in it:
        empty = False
        yield value
    if empty:
107 108
        raise ValueError(error_msg)

109

110 111 112 113 114
@cached_function
def file_hash(filename):
    path = os.path.normpath(filename.encode("UTF-8"))
    m = hashlib.md5(str(len(path)) + ":")
    m.update(path)
115 116 117 118 119 120 121 122
    f = open(filename, 'rb')
    try:
        data = f.read(65000)
        while data:
            m.update(data)
            data = f.read(65000)
    finally:
        f.close()
123 124
    return m.hexdigest()

Stefan Behnel's avatar
Stefan Behnel committed
125

126
def parse_list(s):
127
    """
Jeroen Demeyer's avatar
Jeroen Demeyer committed
128 129 130 131
    >>> parse_list("")
    []
    >>> parse_list("a")
    ['a']
132 133 134 135 136 137 138 139 140
    >>> parse_list("a b c")
    ['a', 'b', 'c']
    >>> parse_list("[a, b, c]")
    ['a', 'b', 'c']
    >>> parse_list('a " " b')
    ['a', ' ', 'b']
    >>> parse_list('[a, ",a", "a,", ",", ]')
    ['a', ',a', 'a,', ',']
    """
Jeroen Demeyer's avatar
Jeroen Demeyer committed
141
    if len(s) >= 2 and s[0] == '[' and s[-1] == ']':
142 143 144 145 146 147 148
        s = s[1:-1]
        delimiter = ','
    else:
        delimiter = ' '
    s, literals = strip_string_literals(s)
    def unquote(literal):
        literal = literal.strip()
149
        if literal[0] in "'\"":
150 151 152
            return literals[literal[1:-1]]
        else:
            return literal
153
    return [unquote(item) for item in s.split(delimiter) if item.strip()]
154

Stefan Behnel's avatar
Stefan Behnel committed
155

156 157 158 159 160 161 162 163 164 165 166 167 168
transitive_str = object()
transitive_list = object()

distutils_settings = {
    'name':                 str,
    'sources':              list,
    'define_macros':        list,
    'undef_macros':         list,
    'libraries':            transitive_list,
    'library_dirs':         transitive_list,
    'runtime_library_dirs': transitive_list,
    'include_dirs':         transitive_list,
    'extra_objects':        list,
169 170
    'extra_compile_args':   transitive_list,
    'extra_link_args':      transitive_list,
171 172 173 174 175
    'export_symbols':       list,
    'depends':              transitive_list,
    'language':             transitive_str,
}

Stefan Behnel's avatar
Stefan Behnel committed
176

177
@cython.locals(start=cython.Py_ssize_t, end=cython.Py_ssize_t)
178
def line_iter(source):
Robert Bradshaw's avatar
Robert Bradshaw committed
179
    if isinstance(source, basestring):
180 181 182 183 184 185 186 187
        start = 0
        while True:
            end = source.find('\n', start)
            if end == -1:
                yield source[start:]
                return
            yield source[start:end]
            start = end+1
Robert Bradshaw's avatar
Robert Bradshaw committed
188 189 190
    else:
        for line in source:
            yield line
191

Stefan Behnel's avatar
Stefan Behnel committed
192

193
class DistutilsInfo(object):
194

Robert Bradshaw's avatar
Robert Bradshaw committed
195
    def __init__(self, source=None, exn=None):
196
        self.values = {}
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
        if source is not None:
            for line in line_iter(source):
                line = line.strip()
                if line != '' and line[0] != '#':
                    break
                line = line[1:].strip()
                if line[:10] == 'distutils:':
                    line = line[10:]
                    ix = line.index('=')
                    key = str(line[:ix].strip())
                    value = line[ix+1:].strip()
                    type = distutils_settings[key]
                    if type in (list, transitive_list):
                        value = parse_list(value)
                        if key == 'define_macros':
                            value = [tuple(macro.split('=')) for macro in value]
                    self.values[key] = value
Robert Bradshaw's avatar
Robert Bradshaw committed
214
        elif exn is not None:
215
            for key in distutils_settings:
Robert Bradshaw's avatar
Robert Bradshaw committed
216
                if key in ('name', 'sources'):
217
                    continue
Robert Bradshaw's avatar
Robert Bradshaw committed
218 219 220
                value = getattr(exn, key, None)
                if value:
                    self.values[key] = value
221

222
    def merge(self, other):
Robert Bradshaw's avatar
Robert Bradshaw committed
223 224
        if other is None:
            return self
225 226 227 228 229 230
        for key, value in other.values.items():
            type = distutils_settings[key]
            if type is transitive_str and key not in self.values:
                self.values[key] = value
            elif type is transitive_list:
                if key in self.values:
231 232
                    # Change a *copy* of the list (Trac #845)
                    all = self.values[key][:]
233 234 235
                    for v in value:
                        if v not in all:
                            all.append(v)
236 237
                    value = all
                self.values[key] = value
238
        return self
239

240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260
    def subs(self, aliases):
        if aliases is None:
            return self
        resolved = DistutilsInfo()
        for key, value in self.values.items():
            type = distutils_settings[key]
            if type in [list, transitive_list]:
                new_value_list = []
                for v in value:
                    if v in aliases:
                        v = aliases[v]
                    if isinstance(v, list):
                        new_value_list += v
                    else:
                        new_value_list.append(v)
                value = new_value_list
            else:
                if value in aliases:
                    value = aliases[value]
            resolved.values[key] = value
        return resolved
261

262 263 264 265
    def apply(self, extension):
        for key, value in self.values.items():
            type = distutils_settings[key]
            if type in [list, transitive_list]:
266 267
                value = getattr(extension, key) + list(value)
            setattr(extension, key, value)
268

Stefan Behnel's avatar
Stefan Behnel committed
269

270 271 272 273
@cython.locals(start=cython.Py_ssize_t, q=cython.Py_ssize_t,
               single_q=cython.Py_ssize_t, double_q=cython.Py_ssize_t,
               hash_mark=cython.Py_ssize_t, end=cython.Py_ssize_t,
               k=cython.Py_ssize_t, counter=cython.Py_ssize_t, quote_len=cython.Py_ssize_t)
Robert Bradshaw's avatar
Robert Bradshaw committed
274 275
def strip_string_literals(code, prefix='__Pyx_L'):
    """
276
    Normalizes every string literal to be of the form '__Pyx_Lxxx',
Robert Bradshaw's avatar
Robert Bradshaw committed
277
    returning the normalized code and a mapping of labels to
278
    string literals.
Robert Bradshaw's avatar
Robert Bradshaw committed
279 280 281 282 283 284
    """
    new_code = []
    literals = {}
    counter = 0
    start = q = 0
    in_quote = False
285 286
    hash_mark = single_q = double_q = -1
    code_len = len(code)
Stefan Behnel's avatar
Stefan Behnel committed
287
    quote_type = quote_len = None
288

Robert Bradshaw's avatar
Robert Bradshaw committed
289
    while True:
290 291 292 293 294 295
        if hash_mark < q:
            hash_mark = code.find('#', q)
        if single_q < q:
            single_q = code.find("'", q)
        if double_q < q:
            double_q = code.find('"', q)
Robert Bradshaw's avatar
Robert Bradshaw committed
296
        q = min(single_q, double_q)
Stefan Behnel's avatar
Stefan Behnel committed
297 298
        if q == -1:
            q = max(single_q, double_q)
299

300
        # We're done.
Robert Bradshaw's avatar
Robert Bradshaw committed
301
        if q == -1 and hash_mark == -1:
302
            new_code.append(code[start:])
Robert Bradshaw's avatar
Robert Bradshaw committed
303
            break
304 305

        # Try to close the quote.
Robert Bradshaw's avatar
Robert Bradshaw committed
306
        elif in_quote:
307
            if code[q-1] == u'\\':
Robert Bradshaw's avatar
Robert Bradshaw committed
308
                k = 2
309
                while q >= k and code[q-k] == u'\\':
Robert Bradshaw's avatar
Robert Bradshaw committed
310 311 312 313
                    k += 1
                if k % 2 == 0:
                    q += 1
                    continue
Stefan Behnel's avatar
Stefan Behnel committed
314 315
            if code[q] == quote_type and (
                    quote_len == 1 or (code_len > q + 2 and quote_type == code[q+1] == code[q+2])):
Robert Bradshaw's avatar
Robert Bradshaw committed
316
                counter += 1
317
                label = "%s%s_" % (prefix, counter)
318 319 320 321 322 323
                literals[label] = code[start+quote_len:q]
                full_quote = code[q:q+quote_len]
                new_code.append(full_quote)
                new_code.append(label)
                new_code.append(full_quote)
                q += quote_len
Robert Bradshaw's avatar
Robert Bradshaw committed
324
                in_quote = False
Robert Bradshaw's avatar
Robert Bradshaw committed
325
                start = q
Robert Bradshaw's avatar
Robert Bradshaw committed
326 327
            else:
                q += 1
328

Robert Bradshaw's avatar
Robert Bradshaw committed
329 330 331
        # Process comment.
        elif -1 != hash_mark and (hash_mark < q or q == -1):
            new_code.append(code[start:hash_mark+1])
332
            end = code.find('\n', hash_mark)
Robert Bradshaw's avatar
Robert Bradshaw committed
333
            counter += 1
334
            label = "%s%s_" % (prefix, counter)
335 336 337 338 339
            if end == -1:
                end_or_none = None
            else:
                end_or_none = end
            literals[label] = code[hash_mark+1:end_or_none]
Robert Bradshaw's avatar
Robert Bradshaw committed
340
            new_code.append(label)
341
            if end == -1:
Robert Bradshaw's avatar
Robert Bradshaw committed
342
                break
343
            start = q = end
344 345

        # Open the quote.
Robert Bradshaw's avatar
Robert Bradshaw committed
346
        else:
347 348
            if code_len >= q+3 and (code[q] == code[q+1] == code[q+2]):
                quote_len = 3
Robert Bradshaw's avatar
Robert Bradshaw committed
349
            else:
350 351 352 353
                quote_len = 1
            in_quote = True
            quote_type = code[q]
            new_code.append(code[start:q])
Robert Bradshaw's avatar
Robert Bradshaw committed
354
            start = q
355
            q += quote_len
356

Robert Bradshaw's avatar
Robert Bradshaw committed
357 358
    return "".join(new_code), literals

359

360 361
dependency_regex = re.compile(r"(?:^from +([0-9a-zA-Z_.]+) +cimport)|"
                              r"(?:^cimport +([0-9a-zA-Z_.]+(?: *, *[0-9a-zA-Z_.]+)*))|"
362 363 364
                              r"(?:^cdef +extern +from +['\"]([^'\"]+)['\"])|"
                              r"(?:^include +['\"]([^'\"]+)['\"])", re.M)

Stefan Behnel's avatar
Stefan Behnel committed
365

366 367 368
def normalize_existing(base_path, rel_paths):
    return normalize_existing0(os.path.dirname(base_path), tuple(set(rel_paths)))

Stefan Behnel's avatar
Stefan Behnel committed
369

370 371
@cached_function
def normalize_existing0(base_dir, rel_paths):
372
    normalized = []
373
    for rel in rel_paths:
374
        path = join_path(base_dir, rel)
375 376 377
        if path_exists(path):
            normalized.append(os.path.normpath(path))
        else:
378
            normalized.append(rel)
379 380
    return normalized

Stefan Behnel's avatar
Stefan Behnel committed
381

382
def resolve_depends(depends, include_dirs):
383 384 385 386 387 388 389 390
    include_dirs = tuple(include_dirs)
    resolved = []
    for depend in depends:
        path = resolve_depend(depend, include_dirs)
        if path is not None:
            resolved.append(path)
    return resolved

Stefan Behnel's avatar
Stefan Behnel committed
391

392 393 394
@cached_function
def resolve_depend(depend, include_dirs):
    if depend[0] == '<' and depend[-1] == '>':
Stefan Behnel's avatar
Stefan Behnel committed
395
        return None
396 397 398 399
    for dir in include_dirs:
        path = join_path(dir, depend)
        if path_exists(path):
            return os.path.normpath(path)
Stefan Behnel's avatar
Stefan Behnel committed
400
    return None
401

Stefan Behnel's avatar
Stefan Behnel committed
402

Robert Bradshaw's avatar
Robert Bradshaw committed
403 404 405
@cached_function
def package(filename):
    dir = os.path.dirname(os.path.abspath(str(filename)))
406
    if dir != filename and is_package_dir(dir):
Robert Bradshaw's avatar
Robert Bradshaw committed
407 408 409 410
        return package(dir) + (os.path.basename(dir),)
    else:
        return ()

Stefan Behnel's avatar
Stefan Behnel committed
411

Robert Bradshaw's avatar
Robert Bradshaw committed
412
@cached_function
Stefan Behnel's avatar
Stefan Behnel committed
413
def fully_qualified_name(filename):
Robert Bradshaw's avatar
Robert Bradshaw committed
414 415 416 417
    module = os.path.splitext(os.path.basename(filename))[0]
    return '.'.join(package(filename) + (module,))


418
@cached_function
Robert Bradshaw's avatar
Robert Bradshaw committed
419
def parse_dependencies(source_filename):
420
    # Actual parsing is way too slow, so we use regular expressions.
Robert Bradshaw's avatar
Robert Bradshaw committed
421 422
    # The only catch is that we must strip comments and string
    # literals ahead of time.
423
    fh = Utils.open_source_file(source_filename, error_handling='ignore')
424 425 426 427
    try:
        source = fh.read()
    finally:
        fh.close()
428
    distutils_info = DistutilsInfo(source)
Robert Bradshaw's avatar
Robert Bradshaw committed
429
    source, literals = strip_string_literals(source)
430 431
    source = source.replace('\\\n', ' ').replace('\t', ' ')

Robert Bradshaw's avatar
Robert Bradshaw committed
432 433 434 435
    # TODO: pure mode
    cimports = []
    includes = []
    externs  = []
436 437
    for m in dependency_regex.finditer(source):
        cimport_from, cimport_list, extern, include = m.groups()
438 439
        if cimport_from:
            cimports.append(cimport_from)
440 441
        elif cimport_list:
            cimports.extend(x.strip() for x in cimport_list.split(","))
442 443
        elif extern:
            externs.append(literals[extern])
Robert Bradshaw's avatar
Robert Bradshaw committed
444
        else:
445
            includes.append(literals[include])
446
    return cimports, includes, externs, distutils_info
Robert Bradshaw's avatar
Robert Bradshaw committed
447 448 449


class DependencyTree(object):
450

451
    def __init__(self, context, quiet=False):
Robert Bradshaw's avatar
Robert Bradshaw committed
452
        self.context = context
453
        self.quiet = quiet
Robert Bradshaw's avatar
Robert Bradshaw committed
454
        self._transitive_cache = {}
455

Robert Bradshaw's avatar
Robert Bradshaw committed
456
    def parse_dependencies(self, source_filename):
457 458
        if path_exists(source_filename):
            source_filename = os.path.normpath(source_filename)
Robert Bradshaw's avatar
Robert Bradshaw committed
459
        return parse_dependencies(source_filename)
460

Robert Bradshaw's avatar
Robert Bradshaw committed
461
    @cached_method
462 463
    def included_files(self, filename):
        # This is messy because included files are textually included, resolving
464
        # cimports (but not includes) relative to the including file.
465 466
        all = set()
        for include in self.parse_dependencies(filename)[1]:
467
            include_path = join_path(os.path.dirname(filename), include)
468
            if not path_exists(include_path):
Robert Bradshaw's avatar
typo  
Robert Bradshaw committed
469
                include_path = self.context.find_include_file(include, None)
470
            if include_path:
471
                if '.' + os.path.sep in include_path:
472 473
                    include_path = os.path.normpath(include_path)
                all.add(include_path)
474
                all.update(self.included_files(include_path))
475
            elif not self.quiet:
Robert Bradshaw's avatar
Robert Bradshaw committed
476
                print("Unable to locate '%s' referenced from '%s'" % (filename, include))
477
        return all
478

479 480
    @cached_method
    def cimports_and_externs(self, filename):
481 482
        # This is really ugly. Nested cimports are resolved with respect to the
        # includer, but includes are resolved with respect to the includee.
483 484 485 486
        cimports, includes, externs = self.parse_dependencies(filename)[:3]
        cimports = set(cimports)
        externs = set(externs)
        for include in self.included_files(filename):
487 488 489
            included_cimports, included_externs = self.cimports_and_externs(include)
            cimports.update(included_cimports)
            externs.update(included_externs)
490
        return tuple(cimports), normalize_existing(filename, externs)
491

492 493
    def cimports(self, filename):
        return self.cimports_and_externs(filename)[0]
494

Robert Bradshaw's avatar
Robert Bradshaw committed
495
    def package(self, filename):
Robert Bradshaw's avatar
Robert Bradshaw committed
496
        return package(filename)
497

Stefan Behnel's avatar
Stefan Behnel committed
498 499
    def fully_qualified_name(self, filename):
        return fully_qualified_name(filename)
500

501
    @cached_method
Robert Bradshaw's avatar
Robert Bradshaw committed
502
    def find_pxd(self, module, filename=None):
503 504
        is_relative = module[0] == '.'
        if is_relative and not filename:
Stefan Behnel's avatar
Stefan Behnel committed
505
            raise NotImplementedError("New relative imports.")
Robert Bradshaw's avatar
Robert Bradshaw committed
506
        if filename is not None:
507 508 509 510 511 512 513 514 515 516 517
            module_path = module.split('.')
            if is_relative:
                module_path.pop(0)  # just explicitly relative
            package_path = list(self.package(filename))
            while module_path and not module_path[0]:
                try:
                    package_path.pop()
                except IndexError:
                    return None   # FIXME: error?
                module_path.pop(0)
            relative = '.'.join(package_path + module_path)
Robert Bradshaw's avatar
Robert Bradshaw committed
518 519 520
            pxd = self.context.find_pxd_file(relative, None)
            if pxd:
                return pxd
521 522
        if is_relative:
            return None   # FIXME: error?
Robert Bradshaw's avatar
Robert Bradshaw committed
523
        return self.context.find_pxd_file(module, None)
524

Robert Bradshaw's avatar
Robert Bradshaw committed
525
    @cached_method
Robert Bradshaw's avatar
Robert Bradshaw committed
526
    def cimported_files(self, filename):
527 528
        if filename[-4:] == '.pyx' and path_exists(filename[:-4] + '.pxd'):
            pxd_list = [filename[:-4] + '.pxd']
Robert Bradshaw's avatar
Robert Bradshaw committed
529
        else:
530 531
            pxd_list = []
        for module in self.cimports(filename):
532
            if module[:7] == 'cython.' or module == 'cython':
533 534
                continue
            pxd_file = self.find_pxd(module, filename)
535
            if pxd_file is not None:
536
                pxd_list.append(pxd_file)
537 538
            elif not self.quiet:
                print("missing cimport in module '%s': %s" % (module, filename))
539
        return tuple(pxd_list)
540

541
    @cached_method
542
    def immediate_dependencies(self, filename):
543 544 545 546 547 548 549
        all = set([filename])
        all.update(self.cimported_files(filename))
        all.update(self.included_files(filename))
        return all

    def all_dependencies(self, filename):
        return self.transitive_merge(filename, self.immediate_dependencies, set.union)
550

Robert Bradshaw's avatar
Robert Bradshaw committed
551
    @cached_method
Robert Bradshaw's avatar
Robert Bradshaw committed
552 553
    def timestamp(self, filename):
        return os.path.getmtime(filename)
554

Robert Bradshaw's avatar
Robert Bradshaw committed
555 556
    def extract_timestamp(self, filename):
        return self.timestamp(filename), filename
557

Robert Bradshaw's avatar
Robert Bradshaw committed
558
    def newest_dependency(self, filename):
559 560 561 562 563 564 565 566 567 568 569 570 571 572
        return max([self.extract_timestamp(f) for f in self.all_dependencies(filename)])

    def transitive_fingerprint(self, filename, extra=None):
        try:
            m = hashlib.md5(__version__)
            m.update(file_hash(filename))
            for x in sorted(self.all_dependencies(filename)):
                if os.path.splitext(x)[1] not in ('.c', '.cpp', '.h'):
                    m.update(file_hash(x))
            if extra is not None:
                m.update(str(extra))
            return m.hexdigest()
        except IOError:
            return None
573

574
    def distutils_info0(self, filename):
575 576 577 578
        info = self.parse_dependencies(filename)[3]
        externs = self.cimports_and_externs(filename)[1]
        if externs:
            if 'depends' in info.values:
Stefan Behnel's avatar
Stefan Behnel committed
579
                info.values['depends'] = list(set(info.values['depends']).union(externs))
580 581 582
            else:
                info.values['depends'] = list(externs)
        return info
583

Robert Bradshaw's avatar
Robert Bradshaw committed
584 585 586 587
    def distutils_info(self, filename, aliases=None, base=None):
        return (self.transitive_merge(filename, self.distutils_info0, DistutilsInfo.merge)
            .subs(aliases)
            .merge(base))
588

Robert Bradshaw's avatar
Robert Bradshaw committed
589 590 591 592 593 594 595
    def transitive_merge(self, node, extract, merge):
        try:
            seen = self._transitive_cache[extract, merge]
        except KeyError:
            seen = self._transitive_cache[extract, merge] = {}
        return self.transitive_merge_helper(
            node, extract, merge, seen, {}, self.cimported_files)[0]
596

Robert Bradshaw's avatar
Robert Bradshaw committed
597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620
    def transitive_merge_helper(self, node, extract, merge, seen, stack, outgoing):
        if node in seen:
            return seen[node], None
        deps = extract(node)
        if node in stack:
            return deps, node
        try:
            stack[node] = len(stack)
            loop = None
            for next in outgoing(node):
                sub_deps, sub_loop = self.transitive_merge_helper(next, extract, merge, seen, stack, outgoing)
                if sub_loop is not None:
                    if loop is not None and stack[loop] < stack[sub_loop]:
                        pass
                    else:
                        loop = sub_loop
                deps = merge(deps, sub_deps)
            if loop == node:
                loop = None
            if loop is None:
                seen[node] = deps
            return deps, loop
        finally:
            del stack[node]
621

Stefan Behnel's avatar
Stefan Behnel committed
622

623
_dep_tree = None
Stefan Behnel's avatar
Stefan Behnel committed
624

625
def create_dependency_tree(ctx=None, quiet=False):
626 627
    global _dep_tree
    if _dep_tree is None:
628
        if ctx is None:
Robert Bradshaw's avatar
Robert Bradshaw committed
629
            ctx = Context(["."], CompilationOptions(default_options))
630
        _dep_tree = DependencyTree(ctx, quiet=quiet)
631 632
    return _dep_tree

Stefan Behnel's avatar
Stefan Behnel committed
633

Robert Bradshaw's avatar
Robert Bradshaw committed
634
# This may be useful for advanced users?
635
def create_extension_list(patterns, exclude=None, ctx=None, aliases=None, quiet=False, language=None,
636
                          exclude_failures=False):
637 638
    if language is not None:
        print('Please put "# distutils: langauge=%s" in your .pyx or .pxd file(s)' % language)
639 640
    if exclude is None:
        exclude = []
641
    if not isinstance(patterns, (list, tuple)):
642
        patterns = [patterns]
643
    explicit_modules = set([m.name for m in patterns if isinstance(m, Extension)])
Robert Bradshaw's avatar
Robert Bradshaw committed
644
    seen = set()
645
    deps = create_dependency_tree(ctx, quiet=quiet)
646 647 648 649
    to_exclude = set()
    if not isinstance(exclude, list):
        exclude = [exclude]
    for pattern in exclude:
650
        to_exclude.update(map(os.path.abspath, extended_iglob(pattern)))
Stefan Behnel's avatar
Stefan Behnel committed
651

652
    module_list = []
653
    module_metadata = {}
654 655

    # workaround for setuptools
marscher's avatar
marscher committed
656
    if 'setuptools' in sys.modules:
Robert Bradshaw's avatar
Robert Bradshaw committed
657
        Extension_distutils = sys.modules['setuptools.extension']._Extension
658
        Extension_setuptools = sys.modules['setuptools'].Extension
marscher's avatar
marscher committed
659
    else:
660
        # dummy class, in case we do not have setuptools
Robert Bradshaw's avatar
Robert Bradshaw committed
661
        Extension_distutils = Extension
662
        class Extension_setuptools(Extension): pass
663

Robert Bradshaw's avatar
Robert Bradshaw committed
664 665 666 667 668 669 670
    for pattern in patterns:
        if isinstance(pattern, str):
            filepattern = pattern
            template = None
            name = '*'
            base = None
            exn_type = Extension
671
            ext_language = language
Robert Bradshaw's avatar
Robert Bradshaw committed
672
        elif isinstance(pattern, (Extension_distutils, Extension_setuptools)):
673 674 675 676
            for filepattern in pattern.sources:
                if os.path.splitext(filepattern)[1] in ('.py', '.pyx'):
                    break
            else:
677 678 679
                # ignore non-cython modules
                module_list.append(pattern)
                continue
Robert Bradshaw's avatar
Robert Bradshaw committed
680 681
            template = pattern
            name = template.name
682 683
            base = DistutilsInfo(exn=template)
            exn_type = template.__class__
684
            ext_language = None  # do not override whatever the Extension says
Robert Bradshaw's avatar
Robert Bradshaw committed
685
        else:
686 687 688 689 690
            msg = str("pattern is not of type str nor subclass of Extension (%s)"
                      " but of type %s and class %s" % (repr(Extension),
                                                        type(pattern),
                                                        pattern.__class__))
            raise TypeError(msg)
Stefan Behnel's avatar
Stefan Behnel committed
691

692
        for file in nonempty(sorted(extended_iglob(filepattern)), "'%s' doesn't match any files" % filepattern):
693
            if os.path.abspath(file) in to_exclude:
694
                continue
695
            pkg = deps.package(file)
696
            if '*' in name:
Stefan Behnel's avatar
Stefan Behnel committed
697
                module_name = deps.fully_qualified_name(file)
698 699
                if module_name in explicit_modules:
                    continue
700 701
            else:
                module_name = name
702

703
            if module_name not in seen:
704 705 706 707 708 709
                try:
                    kwds = deps.distutils_info(file, aliases, base).values
                except Exception:
                    if exclude_failures:
                        continue
                    raise
Robert Bradshaw's avatar
Robert Bradshaw committed
710 711 712 713
                if base is not None:
                    for key, value in base.values.items():
                        if key not in kwds:
                            kwds[key] = value
714

715 716
                sources = [file]
                if template is not None:
717
                    sources += [m for m in template.sources if m != filepattern]
718 719 720 721 722 723
                if 'sources' in kwds:
                    # allow users to add .c files etc.
                    for source in kwds['sources']:
                        source = encode_filename_in_py2(source)
                        if source not in sources:
                            sources.append(source)
724
                    extra_sources = kwds['sources']
725
                    del kwds['sources']
726 727
                else:
                    extra_sources = None
728
                if 'depends' in kwds:
729
                    depends = resolve_depends(kwds['depends'], (kwds.get('include_dirs') or []) + ["."])
730 731
                    if template is not None:
                        # Always include everything from the template.
732
                        depends = set(template.depends).union(depends)
733 734 735
                    # Sort depends to make the metadata dump in the
                    # Cython-generated C code predictable.
                    kwds['depends'] = sorted(depends)
736

737 738 739
                if ext_language and 'language' not in kwds:
                    kwds['language'] = ext_language

740 741
                module_list.append(exn_type(
                        name=module_name,
742
                        sources=sources,
743
                        **kwds))
744 745 746
                if extra_sources:
                    kwds['sources'] = extra_sources
                module_metadata[module_name] = {'distutils': kwds}
747
                m = module_list[-1]
Robert Bradshaw's avatar
Robert Bradshaw committed
748
                seen.add(name)
749
    return module_list, module_metadata
750

Stefan Behnel's avatar
Stefan Behnel committed
751

Robert Bradshaw's avatar
Robert Bradshaw committed
752
# This is the user-exposed entry point.
753
def cythonize(module_list, exclude=None, nthreads=0, aliases=None, quiet=False, force=False, language=None,
754 755 756 757 758 759 760 761 762 763 764 765
              exclude_failures=False, **options):
    """
    Compile a set of source modules into C/C++ files and return a list of distutils
    Extension objects for them.

    As module list, pass either a glob pattern, a list of glob patterns or a list of
    Extension objects.  The latter allows you to configure the extensions separately
    through the normal distutils options.

    When using glob patterns, you can exclude certain module names explicitly
    by passing them into the 'exclude' option.

766 767 768 769 770
    To globally enable C++ mode, you can pass language='c++'.  Otherwise, this
    will be determined at a per-file level based on compiler directives.  This
    affects only modules found based on file names.  Extension instances passed
    into cythonize() will not be changed.

771 772 773 774 775 776 777 778 779 780
    For parallel compilation, set the 'nthreads' option to the number of
    concurrent builds.

    For a broad 'try to compile' mode that ignores compilation failures and
    simply excludes the failed extensions, pass 'exclude_failures=True'. Note
    that this only really makes sense for compiling .py files which can also
    be used without compilation.

    Additional compilation options can be passed as keyword arguments.
    """
781 782
    if exclude is None:
        exclude = []
Robert Bradshaw's avatar
Robert Bradshaw committed
783 784
    if 'include_path' not in options:
        options['include_path'] = ['.']
785
    if 'common_utility_include_dir' in options:
786
        if options.get('cache'):
787
            raise NotImplementedError("common_utility_include_dir does not yet work with caching")
788
        safe_makedirs(options['common_utility_include_dir'])
789 790
    c_options = CompilationOptions(**options)
    cpp_options = CompilationOptions(**options); cpp_options.cplus = True
Robert Bradshaw's avatar
Robert Bradshaw committed
791
    ctx = c_options.create_context()
792
    options = c_options
793
    module_list, module_metadata = create_extension_list(
794 795 796
        module_list,
        exclude=exclude,
        ctx=ctx,
797
        quiet=quiet,
798
        exclude_failures=exclude_failures,
799
        language=language,
800
        aliases=aliases)
801
    deps = create_dependency_tree(ctx, quiet=quiet)
Stefan Behnel's avatar
Stefan Behnel committed
802
    build_dir = getattr(options, 'build_dir', None)
Stefan Behnel's avatar
Stefan Behnel committed
803

804
    modules_by_cfile = {}
805 806
    to_compile = []
    for m in module_list:
Stefan Behnel's avatar
Stefan Behnel committed
807
        if build_dir:
Robert Bradshaw's avatar
Robert Bradshaw committed
808
            root = os.getcwd()  # distutil extension depends are relative to cwd
Stefan Behnel's avatar
Stefan Behnel committed
809
            def copy_to_build_dir(filepath, root=root):
810
                filepath_abs = os.path.abspath(filepath)
811 812 813
                if os.path.isabs(filepath):
                    filepath = filepath_abs
                if filepath_abs.startswith(root):
814
                    mod_dir = join_path(build_dir,
815
                            os.path.dirname(_relpath(filepath, root)))
Robert Bradshaw's avatar
Robert Bradshaw committed
816
                    copy_once_if_newer(filepath_abs, mod_dir)
817 818 819
            for dep in m.depends:
                copy_to_build_dir(dep)

820 821 822
        new_sources = []
        for source in m.sources:
            base, ext = os.path.splitext(source)
823
            if ext in ('.pyx', '.py'):
824 825
                if m.language == 'c++':
                    c_file = base + '.cpp'
Robert Bradshaw's avatar
Robert Bradshaw committed
826
                    options = cpp_options
827 828
                else:
                    c_file = base + '.c'
Robert Bradshaw's avatar
Robert Bradshaw committed
829
                    options = c_options
830 831

                # setup for out of place build directory if enabled
Stefan Behnel's avatar
Stefan Behnel committed
832 833
                if build_dir:
                    c_file = os.path.join(build_dir, c_file)
834
                    dir = os.path.dirname(c_file)
835
                    safe_makedirs_once(dir)
836

837
                if os.path.exists(c_file):
838
                    c_timestamp = os.path.getmtime(c_file)
839 840
                else:
                    c_timestamp = -1
841

Robert Bradshaw's avatar
Robert Bradshaw committed
842 843
                # Priority goes first to modified files, second to direct
                # dependents, and finally to indirect dependents.
844
                if c_timestamp < deps.timestamp(source):
845
                    dep_timestamp, dep = deps.timestamp(source), source
846 847
                    priority = 0
                else:
848
                    dep_timestamp, dep = deps.newest_dependency(source)
849
                    priority = 2 - (dep in deps.immediate_dependencies(source))
850
                if force or c_timestamp < dep_timestamp:
851 852
                    if not quiet:
                        if source == dep:
Robert Bradshaw's avatar
Robert Bradshaw committed
853
                            print("Compiling %s because it changed." % source)
854
                        else:
Robert Bradshaw's avatar
Robert Bradshaw committed
855
                            print("Compiling %s because it depends on %s." % (source, dep))
856 857 858 859 860
                    if not force and hasattr(options, 'cache'):
                        extra = m.language
                        fingerprint = deps.transitive_fingerprint(source, extra)
                    else:
                        fingerprint = None
861
                    to_compile.append((priority, source, c_file, fingerprint, quiet,
862
                                       options, not exclude_failures, module_metadata.get(m.name)))
863
                new_sources.append(c_file)
864 865 866 867
                if c_file not in modules_by_cfile:
                    modules_by_cfile[c_file] = [m]
                else:
                    modules_by_cfile[c_file].append(m)
868 869
            else:
                new_sources.append(source)
Stefan Behnel's avatar
Stefan Behnel committed
870
                if build_dir:
871
                    copy_to_build_dir(source)
872
        m.sources = new_sources
Stefan Behnel's avatar
Stefan Behnel committed
873

874 875
    if hasattr(options, 'cache'):
        if not os.path.exists(options.cache):
876
            os.makedirs(options.cache)
877
    to_compile.sort()
878 879 880
    # Drop "priority" component of "to_compile" entries and add a
    # simple progress indicator.
    N = len(to_compile)
Jeroen Demeyer's avatar
Jeroen Demeyer committed
881
    progress_fmt = "[{0:%d}/{1}] " % len(str(N))
882 883 884 885 886
    for i in range(N):
        progress = progress_fmt.format(i+1, N)
        to_compile[i] = to_compile[i][1:] + (progress,)

    if N <= 1:
887
        nthreads = 0
Robert Bradshaw's avatar
Robert Bradshaw committed
888 889 890 891
    if nthreads:
        # Requires multiprocessing (or Python >= 2.6)
        try:
            import multiprocessing
892 893
            pool = multiprocessing.Pool(
                nthreads, initializer=_init_multiprocessing_helper)
894
        except (ImportError, OSError):
Stefan Behnel's avatar
Stefan Behnel committed
895
            print("multiprocessing required for parallel cythonization")
Robert Bradshaw's avatar
Robert Bradshaw committed
896
            nthreads = 0
897
        else:
898 899 900 901
            # This is a bit more involved than it should be, because KeyboardInterrupts
            # break the multiprocessing workers when using a normal pool.map().
            # See, for example:
            # http://noswap.com/blog/python-multiprocessing-keyboardinterrupt
902
            try:
903
                result = pool.map_async(cythonize_one_helper, to_compile, chunksize=1)
904
                pool.close()
905
                while not result.ready():
906 907 908 909
                    try:
                        result.get(99999)  # seconds
                    except multiprocessing.TimeoutError:
                        pass
910 911
            except KeyboardInterrupt:
                pool.terminate()
912
                raise
913
            pool.join()
Robert Bradshaw's avatar
Robert Bradshaw committed
914
    if not nthreads:
915
        for args in to_compile:
916
            cythonize_one(*args)
Stefan Behnel's avatar
Stefan Behnel committed
917

918 919
    if exclude_failures:
        failed_modules = set()
920
        for c_file, modules in modules_by_cfile.items():
921 922
            if not os.path.exists(c_file):
                failed_modules.update(modules)
923
            elif os.path.getsize(c_file) < 200:
Stefan Behnel's avatar
Stefan Behnel committed
924 925
                f = io_open(c_file, 'r', encoding='iso8859-1')
                try:
926 927 928
                    if f.read(len('#error ')) == '#error ':
                        # dead compilation result
                        failed_modules.update(modules)
Stefan Behnel's avatar
Stefan Behnel committed
929 930
                finally:
                    f.close()
931 932 933 934 935
        if failed_modules:
            for module in failed_modules:
                module_list.remove(module)
            print("Failed compilations: %s" % ', '.join(sorted([
                module.name for module in failed_modules])))
Stefan Behnel's avatar
Stefan Behnel committed
936

Robert Bradshaw's avatar
Robert Bradshaw committed
937 938
    if hasattr(options, 'cache'):
        cleanup_cache(options.cache, getattr(options, 'cache_size', 1024 * 1024 * 100))
939 940 941
    # cythonize() is often followed by the (non-Python-buffered)
    # compiler output, flush now to avoid interleaving output.
    sys.stdout.flush()
942
    return module_list
Robert Bradshaw's avatar
Robert Bradshaw committed
943

Robert Bradshaw's avatar
Robert Bradshaw committed
944

945
if os.environ.get('XML_RESULTS'):
Robert Bradshaw's avatar
Robert Bradshaw committed
946 947 948
    compile_result_dir = os.environ['XML_RESULTS']
    def record_results(func):
        def with_record(*args):
Stefan Behnel's avatar
Stefan Behnel committed
949 950
            t = time.time()
            success = True
Robert Bradshaw's avatar
Robert Bradshaw committed
951
            try:
Stefan Behnel's avatar
Stefan Behnel committed
952 953 954 955
                try:
                    func(*args)
                except:
                    success = False
Robert Bradshaw's avatar
Robert Bradshaw committed
956 957
            finally:
                t = time.time() - t
Stefan Behnel's avatar
Stefan Behnel committed
958
                module = fully_qualified_name(args[0])
Robert Bradshaw's avatar
Robert Bradshaw committed
959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976
                name = "cythonize." + module
                failures = 1 - success
                if success:
                    failure_item = ""
                else:
                    failure_item = "failure"
                output = open(os.path.join(compile_result_dir, name + ".xml"), "w")
                output.write("""
                    <?xml version="1.0" ?>
                    <testsuite name="%(name)s" errors="0" failures="%(failures)s" tests="1" time="%(t)s">
                    <testcase classname="%(name)s" name="cythonize">
                    %(failure_item)s
                    </testcase>
                    </testsuite>
                """.strip() % locals())
                output.close()
        return with_record
else:
Stefan Behnel's avatar
Stefan Behnel committed
977 978 979
    def record_results(func):
        return func

Robert Bradshaw's avatar
Robert Bradshaw committed
980

Robert Bradshaw's avatar
Robert Bradshaw committed
981
# TODO: Share context? Issue: pyx processing leaks into pxd module
Robert Bradshaw's avatar
Robert Bradshaw committed
982
@record_results
983
def cythonize_one(pyx_file, c_file, fingerprint, quiet, options=None, raise_on_failure=True, embedded_metadata=None, progress=""):
984 985
    from ..Compiler.Main import compile, default_options
    from ..Compiler.Errors import CompileError, PyrexError
Robert Bradshaw's avatar
Robert Bradshaw committed
986

987 988 989 990 991 992 993
    if fingerprint:
        if not os.path.exists(options.cache):
            try:
                os.mkdir(options.cache)
            except:
                if not os.path.exists(options.cache):
                    raise
Robert Bradshaw's avatar
Robert Bradshaw committed
994 995
        # Cython-generated c files are highly compressible.
        # (E.g. a compression ratio of about 10 for Sage).
996
        fingerprint_file = join_path(
997
            options.cache, "%s-%s%s" % (os.path.basename(c_file), fingerprint, gzip_ext))
998 999
        if os.path.exists(fingerprint_file):
            if not quiet:
1000
                print("%sFound compiled %s in cache" % (progress, pyx_file))
1001
            os.utime(fingerprint_file, None)
1002
            g = gzip_open(fingerprint_file, 'rb')
1003 1004 1005 1006 1007 1008 1009 1010
            try:
                f = open(c_file, 'wb')
                try:
                    shutil.copyfileobj(g, f)
                finally:
                    f.close()
            finally:
                g.close()
1011
            return
1012
    if not quiet:
1013
        print("%sCythonizing %s" % (progress, pyx_file))
1014 1015 1016
    if options is None:
        options = CompilationOptions(default_options)
    options.output_file = c_file
1017
    options.embedded_metadata = embedded_metadata
1018 1019 1020 1021 1022 1023

    any_failures = 0
    try:
        result = compile([pyx_file], options)
        if result.num_errors > 0:
            any_failures = 1
1024
    except (EnvironmentError, PyrexError) as e:
Stefan Behnel's avatar
Stefan Behnel committed
1025
        sys.stderr.write('%s\n' % e)
1026
        any_failures = 1
1027 1028 1029
        # XXX
        import traceback
        traceback.print_exc()
1030 1031 1032 1033 1034 1035
    except Exception:
        if raise_on_failure:
            raise
        import traceback
        traceback.print_exc()
        any_failures = 1
1036 1037 1038 1039 1040 1041
    if any_failures:
        if raise_on_failure:
            raise CompileError(None, pyx_file)
        elif os.path.exists(c_file):
            os.remove(c_file)
    elif fingerprint:
1042 1043
        f = open(c_file, 'rb')
        try:
1044
            g = gzip_open(fingerprint_file, 'wb')
1045 1046 1047 1048 1049 1050
            try:
                shutil.copyfileobj(f, g)
            finally:
                g.close()
        finally:
            f.close()
Robert Bradshaw's avatar
Robert Bradshaw committed
1051

1052

Stefan Behnel's avatar
typos  
Stefan Behnel committed
1053
def cythonize_one_helper(m):
1054 1055
    import traceback
    try:
1056
        return cythonize_one(*m)
1057 1058 1059
    except Exception:
        traceback.print_exc()
        raise
Robert Bradshaw's avatar
Robert Bradshaw committed
1060

1061 1062 1063 1064 1065 1066 1067

def _init_multiprocessing_helper():
    # KeyboardInterrupt kills workers, so don't let them get it
    import signal
    signal.signal(signal.SIGINT, signal.SIG_IGN)


Robert Bradshaw's avatar
Robert Bradshaw committed
1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080
def cleanup_cache(cache, target_size, ratio=.85):
    try:
        p = subprocess.Popen(['du', '-s', '-k', os.path.abspath(cache)], stdout=subprocess.PIPE)
        res = p.wait()
        if res == 0:
            total_size = 1024 * int(p.stdout.read().strip().split()[0])
            if total_size < target_size:
                return
    except (OSError, ValueError):
        pass
    total_size = 0
    all = []
    for file in os.listdir(cache):
1081
        path = join_path(cache, file)
Robert Bradshaw's avatar
Robert Bradshaw committed
1082 1083 1084 1085 1086 1087 1088 1089 1090
        s = os.stat(path)
        total_size += s.st_size
        all.append((s.st_atime, s.st_size, path))
    if total_size > target_size:
        for time, size, file in reversed(sorted(all)):
            os.unlink(file)
            total_size -= size
            if total_size < target_size * ratio:
                break