1 # SPDX-License-Identifier: GPL-2.0+
3 # Copyright (c) 2016 Google, Inc
13 from patman import command
14 from patman import tout
16 # Output directly (generally this is temporary)
19 # True to keep the output directory around after exiting
20 preserve_outdir = False
22 # Path to the Chrome OS chroot, if we know it
25 # Search paths to use for Filename(), used to find files
28 tool_search_paths = []
30 # Tools and the packages that contain them, on debian
35 # List of paths to use when looking for an input file
38 def PrepareOutputDir(dirname, preserve=False):
39 """Select an output directory, ensuring it exists.
41 This either creates a temporary directory or checks that the one supplied
42 by the user is valid. For a temporary directory, it makes a note to
43 remove it later if required.
46 dirname: a string, name of the output directory to use to store
47 intermediate and output files. If is None - create a temporary
49 preserve: a Boolean. If outdir above is None and preserve is False, the
50 created temporary directory will be destroyed on exit.
53 OSError: If it cannot create the output directory.
55 global outdir, preserve_outdir
57 preserve_outdir = dirname or preserve
60 if not os.path.isdir(outdir):
63 except OSError as err:
64 raise CmdError("Cannot make output directory '%s': '%s'" %
65 (outdir, err.strerror))
66 tout.Debug("Using output directory '%s'" % outdir)
68 outdir = tempfile.mkdtemp(prefix='binman.')
69 tout.Debug("Using temporary directory '%s'" % outdir)
71 def _RemoveOutputDir():
75 tout.Debug("Deleted temporary directory '%s'" % outdir)
78 def FinaliseOutputDir():
79 global outdir, preserve_outdir
81 """Tidy up: delete output directory if temporary and not preserved."""
82 if outdir and not preserve_outdir:
86 def GetOutputFilename(fname):
87 """Return a filename within the output directory.
90 fname: Filename to use for new file
93 The full path of the filename, within the output directory
95 return os.path.join(outdir, fname)
97 def _FinaliseForTest():
98 """Remove the output directory (for use by tests)"""
105 def SetInputDirs(dirname):
106 """Add a list of input directories, where input files are kept.
109 dirname: a list of paths to input directories to use for obtaining
110 files needed by binman to place in the image.
115 tout.Debug("Using input directories %s" % indir)
117 def GetInputFilename(fname):
118 """Return a filename for use as input.
121 fname: Filename to use for new file
124 The full path of the filename, within the input directory
126 if not indir or fname[:1] == '/':
128 for dirname in indir:
129 pathname = os.path.join(dirname, fname)
130 if os.path.exists(pathname):
133 raise ValueError("Filename '%s' not found in input path (%s) (cwd='%s')" %
134 (fname, ','.join(indir), os.getcwd()))
136 def GetInputFilenameGlob(pattern):
137 """Return a list of filenames for use as input.
140 pattern: Filename pattern to search for
143 A list of matching files in all input directories
146 return glob.glob(fname)
148 for dirname in indir:
149 pathname = os.path.join(dirname, pattern)
150 files += glob.glob(pathname)
153 def Align(pos, align):
156 pos = (pos + mask) & ~mask
159 def NotPowerOfTwo(num):
160 return num and (num & (num - 1))
162 def SetToolPaths(toolpaths):
163 """Set the path to search for tools
166 toolpaths: List of paths to search for tools executed by Run()
168 global tool_search_paths
170 tool_search_paths = toolpaths
172 def PathHasFile(path_spec, fname):
173 """Check if a given filename is in the PATH
176 path_spec: Value of PATH variable to check
177 fname: Filename to check
180 True if found, False if not
182 for dir in path_spec.split(':'):
183 if os.path.exists(os.path.join(dir, fname)):
187 def Run(name, *args, **kwargs):
188 """Run a tool with some arguments
190 This runs a 'tool', which is a program used by binman to process files and
191 perhaps produce some output. Tools can be located on the PATH or in a
195 name: Command name to run
196 args: Arguments to the tool
202 binary = kwargs.get('binary')
204 if tool_search_paths:
205 env = dict(os.environ)
206 env['PATH'] = ':'.join(tool_search_paths) + ':' + env['PATH']
207 all_args = (name,) + args
208 result = command.RunPipe([all_args], capture=True, capture_stderr=True,
209 env=env, raise_on_error=False, binary=binary)
210 if result.return_code:
211 raise Exception("Error %d running '%s': %s" %
212 (result.return_code,' '.join(all_args),
216 if env and not PathHasFile(env['PATH'], name):
217 msg = "Please install tool '%s'" % name
218 package = packages.get(name)
220 msg += " (e.g. from package '%s')" % package
221 raise ValueError(msg)
225 """Resolve a file path to an absolute path.
227 If fname starts with ##/ and chroot is available, ##/ gets replaced with
228 the chroot path. If chroot is not available, this file name can not be
229 resolved, `None' is returned.
231 If fname is not prepended with the above prefix, and is not an existing
232 file, the actual file name is retrieved from the passed in string and the
233 search_paths directories (if any) are searched to for the file. If found -
234 the path to the found file is returned, `None' is returned otherwise.
237 fname: a string, the path to resolve.
240 Absolute path to the file or None if not found.
242 if fname.startswith('##/'):
244 fname = os.path.join(chroot_path, fname[3:])
248 # Search for a pathname that exists, and return it if found
249 if fname and not os.path.exists(fname):
250 for path in search_paths:
251 pathname = os.path.join(path, os.path.basename(fname))
252 if os.path.exists(pathname):
255 # If not found, just return the standard, unchanged path
258 def ReadFile(fname, binary=True):
259 """Read and return the contents of a file.
262 fname: path to filename to read, where ## signifiies the chroot.
265 data read from file, as a string.
267 with open(Filename(fname), binary and 'rb' or 'r') as fd:
269 #self._out.Info("Read file '%s' size %d (%#0x)" %
270 #(fname, len(data), len(data)))
273 def WriteFile(fname, data, binary=True):
274 """Write data into a file.
277 fname: path to filename to write
278 data: data to write to file, as a string
280 #self._out.Info("Write file '%s' size %d (%#0x)" %
281 #(fname, len(data), len(data)))
282 with open(Filename(fname), binary and 'wb' or 'w') as fd:
285 def GetBytes(byte, size):
286 """Get a string of bytes of a given size
288 This handles the unfortunate different between Python 2 and Python 2.
291 byte: Numeric byte value to use
292 size: Size of bytes/string to return
295 A bytes type with 'byte' repeated 'size' times
297 if sys.version_info[0] >= 3:
298 data = bytes([byte]) * size
300 data = chr(byte) * size
304 """Make sure a value is a unicode string
306 This allows some amount of compatibility between Python 2 and Python3. For
307 the former, it returns a unicode object.
310 val: string or unicode object
313 unicode version of val
315 if sys.version_info[0] >= 3:
317 return val if isinstance(val, unicode) else val.decode('utf-8')
319 def FromUnicode(val):
320 """Make sure a value is a non-unicode string
322 This allows some amount of compatibility between Python 2 and Python3. For
323 the former, it converts a unicode object to a string.
326 val: string or unicode object
329 non-unicode version of val
331 if sys.version_info[0] >= 3:
333 return val if isinstance(val, str) else val.encode('utf-8')
336 """Convert a character to an ASCII value
338 This is useful because in Python 2 bytes is an alias for str, but in
339 Python 3 they are separate types. This function converts the argument to
340 an ASCII value in either case.
343 ch: A string (Python 2) or byte (Python 3) value
346 integer ASCII value for ch
348 return ord(ch) if type(ch) == str else ch
351 """Convert a byte to a character
353 This is useful because in Python 2 bytes is an alias for str, but in
354 Python 3 they are separate types. This function converts an ASCII value to
355 a value with the appropriate type in either case.
358 byte: A byte or str value
360 return chr(byte) if type(byte) != str else byte
362 def ToChars(byte_list):
363 """Convert a list of bytes to a str/bytes type
366 byte_list: List of ASCII values representing the string
369 string made by concatenating all the ASCII values
371 return ''.join([chr(byte) for byte in byte_list])
374 """Convert a str type into a bytes type
377 string: string to convert
380 Python 3: A bytes type
381 Python 2: A string type
383 if sys.version_info[0] >= 3:
384 return string.encode('utf-8')
388 """Convert a bytes type into a str type
391 bval: bytes value to convert
394 Python 3: A bytes type
395 Python 2: A string type
397 return bval.decode('utf-8')
399 def Compress(indata, algo, with_header=True):
400 """Compress some data using a given algorithm
402 Note that for lzma this uses an old version of the algorithm, not that
405 This requires 'lz4' and 'lzma_alone' tools. It also requires an output
406 directory to be previously set up, by calling PrepareOutputDir().
409 indata: Input data to compress
410 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
417 fname = GetOutputFilename('%s.comp.tmp' % algo)
418 WriteFile(fname, indata)
420 data = Run('lz4', '--no-frame-crc', '-c', fname, binary=True)
421 # cbfstool uses a very old version of lzma
423 outfname = GetOutputFilename('%s.comp.otmp' % algo)
424 Run('lzma_alone', 'e', fname, outfname, '-lc1', '-lp0', '-pb0', '-d8')
425 data = ReadFile(outfname)
427 data = Run('gzip', '-c', fname, binary=True)
429 raise ValueError("Unknown algorithm '%s'" % algo)
431 hdr = struct.pack('<I', len(data))
435 def Decompress(indata, algo, with_header=True):
436 """Decompress some data using a given algorithm
438 Note that for lzma this uses an old version of the algorithm, not that
441 This requires 'lz4' and 'lzma_alone' tools. It also requires an output
442 directory to be previously set up, by calling PrepareOutputDir().
445 indata: Input data to decompress
446 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
454 data_len = struct.unpack('<I', indata[:4])[0]
455 indata = indata[4:4 + data_len]
456 fname = GetOutputFilename('%s.decomp.tmp' % algo)
457 with open(fname, 'wb') as fd:
460 data = Run('lz4', '-dc', fname, binary=True)
462 outfname = GetOutputFilename('%s.decomp.otmp' % algo)
463 Run('lzma_alone', 'd', fname, outfname)
464 data = ReadFile(outfname, binary=True)
466 data = Run('gzip', '-cd', fname, binary=True)
468 raise ValueError("Unknown algorithm '%s'" % algo)
471 CMD_CREATE, CMD_DELETE, CMD_ADD, CMD_REPLACE, CMD_EXTRACT = range(5)
474 CMD_CREATE: 'create',
475 CMD_DELETE: 'delete',
477 CMD_REPLACE: 'replace',
478 CMD_EXTRACT: 'extract',
481 def RunIfwiTool(ifwi_file, cmd, fname=None, subpart=None, entry_name=None):
482 """Run ifwitool with the given arguments:
485 ifwi_file: IFWI file to operation on
486 cmd: Command to execute (CMD_...)
487 fname: Filename of file to add/replace/extract/create (None for
489 subpart: Name of sub-partition to operation on (None for CMD_CREATE)
490 entry_name: Name of directory entry to operate on, or None if none
492 args = ['ifwitool', ifwi_file]
493 args.append(IFWITOOL_CMDS[cmd])
495 args += ['-f', fname]
497 args += ['-n', subpart]
499 args += ['-d', '-e', entry_name]
503 """Convert an integer value (or None) to a string
506 hex value, or 'None' if the value is None
508 return 'None' if val is None else '%#x' % val
511 """Return the size of an object in hex
514 hex value of size, or 'None' if the value is None
516 return 'None' if val is None else '%#x' % len(val)