Use a long-running remote bash shell, rather than docker exec

This commit is contained in:
Joe Rickerby
2020-06-24 16:39:20 +01:00
parent e417791ae4
commit 53c00a0af6
2 changed files with 78 additions and 26 deletions
+77 -25
View File
@@ -1,3 +1,4 @@
import io
import json
import platform
import shlex
@@ -7,7 +8,8 @@ import textwrap
import uuid
from os import PathLike
from pathlib import Path, PurePath
from typing import Dict, List, NamedTuple, Optional, Union, Sequence
from typing import (IO, Dict, List, NamedTuple, Optional, Sequence, TextIO,
Union)
from .util import (BuildOptions, BuildSelector,
get_build_verbosity_extra_flags, prepare_command)
@@ -258,12 +260,17 @@ def troubleshoot(package_dir: Path, error: Exception) -> None:
class DockerContainer:
UTILITY_PYTHON = '/opt/python/cp38-cp38/bin/python'
def __init__(self, docker_image, simulate_32_bit=False):
process: subprocess.Popen
bash_stdin: IO[str]
bash_stdout: IO[str]
def __init__(self, docker_image: str, simulate_32_bit=False):
self.docker_image = docker_image
self.simulate_32_bit = simulate_32_bit
def __enter__(self) -> 'DockerContainer':
self.container_name = f'cibuildwheel-{uuid.uuid4()}'
shell_args = ['linux32', '/bin/bash'] if self.simulate_32_bit else ['/bin/bash']
subprocess.run(
[
'docker', 'create',
@@ -272,14 +279,34 @@ class DockerContainer:
'-i',
'-v', '/:/host', # ignored on CircleCI
self.docker_image,
'/bin/bash'
*shell_args
],
check=True,
)
subprocess.run(['docker', 'start', self.container_name], check=True)
process = subprocess.Popen(
[
'docker', 'start',
'--attach', '--interactive',
self.container_name,
],
encoding='utf8',
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
# make the input buffer large enough to carry a lot of environment
# variables. We choose 256kB.
bufsize=262144,
)
self.process = process
assert process.stdin and process.stdout
self.bash_stdin = process.stdin
self.bash_stdout = process.stdout
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.bash_stdin.close()
self.process.terminate()
self.process.wait()
subprocess.run(['docker', 'rm', '--force', '-v', self.container_name])
self.container_name = None
@@ -323,33 +350,58 @@ class DockerContainer:
def call(self, args: Sequence[Union[str, PathLike]], env: Dict[str, str] = {},
capture_output=False, cwd: Optional[Union[str, PathLike]] = None) -> str:
# ensure paths are converted to strs
args = [str(a) for a in args]
env_exports = '\n'.join(f'export {k}={v}' for k, v in env.items())
chdir = f'cd {cwd}' if cwd else ''
command = ' '.join(shlex.quote(str(a)) for a in args)
end_of_message = str(uuid.uuid4())
if self.simulate_32_bit:
args = ['linux32', *args]
# log the command we're executing
print(f' + {command}')
options = ['-i']
for key, value in env.items():
options += ['--env', f'{key}={value}']
if cwd:
options += ['-w', str(cwd)]
print('+ ' + ' '.join(shlex.quote(str(a)) for a in args))
process = subprocess.run(
['docker', 'exec', *options, self.container_name, *args],
check=True,
universal_newlines=True,
stdout=subprocess.PIPE if capture_output else None
# Write a command to the remote shell. First we write the
# environment variables, exported inside the subshell. We change the
# cwd, if that's required. Then, the command is written. Finally, the
# remote shell is told to write a footer - this will show up in the
# output so we know when to stop reading, and will include the
# returncode of `command`.
self.bash_stdin.write(f'''(
{env_exports}
{chdir}
{command}
printf "%04d%s\n" $? {end_of_message}
)
''')
self.bash_stdin.flush()
if capture_output:
return process.stdout
output_io: TextIO = io.StringIO()
else:
return ''
output_io = sys.stdout
while True:
line = self.bash_stdout.readline()
if line.endswith(end_of_message+'\n'):
footer_offset = (
len(line)
- 1 # newline character
- len(end_of_message) # delimiter
- 4 # 4 returncode decimals
)
returncode_str = line[footer_offset:footer_offset+4]
returncode = int(returncode_str)
# add the last line to output, without the footer
output_io.write(line[0:footer_offset])
break
else:
output_io.write(line)
output = output_io.getvalue() if isinstance(output_io, io.StringIO) else None
if returncode != 0:
raise subprocess.CalledProcessError(returncode, args, output)
return output if output else ''
def get_environment(self) -> Dict[str, str]:
return json.loads(self.call([
+1 -1
View File
@@ -1,5 +1,5 @@
[flake8]
ignore = E501,W503,E741
ignore = E501,W503,E741,E226
application-import-names = cibuildwheel
exclude =
cibuildwheel/resources/,