tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

nodeutil.py (9300B)


      1 # This Source Code Form is subject to the terms of the Mozilla Public
      2 # License, v. 2.0. If a copy of the MPL was not distributed with this
      3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
      4 
      5 import os
      6 import platform
      7 import subprocess
      8 import sys
      9 
     10 from mozboot.util import get_tools_dir
     11 from mozfile import which
     12 from mozfile.mozfile import remove as mozfileremove
     13 from packaging.version import Version
     14 
     15 NODE_MIN_VERSION = Version("12.22.12")
     16 NPM_MIN_VERSION = Version("6.14.16")
     17 
     18 NODE_MACHING_VERSION_NOT_FOUND_MESSAGE = """
     19 Could not find Node.js executable later than %s.
     20 
     21 Executing `mach bootstrap --no-system-changes` should
     22 install a compatible version in ~/.mozbuild on most platforms.
     23 """.strip()
     24 
     25 NPM_MACHING_VERSION_NOT_FOUND_MESSAGE = """
     26 Could not find npm executable later than %s.
     27 
     28 Executing `mach bootstrap --no-system-changes` should
     29 install a compatible version in ~/.mozbuild on most platforms.
     30 """.strip()
     31 
     32 NODE_NOT_FOUND_MESSAGE = """
     33 nodejs is either not installed or is installed to a non-standard path.
     34 
     35 Executing `mach bootstrap --no-system-changes` should
     36 install a compatible version in ~/.mozbuild on most platforms.
     37 """.strip()
     38 
     39 NPM_NOT_FOUND_MESSAGE = """
     40 Node Package Manager (npm) is either not installed or installed to a
     41 non-standard path.
     42 
     43 Executing `mach bootstrap --no-system-changes` should
     44 install a compatible version in ~/.mozbuild on most platforms.
     45 """.strip()
     46 
     47 
     48 def find_node_paths():
     49    """Determines the possible paths for node executables.
     50 
     51    Returns a list of paths, which includes the build state directory.
     52    """
     53    mozbuild_tools_dir = get_tools_dir()
     54 
     55    if platform.system() == "Windows":
     56        mozbuild_node_path = os.path.join(mozbuild_tools_dir, "node")
     57    else:
     58        mozbuild_node_path = os.path.join(mozbuild_tools_dir, "node", "bin")
     59 
     60    # We still fallback to the PATH, since on OSes that don't have toolchain
     61    # artifacts available to download, Node may be coming from $PATH.
     62    paths = [mozbuild_node_path] + os.environ.get("PATH").split(os.pathsep)
     63 
     64    if platform.system() == "Windows":
     65        paths += [
     66            "%s\\nodejs" % os.environ.get("SystemDrive"),
     67            os.path.join(os.environ.get("ProgramFiles"), "nodejs"),
     68            os.path.join(os.environ.get("PROGRAMW6432"), "nodejs"),
     69            os.path.join(os.environ.get("PROGRAMFILES"), "nodejs"),
     70        ]
     71 
     72    return paths
     73 
     74 
     75 def check_executable_version(exe, wrap_call_with_node=False):
     76    """Determine the version of a Node executable by invoking it.
     77 
     78    May raise ``subprocess.CalledProcessError`` or ``ValueError`` on failure.
     79    """
     80    out = None
     81    # npm may be a script (Except on Windows), so we must call it with node.
     82    if wrap_call_with_node and platform.system() != "Windows":
     83        binary, _ = find_node_executable()
     84        if binary:
     85            out = (
     86                subprocess.check_output(
     87                    [binary, exe, "--version"], universal_newlines=True
     88                )
     89                .lstrip("v")
     90                .rstrip()
     91            )
     92 
     93    # If we can't find node, or we don't need to wrap it, fallback to calling
     94    # direct.
     95    if not out:
     96        out = (
     97            subprocess.check_output([exe, "--version"], universal_newlines=True)
     98            .lstrip("v")
     99            .rstrip()
    100        )
    101    return Version(out)
    102 
    103 
    104 def find_node_executable(
    105    nodejs_exe=os.environ.get("NODEJS"), min_version=NODE_MIN_VERSION
    106 ):
    107    """Find a Node executable from the mozbuild directory.
    108 
    109    Returns a tuple containing the the path to an executable binary and a
    110    version tuple. Both tuple entries will be None if a Node executable
    111    could not be resolved.
    112    """
    113    if nodejs_exe:
    114        try:
    115            version = check_executable_version(nodejs_exe)
    116        except (subprocess.CalledProcessError, ValueError):
    117            return None, None
    118 
    119        if version >= min_version:
    120            return nodejs_exe, version.release
    121 
    122        return None, None
    123 
    124    # "nodejs" is first in the tuple on the assumption that it's only likely to
    125    # exist on systems (probably linux distros) where there is a program in the path
    126    # called "node" that does something else.
    127    return find_executable("node", min_version)
    128 
    129 
    130 def find_npm_executable(min_version=NPM_MIN_VERSION):
    131    """Find a Node executable from the mozbuild directory.
    132 
    133    Returns a tuple containing the the path to an executable binary and a
    134    version tuple. Both tuple entries will be None if a Node executable
    135    could not be resolved.
    136    """
    137    return find_executable("npm", min_version, True)
    138 
    139 
    140 def find_executable(name, min_version, use_node_for_version_check=False):
    141    paths = find_node_paths()
    142    exe = which(name, path=paths)
    143 
    144    if not exe:
    145        return None, None
    146 
    147    # Verify we can invoke the executable and its version is acceptable.
    148    try:
    149        version = check_executable_version(exe, use_node_for_version_check)
    150    except (subprocess.CalledProcessError, ValueError):
    151        return None, None
    152 
    153    if version < min_version:
    154        return None, None
    155 
    156    return exe, version.release
    157 
    158 
    159 def check_node_executables_valid():
    160    node_path, version = find_node_executable()
    161    if not node_path:
    162        print(NODE_NOT_FOUND_MESSAGE)
    163        return False
    164    if not version:
    165        print(NODE_MACHING_VERSION_NOT_FOUND_MESSAGE % NODE_MIN_VERSION)
    166        return False
    167 
    168    npm_path, version = find_npm_executable()
    169    if not npm_path:
    170        print(NPM_NOT_FOUND_MESSAGE)
    171        return False
    172    if not version:
    173        print(NPM_MACHING_VERSION_NOT_FOUND_MESSAGE % NPM_MIN_VERSION)
    174        return False
    175 
    176    return True
    177 
    178 
    179 def package_setup(
    180    package_root,
    181    package_name,
    182    should_update=False,
    183    should_clobber=False,
    184    no_optional=False,
    185    skip_logging=False,
    186 ):
    187    """Ensure `package_name` at `package_root` is installed.
    188 
    189    When `should_update` is true, clobber, install, and produce a new
    190    "package-lock.json" file.
    191 
    192    This populates `package_root/node_modules`.
    193 
    194    """
    195    orig_cwd = os.getcwd()
    196 
    197    if should_update:
    198        should_clobber = True
    199 
    200    try:
    201        # npm sometimes fails to respect cwd when it is run using check_call so
    202        # we manually switch folders here instead.
    203        os.chdir(package_root)
    204 
    205        if should_clobber:
    206            remove_directory(os.path.join(package_root, "node_modules"), skip_logging)
    207 
    208        npm_path, _ = find_npm_executable()
    209        if not npm_path:
    210            return 1
    211 
    212        node_path, _ = find_node_executable()
    213        if not node_path:
    214            return 1
    215 
    216        extra_parameters = ["--loglevel=error"]
    217 
    218        if no_optional:
    219            extra_parameters.append("--no-optional")
    220 
    221        package_lock_json_path = os.path.join(package_root, "package-lock.json")
    222 
    223        if should_update:
    224            cmd = [npm_path, "install"]
    225            mozfileremove(package_lock_json_path)
    226        else:
    227            cmd = [npm_path, "ci"]
    228 
    229        # On non-Windows, ensure npm is called via node, as node may not be in the
    230        # path.
    231        if platform.system() != "Windows":
    232            cmd.insert(0, node_path)
    233 
    234        cmd.extend(extra_parameters)
    235 
    236        # Ensure that bare `node` and `npm` in scripts, including post-install scripts, finds the
    237        # binary we're invoking with.  Without this, it's easy for compiled extensions to get
    238        # mismatched versions of the Node.js extension API.
    239        path = os.environ.get("PATH", "").split(os.pathsep)
    240        node_dir = os.path.dirname(node_path)
    241        if node_dir not in path:
    242            path = [node_dir] + path
    243 
    244        if not skip_logging:
    245            print(
    246                'Installing %s for mach using "%s"...' % (package_name, " ".join(cmd))
    247            )
    248        result = call_process(
    249            package_name, cmd, append_env={"PATH": os.pathsep.join(path)}
    250        )
    251 
    252        if not result:
    253            return 1
    254 
    255        bin_path = os.path.join(package_root, "node_modules", ".bin", package_name)
    256 
    257        if not skip_logging:
    258            print("\n%s installed successfully!" % package_name)
    259            print("\nNOTE: Your local %s binary is at %s\n" % (package_name, bin_path))
    260 
    261    finally:
    262        os.chdir(orig_cwd)
    263 
    264 
    265 def remove_directory(path, skip_logging=False):
    266    if not skip_logging:
    267        print("Clobbering %s..." % path)
    268    if sys.platform.startswith("win") and have_winrm():
    269        process = subprocess.Popen(["winrm", "-rf", path])
    270        process.wait()
    271    else:
    272        mozfileremove(path)
    273 
    274 
    275 def call_process(name, cmd, cwd=None, append_env={}):
    276    env = dict(os.environ)
    277    env.update(append_env)
    278 
    279    try:
    280        with open(os.devnull, "w") as fnull:
    281            subprocess.check_call(cmd, cwd=cwd, stdout=fnull, env=env)
    282    except subprocess.CalledProcessError:
    283        if cwd:
    284            print("\nError installing %s in the %s folder, aborting." % (name, cwd))
    285        else:
    286            print("\nError installing %s, aborting." % name)
    287 
    288        return False
    289 
    290    return True
    291 
    292 
    293 def have_winrm():
    294    # `winrm -h` should print 'winrm version ...' and exit 1
    295    try:
    296        p = subprocess.Popen(
    297            ["winrm.exe", "-h"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT
    298        )
    299        return p.wait() == 1 and p.stdout.read().startswith("winrm")
    300    except Exception:
    301        return False