Convert DockerContainer to use binary PIPEs, to handle arbitrary data
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import codecs
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import shlex
|
import shlex
|
||||||
@@ -6,7 +7,7 @@ import sys
|
|||||||
import uuid
|
import uuid
|
||||||
from os import PathLike
|
from os import PathLike
|
||||||
from pathlib import Path, PurePath
|
from pathlib import Path, PurePath
|
||||||
from typing import IO, Dict, List, Optional, Sequence, TextIO, Union
|
from typing import IO, Any, Dict, List, Optional, Sequence, TextIO, Union, cast
|
||||||
|
|
||||||
|
|
||||||
class DockerContainer:
|
class DockerContainer:
|
||||||
@@ -23,21 +24,21 @@ class DockerContainer:
|
|||||||
UTILITY_PYTHON = '/opt/python/cp38-cp38/bin/python'
|
UTILITY_PYTHON = '/opt/python/cp38-cp38/bin/python'
|
||||||
|
|
||||||
process: subprocess.Popen
|
process: subprocess.Popen
|
||||||
bash_stdin: IO[str]
|
bash_stdin: IO[bytes]
|
||||||
bash_stdout: IO[str]
|
bash_stdout: IO[bytes]
|
||||||
|
|
||||||
def __init__(self, docker_image: str, simulate_32_bit=False):
|
def __init__(self, docker_image: str, simulate_32_bit=False):
|
||||||
self.docker_image = docker_image
|
self.docker_image = docker_image
|
||||||
self.simulate_32_bit = simulate_32_bit
|
self.simulate_32_bit = simulate_32_bit
|
||||||
|
|
||||||
def __enter__(self) -> 'DockerContainer':
|
def __enter__(self) -> 'DockerContainer':
|
||||||
self.container_name = f'cibuildwheel-{uuid.uuid4()}'
|
self.name = f'cibuildwheel-{uuid.uuid4()}'
|
||||||
shell_args = ['linux32', '/bin/bash'] if self.simulate_32_bit else ['/bin/bash']
|
shell_args = ['linux32', '/bin/bash'] if self.simulate_32_bit else ['/bin/bash']
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
[
|
[
|
||||||
'docker', 'create',
|
'docker', 'create',
|
||||||
'--env', 'CIBUILDWHEEL',
|
'--env', 'CIBUILDWHEEL',
|
||||||
'--name', self.container_name,
|
'--name', self.name,
|
||||||
'-i',
|
'-i',
|
||||||
'-v', '/:/host', # ignored on CircleCI
|
'-v', '/:/host', # ignored on CircleCI
|
||||||
self.docker_image,
|
self.docker_image,
|
||||||
@@ -49,13 +50,12 @@ class DockerContainer:
|
|||||||
[
|
[
|
||||||
'docker', 'start',
|
'docker', 'start',
|
||||||
'--attach', '--interactive',
|
'--attach', '--interactive',
|
||||||
self.container_name,
|
self.name,
|
||||||
],
|
],
|
||||||
encoding='utf8',
|
|
||||||
stdin=subprocess.PIPE,
|
stdin=subprocess.PIPE,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
bufsize=262144,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert self.process.stdin and self.process.stdout
|
assert self.process.stdin and self.process.stdout
|
||||||
self.bash_stdin = self.process.stdin
|
self.bash_stdin = self.process.stdin
|
||||||
self.bash_stdout = self.process.stdout
|
self.bash_stdout = self.process.stdout
|
||||||
@@ -66,8 +66,8 @@ class DockerContainer:
|
|||||||
self.process.terminate()
|
self.process.terminate()
|
||||||
self.process.wait()
|
self.process.wait()
|
||||||
|
|
||||||
subprocess.run(['docker', 'rm', '--force', '-v', self.container_name])
|
subprocess.run(['docker', 'rm', '--force', '-v', self.name])
|
||||||
self.container_name = None
|
self.name = None
|
||||||
|
|
||||||
def copy_into(self, from_path: Path, to_path: PurePath) -> None:
|
def copy_into(self, from_path: Path, to_path: PurePath) -> None:
|
||||||
# `docker cp` causes 'no space left on device' error when
|
# `docker cp` causes 'no space left on device' error when
|
||||||
@@ -77,13 +77,13 @@ class DockerContainer:
|
|||||||
if from_path.is_dir():
|
if from_path.is_dir():
|
||||||
self.call(['mkdir', '-p', to_path])
|
self.call(['mkdir', '-p', to_path])
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
f'tar cf - . | docker exec -i {self.container_name} tar -xC {to_path} -f -',
|
f'tar cf - . | docker exec -i {self.name} tar -xC {to_path} -f -',
|
||||||
shell=True,
|
shell=True,
|
||||||
check=True,
|
check=True,
|
||||||
cwd=from_path)
|
cwd=from_path)
|
||||||
else:
|
else:
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
f'cat {from_path} | docker exec -i {self.container_name} sh -c "cat > {to_path}"',
|
f'cat {from_path} | docker exec -i {self.name} sh -c "cat > {to_path}"',
|
||||||
shell=True,
|
shell=True,
|
||||||
check=True)
|
check=True)
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ class DockerContainer:
|
|||||||
to_path.mkdir(parents=True, exist_ok=True)
|
to_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
f'docker exec -i {self.container_name} tar -cC {from_path} -f - . | tar -xf -',
|
f'docker exec -i {self.name} tar -cC {from_path} -f - . | tar -xf -',
|
||||||
shell=True,
|
shell=True,
|
||||||
check=True,
|
check=True,
|
||||||
cwd=to_path
|
cwd=to_path
|
||||||
@@ -125,23 +125,23 @@ class DockerContainer:
|
|||||||
# Finally, the remote shell is told to write a footer - this will show
|
# 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
|
# up in the output so we know when to stop reading, and will include
|
||||||
# the returncode of `command`.
|
# the returncode of `command`.
|
||||||
self.bash_stdin.write(f'''(
|
self.bash_stdin.write(bytes(f'''(
|
||||||
{chdir}
|
{chdir}
|
||||||
env {env_assignments} {command}
|
env {env_assignments} {command}
|
||||||
printf "%04d%s\n" $? {end_of_message}
|
printf "%04d%s\n" $? {end_of_message}
|
||||||
)
|
)
|
||||||
''')
|
''', encoding='utf8', errors='surrogateescape'))
|
||||||
self.bash_stdin.flush()
|
self.bash_stdin.flush()
|
||||||
|
|
||||||
if capture_output:
|
if capture_output:
|
||||||
output_io: TextIO = io.StringIO()
|
output_io: IO[bytes] = io.BytesIO()
|
||||||
else:
|
else:
|
||||||
output_io = sys.stdout
|
output_io = sys.stdout.buffer
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
line = self.bash_stdout.readline()
|
line = self.bash_stdout.readline()
|
||||||
|
|
||||||
if line.endswith(end_of_message+'\n'):
|
if line.endswith(b'%s\n' % (bytes(end_of_message, encoding='utf8'))):
|
||||||
footer_offset = (
|
footer_offset = (
|
||||||
len(line)
|
len(line)
|
||||||
- 1 # newline character
|
- 1 # newline character
|
||||||
@@ -156,12 +156,15 @@ class DockerContainer:
|
|||||||
else:
|
else:
|
||||||
output_io.write(line)
|
output_io.write(line)
|
||||||
|
|
||||||
output = output_io.getvalue() if isinstance(output_io, io.StringIO) else None
|
if isinstance(output_io, io.BytesIO):
|
||||||
|
output = str(output_io.getvalue(), encoding='utf8', errors='surrogateescape')
|
||||||
|
else:
|
||||||
|
output = ''
|
||||||
|
|
||||||
if returncode != 0:
|
if returncode != 0:
|
||||||
raise subprocess.CalledProcessError(returncode, args, output)
|
raise subprocess.CalledProcessError(returncode, args, output)
|
||||||
|
|
||||||
return output if output else ''
|
return output
|
||||||
|
|
||||||
def get_environment(self) -> Dict[str, str]:
|
def get_environment(self) -> Dict[str, str]:
|
||||||
return json.loads(self.call([
|
return json.loads(self.call([
|
||||||
|
|||||||
Reference in New Issue
Block a user