mirror of
https://github.com/freedomofpress/dangerzone.git
synced 2025-05-10 15:31:50 +02:00
Compare commits
15 commits
fbe05065c9
...
54413ef676
Author | SHA1 | Date | |
---|---|---|---|
![]() |
54413ef676 | ||
![]() |
7a42827894 | ||
![]() |
01a613d640 | ||
![]() |
396d53b130 | ||
![]() |
2f29095b31 | ||
![]() |
52eae7cd00 | ||
![]() |
ece58cba06 | ||
![]() |
eec4e6a5c3 | ||
![]() |
02261b112e | ||
![]() |
f400205c74 | ||
![]() |
5b1fe4d7ad | ||
![]() |
53214d33d8 | ||
![]() |
7f7fe43711 | ||
![]() |
f31fbfefc6 | ||
![]() |
96e64deae7 |
25 changed files with 495 additions and 532 deletions
7
.github/workflows/build.yml
vendored
7
.github/workflows/build.yml
vendored
|
@ -85,7 +85,7 @@ jobs:
|
||||||
id: cache-container-image
|
id: cache-container-image
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
key: v3-${{ steps.date.outputs.date }}-${{ hashFiles('Dockerfile', 'dangerzone/conversion/common.py', 'dangerzone/conversion/doc_to_pixels.py', 'dangerzone/conversion/pixels_to_pdf.py', 'poetry.lock', 'gvisor_wrapper/entrypoint.py') }}
|
key: v2-${{ steps.date.outputs.date }}-${{ hashFiles('Dockerfile', 'dangerzone/conversion/common.py', 'dangerzone/conversion/doc_to_pixels.py', 'dangerzone/conversion/pixels_to_pdf.py', 'poetry.lock', 'gvisor_wrapper/entrypoint.py') }}
|
||||||
path: |
|
path: |
|
||||||
share/container.tar.gz
|
share/container.tar.gz
|
||||||
share/image-id.txt
|
share/image-id.txt
|
||||||
|
@ -97,7 +97,6 @@ jobs:
|
||||||
python3 ./install/common/build-image.py
|
python3 ./install/common/build-image.py
|
||||||
echo ${{ github.token }} | podman login ghcr.io -u USERNAME --password-stdin
|
echo ${{ github.token }} | podman login ghcr.io -u USERNAME --password-stdin
|
||||||
gunzip -c share/container.tar.gz | podman load
|
gunzip -c share/container.tar.gz | podman load
|
||||||
tag=$(cat share/image-id.txt)
|
|
||||||
podman push \
|
podman push \
|
||||||
dangerzone.rocks/dangerzone:$tag \
|
dangerzone.rocks/dangerzone \
|
||||||
${{ env.IMAGE_REGISTRY }}/dangerzone/dangerzone:tag
|
${{ env.IMAGE_REGISTRY }}/dangerzone/dangerzone
|
||||||
|
|
20
.github/workflows/ci.yml
vendored
20
.github/workflows/ci.yml
vendored
|
@ -59,7 +59,7 @@ jobs:
|
||||||
id: cache-container-image
|
id: cache-container-image
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
key: v3-${{ steps.date.outputs.date }}-${{ hashFiles('Dockerfile', 'dangerzone/conversion/common.py', 'dangerzone/conversion/doc_to_pixels.py', 'dangerzone/conversion/pixels_to_pdf.py', 'poetry.lock', 'gvisor_wrapper/entrypoint.py') }}
|
key: v2-${{ steps.date.outputs.date }}-${{ hashFiles('Dockerfile', 'dangerzone/conversion/common.py', 'dangerzone/conversion/doc_to_pixels.py', 'dangerzone/conversion/pixels_to_pdf.py', 'poetry.lock', 'gvisor_wrapper/entrypoint.py') }}
|
||||||
path: |-
|
path: |-
|
||||||
share/container.tar.gz
|
share/container.tar.gz
|
||||||
share/image-id.txt
|
share/image-id.txt
|
||||||
|
@ -121,14 +121,10 @@ jobs:
|
||||||
key: v1-tessdata-${{ hashFiles('./install/common/download-tessdata.py') }}
|
key: v1-tessdata-${{ hashFiles('./install/common/download-tessdata.py') }}
|
||||||
- name: Run CLI tests
|
- name: Run CLI tests
|
||||||
run: poetry run make test
|
run: poetry run make test
|
||||||
- name: Set up .NET CLI environment
|
# Taken from: https://github.com/orgs/community/discussions/27149#discussioncomment-3254829
|
||||||
uses: actions/setup-dotnet@v4
|
- name: Set path for candle and light
|
||||||
with:
|
run: echo "C:\Program Files (x86)\WiX Toolset v3.14\bin" >> $GITHUB_PATH
|
||||||
dotnet-version: "8.x"
|
shell: bash
|
||||||
- name: Install WiX Toolset
|
|
||||||
run: dotnet tool install --global wix
|
|
||||||
- name: Add WiX UI extension
|
|
||||||
run: wix extension add --global WixToolset.UI.wixext
|
|
||||||
- name: Build the MSI installer
|
- name: Build the MSI installer
|
||||||
# NOTE: This also builds the .exe internally.
|
# NOTE: This also builds the .exe internally.
|
||||||
run: poetry run .\install\windows\build-app.bat
|
run: poetry run .\install\windows\build-app.bat
|
||||||
|
@ -227,7 +223,7 @@ jobs:
|
||||||
- name: Restore container cache
|
- name: Restore container cache
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
key: v3-${{ steps.date.outputs.date }}-${{ hashFiles('Dockerfile', 'dangerzone/conversion/common.py', 'dangerzone/conversion/doc_to_pixels.py', 'dangerzone/conversion/pixels_to_pdf.py', 'poetry.lock', 'gvisor_wrapper/entrypoint.py') }}
|
key: v2-${{ steps.date.outputs.date }}-${{ hashFiles('Dockerfile', 'dangerzone/conversion/common.py', 'dangerzone/conversion/doc_to_pixels.py', 'dangerzone/conversion/pixels_to_pdf.py', 'poetry.lock', 'gvisor_wrapper/entrypoint.py') }}
|
||||||
path: |-
|
path: |-
|
||||||
share/container.tar.gz
|
share/container.tar.gz
|
||||||
share/image-id.txt
|
share/image-id.txt
|
||||||
|
@ -334,7 +330,7 @@ jobs:
|
||||||
- name: Restore container image
|
- name: Restore container image
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
key: v3-${{ steps.date.outputs.date }}-${{ hashFiles('Dockerfile', 'dangerzone/conversion/common.py', 'dangerzone/conversion/doc_to_pixels.py', 'dangerzone/conversion/pixels_to_pdf.py', 'poetry.lock', 'gvisor_wrapper/entrypoint.py') }}
|
key: v2-${{ steps.date.outputs.date }}-${{ hashFiles('Dockerfile', 'dangerzone/conversion/common.py', 'dangerzone/conversion/doc_to_pixels.py', 'dangerzone/conversion/pixels_to_pdf.py', 'poetry.lock', 'gvisor_wrapper/entrypoint.py') }}
|
||||||
path: |-
|
path: |-
|
||||||
share/container.tar.gz
|
share/container.tar.gz
|
||||||
share/image-id.txt
|
share/image-id.txt
|
||||||
|
@ -429,7 +425,7 @@ jobs:
|
||||||
- name: Restore container image
|
- name: Restore container image
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
key: v3-${{ steps.date.outputs.date }}-${{ hashFiles('Dockerfile', 'dangerzone/conversion/common.py', 'dangerzone/conversion/doc_to_pixels.py', 'dangerzone/conversion/pixels_to_pdf.py', 'poetry.lock', 'gvisor_wrapper/entrypoint.py') }}
|
key: v2-${{ steps.date.outputs.date }}-${{ hashFiles('Dockerfile', 'dangerzone/conversion/common.py', 'dangerzone/conversion/doc_to_pixels.py', 'dangerzone/conversion/pixels_to_pdf.py', 'poetry.lock', 'gvisor_wrapper/entrypoint.py') }}
|
||||||
path: |-
|
path: |-
|
||||||
share/container.tar.gz
|
share/container.tar.gz
|
||||||
share/image-id.txt
|
share/image-id.txt
|
||||||
|
|
9
.github/workflows/scan.yml
vendored
9
.github/workflows/scan.yml
vendored
|
@ -20,18 +20,13 @@ jobs:
|
||||||
run: sudo apt install pipx && pipx install poetry
|
run: sudo apt install pipx && pipx install poetry
|
||||||
- name: Build container image
|
- name: Build container image
|
||||||
run: python3 ./install/common/build-image.py --runtime docker --no-save
|
run: python3 ./install/common/build-image.py --runtime docker --no-save
|
||||||
- name: Get image tag
|
|
||||||
id: tag
|
|
||||||
run: |
|
|
||||||
tag=$(docker images dangerzone.rocks/dangerzone --format '{{ .Tag }}')
|
|
||||||
echo "tag=$tag" >> $GITHUB_OUTPUT
|
|
||||||
# NOTE: Scan first without failing, else we won't be able to read the scan
|
# NOTE: Scan first without failing, else we won't be able to read the scan
|
||||||
# report.
|
# report.
|
||||||
- name: Scan container image (no fail)
|
- name: Scan container image (no fail)
|
||||||
uses: anchore/scan-action@v5
|
uses: anchore/scan-action@v5
|
||||||
id: scan_container
|
id: scan_container
|
||||||
with:
|
with:
|
||||||
image: "dangerzone.rocks/dangerzone:${{ steps.tag.outputs.tag }}"
|
image: "dangerzone.rocks/dangerzone:latest"
|
||||||
fail-build: false
|
fail-build: false
|
||||||
only-fixed: false
|
only-fixed: false
|
||||||
severity-cutoff: critical
|
severity-cutoff: critical
|
||||||
|
@ -45,7 +40,7 @@ jobs:
|
||||||
- name: Scan container image
|
- name: Scan container image
|
||||||
uses: anchore/scan-action@v5
|
uses: anchore/scan-action@v5
|
||||||
with:
|
with:
|
||||||
image: "dangerzone.rocks/dangerzone:${{ steps.tag.outputs.tag }}"
|
image: "dangerzone.rocks/dangerzone:latest"
|
||||||
fail-build: true
|
fail-build: true
|
||||||
only-fixed: false
|
only-fixed: false
|
||||||
severity-cutoff: critical
|
severity-cutoff: critical
|
||||||
|
|
9
.github/workflows/scan_released.yml
vendored
9
.github/workflows/scan_released.yml
vendored
|
@ -24,18 +24,13 @@ jobs:
|
||||||
CONTAINER_FILENAME=container-${VERSION:1}-${{ matrix.arch }}.tar.gz
|
CONTAINER_FILENAME=container-${VERSION:1}-${{ matrix.arch }}.tar.gz
|
||||||
wget https://github.com/freedomofpress/dangerzone/releases/download/${VERSION}/${CONTAINER_FILENAME} -O ${CONTAINER_FILENAME}
|
wget https://github.com/freedomofpress/dangerzone/releases/download/${VERSION}/${CONTAINER_FILENAME} -O ${CONTAINER_FILENAME}
|
||||||
docker load -i ${CONTAINER_FILENAME}
|
docker load -i ${CONTAINER_FILENAME}
|
||||||
- name: Get image tag
|
|
||||||
id: tag
|
|
||||||
run: |
|
|
||||||
tag=$(docker images dangerzone.rocks/dangerzone --format '{{ .Tag }}')
|
|
||||||
echo "tag=$tag" >> $GITHUB_OUTPUT
|
|
||||||
# NOTE: Scan first without failing, else we won't be able to read the scan
|
# NOTE: Scan first without failing, else we won't be able to read the scan
|
||||||
# report.
|
# report.
|
||||||
- name: Scan container image (no fail)
|
- name: Scan container image (no fail)
|
||||||
uses: anchore/scan-action@v5
|
uses: anchore/scan-action@v5
|
||||||
id: scan_container
|
id: scan_container
|
||||||
with:
|
with:
|
||||||
image: "dangerzone.rocks/dangerzone:${{ steps.tag.outputs.tag }}"
|
image: "dangerzone.rocks/dangerzone:latest"
|
||||||
fail-build: false
|
fail-build: false
|
||||||
only-fixed: false
|
only-fixed: false
|
||||||
severity-cutoff: critical
|
severity-cutoff: critical
|
||||||
|
@ -49,7 +44,7 @@ jobs:
|
||||||
- name: Scan container image
|
- name: Scan container image
|
||||||
uses: anchore/scan-action@v5
|
uses: anchore/scan-action@v5
|
||||||
with:
|
with:
|
||||||
image: "dangerzone.rocks/dangerzone:${{ steps.tag.outputs.tag }}"
|
image: "dangerzone.rocks/dangerzone:latest"
|
||||||
fail-build: true
|
fail-build: true
|
||||||
only-fixed: false
|
only-fixed: false
|
||||||
severity-cutoff: critical
|
severity-cutoff: critical
|
||||||
|
|
21
BUILD.md
21
BUILD.md
|
@ -471,24 +471,11 @@ poetry shell
|
||||||
.\dev_scripts\dangerzone.bat
|
.\dev_scripts\dangerzone.bat
|
||||||
```
|
```
|
||||||
|
|
||||||
### If you want to build the Windows installer
|
### If you want to build the installer
|
||||||
|
|
||||||
Install [.NET SDK](https://dotnet.microsoft.com/en-us/download) version 6 or later. Then, open a terminal and install the latest version of [WiX Toolset .NET tool](https://wixtoolset.org/) **v5** with:
|
* Go to https://dotnet.microsoft.com/download/dotnet-framework and download and install .NET Framework 3.5 SP1 Runtime. I downloaded `dotnetfx35.exe`.
|
||||||
|
* Go to https://wixtoolset.org/releases/ and download and install WiX toolset. I downloaded `wix314.exe`.
|
||||||
```sh
|
* Add `C:\Program Files (x86)\WiX Toolset v3.14\bin` to the path ([instructions](https://web.archive.org/web/20230221104142/https://windowsloop.com/how-to-add-to-windows-path/)).
|
||||||
dotnet tool install --global wix --version 5.*
|
|
||||||
```
|
|
||||||
|
|
||||||
Install the WiX UI extension. You may need to open a new terminal in order to use the newly installed `wix` .NET tool:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
wix extension add --global WixToolset.UI.wixext/5.x.y
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> To avoid compatibility issues, ensure the WiX UI extension version matches the version of the WiX Toolset.
|
|
||||||
>
|
|
||||||
> Run `wix --version` to check the version of WiX Toolset you have installed and replace `5.x.y` with the full version number without the Git revision.
|
|
||||||
|
|
||||||
### If you want to sign binaries with Authenticode
|
### If you want to sign binaries with Authenticode
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,6 @@ since 0.4.1, and this project adheres to [Semantic Versioning](https://semver.or
|
||||||
|
|
||||||
### Development changes
|
### Development changes
|
||||||
|
|
||||||
Thanks [@jkarasti](https://github.com/jkarasti) for the contribution.
|
|
||||||
- Automate a large portion of our release tasks with `doit` ([#1016](https://github.com/freedomofpress/dangerzone/issues/1016))
|
- Automate a large portion of our release tasks with `doit` ([#1016](https://github.com/freedomofpress/dangerzone/issues/1016))
|
||||||
|
|
||||||
## [0.8.0](https://github.com/freedomofpress/dangerzone/compare/v0.8.0...0.7.1)
|
## [0.8.0](https://github.com/freedomofpress/dangerzone/compare/v0.8.0...0.7.1)
|
||||||
|
|
16
Makefile
16
Makefile
|
@ -66,22 +66,6 @@ test-large: test-large-init ## Run large test set
|
||||||
python -m pytest --tb=no tests/test_large_set.py::TestLargeSet -v $(JUNIT_FLAGS) --junitxml=$(TEST_LARGE_RESULTS)
|
python -m pytest --tb=no tests/test_large_set.py::TestLargeSet -v $(JUNIT_FLAGS) --junitxml=$(TEST_LARGE_RESULTS)
|
||||||
python $(TEST_LARGE_RESULTS)/report.py $(TEST_LARGE_RESULTS)
|
python $(TEST_LARGE_RESULTS)/report.py $(TEST_LARGE_RESULTS)
|
||||||
|
|
||||||
.PHONY: build-clean
|
|
||||||
build-clean:
|
|
||||||
doit clean
|
|
||||||
|
|
||||||
.PHONY: build-macos-intel
|
|
||||||
build-macos-intel: build-clean
|
|
||||||
doit -n 8
|
|
||||||
|
|
||||||
.PHONY: build-macos-arm
|
|
||||||
build-macos-arm: build-clean
|
|
||||||
doit -n 8 macos_build_dmg
|
|
||||||
|
|
||||||
.PHONY: build-linux
|
|
||||||
build-linux: build-clean
|
|
||||||
doit -n 8 fedora_rpm debian_deb
|
|
||||||
|
|
||||||
# Makefile self-help borrowed from the securedrop-client project
|
# Makefile self-help borrowed from the securedrop-client project
|
||||||
# Explaination of the below shell command should it ever break.
|
# Explaination of the below shell command should it ever break.
|
||||||
# 1. Set the field separator to ": ##" and any make targets that might appear between : and ##
|
# 1. Set the field separator to ": ##" and any make targets that might appear between : and ##
|
||||||
|
|
2
QA.md
2
QA.md
|
@ -109,6 +109,7 @@ version. For example:
|
||||||
```
|
```
|
||||||
$ docker images dangerzone.rocks/dangerzone
|
$ docker images dangerzone.rocks/dangerzone
|
||||||
REPOSITORY TAG IMAGE ID CREATED SIZE
|
REPOSITORY TAG IMAGE ID CREATED SIZE
|
||||||
|
dangerzone.rocks/dangerzone latest <image ID> <date> <size>
|
||||||
dangerzone.rocks/dangerzone <tag> <image ID> <date> <size>
|
dangerzone.rocks/dangerzone <tag> <image ID> <date> <size>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -120,6 +121,7 @@ and seeing the following differences:
|
||||||
```
|
```
|
||||||
$ docker images dangerzone.rocks/dangerzone
|
$ docker images dangerzone.rocks/dangerzone
|
||||||
REPOSITORY TAG IMAGE ID CREATED SIZE
|
REPOSITORY TAG IMAGE ID CREATED SIZE
|
||||||
|
dangerzone.rocks/dangerzone latest <different ID> <newer date> <different size>
|
||||||
dangerzone.rocks/dangerzone <other tag> <different ID> <newer date> <different size>
|
dangerzone.rocks/dangerzone <other tag> <different ID> <newer date> <different size>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
15
RELEASE.md
15
RELEASE.md
|
@ -7,7 +7,11 @@ This section documents how we currently release Dangerzone for the different dis
|
||||||
Here is a list of tasks that should be done before issuing the release:
|
Here is a list of tasks that should be done before issuing the release:
|
||||||
|
|
||||||
- [ ] Create a new issue named **QA and Release for version \<VERSION\>**, to track the general progress.
|
- [ ] Create a new issue named **QA and Release for version \<VERSION\>**, to track the general progress.
|
||||||
You can generate its content with the the `poetry run ./dev_scripts/generate-release-tasks.py` command.
|
You can generate its content with:
|
||||||
|
|
||||||
|
```
|
||||||
|
poetry run ./dev_scripts/generate-release-tasks.py`
|
||||||
|
```
|
||||||
- [ ] [Add new Linux platforms and remove obsolete ones](https://github.com/freedomofpress/dangerzone/blob/main/RELEASE.md#add-new-platforms-and-remove-obsolete-ones)
|
- [ ] [Add new Linux platforms and remove obsolete ones](https://github.com/freedomofpress/dangerzone/blob/main/RELEASE.md#add-new-platforms-and-remove-obsolete-ones)
|
||||||
- [ ] Bump the Python dependencies using `poetry lock`
|
- [ ] Bump the Python dependencies using `poetry lock`
|
||||||
- [ ] Update `version` in `pyproject.toml`
|
- [ ] Update `version` in `pyproject.toml`
|
||||||
|
@ -80,9 +84,9 @@ Once we are confident that the release will be out shortly, and doesn't need any
|
||||||
> You can automate these steps from your macOS terminal app with:
|
> You can automate these steps from your macOS terminal app with:
|
||||||
>
|
>
|
||||||
> ```
|
> ```
|
||||||
> export APPLE_ID=<email>
|
> doit clean
|
||||||
> make build-macos-intel # for Intel macOS
|
> doit -n 8 apple_id=<email> # for Intel macOS
|
||||||
> make build-macos-arm # for Apple Silicon macOS
|
> doit -n 8 apple_id=<email> macos_build_dmg # for Apple Silicon macOS
|
||||||
> ```
|
> ```
|
||||||
|
|
||||||
The following needs to happen for both Silicon and Intel chipsets.
|
The following needs to happen for both Silicon and Intel chipsets.
|
||||||
|
@ -230,7 +234,8 @@ Rename `Dangerzone.msi` to `Dangerzone-$VERSION.msi`.
|
||||||
> You can automate these steps from any Linux distribution with:
|
> You can automate these steps from any Linux distribution with:
|
||||||
>
|
>
|
||||||
> ```
|
> ```
|
||||||
> make build-linux
|
> doit clean
|
||||||
|
> doit -n 8 fedora_rpm debian_deb
|
||||||
> ```
|
> ```
|
||||||
>
|
>
|
||||||
> You can then add the created artifacts to the appropriate APT/YUM repo.
|
> You can then add the created artifacts to the appropriate APT/YUM repo.
|
||||||
|
|
|
@ -1,149 +0,0 @@
|
||||||
import gzip
|
|
||||||
import logging
|
|
||||||
import platform
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
from typing import List, Tuple
|
|
||||||
|
|
||||||
from . import errors
|
|
||||||
from .util import get_resource_path, get_subprocess_startupinfo
|
|
||||||
|
|
||||||
CONTAINER_NAME = "dangerzone.rocks/dangerzone"
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def get_runtime_name() -> str:
|
|
||||||
if platform.system() == "Linux":
|
|
||||||
runtime_name = "podman"
|
|
||||||
else:
|
|
||||||
# Windows, Darwin, and unknown use docker for now, dangerzone-vm eventually
|
|
||||||
runtime_name = "docker"
|
|
||||||
return runtime_name
|
|
||||||
|
|
||||||
|
|
||||||
def get_runtime_version() -> Tuple[int, int]:
|
|
||||||
"""Get the major/minor parts of the Docker/Podman version.
|
|
||||||
|
|
||||||
Some of the operations we perform in this module rely on some Podman features
|
|
||||||
that are not available across all of our platforms. In order to have a proper
|
|
||||||
fallback, we need to know the Podman version. More specifically, we're fine with
|
|
||||||
just knowing the major and minor version, since writing/installing a full-blown
|
|
||||||
semver parser is an overkill.
|
|
||||||
"""
|
|
||||||
# Get the Docker/Podman version, using a Go template.
|
|
||||||
runtime = get_runtime_name()
|
|
||||||
if runtime == "podman":
|
|
||||||
query = "{{.Client.Version}}"
|
|
||||||
else:
|
|
||||||
query = "{{.Server.Version}}"
|
|
||||||
|
|
||||||
cmd = [runtime, "version", "-f", query]
|
|
||||||
try:
|
|
||||||
version = subprocess.run(
|
|
||||||
cmd,
|
|
||||||
startupinfo=get_subprocess_startupinfo(),
|
|
||||||
capture_output=True,
|
|
||||||
check=True,
|
|
||||||
).stdout.decode()
|
|
||||||
except Exception as e:
|
|
||||||
msg = f"Could not get the version of the {runtime.capitalize()} tool: {e}"
|
|
||||||
raise RuntimeError(msg) from e
|
|
||||||
|
|
||||||
# Parse this version and return the major/minor parts, since we don't need the
|
|
||||||
# rest.
|
|
||||||
try:
|
|
||||||
major, minor, _ = version.split(".", 3)
|
|
||||||
return (int(major), int(minor))
|
|
||||||
except Exception as e:
|
|
||||||
msg = (
|
|
||||||
f"Could not parse the version of the {runtime.capitalize()} tool"
|
|
||||||
f" (found: '{version}') due to the following error: {e}"
|
|
||||||
)
|
|
||||||
raise RuntimeError(msg)
|
|
||||||
|
|
||||||
|
|
||||||
def get_runtime() -> str:
|
|
||||||
container_tech = get_runtime_name()
|
|
||||||
runtime = shutil.which(container_tech)
|
|
||||||
if runtime is None:
|
|
||||||
raise errors.NoContainerTechException(container_tech)
|
|
||||||
return runtime
|
|
||||||
|
|
||||||
|
|
||||||
def list_image_tags() -> List[str]:
|
|
||||||
"""Get the tags of all loaded Dangerzone images.
|
|
||||||
|
|
||||||
This method returns a mapping of image tags to image IDs, for all Dangerzone
|
|
||||||
images. This can be useful when we want to find which are the local image tags,
|
|
||||||
and which image ID does the "latest" tag point to.
|
|
||||||
"""
|
|
||||||
return (
|
|
||||||
subprocess.check_output(
|
|
||||||
[
|
|
||||||
get_runtime(),
|
|
||||||
"image",
|
|
||||||
"list",
|
|
||||||
"--format",
|
|
||||||
"{{ .Tag }}",
|
|
||||||
CONTAINER_NAME,
|
|
||||||
],
|
|
||||||
text=True,
|
|
||||||
startupinfo=get_subprocess_startupinfo(),
|
|
||||||
)
|
|
||||||
.strip()
|
|
||||||
.split()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def delete_image_tag(tag: str) -> None:
|
|
||||||
"""Delete a Dangerzone image tag."""
|
|
||||||
name = CONTAINER_NAME + ":" + tag
|
|
||||||
log.warning(f"Deleting old container image: {name}")
|
|
||||||
try:
|
|
||||||
subprocess.check_output(
|
|
||||||
[get_runtime(), "rmi", "--force", name],
|
|
||||||
startupinfo=get_subprocess_startupinfo(),
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
log.warning(
|
|
||||||
f"Couldn't delete old container image '{name}', so leaving it there."
|
|
||||||
f" Original error: {e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_expected_tag() -> str:
|
|
||||||
"""Get the tag of the Dangerzone image tarball from the image-id.txt file."""
|
|
||||||
with open(get_resource_path("image-id.txt")) as f:
|
|
||||||
return f.read().strip()
|
|
||||||
|
|
||||||
|
|
||||||
def load_image_tarball() -> None:
|
|
||||||
log.info("Installing Dangerzone container image...")
|
|
||||||
p = subprocess.Popen(
|
|
||||||
[get_runtime(), "load"],
|
|
||||||
stdin=subprocess.PIPE,
|
|
||||||
startupinfo=get_subprocess_startupinfo(),
|
|
||||||
)
|
|
||||||
|
|
||||||
chunk_size = 4 << 20
|
|
||||||
compressed_container_path = get_resource_path("container.tar.gz")
|
|
||||||
with gzip.open(compressed_container_path) as f:
|
|
||||||
while True:
|
|
||||||
chunk = f.read(chunk_size)
|
|
||||||
if len(chunk) > 0:
|
|
||||||
if p.stdin:
|
|
||||||
p.stdin.write(chunk)
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
_, err = p.communicate()
|
|
||||||
if p.returncode < 0:
|
|
||||||
if err:
|
|
||||||
error = err.decode()
|
|
||||||
else:
|
|
||||||
error = "No output"
|
|
||||||
raise errors.ImageInstallationException(
|
|
||||||
f"Could not install container image: {error}"
|
|
||||||
)
|
|
||||||
|
|
||||||
log.info("Successfully installed container image from")
|
|
|
@ -117,26 +117,3 @@ def handle_document_errors(func: F) -> F:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
return cast(F, wrapper)
|
return cast(F, wrapper)
|
||||||
|
|
||||||
|
|
||||||
#### Container-related errors
|
|
||||||
|
|
||||||
|
|
||||||
class ImageNotPresentException(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ImageInstallationException(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class NoContainerTechException(Exception):
|
|
||||||
def __init__(self, container_tech: str) -> None:
|
|
||||||
super().__init__(f"{container_tech} is not installed")
|
|
||||||
|
|
||||||
|
|
||||||
class NotAvailableContainerTechException(Exception):
|
|
||||||
def __init__(self, container_tech: str, error: str) -> None:
|
|
||||||
self.error = error
|
|
||||||
self.container_tech = container_tech
|
|
||||||
super().__init__(f"{container_tech} is not available")
|
|
||||||
|
|
|
@ -25,7 +25,13 @@ else:
|
||||||
|
|
||||||
from .. import errors
|
from .. import errors
|
||||||
from ..document import SAFE_EXTENSION, Document
|
from ..document import SAFE_EXTENSION, Document
|
||||||
from ..isolation_provider.qubes import is_qubes_native_conversion
|
from ..isolation_provider.container import (
|
||||||
|
Container,
|
||||||
|
NoContainerTechException,
|
||||||
|
NotAvailableContainerTechException,
|
||||||
|
)
|
||||||
|
from ..isolation_provider.dummy import Dummy
|
||||||
|
from ..isolation_provider.qubes import Qubes, is_qubes_native_conversion
|
||||||
from ..util import format_exception, get_resource_path, get_version
|
from ..util import format_exception, get_resource_path, get_version
|
||||||
from .logic import Alert, CollapsibleBox, DangerzoneGui, UpdateDialog
|
from .logic import Alert, CollapsibleBox, DangerzoneGui, UpdateDialog
|
||||||
from .updater import UpdateReport
|
from .updater import UpdateReport
|
||||||
|
@ -191,11 +197,14 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
header_layout.addWidget(self.hamburger_button)
|
header_layout.addWidget(self.hamburger_button)
|
||||||
header_layout.addSpacing(15)
|
header_layout.addSpacing(15)
|
||||||
|
|
||||||
if self.dangerzone.isolation_provider.should_wait_install():
|
if isinstance(self.dangerzone.isolation_provider, Container):
|
||||||
# Waiting widget replaces content widget while container runtime isn't available
|
# Waiting widget replaces content widget while container runtime isn't available
|
||||||
self.waiting_widget: WaitingWidget = WaitingWidgetContainer(self.dangerzone)
|
self.waiting_widget: WaitingWidget = WaitingWidgetContainer(self.dangerzone)
|
||||||
self.waiting_widget.finished.connect(self.waiting_finished)
|
self.waiting_widget.finished.connect(self.waiting_finished)
|
||||||
else:
|
|
||||||
|
elif isinstance(self.dangerzone.isolation_provider, Dummy) or isinstance(
|
||||||
|
self.dangerzone.isolation_provider, Qubes
|
||||||
|
):
|
||||||
# Don't wait with dummy converter and on Qubes.
|
# Don't wait with dummy converter and on Qubes.
|
||||||
self.waiting_widget = WaitingWidget()
|
self.waiting_widget = WaitingWidget()
|
||||||
self.dangerzone.is_waiting_finished = True
|
self.dangerzone.is_waiting_finished = True
|
||||||
|
@ -491,11 +500,12 @@ class WaitingWidgetContainer(WaitingWidget):
|
||||||
error: Optional[str] = None
|
error: Optional[str] = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.dangerzone.isolation_provider.is_available()
|
assert isinstance(self.dangerzone.isolation_provider, (Dummy, Container))
|
||||||
except errors.NoContainerTechException as e:
|
self.dangerzone.isolation_provider.is_runtime_available()
|
||||||
|
except NoContainerTechException as e:
|
||||||
log.error(str(e))
|
log.error(str(e))
|
||||||
state = "not_installed"
|
state = "not_installed"
|
||||||
except errors.NotAvailableContainerTechException as e:
|
except NotAvailableContainerTechException as e:
|
||||||
log.error(str(e))
|
log.error(str(e))
|
||||||
state = "not_running"
|
state = "not_running"
|
||||||
error = e.error
|
error = e.error
|
||||||
|
|
|
@ -254,16 +254,6 @@ class IsolationProvider(ABC):
|
||||||
)
|
)
|
||||||
return errors.exception_from_error_code(error_code)
|
return errors.exception_from_error_code(error_code)
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def should_wait_install(self) -> bool:
|
|
||||||
"""Whether this isolation provider takes a lot of time to install."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def is_available(self) -> bool:
|
|
||||||
"""Whether the backing implementation of the isolation provider is available."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_max_parallel_conversions(self) -> int:
|
def get_max_parallel_conversions(self) -> int:
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
|
import gzip
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import shlex
|
import shlex
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import List
|
from typing import Dict, List, Tuple
|
||||||
|
|
||||||
from .. import container_utils, errors
|
|
||||||
from ..document import Document
|
from ..document import Document
|
||||||
from ..util import get_resource_path, get_subprocess_startupinfo
|
from ..util import get_resource_path, get_subprocess_startupinfo
|
||||||
from .base import IsolationProvider, terminate_process_group
|
from .base import IsolationProvider, terminate_process_group
|
||||||
|
@ -24,8 +26,88 @@ else:
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class NoContainerTechException(Exception):
|
||||||
|
def __init__(self, container_tech: str) -> None:
|
||||||
|
super().__init__(f"{container_tech} is not installed")
|
||||||
|
|
||||||
|
|
||||||
|
class NotAvailableContainerTechException(Exception):
|
||||||
|
def __init__(self, container_tech: str, error: str) -> None:
|
||||||
|
self.error = error
|
||||||
|
self.container_tech = container_tech
|
||||||
|
super().__init__(f"{container_tech} is not available")
|
||||||
|
|
||||||
|
|
||||||
|
class ImageNotPresentException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ImageInstallationException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Container(IsolationProvider):
|
class Container(IsolationProvider):
|
||||||
# Name of the dangerzone container
|
# Name of the dangerzone container
|
||||||
|
CONTAINER_NAME = "dangerzone.rocks/dangerzone"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_runtime_name() -> str:
|
||||||
|
if platform.system() == "Linux":
|
||||||
|
runtime_name = "podman"
|
||||||
|
else:
|
||||||
|
# Windows, Darwin, and unknown use docker for now, dangerzone-vm eventually
|
||||||
|
runtime_name = "docker"
|
||||||
|
return runtime_name
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_runtime_version() -> Tuple[int, int]:
|
||||||
|
"""Get the major/minor parts of the Docker/Podman version.
|
||||||
|
|
||||||
|
Some of the operations we perform in this module rely on some Podman features
|
||||||
|
that are not available across all of our platforms. In order to have a proper
|
||||||
|
fallback, we need to know the Podman version. More specifically, we're fine with
|
||||||
|
just knowing the major and minor version, since writing/installing a full-blown
|
||||||
|
semver parser is an overkill.
|
||||||
|
"""
|
||||||
|
# Get the Docker/Podman version, using a Go template.
|
||||||
|
runtime = Container.get_runtime_name()
|
||||||
|
if runtime == "podman":
|
||||||
|
query = "{{.Client.Version}}"
|
||||||
|
else:
|
||||||
|
query = "{{.Server.Version}}"
|
||||||
|
|
||||||
|
cmd = [runtime, "version", "-f", query]
|
||||||
|
try:
|
||||||
|
version = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
startupinfo=get_subprocess_startupinfo(),
|
||||||
|
capture_output=True,
|
||||||
|
check=True,
|
||||||
|
).stdout.decode()
|
||||||
|
except Exception as e:
|
||||||
|
msg = f"Could not get the version of the {runtime.capitalize()} tool: {e}"
|
||||||
|
raise RuntimeError(msg) from e
|
||||||
|
|
||||||
|
# Parse this version and return the major/minor parts, since we don't need the
|
||||||
|
# rest.
|
||||||
|
try:
|
||||||
|
major, minor, _ = version.split(".", 3)
|
||||||
|
return (int(major), int(minor))
|
||||||
|
except Exception as e:
|
||||||
|
msg = (
|
||||||
|
f"Could not parse the version of the {runtime.capitalize()} tool"
|
||||||
|
f" (found: '{version}') due to the following error: {e}"
|
||||||
|
)
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_runtime() -> str:
|
||||||
|
container_tech = Container.get_runtime_name()
|
||||||
|
runtime = shutil.which(container_tech)
|
||||||
|
if runtime is None:
|
||||||
|
raise NoContainerTechException(container_tech)
|
||||||
|
return runtime
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_runtime_security_args() -> List[str]:
|
def get_runtime_security_args() -> List[str]:
|
||||||
"""Security options applicable to the outer Dangerzone container.
|
"""Security options applicable to the outer Dangerzone container.
|
||||||
|
@ -46,12 +128,12 @@ class Container(IsolationProvider):
|
||||||
* Do not log the container's output.
|
* Do not log the container's output.
|
||||||
* Do not map the host user to the container, with `--userns nomap` (available
|
* Do not map the host user to the container, with `--userns nomap` (available
|
||||||
from Podman 4.1 onwards)
|
from Podman 4.1 onwards)
|
||||||
|
- This particular argument is specified in `start_doc_to_pixels_proc()`, but
|
||||||
|
should move here once #748 is merged.
|
||||||
"""
|
"""
|
||||||
if container_utils.get_runtime_name() == "podman":
|
if Container.get_runtime_name() == "podman":
|
||||||
security_args = ["--log-driver", "none"]
|
security_args = ["--log-driver", "none"]
|
||||||
security_args += ["--security-opt", "no-new-privileges"]
|
security_args += ["--security-opt", "no-new-privileges"]
|
||||||
if container_utils.get_runtime_version() >= (4, 1):
|
|
||||||
security_args += ["--userns", "nomap"]
|
|
||||||
else:
|
else:
|
||||||
security_args = ["--security-opt=no-new-privileges:true"]
|
security_args = ["--security-opt=no-new-privileges:true"]
|
||||||
|
|
||||||
|
@ -73,6 +155,110 @@ class Container(IsolationProvider):
|
||||||
|
|
||||||
return security_args
|
return security_args
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def list_image_tags() -> Dict[str, str]:
|
||||||
|
"""Get the tags of all loaded Dangerzone images.
|
||||||
|
|
||||||
|
This method returns a mapping of image tags to image IDs, for all Dangerzone
|
||||||
|
images. This can be useful when we want to find which are the local image tags,
|
||||||
|
and which image ID does the "latest" tag point to.
|
||||||
|
"""
|
||||||
|
images = json.loads(
|
||||||
|
subprocess.check_output(
|
||||||
|
[
|
||||||
|
Container.get_runtime(),
|
||||||
|
"image",
|
||||||
|
"list",
|
||||||
|
"--format",
|
||||||
|
"json",
|
||||||
|
Container.CONTAINER_NAME,
|
||||||
|
],
|
||||||
|
text=True,
|
||||||
|
startupinfo=get_subprocess_startupinfo(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Grab every image name and associate it with an image ID.
|
||||||
|
tags = {}
|
||||||
|
for image in images:
|
||||||
|
for name in image["Names"]:
|
||||||
|
tag = name.split(":")[1]
|
||||||
|
tags[tag] = image["Id"]
|
||||||
|
|
||||||
|
return tags
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete_image_tag(tag: str) -> None:
|
||||||
|
"""Delete a Dangerzone image tag."""
|
||||||
|
name = Container.CONTAINER_NAME + ":" + tag
|
||||||
|
log.warning(f"Deleting old container image: {name}")
|
||||||
|
try:
|
||||||
|
subprocess.check_output(
|
||||||
|
[Container.get_runtime(), "rmi", "--force", name],
|
||||||
|
startupinfo=get_subprocess_startupinfo(),
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(
|
||||||
|
f"Couldn't delete old container image '{name}', so leaving it there."
|
||||||
|
f" Original error: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def add_image_tag(cur_tag: str, new_tag: str) -> None:
|
||||||
|
"""Add a tag to an existing Dangerzone image."""
|
||||||
|
cur_image_name = Container.CONTAINER_NAME + ":" + cur_tag
|
||||||
|
new_image_name = Container.CONTAINER_NAME + ":" + new_tag
|
||||||
|
subprocess.check_output(
|
||||||
|
[
|
||||||
|
Container.get_runtime(),
|
||||||
|
"tag",
|
||||||
|
cur_image_name,
|
||||||
|
new_image_name,
|
||||||
|
],
|
||||||
|
startupinfo=get_subprocess_startupinfo(),
|
||||||
|
)
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
f"Successfully tagged container image '{cur_image_name}' as '{new_image_name}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_expected_tag() -> str:
|
||||||
|
"""Get the tag of the Dangerzone image tarball from the image-id.txt file."""
|
||||||
|
with open(get_resource_path("image-id.txt")) as f:
|
||||||
|
return f.read().strip()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def load_image_tarball() -> None:
|
||||||
|
log.info("Installing Dangerzone container image...")
|
||||||
|
p = subprocess.Popen(
|
||||||
|
[Container.get_runtime(), "load"],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
startupinfo=get_subprocess_startupinfo(),
|
||||||
|
)
|
||||||
|
|
||||||
|
chunk_size = 4 << 20
|
||||||
|
compressed_container_path = get_resource_path("container.tar.gz")
|
||||||
|
with gzip.open(compressed_container_path) as f:
|
||||||
|
while True:
|
||||||
|
chunk = f.read(chunk_size)
|
||||||
|
if len(chunk) > 0:
|
||||||
|
if p.stdin:
|
||||||
|
p.stdin.write(chunk)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
_, err = p.communicate()
|
||||||
|
if p.returncode < 0:
|
||||||
|
if err:
|
||||||
|
error = err.decode()
|
||||||
|
else:
|
||||||
|
error = "No output"
|
||||||
|
raise ImageInstallationException(
|
||||||
|
f"Could not install container image: {error}"
|
||||||
|
)
|
||||||
|
|
||||||
|
log.info("Successfully installed container image from")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def install() -> bool:
|
def install() -> bool:
|
||||||
"""Install the container image tarball, or verify that it's already installed.
|
"""Install the container image tarball, or verify that it's already installed.
|
||||||
|
@ -81,46 +267,50 @@ class Container(IsolationProvider):
|
||||||
1. Get the tags of any locally available images that match Dangerzone's image
|
1. Get the tags of any locally available images that match Dangerzone's image
|
||||||
name.
|
name.
|
||||||
2. Get the expected image tag from the image-id.txt file.
|
2. Get the expected image tag from the image-id.txt file.
|
||||||
- If this tag is present in the local images, then we can return.
|
- If this tag is present in the local images, and that image is also tagged
|
||||||
|
as "latest", then we can return.
|
||||||
- Else, prune the older container images and continue.
|
- Else, prune the older container images and continue.
|
||||||
3. Load the image tarball and make sure it matches the expected tag.
|
3. Load the image tarball and make sure it matches the expected tag.
|
||||||
|
4. Tag that image as "latest", and mark the installation as finished.
|
||||||
"""
|
"""
|
||||||
old_tags = container_utils.list_image_tags()
|
old_tags = Container.list_image_tags()
|
||||||
expected_tag = container_utils.get_expected_tag()
|
expected_tag = Container.get_expected_tag()
|
||||||
|
|
||||||
if expected_tag not in old_tags:
|
if expected_tag not in old_tags:
|
||||||
# Prune older container images.
|
# Prune older container images.
|
||||||
log.info(
|
log.info(
|
||||||
f"Could not find a Dangerzone container image with tag '{expected_tag}'"
|
f"Could not find a Dangerzone container image with tag '{expected_tag}'"
|
||||||
)
|
)
|
||||||
for tag in old_tags:
|
for tag in old_tags.keys():
|
||||||
container_utils.delete_image_tag(tag)
|
Container.delete_image_tag(tag)
|
||||||
|
elif old_tags[expected_tag] != old_tags.get("latest"):
|
||||||
|
log.info(f"The expected tag '{expected_tag}' is not the latest one")
|
||||||
|
Container.add_image_tag(expected_tag, "latest")
|
||||||
|
return True
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Load the image tarball into the container runtime.
|
# Load the image tarball into the container runtime.
|
||||||
container_utils.load_image_tarball()
|
Container.load_image_tarball()
|
||||||
|
|
||||||
# Check that the container image has the expected image tag.
|
# Check that the container image has the expected image tag.
|
||||||
# See https://github.com/freedomofpress/dangerzone/issues/988 for an example
|
# See https://github.com/freedomofpress/dangerzone/issues/988 for an example
|
||||||
# where this was not the case.
|
# where this was not the case.
|
||||||
new_tags = container_utils.list_image_tags()
|
new_tags = Container.list_image_tags()
|
||||||
if expected_tag not in new_tags:
|
if expected_tag not in new_tags:
|
||||||
raise errors.ImageNotPresentException(
|
raise ImageNotPresentException(
|
||||||
f"Could not find expected tag '{expected_tag}' after loading the"
|
f"Could not find expected tag '{expected_tag}' after loading the"
|
||||||
" container image tarball"
|
" container image tarball"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Mark the expected tag as "latest".
|
||||||
|
Container.add_image_tag(expected_tag, "latest")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def should_wait_install() -> bool:
|
def is_runtime_available() -> bool:
|
||||||
return True
|
container_runtime = Container.get_runtime()
|
||||||
|
runtime_name = Container.get_runtime_name()
|
||||||
@staticmethod
|
|
||||||
def is_available() -> bool:
|
|
||||||
container_runtime = container_utils.get_runtime()
|
|
||||||
runtime_name = container_utils.get_runtime_name()
|
|
||||||
# Can we run `docker/podman image ls` without an error
|
# Can we run `docker/podman image ls` without an error
|
||||||
with subprocess.Popen(
|
with subprocess.Popen(
|
||||||
[container_runtime, "image", "ls"],
|
[container_runtime, "image", "ls"],
|
||||||
|
@ -130,9 +320,7 @@ class Container(IsolationProvider):
|
||||||
) as p:
|
) as p:
|
||||||
_, stderr = p.communicate()
|
_, stderr = p.communicate()
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise errors.NotAvailableContainerTechException(
|
raise NotAvailableContainerTechException(runtime_name, stderr.decode())
|
||||||
runtime_name, stderr.decode()
|
|
||||||
)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def doc_to_pixels_container_name(self, document: Document) -> str:
|
def doc_to_pixels_container_name(self, document: Document) -> str:
|
||||||
|
@ -165,22 +353,21 @@ class Container(IsolationProvider):
|
||||||
self,
|
self,
|
||||||
command: List[str],
|
command: List[str],
|
||||||
name: str,
|
name: str,
|
||||||
|
extra_args: List[str] = [],
|
||||||
) -> subprocess.Popen:
|
) -> subprocess.Popen:
|
||||||
container_runtime = container_utils.get_runtime()
|
container_runtime = self.get_runtime()
|
||||||
security_args = self.get_runtime_security_args()
|
security_args = self.get_runtime_security_args()
|
||||||
enable_stdin = ["-i"]
|
enable_stdin = ["-i"]
|
||||||
set_name = ["--name", name]
|
set_name = ["--name", name]
|
||||||
prevent_leakage_args = ["--rm"]
|
prevent_leakage_args = ["--rm"]
|
||||||
image_name = [
|
|
||||||
container_utils.CONTAINER_NAME + ":" + container_utils.get_expected_tag()
|
|
||||||
]
|
|
||||||
args = (
|
args = (
|
||||||
["run"]
|
["run"]
|
||||||
+ security_args
|
+ security_args
|
||||||
+ prevent_leakage_args
|
+ prevent_leakage_args
|
||||||
+ enable_stdin
|
+ enable_stdin
|
||||||
+ set_name
|
+ set_name
|
||||||
+ image_name
|
+ extra_args
|
||||||
|
+ [self.CONTAINER_NAME]
|
||||||
+ command
|
+ command
|
||||||
)
|
)
|
||||||
args = [container_runtime] + args
|
args = [container_runtime] + args
|
||||||
|
@ -196,7 +383,7 @@ class Container(IsolationProvider):
|
||||||
connected to the Docker daemon, and killing it will just close the associated
|
connected to the Docker daemon, and killing it will just close the associated
|
||||||
standard streams.
|
standard streams.
|
||||||
"""
|
"""
|
||||||
container_runtime = container_utils.get_runtime()
|
container_runtime = self.get_runtime()
|
||||||
cmd = [container_runtime, "kill", name]
|
cmd = [container_runtime, "kill", name]
|
||||||
try:
|
try:
|
||||||
# We do not check the exit code of the process here, since the container may
|
# We do not check the exit code of the process here, since the container may
|
||||||
|
@ -229,8 +416,15 @@ class Container(IsolationProvider):
|
||||||
"-m",
|
"-m",
|
||||||
"dangerzone.conversion.doc_to_pixels",
|
"dangerzone.conversion.doc_to_pixels",
|
||||||
]
|
]
|
||||||
|
# NOTE: Using `--userns nomap` is available only on Podman >= 4.1.0.
|
||||||
|
# XXX: Move this under `get_runtime_security_args()` once #748 is merged.
|
||||||
|
extra_args = []
|
||||||
|
if Container.get_runtime_name() == "podman":
|
||||||
|
if Container.get_runtime_version() >= (4, 1):
|
||||||
|
extra_args += ["--userns", "nomap"]
|
||||||
|
|
||||||
name = self.doc_to_pixels_container_name(document)
|
name = self.doc_to_pixels_container_name(document)
|
||||||
return self.exec_container(command, name=name)
|
return self.exec_container(command, name=name, extra_args=extra_args)
|
||||||
|
|
||||||
def terminate_doc_to_pixels_proc(
|
def terminate_doc_to_pixels_proc(
|
||||||
self, document: Document, p: subprocess.Popen
|
self, document: Document, p: subprocess.Popen
|
||||||
|
@ -253,7 +447,7 @@ class Container(IsolationProvider):
|
||||||
# after a podman kill / docker kill invocation, this will likely be the case,
|
# after a podman kill / docker kill invocation, this will likely be the case,
|
||||||
# else the container runtime (Docker/Podman) has experienced a problem, and we
|
# else the container runtime (Docker/Podman) has experienced a problem, and we
|
||||||
# should report it.
|
# should report it.
|
||||||
container_runtime = container_utils.get_runtime()
|
container_runtime = self.get_runtime()
|
||||||
name = self.doc_to_pixels_container_name(document)
|
name = self.doc_to_pixels_container_name(document)
|
||||||
all_containers = subprocess.run(
|
all_containers = subprocess.run(
|
||||||
[container_runtime, "ps", "-a"],
|
[container_runtime, "ps", "-a"],
|
||||||
|
@ -275,11 +469,11 @@ class Container(IsolationProvider):
|
||||||
if cpu_count is not None:
|
if cpu_count is not None:
|
||||||
n_cpu = cpu_count
|
n_cpu = cpu_count
|
||||||
|
|
||||||
elif container_utils.get_runtime_name() == "docker":
|
elif self.get_runtime_name() == "docker":
|
||||||
# For Windows and MacOS containers run in VM
|
# For Windows and MacOS containers run in VM
|
||||||
# So we obtain the CPU count for the VM
|
# So we obtain the CPU count for the VM
|
||||||
n_cpu_str = subprocess.check_output(
|
n_cpu_str = subprocess.check_output(
|
||||||
[container_utils.get_runtime(), "info", "--format", "{{.NCPU}}"],
|
[self.get_runtime(), "info", "--format", "{{.NCPU}}"],
|
||||||
text=True,
|
text=True,
|
||||||
startupinfo=get_subprocess_startupinfo(),
|
startupinfo=get_subprocess_startupinfo(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -40,13 +40,9 @@ class Dummy(IsolationProvider):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_available() -> bool:
|
def is_runtime_available() -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def should_wait_install() -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def start_doc_to_pixels_proc(self, document: Document) -> subprocess.Popen:
|
def start_doc_to_pixels_proc(self, document: Document) -> subprocess.Popen:
|
||||||
cmd = [
|
cmd = [
|
||||||
sys.executable,
|
sys.executable,
|
||||||
|
|
|
@ -21,14 +21,6 @@ class Qubes(IsolationProvider):
|
||||||
def install(self) -> bool:
|
def install(self) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_available() -> bool:
|
|
||||||
return True
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def should_wait_install() -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_max_parallel_conversions(self) -> int:
|
def get_max_parallel_conversions(self) -> int:
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
|
@ -129,6 +129,7 @@ version. For example:
|
||||||
```
|
```
|
||||||
$ docker images dangerzone.rocks/dangerzone
|
$ docker images dangerzone.rocks/dangerzone
|
||||||
REPOSITORY TAG IMAGE ID CREATED SIZE
|
REPOSITORY TAG IMAGE ID CREATED SIZE
|
||||||
|
dangerzone.rocks/dangerzone latest <image ID> <date> <size>
|
||||||
dangerzone.rocks/dangerzone <tag> <image ID> <date> <size>
|
dangerzone.rocks/dangerzone <tag> <image ID> <date> <size>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -140,6 +141,7 @@ and seeing the following differences:
|
||||||
```
|
```
|
||||||
$ docker images dangerzone.rocks/dangerzone
|
$ docker images dangerzone.rocks/dangerzone
|
||||||
REPOSITORY TAG IMAGE ID CREATED SIZE
|
REPOSITORY TAG IMAGE ID CREATED SIZE
|
||||||
|
dangerzone.rocks/dangerzone latest <different ID> <newer date> <different size>
|
||||||
dangerzone.rocks/dangerzone <other tag> <different ID> <newer date> <different size>
|
dangerzone.rocks/dangerzone <other tag> <different ID> <newer date> <different size>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -51,17 +51,8 @@ doit <task>
|
||||||
use_cache = true
|
use_cache = true
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!WARNING]
|
* You can pass the following global parameters with `doit <param>=<value>`:
|
||||||
> Using caching may speed up image builds, but is not suitable for release
|
- `runtime`: The container runtime to use. Either `podman` or `docker`
|
||||||
> artifacts. The ID of our base container image (Alpine Linux) does not change
|
- `release_dir`: Where to store the release artifacts. Default path is
|
||||||
> that often, but its APK package index does. So, if we use caching, we risk
|
|
||||||
> skipping the `apk upgrade` layer and end up with packages that are days
|
|
||||||
> behind.
|
|
||||||
|
|
||||||
* You can pass the following environment variables to the script, in order to
|
|
||||||
affect some global parameters:
|
|
||||||
- `CONTAINER_RUNTIME`: The container runtime to use. Either `podman` (default)
|
|
||||||
or `docker`.
|
|
||||||
- `RELEASE_DIR`: Where to store the release artifacts. Default path is
|
|
||||||
`~/release-assets/<version>`
|
`~/release-assets/<version>`
|
||||||
- `APPLE_ID`: The Apple ID to use when signing/notarizing the macOS DMG.
|
- `apple_id`: The Apple ID to use when signing/notarizing the macOS DMG.
|
||||||
|
|
22
dodo.py
22
dodo.py
|
@ -4,6 +4,7 @@ import platform
|
||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from doit import get_var
|
||||||
from doit.action import CmdAction
|
from doit.action import CmdAction
|
||||||
|
|
||||||
ARCH = "arm64" if platform.machine() == "arm64" else "i686"
|
ARCH = "arm64" if platform.machine() == "arm64" else "i686"
|
||||||
|
@ -12,11 +13,15 @@ FEDORA_VERSIONS = ["40", "41"]
|
||||||
DEBIAN_VERSIONS = ["bullseye", "focal", "jammy", "mantic", "noble", "trixie"]
|
DEBIAN_VERSIONS = ["bullseye", "focal", "jammy", "mantic", "noble", "trixie"]
|
||||||
|
|
||||||
### Global parameters
|
### Global parameters
|
||||||
|
#
|
||||||
|
# Read more about global parameters in
|
||||||
|
# https://pydoit.org/task-args.html#command-line-variables-doit-get-var
|
||||||
|
|
||||||
CONTAINER_RUNTIME = os.environ.get("CONTAINER_RUNTIME", "podman")
|
CONTAINER_RUNTIME = get_var("runtime", "podman")
|
||||||
DEFAULT_RELEASE_DIR = Path.home() / "release-assets" / VERSION
|
DEFAULT_RELEASE_DIR = Path.home() / "release-assets" / VERSION
|
||||||
RELEASE_DIR = Path(os.environ.get("RELEASE_DIR", DEFAULT_RELEASE_DIR))
|
# XXX: Workaround for https://github.com/pydoit/doit/issues/164
|
||||||
APPLE_ID = os.environ.get("APPLE_ID", None)
|
RELEASE_DIR = Path(get_var("release_dir", None) or DEFAULT_RELEASE_DIR)
|
||||||
|
APPLE_ID = get_var("apple_id", None)
|
||||||
|
|
||||||
### Task Parameters
|
### Task Parameters
|
||||||
|
|
||||||
|
@ -45,8 +50,15 @@ PARAM_USE_CACHE = {
|
||||||
|
|
||||||
def list_files(path, recursive=False):
|
def list_files(path, recursive=False):
|
||||||
"""List files in a directory, and optionally traverse into subdirectories."""
|
"""List files in a directory, and optionally traverse into subdirectories."""
|
||||||
glob_fn = Path(path).rglob if recursive else Path(path).glob
|
filepaths = []
|
||||||
return [f for f in glob_fn("*") if f.is_file() and not f.suffix == ".pyc"]
|
for root, _, files in os.walk(path):
|
||||||
|
for f in files:
|
||||||
|
if f.endswith(".pyc"):
|
||||||
|
continue
|
||||||
|
filepaths.append(Path(root) / f)
|
||||||
|
if not recursive:
|
||||||
|
break
|
||||||
|
return filepaths
|
||||||
|
|
||||||
|
|
||||||
def list_language_data():
|
def list_language_data():
|
||||||
|
|
|
@ -73,7 +73,7 @@ def main():
|
||||||
dirty_ident = secrets.token_hex(2)
|
dirty_ident = secrets.token_hex(2)
|
||||||
tag = (
|
tag = (
|
||||||
subprocess.check_output(
|
subprocess.check_output(
|
||||||
["git", "describe", "--long", "--first-parent", f"--dirty=-{dirty_ident}"],
|
["git", "describe", "--first-parent", f"--dirty=-{dirty_ident}"],
|
||||||
)
|
)
|
||||||
.decode()
|
.decode()
|
||||||
.strip()[1:] # remove the "v" prefix of the tag.
|
.strip()[1:] # remove the "v" prefix of the tag.
|
||||||
|
@ -97,9 +97,11 @@ def main():
|
||||||
check=True,
|
check=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Build the container image, and tag it with the calculated tag
|
# Build the container image, and tag it with two tags; the one we calculated
|
||||||
|
# above, and the "latest" tag.
|
||||||
print("Building container image")
|
print("Building container image")
|
||||||
cache_args = [] if args.use_cache else ["--no-cache"]
|
cache_args = [] if args.use_cache else ["--no-cache"]
|
||||||
|
image_name_latest = IMAGE_NAME + ":latest"
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
[
|
[
|
||||||
args.runtime,
|
args.runtime,
|
||||||
|
@ -113,6 +115,8 @@ def main():
|
||||||
"-f",
|
"-f",
|
||||||
"Dockerfile",
|
"Dockerfile",
|
||||||
"--tag",
|
"--tag",
|
||||||
|
image_name_latest,
|
||||||
|
"--tag",
|
||||||
image_name_tagged,
|
image_name_tagged,
|
||||||
],
|
],
|
||||||
check=True,
|
check=True,
|
||||||
|
|
|
@ -17,23 +17,22 @@ signtool.exe sign /v /d "Dangerzone" /a /n "Freedom of the Press Foundation" /fd
|
||||||
REM verify the signature of dangerzone-cli.exe
|
REM verify the signature of dangerzone-cli.exe
|
||||||
signtool.exe verify /pa build\exe.win-amd64-3.12\dangerzone-cli.exe
|
signtool.exe verify /pa build\exe.win-amd64-3.12\dangerzone-cli.exe
|
||||||
|
|
||||||
REM build the wxs file
|
REM build the wix file
|
||||||
python install\windows\build-wxs.py
|
python install\windows\build-wxs.py > build\Dangerzone.wxs
|
||||||
|
|
||||||
REM build the msi package
|
REM build the msi package
|
||||||
cd build
|
cd build
|
||||||
wix build -arch x64 -ext WixToolset.UI.wixext .\Dangerzone.wxs -out Dangerzone.msi
|
candle.exe Dangerzone.wxs
|
||||||
|
light.exe -ext WixUIExtension Dangerzone.wixobj
|
||||||
REM validate Dangerzone.msi
|
|
||||||
wix msi validate Dangerzone.msi
|
|
||||||
|
|
||||||
REM code sign Dangerzone.msi
|
REM code sign Dangerzone.msi
|
||||||
|
insignia.exe -im Dangerzone.msi
|
||||||
signtool.exe sign /v /d "Dangerzone" /a /n "Freedom of the Press Foundation" /fd sha256 /t http://time.certum.pl/ Dangerzone.msi
|
signtool.exe sign /v /d "Dangerzone" /a /n "Freedom of the Press Foundation" /fd sha256 /t http://time.certum.pl/ Dangerzone.msi
|
||||||
|
|
||||||
REM verify the signature of Dangerzone.msi
|
REM verify the signature of Dangerzone.msi
|
||||||
signtool.exe verify /pa Dangerzone.msi
|
signtool.exe verify /pa Dangerzone.msi
|
||||||
|
|
||||||
REM move Dangerzone.msi to dist
|
REM moving Dangerzone.msi to dist
|
||||||
cd ..
|
cd ..
|
||||||
mkdir dist
|
mkdir dist
|
||||||
move build\Dangerzone.msi dist
|
move build\Dangerzone.msi dist
|
||||||
|
|
|
@ -4,75 +4,114 @@ import uuid
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
|
||||||
def build_data(base_path, path_prefix, dir_id, dir_name):
|
def build_data(dirname, dir_prefix, id_, name):
|
||||||
data = {
|
data = {
|
||||||
"directory_name": dir_name,
|
"id": id_,
|
||||||
"directory_id": dir_id,
|
"name": name,
|
||||||
"files": [],
|
"files": [],
|
||||||
"dirs": [],
|
"dirs": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
if dir_id == "INSTALLFOLDER":
|
for basename in os.listdir(dirname):
|
||||||
data["component_id"] = "ApplicationFiles"
|
filename = os.path.join(dirname, basename)
|
||||||
else:
|
if os.path.isfile(filename):
|
||||||
data["component_id"] = "Component" + dir_id
|
data["files"].append(os.path.join(dir_prefix, basename))
|
||||||
data["component_guid"] = str(uuid.uuid4()).upper()
|
elif os.path.isdir(filename):
|
||||||
|
if id_ == "INSTALLDIR":
|
||||||
for entry in os.listdir(base_path):
|
id_prefix = "Folder"
|
||||||
entry_path = os.path.join(base_path, entry)
|
|
||||||
if os.path.isfile(entry_path):
|
|
||||||
data["files"].append(os.path.join(path_prefix, entry))
|
|
||||||
elif os.path.isdir(entry_path):
|
|
||||||
if dir_id == "INSTALLFOLDER":
|
|
||||||
next_dir_prefix = "Folder"
|
|
||||||
else:
|
else:
|
||||||
next_dir_prefix = dir_id
|
id_prefix = id_
|
||||||
|
|
||||||
# Skip lib/PySide6/examples folder due to ilegal file names
|
# Skip lib/PySide6/examples folder due to ilegal file names
|
||||||
if "\\build\\exe.win-amd64-3.12\\lib\\PySide6\\examples" in base_path:
|
if "\\build\\exe.win-amd64-3.12\\lib\\PySide6\\examples" in dirname:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Skip lib/PySide6/qml/QtQuick folder due to ilegal file names
|
# Skip lib/PySide6/qml/QtQuick folder due to ilegal file names
|
||||||
# XXX Since we're not using Qml it should be no problem
|
# XXX Since we're not using Qml it should be no problem
|
||||||
if "\\build\\exe.win-amd64-3.12\\lib\\PySide6\\qml\\QtQuick" in base_path:
|
if "\\build\\exe.win-amd64-3.12\\lib\\PySide6\\qml\\QtQuick" in dirname:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
next_dir_id = next_dir_prefix + entry.capitalize().replace("-", "_")
|
id_value = f"{id_prefix}{basename.capitalize().replace('-', '_')}"
|
||||||
subdata = build_data(
|
data["dirs"].append(
|
||||||
os.path.join(base_path, entry),
|
build_data(
|
||||||
os.path.join(path_prefix, entry),
|
os.path.join(dirname, basename),
|
||||||
next_dir_id,
|
os.path.join(dir_prefix, basename),
|
||||||
entry,
|
id_value,
|
||||||
|
basename,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add the subdirectory only if it contains files or subdirectories
|
if len(data["files"]) > 0:
|
||||||
if subdata["files"] or subdata["dirs"]:
|
if id_ == "INSTALLDIR":
|
||||||
data["dirs"].append(subdata)
|
data["component_id"] = "ApplicationFiles"
|
||||||
|
else:
|
||||||
|
data["component_id"] = "FolderComponent" + id_[len("Folder") :]
|
||||||
|
data["component_guid"] = str(uuid.uuid4())
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def build_directory_xml(root, data):
|
def build_dir_xml(root, data):
|
||||||
attrs = {}
|
attrs = {}
|
||||||
attrs["Id"] = data["directory_id"]
|
if "id" in data:
|
||||||
attrs["Name"] = data["directory_name"]
|
attrs["Id"] = data["id"]
|
||||||
directory_el = ET.SubElement(root, "Directory", attrs)
|
if "name" in data:
|
||||||
|
attrs["Name"] = data["name"]
|
||||||
|
el = ET.SubElement(root, "Directory", attrs)
|
||||||
for subdata in data["dirs"]:
|
for subdata in data["dirs"]:
|
||||||
build_directory_xml(directory_el, subdata)
|
build_dir_xml(el, subdata)
|
||||||
|
|
||||||
|
# If this is the ProgramMenuFolder, add the menu component
|
||||||
|
if "id" in data and data["id"] == "ProgramMenuFolder":
|
||||||
|
component_el = ET.SubElement(
|
||||||
|
el,
|
||||||
|
"Component",
|
||||||
|
Id="ApplicationShortcuts",
|
||||||
|
Guid="539e7de8-a124-4c09-aa55-0dd516aad7bc",
|
||||||
|
)
|
||||||
|
ET.SubElement(
|
||||||
|
component_el,
|
||||||
|
"Shortcut",
|
||||||
|
Id="ApplicationShortcut1",
|
||||||
|
Name="Dangerzone",
|
||||||
|
Description="Dangerzone",
|
||||||
|
Target="[INSTALLDIR]dangerzone.exe",
|
||||||
|
WorkingDirectory="INSTALLDIR",
|
||||||
|
)
|
||||||
|
ET.SubElement(
|
||||||
|
component_el,
|
||||||
|
"RegistryValue",
|
||||||
|
Root="HKCU",
|
||||||
|
Key="Software\Freedom of the Press Foundation\Dangerzone",
|
||||||
|
Name="installed",
|
||||||
|
Type="integer",
|
||||||
|
Value="1",
|
||||||
|
KeyPath="yes",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_components_xml(root, data):
|
def build_components_xml(root, data):
|
||||||
component_el = ET.SubElement(
|
component_ids = []
|
||||||
root,
|
if "component_id" in data:
|
||||||
"Component",
|
component_ids.append(data["component_id"])
|
||||||
Id=data["component_id"],
|
|
||||||
Guid=data["component_guid"],
|
|
||||||
Directory=data["directory_id"],
|
|
||||||
)
|
|
||||||
for filename in data["files"]:
|
|
||||||
ET.SubElement(component_el, "File", Source=filename)
|
|
||||||
for subdata in data["dirs"]:
|
for subdata in data["dirs"]:
|
||||||
build_components_xml(root, subdata)
|
if "component_guid" in subdata:
|
||||||
|
dir_ref_el = ET.SubElement(root, "DirectoryRef", Id=subdata["id"])
|
||||||
|
component_el = ET.SubElement(
|
||||||
|
dir_ref_el,
|
||||||
|
"Component",
|
||||||
|
Id=subdata["component_id"],
|
||||||
|
Guid=subdata["component_guid"],
|
||||||
|
)
|
||||||
|
for filename in subdata["files"]:
|
||||||
|
file_el = ET.SubElement(
|
||||||
|
component_el, "File", Source=filename, Id="file_" + uuid.uuid4().hex
|
||||||
|
)
|
||||||
|
|
||||||
|
component_ids += build_components_xml(root, subdata)
|
||||||
|
|
||||||
|
return component_ids
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
@ -86,188 +125,120 @@ def main():
|
||||||
# -rc markers.
|
# -rc markers.
|
||||||
version = f.read().strip().split("-")[0]
|
version = f.read().strip().split("-")[0]
|
||||||
|
|
||||||
build_dir = os.path.join(
|
dist_dir = os.path.join(
|
||||||
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
|
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
|
||||||
"build",
|
"build",
|
||||||
|
"exe.win-amd64-3.12",
|
||||||
)
|
)
|
||||||
|
|
||||||
cx_freeze_dir = "exe.win-amd64-3.12"
|
|
||||||
|
|
||||||
dist_dir = os.path.join(build_dir, cx_freeze_dir)
|
|
||||||
|
|
||||||
if not os.path.exists(dist_dir):
|
if not os.path.exists(dist_dir):
|
||||||
print("You must build the dangerzone binary before running this")
|
print("You must build the dangerzone binary before running this")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Prepare data for WiX file harvesting from the output of cx_Freeze
|
data = {
|
||||||
data = build_data(
|
"id": "TARGETDIR",
|
||||||
dist_dir,
|
"name": "SourceDir",
|
||||||
cx_freeze_dir,
|
"dirs": [
|
||||||
"INSTALLFOLDER",
|
{
|
||||||
"Dangerzone",
|
"id": "ProgramFilesFolder",
|
||||||
|
"dirs": [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ProgramMenuFolder",
|
||||||
|
"dirs": [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
data["dirs"][0]["dirs"].append(
|
||||||
|
build_data(
|
||||||
|
dist_dir,
|
||||||
|
"exe.win-amd64-3.12",
|
||||||
|
"INSTALLDIR",
|
||||||
|
"Dangerzone",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add the Wix root element
|
root_el = ET.Element("Wix", xmlns="http://schemas.microsoft.com/wix/2006/wi")
|
||||||
wix_el = ET.Element(
|
product_el = ET.SubElement(
|
||||||
"Wix",
|
root_el,
|
||||||
{
|
"Product",
|
||||||
"xmlns": "http://wixtoolset.org/schemas/v4/wxs",
|
|
||||||
"xmlns:ui": "http://wixtoolset.org/schemas/v4/wxs/ui",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add the Package element
|
|
||||||
package_el = ET.SubElement(
|
|
||||||
wix_el,
|
|
||||||
"Package",
|
|
||||||
Name="Dangerzone",
|
Name="Dangerzone",
|
||||||
Manufacturer="Freedom of the Press Foundation",
|
Manufacturer="Freedom of the Press Foundation",
|
||||||
UpgradeCode="12B9695C-965B-4BE0-BC33-21274E809576",
|
Id="*",
|
||||||
|
UpgradeCode="$(var.ProductUpgradeCode)",
|
||||||
Language="1033",
|
Language="1033",
|
||||||
Compressed="yes",
|
|
||||||
Codepage="1252",
|
Codepage="1252",
|
||||||
Version=version,
|
Version="$(var.ProductVersion)",
|
||||||
)
|
)
|
||||||
ET.SubElement(
|
ET.SubElement(
|
||||||
package_el,
|
product_el,
|
||||||
"SummaryInformation",
|
"Package",
|
||||||
|
Id="*",
|
||||||
Keywords="Installer",
|
Keywords="Installer",
|
||||||
Description="Dangerzone " + version + " Installer",
|
Description="Dangerzone $(var.ProductVersion) Installer",
|
||||||
Codepage="1252",
|
Manufacturer="Freedom of the Press Foundation",
|
||||||
|
InstallerVersion="100",
|
||||||
|
Languages="1033",
|
||||||
|
Compressed="yes",
|
||||||
|
SummaryCodepage="1252",
|
||||||
)
|
)
|
||||||
ET.SubElement(package_el, "MediaTemplate", EmbedCab="yes")
|
ET.SubElement(product_el, "Media", Id="1", Cabinet="product.cab", EmbedCab="yes")
|
||||||
ET.SubElement(
|
ET.SubElement(
|
||||||
package_el, "Icon", Id="ProductIcon", SourceFile="..\\share\\dangerzone.ico"
|
product_el, "Icon", Id="ProductIcon", SourceFile="..\\share\\dangerzone.ico"
|
||||||
)
|
)
|
||||||
ET.SubElement(package_el, "Property", Id="ARPPRODUCTICON", Value="ProductIcon")
|
ET.SubElement(product_el, "Property", Id="ARPPRODUCTICON", Value="ProductIcon")
|
||||||
ET.SubElement(
|
ET.SubElement(
|
||||||
package_el,
|
product_el,
|
||||||
"Property",
|
"Property",
|
||||||
Id="ARPHELPLINK",
|
Id="ARPHELPLINK",
|
||||||
Value="https://dangerzone.rocks",
|
Value="https://dangerzone.rocks",
|
||||||
)
|
)
|
||||||
ET.SubElement(
|
ET.SubElement(
|
||||||
package_el,
|
product_el,
|
||||||
"Property",
|
"Property",
|
||||||
Id="ARPURLINFOABOUT",
|
Id="ARPURLINFOABOUT",
|
||||||
Value="https://freedom.press",
|
Value="https://freedom.press",
|
||||||
)
|
)
|
||||||
ET.SubElement(
|
ET.SubElement(
|
||||||
package_el, "ui:WixUI", Id="WixUI_InstallDir", InstallDirectory="INSTALLFOLDER"
|
product_el,
|
||||||
|
"Property",
|
||||||
|
Id="WIXUI_INSTALLDIR",
|
||||||
|
Value="INSTALLDIR",
|
||||||
)
|
)
|
||||||
ET.SubElement(package_el, "UIRef", Id="WixUI_ErrorProgressText")
|
ET.SubElement(product_el, "UIRef", Id="WixUI_InstallDir")
|
||||||
|
ET.SubElement(product_el, "UIRef", Id="WixUI_ErrorProgressText")
|
||||||
ET.SubElement(
|
ET.SubElement(
|
||||||
package_el,
|
product_el,
|
||||||
"WixVariable",
|
"WixVariable",
|
||||||
Id="WixUILicenseRtf",
|
Id="WixUILicenseRtf",
|
||||||
Value="..\\install\\windows\\license.rtf",
|
Value="..\\install\\windows\\license.rtf",
|
||||||
)
|
)
|
||||||
ET.SubElement(
|
ET.SubElement(
|
||||||
package_el,
|
product_el,
|
||||||
"WixVariable",
|
"WixVariable",
|
||||||
Id="WixUIDialogBmp",
|
Id="WixUIDialogBmp",
|
||||||
Value="..\\install\\windows\\dialog.bmp",
|
Value="..\\install\\windows\\dialog.bmp",
|
||||||
)
|
)
|
||||||
ET.SubElement(
|
ET.SubElement(
|
||||||
package_el,
|
product_el,
|
||||||
"MajorUpgrade",
|
"MajorUpgrade",
|
||||||
|
AllowSameVersionUpgrades="yes",
|
||||||
DowngradeErrorMessage="A newer version of [ProductName] is already installed. If you are sure you want to downgrade, remove the existing installation via Programs and Features.",
|
DowngradeErrorMessage="A newer version of [ProductName] is already installed. If you are sure you want to downgrade, remove the existing installation via Programs and Features.",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Workaround for an issue after upgrading from WiX Toolset v3 to v5 where the previous
|
build_dir_xml(product_el, data)
|
||||||
# version of Dangerzone is not uninstalled during the upgrade by checking if the older installation
|
component_ids = build_components_xml(product_el, data)
|
||||||
# exists in "C:\Program Files (x86)\Dangerzone".
|
|
||||||
#
|
|
||||||
# Also handle a special case for Dangerzone 0.8.0 which allows choosing the install location
|
|
||||||
# during install by checking if the registry key for it exists.
|
|
||||||
#
|
|
||||||
# Note that this seems to allow installing Dangerzone 0.8.0 after installing Dangerzone from this branch.
|
|
||||||
# In this case the installer errors until Dangerzone 0.8.0 is uninstalled again
|
|
||||||
#
|
|
||||||
# TODO: Revert this once we are reasonably certain there aren't too many affected Dangerzone installations.
|
|
||||||
find_old_el = ET.SubElement(package_el, "Property", Id="OLDDANGERZONEFOUND")
|
|
||||||
directory_search_el = ET.SubElement(
|
|
||||||
find_old_el,
|
|
||||||
"DirectorySearch",
|
|
||||||
Id="dangerzone_install_folder",
|
|
||||||
Path="C:\\Program Files (x86)\\Dangerzone",
|
|
||||||
)
|
|
||||||
ET.SubElement(directory_search_el, "FileSearch", Name="dangerzone.exe")
|
|
||||||
registry_search_el = ET.SubElement(package_el, "Property", Id="DANGERZONE080FOUND")
|
|
||||||
ET.SubElement(
|
|
||||||
registry_search_el,
|
|
||||||
"RegistrySearch",
|
|
||||||
Root="HKLM",
|
|
||||||
Key="SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{03C2D2B2-9955-4AED-831F-DA4E67FC0FDB}",
|
|
||||||
Name="DisplayName",
|
|
||||||
Type="raw",
|
|
||||||
)
|
|
||||||
ET.SubElement(
|
|
||||||
package_el,
|
|
||||||
"Launch",
|
|
||||||
Condition="NOT OLDDANGERZONEFOUND AND NOT DANGERZONE080FOUND",
|
|
||||||
Message="A previous version of [ProductName] is already installed. Please uninstall it from Programs and Features before proceeding with the installation.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add the ProgramMenuFolder StandardDirectory
|
feature_el = ET.SubElement(product_el, "Feature", Id="DefaultFeature", Level="1")
|
||||||
programmenufolder_el = ET.SubElement(
|
for component_id in component_ids:
|
||||||
package_el,
|
ET.SubElement(feature_el, "ComponentRef", Id=component_id)
|
||||||
"StandardDirectory",
|
|
||||||
Id="ProgramMenuFolder",
|
|
||||||
)
|
|
||||||
# Add a shortcut for Dangerzone in the Start menu
|
|
||||||
shortcut_el = ET.SubElement(
|
|
||||||
programmenufolder_el,
|
|
||||||
"Component",
|
|
||||||
Id="ApplicationShortcuts",
|
|
||||||
Guid="539E7DE8-A124-4C09-AA55-0DD516AAD7BC",
|
|
||||||
)
|
|
||||||
ET.SubElement(
|
|
||||||
shortcut_el,
|
|
||||||
"Shortcut",
|
|
||||||
Id="DangerzoneStartMenuShortcut",
|
|
||||||
Name="Dangerzone",
|
|
||||||
Description="Dangerzone",
|
|
||||||
Target="[INSTALLFOLDER]dangerzone.exe",
|
|
||||||
WorkingDirectory="INSTALLFOLDER",
|
|
||||||
)
|
|
||||||
ET.SubElement(
|
|
||||||
shortcut_el,
|
|
||||||
"RegistryValue",
|
|
||||||
Root="HKCU",
|
|
||||||
Key="Software\\Freedom of the Press Foundation\\Dangerzone",
|
|
||||||
Name="installed",
|
|
||||||
Type="integer",
|
|
||||||
Value="1",
|
|
||||||
KeyPath="yes",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add the ProgramFilesFolder StandardDirectory
|
|
||||||
programfilesfolder_el = ET.SubElement(
|
|
||||||
package_el,
|
|
||||||
"StandardDirectory",
|
|
||||||
Id="ProgramFiles64Folder",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create the directory structure for the installed product
|
|
||||||
build_directory_xml(programfilesfolder_el, data)
|
|
||||||
|
|
||||||
# Create a component group for application components
|
|
||||||
applicationcomponents_el = ET.SubElement(
|
|
||||||
package_el, "ComponentGroup", Id="ApplicationComponents"
|
|
||||||
)
|
|
||||||
# Populate the application components group with components for the installed package
|
|
||||||
build_components_xml(applicationcomponents_el, data)
|
|
||||||
|
|
||||||
# Add the Feature element
|
|
||||||
feature_el = ET.SubElement(package_el, "Feature", Id="DefaultFeature", Level="1")
|
|
||||||
ET.SubElement(feature_el, "ComponentGroupRef", Id="ApplicationComponents")
|
|
||||||
ET.SubElement(feature_el, "ComponentRef", Id="ApplicationShortcuts")
|
ET.SubElement(feature_el, "ComponentRef", Id="ApplicationShortcuts")
|
||||||
|
|
||||||
ET.indent(wix_el, space=" ")
|
print('<?xml version="1.0" encoding="windows-1252"?>')
|
||||||
|
print(f'<?define ProductVersion = "{version}"?>')
|
||||||
with open(os.path.join(build_dir, "Dangerzone.wxs"), "w") as wxs_file:
|
print('<?define ProductUpgradeCode = "12b9695c-965b-4be0-bc33-21274e809576"?>')
|
||||||
wxs_file.write(ET.tostring(wix_el).decode())
|
ET.indent(root_el)
|
||||||
|
print(ET.tostring(root_el).decode())
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
@ -71,8 +71,7 @@ follow_links = false
|
||||||
verbosity = 3
|
verbosity = 3
|
||||||
|
|
||||||
[tool.doit.tasks.build_image]
|
[tool.doit.tasks.build_image]
|
||||||
# DO NOT change this to 'true' for release artifacts, else we risk building
|
# DO NOT change this to 'true' for release artifacts.
|
||||||
# images that are a few days behind. See also: docs/developer/doit.md
|
|
||||||
use_cache = false
|
use_cache = false
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|
|
@ -10,7 +10,6 @@ from pytest_mock import MockerFixture
|
||||||
from pytest_subprocess import FakeProcess
|
from pytest_subprocess import FakeProcess
|
||||||
from pytestqt.qtbot import QtBot
|
from pytestqt.qtbot import QtBot
|
||||||
|
|
||||||
from dangerzone import errors
|
|
||||||
from dangerzone.document import Document
|
from dangerzone.document import Document
|
||||||
from dangerzone.gui import MainWindow
|
from dangerzone.gui import MainWindow
|
||||||
from dangerzone.gui import main_window as main_window_module
|
from dangerzone.gui import main_window as main_window_module
|
||||||
|
@ -26,7 +25,11 @@ from dangerzone.gui.main_window import (
|
||||||
WaitingWidgetContainer,
|
WaitingWidgetContainer,
|
||||||
)
|
)
|
||||||
from dangerzone.gui.updater import UpdateReport, UpdaterThread
|
from dangerzone.gui.updater import UpdateReport, UpdaterThread
|
||||||
from dangerzone.isolation_provider.container import Container
|
from dangerzone.isolation_provider.container import (
|
||||||
|
Container,
|
||||||
|
NoContainerTechException,
|
||||||
|
NotAvailableContainerTechException,
|
||||||
|
)
|
||||||
from dangerzone.isolation_provider.dummy import Dummy
|
from dangerzone.isolation_provider.dummy import Dummy
|
||||||
|
|
||||||
from .test_updater import assert_report_equal, default_updater_settings
|
from .test_updater import assert_report_equal, default_updater_settings
|
||||||
|
@ -509,8 +512,8 @@ def test_not_available_container_tech_exception(
|
||||||
# Setup
|
# Setup
|
||||||
mock_app = mocker.MagicMock()
|
mock_app = mocker.MagicMock()
|
||||||
dummy = Dummy()
|
dummy = Dummy()
|
||||||
fn = mocker.patch.object(dummy, "is_available")
|
fn = mocker.patch.object(dummy, "is_runtime_available")
|
||||||
fn.side_effect = errors.NotAvailableContainerTechException(
|
fn.side_effect = NotAvailableContainerTechException(
|
||||||
"podman", "podman image ls logs"
|
"podman", "podman image ls logs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -533,7 +536,7 @@ def test_no_container_tech_exception(qtbot: QtBot, mocker: MockerFixture) -> Non
|
||||||
dummy = mocker.MagicMock()
|
dummy = mocker.MagicMock()
|
||||||
|
|
||||||
# Raise
|
# Raise
|
||||||
dummy.is_available.side_effect = errors.NoContainerTechException("podman")
|
dummy.is_runtime_available.side_effect = NoContainerTechException("podman")
|
||||||
|
|
||||||
dz = DangerzoneGui(mock_app, dummy)
|
dz = DangerzoneGui(mock_app, dummy)
|
||||||
widget = WaitingWidgetContainer(dz)
|
widget = WaitingWidgetContainer(dz)
|
||||||
|
|
|
@ -4,8 +4,12 @@ import pytest
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
from pytest_subprocess import FakeProcess
|
from pytest_subprocess import FakeProcess
|
||||||
|
|
||||||
from dangerzone import container_utils, errors
|
from dangerzone.isolation_provider.container import (
|
||||||
from dangerzone.isolation_provider.container import Container
|
Container,
|
||||||
|
ImageInstallationException,
|
||||||
|
ImageNotPresentException,
|
||||||
|
NotAvailableContainerTechException,
|
||||||
|
)
|
||||||
from dangerzone.isolation_provider.qubes import is_qubes_native_conversion
|
from dangerzone.isolation_provider.qubes import is_qubes_native_conversion
|
||||||
|
|
||||||
from .base import IsolationProviderTermination, IsolationProviderTest
|
from .base import IsolationProviderTermination, IsolationProviderTest
|
||||||
|
@ -23,27 +27,31 @@ def provider() -> Container:
|
||||||
|
|
||||||
|
|
||||||
class TestContainer(IsolationProviderTest):
|
class TestContainer(IsolationProviderTest):
|
||||||
def test_is_available_raises(self, provider: Container, fp: FakeProcess) -> None:
|
def test_is_runtime_available_raises(
|
||||||
|
self, provider: Container, fp: FakeProcess
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
NotAvailableContainerTechException should be raised when
|
NotAvailableContainerTechException should be raised when
|
||||||
the "podman image ls" command fails.
|
the "podman image ls" command fails.
|
||||||
"""
|
"""
|
||||||
fp.register_subprocess(
|
fp.register_subprocess(
|
||||||
[container_utils.get_runtime(), "image", "ls"],
|
[provider.get_runtime(), "image", "ls"],
|
||||||
returncode=-1,
|
returncode=-1,
|
||||||
stderr="podman image ls logs",
|
stderr="podman image ls logs",
|
||||||
)
|
)
|
||||||
with pytest.raises(errors.NotAvailableContainerTechException):
|
with pytest.raises(NotAvailableContainerTechException):
|
||||||
provider.is_available()
|
provider.is_runtime_available()
|
||||||
|
|
||||||
def test_is_available_works(self, provider: Container, fp: FakeProcess) -> None:
|
def test_is_runtime_available_works(
|
||||||
|
self, provider: Container, fp: FakeProcess
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
No exception should be raised when the "podman image ls" can return properly.
|
No exception should be raised when the "podman image ls" can return properly.
|
||||||
"""
|
"""
|
||||||
fp.register_subprocess(
|
fp.register_subprocess(
|
||||||
[container_utils.get_runtime(), "image", "ls"],
|
[provider.get_runtime(), "image", "ls"],
|
||||||
)
|
)
|
||||||
provider.is_available()
|
provider.is_runtime_available()
|
||||||
|
|
||||||
def test_install_raise_if_image_cant_be_installed(
|
def test_install_raise_if_image_cant_be_installed(
|
||||||
self, mocker: MockerFixture, provider: Container, fp: FakeProcess
|
self, mocker: MockerFixture, provider: Container, fp: FakeProcess
|
||||||
|
@ -51,31 +59,32 @@ class TestContainer(IsolationProviderTest):
|
||||||
"""When an image installation fails, an exception should be raised"""
|
"""When an image installation fails, an exception should be raised"""
|
||||||
|
|
||||||
fp.register_subprocess(
|
fp.register_subprocess(
|
||||||
[container_utils.get_runtime(), "image", "ls"],
|
[provider.get_runtime(), "image", "ls"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# First check should return nothing.
|
# First check should return nothing.
|
||||||
fp.register_subprocess(
|
fp.register_subprocess(
|
||||||
[
|
[
|
||||||
container_utils.get_runtime(),
|
provider.get_runtime(),
|
||||||
"image",
|
"image",
|
||||||
"list",
|
"list",
|
||||||
"--format",
|
"--format",
|
||||||
"{{ .Tag }}",
|
"json",
|
||||||
"dangerzone.rocks/dangerzone",
|
"dangerzone.rocks/dangerzone",
|
||||||
],
|
],
|
||||||
occurrences=2,
|
occurrences=2,
|
||||||
|
stdout="{}",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Make podman load fail
|
# Make podman load fail
|
||||||
mocker.patch("gzip.open", mocker.mock_open(read_data=""))
|
mocker.patch("gzip.open", mocker.mock_open(read_data=""))
|
||||||
|
|
||||||
fp.register_subprocess(
|
fp.register_subprocess(
|
||||||
[container_utils.get_runtime(), "load"],
|
[provider.get_runtime(), "load"],
|
||||||
returncode=-1,
|
returncode=-1,
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(errors.ImageInstallationException):
|
with pytest.raises(ImageInstallationException):
|
||||||
provider.install()
|
provider.install()
|
||||||
|
|
||||||
def test_install_raises_if_still_not_installed(
|
def test_install_raises_if_still_not_installed(
|
||||||
|
@ -84,28 +93,29 @@ class TestContainer(IsolationProviderTest):
|
||||||
"""When an image keep being not installed, it should return False"""
|
"""When an image keep being not installed, it should return False"""
|
||||||
|
|
||||||
fp.register_subprocess(
|
fp.register_subprocess(
|
||||||
[container_utils.get_runtime(), "image", "ls"],
|
[provider.get_runtime(), "image", "ls"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# First check should return nothing.
|
# First check should return nothing.
|
||||||
fp.register_subprocess(
|
fp.register_subprocess(
|
||||||
[
|
[
|
||||||
container_utils.get_runtime(),
|
provider.get_runtime(),
|
||||||
"image",
|
"image",
|
||||||
"list",
|
"list",
|
||||||
"--format",
|
"--format",
|
||||||
"{{ .Tag }}",
|
"json",
|
||||||
"dangerzone.rocks/dangerzone",
|
"dangerzone.rocks/dangerzone",
|
||||||
],
|
],
|
||||||
occurrences=2,
|
occurrences=2,
|
||||||
|
stdout="{}",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Patch gzip.open and podman load so that it works
|
# Patch gzip.open and podman load so that it works
|
||||||
mocker.patch("gzip.open", mocker.mock_open(read_data=""))
|
mocker.patch("gzip.open", mocker.mock_open(read_data=""))
|
||||||
fp.register_subprocess(
|
fp.register_subprocess(
|
||||||
[container_utils.get_runtime(), "load"],
|
[provider.get_runtime(), "load"],
|
||||||
)
|
)
|
||||||
with pytest.raises(errors.ImageNotPresentException):
|
with pytest.raises(ImageNotPresentException):
|
||||||
provider.install()
|
provider.install()
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue