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, allow_missing=False):
118 """Return a filename for use as input.
121 fname: Filename to use for new file
122 allow_missing: True if the filename can be missing
125 The full path of the filename, within the input directory, or
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):
137 raise ValueError("Filename '%s' not found in input path (%s) (cwd='%s')" %
138 (fname, ','.join(indir), os.getcwd()))
140 def GetInputFilenameGlob(pattern):
141 """Return a list of filenames for use as input.
144 pattern: Filename pattern to search for
147 A list of matching files in all input directories
150 return glob.glob(fname)
152 for dirname in indir:
153 pathname = os.path.join(dirname, pattern)
154 files += glob.glob(pathname)
157 def Align(pos, align):
160 pos = (pos + mask) & ~mask
163 def NotPowerOfTwo(num):
164 return num and (num & (num - 1))
166 def SetToolPaths(toolpaths):
167 """Set the path to search for tools
170 toolpaths: List of paths to search for tools executed by Run()
172 global tool_search_paths
174 tool_search_paths = toolpaths
176 def PathHasFile(path_spec, fname):
177 """Check if a given filename is in the PATH
180 path_spec: Value of PATH variable to check
181 fname: Filename to check
184 True if found, False if not
186 for dir in path_spec.split(':'):
187 if os.path.exists(os.path.join(dir, fname)):
191 def Run(name, *args, **kwargs):
192 """Run a tool with some arguments
194 This runs a 'tool', which is a program used by binman to process files and
195 perhaps produce some output. Tools can be located on the PATH or in a
199 name: Command name to run
200 args: Arguments to the tool
206 binary = kwargs.get('binary')
208 if tool_search_paths:
209 env = dict(os.environ)
210 env['PATH'] = ':'.join(tool_search_paths) + ':' + env['PATH']
211 all_args = (name,) + args
212 result = command.RunPipe([all_args], capture=True, capture_stderr=True,
213 env=env, raise_on_error=False, binary=binary)
214 if result.return_code:
215 raise Exception("Error %d running '%s': %s" %
216 (result.return_code,' '.join(all_args),
220 if env and not PathHasFile(env['PATH'], name):
221 msg = "Please install tool '%s'" % name
222 package = packages.get(name)
224 msg += " (e.g. from package '%s')" % package
225 raise ValueError(msg)
229 """Resolve a file path to an absolute path.
231 If fname starts with ##/ and chroot is available, ##/ gets replaced with
232 the chroot path. If chroot is not available, this file name can not be
233 resolved, `None' is returned.
235 If fname is not prepended with the above prefix, and is not an existing
236 file, the actual file name is retrieved from the passed in string and the
237 search_paths directories (if any) are searched to for the file. If found -
238 the path to the found file is returned, `None' is returned otherwise.
241 fname: a string, the path to resolve.
244 Absolute path to the file or None if not found.
246 if fname.startswith('##/'):
248 fname = os.path.join(chroot_path, fname[3:])
252 # Search for a pathname that exists, and return it if found
253 if fname and not os.path.exists(fname):
254 for path in search_paths:
255 pathname = os.path.join(path, os.path.basename(fname))
256 if os.path.exists(pathname):
259 # If not found, just return the standard, unchanged path
262 def ReadFile(fname, binary=True):
263 """Read and return the contents of a file.
266 fname: path to filename to read, where ## signifiies the chroot.
269 data read from file, as a string.
271 with open(Filename(fname), binary and 'rb' or 'r') as fd:
273 #self._out.Info("Read file '%s' size %d (%#0x)" %
274 #(fname, len(data), len(data)))
277 def WriteFile(fname, data, binary=True):
278 """Write data into a file.
281 fname: path to filename to write
282 data: data to write to file, as a string
284 #self._out.Info("Write file '%s' size %d (%#0x)" %
285 #(fname, len(data), len(data)))
286 with open(Filename(fname), binary and 'wb' or 'w') as fd:
289 def GetBytes(byte, size):
290 """Get a string of bytes of a given size
292 This handles the unfortunate different between Python 2 and Python 2.
295 byte: Numeric byte value to use
296 size: Size of bytes/string to return
299 A bytes type with 'byte' repeated 'size' times
301 if sys.version_info[0] >= 3:
302 data = bytes([byte]) * size
304 data = chr(byte) * size
308 """Make sure a value is a unicode string
310 This allows some amount of compatibility between Python 2 and Python3. For
311 the former, it returns a unicode object.
314 val: string or unicode object
317 unicode version of val
319 if sys.version_info[0] >= 3:
321 return val if isinstance(val, unicode) else val.decode('utf-8')
323 def FromUnicode(val):
324 """Make sure a value is a non-unicode string
326 This allows some amount of compatibility between Python 2 and Python3. For
327 the former, it converts a unicode object to a string.
330 val: string or unicode object
333 non-unicode version of val
335 if sys.version_info[0] >= 3:
337 return val if isinstance(val, str) else val.encode('utf-8')
340 """Convert a character to an ASCII value
342 This is useful because in Python 2 bytes is an alias for str, but in
343 Python 3 they are separate types. This function converts the argument to
344 an ASCII value in either case.
347 ch: A string (Python 2) or byte (Python 3) value
350 integer ASCII value for ch
352 return ord(ch) if type(ch) == str else ch
355 """Convert a byte to a character
357 This is useful because in Python 2 bytes is an alias for str, but in
358 Python 3 they are separate types. This function converts an ASCII value to
359 a value with the appropriate type in either case.
362 byte: A byte or str value
364 return chr(byte) if type(byte) != str else byte
366 def ToChars(byte_list):
367 """Convert a list of bytes to a str/bytes type
370 byte_list: List of ASCII values representing the string
373 string made by concatenating all the ASCII values
375 return ''.join([chr(byte) for byte in byte_list])
378 """Convert a str type into a bytes type
381 string: string to convert
384 Python 3: A bytes type
385 Python 2: A string type
387 if sys.version_info[0] >= 3:
388 return string.encode('utf-8')
392 """Convert a bytes type into a str type
395 bval: bytes value to convert
398 Python 3: A bytes type
399 Python 2: A string type
401 return bval.decode('utf-8')
403 def Compress(indata, algo, with_header=True):
404 """Compress some data using a given algorithm
406 Note that for lzma this uses an old version of the algorithm, not that
409 This requires 'lz4' and 'lzma_alone' tools. It also requires an output
410 directory to be previously set up, by calling PrepareOutputDir().
413 indata: Input data to compress
414 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
421 fname = GetOutputFilename('%s.comp.tmp' % algo)
422 WriteFile(fname, indata)
424 data = Run('lz4', '--no-frame-crc', '-c', fname, binary=True)
425 # cbfstool uses a very old version of lzma
427 outfname = GetOutputFilename('%s.comp.otmp' % algo)
428 Run('lzma_alone', 'e', fname, outfname, '-lc1', '-lp0', '-pb0', '-d8')
429 data = ReadFile(outfname)
431 data = Run('gzip', '-c', fname, binary=True)
433 raise ValueError("Unknown algorithm '%s'" % algo)
435 hdr = struct.pack('<I', len(data))
439 def Decompress(indata, algo, with_header=True):
440 """Decompress some data using a given algorithm
442 Note that for lzma this uses an old version of the algorithm, not that
445 This requires 'lz4' and 'lzma_alone' tools. It also requires an output
446 directory to be previously set up, by calling PrepareOutputDir().
449 indata: Input data to decompress
450 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
458 data_len = struct.unpack('<I', indata[:4])[0]
459 indata = indata[4:4 + data_len]
460 fname = GetOutputFilename('%s.decomp.tmp' % algo)
461 with open(fname, 'wb') as fd:
464 data = Run('lz4', '-dc', fname, binary=True)
466 outfname = GetOutputFilename('%s.decomp.otmp' % algo)
467 Run('lzma_alone', 'd', fname, outfname)
468 data = ReadFile(outfname, binary=True)
470 data = Run('gzip', '-cd', fname, binary=True)
472 raise ValueError("Unknown algorithm '%s'" % algo)
475 CMD_CREATE, CMD_DELETE, CMD_ADD, CMD_REPLACE, CMD_EXTRACT = range(5)
478 CMD_CREATE: 'create',
479 CMD_DELETE: 'delete',
481 CMD_REPLACE: 'replace',
482 CMD_EXTRACT: 'extract',
485 def RunIfwiTool(ifwi_file, cmd, fname=None, subpart=None, entry_name=None):
486 """Run ifwitool with the given arguments:
489 ifwi_file: IFWI file to operation on
490 cmd: Command to execute (CMD_...)
491 fname: Filename of file to add/replace/extract/create (None for
493 subpart: Name of sub-partition to operation on (None for CMD_CREATE)
494 entry_name: Name of directory entry to operate on, or None if none
496 args = ['ifwitool', ifwi_file]
497 args.append(IFWITOOL_CMDS[cmd])
499 args += ['-f', fname]
501 args += ['-n', subpart]
503 args += ['-d', '-e', entry_name]
507 """Convert an integer value (or None) to a string
510 hex value, or 'None' if the value is None
512 return 'None' if val is None else '%#x' % val
515 """Return the size of an object in hex
518 hex value of size, or 'None' if the value is None
520 return 'None' if val is None else '%#x' % len(val)