d615227482a06bd96932e91640fb6a25772a6557
[platform/kernel/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
87 def GetOutputFilename(fname):
88     """Return a filename within the output directory.
89
90     Args:
91         fname: Filename to use for new file
92
93     Returns:
94         The full path of the filename, within the output directory
95     """
96     return os.path.join(outdir, fname)
97
98 def _FinaliseForTest():
99     """Remove the output directory (for use by tests)"""
100     global outdir
101
102     if outdir:
103         _RemoveOutputDir()
104
105 def SetInputDirs(dirname):
106     """Add a list of input directories, where input files are kept.
107
108     Args:
109         dirname: a list of paths to input directories to use for obtaining
110                 files needed by binman to place in the image.
111     """
112     global indir
113
114     indir = dirname
115     tout.Debug("Using input directories %s" % indir)
116
117 def GetInputFilename(fname):
118     """Return a filename for use as input.
119
120     Args:
121         fname: Filename to use for new file
122
123     Returns:
124         The full path of the filename, within the input directory
125     """
126     if not indir:
127         return fname
128     for dirname in indir:
129         pathname = os.path.join(dirname, fname)
130         if os.path.exists(pathname):
131             return pathname
132
133     raise ValueError("Filename '%s' not found in input path (%s) (cwd='%s')" %
134                      (fname, ','.join(indir), os.getcwd()))
135
136 def GetInputFilenameGlob(pattern):
137     """Return a list of filenames for use as input.
138
139     Args:
140         pattern: Filename pattern to search for
141
142     Returns:
143         A list of matching files in all input directories
144     """
145     if not indir:
146         return glob.glob(fname)
147     files = []
148     for dirname in indir:
149         pathname = os.path.join(dirname, pattern)
150         files += glob.glob(pathname)
151     return sorted(files)
152
153 def Align(pos, align):
154     if align:
155         mask = align - 1
156         pos = (pos + mask) & ~mask
157     return pos
158
159 def NotPowerOfTwo(num):
160     return num and (num & (num - 1))
161
162 def SetToolPaths(toolpaths):
163     """Set the path to search for tools
164
165     Args:
166         toolpaths: List of paths to search for tools executed by Run()
167     """
168     global tool_search_paths
169
170     tool_search_paths = toolpaths
171
172 def PathHasFile(path_spec, fname):
173     """Check if a given filename is in the PATH
174
175     Args:
176         path_spec: Value of PATH variable to check
177         fname: Filename to check
178
179     Returns:
180         True if found, False if not
181     """
182     for dir in path_spec.split(':'):
183         if os.path.exists(os.path.join(dir, fname)):
184             return True
185     return False
186
187 def Run(name, *args, **kwargs):
188     """Run a tool with some arguments
189
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
192     search path.
193
194     Args:
195         name: Command name to run
196         args: Arguments to the tool
197         kwargs: Options to pass to command.run()
198
199     Returns:
200         CommandResult object
201     """
202     try:
203         env = None
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)
209     except:
210         if env and not PathHasFile(env['PATH'], name):
211             msg = "Please install tool '%s'" % name
212             package = packages.get(name)
213             if package:
214                  msg += " (e.g. from package '%s')" % package
215             raise ValueError(msg)
216         raise
217
218 def Filename(fname):
219     """Resolve a file path to an absolute path.
220
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.
224
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.
229
230     Args:
231       fname: a string,  the path to resolve.
232
233     Returns:
234       Absolute path to the file or None if not found.
235     """
236     if fname.startswith('##/'):
237       if chroot_path:
238         fname = os.path.join(chroot_path, fname[3:])
239       else:
240         return None
241
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):
247                 return pathname
248
249     # If not found, just return the standard, unchanged path
250     return fname
251
252 def ReadFile(fname, binary=True):
253     """Read and return the contents of a file.
254
255     Args:
256       fname: path to filename to read, where ## signifiies the chroot.
257
258     Returns:
259       data read from file, as a string.
260     """
261     with open(Filename(fname), binary and 'rb' or 'r') as fd:
262         data = fd.read()
263     #self._out.Info("Read file '%s' size %d (%#0x)" %
264                    #(fname, len(data), len(data)))
265     return data
266
267 def WriteFile(fname, data):
268     """Write data into a file.
269
270     Args:
271         fname: path to filename to write
272         data: data to write to file, as a string
273     """
274     #self._out.Info("Write file '%s' size %d (%#0x)" %
275                    #(fname, len(data), len(data)))
276     with open(Filename(fname), 'wb') as fd:
277         fd.write(data)
278
279 def GetBytes(byte, size):
280     """Get a string of bytes of a given size
281
282     This handles the unfortunate different between Python 2 and Python 2.
283
284     Args:
285         byte: Numeric byte value to use
286         size: Size of bytes/string to return
287
288     Returns:
289         A bytes type with 'byte' repeated 'size' times
290     """
291     if sys.version_info[0] >= 3:
292         data = bytes([byte]) * size
293     else:
294         data = chr(byte) * size
295     return data
296
297 def ToUnicode(val):
298     """Make sure a value is a unicode string
299
300     This allows some amount of compatibility between Python 2 and Python3. For
301     the former, it returns a unicode object.
302
303     Args:
304         val: string or unicode object
305
306     Returns:
307         unicode version of val
308     """
309     if sys.version_info[0] >= 3:
310         return val
311     return val if isinstance(val, unicode) else val.decode('utf-8')
312
313 def FromUnicode(val):
314     """Make sure a value is a non-unicode string
315
316     This allows some amount of compatibility between Python 2 and Python3. For
317     the former, it converts a unicode object to a string.
318
319     Args:
320         val: string or unicode object
321
322     Returns:
323         non-unicode version of val
324     """
325     if sys.version_info[0] >= 3:
326         return val
327     return val if isinstance(val, str) else val.encode('utf-8')
328
329 def ToByte(ch):
330     """Convert a character to an ASCII value
331
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.
335
336     Args:
337         ch: A string (Python 2) or byte (Python 3) value
338
339     Returns:
340         integer ASCII value for ch
341     """
342     return ord(ch) if type(ch) == str else ch
343
344 def ToChar(byte):
345     """Convert a byte to a character
346
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.
350
351     Args:
352         byte: A byte or str value
353     """
354     return chr(byte) if type(byte) != str else byte
355
356 def ToChars(byte_list):
357     """Convert a list of bytes to a str/bytes type
358
359     Args:
360         byte_list: List of ASCII values representing the string
361
362     Returns:
363         string made by concatenating all the ASCII values
364     """
365     return ''.join([chr(byte) for byte in byte_list])
366
367 def ToBytes(string):
368     """Convert a str type into a bytes type
369
370     Args:
371         string: string to convert value
372
373     Returns:
374         Python 3: A bytes type
375         Python 2: A string type
376     """
377     if sys.version_info[0] >= 3:
378         return string.encode('utf-8')
379     return string
380
381 def Compress(indata, algo, with_header=True):
382     """Compress some data using a given algorithm
383
384     Note that for lzma this uses an old version of the algorithm, not that
385     provided by xz.
386
387     This requires 'lz4' and 'lzma_alone' tools. It also requires an output
388     directory to be previously set up, by calling PrepareOutputDir().
389
390     Args:
391         indata: Input data to compress
392         algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
393
394     Returns:
395         Compressed data
396     """
397     if algo == 'none':
398         return indata
399     fname = GetOutputFilename('%s.comp.tmp' % algo)
400     WriteFile(fname, indata)
401     if algo == 'lz4':
402         data = Run('lz4', '--no-frame-crc', '-c', fname, binary=True)
403     # cbfstool uses a very old version of lzma
404     elif algo == 'lzma':
405         outfname = GetOutputFilename('%s.comp.otmp' % algo)
406         Run('lzma_alone', 'e', fname, outfname, '-lc1', '-lp0', '-pb0', '-d8')
407         data = ReadFile(outfname)
408     elif algo == 'gzip':
409         data = Run('gzip', '-c', fname, binary=True)
410     else:
411         raise ValueError("Unknown algorithm '%s'" % algo)
412     if with_header:
413         hdr = struct.pack('<I', len(data))
414         data = hdr + data
415     return data
416
417 def Decompress(indata, algo, with_header=True):
418     """Decompress some data using a given algorithm
419
420     Note that for lzma this uses an old version of the algorithm, not that
421     provided by xz.
422
423     This requires 'lz4' and 'lzma_alone' tools. It also requires an output
424     directory to be previously set up, by calling PrepareOutputDir().
425
426     Args:
427         indata: Input data to decompress
428         algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
429
430     Returns:
431         Compressed data
432     """
433     if algo == 'none':
434         return indata
435     if with_header:
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:
440         fd.write(indata)
441     if algo == 'lz4':
442         data = Run('lz4', '-dc', fname, binary=True)
443     elif algo == 'lzma':
444         outfname = GetOutputFilename('%s.decomp.otmp' % algo)
445         Run('lzma_alone', 'd', fname, outfname)
446         data = ReadFile(outfname)
447     elif algo == 'gzip':
448         data = Run('gzip', '-cd', fname, binary=True)
449     else:
450         raise ValueError("Unknown algorithm '%s'" % algo)
451     return data
452
453 CMD_CREATE, CMD_DELETE, CMD_ADD, CMD_REPLACE, CMD_EXTRACT = range(5)
454
455 IFWITOOL_CMDS = {
456     CMD_CREATE: 'create',
457     CMD_DELETE: 'delete',
458     CMD_ADD: 'add',
459     CMD_REPLACE: 'replace',
460     CMD_EXTRACT: 'extract',
461     }
462
463 def RunIfwiTool(ifwi_file, cmd, fname=None, subpart=None, entry_name=None):
464     """Run ifwitool with the given arguments:
465
466     Args:
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
470             CMD_DELETE)
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
473     """
474     args = ['ifwitool', ifwi_file]
475     args.append(IFWITOOL_CMDS[cmd])
476     if fname:
477         args += ['-f', fname]
478     if subpart:
479         args += ['-n', subpart]
480     if entry_name:
481         args += ['-d', '-e', entry_name]
482     Run(*args)
483
484 def ToHex(val):
485     """Convert an integer value (or None) to a string
486
487     Returns:
488         hex value, or 'None' if the value is None
489     """
490     return 'None' if val is None else '%#x' % val
491
492 def ToHexSize(val):
493     """Return the size of an object in hex
494
495     Returns:
496         hex value of size, or 'None' if the value is None
497     """
498     return 'None' if val is None else '%#x' % len(val)