Dependencies.py 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283
  1. from __future__ import absolute_import, print_function
  2. import cython
  3. from .. import __version__
  4. import collections
  5. import contextlib
  6. import hashlib
  7. import os
  8. import shutil
  9. import subprocess
  10. import re, sys, time
  11. import warnings
  12. from glob import iglob
  13. from io import open as io_open
  14. from os.path import relpath as _relpath
  15. from distutils.extension import Extension
  16. from distutils.util import strtobool
  17. import zipfile
  18. try:
  19. from collections.abc import Iterable
  20. except ImportError:
  21. from collections import Iterable
  22. try:
  23. import gzip
  24. gzip_open = gzip.open
  25. gzip_ext = '.gz'
  26. except ImportError:
  27. gzip_open = open
  28. gzip_ext = ''
  29. try:
  30. import zlib
  31. zipfile_compression_mode = zipfile.ZIP_DEFLATED
  32. except ImportError:
  33. zipfile_compression_mode = zipfile.ZIP_STORED
  34. try:
  35. import pythran
  36. except:
  37. pythran = None
  38. from .. import Utils
  39. from ..Utils import (cached_function, cached_method, path_exists,
  40. safe_makedirs, copy_file_to_dir_if_newer, is_package_dir, replace_suffix)
  41. from ..Compiler.Main import Context, CompilationOptions, default_options
  42. join_path = cached_function(os.path.join)
  43. copy_once_if_newer = cached_function(copy_file_to_dir_if_newer)
  44. safe_makedirs_once = cached_function(safe_makedirs)
  45. if sys.version_info[0] < 3:
  46. # stupid Py2 distutils enforces str type in list of sources
  47. _fs_encoding = sys.getfilesystemencoding()
  48. if _fs_encoding is None:
  49. _fs_encoding = sys.getdefaultencoding()
  50. def encode_filename_in_py2(filename):
  51. if not isinstance(filename, bytes):
  52. return filename.encode(_fs_encoding)
  53. return filename
  54. else:
  55. def encode_filename_in_py2(filename):
  56. return filename
  57. basestring = str
  58. def _make_relative(file_paths, base=None):
  59. if not base:
  60. base = os.getcwd()
  61. if base[-1] != os.path.sep:
  62. base += os.path.sep
  63. return [_relpath(path, base) if path.startswith(base) else path
  64. for path in file_paths]
  65. def extended_iglob(pattern):
  66. if '{' in pattern:
  67. m = re.match('(.*){([^}]+)}(.*)', pattern)
  68. if m:
  69. before, switch, after = m.groups()
  70. for case in switch.split(','):
  71. for path in extended_iglob(before + case + after):
  72. yield path
  73. return
  74. if '**/' in pattern:
  75. seen = set()
  76. first, rest = pattern.split('**/', 1)
  77. if first:
  78. first = iglob(first+'/')
  79. else:
  80. first = ['']
  81. for root in first:
  82. for path in extended_iglob(join_path(root, rest)):
  83. if path not in seen:
  84. seen.add(path)
  85. yield path
  86. for path in extended_iglob(join_path(root, '*', '**/' + rest)):
  87. if path not in seen:
  88. seen.add(path)
  89. yield path
  90. else:
  91. for path in iglob(pattern):
  92. yield path
  93. def nonempty(it, error_msg="expected non-empty iterator"):
  94. empty = True
  95. for value in it:
  96. empty = False
  97. yield value
  98. if empty:
  99. raise ValueError(error_msg)
  100. @cached_function
  101. def file_hash(filename):
  102. path = os.path.normpath(filename)
  103. prefix = ('%d:%s' % (len(path), path)).encode("UTF-8")
  104. m = hashlib.md5(prefix)
  105. with open(path, 'rb') as f:
  106. data = f.read(65000)
  107. while data:
  108. m.update(data)
  109. data = f.read(65000)
  110. return m.hexdigest()
  111. def update_pythran_extension(ext):
  112. if pythran is None:
  113. raise RuntimeError("You first need to install Pythran to use the np_pythran directive.")
  114. try:
  115. pythran_ext = pythran.config.make_extension(python=True)
  116. except TypeError: # older pythran version only
  117. pythran_ext = pythran.config.make_extension()
  118. ext.include_dirs.extend(pythran_ext['include_dirs'])
  119. ext.extra_compile_args.extend(pythran_ext['extra_compile_args'])
  120. ext.extra_link_args.extend(pythran_ext['extra_link_args'])
  121. ext.define_macros.extend(pythran_ext['define_macros'])
  122. ext.undef_macros.extend(pythran_ext['undef_macros'])
  123. ext.library_dirs.extend(pythran_ext['library_dirs'])
  124. ext.libraries.extend(pythran_ext['libraries'])
  125. ext.language = 'c++'
  126. # These options are not compatible with the way normal Cython extensions work
  127. for bad_option in ["-fwhole-program", "-fvisibility=hidden"]:
  128. try:
  129. ext.extra_compile_args.remove(bad_option)
  130. except ValueError:
  131. pass
  132. def parse_list(s):
  133. """
  134. >>> parse_list("")
  135. []
  136. >>> parse_list("a")
  137. ['a']
  138. >>> parse_list("a b c")
  139. ['a', 'b', 'c']
  140. >>> parse_list("[a, b, c]")
  141. ['a', 'b', 'c']
  142. >>> parse_list('a " " b')
  143. ['a', ' ', 'b']
  144. >>> parse_list('[a, ",a", "a,", ",", ]')
  145. ['a', ',a', 'a,', ',']
  146. """
  147. if len(s) >= 2 and s[0] == '[' and s[-1] == ']':
  148. s = s[1:-1]
  149. delimiter = ','
  150. else:
  151. delimiter = ' '
  152. s, literals = strip_string_literals(s)
  153. def unquote(literal):
  154. literal = literal.strip()
  155. if literal[0] in "'\"":
  156. return literals[literal[1:-1]]
  157. else:
  158. return literal
  159. return [unquote(item) for item in s.split(delimiter) if item.strip()]
  160. transitive_str = object()
  161. transitive_list = object()
  162. bool_or = object()
  163. distutils_settings = {
  164. 'name': str,
  165. 'sources': list,
  166. 'define_macros': list,
  167. 'undef_macros': list,
  168. 'libraries': transitive_list,
  169. 'library_dirs': transitive_list,
  170. 'runtime_library_dirs': transitive_list,
  171. 'include_dirs': transitive_list,
  172. 'extra_objects': list,
  173. 'extra_compile_args': transitive_list,
  174. 'extra_link_args': transitive_list,
  175. 'export_symbols': list,
  176. 'depends': transitive_list,
  177. 'language': transitive_str,
  178. 'np_pythran': bool_or
  179. }
  180. @cython.locals(start=cython.Py_ssize_t, end=cython.Py_ssize_t)
  181. def line_iter(source):
  182. if isinstance(source, basestring):
  183. start = 0
  184. while True:
  185. end = source.find('\n', start)
  186. if end == -1:
  187. yield source[start:]
  188. return
  189. yield source[start:end]
  190. start = end+1
  191. else:
  192. for line in source:
  193. yield line
  194. class DistutilsInfo(object):
  195. def __init__(self, source=None, exn=None):
  196. self.values = {}
  197. if source is not None:
  198. for line in line_iter(source):
  199. line = line.lstrip()
  200. if not line:
  201. continue
  202. if line[0] != '#':
  203. break
  204. line = line[1:].lstrip()
  205. kind = next((k for k in ("distutils:","cython:") if line.startswith(k)), None)
  206. if kind is not None:
  207. key, _, value = [s.strip() for s in line[len(kind):].partition('=')]
  208. type = distutils_settings.get(key, None)
  209. if line.startswith("cython:") and type is None: continue
  210. if type in (list, transitive_list):
  211. value = parse_list(value)
  212. if key == 'define_macros':
  213. value = [tuple(macro.split('=', 1))
  214. if '=' in macro else (macro, None)
  215. for macro in value]
  216. if type is bool_or:
  217. value = strtobool(value)
  218. self.values[key] = value
  219. elif exn is not None:
  220. for key in distutils_settings:
  221. if key in ('name', 'sources','np_pythran'):
  222. continue
  223. value = getattr(exn, key, None)
  224. if value:
  225. self.values[key] = value
  226. def merge(self, other):
  227. if other is None:
  228. return self
  229. for key, value in other.values.items():
  230. type = distutils_settings[key]
  231. if type is transitive_str and key not in self.values:
  232. self.values[key] = value
  233. elif type is transitive_list:
  234. if key in self.values:
  235. # Change a *copy* of the list (Trac #845)
  236. all = self.values[key][:]
  237. for v in value:
  238. if v not in all:
  239. all.append(v)
  240. value = all
  241. self.values[key] = value
  242. elif type is bool_or:
  243. self.values[key] = self.values.get(key, False) | value
  244. return self
  245. def subs(self, aliases):
  246. if aliases is None:
  247. return self
  248. resolved = DistutilsInfo()
  249. for key, value in self.values.items():
  250. type = distutils_settings[key]
  251. if type in [list, transitive_list]:
  252. new_value_list = []
  253. for v in value:
  254. if v in aliases:
  255. v = aliases[v]
  256. if isinstance(v, list):
  257. new_value_list += v
  258. else:
  259. new_value_list.append(v)
  260. value = new_value_list
  261. else:
  262. if value in aliases:
  263. value = aliases[value]
  264. resolved.values[key] = value
  265. return resolved
  266. def apply(self, extension):
  267. for key, value in self.values.items():
  268. type = distutils_settings[key]
  269. if type in [list, transitive_list]:
  270. value = getattr(extension, key) + list(value)
  271. setattr(extension, key, value)
  272. @cython.locals(start=cython.Py_ssize_t, q=cython.Py_ssize_t,
  273. single_q=cython.Py_ssize_t, double_q=cython.Py_ssize_t,
  274. hash_mark=cython.Py_ssize_t, end=cython.Py_ssize_t,
  275. k=cython.Py_ssize_t, counter=cython.Py_ssize_t, quote_len=cython.Py_ssize_t)
  276. def strip_string_literals(code, prefix='__Pyx_L'):
  277. """
  278. Normalizes every string literal to be of the form '__Pyx_Lxxx',
  279. returning the normalized code and a mapping of labels to
  280. string literals.
  281. """
  282. new_code = []
  283. literals = {}
  284. counter = 0
  285. start = q = 0
  286. in_quote = False
  287. hash_mark = single_q = double_q = -1
  288. code_len = len(code)
  289. quote_type = quote_len = None
  290. while True:
  291. if hash_mark < q:
  292. hash_mark = code.find('#', q)
  293. if single_q < q:
  294. single_q = code.find("'", q)
  295. if double_q < q:
  296. double_q = code.find('"', q)
  297. q = min(single_q, double_q)
  298. if q == -1:
  299. q = max(single_q, double_q)
  300. # We're done.
  301. if q == -1 and hash_mark == -1:
  302. new_code.append(code[start:])
  303. break
  304. # Try to close the quote.
  305. elif in_quote:
  306. if code[q-1] == u'\\':
  307. k = 2
  308. while q >= k and code[q-k] == u'\\':
  309. k += 1
  310. if k % 2 == 0:
  311. q += 1
  312. continue
  313. if code[q] == quote_type and (
  314. quote_len == 1 or (code_len > q + 2 and quote_type == code[q+1] == code[q+2])):
  315. counter += 1
  316. label = "%s%s_" % (prefix, counter)
  317. literals[label] = code[start+quote_len:q]
  318. full_quote = code[q:q+quote_len]
  319. new_code.append(full_quote)
  320. new_code.append(label)
  321. new_code.append(full_quote)
  322. q += quote_len
  323. in_quote = False
  324. start = q
  325. else:
  326. q += 1
  327. # Process comment.
  328. elif -1 != hash_mark and (hash_mark < q or q == -1):
  329. new_code.append(code[start:hash_mark+1])
  330. end = code.find('\n', hash_mark)
  331. counter += 1
  332. label = "%s%s_" % (prefix, counter)
  333. if end == -1:
  334. end_or_none = None
  335. else:
  336. end_or_none = end
  337. literals[label] = code[hash_mark+1:end_or_none]
  338. new_code.append(label)
  339. if end == -1:
  340. break
  341. start = q = end
  342. # Open the quote.
  343. else:
  344. if code_len >= q+3 and (code[q] == code[q+1] == code[q+2]):
  345. quote_len = 3
  346. else:
  347. quote_len = 1
  348. in_quote = True
  349. quote_type = code[q]
  350. new_code.append(code[start:q])
  351. start = q
  352. q += quote_len
  353. return "".join(new_code), literals
  354. # We need to allow spaces to allow for conditional compilation like
  355. # IF ...:
  356. # cimport ...
  357. dependency_regex = re.compile(r"(?:^\s*from +([0-9a-zA-Z_.]+) +cimport)|"
  358. r"(?:^\s*cimport +([0-9a-zA-Z_.]+(?: *, *[0-9a-zA-Z_.]+)*))|"
  359. r"(?:^\s*cdef +extern +from +['\"]([^'\"]+)['\"])|"
  360. r"(?:^\s*include +['\"]([^'\"]+)['\"])", re.M)
  361. dependency_after_from_regex = re.compile(
  362. r"(?:^\s+\(([0-9a-zA-Z_., ]*)\)[#\n])|"
  363. r"(?:^\s+([0-9a-zA-Z_., ]*)[#\n])",
  364. re.M)
  365. def normalize_existing(base_path, rel_paths):
  366. return normalize_existing0(os.path.dirname(base_path), tuple(set(rel_paths)))
  367. @cached_function
  368. def normalize_existing0(base_dir, rel_paths):
  369. """
  370. Given some base directory ``base_dir`` and a list of path names
  371. ``rel_paths``, normalize each relative path name ``rel`` by
  372. replacing it by ``os.path.join(base, rel)`` if that file exists.
  373. Return a couple ``(normalized, needed_base)`` where ``normalized``
  374. if the list of normalized file names and ``needed_base`` is
  375. ``base_dir`` if we actually needed ``base_dir``. If no paths were
  376. changed (for example, if all paths were already absolute), then
  377. ``needed_base`` is ``None``.
  378. """
  379. normalized = []
  380. needed_base = None
  381. for rel in rel_paths:
  382. if os.path.isabs(rel):
  383. normalized.append(rel)
  384. continue
  385. path = join_path(base_dir, rel)
  386. if path_exists(path):
  387. normalized.append(os.path.normpath(path))
  388. needed_base = base_dir
  389. else:
  390. normalized.append(rel)
  391. return (normalized, needed_base)
  392. def resolve_depends(depends, include_dirs):
  393. include_dirs = tuple(include_dirs)
  394. resolved = []
  395. for depend in depends:
  396. path = resolve_depend(depend, include_dirs)
  397. if path is not None:
  398. resolved.append(path)
  399. return resolved
  400. @cached_function
  401. def resolve_depend(depend, include_dirs):
  402. if depend[0] == '<' and depend[-1] == '>':
  403. return None
  404. for dir in include_dirs:
  405. path = join_path(dir, depend)
  406. if path_exists(path):
  407. return os.path.normpath(path)
  408. return None
  409. @cached_function
  410. def package(filename):
  411. dir = os.path.dirname(os.path.abspath(str(filename)))
  412. if dir != filename and is_package_dir(dir):
  413. return package(dir) + (os.path.basename(dir),)
  414. else:
  415. return ()
  416. @cached_function
  417. def fully_qualified_name(filename):
  418. module = os.path.splitext(os.path.basename(filename))[0]
  419. return '.'.join(package(filename) + (module,))
  420. @cached_function
  421. def parse_dependencies(source_filename):
  422. # Actual parsing is way too slow, so we use regular expressions.
  423. # The only catch is that we must strip comments and string
  424. # literals ahead of time.
  425. with Utils.open_source_file(source_filename, error_handling='ignore') as fh:
  426. source = fh.read()
  427. distutils_info = DistutilsInfo(source)
  428. source, literals = strip_string_literals(source)
  429. source = source.replace('\\\n', ' ').replace('\t', ' ')
  430. # TODO: pure mode
  431. cimports = []
  432. includes = []
  433. externs = []
  434. for m in dependency_regex.finditer(source):
  435. cimport_from, cimport_list, extern, include = m.groups()
  436. if cimport_from:
  437. cimports.append(cimport_from)
  438. m_after_from = dependency_after_from_regex.search(source, pos=m.end())
  439. if m_after_from:
  440. multiline, one_line = m_after_from.groups()
  441. subimports = multiline or one_line
  442. cimports.extend("{0}.{1}".format(cimport_from, s.strip())
  443. for s in subimports.split(','))
  444. elif cimport_list:
  445. cimports.extend(x.strip() for x in cimport_list.split(","))
  446. elif extern:
  447. externs.append(literals[extern])
  448. else:
  449. includes.append(literals[include])
  450. return cimports, includes, externs, distutils_info
  451. class DependencyTree(object):
  452. def __init__(self, context, quiet=False):
  453. self.context = context
  454. self.quiet = quiet
  455. self._transitive_cache = {}
  456. def parse_dependencies(self, source_filename):
  457. if path_exists(source_filename):
  458. source_filename = os.path.normpath(source_filename)
  459. return parse_dependencies(source_filename)
  460. @cached_method
  461. def included_files(self, filename):
  462. # This is messy because included files are textually included, resolving
  463. # cimports (but not includes) relative to the including file.
  464. all = set()
  465. for include in self.parse_dependencies(filename)[1]:
  466. include_path = join_path(os.path.dirname(filename), include)
  467. if not path_exists(include_path):
  468. include_path = self.context.find_include_file(include, None)
  469. if include_path:
  470. if '.' + os.path.sep in include_path:
  471. include_path = os.path.normpath(include_path)
  472. all.add(include_path)
  473. all.update(self.included_files(include_path))
  474. elif not self.quiet:
  475. print("Unable to locate '%s' referenced from '%s'" % (filename, include))
  476. return all
  477. @cached_method
  478. def cimports_externs_incdirs(self, filename):
  479. # This is really ugly. Nested cimports are resolved with respect to the
  480. # includer, but includes are resolved with respect to the includee.
  481. cimports, includes, externs = self.parse_dependencies(filename)[:3]
  482. cimports = set(cimports)
  483. externs = set(externs)
  484. incdirs = set()
  485. for include in self.included_files(filename):
  486. included_cimports, included_externs, included_incdirs = self.cimports_externs_incdirs(include)
  487. cimports.update(included_cimports)
  488. externs.update(included_externs)
  489. incdirs.update(included_incdirs)
  490. externs, incdir = normalize_existing(filename, externs)
  491. if incdir:
  492. incdirs.add(incdir)
  493. return tuple(cimports), externs, incdirs
  494. def cimports(self, filename):
  495. return self.cimports_externs_incdirs(filename)[0]
  496. def package(self, filename):
  497. return package(filename)
  498. def fully_qualified_name(self, filename):
  499. return fully_qualified_name(filename)
  500. @cached_method
  501. def find_pxd(self, module, filename=None):
  502. is_relative = module[0] == '.'
  503. if is_relative and not filename:
  504. raise NotImplementedError("New relative imports.")
  505. if filename is not None:
  506. module_path = module.split('.')
  507. if is_relative:
  508. module_path.pop(0) # just explicitly relative
  509. package_path = list(self.package(filename))
  510. while module_path and not module_path[0]:
  511. try:
  512. package_path.pop()
  513. except IndexError:
  514. return None # FIXME: error?
  515. module_path.pop(0)
  516. relative = '.'.join(package_path + module_path)
  517. pxd = self.context.find_pxd_file(relative, None)
  518. if pxd:
  519. return pxd
  520. if is_relative:
  521. return None # FIXME: error?
  522. return self.context.find_pxd_file(module, None)
  523. @cached_method
  524. def cimported_files(self, filename):
  525. if filename[-4:] == '.pyx' and path_exists(filename[:-4] + '.pxd'):
  526. pxd_list = [filename[:-4] + '.pxd']
  527. else:
  528. pxd_list = []
  529. # Cimports generates all possible combinations package.module
  530. # when imported as from package cimport module.
  531. for module in self.cimports(filename):
  532. if module[:7] == 'cython.' or module == 'cython':
  533. continue
  534. pxd_file = self.find_pxd(module, filename)
  535. if pxd_file is not None:
  536. pxd_list.append(pxd_file)
  537. return tuple(pxd_list)
  538. @cached_method
  539. def immediate_dependencies(self, filename):
  540. all = set([filename])
  541. all.update(self.cimported_files(filename))
  542. all.update(self.included_files(filename))
  543. return all
  544. def all_dependencies(self, filename):
  545. return self.transitive_merge(filename, self.immediate_dependencies, set.union)
  546. @cached_method
  547. def timestamp(self, filename):
  548. return os.path.getmtime(filename)
  549. def extract_timestamp(self, filename):
  550. return self.timestamp(filename), filename
  551. def newest_dependency(self, filename):
  552. return max([self.extract_timestamp(f) for f in self.all_dependencies(filename)])
  553. def transitive_fingerprint(self, filename, module, compilation_options):
  554. r"""
  555. Return a fingerprint of a cython file that is about to be cythonized.
  556. Fingerprints are looked up in future compilations. If the fingerprint
  557. is found, the cythonization can be skipped. The fingerprint must
  558. incorporate everything that has an influence on the generated code.
  559. """
  560. try:
  561. m = hashlib.md5(__version__.encode('UTF-8'))
  562. m.update(file_hash(filename).encode('UTF-8'))
  563. for x in sorted(self.all_dependencies(filename)):
  564. if os.path.splitext(x)[1] not in ('.c', '.cpp', '.h'):
  565. m.update(file_hash(x).encode('UTF-8'))
  566. # Include the module attributes that change the compilation result
  567. # in the fingerprint. We do not iterate over module.__dict__ and
  568. # include almost everything here as users might extend Extension
  569. # with arbitrary (random) attributes that would lead to cache
  570. # misses.
  571. m.update(str((
  572. module.language,
  573. getattr(module, 'py_limited_api', False),
  574. getattr(module, 'np_pythran', False)
  575. )).encode('UTF-8'))
  576. m.update(compilation_options.get_fingerprint().encode('UTF-8'))
  577. return m.hexdigest()
  578. except IOError:
  579. return None
  580. def distutils_info0(self, filename):
  581. info = self.parse_dependencies(filename)[3]
  582. kwds = info.values
  583. cimports, externs, incdirs = self.cimports_externs_incdirs(filename)
  584. basedir = os.getcwd()
  585. # Add dependencies on "cdef extern from ..." files
  586. if externs:
  587. externs = _make_relative(externs, basedir)
  588. if 'depends' in kwds:
  589. kwds['depends'] = list(set(kwds['depends']).union(externs))
  590. else:
  591. kwds['depends'] = list(externs)
  592. # Add include_dirs to ensure that the C compiler will find the
  593. # "cdef extern from ..." files
  594. if incdirs:
  595. include_dirs = list(kwds.get('include_dirs', []))
  596. for inc in _make_relative(incdirs, basedir):
  597. if inc not in include_dirs:
  598. include_dirs.append(inc)
  599. kwds['include_dirs'] = include_dirs
  600. return info
  601. def distutils_info(self, filename, aliases=None, base=None):
  602. return (self.transitive_merge(filename, self.distutils_info0, DistutilsInfo.merge)
  603. .subs(aliases)
  604. .merge(base))
  605. def transitive_merge(self, node, extract, merge):
  606. try:
  607. seen = self._transitive_cache[extract, merge]
  608. except KeyError:
  609. seen = self._transitive_cache[extract, merge] = {}
  610. return self.transitive_merge_helper(
  611. node, extract, merge, seen, {}, self.cimported_files)[0]
  612. def transitive_merge_helper(self, node, extract, merge, seen, stack, outgoing):
  613. if node in seen:
  614. return seen[node], None
  615. deps = extract(node)
  616. if node in stack:
  617. return deps, node
  618. try:
  619. stack[node] = len(stack)
  620. loop = None
  621. for next in outgoing(node):
  622. sub_deps, sub_loop = self.transitive_merge_helper(next, extract, merge, seen, stack, outgoing)
  623. if sub_loop is not None:
  624. if loop is not None and stack[loop] < stack[sub_loop]:
  625. pass
  626. else:
  627. loop = sub_loop
  628. deps = merge(deps, sub_deps)
  629. if loop == node:
  630. loop = None
  631. if loop is None:
  632. seen[node] = deps
  633. return deps, loop
  634. finally:
  635. del stack[node]
  636. _dep_tree = None
  637. def create_dependency_tree(ctx=None, quiet=False):
  638. global _dep_tree
  639. if _dep_tree is None:
  640. if ctx is None:
  641. ctx = Context(["."], CompilationOptions(default_options))
  642. _dep_tree = DependencyTree(ctx, quiet=quiet)
  643. return _dep_tree
  644. # If this changes, change also docs/src/reference/compilation.rst
  645. # which mentions this function
  646. def default_create_extension(template, kwds):
  647. if 'depends' in kwds:
  648. include_dirs = kwds.get('include_dirs', []) + ["."]
  649. depends = resolve_depends(kwds['depends'], include_dirs)
  650. kwds['depends'] = sorted(set(depends + template.depends))
  651. t = template.__class__
  652. ext = t(**kwds)
  653. metadata = dict(distutils=kwds, module_name=kwds['name'])
  654. return (ext, metadata)
  655. # This may be useful for advanced users?
  656. def create_extension_list(patterns, exclude=None, ctx=None, aliases=None, quiet=False, language=None,
  657. exclude_failures=False):
  658. if language is not None:
  659. print('Warning: passing language={0!r} to cythonize() is deprecated. '
  660. 'Instead, put "# distutils: language={0}" in your .pyx or .pxd file(s)'.format(language))
  661. if exclude is None:
  662. exclude = []
  663. if patterns is None:
  664. return [], {}
  665. elif isinstance(patterns, basestring) or not isinstance(patterns, Iterable):
  666. patterns = [patterns]
  667. explicit_modules = set([m.name for m in patterns if isinstance(m, Extension)])
  668. seen = set()
  669. deps = create_dependency_tree(ctx, quiet=quiet)
  670. to_exclude = set()
  671. if not isinstance(exclude, list):
  672. exclude = [exclude]
  673. for pattern in exclude:
  674. to_exclude.update(map(os.path.abspath, extended_iglob(pattern)))
  675. module_list = []
  676. module_metadata = {}
  677. # workaround for setuptools
  678. if 'setuptools' in sys.modules:
  679. Extension_distutils = sys.modules['setuptools.extension']._Extension
  680. Extension_setuptools = sys.modules['setuptools'].Extension
  681. else:
  682. # dummy class, in case we do not have setuptools
  683. Extension_distutils = Extension
  684. class Extension_setuptools(Extension): pass
  685. # if no create_extension() function is defined, use a simple
  686. # default function.
  687. create_extension = ctx.options.create_extension or default_create_extension
  688. for pattern in patterns:
  689. if isinstance(pattern, str):
  690. filepattern = pattern
  691. template = Extension(pattern, []) # Fake Extension without sources
  692. name = '*'
  693. base = None
  694. ext_language = language
  695. elif isinstance(pattern, (Extension_distutils, Extension_setuptools)):
  696. cython_sources = [s for s in pattern.sources
  697. if os.path.splitext(s)[1] in ('.py', '.pyx')]
  698. if cython_sources:
  699. filepattern = cython_sources[0]
  700. if len(cython_sources) > 1:
  701. print("Warning: Multiple cython sources found for extension '%s': %s\n"
  702. "See http://cython.readthedocs.io/en/latest/src/userguide/sharing_declarations.html "
  703. "for sharing declarations among Cython files." % (pattern.name, cython_sources))
  704. else:
  705. # ignore non-cython modules
  706. module_list.append(pattern)
  707. continue
  708. template = pattern
  709. name = template.name
  710. base = DistutilsInfo(exn=template)
  711. ext_language = None # do not override whatever the Extension says
  712. else:
  713. msg = str("pattern is not of type str nor subclass of Extension (%s)"
  714. " but of type %s and class %s" % (repr(Extension),
  715. type(pattern),
  716. pattern.__class__))
  717. raise TypeError(msg)
  718. for file in nonempty(sorted(extended_iglob(filepattern)), "'%s' doesn't match any files" % filepattern):
  719. if os.path.abspath(file) in to_exclude:
  720. continue
  721. module_name = deps.fully_qualified_name(file)
  722. if '*' in name:
  723. if module_name in explicit_modules:
  724. continue
  725. elif name:
  726. module_name = name
  727. Utils.raise_error_if_module_name_forbidden(module_name)
  728. if module_name not in seen:
  729. try:
  730. kwds = deps.distutils_info(file, aliases, base).values
  731. except Exception:
  732. if exclude_failures:
  733. continue
  734. raise
  735. if base is not None:
  736. for key, value in base.values.items():
  737. if key not in kwds:
  738. kwds[key] = value
  739. kwds['name'] = module_name
  740. sources = [file] + [m for m in template.sources if m != filepattern]
  741. if 'sources' in kwds:
  742. # allow users to add .c files etc.
  743. for source in kwds['sources']:
  744. source = encode_filename_in_py2(source)
  745. if source not in sources:
  746. sources.append(source)
  747. kwds['sources'] = sources
  748. if ext_language and 'language' not in kwds:
  749. kwds['language'] = ext_language
  750. np_pythran = kwds.pop('np_pythran', False)
  751. # Create the new extension
  752. m, metadata = create_extension(template, kwds)
  753. m.np_pythran = np_pythran or getattr(m, 'np_pythran', False)
  754. if m.np_pythran:
  755. update_pythran_extension(m)
  756. module_list.append(m)
  757. # Store metadata (this will be written as JSON in the
  758. # generated C file but otherwise has no purpose)
  759. module_metadata[module_name] = metadata
  760. if file not in m.sources:
  761. # Old setuptools unconditionally replaces .pyx with .c/.cpp
  762. target_file = os.path.splitext(file)[0] + ('.cpp' if m.language == 'c++' else '.c')
  763. try:
  764. m.sources.remove(target_file)
  765. except ValueError:
  766. # never seen this in the wild, but probably better to warn about this unexpected case
  767. print("Warning: Cython source file not found in sources list, adding %s" % file)
  768. m.sources.insert(0, file)
  769. seen.add(name)
  770. return module_list, module_metadata
  771. # This is the user-exposed entry point.
  772. def cythonize(module_list, exclude=None, nthreads=0, aliases=None, quiet=False, force=False, language=None,
  773. exclude_failures=False, **options):
  774. """
  775. Compile a set of source modules into C/C++ files and return a list of distutils
  776. Extension objects for them.
  777. :param module_list: As module list, pass either a glob pattern, a list of glob
  778. patterns or a list of Extension objects. The latter
  779. allows you to configure the extensions separately
  780. through the normal distutils options.
  781. You can also pass Extension objects that have
  782. glob patterns as their sources. Then, cythonize
  783. will resolve the pattern and create a
  784. copy of the Extension for every matching file.
  785. :param exclude: When passing glob patterns as ``module_list``, you can exclude certain
  786. module names explicitly by passing them into the ``exclude`` option.
  787. :param nthreads: The number of concurrent builds for parallel compilation
  788. (requires the ``multiprocessing`` module).
  789. :param aliases: If you want to use compiler directives like ``# distutils: ...`` but
  790. can only know at compile time (when running the ``setup.py``) which values
  791. to use, you can use aliases and pass a dictionary mapping those aliases
  792. to Python strings when calling :func:`cythonize`. As an example, say you
  793. want to use the compiler
  794. directive ``# distutils: include_dirs = ../static_libs/include/``
  795. but this path isn't always fixed and you want to find it when running
  796. the ``setup.py``. You can then do ``# distutils: include_dirs = MY_HEADERS``,
  797. find the value of ``MY_HEADERS`` in the ``setup.py``, put it in a python
  798. variable called ``foo`` as a string, and then call
  799. ``cythonize(..., aliases={'MY_HEADERS': foo})``.
  800. :param quiet: If True, Cython won't print error, warning, or status messages during the
  801. compilation.
  802. :param force: Forces the recompilation of the Cython modules, even if the timestamps
  803. don't indicate that a recompilation is necessary.
  804. :param language: To globally enable C++ mode, you can pass ``language='c++'``. Otherwise, this
  805. will be determined at a per-file level based on compiler directives. This
  806. affects only modules found based on file names. Extension instances passed
  807. into :func:`cythonize` will not be changed. It is recommended to rather
  808. use the compiler directive ``# distutils: language = c++`` than this option.
  809. :param exclude_failures: For a broad 'try to compile' mode that ignores compilation
  810. failures and simply excludes the failed extensions,
  811. pass ``exclude_failures=True``. Note that this only
  812. really makes sense for compiling ``.py`` files which can also
  813. be used without compilation.
  814. :param annotate: If ``True``, will produce a HTML file for each of the ``.pyx`` or ``.py``
  815. files compiled. The HTML file gives an indication
  816. of how much Python interaction there is in
  817. each of the source code lines, compared to plain C code.
  818. It also allows you to see the C/C++ code
  819. generated for each line of Cython code. This report is invaluable when
  820. optimizing a function for speed,
  821. and for determining when to :ref:`release the GIL <nogil>`:
  822. in general, a ``nogil`` block may contain only "white" code.
  823. See examples in :ref:`determining_where_to_add_types` or
  824. :ref:`primes`.
  825. :param compiler_directives: Allow to set compiler directives in the ``setup.py`` like this:
  826. ``compiler_directives={'embedsignature': True}``.
  827. See :ref:`compiler-directives`.
  828. """
  829. if exclude is None:
  830. exclude = []
  831. if 'include_path' not in options:
  832. options['include_path'] = ['.']
  833. if 'common_utility_include_dir' in options:
  834. safe_makedirs(options['common_utility_include_dir'])
  835. if pythran is None:
  836. pythran_options = None
  837. else:
  838. pythran_options = CompilationOptions(**options)
  839. pythran_options.cplus = True
  840. pythran_options.np_pythran = True
  841. c_options = CompilationOptions(**options)
  842. cpp_options = CompilationOptions(**options); cpp_options.cplus = True
  843. ctx = c_options.create_context()
  844. options = c_options
  845. module_list, module_metadata = create_extension_list(
  846. module_list,
  847. exclude=exclude,
  848. ctx=ctx,
  849. quiet=quiet,
  850. exclude_failures=exclude_failures,
  851. language=language,
  852. aliases=aliases)
  853. deps = create_dependency_tree(ctx, quiet=quiet)
  854. build_dir = getattr(options, 'build_dir', None)
  855. def copy_to_build_dir(filepath, root=os.getcwd()):
  856. filepath_abs = os.path.abspath(filepath)
  857. if os.path.isabs(filepath):
  858. filepath = filepath_abs
  859. if filepath_abs.startswith(root):
  860. # distutil extension depends are relative to cwd
  861. mod_dir = join_path(build_dir,
  862. os.path.dirname(_relpath(filepath, root)))
  863. copy_once_if_newer(filepath_abs, mod_dir)
  864. modules_by_cfile = collections.defaultdict(list)
  865. to_compile = []
  866. for m in module_list:
  867. if build_dir:
  868. for dep in m.depends:
  869. copy_to_build_dir(dep)
  870. cy_sources = [
  871. source for source in m.sources
  872. if os.path.splitext(source)[1] in ('.pyx', '.py')]
  873. if len(cy_sources) == 1:
  874. # normal "special" case: believe the Extension module name to allow user overrides
  875. full_module_name = m.name
  876. else:
  877. # infer FQMN from source files
  878. full_module_name = None
  879. new_sources = []
  880. for source in m.sources:
  881. base, ext = os.path.splitext(source)
  882. if ext in ('.pyx', '.py'):
  883. if m.np_pythran:
  884. c_file = base + '.cpp'
  885. options = pythran_options
  886. elif m.language == 'c++':
  887. c_file = base + '.cpp'
  888. options = cpp_options
  889. else:
  890. c_file = base + '.c'
  891. options = c_options
  892. # setup for out of place build directory if enabled
  893. if build_dir:
  894. if os.path.isabs(c_file):
  895. warnings.warn("build_dir has no effect for absolute source paths")
  896. c_file = os.path.join(build_dir, c_file)
  897. dir = os.path.dirname(c_file)
  898. safe_makedirs_once(dir)
  899. if os.path.exists(c_file):
  900. c_timestamp = os.path.getmtime(c_file)
  901. else:
  902. c_timestamp = -1
  903. # Priority goes first to modified files, second to direct
  904. # dependents, and finally to indirect dependents.
  905. if c_timestamp < deps.timestamp(source):
  906. dep_timestamp, dep = deps.timestamp(source), source
  907. priority = 0
  908. else:
  909. dep_timestamp, dep = deps.newest_dependency(source)
  910. priority = 2 - (dep in deps.immediate_dependencies(source))
  911. if force or c_timestamp < dep_timestamp:
  912. if not quiet and not force:
  913. if source == dep:
  914. print("Compiling %s because it changed." % source)
  915. else:
  916. print("Compiling %s because it depends on %s." % (source, dep))
  917. if not force and options.cache:
  918. fingerprint = deps.transitive_fingerprint(source, m, options)
  919. else:
  920. fingerprint = None
  921. to_compile.append((
  922. priority, source, c_file, fingerprint, quiet,
  923. options, not exclude_failures, module_metadata.get(m.name),
  924. full_module_name))
  925. new_sources.append(c_file)
  926. modules_by_cfile[c_file].append(m)
  927. else:
  928. new_sources.append(source)
  929. if build_dir:
  930. copy_to_build_dir(source)
  931. m.sources = new_sources
  932. if options.cache:
  933. if not os.path.exists(options.cache):
  934. os.makedirs(options.cache)
  935. to_compile.sort()
  936. # Drop "priority" component of "to_compile" entries and add a
  937. # simple progress indicator.
  938. N = len(to_compile)
  939. progress_fmt = "[{0:%d}/{1}] " % len(str(N))
  940. for i in range(N):
  941. progress = progress_fmt.format(i+1, N)
  942. to_compile[i] = to_compile[i][1:] + (progress,)
  943. if N <= 1:
  944. nthreads = 0
  945. if nthreads:
  946. # Requires multiprocessing (or Python >= 2.6)
  947. try:
  948. import multiprocessing
  949. pool = multiprocessing.Pool(
  950. nthreads, initializer=_init_multiprocessing_helper)
  951. except (ImportError, OSError):
  952. print("multiprocessing required for parallel cythonization")
  953. nthreads = 0
  954. else:
  955. # This is a bit more involved than it should be, because KeyboardInterrupts
  956. # break the multiprocessing workers when using a normal pool.map().
  957. # See, for example:
  958. # http://noswap.com/blog/python-multiprocessing-keyboardinterrupt
  959. try:
  960. result = pool.map_async(cythonize_one_helper, to_compile, chunksize=1)
  961. pool.close()
  962. while not result.ready():
  963. try:
  964. result.get(99999) # seconds
  965. except multiprocessing.TimeoutError:
  966. pass
  967. except KeyboardInterrupt:
  968. pool.terminate()
  969. raise
  970. pool.join()
  971. if not nthreads:
  972. for args in to_compile:
  973. cythonize_one(*args)
  974. if exclude_failures:
  975. failed_modules = set()
  976. for c_file, modules in modules_by_cfile.items():
  977. if not os.path.exists(c_file):
  978. failed_modules.update(modules)
  979. elif os.path.getsize(c_file) < 200:
  980. f = io_open(c_file, 'r', encoding='iso8859-1')
  981. try:
  982. if f.read(len('#error ')) == '#error ':
  983. # dead compilation result
  984. failed_modules.update(modules)
  985. finally:
  986. f.close()
  987. if failed_modules:
  988. for module in failed_modules:
  989. module_list.remove(module)
  990. print("Failed compilations: %s" % ', '.join(sorted([
  991. module.name for module in failed_modules])))
  992. if options.cache:
  993. cleanup_cache(options.cache, getattr(options, 'cache_size', 1024 * 1024 * 100))
  994. # cythonize() is often followed by the (non-Python-buffered)
  995. # compiler output, flush now to avoid interleaving output.
  996. sys.stdout.flush()
  997. return module_list
  998. if os.environ.get('XML_RESULTS'):
  999. compile_result_dir = os.environ['XML_RESULTS']
  1000. def record_results(func):
  1001. def with_record(*args):
  1002. t = time.time()
  1003. success = True
  1004. try:
  1005. try:
  1006. func(*args)
  1007. except:
  1008. success = False
  1009. finally:
  1010. t = time.time() - t
  1011. module = fully_qualified_name(args[0])
  1012. name = "cythonize." + module
  1013. failures = 1 - success
  1014. if success:
  1015. failure_item = ""
  1016. else:
  1017. failure_item = "failure"
  1018. output = open(os.path.join(compile_result_dir, name + ".xml"), "w")
  1019. output.write("""
  1020. <?xml version="1.0" ?>
  1021. <testsuite name="%(name)s" errors="0" failures="%(failures)s" tests="1" time="%(t)s">
  1022. <testcase classname="%(name)s" name="cythonize">
  1023. %(failure_item)s
  1024. </testcase>
  1025. </testsuite>
  1026. """.strip() % locals())
  1027. output.close()
  1028. return with_record
  1029. else:
  1030. def record_results(func):
  1031. return func
  1032. # TODO: Share context? Issue: pyx processing leaks into pxd module
  1033. @record_results
  1034. def cythonize_one(pyx_file, c_file, fingerprint, quiet, options=None,
  1035. raise_on_failure=True, embedded_metadata=None, full_module_name=None,
  1036. progress=""):
  1037. from ..Compiler.Main import compile_single, default_options
  1038. from ..Compiler.Errors import CompileError, PyrexError
  1039. if fingerprint:
  1040. if not os.path.exists(options.cache):
  1041. safe_makedirs(options.cache)
  1042. # Cython-generated c files are highly compressible.
  1043. # (E.g. a compression ratio of about 10 for Sage).
  1044. fingerprint_file_base = join_path(
  1045. options.cache, "%s-%s" % (os.path.basename(c_file), fingerprint))
  1046. gz_fingerprint_file = fingerprint_file_base + gzip_ext
  1047. zip_fingerprint_file = fingerprint_file_base + '.zip'
  1048. if os.path.exists(gz_fingerprint_file) or os.path.exists(zip_fingerprint_file):
  1049. if not quiet:
  1050. print("%sFound compiled %s in cache" % (progress, pyx_file))
  1051. if os.path.exists(gz_fingerprint_file):
  1052. os.utime(gz_fingerprint_file, None)
  1053. with contextlib.closing(gzip_open(gz_fingerprint_file, 'rb')) as g:
  1054. with contextlib.closing(open(c_file, 'wb')) as f:
  1055. shutil.copyfileobj(g, f)
  1056. else:
  1057. os.utime(zip_fingerprint_file, None)
  1058. dirname = os.path.dirname(c_file)
  1059. with contextlib.closing(zipfile.ZipFile(zip_fingerprint_file)) as z:
  1060. for artifact in z.namelist():
  1061. z.extract(artifact, os.path.join(dirname, artifact))
  1062. return
  1063. if not quiet:
  1064. print("%sCythonizing %s" % (progress, pyx_file))
  1065. if options is None:
  1066. options = CompilationOptions(default_options)
  1067. options.output_file = c_file
  1068. options.embedded_metadata = embedded_metadata
  1069. any_failures = 0
  1070. try:
  1071. result = compile_single(pyx_file, options, full_module_name=full_module_name)
  1072. if result.num_errors > 0:
  1073. any_failures = 1
  1074. except (EnvironmentError, PyrexError) as e:
  1075. sys.stderr.write('%s\n' % e)
  1076. any_failures = 1
  1077. # XXX
  1078. import traceback
  1079. traceback.print_exc()
  1080. except Exception:
  1081. if raise_on_failure:
  1082. raise
  1083. import traceback
  1084. traceback.print_exc()
  1085. any_failures = 1
  1086. if any_failures:
  1087. if raise_on_failure:
  1088. raise CompileError(None, pyx_file)
  1089. elif os.path.exists(c_file):
  1090. os.remove(c_file)
  1091. elif fingerprint:
  1092. artifacts = list(filter(None, [
  1093. getattr(result, attr, None)
  1094. for attr in ('c_file', 'h_file', 'api_file', 'i_file')]))
  1095. if len(artifacts) == 1:
  1096. fingerprint_file = gz_fingerprint_file
  1097. with contextlib.closing(open(c_file, 'rb')) as f:
  1098. with contextlib.closing(gzip_open(fingerprint_file + '.tmp', 'wb')) as g:
  1099. shutil.copyfileobj(f, g)
  1100. else:
  1101. fingerprint_file = zip_fingerprint_file
  1102. with contextlib.closing(zipfile.ZipFile(
  1103. fingerprint_file + '.tmp', 'w', zipfile_compression_mode)) as zip:
  1104. for artifact in artifacts:
  1105. zip.write(artifact, os.path.basename(artifact))
  1106. os.rename(fingerprint_file + '.tmp', fingerprint_file)
  1107. def cythonize_one_helper(m):
  1108. import traceback
  1109. try:
  1110. return cythonize_one(*m)
  1111. except Exception:
  1112. traceback.print_exc()
  1113. raise
  1114. def _init_multiprocessing_helper():
  1115. # KeyboardInterrupt kills workers, so don't let them get it
  1116. import signal
  1117. signal.signal(signal.SIGINT, signal.SIG_IGN)
  1118. def cleanup_cache(cache, target_size, ratio=.85):
  1119. try:
  1120. p = subprocess.Popen(['du', '-s', '-k', os.path.abspath(cache)], stdout=subprocess.PIPE)
  1121. res = p.wait()
  1122. if res == 0:
  1123. total_size = 1024 * int(p.stdout.read().strip().split()[0])
  1124. if total_size < target_size:
  1125. return
  1126. except (OSError, ValueError):
  1127. pass
  1128. total_size = 0
  1129. all = []
  1130. for file in os.listdir(cache):
  1131. path = join_path(cache, file)
  1132. s = os.stat(path)
  1133. total_size += s.st_size
  1134. all.append((s.st_atime, s.st_size, path))
  1135. if total_size > target_size:
  1136. for time, size, file in reversed(sorted(all)):
  1137. os.unlink(file)
  1138. total_size -= size
  1139. if total_size < target_size * ratio:
  1140. break