pw_env_setup: Check that submodules are present

Check that all submodules are present in the checkout, unless they're
listed as optional in the environment config file. Give commands for
checking out any required missing submodules.

Change-Id: I61e5313a0fda413cbca2cc7650a2982c3985f28e
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/49021
Pigweed-Auto-Submit: Rob Mohr <mohrr@google.com>
Reviewed-by: Joe Ethier <jethier@google.com>
Commit-Queue: Auto-Submit <auto-submit@pigweed.google.com.iam.gserviceaccount.com>
diff --git a/pw_env_setup/docs.rst b/pw_env_setup/docs.rst
index 70307b9..2f02a0f 100644
--- a/pw_env_setup/docs.rst
+++ b/pw_env_setup/docs.rst
@@ -227,6 +227,10 @@
   only installing Pigweed Python packages, use the location of the Pigweed
   submodule.
 
+``optional_submodules``
+  By default environment setup will check that all submodules are present in
+  the checkout. Any submodules in this list are excluded from that check.
+
 An example of a config file is below.
 
 .. code-block:: json
@@ -242,7 +246,11 @@
       "gn_targets": [
         ":python.install",
       ]
-    }
+    },
+    "optional_submodules": [
+      "optional/submodule/one",
+      "optional/submodule/two"
+    ]
   }
 
 In case the CIPD packages need to be referenced from other scripts, variables
diff --git a/pw_env_setup/py/pw_env_setup/env_setup.py b/pw_env_setup/py/pw_env_setup/env_setup.py
index 4d53f45..21e53fa 100755
--- a/pw_env_setup/py/pw_env_setup/env_setup.py
+++ b/pw_env_setup/py/pw_env_setup/env_setup.py
@@ -161,6 +161,10 @@
     pass
 
 
+class MissingSubmodulesError(Exception):
+    pass
+
+
 # TODO(mohrr) remove disable=useless-object-inheritance once in Python 3.
 # pylint: disable=useless-object-inheritance
 # pylint: disable=too-many-instance-attributes
@@ -195,9 +199,12 @@
         self._virtualenv_gn_targets = []
         self._optional_submodules = []
 
+        self._config_file_name = getattr(config_file, 'name', 'config file')
         if config_file:
             self._parse_config_file(config_file)
 
+        self._check_submodules()
+
         self._json_file = json_file
         if not self._json_file:
             self._json_file = os.path.join(self._install_dir, 'actions.json')
@@ -260,11 +267,48 @@
         if virtualenv:
             raise ConfigFileError(
                 'unrecognized option in {}: "virtualenv.{}"'.format(
-                    config_file.name, next(iter(virtualenv))))
+                    self._config_file_name, next(iter(virtualenv))))
 
         if config:
             raise ConfigFileError('unrecognized option in {}: "{}"'.format(
-                config_file.name, next(iter(config))))
+                self._config_file_name, next(iter(config))))
+
+    def _check_submodules(self):
+        unitialized = set()
+
+        for line in subprocess.check_output(
+            ['git', 'submodule', 'status', '--recursive'],
+                cwd=self._project_root,
+        ).splitlines():
+            if isinstance(line, bytes):
+                line = line.decode()
+            # Anything but an initial '-' means the submodule is initialized.
+            if not line.startswith('-'):
+                continue
+            unitialized.add(line.split()[1])
+
+        missing = unitialized - set(self._optional_submodules)
+        if missing:
+            print(
+                'Not all submodules are initialized. Please run the '
+                'following commands.',
+                file=sys.stderr)
+            print('', file=sys.stderr)
+
+            for miss in missing:
+                print('    git submodule update --init {}'.format(miss),
+                      file=sys.stderr)
+            print('', file=sys.stderr)
+
+            print(
+                'If these submodules are not required, add them to the '
+                '"optional_submodules"',
+                file=sys.stderr)
+            print('list in the environment config JSON file:', file=sys.stderr)
+            print('    {}'.format(self._config_file_name), file=sys.stderr)
+            print('', file=sys.stderr)
+
+            raise MissingSubmodulesError(', '.join(sorted(missing)))
 
     def _log(self, *args, **kwargs):
         # Not using logging module because it's awkward to flush a log handler.