3feddb292fc70a5a250ab3de30cb01eb17a77403
[oweals/u-boot.git] / tools / patman / tools.py
1 # SPDX-License-Identifier: GPL-2.0+
2 #
3 # Copyright (c) 2016 Google, Inc
4 #
5
6 from __future__ import print_function
7
8 import command
9 import glob
10 import os
11 import shutil
12 import struct
13 import sys
14 import tempfile
15
16 import tout
17
18 # Output directly (generally this is temporary)
19 outdir = None
20
21 # True to keep the output directory around after exiting
22 preserve_outdir = False
23
24 # Path to the Chrome OS chroot, if we know it
25 chroot_path = None
26
27 # Search paths to use for Filename(), used to find files
28 search_paths = []
29
30 tool_search_paths = []
31
32 # Tools and the packages that contain them, on debian
33 packages = {
34     'lz4': 'liblz4-tool',
35     }
36
37 # List of paths to use when looking for an input file
38 indir = []
39
40 def PrepareOutputDir(dirname, preserve=False):
41     """Select an output directory, ensuring it exists.
42
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.
46
47     Args:
48         dirname: a string, name of the output directory to use to store
49                 intermediate and output files. If is None - create a temporary
50                 directory.
51         preserve: a Boolean. If outdir above is None and preserve is False, the
52                 created temporary directory will be destroyed on exit.
53
54     Raises:
55         OSError: If it cannot create the output directory.
56     """
57     global outdir, preserve_outdir
58
59     preserve_outdir = dirname or preserve
60     if dirname:
61         outdir = dirname
62         if not os.path.isdir(outdir):
63             try:
64                 os.makedirs(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)
69     else:
70         outdir = tempfile.mkdtemp(prefix='binman.')
71         tout.Debug("Using temporary directory '%s'" % outdir)
72
73 def _RemoveOutputDir():
74     global outdir
75
76     shutil.rmtree(outdir)
77     tout.Debug("Deleted temporary directory '%s'" % outdir)
78     outdir = None
79
80 def FinaliseOutputDir():
81     global outdir, preserve_outdir
82
83     """Tidy up: delete output directory if temporary and not preserved."""
84     if outdir and not preserve_outdir:
85         _RemoveOutputDir()
86         outdir = None
87
88 def GetOutputFilename(fname):
89     """Return a filename within the output directory.
90
91     Args:
92         fname: Filename to use for new file
93
94     Returns:
95         The full path of the filename, within the output directory
96     """
97     return os.path.join(outdir, fname)
98
99 def _FinaliseForTest():
100     """Remove the output directory (for use by tests)"""
101     global outdir
102
103     if outdir:
104         _RemoveOutputDir()
105         outdir = None
106
107 def SetInputDirs(dirname):
108     """Add a list of input directories, where input files are kept.
109
110     Args:
111         dirname: a list of paths to input directories to use for obtaining
112                 files needed by binman to place in the image.
113     """
114     global indir
115
116     indir = dirname
117     tout.Debug("Using input directories %s" % indir)
118
119 def GetInputFilename(fname):
120     """Return a filename for use as input.
121
122     Args:
123         fname: Filename to use for new file
124
125     Returns:
126         The full path of the filename, within the input directory
127     """
128     if not indir or fname[:1] == '/':
129         return fname
130     for dirname in indir:
131         pathname = os.path.join(dirname, fname)
132         if os.path.exists(pathname):
133             return pathname
134
135     raise ValueError("Filename '%s' not found in input path (%s) (cwd='%s')" %
136                      (fname, ','.join(indir), os.getcwd()))
137
138 def GetInputFilenameGlob(pattern):
139     """Return a list of filenames for use as input.
140
141     Args:
142         pattern: Filename pattern to search for
143
144     Returns:
145         A list of matching files in all input directories
146     """
147     if not indir:
148         return glob.glob(fname)
149     files = []
150     for dirname in indir:
151         pathname = os.path.join(dirname, pattern)
152         files += glob.glob(pathname)
153     return sorted(files)
154
155 def Align(pos, align):
156     if align:
157         mask = align - 1
158         pos = (pos + mask) & ~mask
159     return pos
160
161 def NotPowerOfTwo(num):
162     return num and (num & (num - 1))
163
164 def SetToolPaths(toolpaths):
165     """Set the path to search for tools
166
167     Args:
168         toolpaths: List of paths to search for tools executed by Run()
169     """
170     global tool_search_paths
171
172     tool_search_paths = toolpaths
173
174 def PathHasFile(path_spec, fname):
175     """Check if a given filename is in the PATH
176
177     Args:
178         path_spec: Value of PATH variable to check
179         fname: Filename to check
180
181     Returns:
182         True if found, False if not
183     """
184     for dir in path_spec.split(':'):
185         if os.path.exists(os.path.join(dir, fname)):
186             return True
187     return False
188
189 def Run(name, *args, **kwargs):
190     """Run a tool with some arguments
191
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
194     search path.
195
196     Args:
197         name: Command name to run
198         args: Arguments to the tool
199
200     Returns:
201         CommandResult object
202     """
203     try:
204         binary = kwargs.get('binary')
205         env = None
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),
215                 result.stderr))
216         return result.stdout
217     except:
218         if env and not PathHasFile(env['PATH'], name):
219             msg = "Please install tool '%s'" % name
220             package = packages.get(name)
221             if package:
222                  msg += " (e.g. from package '%s')" % package
223             raise ValueError(msg)
224         raise
225
226 def Filename(fname):
227     """Resolve a file path to an absolute path.
228
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.
232
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.
237
238     Args:
239       fname: a string,  the path to resolve.
240
241     Returns:
242       Absolute path to the file or None if not found.
243     """
244     if fname.startswith('##/'):
245       if chroot_path:
246         fname = os.path.join(chroot_path, fname[3:])
247       else:
248         return None
249
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):
255                 return pathname
256
257     # If not found, just return the standard, unchanged path
258     return fname
259
260 def ReadFile(fname, binary=True):
261     """Read and return the contents of a file.
262
263     Args:
264       fname: path to filename to read, where ## signifiies the chroot.
265
266     Returns:
267       data read from file, as a string.
268     """
269     with open(Filename(fname), binary and 'rb' or 'r') as fd:
270         data = fd.read()
271     #self._out.Info("Read file '%s' size %d (%#0x)" %
272                    #(fname, len(data), len(data)))
273     return data
274
275 def WriteFile(fname, data):
276     """Write data into a file.
277
278     Args:
279         fname: path to filename to write
280         data: data to write to file, as a string
281     """
282     #self._out.Info("Write file '%s' size %d (%#0x)" %
283                    #(fname, len(data), len(data)))
284     with open(Filename(fname), 'wb') as fd:
285         fd.write(data)
286
287 def GetBytes(byte, size):
288     """Get a string of bytes of a given size
289
290     This handles the unfortunate different between Python 2 and Python 2.
291
292     Args:
293         byte: Numeric byte value to use
294         size: Size of bytes/string to return
295
296     Returns:
297         A bytes type with 'byte' repeated 'size' times
298     """
299     if sys.version_info[0] >= 3:
300         data = bytes([byte]) * size
301     else:
302         data = chr(byte) * size
303     return data
304
305 def ToUnicode(val):
306     """Make sure a value is a unicode string
307
308     This allows some amount of compatibility between Python 2 and Python3. For
309     the former, it returns a unicode object.
310
311     Args:
312         val: string or unicode object
313
314     Returns:
315         unicode version of val
316     """
317     if sys.version_info[0] >= 3:
318         return val
319     return val if isinstance(val, unicode) else val.decode('utf-8')
320
321 def FromUnicode(val):
322     """Make sure a value is a non-unicode string
323
324     This allows some amount of compatibility between Python 2 and Python3. For
325     the former, it converts a unicode object to a string.
326
327     Args:
328         val: string or unicode object
329
330     Returns:
331         non-unicode version of val
332     """
333     if sys.version_info[0] >= 3:
334         return val
335     return val if isinstance(val, str) else val.encode('utf-8')
336
337 def ToByte(ch):
338     """Convert a character to an ASCII value
339
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.
343
344     Args:
345         ch: A string (Python 2) or byte (Python 3) value
346
347     Returns:
348         integer ASCII value for ch
349     """
350     return ord(ch) if type(ch) == str else ch
351
352 def ToChar(byte):
353     """Convert a byte to a character
354
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.
358
359     Args:
360         byte: A byte or str value
361     """
362     return chr(byte) if type(byte) != str else byte
363
364 def ToChars(byte_list):
365     """Convert a list of bytes to a str/bytes type
366
367     Args:
368         byte_list: List of ASCII values representing the string
369
370     Returns:
371         string made by concatenating all the ASCII values
372     """
373     return ''.join([chr(byte) for byte in byte_list])
374
375 def ToBytes(string):
376     """Convert a str type into a bytes type
377
378     Args:
379         string: string to convert
380
381     Returns:
382         Python 3: A bytes type
383         Python 2: A string type
384     """
385     if sys.version_info[0] >= 3:
386         return string.encode('utf-8')
387     return string
388
389 def ToString(bval):
390     """Convert a bytes type into a str type
391
392     Args:
393         bval: bytes value to convert
394
395     Returns:
396         Python 3: A bytes type
397         Python 2: A string type
398     """
399     return bval.decode('utf-8')
400
401 def Compress(indata, algo, with_header=True):
402     """Compress some data using a given algorithm
403
404     Note that for lzma this uses an old version of the algorithm, not that
405     provided by xz.
406
407     This requires 'lz4' and 'lzma_alone' tools. It also requires an output
408     directory to be previously set up, by calling PrepareOutputDir().
409
410     Args:
411         indata: Input data to compress
412         algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
413
414     Returns:
415         Compressed data
416     """
417     if algo == 'none':
418         return indata
419     fname = GetOutputFilename('%s.comp.tmp' % algo)
420     WriteFile(fname, indata)
421     if algo == 'lz4':
422         data = Run('lz4', '--no-frame-crc', '-c', fname, binary=True)
423     # cbfstool uses a very old version of lzma
424     elif algo == 'lzma':
425         outfname = GetOutputFilename('%s.comp.otmp' % algo)
426         Run('lzma_alone', 'e', fname, outfname, '-lc1', '-lp0', '-pb0', '-d8')
427         data = ReadFile(outfname)
428     elif algo == 'gzip':
429         data = Run('gzip', '-c', fname, binary=True)
430     else:
431         raise ValueError("Unknown algorithm '%s'" % algo)
432     if with_header:
433         hdr = struct.pack('<I', len(data))
434         data = hdr + data
435     return data
436
437 def Decompress(indata, algo, with_header=True):
438     """Decompress some data using a given algorithm
439
440     Note that for lzma this uses an old version of the algorithm, not that
441     provided by xz.
442
443     This requires 'lz4' and 'lzma_alone' tools. It also requires an output
444     directory to be previously set up, by calling PrepareOutputDir().
445
446     Args:
447         indata: Input data to decompress
448         algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
449
450     Returns:
451         Compressed data
452     """
453     if algo == 'none':
454         return indata
455     if with_header:
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:
460         fd.write(indata)
461     if algo == 'lz4':
462         data = Run('lz4', '-dc', fname, binary=True)
463     elif algo == 'lzma':
464         outfname = GetOutputFilename('%s.decomp.otmp' % algo)
465         Run('lzma_alone', 'd', fname, outfname)
466         data = ReadFile(outfname, binary=True)
467     elif algo == 'gzip':
468         data = Run('gzip', '-cd', fname, binary=True)
469     else:
470         raise ValueError("Unknown algorithm '%s'" % algo)
471     return data
472
473 CMD_CREATE, CMD_DELETE, CMD_ADD, CMD_REPLACE, CMD_EXTRACT = range(5)
474
475 IFWITOOL_CMDS = {
476     CMD_CREATE: 'create',
477     CMD_DELETE: 'delete',
478     CMD_ADD: 'add',
479     CMD_REPLACE: 'replace',
480     CMD_EXTRACT: 'extract',
481     }
482
483 def RunIfwiTool(ifwi_file, cmd, fname=None, subpart=None, entry_name=None):
484     """Run ifwitool with the given arguments:
485
486     Args:
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
490             CMD_DELETE)
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
493     """
494     args = ['ifwitool', ifwi_file]
495     args.append(IFWITOOL_CMDS[cmd])
496     if fname:
497         args += ['-f', fname]
498     if subpart:
499         args += ['-n', subpart]
500     if entry_name:
501         args += ['-d', '-e', entry_name]
502     Run(*args)
503
504 def ToHex(val):
505     """Convert an integer value (or None) to a string
506
507     Returns:
508         hex value, or 'None' if the value is None
509     """
510     return 'None' if val is None else '%#x' % val
511
512 def ToHexSize(val):
513     """Return the size of an object in hex
514
515     Returns:
516         hex value of size, or 'None' if the value is None
517     """
518     return 'None' if val is None else '%#x' % len(val)