blob: e8c7c8bebedbe7557c6bda07761b87929c676864 [file] [log] [blame]
#!/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)