mirror of
https://github.com/freedomofpress/dangerzone.git
synced 2025-04-28 18:02:38 +02:00

Build Dangerzone images and tag them with a unique ID that stems from the Git reop. Note that using tags as image IDs instead of regular image IDs breaks the current Dangerzone expectations, but this will be addressed in subsequent commits.
161 lines
5.2 KiB
Python
161 lines
5.2 KiB
Python
import argparse
|
|
import gzip
|
|
import os
|
|
import platform
|
|
import secrets
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
BUILD_CONTEXT = "dangerzone/"
|
|
IMAGE_NAME = "dangerzone.rocks/dangerzone"
|
|
REQUIREMENTS_TXT = "container-pip-requirements.txt"
|
|
if platform.system() in ["Darwin", "Windows"]:
|
|
CONTAINER_RUNTIME = "docker"
|
|
elif platform.system() == "Linux":
|
|
CONTAINER_RUNTIME = "podman"
|
|
|
|
ARCH = platform.machine()
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument(
|
|
"--runtime",
|
|
choices=["docker", "podman"],
|
|
default=CONTAINER_RUNTIME,
|
|
help=f"The container runtime for building the image (default: {CONTAINER_RUNTIME})",
|
|
)
|
|
parser.add_argument(
|
|
"--no-save",
|
|
action="store_true",
|
|
help="Do not save the container image as a tarball in share/container.tar.gz",
|
|
)
|
|
parser.add_argument(
|
|
"--compress-level",
|
|
type=int,
|
|
choices=range(0, 10),
|
|
default=9,
|
|
help="The Gzip compression level, from 0 (lowest) to 9 (highest, default)",
|
|
)
|
|
parser.add_argument(
|
|
"--use-cache",
|
|
action="store_true",
|
|
help="Use the builder's cache to speed up the builds (not suitable for release builds)",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
tarball_path = Path("share") / "container.tar.gz"
|
|
image_id_path = Path("share") / "image-id.txt"
|
|
|
|
print(f"Building for architecture '{ARCH}'")
|
|
|
|
# Designate a unique tag for this image, depending on the Git commit it was created
|
|
# from:
|
|
# 1. If created from a Git tag (e.g., 0.8.0), the image tag will be `0.8.0`.
|
|
# 2. If created from a commit, it will be something like `0.8.0-31-g6bdaa7a`.
|
|
# 3. If the contents of the Git repo are dirty, we will append a unique identifier
|
|
# for this run, something like `0.8.0-31-g6bdaa7a-fdcb` or `0.8.0-fdcb`.
|
|
dirty_ident = secrets.token_hex(2)
|
|
tag = (
|
|
subprocess.check_output(
|
|
["git", "describe", "--first-parent", f"--dirty=-{dirty_ident}"],
|
|
)
|
|
.decode()
|
|
.strip()[1:] # remove the "v" prefix of the tag.
|
|
)
|
|
image_name_tagged = IMAGE_NAME + ":" + tag
|
|
|
|
print(f"Will tag the container image as '{image_name_tagged}'")
|
|
with open(image_id_path, "w") as f:
|
|
f.write(tag)
|
|
|
|
print("Exporting container pip dependencies")
|
|
with ContainerPipDependencies():
|
|
if not args.use_cache:
|
|
print("Pulling base image")
|
|
subprocess.run(
|
|
[
|
|
args.runtime,
|
|
"pull",
|
|
"alpine:latest",
|
|
],
|
|
check=True,
|
|
)
|
|
|
|
# Build the container image, and tag it with the calculated tag
|
|
print("Building container image")
|
|
cache_args = [] if args.use_cache else ["--no-cache"]
|
|
subprocess.run(
|
|
[
|
|
args.runtime,
|
|
"build",
|
|
BUILD_CONTEXT,
|
|
*cache_args,
|
|
"--build-arg",
|
|
f"REQUIREMENTS_TXT={REQUIREMENTS_TXT}",
|
|
"--build-arg",
|
|
f"ARCH={ARCH}",
|
|
"-f",
|
|
"Dockerfile",
|
|
"--tag",
|
|
image_name_tagged,
|
|
],
|
|
check=True,
|
|
)
|
|
|
|
if not args.no_save:
|
|
print("Saving container image")
|
|
cmd = subprocess.Popen(
|
|
[
|
|
CONTAINER_RUNTIME,
|
|
"save",
|
|
image_name_tagged,
|
|
],
|
|
stdout=subprocess.PIPE,
|
|
)
|
|
|
|
print("Compressing container image")
|
|
chunk_size = 4 << 20
|
|
with gzip.open(
|
|
tarball_path,
|
|
"wb",
|
|
compresslevel=args.compress_level,
|
|
) as gzip_f:
|
|
while True:
|
|
chunk = cmd.stdout.read(chunk_size)
|
|
if len(chunk) > 0:
|
|
gzip_f.write(chunk)
|
|
else:
|
|
break
|
|
cmd.wait(5)
|
|
|
|
|
|
class ContainerPipDependencies:
|
|
"""Generates PIP dependencies within container"""
|
|
|
|
def __enter__(self):
|
|
try:
|
|
container_requirements_txt = subprocess.check_output(
|
|
["poetry", "export", "--only", "container"], universal_newlines=True
|
|
)
|
|
except subprocess.CalledProcessError as e:
|
|
print("FAILURE", e.returncode, e.output)
|
|
print(f"REQUIREMENTS: {container_requirements_txt}")
|
|
# XXX Export container dependencies and exclude pymupdfb since it is not needed in container
|
|
req_txt_pymupdfb_stripped = container_requirements_txt.split("pymupdfb")[0]
|
|
with open(Path(BUILD_CONTEXT) / REQUIREMENTS_TXT, "w") as f:
|
|
if ARCH == "arm64":
|
|
# PyMuPDF needs to be built on ARM64 machines
|
|
# But is already provided as a prebuilt-wheel on other architectures
|
|
f.write(req_txt_pymupdfb_stripped)
|
|
else:
|
|
f.write(container_requirements_txt)
|
|
|
|
def __exit__(self, exc_type, exc_value, exc_tb):
|
|
print("Leaving the context...")
|
|
os.remove(Path(BUILD_CONTEXT) / REQUIREMENTS_TXT)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|