binman: Support help messages for missing blobs

When an external blob is missing it can be quite confusing for the user.
Add a way to provide a help message that is shown.

Signed-off-by: Simon Glass <sjg@chromium.org>
Signed-off-by: Alper Nebi Yasak <alpernebiyasak@gmail.com>
diff --git a/tools/binman/README b/tools/binman/README
index dfa0ed6..fbcfdc7 100644
--- a/tools/binman/README
+++ b/tools/binman/README
@@ -343,6 +343,12 @@
 	Sets the compression algortihm to use (for blobs only). See the entry
 	documentation for details.
 
+missing-msg:
+	Sets the tag of the message to show if this entry is missing. This is
+	used for external blobs. When they are missing it is helpful to show
+	information about what needs to be fixed. See missing-blob-help for the
+	message for each tag.
+
 The attributes supported for images and sections are described below. Several
 are similar to those for entries.
 
diff --git a/tools/binman/control.py b/tools/binman/control.py
index 15bfac5..ee5771e 100644
--- a/tools/binman/control.py
+++ b/tools/binman/control.py
@@ -9,6 +9,7 @@
 import glob
 import os
 import pkg_resources
+import re
 
 import sys
 from patman import tools
@@ -22,6 +23,11 @@
 # Make this global so that it can be referenced from tests
 images = OrderedDict()
 
+# Help text for each type of missing blob, dict:
+#    key: Value of the entry's 'missing-msg' or entry name
+#    value: Text for the help
+missing_blob_help = {}
+
 def _ReadImageDesc(binman_node):
     """Read the image descriptions from the /binman node
 
@@ -54,6 +60,66 @@
             return node
     return None
 
+def _ReadMissingBlobHelp():
+    """Read the missing-blob-help file
+
+    This file containins help messages explaining what to do when external blobs
+    are missing.
+
+    Returns:
+        Dict:
+            key: Message tag (str)
+            value: Message text (str)
+    """
+
+    def _FinishTag(tag, msg, result):
+        if tag:
+            result[tag] = msg.rstrip()
+            tag = None
+            msg = ''
+        return tag, msg
+
+    my_data = pkg_resources.resource_string(__name__, 'missing-blob-help')
+    re_tag = re.compile('^([-a-z0-9]+):$')
+    result = {}
+    tag = None
+    msg = ''
+    for line in my_data.decode('utf-8').splitlines():
+        if not line.startswith('#'):
+            m_tag = re_tag.match(line)
+            if m_tag:
+                _, msg = _FinishTag(tag, msg, result)
+                tag = m_tag.group(1)
+            elif tag:
+                msg += line + '\n'
+    _FinishTag(tag, msg, result)
+    return result
+
+def _ShowBlobHelp(path, text):
+    tout.Warning('\n%s:' % path)
+    for line in text.splitlines():
+        tout.Warning('   %s' % line)
+
+def _ShowHelpForMissingBlobs(missing_list):
+    """Show help for each missing blob to help the user take action
+
+    Args:
+        missing_list: List of Entry objects to show help for
+    """
+    global missing_blob_help
+
+    if not missing_blob_help:
+        missing_blob_help = _ReadMissingBlobHelp()
+
+    for entry in missing_list:
+        tags = entry.GetHelpTags()
+
+        # Show the first match help message
+        for tag in tags:
+            if tag in missing_blob_help:
+                _ShowBlobHelp(entry._node.path, missing_blob_help[tag])
+                break
+
 def GetEntryModules(include_testing=True):
     """Get a set of entry class implementations
 
@@ -478,6 +544,7 @@
     if missing_list:
         tout.Warning("Image '%s' is missing external blobs and is non-functional: %s" %
                      (image.name, ' '.join([e.name for e in missing_list])))
+        _ShowHelpForMissingBlobs(missing_list)
     return bool(missing_list)
 
 
@@ -563,7 +630,7 @@
                 tools.WriteFile(dtb_item._fname, dtb_item.GetContents())
 
             if missing:
-                tout.Warning("Some images are invalid")
+                tout.Warning("\nSome images are invalid")
         finally:
             tools.FinaliseOutputDir()
     finally:
diff --git a/tools/binman/entry.py b/tools/binman/entry.py
index 0f128c4..f7adc3b 100644
--- a/tools/binman/entry.py
+++ b/tools/binman/entry.py
@@ -178,6 +178,7 @@
         self.align_end = fdt_util.GetInt(self._node, 'align-end')
         self.offset_unset = fdt_util.GetBool(self._node, 'offset-unset')
         self.expand_size = fdt_util.GetBool(self._node, 'expand-size')
+        self.missing_msg = fdt_util.GetString(self._node, 'missing-msg')
 
     def GetDefaultFilename(self):
         return None
@@ -827,3 +828,11 @@
             True if allowed, False if not allowed
         """
         return self.allow_missing
+
+    def GetHelpTags(self):
+        """Get the tags use for missing-blob help
+
+        Returns:
+            list of possible tags, most desirable first
+        """
+        return list(filter(None, [self.missing_msg, self.name, self.etype]))
diff --git a/tools/binman/ftest.py b/tools/binman/ftest.py
index a269a74..95b17d0 100644
--- a/tools/binman/ftest.py
+++ b/tools/binman/ftest.py
@@ -3561,7 +3561,7 @@
             self._DoTestFile('168_fit_missing_blob.dts',
                              allow_missing=True)
         err = stderr.getvalue()
-        self.assertRegex(err, "Image 'main-section'.*missing.*: blob-ext")
+        self.assertRegex(err, "Image 'main-section'.*missing.*: atf-bl31")
 
     def testBlobNamedByArgMissing(self):
         """Test handling of a missing entry arg"""
@@ -3692,5 +3692,21 @@
         self.assertIn("default-dt entry argument 'test-fdt3' not found in fdt list: test-fdt1, test-fdt2",
                       str(e.exception))
 
+    def testFitExtblobMissingHelp(self):
+        """Test display of help messages when an external blob is missing"""
+        control.missing_blob_help = control._ReadMissingBlobHelp()
+        control.missing_blob_help['wibble'] = 'Wibble test'
+        control.missing_blob_help['another'] = 'Another test'
+        with test_util.capture_sys_output() as (stdout, stderr):
+            self._DoTestFile('168_fit_missing_blob.dts',
+                             allow_missing=True)
+        err = stderr.getvalue()
+
+        # We can get the tag from the name, the type or the missing-msg
+        # property. Check all three.
+        self.assertIn('You may need to build ARM Trusted', err)
+        self.assertIn('Wibble test', err)
+        self.assertIn('Another test', err)
+
 if __name__ == "__main__":
     unittest.main()
diff --git a/tools/binman/missing-blob-help b/tools/binman/missing-blob-help
new file mode 100644
index 0000000..3de195c
--- /dev/null
+++ b/tools/binman/missing-blob-help
@@ -0,0 +1,11 @@
+# This file contains help messages for missing external blobs. Each message has
+# a tag (MUST be just lower-case text, digits and hyphens) starting in column 1,
+# followed by a colon (:) to indicate its start. The message can include any
+# number of lines, including blank lines.
+#
+# When looking for a tag, Binman uses the value of 'missing-msg' for the entry,
+# the entry name or the entry type, in that order
+
+atf-bl31:
+See the documentation for your board. You may need to build ARM Trusted
+Firmware and build with BL31=/path/to/bl31.bin
diff --git a/tools/binman/test/168_fit_missing_blob.dts b/tools/binman/test/168_fit_missing_blob.dts
index e007aa4..15f6cc0 100644
--- a/tools/binman/test/168_fit_missing_blob.dts
+++ b/tools/binman/test/168_fit_missing_blob.dts
@@ -29,9 +29,16 @@
 					hash-2 {
 						algo = "sha1";
 					};
-					blob-ext {
+					atf-bl31 {
 						filename = "missing";
 					};
+					cros-ec-rw {
+						type = "atf-bl31";
+						missing-msg = "wibble";
+					};
+					another {
+						type = "atf-bl31";
+					};
 				};
 			};
 		};