conftest.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. #-----------------------------------------------------------------------------
  2. # Copyright (c) 2005-2021, PyInstaller Development Team.
  3. #
  4. # Distributed under the terms of the GNU General Public License (version 2
  5. # or later) with exception for distributing the bootloader.
  6. #
  7. # The full license is in the file COPYING.txt, distributed with this software.
  8. #
  9. # SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
  10. #-----------------------------------------------------------------------------
  11. import copy
  12. import glob
  13. import logging
  14. import os
  15. import re
  16. import shutil
  17. import subprocess
  18. from contextlib import suppress
  19. # Set a handler for the root-logger to inhibit 'basicConfig()' (called in PyInstaller.log) is setting up a stream
  20. # handler writing to stderr. This avoids log messages to be written (and captured) twice: once on stderr and
  21. # once by pytests's caplog.
  22. logging.getLogger().addHandler(logging.NullHandler())
  23. # Manages subprocess timeout.
  24. import psutil # noqa: E402
  25. import py # noqa: E402
  26. import pytest # noqa: E402
  27. import sys # noqa: E402
  28. # Expand sys.path with PyInstaller source.
  29. _ROOT_DIR = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..'))
  30. sys.path.append(_ROOT_DIR)
  31. from PyInstaller import __main__ as pyi_main # noqa: E402
  32. from PyInstaller import configure # noqa: E402
  33. from PyInstaller.compat import architecture, is_darwin, is_linux, is_win, safe_repr # noqa: E402
  34. from PyInstaller.depend.analysis import initialize_modgraph # noqa: E402
  35. from PyInstaller.utils.cliutils import archive_viewer # noqa: E402
  36. from PyInstaller.utils.tests import gen_sourcefile # noqa: E402
  37. from PyInstaller.utils.win32 import winutils # noqa: E402
  38. # Timeout for running the executable. If executable does not exit in this time, it is interpreted as a test failure.
  39. _EXE_TIMEOUT = 3 * 60 # In sec.
  40. # Number of retries we should attempt if the executable times out.
  41. _MAX_RETRIES = 2
  42. # All currently supported platforms
  43. SUPPORTED_OSES = {"darwin", "linux", "win32"}
  44. # Fixtures
  45. # --------
  46. @pytest.fixture
  47. def SPEC_DIR(request):
  48. """
  49. Return the directory where the test spec-files reside.
  50. """
  51. return py.path.local(_get_spec_dir(request))
  52. @pytest.fixture
  53. def SCRIPT_DIR(request):
  54. """
  55. Return the directory where the test scripts reside.
  56. """
  57. return py.path.local(_get_script_dir(request))
  58. def pytest_runtest_setup(item):
  59. """
  60. Markers to skip tests based on the current platform.
  61. https://pytest.org/en/stable/example/markers.html#marking-platform-specific-tests-with-pytest
  62. Available markers: see setup.cfg [tool:pytest] markers
  63. - @pytest.mark.darwin (macOS)
  64. - @pytest.mark.linux (GNU/Linux)
  65. - @pytest.mark.win32 (Windows)
  66. """
  67. supported_platforms = SUPPORTED_OSES.intersection(mark.name for mark in item.iter_markers())
  68. plat = sys.platform
  69. if supported_platforms and plat not in supported_platforms:
  70. pytest.skip("does not run on %s" % plat)
  71. @pytest.hookimpl(tryfirst=True, hookwrapper=True)
  72. def pytest_runtest_makereport(item, call):
  73. # Execute all other hooks to obtain the report object.
  74. outcome = yield
  75. rep = outcome.get_result()
  76. # Set a report attribute for each phase of a call, which can be "setup", "call", "teardown".
  77. setattr(item, "rep_" + rep.when, rep)
  78. # Return the base directory which contains the current test module.
  79. def _get_base_dir(request):
  80. return os.path.dirname(os.path.abspath(request.fspath.strpath))
  81. # Directory with Python scripts for functional tests.
  82. def _get_script_dir(request):
  83. return os.path.join(_get_base_dir(request), 'scripts')
  84. # Directory with testing modules used in some tests.
  85. def _get_modules_dir(request):
  86. return os.path.join(_get_base_dir(request), 'modules')
  87. # Directory with .toc log files.
  88. def _get_logs_dir(request):
  89. return os.path.join(_get_base_dir(request), 'logs')
  90. # Return the directory where data for tests is located.
  91. def _get_data_dir(request):
  92. return os.path.join(_get_base_dir(request), 'data')
  93. # Directory with .spec files used in some tests.
  94. def _get_spec_dir(request):
  95. return os.path.join(_get_base_dir(request), 'specs')
  96. @pytest.fixture
  97. def script_dir(request):
  98. return py.path.local(_get_script_dir(request))
  99. # A helper function to copy from data/dir to tmpdir/data.
  100. def _data_dir_copy(
  101. # The pytest request object.
  102. request,
  103. # The name of the subdirectory located in data/name to copy.
  104. subdir_name,
  105. # The tmpdir object for this test. See: https://pytest.org/latest/tmpdir.html.
  106. tmpdir
  107. ):
  108. # Form the source and tmp paths.
  109. source_data_dir = py.path.local(_get_data_dir(request)).join(subdir_name)
  110. tmp_data_dir = tmpdir.join('data', subdir_name)
  111. # Copy the data.
  112. shutil.copytree(source_data_dir.strpath, tmp_data_dir.strpath)
  113. # Return the temporary data directory, so that the copied data can now be used.
  114. return tmp_data_dir
  115. # Define a fixure for the DataDir object.
  116. @pytest.fixture
  117. def data_dir(
  118. # The request object for this test. See
  119. # https://pytest.org/latest/builtin.html#_pytest.python.FixtureRequest
  120. # and
  121. # https://pytest.org/latest/fixture.html#fixtures-can-introspect-the-requesting-test-context.
  122. request,
  123. # The tmpdir object for this test. See https://pytest.org/latest/tmpdir.html.
  124. tmpdir
  125. ):
  126. # Strip the leading 'test_' from the test's name.
  127. name = request.function.__name__[5:]
  128. # Copy to tmpdir and return the path.
  129. return _data_dir_copy(request, name, tmpdir)
  130. class AppBuilder(object):
  131. def __init__(self, tmpdir, request, bundle_mode):
  132. self._tmpdir = tmpdir
  133. self._request = request
  134. self._mode = bundle_mode
  135. self._specdir = str(tmpdir)
  136. self._distdir = str(tmpdir / 'dist')
  137. self._builddir = str(tmpdir / 'build')
  138. def test_spec(self, specfile, *args, **kwargs):
  139. """
  140. Test a Python script that is referenced in the supplied .spec file.
  141. """
  142. __tracebackhide__ = True
  143. specfile = os.path.join(_get_spec_dir(self._request), specfile)
  144. # 'test_script' should handle .spec properly as script.
  145. return self.test_script(specfile, *args, **kwargs)
  146. def test_source(self, source, *args, **kwargs):
  147. """
  148. Test a Python script given as source code.
  149. The source will be written into a file named like the test-function. This file will then be passed to
  150. `test_script`. If you need other related file, e.g., as `.toc`-file for testing the content, put it at at the
  151. normal place. Just mind to take the basnename from the test-function's name.
  152. :param script: Source code to create executable from. This will be saved into a temporary file which is then
  153. passed on to `test_script`.
  154. :param test_id: Test-id for parametrized tests. If given, it will be appended to the script filename, separated
  155. by two underscores.
  156. All other arguments are passed straight on to `test_script`.
  157. Ensure that the caller of `test_source` is in a UTF-8 encoded file with the correct '# -*- coding: utf-8 -*-'
  158. marker.
  159. """
  160. __tracebackhide__ = True
  161. # For parametrized test append the test-id.
  162. scriptfile = gen_sourcefile(self._tmpdir, source, kwargs.setdefault('test_id'))
  163. del kwargs['test_id']
  164. return self.test_script(str(scriptfile), *args, **kwargs)
  165. def test_script(
  166. self, script, pyi_args=None, app_name=None, app_args=None, runtime=None, run_from_path=False, **kwargs
  167. ):
  168. """
  169. Main method to wrap all phases of testing a Python script.
  170. :param script: Name of script to create executable from.
  171. :param pyi_args: Additional arguments to pass to PyInstaller when creating executable.
  172. :param app_name: Name of the executable. This is equivalent to argument --name=APPNAME.
  173. :param app_args: Additional arguments to pass to
  174. :param runtime: Time in seconds how long to keep executable running.
  175. :param toc_log: List of modules that are expected to be bundled with the executable.
  176. """
  177. __tracebackhide__ = True
  178. def marker(line):
  179. # Print some marker to stdout and stderr to make it easier to distinguish the phases in the CI test output.
  180. print('-------', line, '-------')
  181. print('-------', line, '-------', file=sys.stderr)
  182. if pyi_args is None:
  183. pyi_args = []
  184. if app_args is None:
  185. app_args = []
  186. if app_name:
  187. pyi_args.extend(['--name', app_name])
  188. else:
  189. # Derive name from script name.
  190. app_name = os.path.splitext(os.path.basename(script))[0]
  191. # Relative path means that a script from _script_dir is referenced.
  192. if not os.path.isabs(script):
  193. script = os.path.join(_get_script_dir(self._request), script)
  194. self.script = script
  195. assert os.path.exists(self.script), 'Script %s not found.' % script
  196. marker('Starting build.')
  197. if not self._test_building(args=pyi_args):
  198. pytest.fail('Building of %s failed.' % script)
  199. marker('Build finshed, now running executable.')
  200. self._test_executables(app_name, args=app_args, runtime=runtime, run_from_path=run_from_path, **kwargs)
  201. marker('Running executable finished.')
  202. def _test_executables(self, name, args, runtime, run_from_path, **kwargs):
  203. """
  204. Run created executable to make sure it works.
  205. Multipackage-tests generate more than one exe-file and all of them have to be run.
  206. :param args: CLI options to pass to the created executable.
  207. :param runtime: Time in seconds how long to keep the executable running.
  208. :return: Exit code of the executable.
  209. """
  210. __tracebackhide__ = True
  211. exes = self._find_executables(name)
  212. # Empty list means that PyInstaller probably failed to create any executable.
  213. assert exes != [], 'No executable file was found.'
  214. for exe in exes:
  215. # Try to find .toc log file. .toc log file has the same basename as exe file.
  216. toc_log = os.path.join(_get_logs_dir(self._request), os.path.splitext(os.path.basename(exe))[0] + '.toc')
  217. if os.path.exists(toc_log):
  218. if not self._examine_executable(exe, toc_log):
  219. pytest.fail('Matching .toc of %s failed.' % exe)
  220. retcode = self._run_executable(exe, args, run_from_path, runtime)
  221. if retcode != kwargs.get('retcode', 0):
  222. pytest.fail('Running exe %s failed with return-code %s.' % (exe, retcode))
  223. def _find_executables(self, name):
  224. """
  225. Search for all executables generated by the testcase.
  226. If the test-case is called e.g. 'test_multipackage1', this is searching for each of 'test_multipackage1.exe'
  227. and 'multipackage1_?.exe' in both one-file- and one-dir-mode.
  228. :param name: Name of the executable to look for.
  229. :return: List of executables
  230. """
  231. exes = []
  232. onedir_pt = os.path.join(self._distdir, name, name)
  233. onefile_pt = os.path.join(self._distdir, name)
  234. patterns = [
  235. onedir_pt,
  236. onefile_pt,
  237. # Multipackage one-dir
  238. onedir_pt + '_?',
  239. # Multipackage one-file
  240. onefile_pt + '_?'
  241. ]
  242. # For Windows append .exe extension to patterns.
  243. if is_win:
  244. patterns = [pt + '.exe' for pt in patterns]
  245. # For Mac OS append pattern for .app bundles.
  246. if is_darwin:
  247. # e.g: ./dist/name.app/Contents/MacOS/name
  248. pt = os.path.join(self._distdir, name + '.app', 'Contents', 'MacOS', name)
  249. patterns.append(pt)
  250. # Apply file patterns.
  251. for pattern in patterns:
  252. for prog in glob.glob(pattern):
  253. if os.path.isfile(prog):
  254. exes.append(prog)
  255. return exes
  256. def _run_executable(self, prog, args, run_from_path, runtime):
  257. """
  258. Run executable created by PyInstaller.
  259. :param args: CLI options to pass to the created executable.
  260. """
  261. # Run the test in a clean environment to make sure they're really self-contained.
  262. prog_env = copy.deepcopy(os.environ)
  263. prog_env['PATH'] = ''
  264. del prog_env['PATH']
  265. # For Windows we need to keep minimal PATH for successful running of some tests.
  266. if is_win:
  267. # Minimum Windows PATH is in most cases: C:\Windows\system32;C:\Windows
  268. prog_env['PATH'] = os.pathsep.join(winutils.get_system_path())
  269. exe_path = prog
  270. if run_from_path:
  271. # Run executable in the temp directory. Add the directory containing the executable to $PATH. Basically,
  272. # pretend we are a shell executing the program from $PATH.
  273. prog_cwd = str(self._tmpdir)
  274. prog_name = os.path.basename(prog)
  275. prog_env['PATH'] = os.pathsep.join([prog_env.get('PATH', ''), os.path.dirname(prog)])
  276. else:
  277. # Run executable in the directory where it is.
  278. prog_cwd = os.path.dirname(prog)
  279. # The executable will be called with argv[0] as relative not absolute path.
  280. prog_name = os.path.join(os.curdir, os.path.basename(prog))
  281. args = [prog_name] + args
  282. # Using sys.stdout/sys.stderr for subprocess fixes printing messages in Windows command prompt. Py.test is then
  283. # able to collect stdout/sterr messages and display them if a test fails.
  284. for _ in range(_MAX_RETRIES):
  285. retcode = self._run_executable_(args, exe_path, prog_env, prog_cwd, runtime)
  286. if retcode != 1: # retcode == 1 means a timeout
  287. break
  288. return retcode
  289. def _run_executable_(self, args, exe_path, prog_env, prog_cwd, runtime):
  290. process = psutil.Popen(
  291. args, executable=exe_path, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=prog_env, cwd=prog_cwd
  292. )
  293. def _msg(*text):
  294. print('[' + str(process.pid) + '] ', *text)
  295. # Run executable. stderr is redirected to stdout.
  296. _msg('RUNNING: ', safe_repr(exe_path), ', args: ', safe_repr(args))
  297. # 'psutil' allows to use timeout in waiting for a subprocess. If not timeout was specified then it is 'None' -
  298. # no timeout, just waiting. Runtime is useful mostly for interactive tests.
  299. try:
  300. timeout = runtime if runtime else _EXE_TIMEOUT
  301. stdout, stderr = process.communicate(timeout=timeout)
  302. retcode = process.returncode
  303. except (psutil.TimeoutExpired, subprocess.TimeoutExpired):
  304. if runtime:
  305. # When 'runtime' is set, the expired timeout is a good sign that the executable was running successfully
  306. # for a specified time.
  307. # TODO: is there a better way return success than 'retcode = 0'?
  308. retcode = 0
  309. else:
  310. # Exe is running and it is not interactive. Fail the test.
  311. retcode = 1
  312. _msg(f'TIMED OUT while running executable (timeout: {timeout} sec)!')
  313. # Kill the subprocess and its child processes.
  314. for p in list(process.children(recursive=True)) + [process]:
  315. with suppress(psutil.NoSuchProcess):
  316. p.kill()
  317. stdout, stderr = process.communicate()
  318. sys.stdout.buffer.write(stdout)
  319. sys.stderr.buffer.write(stderr)
  320. return retcode
  321. def _test_building(self, args):
  322. """
  323. Run building of test script.
  324. :param args: additional CLI options for PyInstaller.
  325. Return True if build succeded False otherwise.
  326. """
  327. default_args = [
  328. '--debug=bootloader',
  329. '--noupx',
  330. '--specpath', self._specdir,
  331. '--distpath', self._distdir,
  332. '--workpath', self._builddir,
  333. '--path', _get_modules_dir(self._request),
  334. '--log-level=INFO',
  335. ] # yapf: disable
  336. # Choose bundle mode.
  337. if self._mode == 'onedir':
  338. default_args.append('--onedir')
  339. elif self._mode == 'onefile':
  340. default_args.append('--onefile')
  341. # if self._mode is None then just the spec file was supplied.
  342. pyi_args = [self.script] + default_args + args
  343. # TODO: fix return code in running PyInstaller programatically.
  344. PYI_CONFIG = configure.get_config(upx_dir=None)
  345. # Override CACHEDIR for PyInstaller and put it into self.tmpdir
  346. PYI_CONFIG['cachedir'] = str(self._tmpdir)
  347. pyi_main.run(pyi_args, PYI_CONFIG)
  348. retcode = 0
  349. return retcode == 0
  350. def _examine_executable(self, exe, toc_log):
  351. """
  352. Compare log files (now used mostly by multipackage test_name).
  353. :return: True if .toc files match
  354. """
  355. print('EXECUTING MATCHING:', toc_log)
  356. fname_list = archive_viewer.get_archive_content(exe)
  357. fname_list = [fn for fn in fname_list]
  358. with open(toc_log, 'r') as f:
  359. pattern_list = eval(f.read())
  360. # Alphabetical order of patterns.
  361. pattern_list.sort()
  362. missing = []
  363. for pattern in pattern_list:
  364. for fname in fname_list:
  365. if re.match(pattern, fname):
  366. print('MATCH:', pattern, '-->', fname)
  367. break
  368. else:
  369. # No matching entry found
  370. missing.append(pattern)
  371. print('MISSING:', pattern)
  372. # Not all modules matched. Stop comparing other .toc files and fail the test.
  373. if missing:
  374. for m in missing:
  375. print('Missing', m, 'in', exe)
  376. return False
  377. # All patterns matched.
  378. return True
  379. # Scope 'session' should keep the object unchanged for whole tests. This fixture caches basic module graph dependencies
  380. # that are same for every executable.
  381. @pytest.fixture(scope='session')
  382. def pyi_modgraph():
  383. # Explicitly set the log level since the plugin `pytest-catchlog` (un-) sets the root logger's level to NOTSET for
  384. # the setup phase, which will lead to TRACE messages been written out.
  385. import PyInstaller.log as logging
  386. logging.logger.setLevel(logging.DEBUG)
  387. initialize_modgraph()
  388. # Run by default test as onedir and onefile.
  389. @pytest.fixture(params=['onedir', 'onefile'])
  390. def pyi_builder(tmpdir, monkeypatch, request, pyi_modgraph):
  391. # Save/restore environment variable PATH.
  392. monkeypatch.setenv('PATH', os.environ['PATH'])
  393. # PyInstaller or a test case might manipulate 'sys.path'. Reset it for every test.
  394. monkeypatch.syspath_prepend(None)
  395. # Set current working directory to
  396. monkeypatch.chdir(tmpdir)
  397. # Clean up configuration and force PyInstaller to do a clean configuration for another app/test. The value is same
  398. # as the original value.
  399. monkeypatch.setattr('PyInstaller.config.CONF', {'pathex': []})
  400. yield AppBuilder(tmpdir, request, request.param)
  401. if is_darwin or is_linux:
  402. if request.node.rep_setup.passed:
  403. if request.node.rep_call.passed:
  404. if tmpdir.exists():
  405. tmpdir.remove(rec=1, ignore_errors=True)
  406. # Fixture for .spec based tests. With .spec it does not make sense to differentiate onefile/onedir mode.
  407. @pytest.fixture
  408. def pyi_builder_spec(tmpdir, request, monkeypatch, pyi_modgraph):
  409. # Save/restore environment variable PATH.
  410. monkeypatch.setenv('PATH', os.environ['PATH'])
  411. # Set current working directory to
  412. monkeypatch.chdir(tmpdir)
  413. # PyInstaller or a test case might manipulate 'sys.path'. Reset it for every test.
  414. monkeypatch.syspath_prepend(None)
  415. # Clean up configuration and force PyInstaller to do a clean configuration for another app/test. The value is same
  416. # as the original value.
  417. monkeypatch.setattr('PyInstaller.config.CONF', {'pathex': []})
  418. return AppBuilder(tmpdir, request, None)
  419. # Define a fixture which compiles the data/load_dll_using_ctypes/ctypes_dylib.c program in the tmpdir, returning the
  420. # tmpdir object.
  421. @pytest.fixture()
  422. def compiled_dylib(tmpdir, request):
  423. tmp_data_dir = _data_dir_copy(request, 'ctypes_dylib', tmpdir)
  424. # Compile the ctypes_dylib in the tmpdir: Make tmpdir/data the CWD. Do NOT use monkeypatch.chdir() to change and
  425. # monkeypatch.undo() to restore the CWD, since this will undo ALL monkeypatches (such as the pyi_builder's additions
  426. # to sys.path), breaking the test.
  427. old_wd = tmp_data_dir.chdir()
  428. try:
  429. if is_win:
  430. tmp_data_dir = tmp_data_dir.join('ctypes_dylib.dll')
  431. # For Mingw-x64 we must pass '-m32' to build 32-bit binaries
  432. march = '-m32' if architecture == '32bit' else '-m64'
  433. ret = subprocess.call('gcc -shared ' + march + ' ctypes_dylib.c -o ctypes_dylib.dll', shell=True)
  434. if ret != 0:
  435. # Find path to cl.exe file.
  436. from distutils.msvccompiler import MSVCCompiler
  437. comp = MSVCCompiler()
  438. comp.initialize()
  439. cl_path = comp.cc
  440. # Fallback to msvc.
  441. ret = subprocess.call([cl_path, '/LD', 'ctypes_dylib.c'], shell=False)
  442. elif is_darwin:
  443. tmp_data_dir = tmp_data_dir.join('ctypes_dylib.dylib')
  444. # On Mac OS X we need to detect architecture - 32 bit or 64 bit.
  445. arch = 'i386' if architecture == '32bit' else 'x86_64'
  446. cmd = (
  447. 'gcc -arch ' + arch + ' -Wall -dynamiclib '
  448. 'ctypes_dylib.c -o ctypes_dylib.dylib -headerpad_max_install_names'
  449. )
  450. ret = subprocess.call(cmd, shell=True)
  451. id_dylib = os.path.abspath('ctypes_dylib.dylib')
  452. ret = subprocess.call('install_name_tool -id %s ctypes_dylib.dylib' % (id_dylib,), shell=True)
  453. else:
  454. tmp_data_dir = tmp_data_dir.join('ctypes_dylib.so')
  455. ret = subprocess.call('gcc -fPIC -shared ctypes_dylib.c -o ctypes_dylib.so', shell=True)
  456. assert ret == 0, 'Compile ctypes_dylib failed.'
  457. finally:
  458. # Reset the CWD directory.
  459. old_wd.chdir()
  460. return tmp_data_dir
  461. @pytest.fixture
  462. def pyi_windowed_builder(pyi_builder: AppBuilder):
  463. """A pyi_builder equivalent for testing --windowed applications."""
  464. # psutil.Popen() somehow bypasses an application's windowed/console mode so that any application built in
  465. # --windowed mode but invoked with psutil still recieves valid std{in,out,err} handles and behaves exactly like
  466. # a console application. In short, testing windowed mode with psutil is a null test. We must instead use subprocess.
  467. def _run_executable_(args, exe_path, prog_env, prog_cwd, runtime):
  468. return subprocess.run([exe_path, *args], env=prog_env, cwd=prog_cwd, timeout=runtime).returncode
  469. pyi_builder._run_executable_ = _run_executable_
  470. yield pyi_builder