binman: Add support for outputing a map file

It is useful to be able to see a list of regions in each image produced by
binman. Add a -m option to output this information in a '.map' file
alongside the image file.

Signed-off-by: Simon Glass <sjg@chromium.org>
diff --git a/tools/binman/README b/tools/binman/README
index f3a979e..64e529f 100644
--- a/tools/binman/README
+++ b/tools/binman/README
@@ -548,6 +548,25 @@
 to fill in such symbols in U-Boot proper, as well.
 
 
+Map files
+---------
+
+The -m option causes binman to output a .map file for each image that it
+generates. This shows the position and size of each entry. For example:
+
+    Position      Size  Name
+    00000000  00000010  section@0
+     00000000  00000004  u-boot
+    00000010  00000010  section@1
+     00000000  00000004  u-boot
+
+This shows a hierarchical image with two sections, each with a single entry. The
+positions of the sections are absolute hex byte offsets within the image. The
+positions of the entries are relative to their respective sections. The size of
+each entry is also shown, in bytes (hex). The indentation shows the entries
+nested inside their sections.
+
+
 Code coverage
 -------------
 
@@ -628,7 +647,6 @@
   'Access to binman entry positions at run time' above
 - Use of-platdata to make the information available to code that is unable
   to use device tree (such as a very small SPL image)
-- Write an image map to a text file
 - Allow easy building of images by specifying just the board name
 - Produce a full Python binding for libfdt (for upstream)
 - Add an option to decode an image into the constituent binaries
diff --git a/tools/binman/bsection.py b/tools/binman/bsection.py
index 331f806..e0960d4 100644
--- a/tools/binman/bsection.py
+++ b/tools/binman/bsection.py
@@ -301,3 +301,12 @@
 
     def GetEntries(self):
         return self._entries
+
+    def WriteMap(self, fd, indent):
+        """Write a map of the section to a .map file
+
+        Args:
+            fd: File to write the map to
+        """
+        for entry in self._entries.values():
+            entry.WriteMap(fd, indent)
diff --git a/tools/binman/cmdline.py b/tools/binman/cmdline.py
index e9e0434..bf63919 100644
--- a/tools/binman/cmdline.py
+++ b/tools/binman/cmdline.py
@@ -30,6 +30,8 @@
             help='Add a path to a directory to use for input files')
     parser.add_option('-H', '--full-help', action='store_true',
         default=False, help='Display the README file')
+    parser.add_option('-m', '--map', action='store_true',
+        default=False, help='Output a map file for each image')
     parser.add_option('-O', '--outdir', type='string',
         action='store', help='Path to directory to use for intermediate and '
         'output files')
diff --git a/tools/binman/control.py b/tools/binman/control.py
index bc8ed8e..9243472 100644
--- a/tools/binman/control.py
+++ b/tools/binman/control.py
@@ -112,6 +112,8 @@
                 image.ProcessEntryContents()
                 image.WriteSymbols()
                 image.BuildImage()
+                if options.map:
+                    image.WriteMap()
         finally:
             tools.FinaliseOutputDir()
     finally:
diff --git a/tools/binman/entry.py b/tools/binman/entry.py
index 8b46fbb..3811d33 100644
--- a/tools/binman/entry.py
+++ b/tools/binman/entry.py
@@ -4,6 +4,8 @@
 # Base class for all entries
 #
 
+from __future__ import print_function
+
 # importlib was introduced in Python 2.7 but there was a report of it not
 # working in 2.7.12, so we work around this:
 # http://lists.denx.de/pipermail/u-boot/2016-October/269729.html
@@ -50,6 +52,7 @@
         self.section = section
         self.etype = etype
         self._node = node
+        self.name = node and node.name or 'none'
         self.pos = None
         self.size = None
         self.contents_size = 0
@@ -229,3 +232,13 @@
         this function and raise if there is a problem.
         """
         pass
+
+    def WriteMap(self, fd, indent):
+        """Write a map of the entry to a .map file
+
+        Args:
+            fd: File to write the map to
+            indent: Curent indent level of map (0=none, 1=one level, etc.)
+        """
+        print('%s%08x  %08x  %s' % (' ' * indent, self.pos, self.size,
+                                    self.name), file=fd)
diff --git a/tools/binman/etype/section.py b/tools/binman/etype/section.py
index 4e2f686..139fcad 100644
--- a/tools/binman/etype/section.py
+++ b/tools/binman/etype/section.py
@@ -48,3 +48,12 @@
 
     def CheckPosition(self):
         self._section.CheckEntries()
+
+    def WriteMap(self, fd, indent):
+        """Write a map of the section to a .map file
+
+        Args:
+            fd: File to write the map to
+        """
+        super(Entry_section, self).WriteMap(fd, indent)
+        self._section.WriteMap(fd, indent + 1)
diff --git a/tools/binman/ftest.py b/tools/binman/ftest.py
index 9e12df5..61bbb53 100644
--- a/tools/binman/ftest.py
+++ b/tools/binman/ftest.py
@@ -146,16 +146,19 @@
         # options.verbosity = tout.DEBUG
         return control.Binman(options, args)
 
-    def _DoTestFile(self, fname, debug=False):
+    def _DoTestFile(self, fname, debug=False, map=False):
         """Run binman with a given test file
 
         Args:
             fname: Device-tree source filename to use (e.g. 05_simple.dts)
             debug: True to enable debugging output
+            map: True to output map files for the images
         """
         args = ['-p', '-I', self._indir, '-d', self.TestFile(fname)]
         if debug:
             args.append('-D')
+        if map:
+            args.append('-m')
         return self._DoBinman(*args)
 
     def _SetupDtb(self, fname, outfile='u-boot.dtb'):
@@ -180,7 +183,7 @@
             TestFunctional._MakeInputFile(outfile, data)
             return data
 
-    def _DoReadFileDtb(self, fname, use_real_dtb=False):
+    def _DoReadFileDtb(self, fname, use_real_dtb=False, map=False):
         """Run binman and return the resulting image
 
         This runs binman with a given test file and then reads the resulting
@@ -195,11 +198,13 @@
                 the u-boot-dtb entry. Normally this is not needed and the
                 test contents (the U_BOOT_DTB_DATA string) can be used.
                 But in some test we need the real contents.
+            map: True to output map files for the images
 
         Returns:
             Tuple:
                 Resulting image contents
                 Device tree contents
+                Map data showing contents of image (or None if none)
         """
         dtb_data = None
         # Use the compiled test file as the u-boot-dtb input
@@ -207,15 +212,21 @@
             dtb_data = self._SetupDtb(fname)
 
         try:
-            retcode = self._DoTestFile(fname)
+            retcode = self._DoTestFile(fname, map=map)
             self.assertEqual(0, retcode)
 
             # Find the (only) image, read it and return its contents
             image = control.images['image']
             fname = tools.GetOutputFilename('image.bin')
             self.assertTrue(os.path.exists(fname))
+            if map:
+                map_fname = tools.GetOutputFilename('image.map')
+                with open(map_fname) as fd:
+                    map_data = fd.read()
+            else:
+                map_data = None
             with open(fname) as fd:
-                return fd.read(), dtb_data
+                return fd.read(), dtb_data, map_data
         finally:
             # Put the test file back
             if use_real_dtb:
@@ -815,7 +826,7 @@
         """Test that we can cope with an image without microcode (e.g. qemu)"""
         with open(self.TestFile('u_boot_no_ucode_ptr')) as fd:
             TestFunctional._MakeInputFile('u-boot', fd.read())
-        data, dtb = self._DoReadFileDtb('44_x86_optional_ucode.dts', True)
+        data, dtb, _ = self._DoReadFileDtb('44_x86_optional_ucode.dts', True)
 
         # Now check the device tree has no microcode
         self.assertEqual(U_BOOT_NODTB_DATA, data[:len(U_BOOT_NODTB_DATA)])
@@ -929,5 +940,15 @@
         expected = U_BOOT_DATA + '!' * 12 + U_BOOT_DATA + 'a' * 12 + '&' * 8
         self.assertEqual(expected, data)
 
+    def testMap(self):
+        """Tests outputting a map of the images"""
+        _, _, map_data = self._DoReadFileDtb('55_sections.dts', map=True)
+        self.assertEqual('''Position      Size  Name
+00000000  00000010  section@0
+ 00000000  00000004  u-boot
+00000010  00000010  section@1
+ 00000000  00000004  u-boot
+''', map_data)
+
 if __name__ == "__main__":
     unittest.main()
diff --git a/tools/binman/image.py b/tools/binman/image.py
index 74bc46f..835b66c 100644
--- a/tools/binman/image.py
+++ b/tools/binman/image.py
@@ -98,3 +98,11 @@
 
     def GetEntries(self):
         return self._section.GetEntries()
+
+    def WriteMap(self):
+        """Write a map of the image to a .map file"""
+        filename = '%s.map' % self._name
+        fname = tools.GetOutputFilename(filename)
+        with open(fname, 'w') as fd:
+            print('%8s  %8s  %s' % ('Position', 'Size', 'Name'), file=fd)
+            self._section.WriteMap(fd, 0)