3b3e3c0f6c
At present all the 'command' methods return bytes. Most of the time we actually want strings, so change this. We still need to keep the internal representation as bytes since otherwise unicode strings might break over a read() boundary (e.g. 4KB), causing errors. But we can convert the end result to strings. Add a 'binary' parameter to cover the few cases where bytes are needed. Signed-off-by: Simon Glass <sjg@chromium.org>
519 lines
14 KiB
Python
519 lines
14 KiB
Python
# SPDX-License-Identifier: GPL-2.0+
|
|
#
|
|
# Copyright (c) 2016 Google, Inc
|
|
#
|
|
|
|
from __future__ import print_function
|
|
|
|
import command
|
|
import glob
|
|
import os
|
|
import shutil
|
|
import struct
|
|
import sys
|
|
import tempfile
|
|
|
|
import tout
|
|
|
|
# Output directly (generally this is temporary)
|
|
outdir = None
|
|
|
|
# True to keep the output directory around after exiting
|
|
preserve_outdir = False
|
|
|
|
# Path to the Chrome OS chroot, if we know it
|
|
chroot_path = None
|
|
|
|
# Search paths to use for Filename(), used to find files
|
|
search_paths = []
|
|
|
|
tool_search_paths = []
|
|
|
|
# Tools and the packages that contain them, on debian
|
|
packages = {
|
|
'lz4': 'liblz4-tool',
|
|
}
|
|
|
|
# List of paths to use when looking for an input file
|
|
indir = []
|
|
|
|
def PrepareOutputDir(dirname, preserve=False):
|
|
"""Select an output directory, ensuring it exists.
|
|
|
|
This either creates a temporary directory or checks that the one supplied
|
|
by the user is valid. For a temporary directory, it makes a note to
|
|
remove it later if required.
|
|
|
|
Args:
|
|
dirname: a string, name of the output directory to use to store
|
|
intermediate and output files. If is None - create a temporary
|
|
directory.
|
|
preserve: a Boolean. If outdir above is None and preserve is False, the
|
|
created temporary directory will be destroyed on exit.
|
|
|
|
Raises:
|
|
OSError: If it cannot create the output directory.
|
|
"""
|
|
global outdir, preserve_outdir
|
|
|
|
preserve_outdir = dirname or preserve
|
|
if dirname:
|
|
outdir = dirname
|
|
if not os.path.isdir(outdir):
|
|
try:
|
|
os.makedirs(outdir)
|
|
except OSError as err:
|
|
raise CmdError("Cannot make output directory '%s': '%s'" %
|
|
(outdir, err.strerror))
|
|
tout.Debug("Using output directory '%s'" % outdir)
|
|
else:
|
|
outdir = tempfile.mkdtemp(prefix='binman.')
|
|
tout.Debug("Using temporary directory '%s'" % outdir)
|
|
|
|
def _RemoveOutputDir():
|
|
global outdir
|
|
|
|
shutil.rmtree(outdir)
|
|
tout.Debug("Deleted temporary directory '%s'" % outdir)
|
|
outdir = None
|
|
|
|
def FinaliseOutputDir():
|
|
global outdir, preserve_outdir
|
|
|
|
"""Tidy up: delete output directory if temporary and not preserved."""
|
|
if outdir and not preserve_outdir:
|
|
_RemoveOutputDir()
|
|
outdir = None
|
|
|
|
def GetOutputFilename(fname):
|
|
"""Return a filename within the output directory.
|
|
|
|
Args:
|
|
fname: Filename to use for new file
|
|
|
|
Returns:
|
|
The full path of the filename, within the output directory
|
|
"""
|
|
return os.path.join(outdir, fname)
|
|
|
|
def _FinaliseForTest():
|
|
"""Remove the output directory (for use by tests)"""
|
|
global outdir
|
|
|
|
if outdir:
|
|
_RemoveOutputDir()
|
|
outdir = None
|
|
|
|
def SetInputDirs(dirname):
|
|
"""Add a list of input directories, where input files are kept.
|
|
|
|
Args:
|
|
dirname: a list of paths to input directories to use for obtaining
|
|
files needed by binman to place in the image.
|
|
"""
|
|
global indir
|
|
|
|
indir = dirname
|
|
tout.Debug("Using input directories %s" % indir)
|
|
|
|
def GetInputFilename(fname):
|
|
"""Return a filename for use as input.
|
|
|
|
Args:
|
|
fname: Filename to use for new file
|
|
|
|
Returns:
|
|
The full path of the filename, within the input directory
|
|
"""
|
|
if not indir or fname[:1] == '/':
|
|
return fname
|
|
for dirname in indir:
|
|
pathname = os.path.join(dirname, fname)
|
|
if os.path.exists(pathname):
|
|
return pathname
|
|
|
|
raise ValueError("Filename '%s' not found in input path (%s) (cwd='%s')" %
|
|
(fname, ','.join(indir), os.getcwd()))
|
|
|
|
def GetInputFilenameGlob(pattern):
|
|
"""Return a list of filenames for use as input.
|
|
|
|
Args:
|
|
pattern: Filename pattern to search for
|
|
|
|
Returns:
|
|
A list of matching files in all input directories
|
|
"""
|
|
if not indir:
|
|
return glob.glob(fname)
|
|
files = []
|
|
for dirname in indir:
|
|
pathname = os.path.join(dirname, pattern)
|
|
files += glob.glob(pathname)
|
|
return sorted(files)
|
|
|
|
def Align(pos, align):
|
|
if align:
|
|
mask = align - 1
|
|
pos = (pos + mask) & ~mask
|
|
return pos
|
|
|
|
def NotPowerOfTwo(num):
|
|
return num and (num & (num - 1))
|
|
|
|
def SetToolPaths(toolpaths):
|
|
"""Set the path to search for tools
|
|
|
|
Args:
|
|
toolpaths: List of paths to search for tools executed by Run()
|
|
"""
|
|
global tool_search_paths
|
|
|
|
tool_search_paths = toolpaths
|
|
|
|
def PathHasFile(path_spec, fname):
|
|
"""Check if a given filename is in the PATH
|
|
|
|
Args:
|
|
path_spec: Value of PATH variable to check
|
|
fname: Filename to check
|
|
|
|
Returns:
|
|
True if found, False if not
|
|
"""
|
|
for dir in path_spec.split(':'):
|
|
if os.path.exists(os.path.join(dir, fname)):
|
|
return True
|
|
return False
|
|
|
|
def Run(name, *args, **kwargs):
|
|
"""Run a tool with some arguments
|
|
|
|
This runs a 'tool', which is a program used by binman to process files and
|
|
perhaps produce some output. Tools can be located on the PATH or in a
|
|
search path.
|
|
|
|
Args:
|
|
name: Command name to run
|
|
args: Arguments to the tool
|
|
|
|
Returns:
|
|
CommandResult object
|
|
"""
|
|
try:
|
|
binary = kwargs.get('binary')
|
|
env = None
|
|
if tool_search_paths:
|
|
env = dict(os.environ)
|
|
env['PATH'] = ':'.join(tool_search_paths) + ':' + env['PATH']
|
|
all_args = (name,) + args
|
|
result = command.RunPipe([all_args], capture=True, capture_stderr=True,
|
|
env=env, raise_on_error=False, binary=binary)
|
|
if result.return_code:
|
|
raise Exception("Error %d running '%s': %s" %
|
|
(result.return_code,' '.join(all_args),
|
|
result.stderr))
|
|
return result.stdout
|
|
except:
|
|
if env and not PathHasFile(env['PATH'], name):
|
|
msg = "Please install tool '%s'" % name
|
|
package = packages.get(name)
|
|
if package:
|
|
msg += " (e.g. from package '%s')" % package
|
|
raise ValueError(msg)
|
|
raise
|
|
|
|
def Filename(fname):
|
|
"""Resolve a file path to an absolute path.
|
|
|
|
If fname starts with ##/ and chroot is available, ##/ gets replaced with
|
|
the chroot path. If chroot is not available, this file name can not be
|
|
resolved, `None' is returned.
|
|
|
|
If fname is not prepended with the above prefix, and is not an existing
|
|
file, the actual file name is retrieved from the passed in string and the
|
|
search_paths directories (if any) are searched to for the file. If found -
|
|
the path to the found file is returned, `None' is returned otherwise.
|
|
|
|
Args:
|
|
fname: a string, the path to resolve.
|
|
|
|
Returns:
|
|
Absolute path to the file or None if not found.
|
|
"""
|
|
if fname.startswith('##/'):
|
|
if chroot_path:
|
|
fname = os.path.join(chroot_path, fname[3:])
|
|
else:
|
|
return None
|
|
|
|
# Search for a pathname that exists, and return it if found
|
|
if fname and not os.path.exists(fname):
|
|
for path in search_paths:
|
|
pathname = os.path.join(path, os.path.basename(fname))
|
|
if os.path.exists(pathname):
|
|
return pathname
|
|
|
|
# If not found, just return the standard, unchanged path
|
|
return fname
|
|
|
|
def ReadFile(fname, binary=True):
|
|
"""Read and return the contents of a file.
|
|
|
|
Args:
|
|
fname: path to filename to read, where ## signifiies the chroot.
|
|
|
|
Returns:
|
|
data read from file, as a string.
|
|
"""
|
|
with open(Filename(fname), binary and 'rb' or 'r') as fd:
|
|
data = fd.read()
|
|
#self._out.Info("Read file '%s' size %d (%#0x)" %
|
|
#(fname, len(data), len(data)))
|
|
return data
|
|
|
|
def WriteFile(fname, data):
|
|
"""Write data into a file.
|
|
|
|
Args:
|
|
fname: path to filename to write
|
|
data: data to write to file, as a string
|
|
"""
|
|
#self._out.Info("Write file '%s' size %d (%#0x)" %
|
|
#(fname, len(data), len(data)))
|
|
with open(Filename(fname), 'wb') as fd:
|
|
fd.write(data)
|
|
|
|
def GetBytes(byte, size):
|
|
"""Get a string of bytes of a given size
|
|
|
|
This handles the unfortunate different between Python 2 and Python 2.
|
|
|
|
Args:
|
|
byte: Numeric byte value to use
|
|
size: Size of bytes/string to return
|
|
|
|
Returns:
|
|
A bytes type with 'byte' repeated 'size' times
|
|
"""
|
|
if sys.version_info[0] >= 3:
|
|
data = bytes([byte]) * size
|
|
else:
|
|
data = chr(byte) * size
|
|
return data
|
|
|
|
def ToUnicode(val):
|
|
"""Make sure a value is a unicode string
|
|
|
|
This allows some amount of compatibility between Python 2 and Python3. For
|
|
the former, it returns a unicode object.
|
|
|
|
Args:
|
|
val: string or unicode object
|
|
|
|
Returns:
|
|
unicode version of val
|
|
"""
|
|
if sys.version_info[0] >= 3:
|
|
return val
|
|
return val if isinstance(val, unicode) else val.decode('utf-8')
|
|
|
|
def FromUnicode(val):
|
|
"""Make sure a value is a non-unicode string
|
|
|
|
This allows some amount of compatibility between Python 2 and Python3. For
|
|
the former, it converts a unicode object to a string.
|
|
|
|
Args:
|
|
val: string or unicode object
|
|
|
|
Returns:
|
|
non-unicode version of val
|
|
"""
|
|
if sys.version_info[0] >= 3:
|
|
return val
|
|
return val if isinstance(val, str) else val.encode('utf-8')
|
|
|
|
def ToByte(ch):
|
|
"""Convert a character to an ASCII value
|
|
|
|
This is useful because in Python 2 bytes is an alias for str, but in
|
|
Python 3 they are separate types. This function converts the argument to
|
|
an ASCII value in either case.
|
|
|
|
Args:
|
|
ch: A string (Python 2) or byte (Python 3) value
|
|
|
|
Returns:
|
|
integer ASCII value for ch
|
|
"""
|
|
return ord(ch) if type(ch) == str else ch
|
|
|
|
def ToChar(byte):
|
|
"""Convert a byte to a character
|
|
|
|
This is useful because in Python 2 bytes is an alias for str, but in
|
|
Python 3 they are separate types. This function converts an ASCII value to
|
|
a value with the appropriate type in either case.
|
|
|
|
Args:
|
|
byte: A byte or str value
|
|
"""
|
|
return chr(byte) if type(byte) != str else byte
|
|
|
|
def ToChars(byte_list):
|
|
"""Convert a list of bytes to a str/bytes type
|
|
|
|
Args:
|
|
byte_list: List of ASCII values representing the string
|
|
|
|
Returns:
|
|
string made by concatenating all the ASCII values
|
|
"""
|
|
return ''.join([chr(byte) for byte in byte_list])
|
|
|
|
def ToBytes(string):
|
|
"""Convert a str type into a bytes type
|
|
|
|
Args:
|
|
string: string to convert
|
|
|
|
Returns:
|
|
Python 3: A bytes type
|
|
Python 2: A string type
|
|
"""
|
|
if sys.version_info[0] >= 3:
|
|
return string.encode('utf-8')
|
|
return string
|
|
|
|
def ToString(bval):
|
|
"""Convert a bytes type into a str type
|
|
|
|
Args:
|
|
bval: bytes value to convert
|
|
|
|
Returns:
|
|
Python 3: A bytes type
|
|
Python 2: A string type
|
|
"""
|
|
return bval.decode('utf-8')
|
|
|
|
def Compress(indata, algo, with_header=True):
|
|
"""Compress some data using a given algorithm
|
|
|
|
Note that for lzma this uses an old version of the algorithm, not that
|
|
provided by xz.
|
|
|
|
This requires 'lz4' and 'lzma_alone' tools. It also requires an output
|
|
directory to be previously set up, by calling PrepareOutputDir().
|
|
|
|
Args:
|
|
indata: Input data to compress
|
|
algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
|
|
|
|
Returns:
|
|
Compressed data
|
|
"""
|
|
if algo == 'none':
|
|
return indata
|
|
fname = GetOutputFilename('%s.comp.tmp' % algo)
|
|
WriteFile(fname, indata)
|
|
if algo == 'lz4':
|
|
data = Run('lz4', '--no-frame-crc', '-c', fname, binary=True)
|
|
# cbfstool uses a very old version of lzma
|
|
elif algo == 'lzma':
|
|
outfname = GetOutputFilename('%s.comp.otmp' % algo)
|
|
Run('lzma_alone', 'e', fname, outfname, '-lc1', '-lp0', '-pb0', '-d8')
|
|
data = ReadFile(outfname)
|
|
elif algo == 'gzip':
|
|
data = Run('gzip', '-c', fname, binary=True)
|
|
else:
|
|
raise ValueError("Unknown algorithm '%s'" % algo)
|
|
if with_header:
|
|
hdr = struct.pack('<I', len(data))
|
|
data = hdr + data
|
|
return data
|
|
|
|
def Decompress(indata, algo, with_header=True):
|
|
"""Decompress some data using a given algorithm
|
|
|
|
Note that for lzma this uses an old version of the algorithm, not that
|
|
provided by xz.
|
|
|
|
This requires 'lz4' and 'lzma_alone' tools. It also requires an output
|
|
directory to be previously set up, by calling PrepareOutputDir().
|
|
|
|
Args:
|
|
indata: Input data to decompress
|
|
algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
|
|
|
|
Returns:
|
|
Compressed data
|
|
"""
|
|
if algo == 'none':
|
|
return indata
|
|
if with_header:
|
|
data_len = struct.unpack('<I', indata[:4])[0]
|
|
indata = indata[4:4 + data_len]
|
|
fname = GetOutputFilename('%s.decomp.tmp' % algo)
|
|
with open(fname, 'wb') as fd:
|
|
fd.write(indata)
|
|
if algo == 'lz4':
|
|
data = Run('lz4', '-dc', fname, binary=True)
|
|
elif algo == 'lzma':
|
|
outfname = GetOutputFilename('%s.decomp.otmp' % algo)
|
|
Run('lzma_alone', 'd', fname, outfname)
|
|
data = ReadFile(outfname, binary=True)
|
|
elif algo == 'gzip':
|
|
data = Run('gzip', '-cd', fname, binary=True)
|
|
else:
|
|
raise ValueError("Unknown algorithm '%s'" % algo)
|
|
return data
|
|
|
|
CMD_CREATE, CMD_DELETE, CMD_ADD, CMD_REPLACE, CMD_EXTRACT = range(5)
|
|
|
|
IFWITOOL_CMDS = {
|
|
CMD_CREATE: 'create',
|
|
CMD_DELETE: 'delete',
|
|
CMD_ADD: 'add',
|
|
CMD_REPLACE: 'replace',
|
|
CMD_EXTRACT: 'extract',
|
|
}
|
|
|
|
def RunIfwiTool(ifwi_file, cmd, fname=None, subpart=None, entry_name=None):
|
|
"""Run ifwitool with the given arguments:
|
|
|
|
Args:
|
|
ifwi_file: IFWI file to operation on
|
|
cmd: Command to execute (CMD_...)
|
|
fname: Filename of file to add/replace/extract/create (None for
|
|
CMD_DELETE)
|
|
subpart: Name of sub-partition to operation on (None for CMD_CREATE)
|
|
entry_name: Name of directory entry to operate on, or None if none
|
|
"""
|
|
args = ['ifwitool', ifwi_file]
|
|
args.append(IFWITOOL_CMDS[cmd])
|
|
if fname:
|
|
args += ['-f', fname]
|
|
if subpart:
|
|
args += ['-n', subpart]
|
|
if entry_name:
|
|
args += ['-d', '-e', entry_name]
|
|
Run(*args)
|
|
|
|
def ToHex(val):
|
|
"""Convert an integer value (or None) to a string
|
|
|
|
Returns:
|
|
hex value, or 'None' if the value is None
|
|
"""
|
|
return 'None' if val is None else '%#x' % val
|
|
|
|
def ToHexSize(val):
|
|
"""Return the size of an object in hex
|
|
|
|
Returns:
|
|
hex value of size, or 'None' if the value is None
|
|
"""
|
|
return 'None' if val is None else '%#x' % len(val)
|