Bug 1270851 - mach eslint should install eslint and their dependencies if not installed r=gps
authorMichael Ratcliffe <mratcliffe@mozilla.com>
Fri, 13 May 2016 14:06:08 +0100
changeset 297720 6e2add233b54766d44568f81aa207c624bbe4437
parent 297719 b4564c057f93c736521672ffa54b23d9b4c92b94
child 297721 f3f2fa1d7eed5a8262f6401ef18ff8117a3ce43e
child 297722 c11343f547559a1fc96da1e5d36ca3db82cd84d5
push id30265
push userkwierso@gmail.com
push dateTue, 17 May 2016 21:15:15 +0000
treeherdermozilla-central@f3f2fa1d7eed [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgps
bugs1270851
milestone49.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1270851 - mach eslint should install eslint and their dependencies if not installed r=gps
python/mach_commands.py
testing/eslint/package.json
--- a/python/mach_commands.py
+++ b/python/mach_commands.py
@@ -1,16 +1,17 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import, print_function, unicode_literals
 
 import __main__
 import argparse
+import json
 import logging
 import mozpack.path as mozpath
 import os
 import platform
 import subprocess
 import sys
 import which
 from distutils.version import LooseVersion
@@ -20,34 +21,27 @@ from mozbuild.base import (
 )
 
 from mach.decorators import (
     CommandArgument,
     CommandProvider,
     Command,
 )
 
-ESLINT_VERSION = "2.9.0"
+ESLINT_PACKAGES = [
+    "eslint@2.9.0",
+    "eslint-plugin-html@1.4.0",
+    "eslint-plugin-mozilla@0.0.3",
+    "eslint-plugin-react@4.2.3"
+]
 
 ESLINT_NOT_FOUND_MESSAGE = '''
 Could not find eslint!  We looked at the --binary option, at the ESLINT
-environment variable, and then at your path.  Install eslint and needed plugins
-with
-
-mach eslint --setup
-
-and try again.
-'''.strip()
-
-ESLINT_OUTDATED_MESSAGE = '''
-eslint in your path is outdated.
-  path: %(binary)s
-  version: %(version)s
-Expected version: %(min_version)s
-Update eslint with
+environment variable, and then at your local node_modules path. Please Install
+eslint and needed plugins with:
 
 mach eslint --setup
 
 and try again.
 '''.strip()
 
 NODE_NOT_FOUND_MESSAGE = '''
 nodejs v4.2.3 is either not installed or is installed to a non-standard path.
@@ -212,57 +206,63 @@ class MachCommands(MachCommandBase):
     @CommandArgument('-e', '--ext', default='[.js,.jsm,.jsx,.xml,.html]',
         help='Filename extensions to lint, default: "[.js,.jsm,.jsx,.xml,.html]".')
     @CommandArgument('-b', '--binary', default=None,
         help='Path to eslint binary.')
     @CommandArgument('args', nargs=argparse.REMAINDER)  # Passed through to eslint.
     def eslint(self, setup, ext=None, binary=None, args=None):
         '''Run eslint.'''
 
+        module_path = self.get_eslint_module_path()
+
         # eslint requires at least node 4.2.3
         nodePath = self.getNodeOrNpmPath("node", LooseVersion("4.2.3"))
         if not nodePath:
             return 1
 
         if setup:
             return self.eslint_setup()
 
+        npmPath = self.getNodeOrNpmPath("npm")
+        if not npmPath:
+            return 1
+
+        if self.eslintModuleHasIssues():
+            install = self._prompt_yn("\nContinuing will automatically fix "
+                                      "these issues. Would you like to "
+                                      "continue")
+            if install:
+                self.eslint_setup()
+            else:
+                return 1
+
+        # Valid binaries are:
+        #  - Any provided by the binary argument.
+        #  - Any pointed at by the ESLINT environmental variable.
+        #  - Those provided by mach eslint --setup.
+        #
+        #  eslint --setup installs some mozilla specific plugins and installs
+        #  all node modules locally. This is the preferred method of
+        #  installation.
+
         if not binary:
             binary = os.environ.get('ESLINT', None)
+
             if not binary:
-                try:
-                    binary = which.which('eslint')
-                except which.WhichError:
-                    npmPath = self.getNodeOrNpmPath("npm")
-                    if npmPath:
-                        try:
-                            output = subprocess.check_output([npmPath, "bin"],
-                                                             stderr=subprocess.STDOUT)
-                            if output:
-                                base = output.split("\n")[0].strip()
-                                binary = os.path.join(base, "eslint")
-                                if not os.path.isfile(binary):
-                                    binary = None
-                        except (subprocess.CalledProcessError, OSError):
-                            pass
+                binary = os.path.join(module_path, "node_modules", ".bin", "eslint")
+                if not os.path.isfile(binary):
+                    binary = None
 
         if not binary:
             print(ESLINT_NOT_FOUND_MESSAGE)
             return 1
 
         self.log(logging.INFO, 'eslint', {'binary': binary, 'args': args},
             'Running {binary}')
 
-        version_str = subprocess.check_output([binary, "--version"],
-                                              stderr=subprocess.STDOUT)
-        version = LooseVersion(version_str.lstrip('v'))
-        if version < LooseVersion(ESLINT_VERSION):
-            print (ESLINT_OUTDATED_MESSAGE % {"binary": binary, "version": version_str, "min_version": ESLINT_VERSION})
-            return 1
-
         args = args or ['.']
 
         cmd_args = [binary,
                     # Enable the HTML plugin.
                     # We can't currently enable this in the global config file
                     # because it has bad interactions with the SublimeText
                     # ESLint plugin (bug 1229874).
                     '--plugin', 'html',
@@ -284,79 +284,136 @@ class MachCommands(MachCommandBase):
 
         This command will inspect your eslint configuration and
         guide you through an interactive wizard helping you configure
         eslint for optimal use on Mozilla projects.
         """
         orig_cwd = os.getcwd()
         sys.path.append(os.path.dirname(__file__))
 
-        root = self.get_project_root()
-        module_path = root + "/testing/eslint"
+        module_path = self.get_eslint_module_path()
 
         # npm sometimes fails to respect cwd when it is run using check_call so
         # we manually switch folders here instead.
         os.chdir(module_path)
 
         npmPath = self.getNodeOrNpmPath("npm")
         if not npmPath:
             return 1
 
-        # eslint-plugin-mozilla should **never** be installed (linked) globally.
-        # Let's unlink it just in case.
-        self.callProcess("remove-eslint-plugin-mozilla",
-                         [npmPath, "unlink", "eslint-plugin-mozilla"])
-
-        # Install eslint.
-        # Note that that's the version currently compatible with the mozilla
-        # eslint plugin.
-        success = self.callProcess("eslint",
-                                   [npmPath, "install", "eslint@2.9.0"])
-        if not success:
-            return 1
+        # Install eslint and necessary plugins.
+        for pkg in ESLINT_PACKAGES:
+            name, version = pkg.split("@")
+            success = False
 
-        # Install eslint-plugin-mozilla.
-        success = self.callProcess("eslint-plugin-mozilla",
-                                   [npmPath, "install", os.path.join(module_path, "eslint-plugin-mozilla")])
-        if not success:
-            return 1
+            if self.node_package_installed(pkg, cwd=module_path):
+                success = True
+            else:
+                if pkg.startswith("eslint-plugin-mozilla"):
+                    cmd = [npmPath, "install",
+                           os.path.join(module_path, "eslint-plugin-mozilla")]
+                else:
+                    cmd = [npmPath, "install", pkg]
 
-        # Install eslint-plugin-html.
-        success = self.callProcess("eslint-plugin-html",
-                                   [npmPath, "install", "eslint-plugin-html@1.4.0"])
-        if not success:
-            return 1
+                print("Installing %s v%s using \"%s\"..."
+                      % (name, version, " ".join(cmd)))
+                success = self.callProcess(pkg, cmd)
 
-        # Install eslint-plugin-react.
-        success = self.callProcess("eslint-plugin-react",
-                                   [npmPath, "install", "eslint-plugin-react@4.2.3"])
-        if not success:
-            return 1
+            if not success:
+                return 1
+
+        eslint_path = os.path.join(module_path, "node_modules", ".bin", "eslint")
 
         print("\nESLint and approved plugins installed successfully!")
-        print("\nNOTE: Your local eslint binary is at %s/node_modules/.bin/eslint\n" % module_path)
+        print("\nNOTE: Your local eslint binary is at %s\n" % eslint_path)
 
         os.chdir(orig_cwd)
 
     def callProcess(self, name, cmd, cwd=None):
-        print("\nInstalling %s using \"%s\"..." % (name, " ".join(cmd)))
-
         try:
             with open(os.devnull, "w") as fnull:
                 subprocess.check_call(cmd, cwd=cwd, stdout=fnull)
         except subprocess.CalledProcessError:
             if cwd:
                 print("\nError installing %s in the %s folder, aborting." % (name, cwd))
             else:
                 print("\nError installing %s, aborting." % name)
 
             return False
 
         return True
 
+    def eslintModuleHasIssues(self):
+        print("Checking eslint and modules...")
+
+        has_issues = False
+        npmPath = self.getNodeOrNpmPath("npm")
+        module_path = self.get_eslint_module_path()
+
+        for pkg in ESLINT_PACKAGES:
+            name, req_version = pkg.split("@")
+
+            try:
+                with open(os.devnull, "w") as fnull:
+                    global_install = subprocess.check_output([npmPath, "ls", "--json", name, "-g"],
+                                                             stderr=fnull)
+                info = json.loads(global_install)
+                global_version = info["dependencies"][name]["version"]
+            except subprocess.CalledProcessError:
+                global_version = None
+
+            try:
+                with open(os.devnull, "w") as fnull:
+                    local_install = subprocess.check_output([npmPath, "ls", "--json", name],
+                                                            cwd=module_path, stderr=fnull)
+                info = json.loads(local_install)
+                local_version = info["dependencies"][name]["version"]
+            except subprocess.CalledProcessError:
+                local_version = None
+
+            if global_version:
+                if name == "eslint-plugin-mozilla":
+                    print("%s should never be installed globally. This global "
+                          "module will be removed." % name)
+                    has_issues = True
+                else:
+                    print("%s is installed globally. This global module will "
+                          "be ignored. We recommend uninstalling it using "
+                          "sudo %s remove %s -g" % (name, npmPath, name))
+            if local_version:
+                if local_version != req_version:
+                    print("%s v%s is installed locally but is not the "
+                          "required version (v%s). This module will be "
+                          "reinstalled so that the versions match." %
+                          (name, local_version, req_version))
+                    has_issues = True
+            else:
+                print("%s v%s is not installed locally and only local modules "
+                      "are valid. This module will be installed locally."
+                      % (name, req_version))
+                has_issues = True
+
+        return has_issues
+
+    def node_package_installed(self, package_name="", globalInstall=False, cwd=None):
+        try:
+            npmPath = self.getNodeOrNpmPath("npm")
+
+            cmd = [npmPath, "ls", "--parseable", package_name]
+
+            if globalInstall:
+                cmd.append("-g")
+
+            with open(os.devnull, "w") as fnull:
+                subprocess.check_call(cmd, stdout=fnull, stderr=fnull, cwd=cwd)
+
+            return True
+        except subprocess.CalledProcessError:
+            return False
+
     def getPossibleNodePathsWin(self):
         """
         Return possible nodejs paths on Windows.
         """
         if platform.system() != "Windows":
             return []
 
         return list({
@@ -414,8 +471,31 @@ class MachCommands(MachCommandBase):
                 return version >= minversion
             return True
         except (subprocess.CalledProcessError, OSError):
             return False
 
     def get_project_root(self):
         fullpath = os.path.abspath(sys.modules['__main__'].__file__)
         return os.path.dirname(fullpath)
+
+    def get_eslint_module_path(self):
+        return os.path.join(self.get_project_root(), "testing", "eslint")
+
+    def _prompt_yn(self, msg):
+        if not sys.stdin.isatty():
+            return False
+
+        print('%s? [Y/n]' % msg)
+
+        while True:
+            choice = raw_input().lower().strip()
+
+            if not choice:
+                return True
+
+            if choice in ('y', 'yes'):
+                return True
+
+            if choice in ('n', 'no'):
+                return False
+
+            print('Must reply with one of {yes, no, y, n}.')
new file mode 100644
--- /dev/null
+++ b/testing/eslint/package.json
@@ -0,0 +1,12 @@
+{
+  "name": "",
+  "description": "None",
+  "repository": {},
+  "license": "MPL-2.0",
+  "dependencies": {
+    "eslint": "*",
+    "eslint-plugin-html": "*",
+    "eslint-plugin-mozilla": "*",
+    "eslint-plugin-react": "*"
+  }
+}