create_venv: handle missing virtualenv better

Touch up the venvlib exceptions and have ENOENT errors from running
virtualenv throw the more specific VirtualenvMissingError.  Then we
can catch that at the top level to emit a short explanation for the
user to fix things (by installing virtualenv). rather than dumping
large tracebacks & logs that are meaningless to them.

BUG=chromium:1109615
TEST=`./run_pytest -h` w/out virtualenv shows a short error message

Change-Id: I8da8e51bdc995bbbc955bd1e47d704b5e81fc0d4
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/infra_virtualenv/+/2319393
Reviewed-by: Chris McDonald <cjmcdonald@chromium.org>
Commit-Queue: Mike Frysinger <vapier@chromium.org>
Tested-by: Mike Frysinger <vapier@chromium.org>
diff --git a/venv/cros_venv/scripts/create_venv.py b/venv/cros_venv/scripts/create_venv.py
index b7d0ee6..49ef0f7 100644
--- a/venv/cros_venv/scripts/create_venv.py
+++ b/venv/cros_venv/scripts/create_venv.py
@@ -16,6 +16,7 @@
 
 import argparse
 import logging
+import os
 import sys
 
 from cros_venv import venvlib
@@ -36,6 +37,10 @@
     try:
         print(venv.ensure())
         logging.debug('Log file can be found at: %s', venv.logfile)
+    except venvlib.VirtualenvMissingError as e:
+        print('%s: error: %s' % (os.path.basename(sys.argv[0]), e),
+              file=sys.stderr)
+        sys.exit(1)
     except Exception:
         logging.error('Creating venv with reqs file "%s" failed',
                       args.reqs_file, exc_info=True)
diff --git a/venv/cros_venv/venvlib.py b/venv/cros_venv/venvlib.py
index 0464bb2..f55c6f2 100644
--- a/venv/cros_venv/venvlib.py
+++ b/venv/cros_venv/venvlib.py
@@ -10,6 +10,7 @@
 
 import collections
 import distutils.util
+import errno
 import functools
 import hashlib
 import itertools
@@ -36,6 +37,10 @@
 _BASE_DEPENDENCIES = ('setuptools==44.0.0', 'pip==20.0.2')
 
 
+class Error(Exception):
+    """Base exception for all errors in this module."""
+
+
 class _VenvPaths(object):
 
     """Wrapper defining paths inside a versioned virtualenv."""
@@ -167,7 +172,7 @@
             return _load_spec(f)
 
 
-class SpecMismatchError(Exception):
+class SpecMismatchError(Error):
     """Versioned virtualenv specs do not match."""
 
 
@@ -230,11 +235,11 @@
     try:
       result = subprocess.check_output([_VIRTUALENV_COMMAND, '--version'],
                                        stderr=subprocess.STDOUT)
-    except OSError:
-      raise EnvironmentError(
-          'Error executing virtualenv, make sure that you install '
-          'virtualenv first (see '
-          'https://virtualenv.pypa.io/en/latest/installation.html)')
+    except OSError as e:
+      if e.errno == errno.ENOENT:
+          raise VirtualenvMissingError(e)
+      else:
+          raise
 
     # This strips off "rc" and related tags, but we don't care for our use.
     m = re.match(br'^(virtualenv *)?([0-9.]+)', result)
@@ -264,7 +269,7 @@
         raise VirtualenvMissingError(e)
 
 
-class VirtualenvMissingError(Exception):
+class VirtualenvMissingError(Error):
     """Virtualenv is missing."""
 
     def __init__(self, cause):