blob: 10997e438684496338426126fa65729fb409b5eb [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 Glass0a98b282018-09-14 04:57:28 -06006import glob
Simon Glass1f1864b2016-07-25 18:59:08 -06007import os
8import shutil
Simon Glasseb0f4a42019-07-20 12:24:06 -06009import struct
Simon Glasse6d85ff2019-05-14 15:53:47 -060010import sys
Simon Glass1f1864b2016-07-25 18:59:08 -060011import tempfile
12
Simon Glassbf776672020-04-17 18:09:04 -060013from patman import command
14from patman import tout
Simon Glass1f1864b2016-07-25 18:59:08 -060015
Simon Glassaeffc5e2018-07-17 13:25:43 -060016# Output directly (generally this is temporary)
Simon Glass1f1864b2016-07-25 18:59:08 -060017outdir = None
Simon Glassaeffc5e2018-07-17 13:25:43 -060018
19# True to keep the output directory around after exiting
Simon Glass1f1864b2016-07-25 18:59:08 -060020preserve_outdir = False
21
Simon Glassaeffc5e2018-07-17 13:25:43 -060022# Path to the Chrome OS chroot, if we know it
23chroot_path = None
24
25# Search paths to use for Filename(), used to find files
26search_paths = []
27
Simon Glassc22b8cf2019-07-08 13:18:27 -060028tool_search_paths = []
29
Simon Glass04187a82018-09-14 04:57:25 -060030# Tools and the packages that contain them, on debian
31packages = {
32 'lz4': 'liblz4-tool',
33 }
Simon Glassaeffc5e2018-07-17 13:25:43 -060034
Simon Glass1fda1822018-10-01 21:12:44 -060035# List of paths to use when looking for an input file
36indir = []
37
Simon Glass1f1864b2016-07-25 18:59:08 -060038def 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
71def _RemoveOutputDir():
72 global outdir
73
74 shutil.rmtree(outdir)
75 tout.Debug("Deleted temporary directory '%s'" % outdir)
76 outdir = None
77
78def 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()
Simon Glass31353302019-07-20 12:24:07 -060084 outdir = None
Simon Glass1f1864b2016-07-25 18:59:08 -060085
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
Simon Glass10cbd3b2020-12-28 20:34:52 -070097def GetOutputDir():
98 """Return the current output directory
99
100 Returns:
101 str: The output directory
102 """
103 return outdir
104
Simon Glass1f1864b2016-07-25 18:59:08 -0600105def _FinaliseForTest():
106 """Remove the output directory (for use by tests)"""
107 global outdir
108
109 if outdir:
110 _RemoveOutputDir()
Simon Glass31353302019-07-20 12:24:07 -0600111 outdir = None
Simon Glass1f1864b2016-07-25 18:59:08 -0600112
113def SetInputDirs(dirname):
114 """Add a list of input directories, where input files are kept.
115
116 Args:
117 dirname: a list of paths to input directories to use for obtaining
118 files needed by binman to place in the image.
119 """
120 global indir
121
122 indir = dirname
123 tout.Debug("Using input directories %s" % indir)
124
Simon Glass4f9f1052020-07-09 18:39:38 -0600125def GetInputFilename(fname, allow_missing=False):
Simon Glass1f1864b2016-07-25 18:59:08 -0600126 """Return a filename for use as input.
127
128 Args:
129 fname: Filename to use for new file
Simon Glass4f9f1052020-07-09 18:39:38 -0600130 allow_missing: True if the filename can be missing
Simon Glass1f1864b2016-07-25 18:59:08 -0600131
132 Returns:
Simon Glass4f9f1052020-07-09 18:39:38 -0600133 The full path of the filename, within the input directory, or
134 None on error
Simon Glass1f1864b2016-07-25 18:59:08 -0600135 """
Simon Glassf514d8f2019-08-24 07:22:54 -0600136 if not indir or fname[:1] == '/':
Simon Glass1f1864b2016-07-25 18:59:08 -0600137 return fname
138 for dirname in indir:
139 pathname = os.path.join(dirname, fname)
140 if os.path.exists(pathname):
141 return pathname
142
Simon Glass4f9f1052020-07-09 18:39:38 -0600143 if allow_missing:
144 return None
Simon Glass4f5dea42018-07-17 13:25:45 -0600145 raise ValueError("Filename '%s' not found in input path (%s) (cwd='%s')" %
146 (fname, ','.join(indir), os.getcwd()))
Simon Glass1f1864b2016-07-25 18:59:08 -0600147
Simon Glass0a98b282018-09-14 04:57:28 -0600148def GetInputFilenameGlob(pattern):
149 """Return a list of filenames for use as input.
150
151 Args:
152 pattern: Filename pattern to search for
153
154 Returns:
155 A list of matching files in all input directories
156 """
157 if not indir:
158 return glob.glob(fname)
159 files = []
160 for dirname in indir:
161 pathname = os.path.join(dirname, pattern)
162 files += glob.glob(pathname)
163 return sorted(files)
164
Simon Glass1f1864b2016-07-25 18:59:08 -0600165def Align(pos, align):
166 if align:
167 mask = align - 1
168 pos = (pos + mask) & ~mask
169 return pos
170
171def NotPowerOfTwo(num):
172 return num and (num & (num - 1))
Simon Glassaeffc5e2018-07-17 13:25:43 -0600173
Simon Glassc22b8cf2019-07-08 13:18:27 -0600174def SetToolPaths(toolpaths):
175 """Set the path to search for tools
176
177 Args:
178 toolpaths: List of paths to search for tools executed by Run()
179 """
180 global tool_search_paths
181
182 tool_search_paths = toolpaths
183
184def PathHasFile(path_spec, fname):
Simon Glass04187a82018-09-14 04:57:25 -0600185 """Check if a given filename is in the PATH
186
187 Args:
Simon Glassc22b8cf2019-07-08 13:18:27 -0600188 path_spec: Value of PATH variable to check
Simon Glass04187a82018-09-14 04:57:25 -0600189 fname: Filename to check
190
191 Returns:
192 True if found, False if not
193 """
Simon Glassc22b8cf2019-07-08 13:18:27 -0600194 for dir in path_spec.split(':'):
Simon Glass04187a82018-09-14 04:57:25 -0600195 if os.path.exists(os.path.join(dir, fname)):
196 return True
197 return False
198
Alper Nebi Yasak29cc0912020-09-06 14:46:06 +0300199def GetHostCompileTool(name):
200 """Get the host-specific version for a compile tool
201
202 This checks the environment variables that specify which version of
203 the tool should be used (e.g. ${HOSTCC}).
204
205 The following table lists the host-specific versions of the tools
206 this function resolves to:
207
208 Compile Tool | Host version
209 --------------+----------------
210 as | ${HOSTAS}
211 ld | ${HOSTLD}
212 cc | ${HOSTCC}
213 cpp | ${HOSTCPP}
214 c++ | ${HOSTCXX}
215 ar | ${HOSTAR}
216 nm | ${HOSTNM}
217 ldr | ${HOSTLDR}
218 strip | ${HOSTSTRIP}
219 objcopy | ${HOSTOBJCOPY}
220 objdump | ${HOSTOBJDUMP}
221 dtc | ${HOSTDTC}
222
223 Args:
224 name: Command name to run
225
226 Returns:
227 host_name: Exact command name to run instead
228 extra_args: List of extra arguments to pass
229 """
230 host_name = None
231 extra_args = []
232 if name in ('as', 'ld', 'cc', 'cpp', 'ar', 'nm', 'ldr', 'strip',
233 'objcopy', 'objdump', 'dtc'):
234 host_name, *host_args = env.get('HOST' + name.upper(), '').split(' ')
235 elif name == 'c++':
236 host_name, *host_args = env.get('HOSTCXX', '').split(' ')
237
238 if host_name:
239 return host_name, extra_args
240 return name, []
241
Alper Nebi Yasak1e4687a2020-09-06 14:46:05 +0300242def GetTargetCompileTool(name, cross_compile=None):
243 """Get the target-specific version for a compile tool
244
245 This first checks the environment variables that specify which
246 version of the tool should be used (e.g. ${CC}). If those aren't
247 specified, it checks the CROSS_COMPILE variable as a prefix for the
248 tool with some substitutions (e.g. "${CROSS_COMPILE}gcc" for cc).
249
250 The following table lists the target-specific versions of the tools
251 this function resolves to:
252
253 Compile Tool | First choice | Second choice
254 --------------+----------------+----------------------------
255 as | ${AS} | ${CROSS_COMPILE}as
256 ld | ${LD} | ${CROSS_COMPILE}ld.bfd
257 | | or ${CROSS_COMPILE}ld
258 cc | ${CC} | ${CROSS_COMPILE}gcc
259 cpp | ${CPP} | ${CROSS_COMPILE}gcc -E
260 c++ | ${CXX} | ${CROSS_COMPILE}g++
261 ar | ${AR} | ${CROSS_COMPILE}ar
262 nm | ${NM} | ${CROSS_COMPILE}nm
263 ldr | ${LDR} | ${CROSS_COMPILE}ldr
264 strip | ${STRIP} | ${CROSS_COMPILE}strip
265 objcopy | ${OBJCOPY} | ${CROSS_COMPILE}objcopy
266 objdump | ${OBJDUMP} | ${CROSS_COMPILE}objdump
267 dtc | ${DTC} | (no CROSS_COMPILE version)
268
269 Args:
270 name: Command name to run
271
272 Returns:
273 target_name: Exact command name to run instead
274 extra_args: List of extra arguments to pass
275 """
276 env = dict(os.environ)
277
278 target_name = None
279 extra_args = []
280 if name in ('as', 'ld', 'cc', 'cpp', 'ar', 'nm', 'ldr', 'strip',
281 'objcopy', 'objdump', 'dtc'):
282 target_name, *extra_args = env.get(name.upper(), '').split(' ')
283 elif name == 'c++':
284 target_name, *extra_args = env.get('CXX', '').split(' ')
285
286 if target_name:
287 return target_name, extra_args
288
289 if cross_compile is None:
290 cross_compile = env.get('CROSS_COMPILE', '')
291 if not cross_compile:
292 return name, []
293
294 if name in ('as', 'ar', 'nm', 'ldr', 'strip', 'objcopy', 'objdump'):
295 target_name = cross_compile + name
296 elif name == 'ld':
297 try:
298 if Run(cross_compile + 'ld.bfd', '-v'):
299 target_name = cross_compile + 'ld.bfd'
300 except:
301 target_name = cross_compile + 'ld'
302 elif name == 'cc':
303 target_name = cross_compile + 'gcc'
304 elif name == 'cpp':
305 target_name = cross_compile + 'gcc'
306 extra_args = ['-E']
307 elif name == 'c++':
308 target_name = cross_compile + 'g++'
309 else:
310 target_name = name
311 return target_name, extra_args
312
Simon Glass3b3e3c02019-10-31 07:42:50 -0600313def Run(name, *args, **kwargs):
Simon Glassc22b8cf2019-07-08 13:18:27 -0600314 """Run a tool with some arguments
315
316 This runs a 'tool', which is a program used by binman to process files and
317 perhaps produce some output. Tools can be located on the PATH or in a
318 search path.
319
320 Args:
321 name: Command name to run
322 args: Arguments to the tool
Alper Nebi Yasak29cc0912020-09-06 14:46:06 +0300323 for_host: True to resolve the command to the version for the host
Alper Nebi Yasak1e4687a2020-09-06 14:46:05 +0300324 for_target: False to run the command as-is, without resolving it
325 to the version for the compile target
Simon Glassc22b8cf2019-07-08 13:18:27 -0600326
327 Returns:
328 CommandResult object
329 """
Simon Glass04187a82018-09-14 04:57:25 -0600330 try:
Simon Glass3b3e3c02019-10-31 07:42:50 -0600331 binary = kwargs.get('binary')
Alper Nebi Yasak29cc0912020-09-06 14:46:06 +0300332 for_host = kwargs.get('for_host', False)
333 for_target = kwargs.get('for_target', not for_host)
Simon Glassc22b8cf2019-07-08 13:18:27 -0600334 env = None
335 if tool_search_paths:
336 env = dict(os.environ)
337 env['PATH'] = ':'.join(tool_search_paths) + ':' + env['PATH']
Alper Nebi Yasak1e4687a2020-09-06 14:46:05 +0300338 if for_target:
339 name, extra_args = GetTargetCompileTool(name)
340 args = tuple(extra_args) + args
Alper Nebi Yasak29cc0912020-09-06 14:46:06 +0300341 elif for_host:
342 name, extra_args = GetHostCompileTool(name)
343 args = tuple(extra_args) + args
Simon Glassf31e83d2020-11-09 07:45:02 -0700344 name = os.path.expanduser(name) # Expand paths containing ~
Simon Glass6eace392019-08-24 07:22:42 -0600345 all_args = (name,) + args
346 result = command.RunPipe([all_args], capture=True, capture_stderr=True,
Simon Glass3b3e3c02019-10-31 07:42:50 -0600347 env=env, raise_on_error=False, binary=binary)
Simon Glass6eace392019-08-24 07:22:42 -0600348 if result.return_code:
349 raise Exception("Error %d running '%s': %s" %
350 (result.return_code,' '.join(all_args),
351 result.stderr))
352 return result.stdout
Simon Glass04187a82018-09-14 04:57:25 -0600353 except:
Simon Glassc22b8cf2019-07-08 13:18:27 -0600354 if env and not PathHasFile(env['PATH'], name):
355 msg = "Please install tool '%s'" % name
Simon Glass04187a82018-09-14 04:57:25 -0600356 package = packages.get(name)
357 if package:
358 msg += " (e.g. from package '%s')" % package
359 raise ValueError(msg)
360 raise
Simon Glassaeffc5e2018-07-17 13:25:43 -0600361
362def Filename(fname):
363 """Resolve a file path to an absolute path.
364
365 If fname starts with ##/ and chroot is available, ##/ gets replaced with
366 the chroot path. If chroot is not available, this file name can not be
367 resolved, `None' is returned.
368
369 If fname is not prepended with the above prefix, and is not an existing
370 file, the actual file name is retrieved from the passed in string and the
371 search_paths directories (if any) are searched to for the file. If found -
372 the path to the found file is returned, `None' is returned otherwise.
373
374 Args:
375 fname: a string, the path to resolve.
376
377 Returns:
378 Absolute path to the file or None if not found.
379 """
380 if fname.startswith('##/'):
381 if chroot_path:
382 fname = os.path.join(chroot_path, fname[3:])
383 else:
384 return None
385
386 # Search for a pathname that exists, and return it if found
387 if fname and not os.path.exists(fname):
388 for path in search_paths:
389 pathname = os.path.join(path, os.path.basename(fname))
390 if os.path.exists(pathname):
391 return pathname
392
393 # If not found, just return the standard, unchanged path
394 return fname
395
Simon Glass3c47e412019-05-17 22:00:44 -0600396def ReadFile(fname, binary=True):
Simon Glassaeffc5e2018-07-17 13:25:43 -0600397 """Read and return the contents of a file.
398
399 Args:
400 fname: path to filename to read, where ## signifiies the chroot.
401
402 Returns:
403 data read from file, as a string.
404 """
Simon Glass3c47e412019-05-17 22:00:44 -0600405 with open(Filename(fname), binary and 'rb' or 'r') as fd:
Simon Glassaeffc5e2018-07-17 13:25:43 -0600406 data = fd.read()
407 #self._out.Info("Read file '%s' size %d (%#0x)" %
408 #(fname, len(data), len(data)))
409 return data
410
Simon Glassfd709862020-07-05 21:41:50 -0600411def WriteFile(fname, data, binary=True):
Simon Glassaeffc5e2018-07-17 13:25:43 -0600412 """Write data into a file.
413
414 Args:
415 fname: path to filename to write
416 data: data to write to file, as a string
417 """
418 #self._out.Info("Write file '%s' size %d (%#0x)" %
419 #(fname, len(data), len(data)))
Simon Glassfd709862020-07-05 21:41:50 -0600420 with open(Filename(fname), binary and 'wb' or 'w') as fd:
Simon Glassaeffc5e2018-07-17 13:25:43 -0600421 fd.write(data)
Simon Glasse6d85ff2019-05-14 15:53:47 -0600422
423def GetBytes(byte, size):
424 """Get a string of bytes of a given size
425
Simon Glasse6d85ff2019-05-14 15:53:47 -0600426 Args:
427 byte: Numeric byte value to use
428 size: Size of bytes/string to return
429
430 Returns:
431 A bytes type with 'byte' repeated 'size' times
432 """
Simon Glassfc0056e2020-11-08 20:36:18 -0700433 return bytes([byte]) * size
Simon Glass2b6ed5e2019-05-17 22:00:35 -0600434
Simon Glassf6b64812019-05-17 22:00:36 -0600435def ToBytes(string):
436 """Convert a str type into a bytes type
437
438 Args:
Simon Glass3b3e3c02019-10-31 07:42:50 -0600439 string: string to convert
Simon Glassf6b64812019-05-17 22:00:36 -0600440
441 Returns:
Simon Glassfc0056e2020-11-08 20:36:18 -0700442 A bytes type
Simon Glassf6b64812019-05-17 22:00:36 -0600443 """
Simon Glassfc0056e2020-11-08 20:36:18 -0700444 return string.encode('utf-8')
Simon Glass07d9e702019-07-08 13:18:41 -0600445
Simon Glass3b3e3c02019-10-31 07:42:50 -0600446def ToString(bval):
447 """Convert a bytes type into a str type
448
449 Args:
450 bval: bytes value to convert
451
452 Returns:
453 Python 3: A bytes type
454 Python 2: A string type
455 """
456 return bval.decode('utf-8')
457
Simon Glasseb0f4a42019-07-20 12:24:06 -0600458def Compress(indata, algo, with_header=True):
Simon Glass07d9e702019-07-08 13:18:41 -0600459 """Compress some data using a given algorithm
460
461 Note that for lzma this uses an old version of the algorithm, not that
462 provided by xz.
463
464 This requires 'lz4' and 'lzma_alone' tools. It also requires an output
465 directory to be previously set up, by calling PrepareOutputDir().
466
467 Args:
468 indata: Input data to compress
469 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
470
471 Returns:
472 Compressed data
473 """
474 if algo == 'none':
475 return indata
476 fname = GetOutputFilename('%s.comp.tmp' % algo)
477 WriteFile(fname, indata)
478 if algo == 'lz4':
Simon Glass6deff872021-01-06 21:35:11 -0700479 data = Run('lz4', '--no-frame-crc', '-B4', '-5', '-c', fname,
480 binary=True)
Simon Glass07d9e702019-07-08 13:18:41 -0600481 # cbfstool uses a very old version of lzma
482 elif algo == 'lzma':
483 outfname = GetOutputFilename('%s.comp.otmp' % algo)
484 Run('lzma_alone', 'e', fname, outfname, '-lc1', '-lp0', '-pb0', '-d8')
485 data = ReadFile(outfname)
486 elif algo == 'gzip':
Simon Glass3b3e3c02019-10-31 07:42:50 -0600487 data = Run('gzip', '-c', fname, binary=True)
Simon Glass07d9e702019-07-08 13:18:41 -0600488 else:
489 raise ValueError("Unknown algorithm '%s'" % algo)
Simon Glasseb0f4a42019-07-20 12:24:06 -0600490 if with_header:
491 hdr = struct.pack('<I', len(data))
492 data = hdr + data
Simon Glass07d9e702019-07-08 13:18:41 -0600493 return data
494
Simon Glasseb0f4a42019-07-20 12:24:06 -0600495def Decompress(indata, algo, with_header=True):
Simon Glass07d9e702019-07-08 13:18:41 -0600496 """Decompress some data using a given algorithm
497
498 Note that for lzma this uses an old version of the algorithm, not that
499 provided by xz.
500
501 This requires 'lz4' and 'lzma_alone' tools. It also requires an output
502 directory to be previously set up, by calling PrepareOutputDir().
503
504 Args:
505 indata: Input data to decompress
506 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
507
508 Returns:
509 Compressed data
510 """
511 if algo == 'none':
512 return indata
Simon Glasseb0f4a42019-07-20 12:24:06 -0600513 if with_header:
514 data_len = struct.unpack('<I', indata[:4])[0]
515 indata = indata[4:4 + data_len]
Simon Glass07d9e702019-07-08 13:18:41 -0600516 fname = GetOutputFilename('%s.decomp.tmp' % algo)
517 with open(fname, 'wb') as fd:
518 fd.write(indata)
519 if algo == 'lz4':
Simon Glass3b3e3c02019-10-31 07:42:50 -0600520 data = Run('lz4', '-dc', fname, binary=True)
Simon Glass07d9e702019-07-08 13:18:41 -0600521 elif algo == 'lzma':
522 outfname = GetOutputFilename('%s.decomp.otmp' % algo)
523 Run('lzma_alone', 'd', fname, outfname)
Simon Glass3b3e3c02019-10-31 07:42:50 -0600524 data = ReadFile(outfname, binary=True)
Simon Glass07d9e702019-07-08 13:18:41 -0600525 elif algo == 'gzip':
Simon Glass3b3e3c02019-10-31 07:42:50 -0600526 data = Run('gzip', '-cd', fname, binary=True)
Simon Glass07d9e702019-07-08 13:18:41 -0600527 else:
528 raise ValueError("Unknown algorithm '%s'" % algo)
529 return data
Simon Glass1cfdfc02019-07-08 13:18:51 -0600530
531CMD_CREATE, CMD_DELETE, CMD_ADD, CMD_REPLACE, CMD_EXTRACT = range(5)
532
533IFWITOOL_CMDS = {
534 CMD_CREATE: 'create',
535 CMD_DELETE: 'delete',
536 CMD_ADD: 'add',
537 CMD_REPLACE: 'replace',
538 CMD_EXTRACT: 'extract',
539 }
540
541def RunIfwiTool(ifwi_file, cmd, fname=None, subpart=None, entry_name=None):
542 """Run ifwitool with the given arguments:
543
544 Args:
545 ifwi_file: IFWI file to operation on
546 cmd: Command to execute (CMD_...)
547 fname: Filename of file to add/replace/extract/create (None for
548 CMD_DELETE)
549 subpart: Name of sub-partition to operation on (None for CMD_CREATE)
550 entry_name: Name of directory entry to operate on, or None if none
551 """
552 args = ['ifwitool', ifwi_file]
553 args.append(IFWITOOL_CMDS[cmd])
554 if fname:
555 args += ['-f', fname]
556 if subpart:
557 args += ['-n', subpart]
558 if entry_name:
559 args += ['-d', '-e', entry_name]
560 Run(*args)
Simon Glass9f297b02019-07-20 12:23:36 -0600561
562def ToHex(val):
563 """Convert an integer value (or None) to a string
564
565 Returns:
566 hex value, or 'None' if the value is None
567 """
568 return 'None' if val is None else '%#x' % val
569
570def ToHexSize(val):
571 """Return the size of an object in hex
572
573 Returns:
574 hex value of size, or 'None' if the value is None
575 """
576 return 'None' if val is None else '%#x' % len(val)