mirror of
https://github.com/freedomofpress/dangerzone.git
synced 2025-04-28 18:02:38 +02:00
910 lines
29 KiB
Python
Executable file
910 lines
29 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
|
|
import abc
|
|
import argparse
|
|
import difflib
|
|
import logging
|
|
import re
|
|
import selectors
|
|
import subprocess
|
|
import sys
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
CONTENT_QA = r"""## QA
|
|
|
|
To ensure that new releases do not introduce regressions, and support existing
|
|
and newer platforms, we have to do the following:
|
|
|
|
- [ ] In `.circleci/config.yml`, add new platforms and remove obsolete platforms
|
|
- [ ] Make sure that the tip of the `main` branch passes the CI tests.
|
|
- [ ] Create a test build in Windows and make sure it works:
|
|
- [ ] Create a new development environment with Poetry.
|
|
- [ ] Build the container image and ensure the development environment uses
|
|
the new image.
|
|
- [ ] Run the Dangerzone tests.
|
|
- [ ] Build and run the Dangerzone .exe
|
|
- [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below).
|
|
- [ ] Create a test build in macOS (Intel CPU) and make sure it works:
|
|
- [ ] Create a new development environment with Poetry.
|
|
- [ ] Build the container image and ensure the development environment uses
|
|
the new image.
|
|
- [ ] Run the Dangerzone tests.
|
|
- [ ] Create and run an app bundle.
|
|
- [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below).
|
|
- [ ] Create a test build in macOS (M1/2 CPU) and make sure it works:
|
|
- [ ] Create a new development environment with Poetry.
|
|
- [ ] Build the container image and ensure the development environment uses
|
|
the new image.
|
|
- [ ] Run the Dangerzone tests.
|
|
- [ ] Create and run an app bundle.
|
|
- [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below).
|
|
- [ ] Create a test build in the most recent Ubuntu LTS platform (Ubuntu 22.04
|
|
as of writing this) and make sure it works:
|
|
- [ ] Create a new development environment with Poetry.
|
|
- [ ] Build the container image and ensure the development environment uses
|
|
the new image.
|
|
- [ ] Run the Dangerzone tests.
|
|
- [ ] Create a .deb package and install it system-wide.
|
|
- [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below).
|
|
- [ ] Create a test build in the most recent Fedora platform (Fedora 38 as of
|
|
writing this) and make sure it works:
|
|
- [ ] Create a new development environment with Poetry.
|
|
- [ ] Build the container image and ensure the development environment uses
|
|
the new image.
|
|
- [ ] Run the Dangerzone tests.
|
|
- [ ] Create an .rpm package and install it system-wide.
|
|
- [ ] Test some QA scenarios (see [Scenarios](#Scenarios) below).
|
|
|
|
"""
|
|
|
|
CONTENT_QA_SCENARIOS = r"""### Scenarios
|
|
|
|
#### 1. Dangerzone correctly identifies that Docker/Podman is not installed
|
|
|
|
_(Only for MacOS / Windows)_
|
|
|
|
Temporarily hide the Docker/Podman binaries, e.g., rename the `docker` /
|
|
`podman` binaries to something else. Then run Dangerzone. Dangerzone should
|
|
prompt the user to install Docker/Podman.
|
|
|
|
#### 2. Dangerzone correctly identifies that Docker is not running
|
|
|
|
_(Only for MacOS / Windows)_
|
|
|
|
Stop the Docker Desktop application. Then run Dangerzone. Dangerzone should
|
|
prompt the user to start Docker Desktop.
|
|
|
|
#### 3. Dangerzone successfully installs the container image
|
|
|
|
Remove the Dangerzone container image from Docker/Podman. Then run Dangerzone.
|
|
Danerzone should install the container image successfully.
|
|
|
|
#### 4. Dangerzone retains the settings of previous runs
|
|
|
|
Run Dangerzone and make some changes in the settings (e.g., change the OCR
|
|
language, toggle whether to open the document after conversion, etc.). Restart
|
|
Dangerzone. Dangerzone should show the settings that the user chose.
|
|
|
|
#### 5. Dangerzone reports failed conversions
|
|
|
|
Run Dangerzone and convert the `tests/test_docs/sample_bad_pdf.pdf` document.
|
|
Dangerzone should fail gracefully, by reporting that the operation failed, and
|
|
showing the last error message.
|
|
|
|
#### 6. Dangerzone succeeds in converting multiple documents
|
|
|
|
Run Dangerzone against a list of documents, and tick all options. Ensure that:
|
|
* Conversions take place sequentially.
|
|
* Attempting to close the window while converting asks the user if they want to
|
|
abort the conversions.
|
|
* Conversions are completed successfully.
|
|
* Conversions show individual progress.
|
|
* _(Only for Linux)_ The resulting files open with the PDF viewer of our choice.
|
|
* OCR seems to have detected characters in the PDF files.
|
|
* The resulting files have been saved with the proper suffix, in the proper
|
|
location.
|
|
* The original files have been saved in the `unsafe/` directory.
|
|
|
|
#### 7. Dangerzone CLI succeeds in converting multiple documents
|
|
|
|
_(Only for Windows and Linux)_
|
|
|
|
Run Dangerzone CLI against a list of documents. Ensure that conversions happen
|
|
sequentially, are completed successfully, and we see their progress.
|
|
|
|
#### 8. Dangerzone can open a document for conversion via right-click -> "Open With"
|
|
|
|
_(Only for Windows and MacOS)_
|
|
|
|
Go to a directory with office documents, right-click on one, and click on "Open
|
|
With". We should be able to open the file with Dangerzone, and then convert it.
|
|
|
|
#### 9. Updating Dangerzone handles external state correctly.
|
|
|
|
_(Applies to Linux/Windows/MacOS. For MacOS/Windows, it requires an installer
|
|
for the new version)_
|
|
|
|
Install the previous version of Dangerzone system-wide. Open the Dangerzone
|
|
application and enable some non-default settings. Close the Dangerzone
|
|
application and get the container image for that version. For example
|
|
|
|
```
|
|
$ podman images dangerzone.rocks/dangerzone:latest
|
|
REPOSITORY TAG IMAGE ID CREATED SIZE
|
|
dangerzone.rocks/dangerzone latest <image ID> <date> <size>
|
|
```
|
|
|
|
_(use `docker` on Windows/MacOS)_
|
|
|
|
Install the new version of Dangerzone system-wide. Open the Dangerzone
|
|
application and make sure that the previously enabled settings still show up.
|
|
Also, ensure that Dangerzone reports that the new image has been installed, and
|
|
verify that it's different from the old one by doing:
|
|
|
|
```
|
|
$ podman images dangerzone.rocks/dangerzone:latest
|
|
REPOSITORY TAG IMAGE ID CREATED SIZE
|
|
dangerzone.rocks/dangerzone latest <different ID> <newer date> <different size>
|
|
```
|
|
"""
|
|
|
|
CONTENT_BUILD_DEBIAN_UBUNTU = r"""## Debian/Ubuntu
|
|
|
|
Install dependencies:
|
|
|
|
```sh
|
|
sudo apt install -y podman dh-python build-essential fakeroot make libqt5gui5 \
|
|
pipx python3 python3-dev python3-stdeb python3-all
|
|
```
|
|
|
|
Install Poetry using `pipx` (recommended) and add it to your `$PATH`:
|
|
|
|
_(See also a list of [alternative installation
|
|
methods](https://python-poetry.org/docs/#installation))_
|
|
|
|
```sh
|
|
pipx ensurepath
|
|
pipx install poetry
|
|
```
|
|
|
|
After this, restart the terminal window, for the `poetry` command to be in your
|
|
`$PATH`.
|
|
|
|
Change to the `dangerzone` folder, and install the poetry dependencies:
|
|
|
|
> **Note**: due to an issue with [poetry](https://github.com/python-poetry/poetry/issues/1917), if it prompts for your keyring, disable the keyring with `keyring --disable` and run the command again.
|
|
|
|
```
|
|
poetry install
|
|
```
|
|
|
|
Build the latest container:
|
|
|
|
```sh
|
|
./install/linux/build-image.sh
|
|
```
|
|
|
|
Run from source tree:
|
|
|
|
```sh
|
|
# start a shell in the virtual environment
|
|
poetry shell
|
|
|
|
# run the CLI
|
|
./dev_scripts/dangerzone-cli --help
|
|
|
|
# run the GUI
|
|
./dev_scripts/dangerzone
|
|
```
|
|
|
|
Create a .deb:
|
|
|
|
```sh
|
|
./install/linux/build-deb.py
|
|
```
|
|
"""
|
|
|
|
CONTENT_BUILD_FEDORA = r"""## Fedora
|
|
|
|
Install dependencies:
|
|
|
|
```sh
|
|
sudo dnf install -y rpm-build podman python3 pipx qt5-qtbase-gui
|
|
```
|
|
|
|
Install Poetry using `pipx`:
|
|
|
|
```sh
|
|
pipx install poetry
|
|
```
|
|
|
|
Change to the `dangerzone` folder, and install the poetry dependencies:
|
|
|
|
> **Note**: due to an issue with [poetry](https://github.com/python-poetry/poetry/issues/1917), if it prompts for your keyring, disable the keyring with `keyring --disable` and run the command again.
|
|
|
|
```
|
|
poetry install
|
|
```
|
|
|
|
Build the latest container:
|
|
|
|
```sh
|
|
./install/linux/build-image.sh
|
|
```
|
|
|
|
Run from source tree:
|
|
|
|
```sh
|
|
# start a shell in the virtual environment
|
|
poetry shell
|
|
|
|
# run the CLI
|
|
./dev_scripts/dangerzone-cli --help
|
|
|
|
# run the GUI
|
|
./dev_scripts/dangerzone
|
|
```
|
|
|
|
Create a .rpm:
|
|
|
|
```sh
|
|
./install/linux/build-rpm.py
|
|
```
|
|
"""
|
|
|
|
CONTENT_BUILD_WINDOWS = r"""## Windows
|
|
|
|
Install [Docker Desktop](https://www.docker.com/products/docker-desktop).
|
|
|
|
Install the latest version of Python 3.10 (64-bit) [from python.org](https://www.python.org/downloads/windows/). Make sure to check the "Add Python 3.10 to PATH" checkbox on the first page of the installer.
|
|
|
|
|
|
Install Microsoft Visual C++ 14.0 or greater. Get it with ["Microsoft C++ Build Tools"](https://visualstudio.microsoft.com/visual-cpp-build-tools/) and make sure to select "Desktop development with C++" when installing.
|
|
|
|
Install [poetry](https://python-poetry.org/). Open PowerShell, and run:
|
|
|
|
```
|
|
python -m pip install poetry
|
|
```
|
|
|
|
Change to the `dangerzone` folder, and install the poetry dependencies:
|
|
|
|
```
|
|
poetry install
|
|
```
|
|
|
|
Build the dangerzone container image:
|
|
|
|
```sh
|
|
python .\install\windows\build-image.py
|
|
```
|
|
|
|
After that you can launch dangerzone during development with:
|
|
|
|
```
|
|
# start a shell in the virtual environment
|
|
poetry shell
|
|
|
|
# run the CLI
|
|
.\dev_scripts\dangerzone-cli.bat --help
|
|
|
|
# run the GUI
|
|
.\dev_scripts\dangerzone.bat
|
|
```
|
|
"""
|
|
|
|
|
|
class Reference:
|
|
"""Reference a Markdown section in our docs.
|
|
|
|
This class holds a reference to a Markdown section in our docs, and compares it
|
|
against to a stored version of this section in this file. The purpose of this class
|
|
is to warn devs about changes in the docs that may affect the code as well. By
|
|
having the stored doc section and the code in the same file, we limit the cases
|
|
where those two may diverge.
|
|
"""
|
|
|
|
REPO_URL = "https://github.com/freedomofpress/dangerzone"
|
|
instances = []
|
|
|
|
def __init__(self, md_path, content):
|
|
"""Initialize the class using the path for the docs and the cached section."""
|
|
self.md_path = md_path
|
|
self.content = content
|
|
|
|
# Figure out the heading of the section, and the GitHub markdown anchor, from
|
|
# the content.
|
|
first_line = self.content.split("\n", 1)[0]
|
|
self.heading_title = re.sub(r"^\W+", "", first_line)
|
|
self.md_anchor = self.get_md_anchor()
|
|
|
|
self.url = f"{self.REPO_URL}/blob/main/{self.md_path}#{self.md_anchor}"
|
|
self.instances.append(self)
|
|
|
|
def ensure_up_to_date(self):
|
|
"""Check if the referenced text has changed.
|
|
|
|
This is a consistency check to ensure that the specified QA instructions in this
|
|
script are consistent with the ones described in the docs. If the check fails,
|
|
this file needs to be updated.
|
|
"""
|
|
# Get section's text (as the file is now)
|
|
with open(self.md_path, "r") as md_file:
|
|
md_text_current = md_file.read()
|
|
current_section_text = self.find_section_text(md_text_current)
|
|
|
|
# Check if there have been any changes from the cached section stored in this
|
|
# file.
|
|
if not self.content == current_section_text:
|
|
logger.error(
|
|
f"The contents of section '{self.heading_title}' in file"
|
|
f" '{self.md_path}' have changed since this file was last updated!"
|
|
)
|
|
logger.error("Diff follows:")
|
|
sys.stderr.writelines(self.diff(current_section_text))
|
|
logger.error(
|
|
"Please ensure the instructions in this script are up"
|
|
" to date with the respective doc section, and then update the cached"
|
|
" section in this file."
|
|
)
|
|
exit(1)
|
|
|
|
def find_section_text(self, md_text):
|
|
"""Find a section's content in a provided Markdown string."""
|
|
start = end = None
|
|
orig_lines = md_text.splitlines(keepends=True)
|
|
cur_lines = self.content.splitlines(keepends=True)
|
|
for i, line in enumerate(orig_lines):
|
|
if line == cur_lines[0]:
|
|
break
|
|
|
|
start = i
|
|
end = len(cur_lines) + i
|
|
|
|
# Ensure that no extra content has been added in that section, until a new
|
|
# heading begins.
|
|
for i, line in enumerate(orig_lines[end:]):
|
|
if orig_lines[i] and orig_lines[i][0] == "#":
|
|
break
|
|
|
|
end += i
|
|
|
|
return "".join(orig_lines[start:end])
|
|
|
|
def get_md_anchor(self):
|
|
"""Attempt to get an anchor link to the markdown section on github.
|
|
|
|
Related: https://stackoverflow.com/questions/72536973/how-are-github-markdown-anchor-links-constructed
|
|
"""
|
|
# Remove '#' from header
|
|
anchor = re.sub("^[#]+", "", self.heading_title)
|
|
# Format
|
|
anchor = anchor.strip().lower()
|
|
# Convert spaces to dashes
|
|
anchor = anchor.replace(" ", "-")
|
|
# Remove non-alphanumeric (except dash and underscore)
|
|
anchor = re.sub("[^a-zA-Z\-_]", "", anchor)
|
|
|
|
return anchor
|
|
|
|
def diff(self, source):
|
|
"""Return a diff between the section in the docs and the stored one."""
|
|
source = source.splitlines(keepends=True)
|
|
content = self.content.splitlines(keepends=True)
|
|
diff = difflib.unified_diff(
|
|
source, content, fromfile=self.md_path, tofile="qa.py"
|
|
)
|
|
return diff
|
|
|
|
|
|
class QABase(abc.ABC):
|
|
"""Base class for the QA tasks."""
|
|
|
|
platforms = {}
|
|
|
|
REF_QA = Reference("RELEASE.md", content=CONTENT_QA)
|
|
REF_QA_SCENARIOS = Reference("RELEASE.md", content=CONTENT_QA_SCENARIOS)
|
|
|
|
# The following class method is available since Python 3.6. For more details, see:
|
|
# https://docs.python.org/3.6/whatsnew/3.6.html#pep-487-simpler-customization-of-class-creation
|
|
def __init_subclass__(cls, **kwargs):
|
|
super().__init_subclass__(**kwargs)
|
|
# NOTE: Not all subclasses correspond to valid platforms. Some subclasses may
|
|
# provide some basic functionality for an OS type or distro, but not a specific
|
|
# version.
|
|
if cls.get_id():
|
|
cls.platforms[cls.get_id()] = cls
|
|
|
|
def __init__(self, try_auto=False, skip_manual=False, debug=False):
|
|
self.debug = debug
|
|
self.try_auto = try_auto
|
|
self.skip_manual = skip_manual
|
|
self.src = (
|
|
subprocess.run(
|
|
[
|
|
"git",
|
|
"rev-parse",
|
|
"--show-toplevel",
|
|
],
|
|
stdout=subprocess.PIPE,
|
|
)
|
|
.stdout.decode()
|
|
.strip("\n")
|
|
)
|
|
|
|
def task(*msgs, ref=None, auto=False):
|
|
"""Decorator for running a task automatically.
|
|
|
|
This decorator does the following:
|
|
* Check if the user has asked to run the tasks automatically.
|
|
- If not, ask the user if they want to try it out.
|
|
- If yes, run the task (decorated function).
|
|
* If an exception occurs, allow the user to skip the task, or stop the
|
|
execution.
|
|
|
|
Args:
|
|
msgs: A list of messages that we should show to the user, one message per
|
|
line.
|
|
ref: A link that can be used as a reference for a specific task.
|
|
"""
|
|
|
|
def decorator(func):
|
|
def inner(self, *args, **kwargs):
|
|
# If the reference is actually defined in subclasses, grab it from
|
|
# `self`.
|
|
if isinstance(ref, str):
|
|
_ref = getattr(self, ref)
|
|
else:
|
|
_ref = ref
|
|
|
|
# Assume that the first line is the description of the task, and
|
|
# decorate it with some "=" signs.
|
|
dec_msgs = list(msgs)
|
|
num_equals = (80 - 2 - len(msgs[0])) // 2 or 3
|
|
dec_msgs.insert(0, "")
|
|
dec_msgs[1] = "=" * num_equals + " " + msgs[0] + " " + "=" * num_equals
|
|
self.describe(*dec_msgs, ref=_ref)
|
|
|
|
# Handle manual tasks.
|
|
if not auto:
|
|
if self.skip_manual:
|
|
logger.info("Skipping manual tasks, as instructed")
|
|
return
|
|
else:
|
|
return func(self, *args, **kwargs)
|
|
|
|
# Detect if the task should run automatically, either because the user
|
|
# has provided the `--try-auto` flag, or because they typed "auto" when
|
|
# prompted.
|
|
try_auto = False
|
|
if self.try_auto:
|
|
try_auto = True
|
|
else:
|
|
prompt = "Type '[a]uto' to try automatically, or '[s]kip' to skip: "
|
|
choice = self.prompt(
|
|
prompt=prompt, choices=["a", "auto", "s", "skip"]
|
|
)
|
|
try_auto = choice in ("a", "auto")
|
|
|
|
# Run the task automatically, if the user asked to, else assume that the
|
|
# user has run it manually.
|
|
if try_auto:
|
|
logger.info("Trying automatically...")
|
|
out = func(self, *args, **kwargs)
|
|
logger.info("Completed successfully")
|
|
return out
|
|
|
|
return inner
|
|
|
|
return decorator
|
|
|
|
def run(self, *args):
|
|
"""Run a command with extra debug logs."""
|
|
# Construct the command line representation of the provided arguments, by taking
|
|
# the arguments and joining them with a space character. If an argument already
|
|
# has a space character in it, quote it before adding it to the CLI
|
|
# representation.
|
|
cmd_str = ""
|
|
for arg in args:
|
|
if " " in arg:
|
|
cmd_str += " '" + arg + "'"
|
|
else:
|
|
cmd_str += " " + arg
|
|
|
|
logger.debug("Running %s", cmd_str)
|
|
try:
|
|
subprocess.run(args, check=True)
|
|
logger.debug("Successfully ran %s", cmd_str)
|
|
except subprocess.SubprocessError:
|
|
logger.debug("Failed to run %s", cmd_str)
|
|
raise
|
|
|
|
def try_run(self, *args):
|
|
"""Try to run a command and return True/False, depending on the result."""
|
|
try:
|
|
self.run(*args)
|
|
return True
|
|
except subprocess.SubprocessError:
|
|
return False
|
|
|
|
def describe(self, *msgs, ref=None):
|
|
"""Print a task's description.
|
|
|
|
Log one message per line, and optionally allow a URL as a reference.
|
|
"""
|
|
for msg in msgs:
|
|
logger.info(msg)
|
|
if ref:
|
|
logger.info(
|
|
f"For reference, see section '{ref.heading_title}' of '{ref.md_path}'"
|
|
f" either locally, or in '{ref.url}'"
|
|
)
|
|
|
|
def _consume_stdin(self):
|
|
"""Consume the stdin of the process.
|
|
|
|
Consume the stdin of the process so that stray key presses from previous steps
|
|
do not spill over.
|
|
|
|
It's imperative that we don't block in our attempt to consume the process'
|
|
stdin. Typically, this requires setting the stdin file descriptor to
|
|
non-blocking mode. However, this is not straight-forward on Windows.
|
|
|
|
Since we don't care about consuming the file descriptor *fast*, we can create a
|
|
tight loop that checks if the file descriptor has data to read from, and then
|
|
read a byte (else we risk blocking). Once the file descriptor has no data to
|
|
read, we can return.
|
|
"""
|
|
sel = selectors.DefaultSelector()
|
|
sel.register(sys.stdin, selectors.EVENT_READ)
|
|
while sel.select(timeout=0):
|
|
sys.stdin.read(1)
|
|
|
|
def prompt(self, *msgs, ref=None, prompt=None, choices=None):
|
|
"""Print the task's description, and prompt the user for an action."""
|
|
self.describe(*msgs, ref=ref)
|
|
if prompt is None:
|
|
if choices is None:
|
|
prompt = "Press Enter once you completed this step to continue: "
|
|
else:
|
|
prompt = "Valid choices are %s: " % ", ".join(choices)
|
|
|
|
if self.skip_manual:
|
|
logger.info("Skipping manual tasks, as instructed")
|
|
return None
|
|
else:
|
|
self._consume_stdin()
|
|
# If the dev has provided a list of valid choices, do not let the prompt
|
|
# proceed until the user has provided one of those choices.
|
|
while True:
|
|
choice = input(prompt)
|
|
if not choices or choice in choices:
|
|
return choice
|
|
|
|
@task("Begin the QA scenarios", ref=REF_QA_SCENARIOS)
|
|
def qa_scenarios(self, skip=None):
|
|
"""Common prompt for the QA scenarios.
|
|
|
|
This method suggests to the user a way to check the QA scenarios, in a
|
|
Dangerzone environment. Then, it iterates through them, prints their
|
|
description, and asks the user if they pass.
|
|
"""
|
|
skip = skip or []
|
|
self.describe(
|
|
"You can execute into a test environment with:",
|
|
"",
|
|
f" cd {self.src}",
|
|
f" ./dev_scripts/env.py --distro {self.DISTRO} --version"
|
|
f" {self.VERSION} run -g bash",
|
|
"",
|
|
"and run either `dangerzone` or `dangerzone-cli`",
|
|
)
|
|
|
|
scenarios = [
|
|
s[5:] for s in CONTENT_QA_SCENARIOS.splitlines() if s.startswith("#### ")
|
|
]
|
|
|
|
for num, scenario in enumerate(scenarios):
|
|
self.describe(scenario)
|
|
if num + 1 in skip:
|
|
self.describe("Skipping for the current platform")
|
|
else:
|
|
self.prompt("Does it pass?", choices=["y", "n"])
|
|
logger.info("Successfully completed QA scenarios")
|
|
|
|
@classmethod
|
|
@abc.abstractmethod
|
|
def get_id(cls):
|
|
"""Get the identifier of this class for CLI usage."""
|
|
raise NotImplementedError("The class has not specified an identifier")
|
|
|
|
@abc.abstractmethod
|
|
def start(self):
|
|
"""Start the QA tests for a platform."""
|
|
raise NotImplementedError("No steps have been defined")
|
|
|
|
|
|
# TODO: Test this class on Windows thorougly, and finish it.
|
|
class QAWindows(QABase):
|
|
"""Class for the Windows QA tasks."""
|
|
|
|
REF_BUILD = Reference("BUILD.md", content=CONTENT_BUILD_WINDOWS)
|
|
|
|
@QABase.task("Install and Run Docker Desktop", ref=REF_BUILD)
|
|
def install_docker(self):
|
|
logger.info("Checking if Docker Desktop is installed and running")
|
|
if not self.try_run("docker", "info"):
|
|
logger.info("Failed to verify that Docker Desktop is installed and running")
|
|
self.prompt("Ensure that Docker Desktop is installed and running")
|
|
else:
|
|
logger.info("Verified that Docker Desktop is installed and running")
|
|
|
|
@QABase.task(
|
|
"Install Poetry and the project's dependencies", ref=REF_BUILD, auto=True
|
|
)
|
|
def install_poetry(self):
|
|
self.run("python", "-m", "pip", "install", "poetry")
|
|
self.run("poetry", "install")
|
|
|
|
@QABase.task("Build Dangerzone container image", ref=REF_BUILD, auto=True)
|
|
def build_image(self):
|
|
self.run("python", r".\install\windows\build-image.py")
|
|
|
|
@classmethod
|
|
def get_id(cls):
|
|
return "windows"
|
|
|
|
def start(self):
|
|
self.install_docker()
|
|
self.install_poetry()
|
|
self.build_image()
|
|
|
|
|
|
class QALinux(QABase):
|
|
"""Base class for all the Linux QA tasks."""
|
|
|
|
REF_BUILD = None
|
|
DISTRO = None
|
|
VERSION = None
|
|
|
|
def container_run(self, *args):
|
|
"""Run a command inside a Dangerzone environment."""
|
|
self.run(
|
|
f"{self.src}/dev_scripts/env.py",
|
|
"--distro",
|
|
self.DISTRO,
|
|
"--version",
|
|
self.VERSION,
|
|
"run",
|
|
"--dev",
|
|
*args,
|
|
)
|
|
|
|
def shell_run(self, *args):
|
|
"""Run a shell command inside a Dangerzone environment and source dir."""
|
|
args_str = " ".join(args)
|
|
self.container_run("bash", "-c", f"cd dangerzone; {args_str}")
|
|
|
|
def poetry_run(self, *args):
|
|
"""Run a command via Poetry inside a Dangerzone environment."""
|
|
self.shell_run("poetry", "run", *args)
|
|
|
|
@QABase.task(
|
|
"Create Dangerzone build environment",
|
|
"You can also run './dev_scripts/env.py ... build-dev'",
|
|
ref="REF_BUILD",
|
|
auto=True,
|
|
)
|
|
def build_dev_image(self):
|
|
self.run(
|
|
f"{self.src}/dev_scripts/env.py",
|
|
"--distro",
|
|
self.DISTRO,
|
|
"--version",
|
|
self.VERSION,
|
|
"build-dev",
|
|
)
|
|
|
|
@QABase.task("Build Dangerzone image", ref="REF_BUILD", auto=True)
|
|
def build_container_image(self):
|
|
self.shell_run("./install/linux/build-image.sh")
|
|
# FIXME: We need to automate this part, simply by checking that the created
|
|
# image is in `share/image-id.txt`.
|
|
self.prompt("Ensure that the environment uses the created image")
|
|
|
|
@QABase.task("Run tests", ref="REF_BUILD", auto=True)
|
|
def run_tests(self):
|
|
self.poetry_run("make", "test")
|
|
|
|
def build_package(self):
|
|
"""Build the Dangerzone .deb/.rpm package"""
|
|
# Building a pakage is platform-specific, so subclasses must implement it.
|
|
raise NotImplementedError("Building a package is not implemented")
|
|
|
|
@QABase.task("Create Dangerzone QA environment", ref="REF_BUILD", auto=True)
|
|
def build_qa_image(self):
|
|
self.run(
|
|
f"{self.src}/dev_scripts/env.py",
|
|
"--distro",
|
|
self.DISTRO,
|
|
"--version",
|
|
self.VERSION,
|
|
"build",
|
|
)
|
|
|
|
@classmethod
|
|
def get_id(cls):
|
|
"""Return the ID for the QA class.
|
|
|
|
If the QA class is a base one, then either the distro or the version will be
|
|
None. In this case, return None as well, to signify that this class does not
|
|
correspond to a specific platform."""
|
|
if not cls.DISTRO or not cls.VERSION:
|
|
return None
|
|
else:
|
|
return f"{cls.DISTRO}-{cls.VERSION}"
|
|
|
|
def start(self):
|
|
self.build_dev_image()
|
|
self.build_container_image()
|
|
self.run_tests()
|
|
self.build_package()
|
|
self.build_qa_image()
|
|
self.qa_scenarios(skip=[1, 2, 8])
|
|
|
|
|
|
class QADebianBased(QALinux):
|
|
"""Base class for Debian-based distros.
|
|
|
|
This class simply points to the proper build instructions, and builds the Debian
|
|
package.
|
|
"""
|
|
|
|
REF_BUILD = Reference("BUILD.md", content=CONTENT_BUILD_DEBIAN_UBUNTU)
|
|
|
|
@QABase.task("Build .deb", ref=REF_BUILD, auto=True)
|
|
def build_package(self):
|
|
self.shell_run("./install/linux/build-deb.py")
|
|
|
|
|
|
class QADebianBullseye(QADebianBased):
|
|
DISTRO = "debian"
|
|
VERSION = "bullseye"
|
|
|
|
|
|
class QADebianBookworm(QADebianBased):
|
|
DISTRO = "debian"
|
|
VERSION = "bookworm"
|
|
|
|
|
|
class QAUbuntu2004(QADebianBased):
|
|
DISTRO = "ubuntu"
|
|
VERSION = "20.04"
|
|
|
|
|
|
class QAUbuntu2204(QADebianBased):
|
|
DISTRO = "ubuntu"
|
|
VERSION = "22.04"
|
|
|
|
|
|
class QAUbuntu2210(QADebianBased):
|
|
DISTRO = "ubuntu"
|
|
VERSION = "22.10"
|
|
|
|
|
|
class QAFedora(QALinux):
|
|
"""Base class for Fedora distros.
|
|
|
|
This class simply points to the proper build instructions, and builds the RPM
|
|
package.
|
|
"""
|
|
|
|
DISTRO = "fedora"
|
|
REF_BUILD = Reference("BUILD.md", content=CONTENT_BUILD_FEDORA)
|
|
|
|
@QABase.task("Build .rpm", ref=REF_BUILD, auto=True)
|
|
def build_package(self):
|
|
self.container_run(
|
|
"./dangerzone/install/linux/build-rpm.py",
|
|
)
|
|
|
|
|
|
class QAFedora36(QAFedora):
|
|
VERSION = "36"
|
|
|
|
|
|
class QAFedora37(QAFedora):
|
|
VERSION = "37"
|
|
|
|
|
|
class QAFedora38(QAFedora):
|
|
VERSION = "38"
|
|
|
|
|
|
def parse_args():
|
|
parser = argparse.ArgumentParser(
|
|
prog=sys.argv[0],
|
|
description="Run QA tests for a platform",
|
|
)
|
|
parser.add_argument(
|
|
"platform",
|
|
choices=sorted(QABase.platforms.keys()),
|
|
help="Run QA tests for the provided platform",
|
|
nargs="?",
|
|
)
|
|
parser.add_argument(
|
|
"--try-auto",
|
|
action="store_true",
|
|
default=False,
|
|
help="Try to run the automated parts of the QA steps",
|
|
)
|
|
# FIXME: Find a better way to skip tasks in non-interactive environments, while
|
|
# retaining the ability to be semi-prompted in interactive environments.
|
|
parser.add_argument(
|
|
"--skip-manual",
|
|
action="store_true",
|
|
default=False,
|
|
help="Skip the manual parts of the QA steps",
|
|
)
|
|
parser.add_argument(
|
|
"--debug",
|
|
action="store_true",
|
|
default=False,
|
|
help="Enable debug logs",
|
|
)
|
|
parser.add_argument(
|
|
"--check-refs",
|
|
action="store_true",
|
|
default=False,
|
|
help="Check if references to docs still hold",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
if not args.check_refs and not args.platform:
|
|
parser.print_help(sys.stderr)
|
|
exit(1)
|
|
|
|
return args
|
|
|
|
|
|
def setup_logging(debug=False):
|
|
"""Simple way to setup logging.
|
|
|
|
Copied from: https://docs.python.org/3/howto/logging.html
|
|
"""
|
|
# specify level
|
|
if debug:
|
|
lvl = logging.DEBUG
|
|
else:
|
|
lvl = logging.INFO
|
|
|
|
logging.basicConfig(
|
|
level=lvl,
|
|
format="%(asctime)s - %(levelname)s - %(message)s",
|
|
datefmt="%Y-%m-%d %H:%M:%S",
|
|
)
|
|
|
|
|
|
def main():
|
|
args = parse_args()
|
|
setup_logging(args.debug)
|
|
|
|
for ref in Reference.instances:
|
|
ref.ensure_up_to_date()
|
|
if args.check_refs:
|
|
return
|
|
|
|
try:
|
|
qa_cls = QABase.platforms[args.platform]
|
|
except KeyError:
|
|
raise RuntimeError("Unexpected platform: %s", args.platform)
|
|
|
|
logger.info("Starting QA tests for %s", args.platform.capitalize())
|
|
qa_cls(args.try_auto, args.skip_manual, args.debug).start()
|
|
logger.info("Successfully completed QA tests for %s", args.platform.capitalize())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|