blob: e945b54fa282f0544d8b256806531c3b6973e116 [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 Glassac0d4952019-05-14 15:53:47 -060012import sys
Simon Glassbe39f052016-07-25 18:59:08 -060013import tempfile
14
15import tout
16
Simon Glass0362cd22018-07-17 13:25:43 -060017# Output directly (generally this is temporary)
Simon Glassbe39f052016-07-25 18:59:08 -060018outdir = None
Simon Glass0362cd22018-07-17 13:25:43 -060019
20# True to keep the output directory around after exiting
Simon Glassbe39f052016-07-25 18:59:08 -060021preserve_outdir = False
22
Simon Glass0362cd22018-07-17 13:25:43 -060023# Path to the Chrome OS chroot, if we know it
24chroot_path = None
25
26# Search paths to use for Filename(), used to find files
27search_paths = []
28
Simon Glass90c3c4c2019-07-08 13:18:27 -060029tool_search_paths = []
30
Simon Glass778ab842018-09-14 04:57:25 -060031# Tools and the packages that contain them, on debian
32packages = {
33 'lz4': 'liblz4-tool',
34 }
Simon Glass0362cd22018-07-17 13:25:43 -060035
Simon Glass867b9062018-10-01 21:12:44 -060036# List of paths to use when looking for an input file
37indir = []
38
Simon Glassbe39f052016-07-25 18:59:08 -060039def PrepareOutputDir(dirname, preserve=False):
40 """Select an output directory, ensuring it exists.
41
42 This either creates a temporary directory or checks that the one supplied
43 by the user is valid. For a temporary directory, it makes a note to
44 remove it later if required.
45
46 Args:
47 dirname: a string, name of the output directory to use to store
48 intermediate and output files. If is None - create a temporary
49 directory.
50 preserve: a Boolean. If outdir above is None and preserve is False, the
51 created temporary directory will be destroyed on exit.
52
53 Raises:
54 OSError: If it cannot create the output directory.
55 """
56 global outdir, preserve_outdir
57
58 preserve_outdir = dirname or preserve
59 if dirname:
60 outdir = dirname
61 if not os.path.isdir(outdir):
62 try:
63 os.makedirs(outdir)
64 except OSError as err:
65 raise CmdError("Cannot make output directory '%s': '%s'" %
66 (outdir, err.strerror))
67 tout.Debug("Using output directory '%s'" % outdir)
68 else:
69 outdir = tempfile.mkdtemp(prefix='binman.')
70 tout.Debug("Using temporary directory '%s'" % outdir)
71
72def _RemoveOutputDir():
73 global outdir
74
75 shutil.rmtree(outdir)
76 tout.Debug("Deleted temporary directory '%s'" % outdir)
77 outdir = None
78
79def FinaliseOutputDir():
80 global outdir, preserve_outdir
81
82 """Tidy up: delete output directory if temporary and not preserved."""
83 if outdir and not preserve_outdir:
84 _RemoveOutputDir()
85
86def 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
97def _FinaliseForTest():
98 """Remove the output directory (for use by tests)"""
99 global outdir
100
101 if outdir:
102 _RemoveOutputDir()
103
104def SetInputDirs(dirname):
105 """Add a list of input directories, where input files are kept.
106
107 Args:
108 dirname: a list of paths to input directories to use for obtaining
109 files needed by binman to place in the image.
110 """
111 global indir
112
113 indir = dirname
114 tout.Debug("Using input directories %s" % indir)
115
116def GetInputFilename(fname):
117 """Return a filename for use as input.
118
119 Args:
120 fname: Filename to use for new file
121
122 Returns:
123 The full path of the filename, within the input directory
124 """
125 if not indir:
126 return fname
127 for dirname in indir:
128 pathname = os.path.join(dirname, fname)
129 if os.path.exists(pathname):
130 return pathname
131
Simon Glassf2eb0542018-07-17 13:25:45 -0600132 raise ValueError("Filename '%s' not found in input path (%s) (cwd='%s')" %
133 (fname, ','.join(indir), os.getcwd()))
Simon Glassbe39f052016-07-25 18:59:08 -0600134
Simon Glassac6328c2018-09-14 04:57:28 -0600135def GetInputFilenameGlob(pattern):
136 """Return a list of filenames for use as input.
137
138 Args:
139 pattern: Filename pattern to search for
140
141 Returns:
142 A list of matching files in all input directories
143 """
144 if not indir:
145 return glob.glob(fname)
146 files = []
147 for dirname in indir:
148 pathname = os.path.join(dirname, pattern)
149 files += glob.glob(pathname)
150 return sorted(files)
151
Simon Glassbe39f052016-07-25 18:59:08 -0600152def Align(pos, align):
153 if align:
154 mask = align - 1
155 pos = (pos + mask) & ~mask
156 return pos
157
158def NotPowerOfTwo(num):
159 return num and (num & (num - 1))
Simon Glass0362cd22018-07-17 13:25:43 -0600160
Simon Glass90c3c4c2019-07-08 13:18:27 -0600161def SetToolPaths(toolpaths):
162 """Set the path to search for tools
163
164 Args:
165 toolpaths: List of paths to search for tools executed by Run()
166 """
167 global tool_search_paths
168
169 tool_search_paths = toolpaths
170
171def PathHasFile(path_spec, fname):
Simon Glass778ab842018-09-14 04:57:25 -0600172 """Check if a given filename is in the PATH
173
174 Args:
Simon Glass90c3c4c2019-07-08 13:18:27 -0600175 path_spec: Value of PATH variable to check
Simon Glass778ab842018-09-14 04:57:25 -0600176 fname: Filename to check
177
178 Returns:
179 True if found, False if not
180 """
Simon Glass90c3c4c2019-07-08 13:18:27 -0600181 for dir in path_spec.split(':'):
Simon Glass778ab842018-09-14 04:57:25 -0600182 if os.path.exists(os.path.join(dir, fname)):
183 return True
184 return False
185
Simon Glassd8e9bca2019-05-14 15:53:44 -0600186def Run(name, *args, **kwargs):
Simon Glass90c3c4c2019-07-08 13:18:27 -0600187 """Run a tool with some arguments
188
189 This runs a 'tool', which is a program used by binman to process files and
190 perhaps produce some output. Tools can be located on the PATH or in a
191 search path.
192
193 Args:
194 name: Command name to run
195 args: Arguments to the tool
196 kwargs: Options to pass to command.run()
197
198 Returns:
199 CommandResult object
200 """
Simon Glass778ab842018-09-14 04:57:25 -0600201 try:
Simon Glass90c3c4c2019-07-08 13:18:27 -0600202 env = None
203 if tool_search_paths:
204 env = dict(os.environ)
205 env['PATH'] = ':'.join(tool_search_paths) + ':' + env['PATH']
206 return command.Run(name, *args, capture=True,
207 capture_stderr=True, env=env, **kwargs)
Simon Glass778ab842018-09-14 04:57:25 -0600208 except:
Simon Glass90c3c4c2019-07-08 13:18:27 -0600209 if env and not PathHasFile(env['PATH'], name):
210 msg = "Please install tool '%s'" % name
Simon Glass778ab842018-09-14 04:57:25 -0600211 package = packages.get(name)
212 if package:
213 msg += " (e.g. from package '%s')" % package
214 raise ValueError(msg)
215 raise
Simon Glass0362cd22018-07-17 13:25:43 -0600216
217def Filename(fname):
218 """Resolve a file path to an absolute path.
219
220 If fname starts with ##/ and chroot is available, ##/ gets replaced with
221 the chroot path. If chroot is not available, this file name can not be
222 resolved, `None' is returned.
223
224 If fname is not prepended with the above prefix, and is not an existing
225 file, the actual file name is retrieved from the passed in string and the
226 search_paths directories (if any) are searched to for the file. If found -
227 the path to the found file is returned, `None' is returned otherwise.
228
229 Args:
230 fname: a string, the path to resolve.
231
232 Returns:
233 Absolute path to the file or None if not found.
234 """
235 if fname.startswith('##/'):
236 if chroot_path:
237 fname = os.path.join(chroot_path, fname[3:])
238 else:
239 return None
240
241 # Search for a pathname that exists, and return it if found
242 if fname and not os.path.exists(fname):
243 for path in search_paths:
244 pathname = os.path.join(path, os.path.basename(fname))
245 if os.path.exists(pathname):
246 return pathname
247
248 # If not found, just return the standard, unchanged path
249 return fname
250
Simon Glass0696ee62019-05-17 22:00:44 -0600251def ReadFile(fname, binary=True):
Simon Glass0362cd22018-07-17 13:25:43 -0600252 """Read and return the contents of a file.
253
254 Args:
255 fname: path to filename to read, where ## signifiies the chroot.
256
257 Returns:
258 data read from file, as a string.
259 """
Simon Glass0696ee62019-05-17 22:00:44 -0600260 with open(Filename(fname), binary and 'rb' or 'r') as fd:
Simon Glass0362cd22018-07-17 13:25:43 -0600261 data = fd.read()
262 #self._out.Info("Read file '%s' size %d (%#0x)" %
263 #(fname, len(data), len(data)))
264 return data
265
266def WriteFile(fname, data):
267 """Write data into a file.
268
269 Args:
270 fname: path to filename to write
271 data: data to write to file, as a string
272 """
273 #self._out.Info("Write file '%s' size %d (%#0x)" %
274 #(fname, len(data), len(data)))
275 with open(Filename(fname), 'wb') as fd:
276 fd.write(data)
Simon Glassac0d4952019-05-14 15:53:47 -0600277
278def GetBytes(byte, size):
279 """Get a string of bytes of a given size
280
281 This handles the unfortunate different between Python 2 and Python 2.
282
283 Args:
284 byte: Numeric byte value to use
285 size: Size of bytes/string to return
286
287 Returns:
288 A bytes type with 'byte' repeated 'size' times
289 """
290 if sys.version_info[0] >= 3:
291 data = bytes([byte]) * size
292 else:
293 data = chr(byte) * size
294 return data
Simon Glass8bb7a7a2019-05-14 15:53:50 -0600295
296def ToUnicode(val):
297 """Make sure a value is a unicode string
298
299 This allows some amount of compatibility between Python 2 and Python3. For
300 the former, it returns a unicode object.
301
302 Args:
303 val: string or unicode object
304
305 Returns:
306 unicode version of val
307 """
308 if sys.version_info[0] >= 3:
309 return val
310 return val if isinstance(val, unicode) else val.decode('utf-8')
311
312def FromUnicode(val):
313 """Make sure a value is a non-unicode string
314
315 This allows some amount of compatibility between Python 2 and Python3. For
316 the former, it converts a unicode object to a string.
317
318 Args:
319 val: string or unicode object
320
321 Returns:
322 non-unicode version of val
323 """
324 if sys.version_info[0] >= 3:
325 return val
326 return val if isinstance(val, str) else val.encode('utf-8')
Simon Glassd8f593f2019-05-17 22:00:35 -0600327
328def ToByte(ch):
329 """Convert a character to an ASCII value
330
331 This is useful because in Python 2 bytes is an alias for str, but in
332 Python 3 they are separate types. This function converts the argument to
333 an ASCII value in either case.
334
335 Args:
336 ch: A string (Python 2) or byte (Python 3) value
337
338 Returns:
339 integer ASCII value for ch
340 """
341 return ord(ch) if type(ch) == str else ch
342
343def ToChar(byte):
344 """Convert a byte to a character
345
346 This is useful because in Python 2 bytes is an alias for str, but in
347 Python 3 they are separate types. This function converts an ASCII value to
348 a value with the appropriate type in either case.
349
350 Args:
351 byte: A byte or str value
352 """
353 return chr(byte) if type(byte) != str else byte
Simon Glass1cd40082019-05-17 22:00:36 -0600354
355def ToChars(byte_list):
356 """Convert a list of bytes to a str/bytes type
357
358 Args:
359 byte_list: List of ASCII values representing the string
360
361 Returns:
362 string made by concatenating all the ASCII values
363 """
364 return ''.join([chr(byte) for byte in byte_list])
365
366def ToBytes(string):
367 """Convert a str type into a bytes type
368
369 Args:
370 string: string to convert value
371
372 Returns:
373 Python 3: A bytes type
374 Python 2: A string type
375 """
376 if sys.version_info[0] >= 3:
377 return string.encode('utf-8')
378 return string
Simon Glassdfd19012019-07-08 13:18:41 -0600379
380def Compress(indata, algo):
381 """Compress some data using a given algorithm
382
383 Note that for lzma this uses an old version of the algorithm, not that
384 provided by xz.
385
386 This requires 'lz4' and 'lzma_alone' tools. It also requires an output
387 directory to be previously set up, by calling PrepareOutputDir().
388
389 Args:
390 indata: Input data to compress
391 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
392
393 Returns:
394 Compressed data
395 """
396 if algo == 'none':
397 return indata
398 fname = GetOutputFilename('%s.comp.tmp' % algo)
399 WriteFile(fname, indata)
400 if algo == 'lz4':
401 data = Run('lz4', '--no-frame-crc', '-c', fname, binary=True)
402 # cbfstool uses a very old version of lzma
403 elif algo == 'lzma':
404 outfname = GetOutputFilename('%s.comp.otmp' % algo)
405 Run('lzma_alone', 'e', fname, outfname, '-lc1', '-lp0', '-pb0', '-d8')
406 data = ReadFile(outfname)
407 elif algo == 'gzip':
408 data = Run('gzip', '-c', fname, binary=True)
409 else:
410 raise ValueError("Unknown algorithm '%s'" % algo)
411 return data
412
413def Decompress(indata, algo):
414 """Decompress some data using a given algorithm
415
416 Note that for lzma this uses an old version of the algorithm, not that
417 provided by xz.
418
419 This requires 'lz4' and 'lzma_alone' tools. It also requires an output
420 directory to be previously set up, by calling PrepareOutputDir().
421
422 Args:
423 indata: Input data to decompress
424 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
425
426 Returns:
427 Compressed data
428 """
429 if algo == 'none':
430 return indata
431 fname = GetOutputFilename('%s.decomp.tmp' % algo)
432 with open(fname, 'wb') as fd:
433 fd.write(indata)
434 if algo == 'lz4':
435 data = Run('lz4', '-dc', fname, binary=True)
436 elif algo == 'lzma':
437 outfname = GetOutputFilename('%s.decomp.otmp' % algo)
438 Run('lzma_alone', 'd', fname, outfname)
439 data = ReadFile(outfname)
440 elif algo == 'gzip':
441 data = Run('gzip', '-cd', fname, binary=True)
442 else:
443 raise ValueError("Unknown algorithm '%s'" % algo)
444 return data
Simon Glass578d53e2019-07-08 13:18:51 -0600445
446CMD_CREATE, CMD_DELETE, CMD_ADD, CMD_REPLACE, CMD_EXTRACT = range(5)
447
448IFWITOOL_CMDS = {
449 CMD_CREATE: 'create',
450 CMD_DELETE: 'delete',
451 CMD_ADD: 'add',
452 CMD_REPLACE: 'replace',
453 CMD_EXTRACT: 'extract',
454 }
455
456def RunIfwiTool(ifwi_file, cmd, fname=None, subpart=None, entry_name=None):
457 """Run ifwitool with the given arguments:
458
459 Args:
460 ifwi_file: IFWI file to operation on
461 cmd: Command to execute (CMD_...)
462 fname: Filename of file to add/replace/extract/create (None for
463 CMD_DELETE)
464 subpart: Name of sub-partition to operation on (None for CMD_CREATE)
465 entry_name: Name of directory entry to operate on, or None if none
466 """
467 args = ['ifwitool', ifwi_file]
468 args.append(IFWITOOL_CMDS[cmd])
469 if fname:
470 args += ['-f', fname]
471 if subpart:
472 args += ['-n', subpart]
473 if entry_name:
474 args += ['-d', '-e', entry_name]
475 Run(*args)