blob: 4a7fcdad214e7386cf46d0cde55a7bc68ca3006d [file] [log] [blame]
Tom Rini83d290c2018-05-06 17:58:06 -04001# SPDX-License-Identifier: GPL-2.0+
Simon Glass1f1864b2016-07-25 18:59:08 -06002#
3# Copyright (c) 2016 Google, Inc
4#
Simon Glass1f1864b2016-07-25 18:59:08 -06005
Simon Glass1cfdfc02019-07-08 13:18:51 -06006from __future__ import print_function
7
Simon Glassaeffc5e2018-07-17 13:25:43 -06008import command
Simon Glass0a98b282018-09-14 04:57:28 -06009import glob
Simon Glass1f1864b2016-07-25 18:59:08 -060010import os
11import shutil
Simon Glasseb0f4a42019-07-20 12:24:06 -060012import struct
Simon Glasse6d85ff2019-05-14 15:53:47 -060013import sys
Simon Glass1f1864b2016-07-25 18:59:08 -060014import tempfile
15
16import tout
17
Simon Glassaeffc5e2018-07-17 13:25:43 -060018# Output directly (generally this is temporary)
Simon Glass1f1864b2016-07-25 18:59:08 -060019outdir = None
Simon Glassaeffc5e2018-07-17 13:25:43 -060020
21# True to keep the output directory around after exiting
Simon Glass1f1864b2016-07-25 18:59:08 -060022preserve_outdir = False
23
Simon Glassaeffc5e2018-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 Glassc22b8cf2019-07-08 13:18:27 -060030tool_search_paths = []
31
Simon Glass04187a82018-09-14 04:57:25 -060032# Tools and the packages that contain them, on debian
33packages = {
34 'lz4': 'liblz4-tool',
35 }
Simon Glassaeffc5e2018-07-17 13:25:43 -060036
Simon Glass1fda1822018-10-01 21:12:44 -060037# List of paths to use when looking for an input file
38indir = []
39
Simon Glass1f1864b2016-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()
Simon Glass31353302019-07-20 12:24:07 -060086 outdir = None
Simon Glass1f1864b2016-07-25 18:59:08 -060087
88def 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
99def _FinaliseForTest():
100 """Remove the output directory (for use by tests)"""
101 global outdir
102
103 if outdir:
104 _RemoveOutputDir()
Simon Glass31353302019-07-20 12:24:07 -0600105 outdir = None
Simon Glass1f1864b2016-07-25 18:59:08 -0600106
107def 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
119def 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 """
Simon Glassf514d8f2019-08-24 07:22:54 -0600128 if not indir or fname[:1] == '/':
Simon Glass1f1864b2016-07-25 18:59:08 -0600129 return fname
130 for dirname in indir:
131 pathname = os.path.join(dirname, fname)
132 if os.path.exists(pathname):
133 return pathname
134
Simon Glass4f5dea42018-07-17 13:25:45 -0600135 raise ValueError("Filename '%s' not found in input path (%s) (cwd='%s')" %
136 (fname, ','.join(indir), os.getcwd()))
Simon Glass1f1864b2016-07-25 18:59:08 -0600137
Simon Glass0a98b282018-09-14 04:57:28 -0600138def 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
Simon Glass1f1864b2016-07-25 18:59:08 -0600155def Align(pos, align):
156 if align:
157 mask = align - 1
158 pos = (pos + mask) & ~mask
159 return pos
160
161def NotPowerOfTwo(num):
162 return num and (num & (num - 1))
Simon Glassaeffc5e2018-07-17 13:25:43 -0600163
Simon Glassc22b8cf2019-07-08 13:18:27 -0600164def 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
174def PathHasFile(path_spec, fname):
Simon Glass04187a82018-09-14 04:57:25 -0600175 """Check if a given filename is in the PATH
176
177 Args:
Simon Glassc22b8cf2019-07-08 13:18:27 -0600178 path_spec: Value of PATH variable to check
Simon Glass04187a82018-09-14 04:57:25 -0600179 fname: Filename to check
180
181 Returns:
182 True if found, False if not
183 """
Simon Glassc22b8cf2019-07-08 13:18:27 -0600184 for dir in path_spec.split(':'):
Simon Glass04187a82018-09-14 04:57:25 -0600185 if os.path.exists(os.path.join(dir, fname)):
186 return True
187 return False
188
Simon Glass3b1c0b02019-08-24 07:22:41 -0600189def Run(name, *args):
Simon Glassc22b8cf2019-07-08 13:18:27 -0600190 """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
Simon Glassc22b8cf2019-07-08 13:18:27 -0600199
200 Returns:
201 CommandResult object
202 """
Simon Glass04187a82018-09-14 04:57:25 -0600203 try:
Simon Glassc22b8cf2019-07-08 13:18:27 -0600204 env = None
205 if tool_search_paths:
206 env = dict(os.environ)
207 env['PATH'] = ':'.join(tool_search_paths) + ':' + env['PATH']
Simon Glass6eace392019-08-24 07:22:42 -0600208 all_args = (name,) + args
209 result = command.RunPipe([all_args], capture=True, capture_stderr=True,
210 env=env, raise_on_error=False)
211 if result.return_code:
212 raise Exception("Error %d running '%s': %s" %
213 (result.return_code,' '.join(all_args),
214 result.stderr))
215 return result.stdout
Simon Glass04187a82018-09-14 04:57:25 -0600216 except:
Simon Glassc22b8cf2019-07-08 13:18:27 -0600217 if env and not PathHasFile(env['PATH'], name):
218 msg = "Please install tool '%s'" % name
Simon Glass04187a82018-09-14 04:57:25 -0600219 package = packages.get(name)
220 if package:
221 msg += " (e.g. from package '%s')" % package
222 raise ValueError(msg)
223 raise
Simon Glassaeffc5e2018-07-17 13:25:43 -0600224
225def Filename(fname):
226 """Resolve a file path to an absolute path.
227
228 If fname starts with ##/ and chroot is available, ##/ gets replaced with
229 the chroot path. If chroot is not available, this file name can not be
230 resolved, `None' is returned.
231
232 If fname is not prepended with the above prefix, and is not an existing
233 file, the actual file name is retrieved from the passed in string and the
234 search_paths directories (if any) are searched to for the file. If found -
235 the path to the found file is returned, `None' is returned otherwise.
236
237 Args:
238 fname: a string, the path to resolve.
239
240 Returns:
241 Absolute path to the file or None if not found.
242 """
243 if fname.startswith('##/'):
244 if chroot_path:
245 fname = os.path.join(chroot_path, fname[3:])
246 else:
247 return None
248
249 # Search for a pathname that exists, and return it if found
250 if fname and not os.path.exists(fname):
251 for path in search_paths:
252 pathname = os.path.join(path, os.path.basename(fname))
253 if os.path.exists(pathname):
254 return pathname
255
256 # If not found, just return the standard, unchanged path
257 return fname
258
Simon Glass3c47e412019-05-17 22:00:44 -0600259def ReadFile(fname, binary=True):
Simon Glassaeffc5e2018-07-17 13:25:43 -0600260 """Read and return the contents of a file.
261
262 Args:
263 fname: path to filename to read, where ## signifiies the chroot.
264
265 Returns:
266 data read from file, as a string.
267 """
Simon Glass3c47e412019-05-17 22:00:44 -0600268 with open(Filename(fname), binary and 'rb' or 'r') as fd:
Simon Glassaeffc5e2018-07-17 13:25:43 -0600269 data = fd.read()
270 #self._out.Info("Read file '%s' size %d (%#0x)" %
271 #(fname, len(data), len(data)))
272 return data
273
274def WriteFile(fname, data):
275 """Write data into a file.
276
277 Args:
278 fname: path to filename to write
279 data: data to write to file, as a string
280 """
281 #self._out.Info("Write file '%s' size %d (%#0x)" %
282 #(fname, len(data), len(data)))
283 with open(Filename(fname), 'wb') as fd:
284 fd.write(data)
Simon Glasse6d85ff2019-05-14 15:53:47 -0600285
286def GetBytes(byte, size):
287 """Get a string of bytes of a given size
288
289 This handles the unfortunate different between Python 2 and Python 2.
290
291 Args:
292 byte: Numeric byte value to use
293 size: Size of bytes/string to return
294
295 Returns:
296 A bytes type with 'byte' repeated 'size' times
297 """
298 if sys.version_info[0] >= 3:
299 data = bytes([byte]) * size
300 else:
301 data = chr(byte) * size
302 return data
Simon Glass513eace2019-05-14 15:53:50 -0600303
304def ToUnicode(val):
305 """Make sure a value is a unicode string
306
307 This allows some amount of compatibility between Python 2 and Python3. For
308 the former, it returns a unicode object.
309
310 Args:
311 val: string or unicode object
312
313 Returns:
314 unicode version of val
315 """
316 if sys.version_info[0] >= 3:
317 return val
318 return val if isinstance(val, unicode) else val.decode('utf-8')
319
320def FromUnicode(val):
321 """Make sure a value is a non-unicode string
322
323 This allows some amount of compatibility between Python 2 and Python3. For
324 the former, it converts a unicode object to a string.
325
326 Args:
327 val: string or unicode object
328
329 Returns:
330 non-unicode version of val
331 """
332 if sys.version_info[0] >= 3:
333 return val
334 return val if isinstance(val, str) else val.encode('utf-8')
Simon Glass2b6ed5e2019-05-17 22:00:35 -0600335
336def ToByte(ch):
337 """Convert a character to an ASCII value
338
339 This is useful because in Python 2 bytes is an alias for str, but in
340 Python 3 they are separate types. This function converts the argument to
341 an ASCII value in either case.
342
343 Args:
344 ch: A string (Python 2) or byte (Python 3) value
345
346 Returns:
347 integer ASCII value for ch
348 """
349 return ord(ch) if type(ch) == str else ch
350
351def ToChar(byte):
352 """Convert a byte to a character
353
354 This is useful because in Python 2 bytes is an alias for str, but in
355 Python 3 they are separate types. This function converts an ASCII value to
356 a value with the appropriate type in either case.
357
358 Args:
359 byte: A byte or str value
360 """
361 return chr(byte) if type(byte) != str else byte
Simon Glassf6b64812019-05-17 22:00:36 -0600362
363def ToChars(byte_list):
364 """Convert a list of bytes to a str/bytes type
365
366 Args:
367 byte_list: List of ASCII values representing the string
368
369 Returns:
370 string made by concatenating all the ASCII values
371 """
372 return ''.join([chr(byte) for byte in byte_list])
373
374def ToBytes(string):
375 """Convert a str type into a bytes type
376
377 Args:
378 string: string to convert value
379
380 Returns:
381 Python 3: A bytes type
382 Python 2: A string type
383 """
384 if sys.version_info[0] >= 3:
385 return string.encode('utf-8')
386 return string
Simon Glass07d9e702019-07-08 13:18:41 -0600387
Simon Glasseb0f4a42019-07-20 12:24:06 -0600388def Compress(indata, algo, with_header=True):
Simon Glass07d9e702019-07-08 13:18:41 -0600389 """Compress some data using a given algorithm
390
391 Note that for lzma this uses an old version of the algorithm, not that
392 provided by xz.
393
394 This requires 'lz4' and 'lzma_alone' tools. It also requires an output
395 directory to be previously set up, by calling PrepareOutputDir().
396
397 Args:
398 indata: Input data to compress
399 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
400
401 Returns:
402 Compressed data
403 """
404 if algo == 'none':
405 return indata
406 fname = GetOutputFilename('%s.comp.tmp' % algo)
407 WriteFile(fname, indata)
408 if algo == 'lz4':
Simon Glass3b1c0b02019-08-24 07:22:41 -0600409 data = Run('lz4', '--no-frame-crc', '-c', fname)
Simon Glass07d9e702019-07-08 13:18:41 -0600410 # cbfstool uses a very old version of lzma
411 elif algo == 'lzma':
412 outfname = GetOutputFilename('%s.comp.otmp' % algo)
413 Run('lzma_alone', 'e', fname, outfname, '-lc1', '-lp0', '-pb0', '-d8')
414 data = ReadFile(outfname)
415 elif algo == 'gzip':
Simon Glass3b1c0b02019-08-24 07:22:41 -0600416 data = Run('gzip', '-c', fname)
Simon Glass07d9e702019-07-08 13:18:41 -0600417 else:
418 raise ValueError("Unknown algorithm '%s'" % algo)
Simon Glasseb0f4a42019-07-20 12:24:06 -0600419 if with_header:
420 hdr = struct.pack('<I', len(data))
421 data = hdr + data
Simon Glass07d9e702019-07-08 13:18:41 -0600422 return data
423
Simon Glasseb0f4a42019-07-20 12:24:06 -0600424def Decompress(indata, algo, with_header=True):
Simon Glass07d9e702019-07-08 13:18:41 -0600425 """Decompress some data using a given algorithm
426
427 Note that for lzma this uses an old version of the algorithm, not that
428 provided by xz.
429
430 This requires 'lz4' and 'lzma_alone' tools. It also requires an output
431 directory to be previously set up, by calling PrepareOutputDir().
432
433 Args:
434 indata: Input data to decompress
435 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
436
437 Returns:
438 Compressed data
439 """
440 if algo == 'none':
441 return indata
Simon Glasseb0f4a42019-07-20 12:24:06 -0600442 if with_header:
443 data_len = struct.unpack('<I', indata[:4])[0]
444 indata = indata[4:4 + data_len]
Simon Glass07d9e702019-07-08 13:18:41 -0600445 fname = GetOutputFilename('%s.decomp.tmp' % algo)
446 with open(fname, 'wb') as fd:
447 fd.write(indata)
448 if algo == 'lz4':
Simon Glass3b1c0b02019-08-24 07:22:41 -0600449 data = Run('lz4', '-dc', fname)
Simon Glass07d9e702019-07-08 13:18:41 -0600450 elif algo == 'lzma':
451 outfname = GetOutputFilename('%s.decomp.otmp' % algo)
452 Run('lzma_alone', 'd', fname, outfname)
453 data = ReadFile(outfname)
454 elif algo == 'gzip':
Simon Glass3b1c0b02019-08-24 07:22:41 -0600455 data = Run('gzip', '-cd', fname)
Simon Glass07d9e702019-07-08 13:18:41 -0600456 else:
457 raise ValueError("Unknown algorithm '%s'" % algo)
458 return data
Simon Glass1cfdfc02019-07-08 13:18:51 -0600459
460CMD_CREATE, CMD_DELETE, CMD_ADD, CMD_REPLACE, CMD_EXTRACT = range(5)
461
462IFWITOOL_CMDS = {
463 CMD_CREATE: 'create',
464 CMD_DELETE: 'delete',
465 CMD_ADD: 'add',
466 CMD_REPLACE: 'replace',
467 CMD_EXTRACT: 'extract',
468 }
469
470def RunIfwiTool(ifwi_file, cmd, fname=None, subpart=None, entry_name=None):
471 """Run ifwitool with the given arguments:
472
473 Args:
474 ifwi_file: IFWI file to operation on
475 cmd: Command to execute (CMD_...)
476 fname: Filename of file to add/replace/extract/create (None for
477 CMD_DELETE)
478 subpart: Name of sub-partition to operation on (None for CMD_CREATE)
479 entry_name: Name of directory entry to operate on, or None if none
480 """
481 args = ['ifwitool', ifwi_file]
482 args.append(IFWITOOL_CMDS[cmd])
483 if fname:
484 args += ['-f', fname]
485 if subpart:
486 args += ['-n', subpart]
487 if entry_name:
488 args += ['-d', '-e', entry_name]
489 Run(*args)
Simon Glass9f297b02019-07-20 12:23:36 -0600490
491def ToHex(val):
492 """Convert an integer value (or None) to a string
493
494 Returns:
495 hex value, or 'None' if the value is None
496 """
497 return 'None' if val is None else '%#x' % val
498
499def ToHexSize(val):
500 """Return the size of an object in hex
501
502 Returns:
503 hex value of size, or 'None' if the value is None
504 """
505 return 'None' if val is None else '%#x' % len(val)