|
| 1 | +import os |
| 2 | +from os.path import splitext, sep as filesep, join as pjoin, relpath |
| 3 | +from hashlib import sha1 |
| 4 | + |
| 5 | +from distutils.command.build_ext import build_ext |
| 6 | +from distutils.command.sdist import sdist |
| 7 | +from distutils.version import LooseVersion |
| 8 | + |
| 9 | + |
| 10 | +def derror_maker(klass, msg): |
| 11 | + """ Decorate distutils class to make run method raise error """ |
| 12 | + class K(klass): |
| 13 | + def run(self): |
| 14 | + raise RuntimeError(msg) |
| 15 | + return K |
| 16 | + |
| 17 | + |
| 18 | +def stamped_pyx_ok(exts, hash_stamp_fname): |
| 19 | + """ Check for match of recorded hashes for pyx, corresponding c files |
| 20 | +
|
| 21 | + Parameters |
| 22 | + ---------- |
| 23 | + exts : sequence of ``Extension`` |
| 24 | + distutils ``Extension`` instances, in fact only need to contain a |
| 25 | + ``sources`` sequence field. |
| 26 | + hash_stamp_fname : str |
| 27 | + filename of text file containing hash stamps |
| 28 | +
|
| 29 | + Returns |
| 30 | + ------- |
| 31 | + tf : bool |
| 32 | + True if there is a corresponding c file for each pyx or py file in |
| 33 | + `exts` sources, and the hash for both the (pyx, py) file *and* the c |
| 34 | + file match those recorded in the file named in `hash_stamp_fname`. |
| 35 | + """ |
| 36 | + # Calculate hashes for pyx and c files. Check for presence of c files. |
| 37 | + stamps = {} |
| 38 | + for mod in exts: |
| 39 | + for source in mod.sources: |
| 40 | + base, ext = splitext(source) |
| 41 | + if not ext in ('.pyx', '.py'): |
| 42 | + continue |
| 43 | + source_hash = sha1(open(source, 'rb').read()).hexdigest() |
| 44 | + c_fname = base + '.c' |
| 45 | + try: |
| 46 | + c_file = open(c_fname, 'rb') |
| 47 | + except IOError: |
| 48 | + return False |
| 49 | + c_hash = sha1(c_file.read()).hexdigest() |
| 50 | + stamps[source_hash] = source |
| 51 | + stamps[c_hash] = c_fname |
| 52 | + # Read stamps from hash_stamp_fname; check in stamps dictionary |
| 53 | + try: |
| 54 | + stamp_file = open(hash_stamp_fname, 'rt') |
| 55 | + except IOError: |
| 56 | + return False |
| 57 | + for line in stamp_file: |
| 58 | + if line.startswith('#'): |
| 59 | + continue |
| 60 | + fname, hash = [e.strip() for e in line.split(',')] |
| 61 | + if not hash in stamps: |
| 62 | + return False |
| 63 | + # Compare path made canonical for \/ |
| 64 | + fname = fname.replace(filesep, '/') |
| 65 | + if not stamps[hash].replace(filesep, '/') == fname: |
| 66 | + return False |
| 67 | + stamps.pop(hash) |
| 68 | + # All good if we found all hashes we need |
| 69 | + return len(stamps) == 0 |
| 70 | + |
| 71 | + |
| 72 | +def cyproc_exts(exts, cython_min_version, |
| 73 | + hash_stamps_fname = 'pyx-stamps', |
| 74 | + build_ext=build_ext): |
| 75 | + """ Process sequence of `exts` to check if we need Cython. Return builder |
| 76 | +
|
| 77 | + Parameters |
| 78 | + ---------- |
| 79 | + exts : sequence of distutils ``Extension`` |
| 80 | + If we already have good c files for any pyx or py sources, we replace |
| 81 | + the pyx or py files with their compiled up c versions inplace. |
| 82 | + cython_min_version : str |
| 83 | + Minimum cython version neede for compile |
| 84 | + hash_stamps_fname : str, optional |
| 85 | + filename with hashes for pyx/py and c files known to be in sync. Default |
| 86 | + is 'pyx-stamps' |
| 87 | + build_ext : distutils command |
| 88 | + default build_ext to return if not cythonizing. Default is distutils |
| 89 | + ``build_ext`` class |
| 90 | +
|
| 91 | + Returns |
| 92 | + ------- |
| 93 | + builder : ``distutils`` ``build_ext`` class or similar |
| 94 | + Can be ``build_ext`` input (if we have good c files) or cython |
| 95 | + ``build_ext`` if we have a good cython, or a class raising an informative |
| 96 | + error on ``run()`` |
| 97 | + need_cython : bool |
| 98 | + True if we need Cython to build extensions, False otherwise. |
| 99 | + """ |
| 100 | + if stamped_pyx_ok(exts, hash_stamps_fname): |
| 101 | + # Replace pyx with c files, use standard builder |
| 102 | + for mod in exts: |
| 103 | + sources = [] |
| 104 | + for source in mod.sources: |
| 105 | + base, ext = splitext(source) |
| 106 | + if ext in ('.pyx', '.py'): |
| 107 | + sources.append(base + '.c') |
| 108 | + else: |
| 109 | + sources.append(source) |
| 110 | + mod.sources = sources |
| 111 | + return build_ext, False |
| 112 | + # We need cython |
| 113 | + try: |
| 114 | + from Cython.Compiler.Version import version as cyversion |
| 115 | + except ImportError: |
| 116 | + return derror_maker(build_ext, |
| 117 | + 'Need cython>={0} to build extensions ' |
| 118 | + 'but cannot import "Cython"'.format( |
| 119 | + cython_min_version)), True |
| 120 | + if LooseVersion(cyversion) >= cython_min_version: |
| 121 | + from Cython.Distutils import build_ext as extbuilder |
| 122 | + return extbuilder, True |
| 123 | + return derror_maker(build_ext, |
| 124 | + 'Need cython>={0} to build extensions' |
| 125 | + 'but found cython version {1}'.format( |
| 126 | + cython_min_version, cyversion)), True |
| 127 | + |
| 128 | + |
| 129 | +def build_stamp(pyxes, include_dirs=()): |
| 130 | + """ Cythonize files in `pyxes`, return pyx, C filenames, hashes |
| 131 | +
|
| 132 | + Parameters |
| 133 | + ---------- |
| 134 | + pyxes : sequence |
| 135 | + sequence of filenames of files on which to run Cython |
| 136 | + include_dirs : sequence |
| 137 | + Any extra include directories in which to find Cython files. |
| 138 | +
|
| 139 | + Returns |
| 140 | + ------- |
| 141 | + pyx_defs : dict |
| 142 | + dict has key, value pairs of <pyx_filename>, <pyx_info>, where |
| 143 | + <pyx_info> is a dict with key, value pairs of "pyx_hash", <pyx file SHA1 |
| 144 | + hash>; "c_filename", <c filemane>; "c_hash", <c file SHA1 hash>. |
| 145 | + """ |
| 146 | + pyx_defs = {} |
| 147 | + from Cython.Compiler.Main import compile |
| 148 | + from Cython.Compiler.CmdLine import parse_command_line |
| 149 | + includes = sum([['--include-dir', d] for d in include_dirs], []) |
| 150 | + for source in pyxes: |
| 151 | + base, ext = splitext(source) |
| 152 | + pyx_hash = sha1(open(source, 'rt').read().encode('utf-8')).hexdigest() |
| 153 | + c_filename = base + '.c' |
| 154 | + options, sources = parse_command_line(includes + [source]) |
| 155 | + result = compile(sources, options) |
| 156 | + if result.num_errors > 0: |
| 157 | + raise RuntimeError('Cython failed to compile ' + source) |
| 158 | + c_hash = sha1(open(c_filename, 'rt').read().encode('utf-8')).hexdigest() |
| 159 | + pyx_defs[source] = dict(pyx_hash=pyx_hash, |
| 160 | + c_filename=c_filename, |
| 161 | + c_hash=c_hash) |
| 162 | + return pyx_defs |
| 163 | + |
| 164 | + |
| 165 | +def write_stamps(pyx_defs, stamp_fname='pyx-stamps'): |
| 166 | + """ Write stamp information in `pyx_defs` to filename `stamp_fname` |
| 167 | +
|
| 168 | + Parameters |
| 169 | + ---------- |
| 170 | + pyx_defs : dict |
| 171 | + dict has key, value pairs of <pyx_filename>, <pyx_info>, where |
| 172 | + <pyx_info> is a dict with key, value pairs of "pyx_hash", <pyx file SHA1 |
| 173 | + hash>; "c_filename", <c filemane>; "c_hash", <c file SHA1 hash>. |
| 174 | + stamp_fname : str |
| 175 | + filename to which to write stamp information |
| 176 | + """ |
| 177 | + with open(stamp_fname, 'wt') as stamp_file: |
| 178 | + stamp_file.write('# SHA1 hashes for pyx files and generated c files\n') |
| 179 | + stamp_file.write('# Auto-generated file, do not edit\n') |
| 180 | + for pyx_fname, pyx_info in pyx_defs.items(): |
| 181 | + stamp_file.write('%s, %s\n' % (pyx_fname, |
| 182 | + pyx_info['pyx_hash'])) |
| 183 | + stamp_file.write('%s, %s\n' % (pyx_info['c_filename'], |
| 184 | + pyx_info['c_hash'])) |
| 185 | + |
| 186 | + |
| 187 | +def find_pyx(root_dir): |
| 188 | + """ Recursively find files with extension '.pyx' starting at `root_dir` |
| 189 | +
|
| 190 | + Parameters |
| 191 | + ---------- |
| 192 | + root_dir : str |
| 193 | + Directory from which to search for pyx files. |
| 194 | +
|
| 195 | + Returns |
| 196 | + ------- |
| 197 | + pyxes : list |
| 198 | + list of filenames relative to `root_dir` |
| 199 | + """ |
| 200 | + pyxes = [] |
| 201 | + for dirpath, dirnames, filenames in os.walk(root_dir): |
| 202 | + for filename in filenames: |
| 203 | + if not filename.endswith('.pyx'): |
| 204 | + continue |
| 205 | + base = relpath(dirpath, root_dir) |
| 206 | + pyxes.append(pjoin(base, filename)) |
| 207 | + return pyxes |
| 208 | + |
| 209 | + |
| 210 | +def get_pyx_sdist(sdist_like=sdist, hash_stamps_fname='pyx-stamps', |
| 211 | + include_dirs=()): |
| 212 | + """ Add pyx->c conversion, hash recording to sdist command `sdist_like` |
| 213 | +
|
| 214 | + Parameters |
| 215 | + ---------- |
| 216 | + sdist_like : sdist command class, optional |
| 217 | + command that will do work of ``distutils.command.sdist.sdist``. By |
| 218 | + default we use the distutils version |
| 219 | + hash_stamps_fname : str, optional |
| 220 | + filename to which to write hashes of pyx / py and c files. Default is |
| 221 | + ``pyx-stamps`` |
| 222 | + include_dirs : sequence |
| 223 | + Any extra include directories in which to find Cython files. |
| 224 | +
|
| 225 | + Returns |
| 226 | + ------- |
| 227 | + modified_sdist : sdist-like command class |
| 228 | + decorated `sdist_like` class, for compiling pyx / py files to c, putting |
| 229 | + the .c files in the the source archive, and writing hashes for these |
| 230 | + into the file named from `hash_stamps_fname` |
| 231 | + """ |
| 232 | + class PyxSDist(sdist_like): |
| 233 | + """ Custom distutils sdist command to generate .c files from pyx files. |
| 234 | +
|
| 235 | + Running the command object ``obj.run()`` will compile the pyx / py files |
| 236 | + in any extensions, into c files, and add them to the list of files to |
| 237 | + put into the source archive, as well as the usual behavior of distutils |
| 238 | + ``sdist``. It will also take the sha1 hashes of the pyx / py and c |
| 239 | + files, and store them in a file ``pyx-stamps``, and put this file in the |
| 240 | + release tree. This allows someone who has the archive to know that the |
| 241 | + pyx and c files that they have are the ones packed into the archive, and |
| 242 | + therefore they may not need Cython at install time. See |
| 243 | + ``cython_process_exts`` for the build-time command. |
| 244 | + """ |
| 245 | + |
| 246 | + def make_distribution(self): |
| 247 | + """ Compile pyx to c files, add to sources, stamp sha1s """ |
| 248 | + pyxes = [] |
| 249 | + for mod in self.distribution.ext_modules: |
| 250 | + for source in mod.sources: |
| 251 | + base, ext = splitext(source) |
| 252 | + if ext in ('.pyx', '.py'): |
| 253 | + pyxes.append(source) |
| 254 | + self.pyx_defs = build_stamp(pyxes, include_dirs) |
| 255 | + for pyx_fname, pyx_info in self.pyx_defs.items(): |
| 256 | + self.filelist.append(pyx_info['c_filename']) |
| 257 | + sdist_like.make_distribution(self) |
| 258 | + |
| 259 | + def make_release_tree(self, base_dir, files): |
| 260 | + """ Put pyx stamps file into release tree """ |
| 261 | + sdist_like.make_release_tree(self, base_dir, files) |
| 262 | + stamp_fname = pjoin(base_dir, hash_stamps_fname) |
| 263 | + write_stamps(self.pyx_defs, stamp_fname) |
| 264 | + |
| 265 | + return PyxSDist |
| 266 | + |
| 267 | + |
| 268 | +def build_stamp_source(root_dir=None, stamp_fname='pyx-stamps', |
| 269 | + include_dirs=None): |
| 270 | + """ Build cython c files, make stamp file in source tree `root_dir` |
| 271 | +
|
| 272 | + Parameters |
| 273 | + ---------- |
| 274 | + root_dir : None or str, optional |
| 275 | + Directory from which to find ``.pyx`` files. If None, use current |
| 276 | + working directory. |
| 277 | + stamp_fname : str, optional |
| 278 | + Filename for stamp file we will write |
| 279 | + include_dirs : None or sequence |
| 280 | + Any extra Cython include directories |
| 281 | + """ |
| 282 | + if root_dir is None: |
| 283 | + root_dir = os.getcwd() |
| 284 | + if include_dirs is None: |
| 285 | + include_dirs = [pjoin(root_dir, 'src')] |
| 286 | + pyxes = find_pyx(root_dir) |
| 287 | + pyx_defs = build_stamp(pyxes, include_dirs=include_dirs) |
| 288 | + write_stamps(pyx_defs, stamp_fname) |
0 commit comments