|  | #!/usr/bin/env python3 | 
|  |  | 
|  | # Copyright 2020 Google LLC | 
|  | # | 
|  | # Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | # you may not use this file except in compliance with the License. | 
|  | # You may obtain a copy of the License at | 
|  | # | 
|  | #      https://www.apache.org/licenses/LICENSE-2.0 | 
|  | # | 
|  | # Unless required by applicable law or agreed to in writing, software | 
|  | # distributed under the License is distributed on an "AS IS" BASIS, | 
|  | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | # See the License for the specific language governing permissions and | 
|  | # limitations under the License. | 
|  | """Manages IREE Docker image definitions. | 
|  |  | 
|  | Includes information on their dependency graph and GCR URL. | 
|  |  | 
|  | Example usage: | 
|  |  | 
|  | Rebuild the cmake image and all images that transitively on depend on it, | 
|  | tagging them with `latest`: | 
|  | python3 build_tools/docker/manage_images.py --build --image cmake | 
|  |  | 
|  | Print out output for rebuilding the cmake image and all images that | 
|  | transitively on depend on it, but don't take side-effecting actions: | 
|  | python3 build_tools/docker/manage_images.py --build --image cmake --dry-run | 
|  |  | 
|  | Push all `prod` images to GCR: | 
|  | python3 build_tools/docker/manage_images.py --push --tag prod --images all | 
|  |  | 
|  | Rebuild and push all images and update references to them in the repository: | 
|  | python3 build_tools/docker/manage_images.py --push --images all | 
|  | --update-references | 
|  | """ | 
|  |  | 
|  | import argparse | 
|  | import fileinput | 
|  | import os | 
|  | import posixpath | 
|  | import re | 
|  | import subprocess | 
|  | import sys | 
|  |  | 
|  | IREE_GCR_URL = 'gcr.io/iree-oss/' | 
|  | DOCKER_DIR = 'build_tools/docker/' | 
|  |  | 
|  | # Map from image names to images that they depend on. | 
|  | IMAGES_TO_DEPENDENCIES = { | 
|  | 'base': [], | 
|  | 'bazel': ['base', 'util'], | 
|  | 'bazel-python': ['bazel'], | 
|  | 'bazel-tensorflow': ['bazel-python'], | 
|  | 'bazel-tensorflow-nvidia': ['bazel-tensorflow-vulkan'], | 
|  | 'bazel-tensorflow-swiftshader': ['bazel-tensorflow-vulkan', 'swiftshader'], | 
|  | 'bazel-tensorflow-vulkan': ['bazel-tensorflow'], | 
|  | 'cmake': ['base', 'util'], | 
|  | 'cmake-android': ['cmake', 'util'], | 
|  | 'cmake-python': ['cmake'], | 
|  | 'cmake-python-nvidia': ['cmake-python-vulkan'], | 
|  | 'cmake-python-swiftshader': ['cmake-python-vulkan', 'swiftshader'], | 
|  | 'cmake-python-vulkan': ['cmake-python'], | 
|  | 'rbe-toolchain': [], | 
|  | 'swiftshader': ['cmake'], | 
|  | 'util': [], | 
|  | } | 
|  |  | 
|  | IMAGES_TO_DEPENDENT_IMAGES = {k: [] for k in IMAGES_TO_DEPENDENCIES} | 
|  | for image, dependencies in IMAGES_TO_DEPENDENCIES.items(): | 
|  | for dependency in dependencies: | 
|  | IMAGES_TO_DEPENDENT_IMAGES[dependency].append(image) | 
|  |  | 
|  | IMAGES_HELP = [f'`{name}`' for name in IMAGES_TO_DEPENDENCIES] | 
|  | IMAGES_HELP = f'{", ".join(IMAGES_HELP)} or `all`' | 
|  |  | 
|  |  | 
|  | def parse_arguments(): | 
|  | """Parses command-line options.""" | 
|  | parser = argparse.ArgumentParser( | 
|  | description="Build IREE's Docker images and optionally push them to GCR.") | 
|  | parser.add_argument('--images', | 
|  | '--image', | 
|  | type=str, | 
|  | required=True, | 
|  | action='append', | 
|  | help=f'Name of the image to build: {IMAGES_HELP}.') | 
|  | parser.add_argument( | 
|  | '--tag', | 
|  | type=str, | 
|  | default='latest', | 
|  | help='Tag for the images to build. Defaults to `latest` (which is good ' | 
|  | 'for testing changes in a PR). Use `prod` to update the images that the ' | 
|  | 'CI caches.') | 
|  | parser.add_argument('--pull', | 
|  | action='store_true', | 
|  | help='Pull the specified image before building.') | 
|  | parser.add_argument('--build', | 
|  | action='store_true', | 
|  | help='Build new images from the current Dockerfiles.') | 
|  | parser.add_argument( | 
|  | '--push', | 
|  | action='store_true', | 
|  | help='Push the built images to GCR. Requires gcloud authorization.') | 
|  | parser.add_argument( | 
|  | '--update_references', | 
|  | '--update-references', | 
|  | action='store_true', | 
|  | help='Update all references to the specified images to point at the new' | 
|  | ' digest.') | 
|  | parser.add_argument( | 
|  | '--dry_run', | 
|  | '--dry-run', | 
|  | '-n', | 
|  | action='store_true', | 
|  | help='Print output without building or pushing any images.') | 
|  |  | 
|  | args = parser.parse_args() | 
|  | for image in args.images: | 
|  | if image == 'all': | 
|  | # Sort for a determinstic order | 
|  | args.images = sorted(IMAGES_TO_DEPENDENCIES.keys()) | 
|  | elif image not in IMAGES_TO_DEPENDENCIES: | 
|  | raise parser.error('Expected --image to be one of:\n' | 
|  | f'  {IMAGES_HELP}\n' | 
|  | f'but got `{image}`.') | 
|  | return args | 
|  |  | 
|  |  | 
|  | def get_ordered_images_to_process(images): | 
|  | unmarked_images = list(images) | 
|  | # Python doesn't have a builtin OrderedSet | 
|  | marked_images = set() | 
|  | order = [] | 
|  |  | 
|  | def visit(image): | 
|  | if image in marked_images: | 
|  | return | 
|  | for dependent_images in IMAGES_TO_DEPENDENT_IMAGES[image]: | 
|  | visit(dependent_images) | 
|  | marked_images.add(image) | 
|  | order.append(image) | 
|  |  | 
|  | while unmarked_images: | 
|  | visit(unmarked_images.pop()) | 
|  |  | 
|  | order.reverse() | 
|  | return order | 
|  |  | 
|  |  | 
|  | def stream_command(command, dry_run=False): | 
|  | print(f'Running: `{" ".join(command)}`') | 
|  | if dry_run: | 
|  | return 0 | 
|  | process = subprocess.Popen(command, | 
|  | bufsize=1, | 
|  | stderr=subprocess.STDOUT, | 
|  | stdout=subprocess.PIPE, | 
|  | universal_newlines=True) | 
|  | for line in process.stdout: | 
|  | print(line, end='') | 
|  |  | 
|  | if process.poll() is None: | 
|  | raise RuntimeError('Unexpected end of output while process is not finished') | 
|  | return process.poll() | 
|  |  | 
|  |  | 
|  | def check_stream_command(command, dry_run=False): | 
|  | exit_code = stream_command(command, dry_run=dry_run) | 
|  | if exit_code != 0: | 
|  | print(f'Command failed with exit code {exit_code}: `{" ".join(command)}`') | 
|  | sys.exit(exit_code) | 
|  |  | 
|  |  | 
|  | def get_repo_digest(image): | 
|  | inspect_command = [ | 
|  | 'docker', | 
|  | 'image', | 
|  | 'inspect', | 
|  | f'{image}', | 
|  | '-f', | 
|  | '{{index .RepoDigests 0}}', | 
|  | ] | 
|  | inspect_process = subprocess.run(inspect_command, | 
|  | universal_newlines=True, | 
|  | stdout=subprocess.PIPE, | 
|  | stderr=subprocess.PIPE, | 
|  | timeout=10) | 
|  | if inspect_process.returncode != 0: | 
|  | print(f'Computing the repository digest for {image} failed.' | 
|  | ' Has it been pushed to GCR?') | 
|  | print(f'Output from `{" ".join(inspect_command)}`:') | 
|  | print(inspect_process.stdout, end='') | 
|  | print(inspect_process.stderr, end='') | 
|  | sys.exit(inspect_process.returncode) | 
|  | _, repo_digest = inspect_process.stdout.strip().split('@') | 
|  | return repo_digest | 
|  |  | 
|  |  | 
|  | def update_rbe_reference(digest, dry_run=False): | 
|  | print('Updating WORKSPACE file for rbe-toolchain') | 
|  | for line in fileinput.input(files=['WORKSPACE'], inplace=(not dry_run)): | 
|  | if line.strip().startswith('digest ='): | 
|  | print(re.sub('sha256:[a-zA-Z0-9]+', digest, line), end='') | 
|  | else: | 
|  | print(line, end='') | 
|  |  | 
|  |  | 
|  | def update_references(image_name, digest, dry_run=False): | 
|  | print(f'Updating references to {image_name}') | 
|  |  | 
|  | grep_command = ['git', 'grep', '-l', f'{image_name}@sha256'] | 
|  | grep_process = subprocess.run(grep_command, | 
|  | stdout=subprocess.PIPE, | 
|  | stderr=subprocess.PIPE, | 
|  | timeout=5, | 
|  | universal_newlines=True) | 
|  | if grep_process.returncode > 1: | 
|  | print(f'{" ".join(grep_command)} ' | 
|  | f'failed with exit code {grep_process.returncode}') | 
|  | sys.exit(grep_process.returncode) | 
|  | if grep_process.returncode == 1: | 
|  | print(f'Found no references to {image_name}') | 
|  | return | 
|  |  | 
|  | files = grep_process.stdout.split() | 
|  | print(f'Updating references in {len(files)} files: {files}') | 
|  | for line in fileinput.input(files=files, inplace=(not dry_run)): | 
|  | print(re.sub(f'{image_name}@sha256:[a-zA-Z0-9]+', f'{image_name}@{digest}', | 
|  | line), | 
|  | end='') | 
|  |  | 
|  |  | 
|  | if __name__ == '__main__': | 
|  | args = parse_arguments() | 
|  |  | 
|  | # Ensure the user has the correct authorization if they try to push to GCR. | 
|  | if args.push: | 
|  | if stream_command(['which', 'gcloud']) != 0: | 
|  | print('gcloud not found.' | 
|  | ' See https://cloud.google.com/sdk/install for installation.') | 
|  | sys.exit(1) | 
|  | check_stream_command(['gcloud', 'auth', 'configure-docker'], | 
|  | dry_run=args.dry_run) | 
|  |  | 
|  | images_to_process = get_ordered_images_to_process(args.images) | 
|  | print(f'Also processing dependent images. Will process: {images_to_process}') | 
|  |  | 
|  | for image in images_to_process: | 
|  | print(f'Processing image {image}') | 
|  | image_name = posixpath.join(IREE_GCR_URL, image) | 
|  | image_tag = f'{image_name}:{args.tag}' | 
|  | image_path = os.path.join(DOCKER_DIR, image) | 
|  |  | 
|  | if args.pull: | 
|  | check_stream_command(['docker', 'pull', image_tag], dry_run=args.dry_run) | 
|  |  | 
|  | if args.build: | 
|  | check_stream_command(['docker', 'build', '--tag', image_tag, image_path], | 
|  | dry_run=args.dry_run) | 
|  |  | 
|  | if args.push: | 
|  | check_stream_command(['docker', 'push', image_tag], dry_run=args.dry_run) | 
|  |  | 
|  | if args.update_references: | 
|  | digest = get_repo_digest(image_tag) | 
|  | # Just hardcode this oddity | 
|  | if image == 'rbe-toolchain': | 
|  | update_rbe_reference(digest, dry_run=args.dry_run) | 
|  | update_references(image_name, digest, dry_run=args.dry_run) |