compilation.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648
  1. import glob
  2. import os
  3. import shutil
  4. import subprocess
  5. import sys
  6. import tempfile
  7. import warnings
  8. from sysconfig import get_config_var, get_config_vars, get_path
  9. from .runners import (
  10. CCompilerRunner,
  11. CppCompilerRunner,
  12. FortranCompilerRunner
  13. )
  14. from .util import (
  15. get_abspath, make_dirs, copy, Glob, ArbitraryDepthGlob,
  16. glob_at_depth, import_module_from_file, pyx_is_cplus,
  17. sha256_of_string, sha256_of_file, CompileError
  18. )
  19. if os.name == 'posix':
  20. objext = '.o'
  21. elif os.name == 'nt':
  22. objext = '.obj'
  23. else:
  24. warnings.warn("Unknown os.name: {}".format(os.name))
  25. objext = '.o'
  26. def compile_sources(files, Runner=None, destdir=None, cwd=None, keep_dir_struct=False,
  27. per_file_kwargs=None, **kwargs):
  28. """ Compile source code files to object files.
  29. Parameters
  30. ==========
  31. files : iterable of str
  32. Paths to source files, if ``cwd`` is given, the paths are taken as relative.
  33. Runner: CompilerRunner subclass (optional)
  34. Could be e.g. ``FortranCompilerRunner``. Will be inferred from filename
  35. extensions if missing.
  36. destdir: str
  37. Output directory, if cwd is given, the path is taken as relative.
  38. cwd: str
  39. Working directory. Specify to have compiler run in other directory.
  40. also used as root of relative paths.
  41. keep_dir_struct: bool
  42. Reproduce directory structure in `destdir`. default: ``False``
  43. per_file_kwargs: dict
  44. Dict mapping instances in ``files`` to keyword arguments.
  45. \\*\\*kwargs: dict
  46. Default keyword arguments to pass to ``Runner``.
  47. """
  48. _per_file_kwargs = {}
  49. if per_file_kwargs is not None:
  50. for k, v in per_file_kwargs.items():
  51. if isinstance(k, Glob):
  52. for path in glob.glob(k.pathname):
  53. _per_file_kwargs[path] = v
  54. elif isinstance(k, ArbitraryDepthGlob):
  55. for path in glob_at_depth(k.filename, cwd):
  56. _per_file_kwargs[path] = v
  57. else:
  58. _per_file_kwargs[k] = v
  59. # Set up destination directory
  60. destdir = destdir or '.'
  61. if not os.path.isdir(destdir):
  62. if os.path.exists(destdir):
  63. raise OSError("{} is not a directory".format(destdir))
  64. else:
  65. make_dirs(destdir)
  66. if cwd is None:
  67. cwd = '.'
  68. for f in files:
  69. copy(f, destdir, only_update=True, dest_is_dir=True)
  70. # Compile files and return list of paths to the objects
  71. dstpaths = []
  72. for f in files:
  73. if keep_dir_struct:
  74. name, ext = os.path.splitext(f)
  75. else:
  76. name, ext = os.path.splitext(os.path.basename(f))
  77. file_kwargs = kwargs.copy()
  78. file_kwargs.update(_per_file_kwargs.get(f, {}))
  79. dstpaths.append(src2obj(f, Runner, cwd=cwd, **file_kwargs))
  80. return dstpaths
  81. def get_mixed_fort_c_linker(vendor=None, cplus=False, cwd=None):
  82. vendor = vendor or os.environ.get('SYMPY_COMPILER_VENDOR', 'gnu')
  83. if vendor.lower() == 'intel':
  84. if cplus:
  85. return (FortranCompilerRunner,
  86. {'flags': ['-nofor_main', '-cxxlib']}, vendor)
  87. else:
  88. return (FortranCompilerRunner,
  89. {'flags': ['-nofor_main']}, vendor)
  90. elif vendor.lower() == 'gnu' or 'llvm':
  91. if cplus:
  92. return (CppCompilerRunner,
  93. {'lib_options': ['fortran']}, vendor)
  94. else:
  95. return (FortranCompilerRunner,
  96. {}, vendor)
  97. else:
  98. raise ValueError("No vendor found.")
  99. def link(obj_files, out_file=None, shared=False, Runner=None,
  100. cwd=None, cplus=False, fort=False, **kwargs):
  101. """ Link object files.
  102. Parameters
  103. ==========
  104. obj_files: iterable of str
  105. Paths to object files.
  106. out_file: str (optional)
  107. Path to executable/shared library, if ``None`` it will be
  108. deduced from the last item in obj_files.
  109. shared: bool
  110. Generate a shared library?
  111. Runner: CompilerRunner subclass (optional)
  112. If not given the ``cplus`` and ``fort`` flags will be inspected
  113. (fallback is the C compiler).
  114. cwd: str
  115. Path to the root of relative paths and working directory for compiler.
  116. cplus: bool
  117. C++ objects? default: ``False``.
  118. fort: bool
  119. Fortran objects? default: ``False``.
  120. \\*\\*kwargs: dict
  121. Keyword arguments passed to ``Runner``.
  122. Returns
  123. =======
  124. The absolute path to the generated shared object / executable.
  125. """
  126. if out_file is None:
  127. out_file, ext = os.path.splitext(os.path.basename(obj_files[-1]))
  128. if shared:
  129. out_file += get_config_var('EXT_SUFFIX')
  130. if not Runner:
  131. if fort:
  132. Runner, extra_kwargs, vendor = \
  133. get_mixed_fort_c_linker(
  134. vendor=kwargs.get('vendor', None),
  135. cplus=cplus,
  136. cwd=cwd,
  137. )
  138. for k, v in extra_kwargs.items():
  139. if k in kwargs:
  140. kwargs[k].expand(v)
  141. else:
  142. kwargs[k] = v
  143. else:
  144. if cplus:
  145. Runner = CppCompilerRunner
  146. else:
  147. Runner = CCompilerRunner
  148. flags = kwargs.pop('flags', [])
  149. if shared:
  150. if '-shared' not in flags:
  151. flags.append('-shared')
  152. run_linker = kwargs.pop('run_linker', True)
  153. if not run_linker:
  154. raise ValueError("run_linker was set to False (nonsensical).")
  155. out_file = get_abspath(out_file, cwd=cwd)
  156. runner = Runner(obj_files, out_file, flags, cwd=cwd, **kwargs)
  157. runner.run()
  158. return out_file
  159. def link_py_so(obj_files, so_file=None, cwd=None, libraries=None,
  160. cplus=False, fort=False, **kwargs):
  161. """ Link Python extension module (shared object) for importing
  162. Parameters
  163. ==========
  164. obj_files: iterable of str
  165. Paths to object files to be linked.
  166. so_file: str
  167. Name (path) of shared object file to create. If not specified it will
  168. have the basname of the last object file in `obj_files` but with the
  169. extension '.so' (Unix).
  170. cwd: path string
  171. Root of relative paths and working directory of linker.
  172. libraries: iterable of strings
  173. Libraries to link against, e.g. ['m'].
  174. cplus: bool
  175. Any C++ objects? default: ``False``.
  176. fort: bool
  177. Any Fortran objects? default: ``False``.
  178. kwargs**: dict
  179. Keyword arguments passed to ``link(...)``.
  180. Returns
  181. =======
  182. Absolute path to the generate shared object.
  183. """
  184. libraries = libraries or []
  185. include_dirs = kwargs.pop('include_dirs', [])
  186. library_dirs = kwargs.pop('library_dirs', [])
  187. # Add Python include and library directories
  188. # PY_LDFLAGS does not available on all python implementations
  189. # e.g. when with pypy, so it's LDFLAGS we need to use
  190. if sys.platform == "win32":
  191. warnings.warn("Windows not yet supported.")
  192. elif sys.platform == 'darwin':
  193. cfgDict = get_config_vars()
  194. kwargs['linkline'] = kwargs.get('linkline', []) + [cfgDict['LDFLAGS']]
  195. library_dirs += [cfgDict['LIBDIR']]
  196. # In macOS, linker needs to compile frameworks
  197. # e.g. "-framework CoreFoundation"
  198. is_framework = False
  199. for opt in cfgDict['LIBS'].split():
  200. if is_framework:
  201. kwargs['linkline'] = kwargs.get('linkline', []) + ['-framework', opt]
  202. is_framework = False
  203. elif opt.startswith('-l'):
  204. libraries.append(opt[2:])
  205. elif opt.startswith('-framework'):
  206. is_framework = True
  207. # The python library is not included in LIBS
  208. libfile = cfgDict['LIBRARY']
  209. libname = ".".join(libfile.split('.')[:-1])[3:]
  210. libraries.append(libname)
  211. elif sys.platform[:3] == 'aix':
  212. # Don't use the default code below
  213. pass
  214. else:
  215. if get_config_var('Py_ENABLE_SHARED'):
  216. cfgDict = get_config_vars()
  217. kwargs['linkline'] = kwargs.get('linkline', []) + [cfgDict['LDFLAGS']]
  218. library_dirs += [cfgDict['LIBDIR']]
  219. for opt in cfgDict['BLDLIBRARY'].split():
  220. if opt.startswith('-l'):
  221. libraries += [opt[2:]]
  222. else:
  223. pass
  224. flags = kwargs.pop('flags', [])
  225. needed_flags = ('-pthread',)
  226. for flag in needed_flags:
  227. if flag not in flags:
  228. flags.append(flag)
  229. return link(obj_files, shared=True, flags=flags, cwd=cwd,
  230. cplus=cplus, fort=fort, include_dirs=include_dirs,
  231. libraries=libraries, library_dirs=library_dirs, **kwargs)
  232. def simple_cythonize(src, destdir=None, cwd=None, **cy_kwargs):
  233. """ Generates a C file from a Cython source file.
  234. Parameters
  235. ==========
  236. src: str
  237. Path to Cython source.
  238. destdir: str (optional)
  239. Path to output directory (default: '.').
  240. cwd: path string (optional)
  241. Root of relative paths (default: '.').
  242. **cy_kwargs:
  243. Second argument passed to cy_compile. Generates a .cpp file if ``cplus=True`` in ``cy_kwargs``,
  244. else a .c file.
  245. """
  246. from Cython.Compiler.Main import (
  247. default_options, CompilationOptions
  248. )
  249. from Cython.Compiler.Main import compile as cy_compile
  250. assert src.lower().endswith('.pyx') or src.lower().endswith('.py')
  251. cwd = cwd or '.'
  252. destdir = destdir or '.'
  253. ext = '.cpp' if cy_kwargs.get('cplus', False) else '.c'
  254. c_name = os.path.splitext(os.path.basename(src))[0] + ext
  255. dstfile = os.path.join(destdir, c_name)
  256. if cwd:
  257. ori_dir = os.getcwd()
  258. else:
  259. ori_dir = '.'
  260. os.chdir(cwd)
  261. try:
  262. cy_options = CompilationOptions(default_options)
  263. cy_options.__dict__.update(cy_kwargs)
  264. # Set language_level if not set by cy_kwargs
  265. # as not setting it is deprecated
  266. if 'language_level' not in cy_kwargs:
  267. cy_options.__dict__['language_level'] = 3
  268. cy_result = cy_compile([src], cy_options)
  269. if cy_result.num_errors > 0:
  270. raise ValueError("Cython compilation failed.")
  271. # Move generated C file to destination
  272. # In macOS, the generated C file is in the same directory as the source
  273. # but the /var is a symlink to /private/var, so we need to use realpath
  274. if os.path.realpath(os.path.dirname(src)) != os.path.realpath(destdir):
  275. if os.path.exists(dstfile):
  276. os.unlink(dstfile)
  277. shutil.move(os.path.join(os.path.dirname(src), c_name), destdir)
  278. finally:
  279. os.chdir(ori_dir)
  280. return dstfile
  281. extension_mapping = {
  282. '.c': (CCompilerRunner, None),
  283. '.cpp': (CppCompilerRunner, None),
  284. '.cxx': (CppCompilerRunner, None),
  285. '.f': (FortranCompilerRunner, None),
  286. '.for': (FortranCompilerRunner, None),
  287. '.ftn': (FortranCompilerRunner, None),
  288. '.f90': (FortranCompilerRunner, None), # ifort only knows about .f90
  289. '.f95': (FortranCompilerRunner, 'f95'),
  290. '.f03': (FortranCompilerRunner, 'f2003'),
  291. '.f08': (FortranCompilerRunner, 'f2008'),
  292. }
  293. def src2obj(srcpath, Runner=None, objpath=None, cwd=None, inc_py=False, **kwargs):
  294. """ Compiles a source code file to an object file.
  295. Files ending with '.pyx' assumed to be cython files and
  296. are dispatched to pyx2obj.
  297. Parameters
  298. ==========
  299. srcpath: str
  300. Path to source file.
  301. Runner: CompilerRunner subclass (optional)
  302. If ``None``: deduced from extension of srcpath.
  303. objpath : str (optional)
  304. Path to generated object. If ``None``: deduced from ``srcpath``.
  305. cwd: str (optional)
  306. Working directory and root of relative paths. If ``None``: current dir.
  307. inc_py: bool
  308. Add Python include path to kwarg "include_dirs". Default: False
  309. \\*\\*kwargs: dict
  310. keyword arguments passed to Runner or pyx2obj
  311. """
  312. name, ext = os.path.splitext(os.path.basename(srcpath))
  313. if objpath is None:
  314. if os.path.isabs(srcpath):
  315. objpath = '.'
  316. else:
  317. objpath = os.path.dirname(srcpath)
  318. objpath = objpath or '.' # avoid objpath == ''
  319. if os.path.isdir(objpath):
  320. objpath = os.path.join(objpath, name + objext)
  321. include_dirs = kwargs.pop('include_dirs', [])
  322. if inc_py:
  323. py_inc_dir = get_path('include')
  324. if py_inc_dir not in include_dirs:
  325. include_dirs.append(py_inc_dir)
  326. if ext.lower() == '.pyx':
  327. return pyx2obj(srcpath, objpath=objpath, include_dirs=include_dirs, cwd=cwd,
  328. **kwargs)
  329. if Runner is None:
  330. Runner, std = extension_mapping[ext.lower()]
  331. if 'std' not in kwargs:
  332. kwargs['std'] = std
  333. flags = kwargs.pop('flags', [])
  334. needed_flags = ('-fPIC',)
  335. for flag in needed_flags:
  336. if flag not in flags:
  337. flags.append(flag)
  338. # src2obj implies not running the linker...
  339. run_linker = kwargs.pop('run_linker', False)
  340. if run_linker:
  341. raise CompileError("src2obj called with run_linker=True")
  342. runner = Runner([srcpath], objpath, include_dirs=include_dirs,
  343. run_linker=run_linker, cwd=cwd, flags=flags, **kwargs)
  344. runner.run()
  345. return objpath
  346. def pyx2obj(pyxpath, objpath=None, destdir=None, cwd=None,
  347. include_dirs=None, cy_kwargs=None, cplus=None, **kwargs):
  348. """
  349. Convenience function
  350. If cwd is specified, pyxpath and dst are taken to be relative
  351. If only_update is set to `True` the modification time is checked
  352. and compilation is only run if the source is newer than the
  353. destination
  354. Parameters
  355. ==========
  356. pyxpath: str
  357. Path to Cython source file.
  358. objpath: str (optional)
  359. Path to object file to generate.
  360. destdir: str (optional)
  361. Directory to put generated C file. When ``None``: directory of ``objpath``.
  362. cwd: str (optional)
  363. Working directory and root of relative paths.
  364. include_dirs: iterable of path strings (optional)
  365. Passed onto src2obj and via cy_kwargs['include_path']
  366. to simple_cythonize.
  367. cy_kwargs: dict (optional)
  368. Keyword arguments passed onto `simple_cythonize`
  369. cplus: bool (optional)
  370. Indicate whether C++ is used. default: auto-detect using ``.util.pyx_is_cplus``.
  371. compile_kwargs: dict
  372. keyword arguments passed onto src2obj
  373. Returns
  374. =======
  375. Absolute path of generated object file.
  376. """
  377. assert pyxpath.endswith('.pyx')
  378. cwd = cwd or '.'
  379. objpath = objpath or '.'
  380. destdir = destdir or os.path.dirname(objpath)
  381. abs_objpath = get_abspath(objpath, cwd=cwd)
  382. if os.path.isdir(abs_objpath):
  383. pyx_fname = os.path.basename(pyxpath)
  384. name, ext = os.path.splitext(pyx_fname)
  385. objpath = os.path.join(objpath, name + objext)
  386. cy_kwargs = cy_kwargs or {}
  387. cy_kwargs['output_dir'] = cwd
  388. if cplus is None:
  389. cplus = pyx_is_cplus(pyxpath)
  390. cy_kwargs['cplus'] = cplus
  391. interm_c_file = simple_cythonize(pyxpath, destdir=destdir, cwd=cwd, **cy_kwargs)
  392. include_dirs = include_dirs or []
  393. flags = kwargs.pop('flags', [])
  394. needed_flags = ('-fwrapv', '-pthread', '-fPIC')
  395. for flag in needed_flags:
  396. if flag not in flags:
  397. flags.append(flag)
  398. options = kwargs.pop('options', [])
  399. if kwargs.pop('strict_aliasing', False):
  400. raise CompileError("Cython requires strict aliasing to be disabled.")
  401. # Let's be explicit about standard
  402. if cplus:
  403. std = kwargs.pop('std', 'c++98')
  404. else:
  405. std = kwargs.pop('std', 'c99')
  406. return src2obj(interm_c_file, objpath=objpath, cwd=cwd,
  407. include_dirs=include_dirs, flags=flags, std=std,
  408. options=options, inc_py=True, strict_aliasing=False,
  409. **kwargs)
  410. def _any_X(srcs, cls):
  411. for src in srcs:
  412. name, ext = os.path.splitext(src)
  413. key = ext.lower()
  414. if key in extension_mapping:
  415. if extension_mapping[key][0] == cls:
  416. return True
  417. return False
  418. def any_fortran_src(srcs):
  419. return _any_X(srcs, FortranCompilerRunner)
  420. def any_cplus_src(srcs):
  421. return _any_X(srcs, CppCompilerRunner)
  422. def compile_link_import_py_ext(sources, extname=None, build_dir='.', compile_kwargs=None,
  423. link_kwargs=None):
  424. """ Compiles sources to a shared object (Python extension) and imports it
  425. Sources in ``sources`` which is imported. If shared object is newer than the sources, they
  426. are not recompiled but instead it is imported.
  427. Parameters
  428. ==========
  429. sources : string
  430. List of paths to sources.
  431. extname : string
  432. Name of extension (default: ``None``).
  433. If ``None``: taken from the last file in ``sources`` without extension.
  434. build_dir: str
  435. Path to directory in which objects files etc. are generated.
  436. compile_kwargs: dict
  437. keyword arguments passed to ``compile_sources``
  438. link_kwargs: dict
  439. keyword arguments passed to ``link_py_so``
  440. Returns
  441. =======
  442. The imported module from of the Python extension.
  443. """
  444. if extname is None:
  445. extname = os.path.splitext(os.path.basename(sources[-1]))[0]
  446. compile_kwargs = compile_kwargs or {}
  447. link_kwargs = link_kwargs or {}
  448. try:
  449. mod = import_module_from_file(os.path.join(build_dir, extname), sources)
  450. except ImportError:
  451. objs = compile_sources(list(map(get_abspath, sources)), destdir=build_dir,
  452. cwd=build_dir, **compile_kwargs)
  453. so = link_py_so(objs, cwd=build_dir, fort=any_fortran_src(sources),
  454. cplus=any_cplus_src(sources), **link_kwargs)
  455. mod = import_module_from_file(so)
  456. return mod
  457. def _write_sources_to_build_dir(sources, build_dir):
  458. build_dir = build_dir or tempfile.mkdtemp()
  459. if not os.path.isdir(build_dir):
  460. raise OSError("Non-existent directory: ", build_dir)
  461. source_files = []
  462. for name, src in sources:
  463. dest = os.path.join(build_dir, name)
  464. differs = True
  465. sha256_in_mem = sha256_of_string(src.encode('utf-8')).hexdigest()
  466. if os.path.exists(dest):
  467. if os.path.exists(dest + '.sha256'):
  468. with open(dest + '.sha256') as fh:
  469. sha256_on_disk = fh.read()
  470. else:
  471. sha256_on_disk = sha256_of_file(dest).hexdigest()
  472. differs = sha256_on_disk != sha256_in_mem
  473. if differs:
  474. with open(dest, 'wt') as fh:
  475. fh.write(src)
  476. with open(dest + '.sha256', 'wt') as fh:
  477. fh.write(sha256_in_mem)
  478. source_files.append(dest)
  479. return source_files, build_dir
  480. def compile_link_import_strings(sources, build_dir=None, **kwargs):
  481. """ Compiles, links and imports extension module from source.
  482. Parameters
  483. ==========
  484. sources : iterable of name/source pair tuples
  485. build_dir : string (default: None)
  486. Path. ``None`` implies use a temporary directory.
  487. **kwargs:
  488. Keyword arguments passed onto `compile_link_import_py_ext`.
  489. Returns
  490. =======
  491. mod : module
  492. The compiled and imported extension module.
  493. info : dict
  494. Containing ``build_dir`` as 'build_dir'.
  495. """
  496. source_files, build_dir = _write_sources_to_build_dir(sources, build_dir)
  497. mod = compile_link_import_py_ext(source_files, build_dir=build_dir, **kwargs)
  498. info = {"build_dir": build_dir}
  499. return mod, info
  500. def compile_run_strings(sources, build_dir=None, clean=False, compile_kwargs=None, link_kwargs=None):
  501. """ Compiles, links and runs a program built from sources.
  502. Parameters
  503. ==========
  504. sources : iterable of name/source pair tuples
  505. build_dir : string (default: None)
  506. Path. ``None`` implies use a temporary directory.
  507. clean : bool
  508. Whether to remove build_dir after use. This will only have an
  509. effect if ``build_dir`` is ``None`` (which creates a temporary directory).
  510. Passing ``clean == True`` and ``build_dir != None`` raises a ``ValueError``.
  511. This will also set ``build_dir`` in returned info dictionary to ``None``.
  512. compile_kwargs: dict
  513. Keyword arguments passed onto ``compile_sources``
  514. link_kwargs: dict
  515. Keyword arguments passed onto ``link``
  516. Returns
  517. =======
  518. (stdout, stderr): pair of strings
  519. info: dict
  520. Containing exit status as 'exit_status' and ``build_dir`` as 'build_dir'
  521. """
  522. if clean and build_dir is not None:
  523. raise ValueError("Automatic removal of build_dir is only available for temporary directory.")
  524. try:
  525. source_files, build_dir = _write_sources_to_build_dir(sources, build_dir)
  526. objs = compile_sources(list(map(get_abspath, source_files)), destdir=build_dir,
  527. cwd=build_dir, **(compile_kwargs or {}))
  528. prog = link(objs, cwd=build_dir,
  529. fort=any_fortran_src(source_files),
  530. cplus=any_cplus_src(source_files), **(link_kwargs or {}))
  531. p = subprocess.Popen([prog], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  532. exit_status = p.wait()
  533. stdout, stderr = [txt.decode('utf-8') for txt in p.communicate()]
  534. finally:
  535. if clean and os.path.isdir(build_dir):
  536. shutil.rmtree(build_dir)
  537. build_dir = None
  538. info = {"exit_status": exit_status, "build_dir": build_dir}
  539. return (stdout, stderr), info