From f9cb268e0d6bd3cc51c274bb83bbd6d5deeb49bd Mon Sep 17 00:00:00 2001 From: Min RK Date: Sat, 11 Apr 2015 16:19:00 -0700 Subject: [PATCH] add `setup.py js` from JupyterHub - fetches node tools locally with npm - runs bower - setup.py css uses local less as well, so no need to `npm install -g` anything. --- setup.py | 47 ++------------ setupbase.py | 178 ++++++++++++++++++++++++++++++++++----------------- 2 files changed, 126 insertions(+), 99 deletions(-) diff --git a/setup.py b/setup.py index 954ce30a6..1c05f26c8 100755 --- a/setup.py +++ b/setup.py @@ -53,11 +53,8 @@ from setupbase import ( find_packages, find_package_data, check_package_data_first, - check_submodule_status, - require_submodules, - update_submodules, - UpdateSubmodules, CompileCSS, + Bower, JavascriptVersion, css_js_prerelease, ) @@ -91,42 +88,6 @@ setup_args = dict( ) -#------------------------------------------------------------------------------- -# Make sure we aren't trying to run without submodules -#------------------------------------------------------------------------------- -here = os.path.abspath(os.path.dirname(__file__)) - -def require_clean_submodules(): - """Check on git submodules before distutils can do anything - - Since distutils cannot be trusted to update the tree - after everything has been set in motion, - this is not a distutils command. - """ - # PACKAGERS: Add a return here to skip checks for git submodules - - # don't do anything if nothing is actually supposed to happen - for do_nothing in ('-h', '--help', '--help-commands', 'clean', 'submodule'): - if do_nothing in sys.argv: - return - - status = check_submodule_status(here) - - if status == "missing": - print("checking out submodules for the first time") - update_submodules(here) - elif status == "unclean": - print('\n'.join([ - "Cannot build / install with unclean submodules", - "Please update submodules with", - " python setup.py submodule", - "or", - " git submodule update", - "or commit any submodule changes you have made." - ])) - sys.exit(1) - -require_clean_submodules() #--------------------------------------------------------------------------- # Find all the packages, package data, and data_files @@ -147,8 +108,8 @@ setup_args['cmdclass'] = { 'build_py': css_js_prerelease( check_package_data_first(build_py)), 'sdist' : css_js_prerelease(sdist), - 'submodule' : UpdateSubmodules, 'css' : CompileCSS, + 'js' : Bower, 'jsversion' : JavascriptVersion, } @@ -198,7 +159,9 @@ extras_require = { if 'setuptools' in sys.modules: # setup.py develop should check for submodules from setuptools.command.develop import develop - setup_args['cmdclass']['develop'] = require_submodules(develop) + setup_args['cmdclass']['develop'] = css_js_prerelease(develop) + if not PY3: + setup_args['setup_requires'] = ['ipython_genutils'] try: from wheel.bdist_wheel import bdist_wheel diff --git a/setupbase.py b/setupbase.py index 4111bf959..4629ca558 100644 --- a/setupbase.py +++ b/setupbase.py @@ -22,7 +22,7 @@ from distutils.cmd import Command from distutils.errors import DistutilsExecError from fnmatch import fnmatch from glob import glob -from subprocess import Popen, PIPE +from subprocess import Popen, PIPE, check_call #------------------------------------------------------------------------------- # Useful globals and utility functions @@ -175,56 +175,86 @@ def check_package_data_first(command): command.run(self) return DecoratedCommand - #--------------------------------------------------------------------------- -# VCS related +# Notebook related #--------------------------------------------------------------------------- -# utils.submodule has checks for submodule status -submodule = {} -execfile(pjoin('jupyter_notebook','submodule.py'), submodule) -check_submodule_status = submodule['check_submodule_status'] -update_submodules = submodule['update_submodules'] +try: + from shutil import which +except ImportError: + from ipython_genutils import which + +static = pjoin(repo_root, 'jupyter_notebook', 'static') + +npm_path = os.pathsep.join([ + pjoin(repo_root, 'node_modules', '.bin'), + os.environ.get("PATH", os.defpath), +]) -class UpdateSubmodules(Command): - """Update git submodules +def mtime(path): + """shorthand for mtime""" + return os.stat(path).st_mtime + +py3compat_ns = {} + +class Bower(Command): + description = "fetch static client-side components with bower" - The Notebook's external javascript dependencies live in a separate repo. - """ - description = "Update git submodules" - user_options = [] + user_options = [ + ('force', 'f', "force fetching of bower dependencies"), + ] def initialize_options(self): - pass + self.force = False def finalize_options(self): - pass + self.force = bool(self.force) + + bower_dir = pjoin(static, 'components') + node_modules = pjoin(repo_root, 'node_modules') + + def should_run(self): + if self.force: + return True + if not os.path.exists(self.bower_dir): + return True + return mtime(self.bower_dir) < mtime(pjoin(repo_root, 'bower.json')) + + def should_run_npm(self): + if not which('npm'): + print("npm unavailable", file=sys.stderr) + return False + if not os.path.exists(self.node_modules): + return True + return mtime(self.node_modules) < mtime(pjoin(repo_root, 'package.json')) def run(self): - try: - self.spawn('git submodule init'.split()) - self.spawn('git submodule update --recursive'.split()) - except Exception as e: - print(e) + if not self.should_run(): + print("bower dependencies up to date") + return - if not check_submodule_status(repo_root) == 'clean': - print("submodules could not be checked out") - sys.exit(1) - - -def require_submodules(command): - """decorator for instructing a command to check for submodules before running""" - class DecoratedCommand(command): - def run(self): - if not check_submodule_status(repo_root) == 'clean': - print("submodules missing! Run `setup.py submodule` and try again") - sys.exit(1) - command.run(self) - return DecoratedCommand + if self.should_run_npm(): + print("installing build dependencies with npm") + check_call(['npm', 'install'], cwd=repo_root) + os.utime(self.node_modules) + + env = os.environ.copy() + env['PATH'] = npm_path + + try: + check_call( + ['bower', 'install', '--allow-root', '--config.interactive=false'], + cwd=repo_root, + env=env, + ) + except OSError as e: + print("Failed to run bower: %s" % e, file=sys.stderr) + print("You can install js dependencies with `npm install`", file=sys.stderr) + raise + os.utime(self.bower_dir) + # update package data in case this created new files + self.distribution.package_data = find_package_data() -#--------------------------------------------------------------------------- -# Notebook related -#--------------------------------------------------------------------------- class CompileCSS(Command): """Recompile Notebook CSS @@ -235,33 +265,65 @@ class CompileCSS(Command): """ description = "Recompile Notebook CSS" user_options = [ - ('minify', 'x', "minify CSS"), ('force', 'f', "force recompilation of CSS"), ] def initialize_options(self): - self.minify = False self.force = False def finalize_options(self): - self.minify = bool(self.minify) self.force = bool(self.force) - def run(self): - cmd = ['invoke', 'css'] - if self.minify: - cmd.append('--minify') + def should_run(self): + """Does less need to run?""" if self.force: - cmd.append('--force') - try: - p = Popen(cmd, cwd=pjoin(repo_root, "jupyter_notebook"), stderr=PIPE) - except OSError: - raise DistutilsExecError("invoke is required to rebuild css (pip install invoke)") - out, err = p.communicate() - if p.returncode: - if sys.version_info[0] >= 3: - err = err.decode('utf8', 'replace') - raise DistutilsExecError(err.strip()) + return True + + css_targets = [pjoin(static, 'css', '%s.min.css' % name) for name in ('ipython', 'style')] + css_maps = [t + '.map' for t in css_targets] + targets = css_targets + css_maps + if not all(os.path.exists(t) for t in targets): + # some generated files don't exist + return True + earliest_target = sorted(mtime(t) for t in targets)[0] + + # check if any .less files are newer than the generated targets + for (dirpath, dirnames, filenames) in os.walk(static): + for f in filenames: + if f.endswith('.less'): + path = pjoin(static, dirpath, f) + timestamp = mtime(path) + if timestamp > earliest_target: + return True + + return False + + def run(self): + if not self.should_run(): + print("CSS up-to-date") + return + + self.run_command('js') + env = os.environ.copy() + env['PATH'] = npm_path + for name in ('ipython', 'style'): + less = pjoin(static, 'style', '%s.less' % name) + css = pjoin(static, 'style', '%s.min.css' % name) + sourcemap = css + '.map' + try: + check_call([ + 'lessc', '--clean-css', + '--source-map-basepath={}'.format(static), + '--source-map={}'.format(sourcemap), + '--source-map-rootpath=../', + less, css, + ], cwd=repo_root, env=env) + except OSError as e: + print("Failed to run lessc: %s" % e, file=sys.stderr) + print("You can install js dependencies with `npm install`", file=sys.stderr) + raise + # update package data in case this created new files + self.distribution.package_data = find_package_data() class JavascriptVersion(Command): @@ -291,12 +353,14 @@ class JavascriptVersion(Command): def css_js_prerelease(command): - """decorator for building js/minified css prior to a release""" + """decorator for building js/minified css prior to another command""" class DecoratedCommand(command): def run(self): self.distribution.run_command('jsversion') + js = self.distribution.get_command_obj('js') + js.force = True css = self.distribution.get_command_obj('css') - css.minify = True + css.force = True try: self.distribution.run_command('css') except Exception as e: