Installer cleanup/documentation.

BUG=chrome-os-partner:25941
CQ-DEPEND=CL:187223
TEST=Unit tests
TEST=Manual on device, patching image, and patching stateful partition
TEST=Full buildbot build (link)

Change-Id: I221ee51ad79dbacabff57497b14d5f70b4fc22bc
Reviewed-on: https://chromium-review.googlesource.com/187224
Reviewed-by: Jon Salz <jsalz@chromium.org>
Commit-Queue: Jon Salz <jsalz@chromium.org>
Tested-by: Jon Salz <jsalz@chromium.org>
diff --git a/py/toolkit/installer.py b/py/toolkit/installer.py
index 8fbde66..17e7014 100755
--- a/py/toolkit/installer.py
+++ b/py/toolkit/installer.py
@@ -15,7 +15,9 @@
 import argparse
 from contextlib import contextmanager
 import os
+import re
 import sys
+import tempfile
 
 import factory_common  # pylint: disable=W0611
 from cros.factory.test import factory
@@ -25,6 +27,56 @@
 
 INSTALLER_PATH = 'usr/local/factory/py/toolkit/installer.py'
 
+# Short and sweet help header for the executable generated by makeself.
+HELP_HEADER = """
+Installs the factory toolkit, transforming a test image into a factory test
+image. You can:
+
+- Install the factory toolkit on a CrOS device that is running a test
+  image.  To do this, copy install_factory_toolkit.run to the device and
+  run it.  The factory tests will then come up on the next boot.
+
+    rsync -a install_factory_toolkit.run crosdevice:/tmp
+    ssh crosdevice '/tmp/install_factory_toolkit.run && sync && reboot'
+
+- Modify a test image, turning it into a factory test image.  When you
+  use the image on a device, the factory tests will come up.
+
+    install_factory_toolkit.run chromiumos_test_image.bin
+"""
+
+HELP_HEADER_ADVANCED = """
+- (advanced) Modify a mounted stateful partition, turning it into a factory
+  test image.  This is equivalent to the previous command:
+
+    mount_partition -rw chromiumos_test_image.bin 1 /mnt/stateful
+    install_factory_toolkit.run /mnt/stateful
+    umount /mnt/stateful
+
+- (advanced) Unpack the factory toolkit, modify a file, and then repack it.
+
+    # Unpack but don't actually install
+    install_factory_toolkit.run --target /tmp/toolkit --noexec
+    # Edit some files in /tmp/toolkit
+    emacs /tmp/toolkit/whatever
+    # Repack
+    install_factory_toolkit.run -- --repack /tmp/toolkit \\
+        --pack-into /path/to/new/install_factory_toolkit.run
+"""
+
+# The makeself-generated header comes next.  This is a little confusing,
+# so explain.
+HELP_HEADER_MAKESELF = """
+For complete usage information and advanced operations, run
+"install_factory_toolkit.run -- --help" (note the extra "--").
+
+Following is the help message from makeself, which was used to create
+this self-extracting archive.
+
+-----
+"""
+
+
 class FactoryToolkitInstaller():
   """Factory toolkit installer.
 
@@ -38,15 +90,32 @@
   """
 
   def __init__(self, src, dest, no_enable, system_root='/'):
+    self._src = src
     self._system_root = system_root
     if dest == self._system_root:
       self._usr_local_dest = os.path.join(dest, 'usr', 'local')
       self._var_dest = os.path.join(dest, 'var')
+
+      # Make sure we're on a CrOS device.
+      lsb_release = self._ReadLSBRelease()
+      is_cros = (
+        lsb_release and
+        re.match('^CHROMEOS_RELEASE', lsb_release, re.MULTILINE) is not None)
+
+      if not is_cros:
+        sys.stderr.write(
+            "ERROR: You're not on a CrOS device (/etc/lsb-release does not\n"
+            "contain CHROMEOS_RELEASE), so you must specify a test image or a\n"
+            "mounted stateful partition on which to install the factory\n"
+            "toolkit.  Please run\n"
+            "\n"
+            "  install_factory_toolkit.run -- --help\n"
+            "\n"
+            "for help.\n")
+        sys.exit(1)
       if os.getuid() != 0:
-        raise Exception('Must be root to install on live machine!')
-      if not os.path.exists('/etc/lsb-release'):
-        raise Exception('/etc/lsb-release is missing. '
-                        'Are you running this in chroot?')
+        raise Exception('You must be root to install the factory toolkit on a '
+                        'CrOS device.')
     else:
       self._usr_local_dest = os.path.join(dest, 'dev_image')
       self._var_dest = os.path.join(dest, 'var_overlay')
@@ -66,26 +135,37 @@
       raise Exception(
           'This installer must be run from within the factory toolkit!')
 
+  @staticmethod
+  def _ReadLSBRelease():
+    """Returns the contents of /etc/lsb-release, or None if it does not
+    exist."""
+    if os.path.exists('/etc/lsb-release'):
+      with open('/etc/lsb-release') as f:
+        return f.read()
+    return None
+
   def WarningMessage(self, target_test_image=None):
+    with open(os.path.join(self._src, 'VERSION')) as f:
+      ret = f.read()
     if target_test_image:
-      ret = (
+      ret += (
           '\n'
           '\n'
-          '*** You are about to patch factory toolkit into:\n'
+          '*** You are about to patch the factory toolkit into:\n'
           '***   %s\n'
           '***' % target_test_image)
     else:
-      ret = (
+      ret += (
           '\n'
           '\n'
-          '*** You are about to install factory toolkit to:\n'
+          '*** You are about to install the factory toolkit to:\n'
           '***   %s\n'
           '***' % self._dest)
     if self._dest == self._system_root:
       if self._no_enable:
         ret += ('\n'
           '*** Factory tests will be disabled after this process is done, but\n'
-          '*** you can enable them by creating factory enabled tag:\n'
+          '*** you can enable them by creating the factory enabled tag:\n'
           '***   %s\n'
           '***' % self._tag_file)
       else:
@@ -93,7 +173,8 @@
           '*** After this process is done, your device will start factory\n'
           '*** tests on the next reboot.\n'
           '***\n'
-          '*** Factory tests can be disabled by deleting factory enabled tag:\n'
+          '*** Factory tests can be disabled by deleting the factory enabled\n'
+          '*** tag:\n'
           '***   %s\n'
           '***' % self._tag_file)
     return ret
@@ -140,9 +221,13 @@
   """Packs the files containing this script into a factory toolkit."""
   with open(os.path.join(src_root, 'VERSION'), 'r') as f:
     version = f.read().strip()
-  Spawn([os.path.join(src_root, 'makeself.sh'), '--bzip2', '--nox11',
-         src_root, output_path, version, INSTALLER_PATH],
-         check_call=True, log=True)
+  with tempfile.NamedTemporaryFile() as help_header:
+    help_header.write(version + "\n" + HELP_HEADER + HELP_HEADER_MAKESELF)
+    help_header.flush()
+    Spawn([os.path.join(src_root, 'makeself.sh'), '--bzip2', '--nox11',
+           '--help-header', help_header.name,
+           src_root, output_path, version, INSTALLER_PATH, '--in-exe'],
+          check_call=True, log=True)
   print ('\n'
       '  Factory toolkit generated at %s.\n'
       '\n'
@@ -151,13 +236,28 @@
       '\n'
       '  Alternatively, the factory toolkit can be used to patch a test\n'
       '  image. For more information, run:\n'
-      '    %s -- --help\n'
+      '    %s --help\n'
       '\n' % (output_path, output_path))
 
 
 def main():
+  import logging
+  logging.basicConfig(level=logging.INFO)
+
+  # In order to determine which usage message to show, first determine
+  # whether we're in the self-extracting archive.  Do this first
+  # because we need it to even parse the arguments.
+  if '--in-exe' in sys.argv:
+    sys.argv = [x for x in sys.argv if x != '--in-exe']
+    in_archive = True
+  else:
+    in_archive = False
+
   parser = argparse.ArgumentParser(
-      description='Factory toolkit installer.')
+      description=HELP_HEADER + HELP_HEADER_ADVANCED,
+      usage=('install_factory_toolkit.run -- [options]' if in_archive
+             else None),
+      formatter_class=argparse.RawDescriptionHelpFormatter)
   parser.add_argument('dest', nargs='?', default='/',
       help='A test image or the mount point of the stateful partition. '
            "If omitted, install to live system, i.e. '/'.")
@@ -183,6 +283,12 @@
     PackFactoryToolkit(src_root, args.pack_into)
     return
 
+  if not in_archive:
+    # If you're not in the self-extracting archive, you're not allowed to
+    # do anything except the above --pack-into call.
+    parser.error('Not running from install_factory_toolkit.run; '
+                 'only --pack-into (without --repack) is allowed')
+
   # Change to original working directory in case the user specifies
   # a relative path.
   # TODO: Use USER_PWD instead when makeself is upgraded