Regenerate bootstrap if it has updated.

If the Python source has updated or the requirements have, we need to
regenerate the bootstrap.

Test: Checked the following workflow:
    remove out directory
    checkbuild.py -> bootstraps
    checkbuild.py -> no regen
    add requirements for checkbuild.py, checkbuild.py -> regen
    checkbuild.py -> no regen
    change requirements file, checkbuild.py -> regen
    checkbuild.py -> no regen
    remove requirements for checkbuild.py, checkbuild.py -> regen
    checkbuild.py -> no regen
    change Python README, checkbuild.py -> regen
    checkbuild.py -> no regen

Bug: None
Change-Id: I2f832dbb03047a040f538a60a14b38ddf55a5fbf
diff --git a/bootstrap/__init__.py b/bootstrap/__init__.py
index 9883086..5d3741f 100644
--- a/bootstrap/__init__.py
+++ b/bootstrap/__init__.py
@@ -38,6 +38,9 @@
     return os.path.normpath(os.path.join(THIS_DIR, '../..', *args))
 
 
+PYTHON_SOURCE = android_path('external/python/cpython3')
+
+
 def _get_dir_from_env(default, env_var):
     """Returns the path to a directory specified by the environment.
 
@@ -130,9 +133,8 @@
     try:
         os.chdir(build_dir)
 
-        python_src = android_path('external/python/cpython3')
         check_output([
-            os.path.join(python_src, 'configure'),
+            os.path.join(PYTHON_SOURCE, 'configure'),
             '--prefix=' + install_dir,
 
             # This enables PGO and requires running all the Python tests to
@@ -209,6 +211,93 @@
         self.finish()
 
 
+def read_requirements(requirements):
+    """Returns the contents of a requirements file or None.
+
+    Args:
+        requirements: Path to a requirements.txt file that may or may not
+        exist, or none.
+
+    Returns:
+        The contents of the requirements file if it exists, or None if the
+        requirequirements file is None or does not exist.
+    """
+
+    if requirements is None:
+        return None
+
+    if not os.path.exists(requirements):
+        return None
+
+    with open(requirements) as requirements_file:
+        return requirements_file.read()
+
+
+class BootstrapManifest(object):  # pylint: disable=useless-object-inheritance
+    """Describes the contents of the bootstrapped directory."""
+
+    SOURCE_MANIFEST_PATH = os.path.join(PYTHON_SOURCE, 'README.rst')
+
+    def __init__(self, install_path, requirements):
+        self.install_path = install_path
+        self.manifest_file = os.path.join(self.install_path, '.bootstrapped')
+
+        self.requested_requirements_path = requirements
+        self.bootstrapped_requirements_path = os.path.join(
+            self.install_path, 'requirements.txt')
+
+        self.requested_requirements = read_requirements(
+            self.requested_requirements_path)
+        self.bootstrapped_requirements = read_requirements(
+            self.bootstrapped_requirements_path)
+
+    def is_up_to_date(self):
+        """Returns True if the bootstrap install is up to date."""
+        if not os.path.exists(self.manifest_file):
+            return False
+        if not self.versions_match():
+            logger().info('Bootstrap out of date: Python has changed.')
+            return False
+        if self.requested_requirements != self.bootstrapped_requirements:
+            logger().info('Bootstrap out of date: requirements have changed.')
+            return False
+        return True
+
+    def versions_match(self):
+        """Returns True if the bootstrap has an up to date Python."""
+        # Ideally this would be a check of the git revision of the Python
+        # source, but we can't assume that information is available on the
+        # build servers. For now, assume the README.rst will change for any
+        # update. This should be fine since updates should include a change to
+        # the version number.
+
+        # This function should not be called if this file does not exist.
+        assert os.path.exists(self.manifest_file)
+
+        with open(self.SOURCE_MANIFEST_PATH) as readme_rst:
+            source_manifest = readme_rst.read()
+        with open(self.manifest_file) as manifest_file:
+            bootstrapped_manifest = manifest_file.read()
+
+        return source_manifest == bootstrapped_manifest
+
+    def save(self):
+        """Saves the bootstrap manifest to disk."""
+        self.save_python_version()
+        self.save_requirements()
+
+    def save_python_version(self):
+        shutil.copy2(self.SOURCE_MANIFEST_PATH, self.manifest_file)
+
+    def save_requirements(self):
+        if self.requested_requirements is not None:
+            shutil.copy2(self.requested_requirements_path,
+                         self.bootstrapped_requirements_path)
+        # An existing bootstrap directory is removed if it needed to be
+        # updated, so no need to remove an existing requirements file in the
+        # case where a requirements file was used but no longer is.
+
+
 def do_bootstrap(install_dir, requirements):
     """Helper function for bootstrapping.
 
@@ -226,10 +315,15 @@
     """
     build_dir = path_in_out('bootstrap-build')
 
-    bootstrap_completed_file = path_in_out('.bootstrapped')
-    if os.path.exists(bootstrap_completed_file):
+    bootstrap_manifest = BootstrapManifest(install_dir, requirements)
+    if bootstrap_manifest.is_up_to_date():
         return
 
+    # If the bootstrap exists but is not up to date, purge it to ensure no
+    # stale files remain.
+    if os.path.exists(install_dir):
+        shutil.rmtree(install_dir)
+
     timer = Timer()
     with timer:
         build_python(install_dir, build_dir)
@@ -237,8 +331,7 @@
             install_requirements(install_dir, requirements)
     logger().info('Bootstrapping completed in %s', timer.duration)
 
-    with open(bootstrap_completed_file, 'w'):
-        pass
+    bootstrap_manifest.save()
 
 
 def bootstrap(requirements=None):