blob: d8e01a3e60ec4252a5fe36ce76774c70c7882e6f [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 Glass3b3e3c02019-10-31 07:42:50 -0600479 data = Run('lz4', '--no-frame-crc', '-c', fname, binary=True)
Simon Glass07d9e702019-07-08 13:18:41 -0600480 # cbfstool uses a very old version of lzma
481 elif algo == 'lzma':
482 outfname = GetOutputFilename('%s.comp.otmp' % algo)
483 Run('lzma_alone', 'e', fname, outfname, '-lc1', '-lp0', '-pb0', '-d8')
484 data = ReadFile(outfname)
485 elif algo == 'gzip':
Simon Glass3b3e3c02019-10-31 07:42:50 -0600486 data = Run('gzip', '-c', fname, binary=True)
Simon Glass07d9e702019-07-08 13:18:41 -0600487 else:
488 raise ValueError("Unknown algorithm '%s'" % algo)
Simon Glasseb0f4a42019-07-20 12:24:06 -0600489 if with_header:
490 hdr = struct.pack('<I', len(data))
491 data = hdr + data
Simon Glass07d9e702019-07-08 13:18:41 -0600492 return data
493
Simon Glasseb0f4a42019-07-20 12:24:06 -0600494def Decompress(indata, algo, with_header=True):
Simon Glass07d9e702019-07-08 13:18:41 -0600495 """Decompress some data using a given algorithm
496
497 Note that for lzma this uses an old version of the algorithm, not that
498 provided by xz.
499
500 This requires 'lz4' and 'lzma_alone' tools. It also requires an output
501 directory to be previously set up, by calling PrepareOutputDir().
502
503 Args:
504 indata: Input data to decompress
505 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
506
507 Returns:
508 Compressed data
509 """
510 if algo == 'none':
511 return indata
Simon Glasseb0f4a42019-07-20 12:24:06 -0600512 if with_header:
513 data_len = struct.unpack('<I', indata[:4])[0]
514 indata = indata[4:4 + data_len]
Simon Glass07d9e702019-07-08 13:18:41 -0600515 fname = GetOutputFilename('%s.decomp.tmp' % algo)
516 with open(fname, 'wb') as fd:
517 fd.write(indata)
518 if algo == 'lz4':
Simon Glass3b3e3c02019-10-31 07:42:50 -0600519 data = Run('lz4', '-dc', fname, binary=True)
Simon Glass07d9e702019-07-08 13:18:41 -0600520 elif algo == 'lzma':
521 outfname = GetOutputFilename('%s.decomp.otmp' % algo)
522 Run('lzma_alone', 'd', fname, outfname)
Simon Glass3b3e3c02019-10-31 07:42:50 -0600523 data = ReadFile(outfname, binary=True)
Simon Glass07d9e702019-07-08 13:18:41 -0600524 elif algo == 'gzip':
Simon Glass3b3e3c02019-10-31 07:42:50 -0600525 data = Run('gzip', '-cd', fname, binary=True)
Simon Glass07d9e702019-07-08 13:18:41 -0600526 else:
527 raise ValueError("Unknown algorithm '%s'" % algo)
528 return data
Simon Glass1cfdfc02019-07-08 13:18:51 -0600529
530CMD_CREATE, CMD_DELETE, CMD_ADD, CMD_REPLACE, CMD_EXTRACT = range(5)
531
532IFWITOOL_CMDS = {
533 CMD_CREATE: 'create',
534 CMD_DELETE: 'delete',
535 CMD_ADD: 'add',
536 CMD_REPLACE: 'replace',
537 CMD_EXTRACT: 'extract',
538 }
539
540def RunIfwiTool(ifwi_file, cmd, fname=None, subpart=None, entry_name=None):
541 """Run ifwitool with the given arguments:
542
543 Args:
544 ifwi_file: IFWI file to operation on
545 cmd: Command to execute (CMD_...)
546 fname: Filename of file to add/replace/extract/create (None for
547 CMD_DELETE)
548 subpart: Name of sub-partition to operation on (None for CMD_CREATE)
549 entry_name: Name of directory entry to operate on, or None if none
550 """
551 args = ['ifwitool', ifwi_file]
552 args.append(IFWITOOL_CMDS[cmd])
553 if fname:
554 args += ['-f', fname]
555 if subpart:
556 args += ['-n', subpart]
557 if entry_name:
558 args += ['-d', '-e', entry_name]
559 Run(*args)
Simon Glass9f297b02019-07-20 12:23:36 -0600560
561def ToHex(val):
562 """Convert an integer value (or None) to a string
563
564 Returns:
565 hex value, or 'None' if the value is None
566 """
567 return 'None' if val is None else '%#x' % val
568
569def ToHexSize(val):
570 """Return the size of an object in hex
571
572 Returns:
573 hex value of size, or 'None' if the value is None
574 """
575 return 'None' if val is None else '%#x' % len(val)