1 # SPDX-License-Identifier: GPL-2.0+
3 # Copyright (c) 2016 Google, Inc
6 from __future__ import print_function
18 # Output directly (generally this is temporary)
21 # True to keep the output directory around after exiting
22 preserve_outdir = False
24 # Path to the Chrome OS chroot, if we know it
27 # Search paths to use for Filename(), used to find files
30 tool_search_paths = []
32 # Tools and the packages that contain them, on debian
37 # List of paths to use when looking for an input file
40 def PrepareOutputDir(dirname, preserve=False):
41 """Select an output directory, ensuring it exists.
43 This either creates a temporary directory or checks that the one supplied
44 by the user is valid. For a temporary directory, it makes a note to
45 remove it later if required.
48 dirname: a string, name of the output directory to use to store
49 intermediate and output files. If is None - create a temporary
51 preserve: a Boolean. If outdir above is None and preserve is False, the
52 created temporary directory will be destroyed on exit.
55 OSError: If it cannot create the output directory.
57 global outdir, preserve_outdir
59 preserve_outdir = dirname or preserve
62 if not os.path.isdir(outdir):
65 except OSError as err:
66 raise CmdError("Cannot make output directory '%s': '%s'" %
67 (outdir, err.strerror))
68 tout.Debug("Using output directory '%s'" % outdir)
70 outdir = tempfile.mkdtemp(prefix='binman.')
71 tout.Debug("Using temporary directory '%s'" % outdir)
73 def _RemoveOutputDir():
77 tout.Debug("Deleted temporary directory '%s'" % outdir)
80 def FinaliseOutputDir():
81 global outdir, preserve_outdir
83 """Tidy up: delete output directory if temporary and not preserved."""
84 if outdir and not preserve_outdir:
88 def GetOutputFilename(fname):
89 """Return a filename within the output directory.
92 fname: Filename to use for new file
95 The full path of the filename, within the output directory
97 return os.path.join(outdir, fname)
99 def _FinaliseForTest():
100 """Remove the output directory (for use by tests)"""
107 def SetInputDirs(dirname):
108 """Add a list of input directories, where input files are kept.
111 dirname: a list of paths to input directories to use for obtaining
112 files needed by binman to place in the image.
117 tout.Debug("Using input directories %s" % indir)
119 def GetInputFilename(fname):
120 """Return a filename for use as input.
123 fname: Filename to use for new file
126 The full path of the filename, within the input directory
128 if not indir or fname[:1] == '/':
130 for dirname in indir:
131 pathname = os.path.join(dirname, fname)
132 if os.path.exists(pathname):
135 raise ValueError("Filename '%s' not found in input path (%s) (cwd='%s')" %
136 (fname, ','.join(indir), os.getcwd()))
138 def GetInputFilenameGlob(pattern):
139 """Return a list of filenames for use as input.
142 pattern: Filename pattern to search for
145 A list of matching files in all input directories
148 return glob.glob(fname)
150 for dirname in indir:
151 pathname = os.path.join(dirname, pattern)
152 files += glob.glob(pathname)
155 def Align(pos, align):
158 pos = (pos + mask) & ~mask
161 def NotPowerOfTwo(num):
162 return num and (num & (num - 1))
164 def SetToolPaths(toolpaths):
165 """Set the path to search for tools
168 toolpaths: List of paths to search for tools executed by Run()
170 global tool_search_paths
172 tool_search_paths = toolpaths
174 def PathHasFile(path_spec, fname):
175 """Check if a given filename is in the PATH
178 path_spec: Value of PATH variable to check
179 fname: Filename to check
182 True if found, False if not
184 for dir in path_spec.split(':'):
185 if os.path.exists(os.path.join(dir, fname)):
189 def Run(name, *args, **kwargs):
190 """Run a tool with some arguments
192 This runs a 'tool', which is a program used by binman to process files and
193 perhaps produce some output. Tools can be located on the PATH or in a
197 name: Command name to run
198 args: Arguments to the tool
204 binary = kwargs.get('binary')
206 if tool_search_paths:
207 env = dict(os.environ)
208 env['PATH'] = ':'.join(tool_search_paths) + ':' + env['PATH']
209 all_args = (name,) + args
210 result = command.RunPipe([all_args], capture=True, capture_stderr=True,
211 env=env, raise_on_error=False, binary=binary)
212 if result.return_code:
213 raise Exception("Error %d running '%s': %s" %
214 (result.return_code,' '.join(all_args),
218 if env and not PathHasFile(env['PATH'], name):
219 msg = "Please install tool '%s'" % name
220 package = packages.get(name)
222 msg += " (e.g. from package '%s')" % package
223 raise ValueError(msg)
227 """Resolve a file path to an absolute path.
229 If fname starts with ##/ and chroot is available, ##/ gets replaced with
230 the chroot path. If chroot is not available, this file name can not be
231 resolved, `None' is returned.
233 If fname is not prepended with the above prefix, and is not an existing
234 file, the actual file name is retrieved from the passed in string and the
235 search_paths directories (if any) are searched to for the file. If found -
236 the path to the found file is returned, `None' is returned otherwise.
239 fname: a string, the path to resolve.
242 Absolute path to the file or None if not found.
244 if fname.startswith('##/'):
246 fname = os.path.join(chroot_path, fname[3:])
250 # Search for a pathname that exists, and return it if found
251 if fname and not os.path.exists(fname):
252 for path in search_paths:
253 pathname = os.path.join(path, os.path.basename(fname))
254 if os.path.exists(pathname):
257 # If not found, just return the standard, unchanged path
260 def ReadFile(fname, binary=True):
261 """Read and return the contents of a file.
264 fname: path to filename to read, where ## signifiies the chroot.
267 data read from file, as a string.
269 with open(Filename(fname), binary and 'rb' or 'r') as fd:
271 #self._out.Info("Read file '%s' size %d (%#0x)" %
272 #(fname, len(data), len(data)))
275 def WriteFile(fname, data):
276 """Write data into a file.
279 fname: path to filename to write
280 data: data to write to file, as a string
282 #self._out.Info("Write file '%s' size %d (%#0x)" %
283 #(fname, len(data), len(data)))
284 with open(Filename(fname), 'wb') as fd:
287 def GetBytes(byte, size):
288 """Get a string of bytes of a given size
290 This handles the unfortunate different between Python 2 and Python 2.
293 byte: Numeric byte value to use
294 size: Size of bytes/string to return
297 A bytes type with 'byte' repeated 'size' times
299 if sys.version_info[0] >= 3:
300 data = bytes([byte]) * size
302 data = chr(byte) * size
306 """Make sure a value is a unicode string
308 This allows some amount of compatibility between Python 2 and Python3. For
309 the former, it returns a unicode object.
312 val: string or unicode object
315 unicode version of val
317 if sys.version_info[0] >= 3:
319 return val if isinstance(val, unicode) else val.decode('utf-8')
321 def FromUnicode(val):
322 """Make sure a value is a non-unicode string
324 This allows some amount of compatibility between Python 2 and Python3. For
325 the former, it converts a unicode object to a string.
328 val: string or unicode object
331 non-unicode version of val
333 if sys.version_info[0] >= 3:
335 return val if isinstance(val, str) else val.encode('utf-8')
338 """Convert a character to an ASCII value
340 This is useful because in Python 2 bytes is an alias for str, but in
341 Python 3 they are separate types. This function converts the argument to
342 an ASCII value in either case.
345 ch: A string (Python 2) or byte (Python 3) value
348 integer ASCII value for ch
350 return ord(ch) if type(ch) == str else ch
353 """Convert a byte to a character
355 This is useful because in Python 2 bytes is an alias for str, but in
356 Python 3 they are separate types. This function converts an ASCII value to
357 a value with the appropriate type in either case.
360 byte: A byte or str value
362 return chr(byte) if type(byte) != str else byte
364 def ToChars(byte_list):
365 """Convert a list of bytes to a str/bytes type
368 byte_list: List of ASCII values representing the string
371 string made by concatenating all the ASCII values
373 return ''.join([chr(byte) for byte in byte_list])
376 """Convert a str type into a bytes type
379 string: string to convert
382 Python 3: A bytes type
383 Python 2: A string type
385 if sys.version_info[0] >= 3:
386 return string.encode('utf-8')
390 """Convert a bytes type into a str type
393 bval: bytes value to convert
396 Python 3: A bytes type
397 Python 2: A string type
399 return bval.decode('utf-8')
401 def Compress(indata, algo, with_header=True):
402 """Compress some data using a given algorithm
404 Note that for lzma this uses an old version of the algorithm, not that
407 This requires 'lz4' and 'lzma_alone' tools. It also requires an output
408 directory to be previously set up, by calling PrepareOutputDir().
411 indata: Input data to compress
412 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
419 fname = GetOutputFilename('%s.comp.tmp' % algo)
420 WriteFile(fname, indata)
422 data = Run('lz4', '--no-frame-crc', '-c', fname, binary=True)
423 # cbfstool uses a very old version of lzma
425 outfname = GetOutputFilename('%s.comp.otmp' % algo)
426 Run('lzma_alone', 'e', fname, outfname, '-lc1', '-lp0', '-pb0', '-d8')
427 data = ReadFile(outfname)
429 data = Run('gzip', '-c', fname, binary=True)
431 raise ValueError("Unknown algorithm '%s'" % algo)
433 hdr = struct.pack('<I', len(data))
437 def Decompress(indata, algo, with_header=True):
438 """Decompress some data using a given algorithm
440 Note that for lzma this uses an old version of the algorithm, not that
443 This requires 'lz4' and 'lzma_alone' tools. It also requires an output
444 directory to be previously set up, by calling PrepareOutputDir().
447 indata: Input data to decompress
448 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
456 data_len = struct.unpack('<I', indata[:4])[0]
457 indata = indata[4:4 + data_len]
458 fname = GetOutputFilename('%s.decomp.tmp' % algo)
459 with open(fname, 'wb') as fd:
462 data = Run('lz4', '-dc', fname, binary=True)
464 outfname = GetOutputFilename('%s.decomp.otmp' % algo)
465 Run('lzma_alone', 'd', fname, outfname)
466 data = ReadFile(outfname, binary=True)
468 data = Run('gzip', '-cd', fname, binary=True)
470 raise ValueError("Unknown algorithm '%s'" % algo)
473 CMD_CREATE, CMD_DELETE, CMD_ADD, CMD_REPLACE, CMD_EXTRACT = range(5)
476 CMD_CREATE: 'create',
477 CMD_DELETE: 'delete',
479 CMD_REPLACE: 'replace',
480 CMD_EXTRACT: 'extract',
483 def RunIfwiTool(ifwi_file, cmd, fname=None, subpart=None, entry_name=None):
484 """Run ifwitool with the given arguments:
487 ifwi_file: IFWI file to operation on
488 cmd: Command to execute (CMD_...)
489 fname: Filename of file to add/replace/extract/create (None for
491 subpart: Name of sub-partition to operation on (None for CMD_CREATE)
492 entry_name: Name of directory entry to operate on, or None if none
494 args = ['ifwitool', ifwi_file]
495 args.append(IFWITOOL_CMDS[cmd])
497 args += ['-f', fname]
499 args += ['-n', subpart]
501 args += ['-d', '-e', entry_name]
505 """Convert an integer value (or None) to a string
508 hex value, or 'None' if the value is None
510 return 'None' if val is None else '%#x' % val
513 """Return the size of an object in hex
516 hex value of size, or 'None' if the value is None
518 return 'None' if val is None else '%#x' % len(val)