author Rajesh Kathiriya <>
Wed, 17 May 2017 23:07:21 +0530
changeset 360829 a709c2aec00c22ab4555f650f2990c465006b1dd
parent 358622 23e285c5aa7068672d2bf025b23fdeee0cd4f06b
child 361315 21941d086084a51bb24ee7d548c00fa37618f09b
permissions -rw-r--r--
bug 1346994 - Updated nodejs not found message r=standard8 MozReview-Commit-ID: AKZyW9HPvmY

# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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

from filecmp import dircmp
import json
import os
import platform
import re
import subprocess
import sys
from distutils.version import LooseVersion
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "..", "python", "which"))
import which

nodejs is out of date. You currently have node %s but v6.9.1 is required.
Please update nodejs from and try again.

nodejs is either not installed or is installed to a non-standard path.
Please install nodejs from and try again.

Valid installation paths:

Node Package Manager (npm) is either not installed or installed to a
non-standard path. Please install npm from (it comes as an
option in the node installation) and try again.

Valid installation paths:

VERSION_RE = re.compile(r"^\d+\.\d+\.\d+$")
CARET_VERSION_RANGE_RE = re.compile(r"^\^((\d+)\.\d+\.\d+)$")

LINTED_EXTENSIONS = ['js', 'jsm', 'jsx', 'xml', 'html', 'xhtml']
EXTENSIONS_RE = re.compile(r'.+\.(?:%s)$' % '|'.join(LINTED_EXTENSIONS))

project_root = None

def eslint_setup():
    """Ensure eslint is optimally configured.

    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()

    module_path = get_eslint_module_path()

    # npm sometimes fails to respect cwd when it is run using check_call so
    # we manually switch folders here instead.

    npm_path = get_node_or_npm_path("npm")
    if not npm_path:
        return 1

    extra_parameters = ["--loglevel=error"]

    # Install ESLint and external plugins
    cmd = [npm_path, "install"]
    print("Installing eslint for mach using \"%s\"..." % (" ".join(cmd)))
    if not call_process("eslint", cmd):
        return 1

    # Install in-tree ESLint plugin mozilla.
    cmd = [npm_path, "install",
           os.path.join(module_path, "eslint-plugin-mozilla")]
    print("Installing eslint-plugin-mozilla using \"%s\"..." % (" ".join(cmd)))
    if not call_process("eslint-plugin-mozilla", cmd):
        return 1

    # Install in-tree ESLint plugin spidermonkey.
    cmd = [npm_path, "install",
           os.path.join(module_path, "eslint-plugin-spidermonkey-js")]
    print("Installing eslint-plugin-spidermonkey-js using \"%s\"..." % (" ".join(cmd)))
    if not call_process("eslint-plugin-spidermonkey-js", cmd):
        return 1

    eslint_path = os.path.join(get_project_root(), "node_modules", ".bin", "eslint")

    print("\nESLint and approved plugins installed successfully!")
    print("\nNOTE: Your local eslint binary is at %s\n" % eslint_path)


def call_process(name, cmd, cwd=None):
        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))
            print("\nError installing %s, aborting." % name)

        return False

    return True

def expected_eslint_modules():
    # Read the expected version of ESLint and external modules
    expected_modules_path = os.path.join(get_project_root(), "package.json")
    with open(expected_modules_path, "r") as f:
        expected_modules = json.load(f)["dependencies"]

    # Also read the in-tree ESLint plugin mozilla information.
    mozilla_json_path = os.path.join(get_eslint_module_path(),
                                     "eslint-plugin-mozilla", "package.json")
    with open(mozilla_json_path, "r") as f:
        expected_modules["eslint-plugin-mozilla"] = json.load(f)

    # Also read the in-tree ESLint plugin spidermonkey information.
    mozilla_json_path = os.path.join(get_eslint_module_path(),
                                     "eslint-plugin-spidermonkey-js", "package.json")
    with open(mozilla_json_path, "r") as f:
        expected_modules["eslint-plugin-spidermonkey-js"] = json.load(f)

    return expected_modules

def check_eslint_files(node_modules_path, name):
    def check_file_diffs(dcmp):
        # Diff files only looks at files that are different. Not for files
        # that are only present on one side. This should be generally OK as
        # new files will need to be added in the index.js for the package.
        if dcmp.diff_files and dcmp.diff_files != ['package.json']:
            return True

        result = False

        # Again, we only look at common sub directories for the same reason
        # as above.
        for sub_dcmp in dcmp.subdirs.values():
            result = result or check_file_diffs(sub_dcmp)

        return result

    dcmp = dircmp(os.path.join(node_modules_path, name),
                  os.path.join(get_eslint_module_path(), name))

    return check_file_diffs(dcmp)

def eslint_module_needs_setup():
    has_issues = False
    node_modules_path = os.path.join(get_project_root(), "node_modules")

    for name, expected_data in expected_eslint_modules().iteritems():
        # expected_eslint_modules returns a string for the version number of
        # dependencies for installation of eslint generally, and an object
        # for our in-tree plugins (which contains the entire module info).
        if "version" in expected_data:
            version_range = expected_data["version"]
            version_range = expected_data

        path = os.path.join(node_modules_path, name, "package.json")

        if not os.path.exists(path):
            print("%s v%s needs to be installed locally." % (name, version_range))
            has_issues = True

        data = json.load(open(path))

        if not version_in_range(data["version"], version_range):
            print("%s v%s should be v%s." % (name, data["version"], version_range))
            has_issues = True

        if name == "eslint-plugin-mozilla" or name == "eslint-plugin-spidermonkey-js":
            # For our in-tree modules, check that package.json has the same dependencies.
            if (cmp(data["dependencies"], expected_data["dependencies"]) != 0 or
                cmp(data["devDependencies"], expected_data["devDependencies"]) != 0):
                print("Dependencies of %s differ." % (name))
                has_issues = True

            # We also need to check the files themselves in case one changed without
            # the version number being updated.
            if check_eslint_files(node_modules_path, name):
                print("%s has out of-date files." % (name))
                has_issues = True

    return has_issues

def version_in_range(version, version_range):
    Check if a module version is inside a version range.  Only supports explicit versions and
    caret ranges for the moment, since that's all we've used so far.
    if version == version_range:
        return True

    version_match = VERSION_RE.match(version)
    if not version_match:
        raise RuntimeError("mach eslint doesn't understand module version %s" % version)
    version = LooseVersion(version)

    # Caret ranges as specified by npm allow changes that do not modify the left-most non-zero
    # digit in the [major, minor, patch] tuple.  The code below assumes the major digit is
    # non-zero.
    range_match = CARET_VERSION_RANGE_RE.match(version_range)
    if range_match:
        range_version =
        range_major = int(

        range_min = LooseVersion(range_version)
        range_max = LooseVersion("%d.0.0" % (range_major + 1))

        return range_min <= version < range_max

    return False

def get_possible_node_paths_win():
    Return possible nodejs paths on Windows.
    if platform.system() != "Windows":
        return []

    return list({
        "%s\\nodejs" % os.environ.get("SystemDrive"),
        os.path.join(os.environ.get("ProgramFiles"), "nodejs"),
        os.path.join(os.environ.get("PROGRAMW6432"), "nodejs"),
        os.path.join(os.environ.get("PROGRAMFILES"), "nodejs")

def which_path(filename):
    Return the nodejs or npm path.
    path = None

    if platform.system() == "Windows":
        for ext in [".cmd", ".exe", ""]:
                path = which.which(filename + ext, path=get_possible_node_paths_win())
            except which.WhichError:
            path = which.which(filename)
        except which.WhichError:
            if filename == "node":
                # Retry it with "nodejs" as Linux tends to prefer nodejs rather than node.
                return which_path("nodejs")

    return path

def get_node_or_npm_path(filename, minversion=None):
    node_or_npm_path = which_path(filename)

    if not node_or_npm_path:
        if filename in ('node', 'nodejs'):
        elif filename == "npm":

        if platform.system() == "Windows":
            app_paths = get_possible_node_paths_win()

            for p in app_paths:
                print("  - %s" % p)
        elif platform.system() == "Darwin":
            print("  - /usr/local/bin/{}".format(filename))
        elif platform.system() == "Linux":
            print("  - /usr/bin/{}".format(filename))

        return None

    if not minversion:
        return node_or_npm_path

    version_str = get_version(node_or_npm_path)

    version = LooseVersion(version_str.lstrip('v'))

    if version > minversion:
        return node_or_npm_path

    print(NODE_MACHING_VERSION_NOT_FOUND_MESSAGE % version_str.strip())

    return None

def get_version(path):
        version_str = subprocess.check_output([path, "--version"],
        return version_str
    except (subprocess.CalledProcessError, OSError):
        return None

def set_project_root(root=None):
    """Sets the project root to the supplied path, or works out what the root
    is based on looking for 'mach'.

    Keyword arguments:
    root - (optional) The path to set the root to.
    global project_root

    if root:
        project_root = root

    file_found = False
    folder = os.getcwd()

    while (folder):
        if os.path.exists(os.path.join(folder, 'mach')):
            file_found = True
            folder = os.path.dirname(folder)

    if file_found:
        project_root = os.path.abspath(folder)

def get_project_root():
    """Returns the absolute path to the root of the project, see set_project_root()
    for how this is determined.
    global project_root

    if not project_root:

    return project_root

def get_eslint_module_path():
    return os.path.join(get_project_root(), "tools", "lint", "eslint")

def check_node_executables_valid():
    # eslint requires at least node 6.9.1
    node_path = get_node_or_npm_path("node", LooseVersion("6.9.1"))
    if not node_path:
        return False

    npm_path = get_node_or_npm_path("npm")
    if not npm_path:
        return False

    return True