import argparse import io import re import tarfile from pathlib import Path import typing import docker from docker import DockerClient from docker.models.images import Image from docker.utils.json_stream import json_stream DEFAULT_DOCKERFILE = Path("Dockerfile") DEFAULT_OUTPUT_DIR = Path("PyQt5-stubs") def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Build PyQt stubs in Docker") # noinspection PyTypeChecker parser.add_argument('-d', '--dockerfile', type=Path, default=DEFAULT_DOCKERFILE, help="Dockerfile to build") # noinspection PyTypeChecker parser.add_argument('-o', '--output-dir', type=Path, default=DEFAULT_OUTPUT_DIR, help="Directory to find package(s) to be built. " "Defaults to ./pkg") return parser.parse_args() def main(): args = parse_args() docker_client = docker.from_env() image_id = build_image(docker_client, args.dockerfile) extract_output(docker_client, image_id, args.output_dir) def build_image(docker_client: DockerClient, dockerfile: Path) -> str: image_name = "pyqt5-stubs" # Using low-level API so that we can log as it occurs instead of only # after build has finished/failed resp = docker_client.api.build( path=str(dockerfile.parent), rm=True, tag=image_name) image_id: str = typing.cast(str, None) for chunk in json_stream(resp): if 'error' in chunk: message = f"Error while building Dockerfile for " \ f"{image_name}:\n{chunk['error']}" print(message) raise DockerBuildError(message) elif 'stream' in chunk: print(chunk['stream'].rstrip('\n')) # Taken from the high level API implementation of build match = re.search(r'(^Successfully built |sha256:)([0-9a-f]+)$', chunk['stream']) if match: image_id = match.group(2) if not image_id: message = f"Unknown Error while building Dockerfile for " \ f"{image_name}. Build did not return an image ID" raise DockerBuildError(message) return image_id def extract_output(docker_client: DockerClient, image_id: str, output_dir: Path) -> None: image = docker_client.images.get(image_id) container = docker_client.containers.create(image) # Get archive tar bytes from the container as a sequence of bytes package_tar_byte_gen: typing.Generator[bytes, None, None] package_tar_byte_gen, _ = container.get_archive("/output/", chunk_size=None) # Concat all the chunks together package_tar_bytes: bytes package_tar_bytes = b"".join(package_tar_byte_gen) # Create a tarfile from the tar bytes tar_file_object = io.BytesIO(package_tar_bytes) package_tar = tarfile.open(fileobj=tar_file_object) # Extract the files from the tarfile to the disk for tar_deb_info in package_tar.getmembers(): # Ignore directories if not tar_deb_info.isfile(): continue # Directory that will contain the output files output_dir.mkdir(parents=True, exist_ok=True) # Filename (without outer directory) tar_deb_info.name = Path(tar_deb_info.name).name # Extract package_tar.extract(tar_deb_info, output_dir) class DockerBuildError(RuntimeError): def __init__(self, message): self.message = message if __name__ == '__main__': main()