binman: Allow creation of entry documentation

Binman supports quite a number of different entries now. The operation of
these is not always obvious but at present the source code is the only
reference for understanding how an entry works.

Add a way to create documentation (from the source code) which can be put
in a new 'README.entries' file.

Signed-off-by: Simon Glass <sjg@chromium.org>
diff --git a/tools/binman/binman.py b/tools/binman/binman.py
index 52e02ed..1536e95 100755
--- a/tools/binman/binman.py
+++ b/tools/binman/binman.py
@@ -77,9 +77,20 @@
       return 1
     return 0
 
+def GetEntryModules(include_testing=True):
+    """Get a set of entry class implementations
+
+    Returns:
+        Set of paths to entry class filenames
+    """
+    glob_list = glob.glob(os.path.join(our_path, 'etype/*.py'))
+    return set([os.path.splitext(os.path.basename(item))[0]
+                for item in glob_list
+                if include_testing or '_testing' not in item])
+
 def RunTestCoverage():
     """Run the tests and check that we get 100% coverage"""
-    glob_list = glob.glob(os.path.join(our_path, 'etype/*.py'))
+    glob_list = GetEntryModules(False)
     all_set = set([os.path.splitext(os.path.basename(item))[0]
                    for item in glob_list if '_testing' not in item])
     test_util.RunTestCoverage('tools/binman/binman.py', None,
@@ -107,13 +118,8 @@
     elif options.test_coverage:
         RunTestCoverage()
 
-    elif options.full_help:
-        pager = os.getenv('PAGER')
-        if not pager:
-            pager = 'more'
-        fname = os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])),
-                            'README')
-        command.Run(pager, fname)
+    elif options.entry_docs:
+        control.WriteEntryDocs(GetEntryModules())
 
     else:
         try:
diff --git a/tools/binman/cmdline.py b/tools/binman/cmdline.py
index 54e4fb1..f0de4de 100644
--- a/tools/binman/cmdline.py
+++ b/tools/binman/cmdline.py
@@ -28,6 +28,8 @@
             help='Configuration file (.dtb) to use')
     parser.add_option('-D', '--debug', action='store_true',
             help='Enabling debugging (provides a full traceback on error)')
+    parser.add_option('-E', '--entry-docs', action='store_true',
+            help='Write out entry documentation (see README.entries)')
     parser.add_option('-I', '--indir', action='append',
             help='Add a path to a directory to use for input files')
     parser.add_option('-H', '--full-help', action='store_true',
diff --git a/tools/binman/control.py b/tools/binman/control.py
index 3c931d9..2de1c86 100644
--- a/tools/binman/control.py
+++ b/tools/binman/control.py
@@ -92,6 +92,10 @@
 def GetEntryArg(name):
     return entry_args.get(name)
 
+def WriteEntryDocs(modules, test_missing=None):
+    from entry import Entry
+    Entry.WriteDocs(modules, test_missing)
+
 def Binman(options, args):
     """The main control code for binman
 
diff --git a/tools/binman/entry.py b/tools/binman/entry.py
index de07f27..dc09b81 100644
--- a/tools/binman/entry.py
+++ b/tools/binman/entry.py
@@ -78,20 +78,18 @@
             self.ReadNode()
 
     @staticmethod
-    def Create(section, node, etype=None):
-        """Create a new entry for a node.
+    def Lookup(section, node_path, etype):
+        """Look up the entry class for a node.
 
         Args:
-            section:  Section object containing this node
-            node:   Node object containing information about the entry to create
-            etype:  Entry type to use, or None to work it out (used for tests)
+            section:   Section object containing this node
+            node_node: Path name of Node object containing information about
+                       the entry to create (used for errors)
+            etype:   Entry type to use
 
         Returns:
-            A new Entry object of the correct type (a subclass of Entry)
+            The entry class object if found, else None
         """
-        if not etype:
-            etype = fdt_util.GetString(node, 'type', node.name)
-
         # Convert something like 'u-boot@0' to 'u_boot' since we are only
         # interested in the type.
         module_name = etype.replace('-', '_')
@@ -110,15 +108,34 @@
                     module = importlib.import_module(module_name)
                 else:
                     module = __import__(module_name)
-            except ImportError:
-                raise ValueError("Unknown entry type '%s' in node '%s'" %
-                        (etype, node.path))
+            except ImportError as e:
+                raise ValueError("Unknown entry type '%s' in node '%s' (expected etype/%s.py, error '%s'" %
+                                 (etype, node_path, module_name, e))
             finally:
                 sys.path = old_path
             modules[module_name] = module
 
+        # Look up the expected class name
+        return getattr(module, 'Entry_%s' % module_name)
+
+    @staticmethod
+    def Create(section, node, etype=None):
+        """Create a new entry for a node.
+
+        Args:
+            section: Section object containing this node
+            node:    Node object containing information about the entry to
+                     create
+            etype:   Entry type to use, or None to work it out (used for tests)
+
+        Returns:
+            A new Entry object of the correct type (a subclass of Entry)
+        """
+        if not etype:
+            etype = fdt_util.GetString(node, 'type', node.name)
+        obj = Entry.Lookup(section, node.path, etype)
+
         # Call its constructor to get the object we want.
-        obj = getattr(module, 'Entry_%s' % module_name)
         return obj(section, etype, node)
 
     def ReadNode(self):
@@ -376,3 +393,53 @@
         else:
             value = fdt_util.GetDatatype(self._node, name, datatype)
         return value
+
+    @staticmethod
+    def WriteDocs(modules, test_missing=None):
+        """Write out documentation about the various entry types to stdout
+
+        Args:
+            modules: List of modules to include
+            test_missing: Used for testing. This is a module to report
+                as missing
+        """
+        print('''Binman Entry Documentation
+===========================
+
+This file describes the entry types supported by binman. These entry types can
+be placed in an image one by one to build up a final firmware image. It is
+fairly easy to create new entry types. Just add a new file to the 'etype'
+directory. You can use the existing entries as examples.
+
+Note that some entries are subclasses of others, using and extending their
+features to produce new behaviours.
+
+
+''')
+        modules = sorted(modules)
+
+        # Don't show the test entry
+        if '_testing' in modules:
+            modules.remove('_testing')
+        missing = []
+        for name in modules:
+            module = Entry.Lookup(name, name, name)
+            docs = getattr(module, '__doc__')
+            if test_missing == name:
+                docs = None
+            if docs:
+                lines = docs.splitlines()
+                first_line = lines[0]
+                rest = [line[4:] for line in lines[1:]]
+                hdr = 'Entry: %s: %s' % (name.replace('_', '-'), first_line)
+                print(hdr)
+                print('-' * len(hdr))
+                print('\n'.join(rest))
+                print()
+                print()
+            else:
+                missing.append(name)
+
+        if missing:
+            raise ValueError('Documentation is missing for modules: %s' %
+                             ', '.join(missing))
diff --git a/tools/binman/etype/u_boot_dtb_with_ucode.py b/tools/binman/etype/u_boot_dtb_with_ucode.py
index 752d0ac..adcb898 100644
--- a/tools/binman/etype/u_boot_dtb_with_ucode.py
+++ b/tools/binman/etype/u_boot_dtb_with_ucode.py
@@ -6,7 +6,6 @@
 #
 
 import control
-import fdt
 from entry import Entry
 from blob import Entry_blob
 import tools
@@ -38,6 +37,9 @@
         return 'u-boot.dtb'
 
     def ProcessFdt(self, fdt):
+        # So the module can be loaded without it
+        import fdt
+
         # If the section does not need microcode, there is nothing to do
         ucode_dest_entry = self.section.FindEntryType(
             'u-boot-spl-with-ucode-ptr')
diff --git a/tools/binman/ftest.py b/tools/binman/ftest.py
index 6968896..9c01805 100644
--- a/tools/binman/ftest.py
+++ b/tools/binman/ftest.py
@@ -21,6 +21,7 @@
 import elf
 import fdt
 import fdt_util
+import test_util
 import tools
 import tout
 
@@ -1177,6 +1178,20 @@
                     TEXT_DATA3 + 'some text')
         self.assertEqual(expected, data)
 
+    def testEntryDocs(self):
+        """Test for creation of entry documentation"""
+        with test_util.capture_sys_output() as (stdout, stderr):
+            control.WriteEntryDocs(binman.GetEntryModules())
+        self.assertTrue(len(stdout.getvalue()) > 0)
+
+    def testEntryDocsMissing(self):
+        """Test handling of missing entry documentation"""
+        with self.assertRaises(ValueError) as e:
+            with test_util.capture_sys_output() as (stdout, stderr):
+                control.WriteEntryDocs(binman.GetEntryModules(), 'u_boot')
+        self.assertIn('Documentation is missing for modules: u_boot',
+                      str(e.exception))
+
 
 if __name__ == "__main__":
     unittest.main()