binman: Add command-line support for replacing entries

Add a 'replace' command to binman to permit entries to be replaced, either
individually or all at once (using a filter).

Signed-off-by: Simon Glass <sjg@chromium.org>
This commit is contained in:
Simon Glass 2019-07-20 12:24:15 -06:00
parent bb5edc1d3c
commit a6cb995096
5 changed files with 327 additions and 4 deletions

View File

@ -589,9 +589,24 @@ that there is an 'fdtmap' entry in the image. For example:
$ binman replace -i image.bin section/cbfs/u-boot
which will write the contents of the file 'u-boot' from the current directory
to the that entry. If the entry size changes, you must add the 'allow-repack'
property to the original image before generating it (see above), otherwise you
will get an error.
to the that entry, compressing if necessary. If the entry size changes, you must
add the 'allow-repack' property to the original image before generating it (see
above), otherwise you will get an error.
You can also use a particular file, in this case u-boot.bin:
$ binman replace -i image.bin section/cbfs/u-boot -f u-boot.bin
It is possible to replace all files from a source directory which uses the same
hierarchy as the entries:
$ binman replace -i image.bin -I indir
Files that are missing will generate a warning.
You can also replace just a selection of entries:
$ binman replace -i image.bin "*u-boot*" -I indir
Logging
@ -959,7 +974,6 @@ Some ideas:
- Allow easy building of images by specifying just the board name
- Support building an image for a board (-b) more completely, with a
configurable build directory
- Support updating binaries in an image (with repacking)
- Support adding FITs to an image
- Support for ARM Trusted Firmware (ATF)
- Detect invalid properties in nodes

View File

@ -84,6 +84,23 @@ controlled by a description in the board device tree.'''
extract_parser.add_argument('-U', '--uncompressed', action='store_true',
help='Output raw uncompressed data for compressed entries')
replace_parser = subparsers.add_parser('replace',
help='Replace entries in an image')
replace_parser.add_argument('-C', '--compressed', action='store_true',
help='Input data is already compressed if needed for the entry')
replace_parser.add_argument('-i', '--image', type=str, required=True,
help='Image filename to extract')
replace_parser.add_argument('-f', '--filename', type=str,
help='Input filename to read from')
replace_parser.add_argument('-F', '--fix-size', action='store_true',
help="Don't allow entries to be resized")
replace_parser.add_argument('-I', '--indir', type=str, default='',
help='Path to directory to use for input files')
replace_parser.add_argument('-m', '--map', action='store_true',
default=False, help='Output a map file for the updated image')
replace_parser.add_argument('paths', type=str, nargs='*',
help='Paths within file to extract (wildcard)')
test_parser = subparsers.add_parser('test', help='Run tests')
test_parser.add_argument('-P', '--processes', type=int,
help='set number of processes to use for running tests')

View File

@ -253,6 +253,71 @@ def WriteEntry(image_fname, entry_path, data, do_compress=True,
return image
def ReplaceEntries(image_fname, input_fname, indir, entry_paths,
do_compress=True, allow_resize=True, write_map=False):
"""Replace the data from one or more entries from input files
Args:
image_fname: Image filename to process
input_fname: Single input ilename to use if replacing one file, None
otherwise
indir: Input directory to use (for any number of files), else None
entry_paths: List of entry paths to extract
do_compress: True if the input data is uncompressed and may need to be
compressed if the entry requires it, False if the data is already
compressed.
write_map: True to write a map file
Returns:
List of EntryInfo records that were written
"""
image = Image.FromFile(image_fname)
# Replace an entry from a single file, as a special case
if input_fname:
if not entry_paths:
raise ValueError('Must specify an entry path to read with -f')
if len(entry_paths) != 1:
raise ValueError('Must specify exactly one entry path to write with -f')
entry = image.FindEntryPath(entry_paths[0])
data = tools.ReadFile(input_fname)
tout.Notice("Read %#x bytes from file '%s'" % (len(data), input_fname))
WriteEntryToImage(image, entry, data, do_compress=do_compress,
allow_resize=allow_resize, write_map=write_map)
return
# Otherwise we will input from a path given by the entry path of each entry.
# This means that files must appear in subdirectories if they are part of
# a sub-section.
einfos = image.GetListEntries(entry_paths)[0]
tout.Notice("Replacing %d matching entries in image '%s'" %
(len(einfos), image_fname))
BeforeReplace(image, allow_resize)
for einfo in einfos:
entry = einfo.entry
if entry.GetEntries():
tout.Info("Skipping section entry '%s'" % entry.GetPath())
continue
path = entry.GetPath()[1:]
fname = os.path.join(indir, path)
if os.path.exists(fname):
tout.Notice("Write entry '%s' from file '%s'" %
(entry.GetPath(), fname))
data = tools.ReadFile(fname)
ReplaceOneEntry(image, entry, data, do_compress, allow_resize)
else:
tout.Warning("Skipping entry '%s' from missing file '%s'" %
(entry.GetPath(), fname))
AfterReplace(image, allow_resize=allow_resize, write_map=write_map)
return image
def PrepareImagesAndDtbs(dtb_fname, select_images, update_fdt):
"""Prepare the images to be processed and select the device tree
@ -420,6 +485,16 @@ def Binman(args):
tools.FinaliseOutputDir()
return 0
if args.cmd == 'replace':
try:
tools.PrepareOutputDir(None)
ReplaceEntries(args.image, args.filename, args.indir, args.paths,
do_compress=not args.compressed,
allow_resize=not args.fix_size, write_map=args.map)
finally:
tools.FinaliseOutputDir()
return 0
# Try to figure out which device tree contains our image description
if args.dt:
dtb_fname = args.dt

View File

@ -3047,6 +3047,195 @@ class TestFunctional(unittest.TestCase):
data = control.ReadEntry(updated_fname, entry_name)
self.assertEqual(expected, data)
def _SetupForReplace(self):
"""Set up some files to use to replace entries
This generates an image, copies it to a new file, extracts all the files
in it and updates some of them
Returns:
List
Image filename
Output directory
Expected values for updated entries, each a string
"""
data = self._DoReadFileRealDtb('143_replace_all.dts')
updated_fname = tools.GetOutputFilename('image-updated.bin')
tools.WriteFile(updated_fname, data)
outdir = os.path.join(self._indir, 'extract')
einfos = control.ExtractEntries(updated_fname, None, outdir, [])
expected1 = b'x' + U_BOOT_DATA + b'y'
u_boot_fname1 = os.path.join(outdir, 'u-boot')
tools.WriteFile(u_boot_fname1, expected1)
expected2 = b'a' + U_BOOT_DATA + b'b'
u_boot_fname2 = os.path.join(outdir, 'u-boot2')
tools.WriteFile(u_boot_fname2, expected2)
expected_text = b'not the same text'
text_fname = os.path.join(outdir, 'text')
tools.WriteFile(text_fname, expected_text)
dtb_fname = os.path.join(outdir, 'u-boot-dtb')
dtb = fdt.FdtScan(dtb_fname)
node = dtb.GetNode('/binman/text')
node.AddString('my-property', 'the value')
dtb.Sync(auto_resize=True)
dtb.Flush()
return updated_fname, outdir, expected1, expected2, expected_text
def _CheckReplaceMultiple(self, entry_paths):
"""Handle replacing the contents of multiple entries
Args:
entry_paths: List of entry paths to replace
Returns:
List
Dict of entries in the image:
key: Entry name
Value: Entry object
Expected values for updated entries, each a string
"""
updated_fname, outdir, expected1, expected2, expected_text = (
self._SetupForReplace())
control.ReplaceEntries(updated_fname, None, outdir, entry_paths)
image = Image.FromFile(updated_fname)
image.LoadData()
return image.GetEntries(), expected1, expected2, expected_text
def testReplaceAll(self):
"""Test replacing the contents of all entries"""
entries, expected1, expected2, expected_text = (
self._CheckReplaceMultiple([]))
data = entries['u-boot'].data
self.assertEqual(expected1, data)
data = entries['u-boot2'].data
self.assertEqual(expected2, data)
data = entries['text'].data
self.assertEqual(expected_text, data)
# Check that the device tree is updated
data = entries['u-boot-dtb'].data
dtb = fdt.Fdt.FromData(data)
dtb.Scan()
node = dtb.GetNode('/binman/text')
self.assertEqual('the value', node.props['my-property'].value)
def testReplaceSome(self):
"""Test replacing the contents of a few entries"""
entries, expected1, expected2, expected_text = (
self._CheckReplaceMultiple(['u-boot2', 'text']))
# This one should not change
data = entries['u-boot'].data
self.assertEqual(U_BOOT_DATA, data)
data = entries['u-boot2'].data
self.assertEqual(expected2, data)
data = entries['text'].data
self.assertEqual(expected_text, data)
def testReplaceCmd(self):
"""Test replacing a file fron an image on the command line"""
self._DoReadFileRealDtb('143_replace_all.dts')
try:
tmpdir, updated_fname = self._SetupImageInTmpdir()
fname = os.path.join(tmpdir, 'update-u-boot.bin')
expected = b'x' * len(U_BOOT_DATA)
tools.WriteFile(fname, expected)
self._DoBinman('replace', '-i', updated_fname, 'u-boot', '-f', fname)
data = tools.ReadFile(updated_fname)
self.assertEqual(expected, data[:len(expected)])
map_fname = os.path.join(tmpdir, 'image-updated.map')
self.assertFalse(os.path.exists(map_fname))
finally:
shutil.rmtree(tmpdir)
def testReplaceCmdSome(self):
"""Test replacing some files fron an image on the command line"""
updated_fname, outdir, expected1, expected2, expected_text = (
self._SetupForReplace())
self._DoBinman('replace', '-i', updated_fname, '-I', outdir,
'u-boot2', 'text')
tools.PrepareOutputDir(None)
image = Image.FromFile(updated_fname)
image.LoadData()
entries = image.GetEntries()
# This one should not change
data = entries['u-boot'].data
self.assertEqual(U_BOOT_DATA, data)
data = entries['u-boot2'].data
self.assertEqual(expected2, data)
data = entries['text'].data
self.assertEqual(expected_text, data)
def testReplaceMissing(self):
"""Test replacing entries where the file is missing"""
updated_fname, outdir, expected1, expected2, expected_text = (
self._SetupForReplace())
# Remove one of the files, to generate a warning
u_boot_fname1 = os.path.join(outdir, 'u-boot')
os.remove(u_boot_fname1)
with test_util.capture_sys_output() as (stdout, stderr):
control.ReplaceEntries(updated_fname, None, outdir, [])
self.assertIn("Skipping entry '/u-boot' from missing file",
stdout.getvalue())
def testReplaceCmdMap(self):
"""Test replacing a file fron an image on the command line"""
self._DoReadFileRealDtb('143_replace_all.dts')
try:
tmpdir, updated_fname = self._SetupImageInTmpdir()
fname = os.path.join(self._indir, 'update-u-boot.bin')
expected = b'x' * len(U_BOOT_DATA)
tools.WriteFile(fname, expected)
self._DoBinman('replace', '-i', updated_fname, 'u-boot',
'-f', fname, '-m')
map_fname = os.path.join(tmpdir, 'image-updated.map')
self.assertTrue(os.path.exists(map_fname))
finally:
shutil.rmtree(tmpdir)
def testReplaceNoEntryPaths(self):
"""Test replacing an entry without an entry path"""
self._DoReadFileRealDtb('143_replace_all.dts')
image_fname = tools.GetOutputFilename('image.bin')
with self.assertRaises(ValueError) as e:
control.ReplaceEntries(image_fname, 'fname', None, [])
self.assertIn('Must specify an entry path to read with -f',
str(e.exception))
def testReplaceTooManyEntryPaths(self):
"""Test extracting some entries"""
self._DoReadFileRealDtb('143_replace_all.dts')
image_fname = tools.GetOutputFilename('image.bin')
with self.assertRaises(ValueError) as e:
control.ReplaceEntries(image_fname, 'fname', None, ['a', 'b'])
self.assertIn('Must specify exactly one entry path to write with -f',
str(e.exception))
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,28 @@
// SPDX-License-Identifier: GPL-2.0+
/dts-v1/;
/ {
#address-cells = <1>;
#size-cells = <1>;
binman {
size = <0xc00>;
allow-repack;
u-boot {
};
fdtmap {
};
u-boot2 {
type = "u-boot";
};
text {
text = "some text";
};
u-boot-dtb {
};
image-header {
location = "end";
};
};
};