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