blob: d615227482a06bd96932e91640fb6a25772a6557 [file] [log] [blame]
Tom Rini10e47792018-05-06 17:58:06 -04001# SPDX-License-Identifier: GPL-2.0+
Simon Glassbe39f052016-07-25 18:59:08 -06002#
3# Copyright (c) 2016 Google, Inc
4#
Simon Glassbe39f052016-07-25 18:59:08 -06005
Simon Glass578d53e2019-07-08 13:18:51 -06006from __future__ import print_function
7
Simon Glass0362cd22018-07-17 13:25:43 -06008import command
Simon Glassac6328c2018-09-14 04:57:28 -06009import glob
Simon Glassbe39f052016-07-25 18:59:08 -060010import os
11import shutil
Simon Glass37fdd142019-07-20 12:24:06 -060012import struct
Simon Glassac0d4952019-05-14 15:53:47 -060013import sys
Simon Glassbe39f052016-07-25 18:59:08 -060014import tempfile
15
16import tout
17
Simon Glass0362cd22018-07-17 13:25:43 -060018# Output directly (generally this is temporary)
Simon Glassbe39f052016-07-25 18:59:08 -060019outdir = None
Simon Glass0362cd22018-07-17 13:25:43 -060020
21# True to keep the output directory around after exiting
Simon Glassbe39f052016-07-25 18:59:08 -060022preserve_outdir = False
23
Simon Glass0362cd22018-07-17 13:25:43 -060024# Path to the Chrome OS chroot, if we know it
25chroot_path = None
26
27# Search paths to use for Filename(), used to find files
28search_paths = []
29
Simon Glass90c3c4c2019-07-08 13:18:27 -060030tool_search_paths = []
31
Simon Glass778ab842018-09-14 04:57:25 -060032# Tools and the packages that contain them, on debian
33packages = {
34 'lz4': 'liblz4-tool',
35 }
Simon Glass0362cd22018-07-17 13:25:43 -060036
Simon Glass867b9062018-10-01 21:12:44 -060037# List of paths to use when looking for an input file
38indir = []
39
Simon Glassbe39f052016-07-25 18:59:08 -060040def 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
73def _RemoveOutputDir():
74 global outdir
75
76 shutil.rmtree(outdir)
77 tout.Debug("Deleted temporary directory '%s'" % outdir)
78 outdir = None
79
80def 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
87def 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
98def _FinaliseForTest():
99 """Remove the output directory (for use by tests)"""
100 global outdir
101
102 if outdir:
103 _RemoveOutputDir()
104
105def 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
117def 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
Simon Glassf2eb0542018-07-17 13:25:45 -0600133 raise ValueError("Filename '%s' not found in input path (%s) (cwd='%s')" %
134 (fname, ','.join(indir), os.getcwd()))
Simon Glassbe39f052016-07-25 18:59:08 -0600135
Simon Glassac6328c2018-09-14 04:57:28 -0600136def 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
Simon Glassbe39f052016-07-25 18:59:08 -0600153def Align(pos, align):
154 if align:
155 mask = align - 1
156 pos = (pos + mask) & ~mask
157 return pos
158
159def NotPowerOfTwo(num):
160 return num and (num & (num - 1))
Simon Glass0362cd22018-07-17 13:25:43 -0600161
Simon Glass90c3c4c2019-07-08 13:18:27 -0600162def 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
172def PathHasFile(path_spec, fname):
Simon Glass778ab842018-09-14 04:57:25 -0600173 """Check if a given filename is in the PATH
174
175 Args:
Simon Glass90c3c4c2019-07-08 13:18:27 -0600176 path_spec: Value of PATH variable to check
Simon Glass778ab842018-09-14 04:57:25 -0600177 fname: Filename to check
178
179 Returns:
180 True if found, False if not
181 """
Simon Glass90c3c4c2019-07-08 13:18:27 -0600182 for dir in path_spec.split(':'):
Simon Glass778ab842018-09-14 04:57:25 -0600183 if os.path.exists(os.path.join(dir, fname)):
184 return True
185 return False
186
Simon Glassd8e9bca2019-05-14 15:53:44 -0600187def Run(name, *args, **kwargs):
Simon Glass90c3c4c2019-07-08 13:18:27 -0600188 """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 """
Simon Glass778ab842018-09-14 04:57:25 -0600202 try:
Simon Glass90c3c4c2019-07-08 13:18:27 -0600203 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)
Simon Glass778ab842018-09-14 04:57:25 -0600209 except:
Simon Glass90c3c4c2019-07-08 13:18:27 -0600210 if env and not PathHasFile(env['PATH'], name):
211 msg = "Please install tool '%s'" % name
Simon Glass778ab842018-09-14 04:57:25 -0600212 package = packages.get(name)
213 if package:
214 msg += " (e.g. from package '%s')" % package
215 raise ValueError(msg)
216 raise
Simon Glass0362cd22018-07-17 13:25:43 -0600217
218def 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
Simon Glass0696ee62019-05-17 22:00:44 -0600252def ReadFile(fname, binary=True):
Simon Glass0362cd22018-07-17 13:25:43 -0600253 """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 """
Simon Glass0696ee62019-05-17 22:00:44 -0600261 with open(Filename(fname), binary and 'rb' or 'r') as fd:
Simon Glass0362cd22018-07-17 13:25:43 -0600262 data = fd.read()
263 #self._out.Info("Read file '%s' size %d (%#0x)" %
264 #(fname, len(data), len(data)))
265 return data
266
267def 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)
Simon Glassac0d4952019-05-14 15:53:47 -0600278
279def 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
Simon Glass8bb7a7a2019-05-14 15:53:50 -0600296
297def 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
313def 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')
Simon Glassd8f593f2019-05-17 22:00:35 -0600328
329def 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
344def 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
Simon Glass1cd40082019-05-17 22:00:36 -0600355
356def 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
367def 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
Simon Glassdfd19012019-07-08 13:18:41 -0600380
Simon Glass37fdd142019-07-20 12:24:06 -0600381def Compress(indata, algo, with_header=True):
Simon Glassdfd19012019-07-08 13:18:41 -0600382 """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)
Simon Glass37fdd142019-07-20 12:24:06 -0600412 if with_header:
413 hdr = struct.pack('<I', len(data))
414 data = hdr + data
Simon Glassdfd19012019-07-08 13:18:41 -0600415 return data
416
Simon Glass37fdd142019-07-20 12:24:06 -0600417def Decompress(indata, algo, with_header=True):
Simon Glassdfd19012019-07-08 13:18:41 -0600418 """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
Simon Glass37fdd142019-07-20 12:24:06 -0600435 if with_header:
436 data_len = struct.unpack('<I', indata[:4])[0]
437 indata = indata[4:4 + data_len]
Simon Glassdfd19012019-07-08 13:18:41 -0600438 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
Simon Glass578d53e2019-07-08 13:18:51 -0600452
453CMD_CREATE, CMD_DELETE, CMD_ADD, CMD_REPLACE, CMD_EXTRACT = range(5)
454
455IFWITOOL_CMDS = {
456 CMD_CREATE: 'create',
457 CMD_DELETE: 'delete',
458 CMD_ADD: 'add',
459 CMD_REPLACE: 'replace',
460 CMD_EXTRACT: 'extract',
461 }
462
463def 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)
Simon Glassb6dff4c2019-07-20 12:23:36 -0600483
484def 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
492def 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)