blob: 3eea961bbc00b1a20acbff88be39e10d14dd449f [file] [log] [blame]
#!/usr/bin/env python3
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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
#
# http://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.
"""
This script cooridnates other scripts to put together a release.
Generated images will have a label in the form apache/heron:<tag> and will be placed in the //distro/ directory.
## Examples
List available target distrobutions on separate stdout lines:
./docker-images
Build and tag a single distorbution image then print where the archive's path:
./docker-images build 0.1.0-debian11 debian11
Build and tag all distrobution images then print each archive's path:
./docker-images build "$(git describe --tags)" --all
"""
from pathlib import Path
import logging
import re
import shutil
import subprocess
import sys
import tempfile
import typing
ROOT = Path(__file__).resolve().parent.parent.parent
BUILD_ARTIFACTS = ROOT / "docker/scripts/build-artifacts.sh"
BUILD_IMAGE = ROOT / "docker/scripts/build-docker.sh"
class BuildFailure(Exception):
"""Raised to indicate a failure buliding."""
class BadDistrobutionName(BuildFailure):
"""Raised when a bad distrobution name is provided."""
def configure_logging(debug: bool):
"""Use standard logging config and write to stdout and a logfile."""
logging.basicConfig(
format="[%(asctime)s] %(levelname)s: %(message)s",
level=(logging.DEBUG if debug else logging.INFO),
)
def log_run(args: typing.List[str], log: typing.IO[str]) -> subprocess.CompletedProcess:
"""Run an executable and direct its output to the given log file."""
return subprocess.run(
args, stdout=log, stderr=log, universal_newlines=True, check=True
)
def build_dockerfile(
scratch: Path, dist: str, tag: str, out_dir: Path, log: typing.IO[str]
) -> Path:
"""
Raises CalledProcessError if either of the external scripts fail.
"""
logging.info("building package for %s", dist)
log_run([str(BUILD_ARTIFACTS), dist, tag, scratch], log)
logging.info("building docker image for %s", dist)
log_run([str(BUILD_IMAGE), dist, tag, scratch], log)
tar = Path(scratch) / f"heron-docker-{tag}-{dist}.tar.gz"
tar_out = out_dir / tar.name
tar.replace(tar_out)
logging.info("docker image complete: %s", tar_out)
return tar_out
def available_distrobutions() -> typing.List[str]:
"""Return a list of available target distrobutions."""
compile_files = (ROOT / "docker/compile").glob("Dockerfile.*")
dist_files = (ROOT / "docker/dist").glob("Dockerfile.dist.*")
compile_distros = {re.sub(r"^Dockerfile\.", "", f.name) for f in compile_files}
dist_distros = {re.sub(r"^Dockerfile\.dist\.", "", f.name) for f in dist_files}
distros = compile_distros & dist_distros
mismatch = (compile_distros | dist_distros) ^ distros
if mismatch:
logging.warning(
"docker distros found without both compile+dist files: %s", mismatch
)
return sorted(distros)
def build_target(tag: str, target: str) -> typing.List[Path]:
"""Build docker images for the given target distrobutions."""
debug = True
distros = available_distrobutions()
logging.debug("available distro targets: %s", distros)
if target == "--all":
targets = distros
elif target not in distros:
raise BadDistrobutionName(f"distrobution {target!r} does not exist")
else:
targets = [target]
out_dir = ROOT / "dist"
out_dir.mkdir(exist_ok=True)
for target in targets:
scratch = Path(tempfile.mkdtemp(prefix=f"build-{target}-"))
log_path = scratch / "log.txt"
log = log_path.open("w")
logging.debug("building %s", target)
try:
tar = build_dockerfile(scratch, target, tag, out_dir, log)
except Exception as e:
logging.error(
"an error occurred building %s. See log in %s", target, log_path
)
if isinstance(e, subprocess.CalledProcessError):
raise BuildFailure("failure in underlying build scripts") from e
raise
if not debug:
shutil.rmtree(scratch)
yield tar
def cli(args=sys.argv):
operation = sys.argv[1]
if operation == "list":
print("\n".join(available_distrobutions()))
elif operation == "build":
tag, target = sys.argv[2:]
try:
for archive in build_target(tag=tag, target=target):
print(archive)
except BuildFailure as e:
logging.error(e)
pass
else:
logging.error("unknown operation %r", operation)
if __name__ == "__main__":
configure_logging(debug=True)
cli()