##// END OF EJS Templates
friendlier error messages when invoke/lessc are missing...
Min RK -
Show More
@@ -1,83 +1,108 b''
1 1 """invoke task file to build CSS"""
2 2
3 from invoke import task, run
3 from __future__ import print_function
4
4 5 import os
6 import sys
5 7 from distutils.version import LooseVersion as V
6 8 from subprocess import check_output
7 9
10 from invoke import task, run
11 from invoke.runner import Result
12 from invoke.exceptions import Failure
13
8 14 pjoin = os.path.join
9 15 static_dir = 'static'
10 16 components_dir = pjoin(static_dir, 'components')
11 17 here = os.path.dirname(__file__)
12 18
13 19 min_less_version = '2.0'
14 20 max_less_version = '3.0' # exclusive if string
15 21
22
23 def _fail(msg=''):
24 """Fail a task, logging a message to stderr
25
26 raises a special Failure Exception from invoke.
27 """
28 if msg:
29 print(msg, file=sys.stderr)
30 # raising a Failure allows us to avoid a traceback
31 # we only care about exited, but stdout, stderr, pty are required args
32 raise Failure(Result(stdout='', stderr='', pty=False, exited=1))
33
16 34 def _need_css_update():
17 35 """Does less need to run?"""
18 36
19 37 static_path = pjoin(here, static_dir)
20 38 css_targets = [
21 39 pjoin(static_path, 'style', '%s.min.css' % name)
22 40 for name in ('style', 'ipython')
23 41 ]
24 42 css_maps = [t + '.map' for t in css_targets]
25 43 targets = css_targets + css_maps
26 44 if not all(os.path.exists(t) for t in targets):
27 45 # some generated files don't exist
28 46 return True
29 47 earliest_target = sorted(os.stat(t).st_mtime for t in targets)[0]
30 48
31 49 # check if any .less files are newer than the generated targets
32 50 for (dirpath, dirnames, filenames) in os.walk(static_path):
33 51 for f in filenames:
34 52 if f.endswith('.less'):
35 53 path = pjoin(static_path, dirpath, f)
36 54 timestamp = os.stat(path).st_mtime
37 55 if timestamp > earliest_target:
38 56 return True
39 57
40 58 return False
41 59
42 60 @task
43 61 def css(minify=False, verbose=False, force=False):
44 62 """generate the css from less files"""
45 63 # minify implies force because it's not the default behavior
46 64 if not force and not minify and not _need_css_update():
47 65 print("css up-to-date")
48 66 return
49 67
50 68 for name in ('style', 'ipython'):
51 69 source = pjoin('style', "%s.less" % name)
52 70 target = pjoin('style', "%s.min.css" % name)
53 71 sourcemap = pjoin('style', "%s.min.css.map" % name)
54 72 _compile_less(source, target, sourcemap, minify, verbose)
55 73
74
56 75 def _compile_less(source, target, sourcemap, minify=True, verbose=False):
57 76 """Compile a less file by source and target relative to static_dir"""
58 77 min_flag = '-x' if minify else ''
59 78 ver_flag = '--verbose' if verbose else ''
60 79
80 install = "(npm install -g 'less@<{}')".format(max_less_version)
61 81 # pin less to version number from above
62 82 try:
63 83 out = check_output(['lessc', '--version'])
64 84 except OSError as err:
65 raise ValueError("Unable to find lessc. Please install lessc >= %s and < %s " \
66 % (min_less_version, max_less_version))
85 _fail("Unable to find lessc. Rebuilding css requires less >= {0} and < {1} {2}".format(
86 min_less_version, max_less_version, install
87 ))
67 88 out = out.decode('utf8', 'replace')
68 89 less_version = out.split()[1]
69 90 if min_less_version and V(less_version) < V(min_less_version):
70 raise ValueError("lessc too old: %s < %s. Use `$ npm install less@X.Y.Z` to install a specific version of less" % (less_version, min_less_version))
91 _fail("lessc too old: {} < {} {}".format(
92 less_version, min_less_version, install,
93 ))
71 94 if max_less_version and V(less_version) >= V(max_less_version):
72 raise ValueError("lessc too new: %s >= %s. Use `$ npm install less@X.Y.Z` to install a specific version of less" % (less_version, max_less_version))
95 _fail("lessc too new: {} >= {} {}".format(
96 less_version, max_less_version, install,
97 ))
73 98
74 99 static_path = pjoin(here, static_dir)
75 100 cwd = os.getcwd()
76 101 try:
77 102 os.chdir(static_dir)
78 103 run('lessc {min_flag} {ver_flag} --source-map={sourcemap} --source-map-basepath={static_path} --source-map-rootpath="../" {source} {target}'.format(**locals()),
79 104 echo=True,
80 105 )
81 106 finally:
82 107 os.chdir(cwd)
83 108
@@ -1,755 +1,765 b''
1 1 # encoding: utf-8
2 2 """
3 3 This module defines the things that are used in setup.py for building IPython
4 4
5 5 This includes:
6 6
7 7 * The basic arguments to setup
8 8 * Functions for finding things like packages, package data, etc.
9 9 * A function for checking dependencies.
10 10 """
11 11
12 12 # Copyright (c) IPython Development Team.
13 13 # Distributed under the terms of the Modified BSD License.
14 14
15 15 from __future__ import print_function
16 16
17 17 import errno
18 18 import os
19 19 import sys
20 20
21 21 from distutils import log
22 22 from distutils.command.build_py import build_py
23 23 from distutils.command.build_scripts import build_scripts
24 24 from distutils.command.install import install
25 25 from distutils.command.install_scripts import install_scripts
26 26 from distutils.cmd import Command
27 from distutils.errors import DistutilsExecError
27 28 from fnmatch import fnmatch
28 29 from glob import glob
29 from subprocess import check_call
30 from subprocess import Popen, PIPE
30 31
31 32 from setupext import install_data_ext
32 33
33 34 #-------------------------------------------------------------------------------
34 35 # Useful globals and utility functions
35 36 #-------------------------------------------------------------------------------
36 37
37 38 # A few handy globals
38 39 isfile = os.path.isfile
39 40 pjoin = os.path.join
40 41 repo_root = os.path.dirname(os.path.abspath(__file__))
41 42
42 43 def oscmd(s):
43 44 print(">", s)
44 45 os.system(s)
45 46
46 47 # Py3 compatibility hacks, without assuming IPython itself is installed with
47 48 # the full py3compat machinery.
48 49
49 50 try:
50 51 execfile
51 52 except NameError:
52 53 def execfile(fname, globs, locs=None):
53 54 locs = locs or globs
54 55 exec(compile(open(fname).read(), fname, "exec"), globs, locs)
55 56
56 57 # A little utility we'll need below, since glob() does NOT allow you to do
57 58 # exclusion on multiple endings!
58 59 def file_doesnt_endwith(test,endings):
59 60 """Return true if test is a file and its name does NOT end with any
60 61 of the strings listed in endings."""
61 62 if not isfile(test):
62 63 return False
63 64 for e in endings:
64 65 if test.endswith(e):
65 66 return False
66 67 return True
67 68
68 69 #---------------------------------------------------------------------------
69 70 # Basic project information
70 71 #---------------------------------------------------------------------------
71 72
72 73 # release.py contains version, authors, license, url, keywords, etc.
73 74 execfile(pjoin(repo_root, 'IPython','core','release.py'), globals())
74 75
75 76 # Create a dict with the basic information
76 77 # This dict is eventually passed to setup after additional keys are added.
77 78 setup_args = dict(
78 79 name = name,
79 80 version = version,
80 81 description = description,
81 82 long_description = long_description,
82 83 author = author,
83 84 author_email = author_email,
84 85 url = url,
85 86 download_url = download_url,
86 87 license = license,
87 88 platforms = platforms,
88 89 keywords = keywords,
89 90 classifiers = classifiers,
90 91 cmdclass = {'install_data': install_data_ext},
91 92 )
92 93
93 94
94 95 #---------------------------------------------------------------------------
95 96 # Find packages
96 97 #---------------------------------------------------------------------------
97 98
98 99 def find_packages():
99 100 """
100 101 Find all of IPython's packages.
101 102 """
102 103 excludes = ['deathrow', 'quarantine']
103 104 packages = []
104 105 for dir,subdirs,files in os.walk('IPython'):
105 106 package = dir.replace(os.path.sep, '.')
106 107 if any(package.startswith('IPython.'+exc) for exc in excludes):
107 108 # package is to be excluded (e.g. deathrow)
108 109 continue
109 110 if '__init__.py' not in files:
110 111 # not a package
111 112 continue
112 113 packages.append(package)
113 114 return packages
114 115
115 116 #---------------------------------------------------------------------------
116 117 # Find package data
117 118 #---------------------------------------------------------------------------
118 119
119 120 def find_package_data():
120 121 """
121 122 Find IPython's package_data.
122 123 """
123 124 # This is not enough for these things to appear in an sdist.
124 125 # We need to muck with the MANIFEST to get this to work
125 126
126 127 # exclude components and less from the walk;
127 128 # we will build the components separately
128 129 excludes = [
129 130 pjoin('static', 'components'),
130 131 pjoin('static', '*', 'less'),
131 132 ]
132 133
133 134 # walk notebook resources:
134 135 cwd = os.getcwd()
135 136 os.chdir(os.path.join('IPython', 'html'))
136 137 static_data = []
137 138 for parent, dirs, files in os.walk('static'):
138 139 if any(fnmatch(parent, pat) for pat in excludes):
139 140 # prevent descending into subdirs
140 141 dirs[:] = []
141 142 continue
142 143 for f in files:
143 144 static_data.append(pjoin(parent, f))
144 145
145 146 components = pjoin("static", "components")
146 147 # select the components we actually need to install
147 148 # (there are lots of resources we bundle for sdist-reasons that we don't actually use)
148 149 static_data.extend([
149 150 pjoin(components, "backbone", "backbone-min.js"),
150 151 pjoin(components, "bootstrap", "js", "bootstrap.min.js"),
151 152 pjoin(components, "bootstrap-tour", "build", "css", "bootstrap-tour.min.css"),
152 153 pjoin(components, "bootstrap-tour", "build", "js", "bootstrap-tour.min.js"),
153 154 pjoin(components, "es6-promise", "*.js"),
154 155 pjoin(components, "font-awesome", "fonts", "*.*"),
155 156 pjoin(components, "google-caja", "html-css-sanitizer-minified.js"),
156 157 pjoin(components, "jquery", "jquery.min.js"),
157 158 pjoin(components, "jquery-ui", "ui", "minified", "jquery-ui.min.js"),
158 159 pjoin(components, "jquery-ui", "themes", "smoothness", "jquery-ui.min.css"),
159 160 pjoin(components, "jquery-ui", "themes", "smoothness", "images", "*"),
160 161 pjoin(components, "marked", "lib", "marked.js"),
161 162 pjoin(components, "requirejs", "require.js"),
162 163 pjoin(components, "underscore", "underscore-min.js"),
163 164 pjoin(components, "moment", "moment.js"),
164 165 pjoin(components, "moment", "min", "moment.min.js"),
165 166 pjoin(components, "term.js", "src", "term.js"),
166 167 pjoin(components, "text-encoding", "lib", "encoding.js"),
167 168 ])
168 169
169 170 # Ship all of Codemirror's CSS and JS
170 171 for parent, dirs, files in os.walk(pjoin(components, 'codemirror')):
171 172 for f in files:
172 173 if f.endswith(('.js', '.css')):
173 174 static_data.append(pjoin(parent, f))
174 175
175 176 os.chdir(os.path.join('tests',))
176 177 js_tests = glob('*.js') + glob('*/*.js')
177 178
178 179 os.chdir(os.path.join(cwd, 'IPython', 'nbconvert'))
179 180 nbconvert_templates = [os.path.join(dirpath, '*.*')
180 181 for dirpath, _, _ in os.walk('templates')]
181 182
182 183 os.chdir(cwd)
183 184
184 185 package_data = {
185 186 'IPython.config.profile' : ['README*', '*/*.py'],
186 187 'IPython.core.tests' : ['*.png', '*.jpg'],
187 188 'IPython.lib.tests' : ['*.wav'],
188 189 'IPython.testing.plugin' : ['*.txt'],
189 190 'IPython.html' : ['templates/*'] + static_data,
190 191 'IPython.html.tests' : js_tests,
191 192 'IPython.qt.console' : ['resources/icon/*.svg'],
192 193 'IPython.nbconvert' : nbconvert_templates +
193 194 [
194 195 'tests/files/*.*',
195 196 'exporters/tests/files/*.*',
196 197 'preprocessors/tests/files/*.*',
197 198 ],
198 199 'IPython.nbconvert.filters' : ['marked.js'],
199 200 'IPython.nbformat' : [
200 201 'tests/*.ipynb',
201 202 'v3/nbformat.v3.schema.json',
202 203 'v4/nbformat.v4.schema.json',
203 204 ],
204 205 'IPython.kernel': ['resources/*.*'],
205 206 }
206 207
207 208 return package_data
208 209
209 210
210 211 def check_package_data(package_data):
211 212 """verify that package_data globs make sense"""
212 213 print("checking package data")
213 214 for pkg, data in package_data.items():
214 215 pkg_root = pjoin(*pkg.split('.'))
215 216 for d in data:
216 217 path = pjoin(pkg_root, d)
217 218 if '*' in path:
218 219 assert len(glob(path)) > 0, "No files match pattern %s" % path
219 220 else:
220 221 assert os.path.exists(path), "Missing package data: %s" % path
221 222
222 223
223 224 def check_package_data_first(command):
224 225 """decorator for checking package_data before running a given command
225 226
226 227 Probably only needs to wrap build_py
227 228 """
228 229 class DecoratedCommand(command):
229 230 def run(self):
230 231 check_package_data(self.package_data)
231 232 command.run(self)
232 233 return DecoratedCommand
233 234
234 235
235 236 #---------------------------------------------------------------------------
236 237 # Find data files
237 238 #---------------------------------------------------------------------------
238 239
239 240 def make_dir_struct(tag,base,out_base):
240 241 """Make the directory structure of all files below a starting dir.
241 242
242 243 This is just a convenience routine to help build a nested directory
243 244 hierarchy because distutils is too stupid to do this by itself.
244 245
245 246 XXX - this needs a proper docstring!
246 247 """
247 248
248 249 # we'll use these a lot below
249 250 lbase = len(base)
250 251 pathsep = os.path.sep
251 252 lpathsep = len(pathsep)
252 253
253 254 out = []
254 255 for (dirpath,dirnames,filenames) in os.walk(base):
255 256 # we need to strip out the dirpath from the base to map it to the
256 257 # output (installation) path. This requires possibly stripping the
257 258 # path separator, because otherwise pjoin will not work correctly
258 259 # (pjoin('foo/','/bar') returns '/bar').
259 260
260 261 dp_eff = dirpath[lbase:]
261 262 if dp_eff.startswith(pathsep):
262 263 dp_eff = dp_eff[lpathsep:]
263 264 # The output path must be anchored at the out_base marker
264 265 out_path = pjoin(out_base,dp_eff)
265 266 # Now we can generate the final filenames. Since os.walk only produces
266 267 # filenames, we must join back with the dirpath to get full valid file
267 268 # paths:
268 269 pfiles = [pjoin(dirpath,f) for f in filenames]
269 270 # Finally, generate the entry we need, which is a pari of (output
270 271 # path, files) for use as a data_files parameter in install_data.
271 272 out.append((out_path, pfiles))
272 273
273 274 return out
274 275
275 276
276 277 def find_data_files():
277 278 """
278 279 Find IPython's data_files.
279 280
280 281 Just man pages at this point.
281 282 """
282 283
283 284 manpagebase = pjoin('share', 'man', 'man1')
284 285
285 286 # Simple file lists can be made by hand
286 287 manpages = [f for f in glob(pjoin('docs','man','*.1.gz')) if isfile(f)]
287 288 if not manpages:
288 289 # When running from a source tree, the manpages aren't gzipped
289 290 manpages = [f for f in glob(pjoin('docs','man','*.1')) if isfile(f)]
290 291
291 292 # And assemble the entire output list
292 293 data_files = [ (manpagebase, manpages) ]
293 294
294 295 return data_files
295 296
296 297
297 298 def make_man_update_target(manpage):
298 299 """Return a target_update-compliant tuple for the given manpage.
299 300
300 301 Parameters
301 302 ----------
302 303 manpage : string
303 304 Name of the manpage, must include the section number (trailing number).
304 305
305 306 Example
306 307 -------
307 308
308 309 >>> make_man_update_target('ipython.1') #doctest: +NORMALIZE_WHITESPACE
309 310 ('docs/man/ipython.1.gz',
310 311 ['docs/man/ipython.1'],
311 312 'cd docs/man && gzip -9c ipython.1 > ipython.1.gz')
312 313 """
313 314 man_dir = pjoin('docs', 'man')
314 315 manpage_gz = manpage + '.gz'
315 316 manpath = pjoin(man_dir, manpage)
316 317 manpath_gz = pjoin(man_dir, manpage_gz)
317 318 gz_cmd = ( "cd %(man_dir)s && gzip -9c %(manpage)s > %(manpage_gz)s" %
318 319 locals() )
319 320 return (manpath_gz, [manpath], gz_cmd)
320 321
321 322 # The two functions below are copied from IPython.utils.path, so we don't need
322 323 # to import IPython during setup, which fails on Python 3.
323 324
324 325 def target_outdated(target,deps):
325 326 """Determine whether a target is out of date.
326 327
327 328 target_outdated(target,deps) -> 1/0
328 329
329 330 deps: list of filenames which MUST exist.
330 331 target: single filename which may or may not exist.
331 332
332 333 If target doesn't exist or is older than any file listed in deps, return
333 334 true, otherwise return false.
334 335 """
335 336 try:
336 337 target_time = os.path.getmtime(target)
337 338 except os.error:
338 339 return 1
339 340 for dep in deps:
340 341 dep_time = os.path.getmtime(dep)
341 342 if dep_time > target_time:
342 343 #print "For target",target,"Dep failed:",dep # dbg
343 344 #print "times (dep,tar):",dep_time,target_time # dbg
344 345 return 1
345 346 return 0
346 347
347 348
348 349 def target_update(target,deps,cmd):
349 350 """Update a target with a given command given a list of dependencies.
350 351
351 352 target_update(target,deps,cmd) -> runs cmd if target is outdated.
352 353
353 354 This is just a wrapper around target_outdated() which calls the given
354 355 command if target is outdated."""
355 356
356 357 if target_outdated(target,deps):
357 358 os.system(cmd)
358 359
359 360 #---------------------------------------------------------------------------
360 361 # Find scripts
361 362 #---------------------------------------------------------------------------
362 363
363 364 def find_entry_points():
364 365 """Defines the command line entry points for IPython
365 366
366 367 This always uses setuptools-style entry points. When setuptools is not in
367 368 use, our own build_scripts_entrypt class below parses these and builds
368 369 command line scripts.
369 370
370 371 Each of our entry points gets both a plain name, e.g. ipython, and one
371 372 suffixed with the Python major version number, e.g. ipython3.
372 373 """
373 374 ep = [
374 375 'ipython%s = IPython:start_ipython',
375 376 'ipcontroller%s = IPython.parallel.apps.ipcontrollerapp:launch_new_instance',
376 377 'ipengine%s = IPython.parallel.apps.ipengineapp:launch_new_instance',
377 378 'ipcluster%s = IPython.parallel.apps.ipclusterapp:launch_new_instance',
378 379 'iptest%s = IPython.testing.iptestcontroller:main',
379 380 ]
380 381 suffix = str(sys.version_info[0])
381 382 return [e % '' for e in ep] + [e % suffix for e in ep]
382 383
383 384 script_src = """#!{executable}
384 385 # This script was automatically generated by setup.py
385 386 if __name__ == '__main__':
386 387 from {mod} import {func}
387 388 {func}()
388 389 """
389 390
390 391 class build_scripts_entrypt(build_scripts):
391 392 """Build the command line scripts
392 393
393 394 Parse setuptools style entry points and write simple scripts to run the
394 395 target functions.
395 396
396 397 On Windows, this also creates .cmd wrappers for the scripts so that you can
397 398 easily launch them from a command line.
398 399 """
399 400 def run(self):
400 401 self.mkpath(self.build_dir)
401 402 outfiles = []
402 403 for script in find_entry_points():
403 404 name, entrypt = script.split('=')
404 405 name = name.strip()
405 406 entrypt = entrypt.strip()
406 407 outfile = os.path.join(self.build_dir, name)
407 408 outfiles.append(outfile)
408 409 print('Writing script to', outfile)
409 410
410 411 mod, func = entrypt.split(':')
411 412 with open(outfile, 'w') as f:
412 413 f.write(script_src.format(executable=sys.executable,
413 414 mod=mod, func=func))
414 415
415 416 if sys.platform == 'win32':
416 417 # Write .cmd wrappers for Windows so 'ipython' etc. work at the
417 418 # command line
418 419 cmd_file = os.path.join(self.build_dir, name + '.cmd')
419 420 cmd = '@"{python}" "%~dp0\{script}" %*\r\n'.format(
420 421 python=sys.executable, script=name)
421 422 log.info("Writing %s wrapper script" % cmd_file)
422 423 with open(cmd_file, 'w') as f:
423 424 f.write(cmd)
424 425
425 426 return outfiles, outfiles
426 427
427 428 class install_lib_symlink(Command):
428 429 user_options = [
429 430 ('install-dir=', 'd', "directory to install to"),
430 431 ]
431 432
432 433 def initialize_options(self):
433 434 self.install_dir = None
434 435
435 436 def finalize_options(self):
436 437 self.set_undefined_options('symlink',
437 438 ('install_lib', 'install_dir'),
438 439 )
439 440
440 441 def run(self):
441 442 if sys.platform == 'win32':
442 443 raise Exception("This doesn't work on Windows.")
443 444 pkg = os.path.join(os.getcwd(), 'IPython')
444 445 dest = os.path.join(self.install_dir, 'IPython')
445 446 if os.path.islink(dest):
446 447 print('removing existing symlink at %s' % dest)
447 448 os.unlink(dest)
448 449 print('symlinking %s -> %s' % (pkg, dest))
449 450 os.symlink(pkg, dest)
450 451
451 452 class unsymlink(install):
452 453 def run(self):
453 454 dest = os.path.join(self.install_lib, 'IPython')
454 455 if os.path.islink(dest):
455 456 print('removing symlink at %s' % dest)
456 457 os.unlink(dest)
457 458 else:
458 459 print('No symlink exists at %s' % dest)
459 460
460 461 class install_symlinked(install):
461 462 def run(self):
462 463 if sys.platform == 'win32':
463 464 raise Exception("This doesn't work on Windows.")
464 465
465 466 # Run all sub-commands (at least those that need to be run)
466 467 for cmd_name in self.get_sub_commands():
467 468 self.run_command(cmd_name)
468 469
469 470 # 'sub_commands': a list of commands this command might have to run to
470 471 # get its work done. See cmd.py for more info.
471 472 sub_commands = [('install_lib_symlink', lambda self:True),
472 473 ('install_scripts_sym', lambda self:True),
473 474 ]
474 475
475 476 class install_scripts_for_symlink(install_scripts):
476 477 """Redefined to get options from 'symlink' instead of 'install'.
477 478
478 479 I love distutils almost as much as I love setuptools.
479 480 """
480 481 def finalize_options(self):
481 482 self.set_undefined_options('build', ('build_scripts', 'build_dir'))
482 483 self.set_undefined_options('symlink',
483 484 ('install_scripts', 'install_dir'),
484 485 ('force', 'force'),
485 486 ('skip_build', 'skip_build'),
486 487 )
487 488
488 489 #---------------------------------------------------------------------------
489 490 # Verify all dependencies
490 491 #---------------------------------------------------------------------------
491 492
492 493 def check_for_dependencies():
493 494 """Check for IPython's dependencies.
494 495
495 496 This function should NOT be called if running under setuptools!
496 497 """
497 498 from setupext.setupext import (
498 499 print_line, print_raw, print_status,
499 500 check_for_sphinx, check_for_pygments,
500 501 check_for_nose, check_for_pexpect,
501 502 check_for_pyzmq, check_for_readline,
502 503 check_for_jinja2, check_for_tornado
503 504 )
504 505 print_line()
505 506 print_raw("BUILDING IPYTHON")
506 507 print_status('python', sys.version)
507 508 print_status('platform', sys.platform)
508 509 if sys.platform == 'win32':
509 510 print_status('Windows version', sys.getwindowsversion())
510 511
511 512 print_raw("")
512 513 print_raw("OPTIONAL DEPENDENCIES")
513 514
514 515 check_for_sphinx()
515 516 check_for_pygments()
516 517 check_for_nose()
517 518 if os.name == 'posix':
518 519 check_for_pexpect()
519 520 check_for_pyzmq()
520 521 check_for_tornado()
521 522 check_for_readline()
522 523 check_for_jinja2()
523 524
524 525 #---------------------------------------------------------------------------
525 526 # VCS related
526 527 #---------------------------------------------------------------------------
527 528
528 529 # utils.submodule has checks for submodule status
529 530 execfile(pjoin('IPython','utils','submodule.py'), globals())
530 531
531 532 class UpdateSubmodules(Command):
532 533 """Update git submodules
533 534
534 535 IPython's external javascript dependencies live in a separate repo.
535 536 """
536 537 description = "Update git submodules"
537 538 user_options = []
538 539
539 540 def initialize_options(self):
540 541 pass
541 542
542 543 def finalize_options(self):
543 544 pass
544 545
545 546 def run(self):
546 547 failure = False
547 548 try:
548 549 self.spawn('git submodule init'.split())
549 550 self.spawn('git submodule update --recursive'.split())
550 551 except Exception as e:
551 552 failure = e
552 553 print(e)
553 554
554 555 if not check_submodule_status(repo_root) == 'clean':
555 556 print("submodules could not be checked out")
556 557 sys.exit(1)
557 558
558 559
559 560 def git_prebuild(pkg_dir, build_cmd=build_py):
560 561 """Return extended build or sdist command class for recording commit
561 562
562 563 records git commit in IPython.utils._sysinfo.commit
563 564
564 565 for use in IPython.utils.sysinfo.sys_info() calls after installation.
565 566
566 567 Also ensures that submodules exist prior to running
567 568 """
568 569
569 570 class MyBuildPy(build_cmd):
570 571 ''' Subclass to write commit data into installation tree '''
571 572 def run(self):
572 573 build_cmd.run(self)
573 574 # this one will only fire for build commands
574 575 if hasattr(self, 'build_lib'):
575 576 self._record_commit(self.build_lib)
576 577
577 578 def make_release_tree(self, base_dir, files):
578 579 # this one will fire for sdist
579 580 build_cmd.make_release_tree(self, base_dir, files)
580 581 self._record_commit(base_dir)
581 582
582 583 def _record_commit(self, base_dir):
583 584 import subprocess
584 585 proc = subprocess.Popen('git rev-parse --short HEAD',
585 586 stdout=subprocess.PIPE,
586 587 stderr=subprocess.PIPE,
587 588 shell=True)
588 589 repo_commit, _ = proc.communicate()
589 590 repo_commit = repo_commit.strip().decode("ascii")
590 591
591 592 out_pth = pjoin(base_dir, pkg_dir, 'utils', '_sysinfo.py')
592 593 if os.path.isfile(out_pth) and not repo_commit:
593 594 # nothing to write, don't clobber
594 595 return
595 596
596 597 print("writing git commit '%s' to %s" % (repo_commit, out_pth))
597 598
598 599 # remove to avoid overwriting original via hard link
599 600 try:
600 601 os.remove(out_pth)
601 602 except (IOError, OSError):
602 603 pass
603 604 with open(out_pth, 'w') as out_file:
604 605 out_file.writelines([
605 606 '# GENERATED BY setup.py\n',
606 607 'commit = u"%s"\n' % repo_commit,
607 608 ])
608 609 return require_submodules(MyBuildPy)
609 610
610 611
611 612 def require_submodules(command):
612 613 """decorator for instructing a command to check for submodules before running"""
613 614 class DecoratedCommand(command):
614 615 def run(self):
615 616 if not check_submodule_status(repo_root) == 'clean':
616 617 print("submodules missing! Run `setup.py submodule` and try again")
617 618 sys.exit(1)
618 619 command.run(self)
619 620 return DecoratedCommand
620 621
621 622 #---------------------------------------------------------------------------
622 623 # bdist related
623 624 #---------------------------------------------------------------------------
624 625
625 626 def get_bdist_wheel():
626 627 """Construct bdist_wheel command for building wheels
627 628
628 629 Constructs py2-none-any tag, instead of py2.7-none-any
629 630 """
630 631 class RequiresWheel(Command):
631 632 description = "Dummy command for missing bdist_wheel"
632 633 user_options = []
633 634
634 635 def initialize_options(self):
635 636 pass
636 637
637 638 def finalize_options(self):
638 639 pass
639 640
640 641 def run(self):
641 642 print("bdist_wheel requires the wheel package")
642 643 sys.exit(1)
643 644
644 645 if 'setuptools' not in sys.modules:
645 646 return RequiresWheel
646 647 else:
647 648 try:
648 649 from wheel.bdist_wheel import bdist_wheel, read_pkg_info, write_pkg_info
649 650 except ImportError:
650 651 return RequiresWheel
651 652
652 653 class bdist_wheel_tag(bdist_wheel):
653 654
654 655 def add_requirements(self, metadata_path):
655 656 """transform platform-dependent requirements"""
656 657 pkg_info = read_pkg_info(metadata_path)
657 658 # pkg_info is an email.Message object (?!)
658 659 # we have to remove the unconditional 'readline' and/or 'pyreadline' entries
659 660 # and transform them to conditionals
660 661 requires = pkg_info.get_all('Requires-Dist')
661 662 del pkg_info['Requires-Dist']
662 663 def _remove_startswith(lis, prefix):
663 664 """like list.remove, but with startswith instead of =="""
664 665 found = False
665 666 for idx, item in enumerate(lis):
666 667 if item.startswith(prefix):
667 668 found = True
668 669 break
669 670 if found:
670 671 lis.pop(idx)
671 672
672 673 for pkg in ("gnureadline", "pyreadline", "mock"):
673 674 _remove_startswith(requires, pkg)
674 675 requires.append("gnureadline; sys.platform == 'darwin' and platform.python_implementation == 'CPython'")
675 676 requires.append("pyreadline (>=2.0); extra == 'terminal' and sys.platform == 'win32' and platform.python_implementation == 'CPython'")
676 677 requires.append("pyreadline (>=2.0); extra == 'all' and sys.platform == 'win32' and platform.python_implementation == 'CPython'")
677 678 requires.append("mock; extra == 'test' and python_version < '3.3'")
678 679 for r in requires:
679 680 pkg_info['Requires-Dist'] = r
680 681 write_pkg_info(metadata_path, pkg_info)
681 682
682 683 return bdist_wheel_tag
683 684
684 685 #---------------------------------------------------------------------------
685 686 # Notebook related
686 687 #---------------------------------------------------------------------------
687 688
688 689 class CompileCSS(Command):
689 690 """Recompile Notebook CSS
690 691
691 692 Regenerate the compiled CSS from LESS sources.
692 693
693 694 Requires various dev dependencies, such as invoke and lessc.
694 695 """
695 696 description = "Recompile Notebook CSS"
696 697 user_options = [
697 698 ('minify', 'x', "minify CSS"),
698 699 ('force', 'f', "force recompilation of CSS"),
699 700 ]
700 701
701 702 def initialize_options(self):
702 703 self.minify = False
703 704 self.force = False
704 705
705 706 def finalize_options(self):
706 707 self.minify = bool(self.minify)
707 708 self.force = bool(self.force)
708 709
709 710 def run(self):
710 711 cmd = ['invoke', 'css']
711 712 if self.minify:
712 713 cmd.append('--minify')
713 714 if self.force:
714 715 cmd.append('--force')
715 check_call(cmd, cwd=pjoin(repo_root, "IPython", "html"))
716 try:
717 p = Popen(cmd, cwd=pjoin(repo_root, "IPython", "html"), stderr=PIPE)
718 except OSError:
719 raise DistutilsExecError("invoke is required to rebuild css (pip install invoke)")
720 out, err = p.communicate()
721 if p.returncode:
722 if sys.version_info[0] >= 3:
723 err = err.decode('utf8', 'replace')
724 raise DistutilsExecError(err.strip())
716 725
717 726
718 727 class JavascriptVersion(Command):
719 728 """write the javascript version to notebook javascript"""
720 729 description = "Write IPython version to javascript"
721 730 user_options = []
722 731
723 732 def initialize_options(self):
724 733 pass
725 734
726 735 def finalize_options(self):
727 736 pass
728 737
729 738 def run(self):
730 739 nsfile = pjoin(repo_root, "IPython", "html", "static", "base", "js", "namespace.js")
731 740 with open(nsfile) as f:
732 741 lines = f.readlines()
733 742 with open(nsfile, 'w') as f:
734 743 for line in lines:
735 744 if line.startswith("IPython.version"):
736 745 line = 'IPython.version = "{0}";\n'.format(version)
737 746 f.write(line)
738 747
739 748
740 749 def css_js_prerelease(command, strict=True):
741 750 """decorator for building js/minified css prior to a release"""
742 751 class DecoratedCommand(command):
743 752 def run(self):
744 753 self.distribution.run_command('jsversion')
745 754 css = self.distribution.get_command_obj('css')
746 755 css.minify = True
747 756 try:
748 757 self.distribution.run_command('css')
749 758 except Exception as e:
750 759 if strict:
751 760 raise
752 761 else:
753 log.warn("Failed to build css sourcemaps: %s" % e)
762 log.warn("rebuilding css and sourcemaps failed (not a problem)")
763 log.warn(str(e))
754 764 command.run(self)
755 765 return DecoratedCommand
General Comments 0
You need to be logged in to leave comments. Login now