python/mozbuild/mozbuild/nodeutil.py
author Mark Banner <standard8@mozilla.com>
Tue, 25 Sep 2018 18:15:51 +0000
changeset 438163 951f04d1bb518e722e13b2a15580921590eb2b44
child 448229 f851c3e82c2bad0afd8a2460245c9054c908e79f
permissions -rw-r--r--
Bug 1482435 - Separate out nodejs finding logic from configure and use it for ESLint. r=firefox-build-system-reviewers,gps This extracts the current logic for finding nodejs into its own module in mozbuild. Configure and ESLint then use it. For ESLint, this will change the first location it looks for nodejs to be the .mozbuild directory. Differential Revision: https://phabricator.services.mozilla.com/D6430

# 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

import os
import subprocess
import platform
from mozboot.util import get_state_dir
import which

from distutils.version import (
    StrictVersion,
)

NODE_MIN_VERSION = StrictVersion("8.11.0")
NPM_MIN_VERSION = StrictVersion("5.6.0")


def find_node_paths():
    """ Determines the possible paths for node executables.

    Returns a list of paths, which includes the build state directory.
    """
    # Also add in the location to which `mach bootstrap` or
    # `mach artifact toolchain` installs clang.
    mozbuild_state_dir, _ = get_state_dir()

    if platform.system() == "Windows":
        mozbuild_node_path = os.path.join(mozbuild_state_dir, 'node')
    else:
        mozbuild_node_path = os.path.join(mozbuild_state_dir, 'node', 'bin')

    # We still fallback to the PATH, since on OSes that don't have toolchain
    # artifacts available to download, Node may be coming from $PATH.
    paths = [mozbuild_node_path] + os.environ.get('PATH').split(os.pathsep)

    if platform.system() == "Windows":
        paths += [
            "%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")
        ]

    return paths


def check_executable_version(exe):
    """Determine the version of a Node executable by invoking it.

    May raise ``subprocess.CalledProcessError`` or ``ValueError`` on failure.
    """
    out = subprocess.check_output([exe, "--version"]).lstrip('v').rstrip()
    return StrictVersion(out)


def simple_which(filename, path=None):
    # Note: On windows, npm uses ".cmd"
    exts = [".cmd", ".exe", ""] if platform.system() == "Windows" else [""]

    for ext in exts:
        try:
            return which.which(filename + ext, path)
        except which.WhichError:
            pass

    # If we got this far, we didn't find it with any of the extensions, so
    # just return.
    return None


def find_node_executable(nodejs_exe=os.environ.get('NODEJS'), min_version=NODE_MIN_VERSION):
    """Find a Node executable from the mozbuild directory.

    Returns a tuple containing the the path to an executable binary and a
    version tuple. Both tuple entries will be None if a Node executable
    could not be resolved.
    """
    if nodejs_exe:
        try:
            version = check_executable_version(nodejs_exe)
        except (subprocess.CalledProcessError, ValueError):
            return None, None

        if version >= min_version:
            return nodejs_exe, version.version

        return None, None

    # "nodejs" is first in the tuple on the assumption that it's only likely to
    # exist on systems (probably linux distros) where there is a program in the path
    # called "node" that does something else.
    return find_executable(['nodejs', 'node'], min_version)


def find_npm_executable(min_version=NPM_MIN_VERSION):
    """Find a Node executable from the mozbuild directory.

    Returns a tuple containing the the path to an executable binary and a
    version tuple. Both tuple entries will be None if a Node executable
    could not be resolved.
    """
    return find_executable(["npm"], min_version)


def find_executable(names, min_version):
    paths = find_node_paths()

    found_exe = None
    for name in names:
        try:
            exe = simple_which(name, paths)
        except which.WhichError:
            continue

        if not exe:
            continue

        if not found_exe:
            found_exe = exe

        # We always verify we can invoke the executable and its version is
        # sane.
        try:
            version = check_executable_version(exe)
        except (subprocess.CalledProcessError, ValueError):
            continue

        if version >= min_version:
            return exe, version.version

    return found_exe, None