Bug 1470332 - teach 'mach bootstrap' to install Node on Debian/Ubuntu, r?gps draft
authordmose@mozilla.org <dmose@mozilla.org>
Mon, 09 Jul 2018 16:24:45 -0400
changeset 815832 333fa6f633f082f62a4a4e090eba54ea9392224b
parent 815831 34e9acac2e4b97e4cf610f3f4244c9ae361e2f9d
push id115659
push userbmo:dmose@mozilla.org
push dateTue, 10 Jul 2018 02:02:24 +0000
reviewersgps
bugs1470332
milestone62.0
Bug 1470332 - teach 'mach bootstrap' to install Node on Debian/Ubuntu, r?gps MozReview-Commit-ID: IHdpNe0g8Nd
python/mozboot/mozboot/base.py
python/mozboot/mozboot/bootstrap.py
python/mozboot/mozboot/debian.py
--- a/python/mozboot/mozboot/base.py
+++ b/python/mozboot/mozboot/base.py
@@ -68,16 +68,46 @@ the $PATH environment variable.
 
 We recommend the following tools for installing Python:
 
     pyenv   -- https://github.com/yyuu/pyenv)
     pythonz -- https://github.com/saghul/pythonz
     official installers -- http://www.python.org/
 '''
 
+NODE_UNABLE_UPGRADE = '''
+You are currently running NodeJS %s. Running %s or newer is required.
+
+Unfortunately, this bootstrapper does not currently know how to automatically
+upgrade NodeJS on your machine.
+
+Please search the Internet for how to upgrade your NodeJS and try running this
+bootstrapper again to ensure your machine is up to date.
+'''
+
+NODE_UPGRADE_FAILED = '''
+We attempted to upgrade NodeJS to a modern version (%s or newer).
+However, you appear to still have version %s.
+
+It's possible your package manager doesn't yet expose a modern version of
+NodeJS. It's also possible NodeJS is not being installed in the search path for
+this shell. Try creating a new shell and run this bootstrapper again.
+
+If this continues to fail and you are sure you have a modern NodeJS on your
+system, ensure it is on the $PATH and try again. If that fails, you'll need to
+install NodeJS manually and ensure the path with the node binary is listed in
+the $PATH environment variable.
+
+We recommend installing NodeJS from one of the following sources:
+
+    package manager builds: https://nodejs.org/en/download/package-manager/
+    nvm: https://github.com/creationix/nvm
+    official builds: https://nodejs.org/en/download/
+'''
+
 RUST_INSTALL_COMPLETE = '''
 Rust installation complete. You should now have rustc and cargo
 in %(cargo_bin)s
 
 The installer tries to add these to your default shell PATH, so
 restarting your shell and running this script again may work.
 If it doesn't, you'll need to add the new command location
 manually.
@@ -147,16 +177,18 @@ ac_add_options --enable-artifact-builds
 MODERN_MERCURIAL_VERSION = LooseVersion('4.3.3')
 
 # Upgrade Python older than this.
 MODERN_PYTHON_VERSION = LooseVersion('2.7.3')
 
 # Upgrade rust older than this.
 MODERN_RUST_VERSION = LooseVersion('1.24.0')
 
+# Upgrade NodeJS older than this.
+MODERN_NODE_VERSION = LooseVersion('8.11.0')
 
 class BaseBootstrapper(object):
     """Base class for system bootstrappers."""
 
     def __init__(self, no_interactive=False):
         self.package_manager_updated = False
         self.no_interactive = no_interactive
         self.state_dir = None
@@ -427,17 +459,17 @@ class BaseBootstrapper(object):
         if not name:
             name = os.path.basename(path)
         if name.endswith('.exe'):
             name = name[:-4]
 
         info = self.check_output([path, '--version'],
                                  env=env,
                                  stderr=subprocess.STDOUT)
-        match = re.search(name + ' ([a-z0-9\.]+)', info)
+        match = re.search(name + ' ?([a-z0-9\.]+)', info)
         if not match:
             print('ERROR! Unable to identify %s version.' % name)
             return None
 
         return LooseVersion(match.group(1))
 
     def _hg_cleanenv(self, load_hgrc=False):
         """ Returns a copy of the current environment updated with the HGPLAIN
@@ -544,16 +576,61 @@ class BaseBootstrapper(object):
 
     def upgrade_python(self, current):
         """Upgrade Python.
 
         Child classes should reimplement this.
         """
         print(PYTHON_UNABLE_UPGRADE % (current, MODERN_PYTHON_VERSION))
 
+    def is_node_modern(self):
+        node = None
+
+        for test in ['nodejs', 'node']:
+            node = self.which(test)
+            if node:
+                break
+
+        assert node
+
+        # "node --version" doesn't print the name of the program at all,
+        # but it does prepend "v" to the version string.  Good times!
+        our = self._parse_version(node, name='v')
+        if not our:
+            return False, None
+
+        return our >= MODERN_NODE_VERSION, our
+
+    def ensure_node_modern(self):
+        modern, version = self.is_node_modern()
+
+        if modern:
+            print('Your version of NodeJS (%s) is new enough.' % version)
+            return
+
+        print('Your version of NodeJS (%s) is too old. Will try to upgrade.' %
+              version)
+
+        self._ensure_package_manager_updated()
+        self.upgrade_node(version)
+
+        modern, after = self.is_node_modern()
+
+        if not modern:
+            print(NODE_UPGRADE_FAILED % (MODERN_NODE_VERSION, after))
+            sys.exit(1)
+
+    def upgrade_node(self, current):
+        """Upgrade NodeJS.
+
+        Child classes should reimplement this.
+        """
+        print(NODE_UNABLE_UPGRADE % (current, MODERN_NODE_VERSION))
+        sys.exit(1)
+
     def is_rust_modern(self):
         rustc = self.which('rustc')
         if not rustc:
             print('Could not find a Rust compiler.')
             return False, None
 
         our = self._parse_version(rustc)
         if not our:
--- a/python/mozboot/mozboot/bootstrap.py
+++ b/python/mozboot/mozboot/bootstrap.py
@@ -277,16 +277,17 @@ class Bootstrapper(object):
 
         self.instance.install_system_packages()
 
         # Like 'install_browser_packages' or 'install_mobile_android_packages'.
         getattr(self.instance, 'install_%s_packages' % application)()
 
         hg_installed, hg_modern = self.instance.ensure_mercurial_modern()
         self.instance.ensure_python_modern()
+        self.instance.ensure_node_modern()
         self.instance.ensure_rust_modern()
 
         # The state directory code is largely duplicated from mach_bootstrap.py.
         # We can't easily import mach_bootstrap.py because the bootstrapper may
         # run in self-contained mode and only the files in this directory will
         # be available. We /could/ refactor parts of mach_bootstrap.py to be
         # part of this directory to avoid the code duplication.
         state_dir, _ = get_state_dir()
--- a/python/mozboot/mozboot/debian.py
+++ b/python/mozboot/mozboot/debian.py
@@ -181,8 +181,45 @@ class DebianBootstrapper(StyloInstall, B
         # No Mercurial.
         if res == 3:
             print('Not installing Mercurial.')
             return False
 
         # pip.
         assert res == 1
         self.run_as_root(['pip', 'install', '--upgrade', 'Mercurial'])
+
+    def upgrade_node(self, current):
+        import os
+        import errno
+        import stat
+        import tempfile
+
+        # setup_8.x is chosen since 8.11.0 is the current minimum.
+        # We _could_ use setup_10.x here, but that seems like a footgun, since
+        # people who aren't conscious of node versioning but want to script
+        # something with node could end up using 10.x features unknowingly,
+        # have them work locally, and then explode on the try servers.
+        node_setup_url = "https://deb.nodesource.com/setup_8.x"
+
+        # hash created by hand against current version of script by sha256sum
+        setup_hash = "84d4d2b394eb3a16dcbcf89c8f7c223eec5eb96bc2f88dee617085566986966d"
+
+        print('Downloading node-setup-... ', end='')
+        fd, node_setup = tempfile.mkstemp(prefix=os.path.basename(node_setup_url))
+        os.close(fd)
+
+        try:
+            self.http_download_and_save(node_setup_url, node_setup, setup_hash)
+            mode = os.stat(node_setup).st_mode
+            os.chmod(node_setup, mode | stat.S_IRWXU)
+            print('Ok')
+            print('Running node-setup...')
+            self.run_as_root([node_setup])
+            self.run_as_root(['apt-get', 'install', '-y', 'nodejs'])
+        finally:
+            try:
+                os.remove(node_setup)
+            except OSError as e:
+                if e.errno != errno.ENOENT:
+                    raise
+
+