Repository: ipython/ipython

Issue:
Mention new dev requirements somewhere in the docs (or make more prominent)
While trying to build the master branch of ipython, I noted the following warning:

```
Failed to build css sourcemaps: [Errno 2] No such file or directory
```

Tracking this down in the source code, I eventually realized that this is because I don't have `lessc` or `invoke`.  I figured out what LESS is in this context, but still am not sure what `invoke` is supposed to be.  So it would be helpful if this were mentioned somewhere quite prominent in the dev docs.



Discussion:
Hi, [invoke](https://pypi.python.org/pypi/invoke/0.9.0) is the part of [fabric](https://pypi.python.org/pypi/Fabric/1.10.1) that only run local task. You can install install it with pip. 

Though you should not need it to build for local instal the css should be already bundled. 
If you are just doing a local install you can probably do a `python setup.py symlink`, IIRC and  *nix only.
and it should work. 

I had to install `lessc` some months ago to be able to run `python setup.py install` without seeing errors.

Besides, `lessc` always touches some sourcemaps in the tree, so I have to revert those changes too before pulling next time.

Note that 'seeing errors' is different from there being any problem. Without invoke, you will see this:

> Failed to build css sourcemaps: [Errno 2] No such file or directory: 'invoke'

Which means just what it says: you won't have css sourcemaps. This is not a problem, and installation succeeds just fine. You do not need less or invoke to install or run IPython, only to actively develop the LESS source files.

Should we add to the "Failed to build" message an explicit "this doesn't matter"? Having invoke and lacking less looks like a real error, since it shows a traceback, but it's similarly inconsequential. Any actual error would prevent installation from succeeding.


Resolving patch:
diff --git a/IPython/html/tasks.py b/IPython/html/tasks.py
--- a/IPython/html/tasks.py
+++ b/IPython/html/tasks.py
@@ -1,10 +1,16 @@
 """invoke task file to build CSS"""
 
-from invoke import task, run
+from __future__ import print_function
+
 import os
+import sys
 from distutils.version import LooseVersion as V
 from subprocess import check_output
 
+from invoke import task, run
+from invoke.runner import Result
+from invoke.exceptions import Failure
+
 pjoin = os.path.join
 static_dir = 'static'
 components_dir = pjoin(static_dir, 'components')
@@ -13,6 +19,18 @@
 min_less_version = '2.0'
 max_less_version = '3.0' # exclusive if string
 
+
+def _fail(msg=''):
+    """Fail a task, logging a message to stderr
+    
+    raises a special Failure Exception from invoke.
+    """
+    if msg:
+        print(msg, file=sys.stderr)
+    # raising a Failure allows us to avoid a traceback
+    # we only care about exited, but stdout, stderr, pty are required args
+    raise Failure(Result(stdout='', stderr='', pty=False, exited=1))
+
 def _need_css_update():
     """Does less need to run?"""
     
@@ -53,23 +71,30 @@ def css(minify=False, verbose=False, force=False):
         sourcemap = pjoin('style', "%s.min.css.map" % name)
         _compile_less(source, target, sourcemap, minify, verbose)
 
+
 def _compile_less(source, target, sourcemap, minify=True, verbose=False):
     """Compile a less file by source and target relative to static_dir"""
     min_flag = '-x' if minify else ''
     ver_flag = '--verbose' if verbose else ''
     
+    install = "(npm install -g 'less@<{}')".format(max_less_version)
     # pin less to version number from above
     try:
         out = check_output(['lessc', '--version'])
     except OSError as err:
-        raise ValueError("Unable to find lessc.  Please install lessc >= %s and < %s " \
-                         % (min_less_version, max_less_version))
+        _fail("Unable to find lessc.  Rebuilding css requires less >= {0} and < {1} {2}".format(
+            min_less_version, max_less_version, install
+        ))
     out = out.decode('utf8', 'replace')
     less_version = out.split()[1]
     if min_less_version and V(less_version) < V(min_less_version):
-        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))
+        _fail("lessc too old: {} < {} {}".format(
+            less_version, min_less_version, install,
+        ))
     if max_less_version and V(less_version) >= V(max_less_version):
-        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))
+        _fail("lessc too new: {} >= {} {}".format(
+            less_version, max_less_version, install,
+        ))
     
     static_path = pjoin(here, static_dir)
     cwd = os.getcwd()
diff --git a/setupbase.py b/setupbase.py
--- a/setupbase.py
+++ b/setupbase.py
@@ -24,9 +24,10 @@
 from distutils.command.install import install
 from distutils.command.install_scripts import install_scripts
 from distutils.cmd import Command
+from distutils.errors import DistutilsExecError
 from fnmatch import fnmatch
 from glob import glob
-from subprocess import check_call
+from subprocess import Popen, PIPE
 
 from setupext import install_data_ext
 
@@ -712,7 +713,15 @@ def run(self):
             cmd.append('--minify')
         if self.force:
             cmd.append('--force')
-        check_call(cmd, cwd=pjoin(repo_root, "IPython", "html"))
+        try:
+            p = Popen(cmd, cwd=pjoin(repo_root, "IPython", "html"), 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())
 
 
 class JavascriptVersion(Command):
@@ -750,6 +759,7 @@ def run(self):
                 if strict:
                     raise
                 else:
-                    log.warn("Failed to build css sourcemaps: %s" % e)
+                    log.warn("rebuilding css and sourcemaps failed (not a problem)")
+                    log.warn(str(e))
             command.run(self)
     return DecoratedCommand


Test patch: