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

As part of this change, the dev (build) and end-user test images names changed from `dangerzone.rocks/*` to `dangerzone/`, to allow using another container registry. For the needs of our CI, we use ghcr.io.
935 lines
33 KiB
Python
Executable file
935 lines
33 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
|
|
import argparse
|
|
import hashlib
|
|
import os
|
|
import pathlib
|
|
import platform
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import urllib.request
|
|
from datetime import date
|
|
|
|
DEFAULT_GUI = True
|
|
DEFAULT_USER = "user"
|
|
DEFAULT_DRY = False
|
|
DEFAULT_DEV = False
|
|
DEFAULT_SHOW_DOCKERFILE = False
|
|
DEFAULT_DOWNLOAD_PYSIDE6 = False
|
|
|
|
PYSIDE6_VERSION = "6.7.1"
|
|
PYSIDE6_RPM = "python3-pyside6-{pyside6_version}-1.fc{fedora_version}.x86_64.rpm"
|
|
PYSIDE6_URL = (
|
|
"https://packages.freedom.press/yum-tools-prod/dangerzone/f{fedora_version}/%s"
|
|
% PYSIDE6_RPM
|
|
)
|
|
|
|
PYSIDE6_DL_MESSAGE = """\
|
|
Downloading PySide6 RPM from:
|
|
|
|
{pyside6_url}
|
|
|
|
into the following local path:
|
|
|
|
{pyside6_local_path}
|
|
|
|
The RPM is over 100 MB, so this operation may take a while...
|
|
"""
|
|
|
|
PYSIDE6_NOT_FOUND_ERROR = """\
|
|
The following package is not present in your system:
|
|
|
|
{pyside6_local_path}
|
|
|
|
You can build it locally and copy it in the expected path, following the instructions
|
|
in:
|
|
|
|
https://github.com/freedomofpress/python3-pyside6-rpm
|
|
|
|
Alternatively, you can rerun the command adding the '--download-pyside6' flag, which
|
|
will download it from:
|
|
|
|
{pyside6_url}
|
|
"""
|
|
|
|
# The Linux distributions that we currently support.
|
|
# FIXME: Add a version mapping to avoid mistakes.
|
|
# FIXME: Maybe create an enum for these values.
|
|
DISTROS = ["debian", "fedora", "ubuntu"]
|
|
CONTAINER_RUNTIMES = ["podman", "docker"]
|
|
IMAGES_REGISTRY = "ghcr.io/freedomofpress/"
|
|
IMAGE_NAME_BUILD_DEV_FMT = "v2/dangerzone/build-dev/{distro}-{version}:{hash}"
|
|
IMAGE_NAME_BUILD_ENDUSER_FMT = "v2/dangerzone/end-user/{distro}-{version}:{hash}"
|
|
|
|
EPILOG = """\
|
|
Examples:
|
|
|
|
Build a Dangerzone environment for development (build-dev) or testing (build) based on
|
|
Ubuntu 22.04:
|
|
|
|
env.py --distro ubuntu --version 22.04 build-dev
|
|
env.py --distro ubuntu --version 22.04 build
|
|
|
|
Inspect the Dockerfile for the environments:
|
|
|
|
env.py --distro ubuntu --version 22.04 build-dev --show-dockerfile
|
|
env.py --distro ubuntu --version 22.04 build --show-dockerfile
|
|
|
|
Run an interactive shell in the development or end-user environment:
|
|
|
|
env.py --distro ubuntu --version 22.04 run --dev bash
|
|
env.py --distro ubuntu --version 22.04 run bash
|
|
|
|
Run Dangerzone in the development environment:
|
|
|
|
env.py --distro ubuntu --version 22.04 run --dev bash
|
|
user@dangerzone-dev:~$ cd dangerzone/
|
|
user@dangerzone-dev:~$ poetry run ./dev_scripts/dangerzone
|
|
|
|
Run Dangerzone in the end-user environment:
|
|
|
|
env.py --distro ubuntu --version 22.04 run dangerzone
|
|
|
|
"""
|
|
|
|
# NOTE: For Ubuntu 20.04 specifically, we need to install some extra deps, mainly for
|
|
# Podman. This needs to take place both in our dev and end-user environment. See the
|
|
# corresponding note in our Installation section:
|
|
#
|
|
# https://github.com/freedomofpress/dangerzone/blob/main/INSTALL.md#ubuntu-debian
|
|
DOCKERFILE_UBUNTU_2004_DEPS = r"""
|
|
ARG DEBIAN_FRONTEND=noninteractive
|
|
|
|
RUN apt-get update \
|
|
&& apt-get install -y python-all python3.9 curl wget gnupg2 \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
RUN . /etc/os-release \
|
|
&& sh -c "echo 'deb http://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_$VERSION_ID/ /' \
|
|
> /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list" \
|
|
&& wget -nv https://download.opensuse.org/repositories/devel:kubic:libcontainers:stable/xUbuntu_$VERSION_ID/Release.key -O- \
|
|
| apt-key add -
|
|
"""
|
|
|
|
# XXX: overcome the fact that ubuntu images (starting on 23.04) ship with the 'ubuntu'
|
|
# user by default https://bugs.launchpad.net/cloud-images/+bug/2005129
|
|
# Related issue https://github.com/freedomofpress/dangerzone/pull/461
|
|
DOCKERFILE_UBUNTU_REM_USER = r"""
|
|
RUN touch /var/mail/ubuntu && chown ubuntu /var/mail/ubuntu && userdel -r ubuntu
|
|
"""
|
|
|
|
# On Ubuntu Jammy, use a different conmon version, as acquired from our apt-tools-prod
|
|
# repo. For more details, read:
|
|
# https://github.com/freedomofpress/dangerzone/issues/685
|
|
DOCKERFILE_CONMON_UPDATE = r"""
|
|
RUN apt-get update \
|
|
&& apt-get install -y ca-certificates \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
COPY apt-tools-prod.sources /etc/apt/sources.list.d/
|
|
COPY apt-tools-prod.pref /etc/apt/preferences.d/
|
|
"""
|
|
|
|
# FIXME: Do we really need the python3-venv packages?
|
|
DOCKERFILE_BUILD_DEV_DEBIAN_DEPS = r"""
|
|
ARG DEBIAN_FRONTEND=noninteractive
|
|
|
|
# NOTE: Podman has several recommended packages that are actually essential for rootless
|
|
# containers. However, certain Podman versions (e.g., in Debian Trixie) bring Systemd in
|
|
# as a recommended dependency. The latter is a cause for problems, so we prefer to
|
|
# install only a subset of the recommended Podman packages. See also:
|
|
# https://github.com/freedomofpress/dangerzone/issues/689
|
|
RUN apt-get update \
|
|
&& apt-get install -y --no-install-recommends podman uidmap slirp4netns \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
RUN apt-get update \
|
|
&& apt-get install -y --no-install-recommends dh-python make build-essential \
|
|
git {qt_deps} pipx python3 python3-venv dpkg-dev debhelper python3-setuptools \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
# NOTE: `pipx install poetry` fails on Ubuntu Focal, when installed through APT. By
|
|
# installing the latest version, we sidestep this issue.
|
|
RUN bash -c 'if [[ "$(pipx --version)" < "1" ]]; then \
|
|
apt-get update \
|
|
&& apt-get remove -y pipx \
|
|
&& apt-get install -y --no-install-recommends python3-pip \
|
|
&& pip install pipx \
|
|
&& rm -rf /var/lib/apt/lists/*; \
|
|
else true; fi'
|
|
RUN apt-get update \
|
|
&& apt-get install -y --no-install-recommends mupdf thunar \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
"""
|
|
|
|
# FIXME: Install Poetry on Fedora via package manager.
|
|
DOCKERFILE_BUILD_DEV_FEDORA_DEPS = r"""
|
|
RUN dnf install -y git rpm-build podman python3 python3-devel python3-poetry-core \
|
|
pipx make qt6-qtbase-gui \
|
|
&& dnf clean all
|
|
|
|
# FIXME: Drop this fix after it's resolved upstream.
|
|
# See https://github.com/freedomofpress/dangerzone/issues/286#issuecomment-1347149783
|
|
RUN rpm --restore shadow-utils
|
|
|
|
RUN dnf install -y mupdf thunar && dnf clean all
|
|
"""
|
|
|
|
# The Dockerfile for building a development environment for Dangerzone. Parts of the
|
|
# Dockerfile will be populated during runtime.
|
|
DOCKERFILE_BUILD_DEV = r"""FROM {distro}:{version}
|
|
|
|
{install_deps}
|
|
|
|
#########################################
|
|
# Create a non-root user to run Dangerzone
|
|
RUN adduser user
|
|
# See https://github.com/freedomofpress/dangerzone/issues/286#issuecomment-1347149783
|
|
RUN echo user:2000:2000 > /etc/subuid
|
|
RUN echo user:2000:2000 > /etc/subgid
|
|
|
|
# XXX: We need the empty source folder, so that we can trick Poetry to create a
|
|
# link to the project's path. This way, we should be able to do `import
|
|
# dangerzone` from within the container.
|
|
RUN mkdir -p /home/user/dangerzone/dangerzone
|
|
RUN touch /home/user/dangerzone/dangerzone/__init__.py
|
|
|
|
USER user
|
|
WORKDIR /home/user
|
|
VOLUME /home/user/dangerzone
|
|
|
|
# Force Podman to use a specific configuration.
|
|
# See https://github.com/freedomofpress/dangerzone/issues/489
|
|
RUN mkdir -p /home/user/.config/containers
|
|
COPY storage.conf /home/user/.config/containers
|
|
|
|
# Install Poetry under ~/.local/bin.
|
|
# See https://github.com/freedomofpress/dangerzone/issues/351
|
|
# FIXME: pipx install poetry does not work for Ubuntu Focal.
|
|
ENV PATH="$PATH:/home/user/.local/bin"
|
|
RUN pipx install poetry
|
|
|
|
COPY pyproject.toml poetry.lock /home/user/dangerzone/
|
|
RUN cd /home/user/dangerzone && poetry --no-ansi install
|
|
"""
|
|
|
|
DOCKERFILE_BUILD_DEBIAN_DEPS = r"""
|
|
ARG DEBIAN_FRONTEND=noninteractive
|
|
RUN apt-get update \
|
|
&& apt-get install -y --no-install-recommends mupdf thunar \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
"""
|
|
|
|
DOCKERFILE_BUILD_FEDORA_39_DEPS = r"""
|
|
COPY {pyside6_rpm} /tmp/pyside6.rpm
|
|
RUN dnf install -y /tmp/pyside6.rpm
|
|
"""
|
|
|
|
DOCKERFILE_BUILD_FEDORA_DEPS = r"""
|
|
RUN dnf install -y mupdf thunar && dnf clean all
|
|
|
|
# FIXME: Drop this fix after it's resolved upstream.
|
|
# See https://github.com/freedomofpress/dangerzone/issues/286#issuecomment-1347149783
|
|
RUN rpm --restore shadow-utils
|
|
"""
|
|
|
|
# The Dockerfile for building an environment with Dangerzone installed in it. Parts of
|
|
# the Dockerfile will be populated during runtime.
|
|
#
|
|
# FIXME: The fact that we remove the package does not reduce the image size. We need to
|
|
# flatten the image layers as well.
|
|
DOCKERFILE_BUILD = r"""FROM {distro}:{version}
|
|
|
|
{install_deps}
|
|
|
|
COPY {package} /tmp/{package}
|
|
RUN {install_cmd} /tmp/{package}
|
|
RUN rm /tmp/{package}
|
|
|
|
#########################################
|
|
# Create a non-root user to run Dangerzone
|
|
RUN adduser user
|
|
# See https://github.com/freedomofpress/dangerzone/issues/286#issuecomment-1347149783
|
|
RUN echo user:2000:2000 > /etc/subuid
|
|
RUN echo user:2000:2000 > /etc/subgid
|
|
|
|
USER user
|
|
WORKDIR /home/user
|
|
|
|
########################################
|
|
# Force Podman to use a specific configuration.
|
|
# See https://github.com/freedomofpress/dangerzone/issues/489
|
|
RUN mkdir -p /home/user/.config/containers
|
|
COPY storage.conf /home/user/.config/containers
|
|
"""
|
|
|
|
|
|
def run(*args):
|
|
"""Simple function that runs a command, validates it, and returns the output"""
|
|
return subprocess.run(
|
|
args, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
).stdout
|
|
|
|
|
|
def git_root():
|
|
"""Get the root directory of the Git repo."""
|
|
# FIXME: Use a Git Python binding for this.
|
|
# FIXME: Make this work if called outside the repo.
|
|
path = run("git", "rev-parse", "--show-toplevel").decode().strip("\n")
|
|
return pathlib.Path(path)
|
|
|
|
|
|
def user_data():
|
|
"""Get the user data dir in (which differs on different OSes)"""
|
|
home = pathlib.Path.home()
|
|
system = platform.system()
|
|
|
|
if system == "Windows":
|
|
return home / "AppData" / "Local"
|
|
elif system == "Linux":
|
|
return home / ".local" / "share"
|
|
elif system == "Darwin":
|
|
return home / "Library" / "Application Support"
|
|
|
|
|
|
def dz_dev_root():
|
|
"""Get the directory where we will store dangerzone-dev related files"""
|
|
return user_data() / "dangerzone-dev"
|
|
|
|
|
|
def distro_root(distro, version):
|
|
"""Get the root directory for the specific Linux environment."""
|
|
return dz_dev_root() / "envs" / distro / version
|
|
|
|
|
|
def distro_state(distro, version):
|
|
"""Get the directory where we will store the state for the distro."""
|
|
return distro_root(distro, version) / "state"
|
|
|
|
|
|
def distro_build(distro, version):
|
|
"""Get the directory where we will store the build files for the distro."""
|
|
return distro_root(distro, version) / "build"
|
|
|
|
|
|
def image_name_build_dev(distro, version):
|
|
"""Get the container image for the dev variant of a Dangerzone environment."""
|
|
hash = hash_files(
|
|
get_files_in("dev_scripts")
|
|
+ [
|
|
git_root() / "pyproject.toml",
|
|
git_root() / "poetry.lock",
|
|
],
|
|
include_current_date=True,
|
|
)
|
|
|
|
return IMAGE_NAME_BUILD_DEV_FMT.format(distro=distro, version=version, hash=hash)
|
|
|
|
|
|
def image_name_build_enduser(distro, version):
|
|
"""Get the container image for the Dangerzone end-user environment."""
|
|
|
|
hash = hash_files(
|
|
get_files_in("install/linux", "debian"), include_current_date=True
|
|
)
|
|
return IMAGE_NAME_BUILD_ENDUSER_FMT.format(
|
|
distro=distro, version=version, hash=hash
|
|
)
|
|
|
|
|
|
def dz_version():
|
|
"""Get the current Dangerzone version."""
|
|
with open(git_root() / "share/version.txt") as f:
|
|
return f.read().strip()
|
|
|
|
|
|
def hash_files(
|
|
file_paths: list[pathlib.Path], include_current_date: bool = False
|
|
) -> str:
|
|
"""Returns the hash value of a list of files using the sha256 hashing algorithm.
|
|
|
|
If specified, also adds the current date in the hash.
|
|
"""
|
|
hash_obj = hashlib.new("sha256")
|
|
for path in file_paths:
|
|
with open(path, "rb") as file:
|
|
file_data = file.read()
|
|
hash_obj.update(file_data)
|
|
|
|
if include_current_date:
|
|
current_date = date.today().strftime("%Y-%m-%d")
|
|
hash_obj.update(current_date.encode())
|
|
|
|
return hash_obj.hexdigest()
|
|
|
|
|
|
def get_files_in(*folders: list[str]) -> list[pathlib.Path]:
|
|
"""Return the list of all files present in the given folders"""
|
|
files = []
|
|
for folder in folders:
|
|
files.extend([p for p in (git_root() / folder).glob("**") if p.is_file()])
|
|
return files
|
|
|
|
|
|
class PySide6Manager:
|
|
"""Provision PySide6 RPMs in our Dangerzone environments.
|
|
|
|
This class holds all the logic around checking and downloading PySide RPMs. It can
|
|
check if the required RPM version is present under "/dist", and optionally download
|
|
it.
|
|
"""
|
|
|
|
def __init__(self, distro_name, distro_version):
|
|
if distro_name != "fedora":
|
|
raise RuntimeError("Managing PySide6 RPMs is available only in Fedora")
|
|
self.distro_name = distro_name
|
|
self.distro_version = distro_version
|
|
|
|
@property
|
|
def version(self):
|
|
"""The version of the PySide6 RPM."""
|
|
return PYSIDE6_VERSION
|
|
|
|
@property
|
|
def rpm_name(self):
|
|
"""The name of the PySide6 RPM."""
|
|
return PYSIDE6_RPM.format(
|
|
pyside6_version=self.version, fedora_version=self.distro_version
|
|
)
|
|
|
|
@property
|
|
def rpm_url(self):
|
|
"""The URL of the PySide6 RPM, as hosted in FPF's RPM repo."""
|
|
return PYSIDE6_URL.format(
|
|
pyside6_version=self.version,
|
|
fedora_version=self.distro_version,
|
|
)
|
|
|
|
@property
|
|
def rpm_local_path(self):
|
|
"""The local path where this script will look for the PySide6 RPM."""
|
|
return git_root() / "dist" / self.rpm_name
|
|
|
|
@property
|
|
def is_rpm_present(self):
|
|
"""Check if PySide6 RPM is present in the user's system."""
|
|
return self.rpm_local_path.exists()
|
|
|
|
def download_rpm(self):
|
|
"""Download PySide6 from FPF's RPM repo."""
|
|
print(
|
|
PYSIDE6_DL_MESSAGE.format(
|
|
pyside6_url=self.rpm_url,
|
|
pyside6_local_path=self.rpm_local_path,
|
|
),
|
|
file=sys.stderr,
|
|
)
|
|
try:
|
|
with (
|
|
urllib.request.urlopen(self.rpm_url) as r,
|
|
open(self.rpm_local_path, "wb") as f,
|
|
):
|
|
shutil.copyfileobj(r, f)
|
|
except:
|
|
# NOTE: We purposefully catch all exceptions, since we want to catch Ctrl-C
|
|
# as well.
|
|
print("Download interrupted, removing file", file=sys.stderr)
|
|
self.rpm_local_path.unlink()
|
|
raise
|
|
print("PySide6 was downloaded successfully", file=sys.stderr)
|
|
|
|
|
|
class Env:
|
|
"""A class that implements actions on Dangerzone environments"""
|
|
|
|
def __init__(self, distro, version, runtime):
|
|
"""Initialize an Env class based on some common parameters."""
|
|
self.distro = distro
|
|
self.version = version
|
|
# NOTE: We change "bullseye" to "bullseye-backports", since it contains `pipx`,
|
|
# which is not available through the original repos.
|
|
if self.distro == "debian" and self.version in ("bullseye", "11"):
|
|
self.version = "bullseye-backports"
|
|
|
|
# Try to autodetect the runtime, if the user has not provided it.
|
|
podman_cmd = ["podman"]
|
|
docker_cmd = ["docker"]
|
|
if not runtime:
|
|
if shutil.which("podman"):
|
|
self.runtime = "podman"
|
|
self.runtime_cmd = podman_cmd
|
|
elif shutil.which("docker"):
|
|
self.runtime = "docker"
|
|
self.runtime_cmd = docker_cmd
|
|
else:
|
|
raise SystemError(
|
|
"You need either Podman or Docker installed to continue"
|
|
)
|
|
elif runtime == "podman":
|
|
self.runtime = "podman"
|
|
self.runtime_cmd = podman_cmd
|
|
elif runtime == "docker":
|
|
self.runtime = "docker"
|
|
self.runtime_cmd = docker_cmd
|
|
else:
|
|
raise RuntimeError(f"Unexpected runtime: {runtime}")
|
|
|
|
@classmethod
|
|
def from_args(cls, args):
|
|
"""Create an Env class from CLI arguments"""
|
|
return cls(distro=args.distro, version=args.version, runtime=args.runtime)
|
|
|
|
def find_dz_package(self, path, pattern):
|
|
"""Get the full path of the Dangerzone package in the specified dir.
|
|
|
|
There are times where we don't know the exact name of the Dangerzone package
|
|
that we've built, e.g., because its patch level may have changed.
|
|
|
|
Auto-detect the Dangerzone package based on a pattern that a user has provided,
|
|
and fail if there are none, or multiple matches. If there's a single match, then
|
|
return the full path for the package.
|
|
"""
|
|
matches = list(path.glob(pattern))
|
|
if len(matches) == 0:
|
|
raise RuntimeError(
|
|
f"Could not find Dangerzone package '{pattern}' in '{path}'"
|
|
)
|
|
elif len(matches) > 1:
|
|
raise RuntimeError(
|
|
f"Found more than one matches for Dangerzone package '{pattern}' in"
|
|
f" '{path}'"
|
|
)
|
|
return matches[0]
|
|
|
|
def runtime_run(self, *args):
|
|
"""Run a command for a specific container runtime.
|
|
|
|
A user's environment may have more than one container runtime [1], e.g., Podman
|
|
or Docker. These two runtimes have the same interface, so we can use them
|
|
interchangeably.
|
|
|
|
This method expects a command to run, minus the "docker" / "podman" part. Since
|
|
the command can be any valid command, such as "run" or "build", we can't assume
|
|
anything about the standard streams, so we don't affect them at all.
|
|
|
|
[1]: Technically, a container runtime is a program that implements the Container
|
|
Runtime Interface. We overload this term here, in lieu of a better one.
|
|
"""
|
|
subprocess.run(self.runtime_cmd + list(args), check=True)
|
|
|
|
def run(
|
|
self, cmd, gui=DEFAULT_GUI, user=DEFAULT_USER, dry=DEFAULT_DRY, dev=DEFAULT_DEV
|
|
):
|
|
"""Run a command in a Dangerzone environment."""
|
|
# FIXME: Allow wiping the state of the distro before running the environment, to
|
|
# ensure reproducibility.
|
|
|
|
run_cmd = [
|
|
"run",
|
|
"--rm",
|
|
"-it",
|
|
"-v",
|
|
"/etc/localtime:/etc/localtime:ro",
|
|
# FIXME: Find a more secure invocation.
|
|
"--security-opt",
|
|
"seccomp=unconfined",
|
|
"--privileged",
|
|
]
|
|
|
|
# We need to retain our UID, because we are mounting the Dangerzone source to
|
|
# the container.
|
|
if self.runtime == "podman":
|
|
uidmaps = [
|
|
"--uidmap",
|
|
"1000:0:1",
|
|
"--uidmap",
|
|
"0:1:1000",
|
|
"--uidmap",
|
|
"1001:1001:64536",
|
|
]
|
|
gidmaps = [
|
|
"--gidmap",
|
|
"1000:0:1",
|
|
"--gidmap",
|
|
"0:1:1000",
|
|
"--gidmap",
|
|
"1001:1001:64536",
|
|
]
|
|
run_cmd += uidmaps + gidmaps
|
|
|
|
# Compute container runtime arguments for GUI purposes.
|
|
if gui:
|
|
# Detect X11 display server connection settings.
|
|
env_display = os.environ.get("DISPLAY")
|
|
env_xauthority = os.environ.get("XAUTHORITY")
|
|
run_cmd += [
|
|
"-e",
|
|
f"DISPLAY={env_display}",
|
|
"-v",
|
|
"/tmp/.X11-unix:/tmp/.X11-unix:ro",
|
|
]
|
|
|
|
if env_xauthority:
|
|
run_cmd += [
|
|
"-e",
|
|
f"XAUTHORITY={env_xauthority}",
|
|
"-v",
|
|
f"{env_xauthority}:{env_xauthority}:ro",
|
|
]
|
|
|
|
# FIXME: Detect Wayland connection settings. This requires some extra
|
|
# settings, as we can see in this link:
|
|
#
|
|
# https://github.com/mviereck/x11docker/wiki/How-to-provide-Wayland-socket-to-docker-container
|
|
|
|
# Mount the source and the state of the distro into the container
|
|
dz_src = git_root()
|
|
dist_state = distro_state(self.distro, self.version)
|
|
run_cmd += [
|
|
"-v",
|
|
f"{dz_src}:/home/user/dangerzone",
|
|
"-v",
|
|
f"{dist_state}/containers:/home/user/.local/share/containers",
|
|
"-v",
|
|
f"{dist_state}/.bash_history:/home/user/.bash_history",
|
|
]
|
|
|
|
run_cmd += ["-u", user]
|
|
|
|
# Select the proper container image based on whether the user wants to run the
|
|
# command in a dev or end-user environment.
|
|
if dev:
|
|
run_cmd += [
|
|
"--hostname",
|
|
"dangerzone-dev",
|
|
image_name_build_dev(self.distro, self.version),
|
|
]
|
|
else:
|
|
run_cmd += [
|
|
"--hostname",
|
|
"dangerzone",
|
|
image_name_build_enduser(self.distro, self.version),
|
|
]
|
|
|
|
run_cmd += cmd
|
|
|
|
# If the user has asked to perform a dry-run, then print the command that the
|
|
# script would use internally.
|
|
if dry:
|
|
print(" ".join(self.runtime_cmd + list(run_cmd)))
|
|
return
|
|
|
|
dist_state.mkdir(parents=True, exist_ok=True)
|
|
(dist_state / "containers").mkdir(exist_ok=True)
|
|
(dist_state / ".bash_history").touch(exist_ok=True)
|
|
self.runtime_run(*run_cmd)
|
|
|
|
def pull_image_from_registry(self, image):
|
|
process = subprocess.run(self.runtime_cmd + ["pull", IMAGES_REGISTRY + image])
|
|
return process.returncode == 0
|
|
|
|
def push_image_to_registry(self, image):
|
|
process = subprocess.run(
|
|
self.runtime_cmd + ["push", image, IMAGES_REGISTRY + image]
|
|
)
|
|
return process.returncode == 0
|
|
|
|
def build_dev(self, show_dockerfile=DEFAULT_SHOW_DOCKERFILE, sync=False):
|
|
"""Build a Linux environment and install tools for Dangerzone development."""
|
|
image = image_name_build_dev(self.distro, self.version)
|
|
|
|
if sync and self.pull_image_from_registry(image):
|
|
# The image has been pulled from the registry, no need to build it.
|
|
return
|
|
elif sync:
|
|
print("Image label not in registry, building it")
|
|
|
|
if self.distro == "fedora":
|
|
install_deps = DOCKERFILE_BUILD_DEV_FEDORA_DEPS
|
|
else:
|
|
# Use Qt6 in all of our Linux dev environments, and add a missing
|
|
# libxcb-cursor0 dependency
|
|
#
|
|
# See https://github.com/freedomofpress/dangerzone/issues/482
|
|
qt_deps = "libqt6gui6 libxcb-cursor0"
|
|
install_deps = DOCKERFILE_BUILD_DEV_DEBIAN_DEPS
|
|
if self.distro == "ubuntu" and self.version in ("20.04", "focal"):
|
|
qt_deps = "libqt5gui5 libxcb-cursor0" # Ubuntu Focal has only Qt5.
|
|
install_deps = (
|
|
DOCKERFILE_UBUNTU_2004_DEPS + DOCKERFILE_BUILD_DEV_DEBIAN_DEPS
|
|
)
|
|
elif self.distro == "ubuntu" and self.version in ("22.04", "jammy"):
|
|
# Ubuntu Jammy misses a dependency to `libxkbcommon-x11-0`, which we can
|
|
# install indirectly via `qt6-qpa-plugins`.
|
|
qt_deps += " qt6-qpa-plugins"
|
|
# Ubuntu Jammy requires a more up-to-date conmon package
|
|
# (see https://github.com/freedomofpress/dangerzone/issues/685)
|
|
install_deps = (
|
|
DOCKERFILE_CONMON_UPDATE + DOCKERFILE_BUILD_DEV_DEBIAN_DEPS
|
|
)
|
|
elif self.distro == "ubuntu" and self.version in (
|
|
"23.10",
|
|
"mantic",
|
|
"24.04",
|
|
"noble",
|
|
):
|
|
install_deps = (
|
|
DOCKERFILE_UBUNTU_REM_USER + DOCKERFILE_BUILD_DEV_DEBIAN_DEPS
|
|
)
|
|
elif self.distro == "debian" and self.version in ("bullseye-backports",):
|
|
# Debian Bullseye misses a dependency to libgl1.
|
|
qt_deps += " libgl1"
|
|
|
|
install_deps = install_deps.format(qt_deps=qt_deps)
|
|
|
|
dockerfile = DOCKERFILE_BUILD_DEV.format(
|
|
distro=self.distro, version=self.version, install_deps=install_deps
|
|
)
|
|
if show_dockerfile:
|
|
print(dockerfile)
|
|
return
|
|
|
|
build_dir = distro_build(self.distro, self.version)
|
|
os.makedirs(build_dir, exist_ok=True)
|
|
|
|
# Populate the build context.
|
|
shutil.copy(git_root() / "pyproject.toml", build_dir)
|
|
shutil.copy(git_root() / "poetry.lock", build_dir)
|
|
shutil.copy(git_root() / "dev_scripts" / "storage.conf", build_dir)
|
|
shutil.copy(git_root() / "dev_scripts" / "containers.conf", build_dir)
|
|
if self.distro == "ubuntu" and self.version in ("22.04", "jammy"):
|
|
shutil.copy(git_root() / "dev_scripts" / "apt-tools-prod.pref", build_dir)
|
|
shutil.copy(
|
|
git_root() / "dev_scripts" / "apt-tools-prod.sources", build_dir
|
|
)
|
|
with open(build_dir / "Dockerfile", mode="w") as f:
|
|
f.write(dockerfile)
|
|
|
|
self.runtime_run("build", "-t", image, build_dir)
|
|
|
|
if sync:
|
|
if not self.push_image_to_registry(image):
|
|
print("An error occured while trying to push to the container registry")
|
|
|
|
def build(
|
|
self,
|
|
show_dockerfile=DEFAULT_SHOW_DOCKERFILE,
|
|
download_pyside6=DEFAULT_DOWNLOAD_PYSIDE6,
|
|
):
|
|
"""Build a Linux environment and install Dangerzone in it."""
|
|
build_dir = distro_build(self.distro, self.version)
|
|
os.makedirs(build_dir, exist_ok=True)
|
|
version = dz_version()
|
|
if self.distro == "fedora":
|
|
install_deps = DOCKERFILE_BUILD_FEDORA_DEPS
|
|
package_pattern = f"dangerzone-{version}-*.fc{self.version}.x86_64.rpm"
|
|
package_src = self.find_dz_package(git_root() / "dist", package_pattern)
|
|
package = package_src.name
|
|
package_dst = build_dir / package
|
|
install_cmd = "dnf install -y"
|
|
|
|
# NOTE: For Fedora 39, we check if a PySide6 RPM package exists in
|
|
# the user's system. If not, we either throw an error or download it from
|
|
# FPF's repo, according to the user's choice.
|
|
if self.version == "39":
|
|
pyside6 = PySide6Manager(self.distro, self.version)
|
|
if not pyside6.is_rpm_present:
|
|
if download_pyside6:
|
|
pyside6.download_rpm()
|
|
else:
|
|
print(
|
|
PYSIDE6_NOT_FOUND_ERROR.format(
|
|
pyside6_local_path=pyside6.rpm_local_path,
|
|
pyside6_url=pyside6.rpm_url,
|
|
),
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
shutil.copy(pyside6.rpm_local_path, build_dir / pyside6.rpm_name)
|
|
install_deps = (
|
|
DOCKERFILE_BUILD_FEDORA_DEPS + DOCKERFILE_BUILD_FEDORA_39_DEPS
|
|
).format(pyside6_rpm=pyside6.rpm_name)
|
|
else:
|
|
install_deps = DOCKERFILE_BUILD_DEBIAN_DEPS
|
|
if self.distro == "ubuntu" and self.version in ("20.04", "focal"):
|
|
install_deps = (
|
|
DOCKERFILE_UBUNTU_2004_DEPS + DOCKERFILE_BUILD_DEBIAN_DEPS
|
|
)
|
|
elif self.distro == "ubuntu" and self.version in ("22.04", "jammy"):
|
|
# Ubuntu Jammy requires a more up-to-date conmon
|
|
# package (see https://github.com/freedomofpress/dangerzone/issues/685)
|
|
install_deps = DOCKERFILE_CONMON_UPDATE + DOCKERFILE_BUILD_DEBIAN_DEPS
|
|
elif self.distro == "ubuntu" and self.version in (
|
|
"23.10",
|
|
"mantic",
|
|
"24.04",
|
|
"noble",
|
|
):
|
|
install_deps = DOCKERFILE_UBUNTU_REM_USER + DOCKERFILE_BUILD_DEBIAN_DEPS
|
|
package_pattern = f"dangerzone_{version}-*_*.deb"
|
|
package_src = self.find_dz_package(git_root() / "deb_dist", package_pattern)
|
|
package = package_src.name
|
|
package_dst = build_dir / package
|
|
install_cmd = "apt-get update && apt-get install -y"
|
|
|
|
dockerfile = DOCKERFILE_BUILD.format(
|
|
distro=self.distro,
|
|
version=self.version,
|
|
install_cmd=install_cmd,
|
|
package=package,
|
|
install_deps=install_deps,
|
|
)
|
|
if show_dockerfile:
|
|
print(dockerfile)
|
|
return
|
|
|
|
# Populate the build context.
|
|
shutil.copy(package_src, package_dst)
|
|
shutil.copy(git_root() / "dev_scripts" / "storage.conf", build_dir)
|
|
shutil.copy(git_root() / "dev_scripts" / "containers.conf", build_dir)
|
|
if self.distro == "ubuntu" and self.version in ("22.04", "jammy"):
|
|
shutil.copy(git_root() / "dev_scripts" / "apt-tools-prod.pref", build_dir)
|
|
shutil.copy(
|
|
git_root() / "dev_scripts" / "apt-tools-prod.sources", build_dir
|
|
)
|
|
with open(build_dir / "Dockerfile", mode="w") as f:
|
|
f.write(dockerfile)
|
|
|
|
image = image_name_build_enduser(self.distro, self.version)
|
|
self.runtime_run("build", "-t", image, build_dir)
|
|
|
|
|
|
def env_run(args):
|
|
"""Invoke the 'run' command based on the CLI args."""
|
|
if not args.command:
|
|
print("Please provide a command for the environment")
|
|
sys.exit(1)
|
|
|
|
env = Env.from_args(args)
|
|
return env.run(
|
|
args.command, gui=args.gui, user=args.user, dry=args.dry, dev=args.dev
|
|
)
|
|
|
|
|
|
def env_build_dev(args):
|
|
"""Invoke the 'build-dev' command based on the CLI args."""
|
|
env = Env.from_args(args)
|
|
return env.build_dev(show_dockerfile=args.show_dockerfile, sync=args.sync)
|
|
|
|
|
|
def env_build(args):
|
|
"""Invoke the 'build' command based on the CLI args."""
|
|
env = Env.from_args(args)
|
|
return env.build(
|
|
show_dockerfile=args.show_dockerfile,
|
|
download_pyside6=args.download_pyside6,
|
|
)
|
|
|
|
|
|
def parse_args():
|
|
parser = argparse.ArgumentParser(
|
|
prog=sys.argv[0],
|
|
description="Dev script for handling Dangerzone environments",
|
|
epilog=EPILOG,
|
|
formatter_class=argparse.RawTextHelpFormatter,
|
|
)
|
|
parser.add_argument(
|
|
"--distro",
|
|
choices=DISTROS,
|
|
required=True,
|
|
help="The name of the Linux distro",
|
|
)
|
|
parser.add_argument(
|
|
"--version",
|
|
required=True,
|
|
help="The version of the Linux distro",
|
|
)
|
|
parser.add_argument(
|
|
"--runtime",
|
|
choices=CONTAINER_RUNTIMES,
|
|
help="The name of the container runtime",
|
|
)
|
|
subparsers = parser.add_subparsers(help="Available subcommands")
|
|
subparsers.required = True
|
|
|
|
# Run a command in an environment.
|
|
parser_run = subparsers.add_parser(
|
|
"run", help="Run a command in a Dangerzone environment"
|
|
)
|
|
parser_run.set_defaults(func=env_run)
|
|
parser_run.add_argument(
|
|
"--no-gui",
|
|
default=DEFAULT_GUI,
|
|
action="store_false",
|
|
dest="gui",
|
|
help="Run command with GUI support",
|
|
)
|
|
parser_run.add_argument(
|
|
"--user",
|
|
"-u",
|
|
default=DEFAULT_USER,
|
|
help="Run command as user USER",
|
|
)
|
|
parser_run.add_argument(
|
|
"--dry",
|
|
default=DEFAULT_DRY,
|
|
action="store_true",
|
|
help="Do not run the command, just print it with the container invocation",
|
|
)
|
|
parser_run.add_argument(
|
|
"--dev",
|
|
default=DEFAULT_DEV,
|
|
action="store_true",
|
|
help="Run the command into the dev variant of the Dangerzone environment",
|
|
)
|
|
parser_run.add_argument(
|
|
"command",
|
|
nargs=argparse.REMAINDER,
|
|
help="Run command COMMAND in the Dangerzone environment",
|
|
)
|
|
|
|
# Build a development variant of a Dangerzone environment.
|
|
parser_build_dev = subparsers.add_parser(
|
|
"build-dev",
|
|
help="Build a Linux environment and install tools for Dangerzone development",
|
|
)
|
|
parser_build_dev.set_defaults(func=env_build_dev)
|
|
parser_build_dev.add_argument(
|
|
"--show-dockerfile",
|
|
default=DEFAULT_SHOW_DOCKERFILE,
|
|
action="store_true",
|
|
help="Do not build, only show the Dockerfile",
|
|
)
|
|
parser_build_dev.add_argument(
|
|
"--sync",
|
|
default=False,
|
|
action="store_true",
|
|
help="Attempt to pull the image, build it if not found and push it to the container registry",
|
|
)
|
|
|
|
# Build a development variant of a Dangerzone environment.
|
|
parser_build = subparsers.add_parser(
|
|
"build",
|
|
help="Build a Linux environment and install Dangerzone",
|
|
)
|
|
parser_build.set_defaults(func=env_build)
|
|
parser_build.add_argument(
|
|
"--show-dockerfile",
|
|
default=DEFAULT_SHOW_DOCKERFILE,
|
|
action="store_true",
|
|
help="Do not build, only show the Dockerfile",
|
|
)
|
|
parser_build.add_argument(
|
|
"--download-pyside6",
|
|
default=DEFAULT_DOWNLOAD_PYSIDE6,
|
|
action="store_true",
|
|
help="Download PySide6 from FPF's RPM repo",
|
|
)
|
|
|
|
return parser.parse_args()
|
|
|
|
|
|
def main():
|
|
args = parse_args()
|
|
return args.func(args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|