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:
87 def GetOutputFilename(fname):
88 """Return a filename within the output directory.
91 fname: Filename to use for new file
94 The full path of the filename, within the output directory
96 return os.path.join(outdir, fname)
98 def _FinaliseForTest():
99 """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
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
197 kwargs: Options to pass to command.run()
204 if tool_search_paths:
205 env = dict(os.environ)
206 env['PATH'] = ':'.join(tool_search_paths) + ':' + env['PATH']
207 return command.Run(name, *args, capture=True,
208 capture_stderr=True, env=env, **kwargs)
210 if env and not PathHasFile(env['PATH'], name):
211 msg = "Please install tool '%s'" % name
212 package = packages.get(name)
214 msg += " (e.g. from package '%s')" % package
215 raise ValueError(msg)
219 """Resolve a file path to an absolute path.
221 If fname starts with ##/ and chroot is available, ##/ gets replaced with
222 the chroot path. If chroot is not available, this file name can not be
223 resolved, `None' is returned.
225 If fname is not prepended with the above prefix, and is not an existing
226 file, the actual file name is retrieved from the passed in string and the
227 search_paths directories (if any) are searched to for the file. If found -
228 the path to the found file is returned, `None' is returned otherwise.
231 fname: a string, the path to resolve.
234 Absolute path to the file or None if not found.
236 if fname.startswith('##/'):
238 fname = os.path.join(chroot_path, fname[3:])
242 # Search for a pathname that exists, and return it if found
243 if fname and not os.path.exists(fname):
244 for path in search_paths:
245 pathname = os.path.join(path, os.path.basename(fname))
246 if os.path.exists(pathname):
249 # If not found, just return the standard, unchanged path
252 def ReadFile(fname, binary=True):
253 """Read and return the contents of a file.
256 fname: path to filename to read, where ## signifiies the chroot.
259 data read from file, as a string.
261 with open(Filename(fname), binary and 'rb' or 'r') as fd:
263 #self._out.Info("Read file '%s' size %d (%#0x)" %
264 #(fname, len(data), len(data)))
267 def WriteFile(fname, data):
268 """Write data into a file.
271 fname: path to filename to write
272 data: data to write to file, as a string
274 #self._out.Info("Write file '%s' size %d (%#0x)" %
275 #(fname, len(data), len(data)))
276 with open(Filename(fname), 'wb') as fd:
279 def GetBytes(byte, size):
280 """Get a string of bytes of a given size
282 This handles the unfortunate different between Python 2 and Python 2.
285 byte: Numeric byte value to use
286 size: Size of bytes/string to return
289 A bytes type with 'byte' repeated 'size' times
291 if sys.version_info[0] >= 3:
292 data = bytes([byte]) * size
294 data = chr(byte) * size
298 """Make sure a value is a unicode string
300 This allows some amount of compatibility between Python 2 and Python3. For
301 the former, it returns a unicode object.
304 val: string or unicode object
307 unicode version of val
309 if sys.version_info[0] >= 3:
311 return val if isinstance(val, unicode) else val.decode('utf-8')
313 def FromUnicode(val):
314 """Make sure a value is a non-unicode string
316 This allows some amount of compatibility between Python 2 and Python3. For
317 the former, it converts a unicode object to a string.
320 val: string or unicode object
323 non-unicode version of val
325 if sys.version_info[0] >= 3:
327 return val if isinstance(val, str) else val.encode('utf-8')
330 """Convert a character to an ASCII value
332 This is useful because in Python 2 bytes is an alias for str, but in
333 Python 3 they are separate types. This function converts the argument to
334 an ASCII value in either case.
337 ch: A string (Python 2) or byte (Python 3) value
340 integer ASCII value for ch
342 return ord(ch) if type(ch) == str else ch
345 """Convert a byte to a character
347 This is useful because in Python 2 bytes is an alias for str, but in
348 Python 3 they are separate types. This function converts an ASCII value to
349 a value with the appropriate type in either case.
352 byte: A byte or str value
354 return chr(byte) if type(byte) != str else byte
356 def ToChars(byte_list):
357 """Convert a list of bytes to a str/bytes type
360 byte_list: List of ASCII values representing the string
363 string made by concatenating all the ASCII values
365 return ''.join([chr(byte) for byte in byte_list])
368 """Convert a str type into a bytes type
371 string: string to convert value
374 Python 3: A bytes type
375 Python 2: A string type
377 if sys.version_info[0] >= 3:
378 return string.encode('utf-8')
381 def Compress(indata, algo, with_header=True):
382 """Compress some data using a given algorithm
384 Note that for lzma this uses an old version of the algorithm, not that
387 This requires 'lz4' and 'lzma_alone' tools. It also requires an output
388 directory to be previously set up, by calling PrepareOutputDir().
391 indata: Input data to compress
392 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
399 fname = GetOutputFilename('%s.comp.tmp' % algo)
400 WriteFile(fname, indata)
402 data = Run('lz4', '--no-frame-crc', '-c', fname, binary=True)
403 # cbfstool uses a very old version of lzma
405 outfname = GetOutputFilename('%s.comp.otmp' % algo)
406 Run('lzma_alone', 'e', fname, outfname, '-lc1', '-lp0', '-pb0', '-d8')
407 data = ReadFile(outfname)
409 data = Run('gzip', '-c', fname, binary=True)
411 raise ValueError("Unknown algorithm '%s'" % algo)
413 hdr = struct.pack('<I', len(data))
417 def Decompress(indata, algo, with_header=True):
418 """Decompress some data using a given algorithm
420 Note that for lzma this uses an old version of the algorithm, not that
423 This requires 'lz4' and 'lzma_alone' tools. It also requires an output
424 directory to be previously set up, by calling PrepareOutputDir().
427 indata: Input data to decompress
428 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
436 data_len = struct.unpack('<I', indata[:4])[0]
437 indata = indata[4:4 + data_len]
438 fname = GetOutputFilename('%s.decomp.tmp' % algo)
439 with open(fname, 'wb') as fd:
442 data = Run('lz4', '-dc', fname, binary=True)
444 outfname = GetOutputFilename('%s.decomp.otmp' % algo)
445 Run('lzma_alone', 'd', fname, outfname)
446 data = ReadFile(outfname)
448 data = Run('gzip', '-cd', fname, binary=True)
450 raise ValueError("Unknown algorithm '%s'" % algo)
453 CMD_CREATE, CMD_DELETE, CMD_ADD, CMD_REPLACE, CMD_EXTRACT = range(5)
456 CMD_CREATE: 'create',
457 CMD_DELETE: 'delete',
459 CMD_REPLACE: 'replace',
460 CMD_EXTRACT: 'extract',
463 def RunIfwiTool(ifwi_file, cmd, fname=None, subpart=None, entry_name=None):
464 """Run ifwitool with the given arguments:
467 ifwi_file: IFWI file to operation on
468 cmd: Command to execute (CMD_...)
469 fname: Filename of file to add/replace/extract/create (None for
471 subpart: Name of sub-partition to operation on (None for CMD_CREATE)
472 entry_name: Name of directory entry to operate on, or None if none
474 args = ['ifwitool', ifwi_file]
475 args.append(IFWITOOL_CMDS[cmd])
477 args += ['-f', fname]
479 args += ['-n', subpart]
481 args += ['-d', '-e', entry_name]
485 """Convert an integer value (or None) to a string
488 hex value, or 'None' if the value is None
490 return 'None' if val is None else '%#x' % val
493 """Return the size of an object in hex
496 hex value of size, or 'None' if the value is None
498 return 'None' if val is None else '%#x' % len(val)