tools.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596
  1. # SPDX-License-Identifier: GPL-2.0+
  2. #
  3. # Copyright (c) 2016 Google, Inc
  4. #
  5. import glob
  6. import os
  7. import shlex
  8. import shutil
  9. import sys
  10. import tempfile
  11. import urllib.request
  12. from u_boot_pylib import command
  13. from u_boot_pylib import tout
  14. # Output directly (generally this is temporary)
  15. outdir = None
  16. # True to keep the output directory around after exiting
  17. preserve_outdir = False
  18. # Path to the Chrome OS chroot, if we know it
  19. chroot_path = None
  20. # Search paths to use for filename(), used to find files
  21. search_paths = []
  22. tool_search_paths = []
  23. # Tools and the packages that contain them, on debian
  24. packages = {
  25. 'lz4': 'liblz4-tool',
  26. }
  27. # List of paths to use when looking for an input file
  28. indir = []
  29. def prepare_output_dir(dirname, preserve=False):
  30. """Select an output directory, ensuring it exists.
  31. This either creates a temporary directory or checks that the one supplied
  32. by the user is valid. For a temporary directory, it makes a note to
  33. remove it later if required.
  34. Args:
  35. dirname: a string, name of the output directory to use to store
  36. intermediate and output files. If is None - create a temporary
  37. directory.
  38. preserve: a Boolean. If outdir above is None and preserve is False, the
  39. created temporary directory will be destroyed on exit.
  40. Raises:
  41. OSError: If it cannot create the output directory.
  42. """
  43. global outdir, preserve_outdir
  44. preserve_outdir = dirname or preserve
  45. if dirname:
  46. outdir = dirname
  47. if not os.path.isdir(outdir):
  48. try:
  49. os.makedirs(outdir)
  50. except OSError as err:
  51. raise ValueError(
  52. f"Cannot make output directory 'outdir': 'err.strerror'")
  53. tout.debug("Using output directory '%s'" % outdir)
  54. else:
  55. outdir = tempfile.mkdtemp(prefix='binman.')
  56. tout.debug("Using temporary directory '%s'" % outdir)
  57. def _remove_output_dir():
  58. global outdir
  59. shutil.rmtree(outdir)
  60. tout.debug("Deleted temporary directory '%s'" % outdir)
  61. outdir = None
  62. def finalise_output_dir():
  63. global outdir, preserve_outdir
  64. """Tidy up: delete output directory if temporary and not preserved."""
  65. if outdir and not preserve_outdir:
  66. _remove_output_dir()
  67. outdir = None
  68. def get_output_filename(fname):
  69. """Return a filename within the output directory.
  70. Args:
  71. fname: Filename to use for new file
  72. Returns:
  73. The full path of the filename, within the output directory
  74. """
  75. return os.path.join(outdir, fname)
  76. def get_output_dir():
  77. """Return the current output directory
  78. Returns:
  79. str: The output directory
  80. """
  81. return outdir
  82. def _finalise_for_test():
  83. """Remove the output directory (for use by tests)"""
  84. global outdir
  85. if outdir:
  86. _remove_output_dir()
  87. outdir = None
  88. def set_input_dirs(dirname):
  89. """Add a list of input directories, where input files are kept.
  90. Args:
  91. dirname: a list of paths to input directories to use for obtaining
  92. files needed by binman to place in the image.
  93. """
  94. global indir
  95. indir = dirname
  96. tout.debug("Using input directories %s" % indir)
  97. def get_input_filename(fname, allow_missing=False):
  98. """Return a filename for use as input.
  99. Args:
  100. fname: Filename to use for new file
  101. allow_missing: True if the filename can be missing
  102. Returns:
  103. fname, if indir is None;
  104. full path of the filename, within the input directory;
  105. None, if file is missing and allow_missing is True
  106. Raises:
  107. ValueError if file is missing and allow_missing is False
  108. """
  109. if not indir or fname[:1] == '/':
  110. return fname
  111. for dirname in indir:
  112. pathname = os.path.join(dirname, fname)
  113. if os.path.exists(pathname):
  114. return pathname
  115. if allow_missing:
  116. return None
  117. raise ValueError("Filename '%s' not found in input path (%s) (cwd='%s')" %
  118. (fname, ','.join(indir), os.getcwd()))
  119. def get_input_filename_glob(pattern):
  120. """Return a list of filenames for use as input.
  121. Args:
  122. pattern: Filename pattern to search for
  123. Returns:
  124. A list of matching files in all input directories
  125. """
  126. if not indir:
  127. return glob.glob(pattern)
  128. files = []
  129. for dirname in indir:
  130. pathname = os.path.join(dirname, pattern)
  131. files += glob.glob(pathname)
  132. return sorted(files)
  133. def align(pos, align):
  134. if align:
  135. mask = align - 1
  136. pos = (pos + mask) & ~mask
  137. return pos
  138. def not_power_of_two(num):
  139. return num and (num & (num - 1))
  140. def set_tool_paths(toolpaths):
  141. """Set the path to search for tools
  142. Args:
  143. toolpaths: List of paths to search for tools executed by run()
  144. """
  145. global tool_search_paths
  146. tool_search_paths = toolpaths
  147. def path_has_file(path_spec, fname):
  148. """Check if a given filename is in the PATH
  149. Args:
  150. path_spec: Value of PATH variable to check
  151. fname: Filename to check
  152. Returns:
  153. True if found, False if not
  154. """
  155. for dir in path_spec.split(':'):
  156. if os.path.exists(os.path.join(dir, fname)):
  157. return True
  158. return False
  159. def get_host_compile_tool(env, name):
  160. """Get the host-specific version for a compile tool
  161. This checks the environment variables that specify which version of
  162. the tool should be used (e.g. ${HOSTCC}).
  163. The following table lists the host-specific versions of the tools
  164. this function resolves to:
  165. Compile Tool | Host version
  166. --------------+----------------
  167. as | ${HOSTAS}
  168. ld | ${HOSTLD}
  169. cc | ${HOSTCC}
  170. cpp | ${HOSTCPP}
  171. c++ | ${HOSTCXX}
  172. ar | ${HOSTAR}
  173. nm | ${HOSTNM}
  174. ldr | ${HOSTLDR}
  175. strip | ${HOSTSTRIP}
  176. objcopy | ${HOSTOBJCOPY}
  177. objdump | ${HOSTOBJDUMP}
  178. dtc | ${HOSTDTC}
  179. Args:
  180. name: Command name to run
  181. Returns:
  182. host_name: Exact command name to run instead
  183. extra_args: List of extra arguments to pass
  184. """
  185. host_name = None
  186. extra_args = []
  187. if name in ('as', 'ld', 'cc', 'cpp', 'ar', 'nm', 'ldr', 'strip',
  188. 'objcopy', 'objdump', 'dtc'):
  189. host_name, *host_args = env.get('HOST' + name.upper(), '').split(' ')
  190. elif name == 'c++':
  191. host_name, *host_args = env.get('HOSTCXX', '').split(' ')
  192. if host_name:
  193. return host_name, extra_args
  194. return name, []
  195. def get_target_compile_tool(name, cross_compile=None):
  196. """Get the target-specific version for a compile tool
  197. This first checks the environment variables that specify which
  198. version of the tool should be used (e.g. ${CC}). If those aren't
  199. specified, it checks the CROSS_COMPILE variable as a prefix for the
  200. tool with some substitutions (e.g. "${CROSS_COMPILE}gcc" for cc).
  201. The following table lists the target-specific versions of the tools
  202. this function resolves to:
  203. Compile Tool | First choice | Second choice
  204. --------------+----------------+----------------------------
  205. as | ${AS} | ${CROSS_COMPILE}as
  206. ld | ${LD} | ${CROSS_COMPILE}ld.bfd
  207. | | or ${CROSS_COMPILE}ld
  208. cc | ${CC} | ${CROSS_COMPILE}gcc
  209. cpp | ${CPP} | ${CROSS_COMPILE}gcc -E
  210. c++ | ${CXX} | ${CROSS_COMPILE}g++
  211. ar | ${AR} | ${CROSS_COMPILE}ar
  212. nm | ${NM} | ${CROSS_COMPILE}nm
  213. ldr | ${LDR} | ${CROSS_COMPILE}ldr
  214. strip | ${STRIP} | ${CROSS_COMPILE}strip
  215. objcopy | ${OBJCOPY} | ${CROSS_COMPILE}objcopy
  216. objdump | ${OBJDUMP} | ${CROSS_COMPILE}objdump
  217. dtc | ${DTC} | (no CROSS_COMPILE version)
  218. Args:
  219. name: Command name to run
  220. Returns:
  221. target_name: Exact command name to run instead
  222. extra_args: List of extra arguments to pass
  223. """
  224. env = dict(os.environ)
  225. target_name = None
  226. extra_args = []
  227. if name in ('as', 'ld', 'cc', 'cpp', 'ar', 'nm', 'ldr', 'strip',
  228. 'objcopy', 'objdump', 'dtc'):
  229. target_name, *extra_args = env.get(name.upper(), '').split(' ')
  230. elif name == 'c++':
  231. target_name, *extra_args = env.get('CXX', '').split(' ')
  232. if target_name:
  233. return target_name, extra_args
  234. if cross_compile is None:
  235. cross_compile = env.get('CROSS_COMPILE', '')
  236. if name in ('as', 'ar', 'nm', 'ldr', 'strip', 'objcopy', 'objdump'):
  237. target_name = cross_compile + name
  238. elif name == 'ld':
  239. try:
  240. if run(cross_compile + 'ld.bfd', '-v'):
  241. target_name = cross_compile + 'ld.bfd'
  242. except:
  243. target_name = cross_compile + 'ld'
  244. elif name == 'cc':
  245. target_name = cross_compile + 'gcc'
  246. elif name == 'cpp':
  247. target_name = cross_compile + 'gcc'
  248. extra_args = ['-E']
  249. elif name == 'c++':
  250. target_name = cross_compile + 'g++'
  251. else:
  252. target_name = name
  253. return target_name, extra_args
  254. def get_env_with_path():
  255. """Get an updated environment with the PATH variable set correctly
  256. If there are any search paths set, these need to come first in the PATH so
  257. that these override any other version of the tools.
  258. Returns:
  259. dict: New environment with PATH updated, or None if there are not search
  260. paths
  261. """
  262. if tool_search_paths:
  263. env = dict(os.environ)
  264. env['PATH'] = ':'.join(tool_search_paths) + ':' + env['PATH']
  265. return env
  266. def run_result(name, *args, **kwargs):
  267. """Run a tool with some arguments
  268. This runs a 'tool', which is a program used by binman to process files and
  269. perhaps produce some output. Tools can be located on the PATH or in a
  270. search path.
  271. Args:
  272. name: Command name to run
  273. args: Arguments to the tool
  274. for_host: True to resolve the command to the version for the host
  275. for_target: False to run the command as-is, without resolving it
  276. to the version for the compile target
  277. raise_on_error: Raise an error if the command fails (True by default)
  278. Returns:
  279. CommandResult object
  280. """
  281. try:
  282. binary = kwargs.get('binary')
  283. for_host = kwargs.get('for_host', False)
  284. for_target = kwargs.get('for_target', not for_host)
  285. raise_on_error = kwargs.get('raise_on_error', True)
  286. env = get_env_with_path()
  287. if for_target:
  288. name, extra_args = get_target_compile_tool(name)
  289. args = tuple(extra_args) + args
  290. elif for_host:
  291. name, extra_args = get_host_compile_tool(env, name)
  292. args = tuple(extra_args) + args
  293. name = os.path.expanduser(name) # Expand paths containing ~
  294. all_args = (name,) + args
  295. result = command.run_pipe([all_args], capture=True, capture_stderr=True,
  296. env=env, raise_on_error=False, binary=binary)
  297. if result.return_code:
  298. if raise_on_error:
  299. raise ValueError("Error %d running '%s': %s" %
  300. (result.return_code,' '.join(all_args),
  301. result.stderr or result.stdout))
  302. return result
  303. except ValueError:
  304. if env and not path_has_file(env['PATH'], name):
  305. msg = "Please install tool '%s'" % name
  306. package = packages.get(name)
  307. if package:
  308. msg += " (e.g. from package '%s')" % package
  309. raise ValueError(msg)
  310. raise
  311. def tool_find(name):
  312. """Search the current path for a tool
  313. This uses both PATH and any value from set_tool_paths() to search for a tool
  314. Args:
  315. name (str): Name of tool to locate
  316. Returns:
  317. str: Full path to tool if found, else None
  318. """
  319. name = os.path.expanduser(name) # Expand paths containing ~
  320. paths = []
  321. pathvar = os.environ.get('PATH')
  322. if pathvar:
  323. paths = pathvar.split(':')
  324. if tool_search_paths:
  325. paths += tool_search_paths
  326. for path in paths:
  327. fname = os.path.join(path, name)
  328. if os.path.isfile(fname) and os.access(fname, os.X_OK):
  329. return fname
  330. def run(name, *args, **kwargs):
  331. """Run a tool with some arguments
  332. This runs a 'tool', which is a program used by binman to process files and
  333. perhaps produce some output. Tools can be located on the PATH or in a
  334. search path.
  335. Args:
  336. name: Command name to run
  337. args: Arguments to the tool
  338. for_host: True to resolve the command to the version for the host
  339. for_target: False to run the command as-is, without resolving it
  340. to the version for the compile target
  341. Returns:
  342. CommandResult object
  343. """
  344. result = run_result(name, *args, **kwargs)
  345. if result is not None:
  346. return result.stdout
  347. def filename(fname):
  348. """Resolve a file path to an absolute path.
  349. If fname starts with ##/ and chroot is available, ##/ gets replaced with
  350. the chroot path. If chroot is not available, this file name can not be
  351. resolved, `None' is returned.
  352. If fname is not prepended with the above prefix, and is not an existing
  353. file, the actual file name is retrieved from the passed in string and the
  354. search_paths directories (if any) are searched to for the file. If found -
  355. the path to the found file is returned, `None' is returned otherwise.
  356. Args:
  357. fname: a string, the path to resolve.
  358. Returns:
  359. Absolute path to the file or None if not found.
  360. """
  361. if fname.startswith('##/'):
  362. if chroot_path:
  363. fname = os.path.join(chroot_path, fname[3:])
  364. else:
  365. return None
  366. # Search for a pathname that exists, and return it if found
  367. if fname and not os.path.exists(fname):
  368. for path in search_paths:
  369. pathname = os.path.join(path, os.path.basename(fname))
  370. if os.path.exists(pathname):
  371. return pathname
  372. # If not found, just return the standard, unchanged path
  373. return fname
  374. def read_file(fname, binary=True):
  375. """Read and return the contents of a file.
  376. Args:
  377. fname: path to filename to read, where ## signifiies the chroot.
  378. Returns:
  379. data read from file, as a string.
  380. """
  381. with open(filename(fname), binary and 'rb' or 'r') as fd:
  382. data = fd.read()
  383. #self._out.Info("Read file '%s' size %d (%#0x)" %
  384. #(fname, len(data), len(data)))
  385. return data
  386. def write_file(fname, data, binary=True):
  387. """Write data into a file.
  388. Args:
  389. fname: path to filename to write
  390. data: data to write to file, as a string
  391. """
  392. #self._out.Info("Write file '%s' size %d (%#0x)" %
  393. #(fname, len(data), len(data)))
  394. with open(filename(fname), binary and 'wb' or 'w') as fd:
  395. fd.write(data)
  396. def get_bytes(byte, size):
  397. """Get a string of bytes of a given size
  398. Args:
  399. byte: Numeric byte value to use
  400. size: Size of bytes/string to return
  401. Returns:
  402. A bytes type with 'byte' repeated 'size' times
  403. """
  404. return bytes([byte]) * size
  405. def to_bytes(string):
  406. """Convert a str type into a bytes type
  407. Args:
  408. string: string to convert
  409. Returns:
  410. A bytes type
  411. """
  412. return string.encode('utf-8')
  413. def to_string(bval):
  414. """Convert a bytes type into a str type
  415. Args:
  416. bval: bytes value to convert
  417. Returns:
  418. Python 3: A bytes type
  419. Python 2: A string type
  420. """
  421. return bval.decode('utf-8')
  422. def to_hex(val):
  423. """Convert an integer value (or None) to a string
  424. Returns:
  425. hex value, or 'None' if the value is None
  426. """
  427. return 'None' if val is None else '%#x' % val
  428. def to_hex_size(val):
  429. """Return the size of an object in hex
  430. Returns:
  431. hex value of size, or 'None' if the value is None
  432. """
  433. return 'None' if val is None else '%#x' % len(val)
  434. def print_full_help(fname):
  435. """Print the full help message for a tool using an appropriate pager.
  436. Args:
  437. fname: Path to a file containing the full help message
  438. """
  439. pager = shlex.split(os.getenv('PAGER', ''))
  440. if not pager:
  441. lesspath = shutil.which('less')
  442. pager = [lesspath] if lesspath else None
  443. if not pager:
  444. pager = ['more']
  445. command.run(*pager, fname)
  446. def download(url, tmpdir_pattern='.patman'):
  447. """Download a file to a temporary directory
  448. Args:
  449. url (str): URL to download
  450. tmpdir_pattern (str): pattern to use for the temporary directory
  451. Returns:
  452. Tuple:
  453. Full path to the downloaded archive file in that directory,
  454. or None if there was an error while downloading
  455. Temporary directory name
  456. """
  457. print('- downloading: %s' % url)
  458. leaf = url.split('/')[-1]
  459. tmpdir = tempfile.mkdtemp(tmpdir_pattern)
  460. response = urllib.request.urlopen(url)
  461. fname = os.path.join(tmpdir, leaf)
  462. fd = open(fname, 'wb')
  463. meta = response.info()
  464. size = int(meta.get('Content-Length'))
  465. done = 0
  466. block_size = 1 << 16
  467. status = ''
  468. # Read the file in chunks and show progress as we go
  469. while True:
  470. buffer = response.read(block_size)
  471. if not buffer:
  472. print(chr(8) * (len(status) + 1), '\r', end=' ')
  473. break
  474. done += len(buffer)
  475. fd.write(buffer)
  476. status = r'%10d MiB [%3d%%]' % (done // 1024 // 1024,
  477. done * 100 // size)
  478. status = status + chr(8) * (len(status) + 1)
  479. print(status, end=' ')
  480. sys.stdout.flush()
  481. print('\r', end='')
  482. sys.stdout.flush()
  483. fd.close()
  484. if done != size:
  485. print('Error, failed to download')
  486. os.remove(fname)
  487. fname = None
  488. return fname, tmpdir