mirror of
https://github.com/freedomofpress/dangerzone.git
synced 2025-05-04 04:31:49 +02:00
Compare commits
96 commits
bbac103b64
...
2a56c7f35c
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2a56c7f35c | ||
![]() |
a603700ae6 | ||
![]() |
9599291fa3 | ||
![]() |
e4b7d400c9 | ||
![]() |
5a48de46a2 | ||
![]() |
f668a44cdb | ||
![]() |
6427d7a38b | ||
![]() |
4bcd9ee660 | ||
![]() |
00baf6c87a | ||
![]() |
0ca9e714f4 | ||
![]() |
7e52fe4459 | ||
![]() |
d37630d37b | ||
![]() |
a797a2b6f8 | ||
![]() |
0907ddc561 | ||
![]() |
87a00d0f38 | ||
![]() |
26638a5f2a | ||
![]() |
651d988a37 | ||
![]() |
a19e341378 | ||
![]() |
a6a4aa1a3b | ||
![]() |
e4fccd791f | ||
![]() |
ee3689b32a | ||
![]() |
3a6d73dcb8 | ||
![]() |
ff22c6e160 | ||
![]() |
04096380ff | ||
![]() |
21ca927b8b | ||
![]() |
05040de212 | ||
![]() |
4014c8591b | ||
![]() |
6cd706af10 | ||
![]() |
634b171b97 | ||
![]() |
c99c424f87 | ||
![]() |
19fa11410b | ||
![]() |
10be85b9f2 | ||
![]() |
47d732e603 | ||
![]() |
d6451290db | ||
![]() |
f0bb65cb4e | ||
![]() |
0c741359cc | ||
![]() |
8c61894e25 | ||
![]() |
57667a96be | ||
![]() |
1a644e2506 | ||
![]() |
843e68cdf7 | ||
![]() |
33b2a183ce | ||
![]() |
c7121b69a3 | ||
![]() |
0b3bf89d5b | ||
![]() |
e0b10c5e40 | ||
![]() |
092eec55d1 | ||
![]() |
14a480c3a3 | ||
![]() |
9df825db5c | ||
![]() |
2ee22a497a | ||
![]() |
b5c09e51d8 | ||
![]() |
37c7608c0f | ||
![]() |
972b264236 | ||
![]() |
e38d8e5db0 | ||
![]() |
f92833cdff | ||
![]() |
07aad5edba | ||
![]() |
e8ca12eb11 | ||
![]() |
491cca6341 | ||
![]() |
0a7b79f61a | ||
![]() |
86eab5d222 | ||
![]() |
ed39c056bb | ||
![]() |
983622fe59 | ||
![]() |
8e99764952 | ||
![]() |
20cd9cfc5c | ||
![]() |
f082641b71 | ||
![]() |
c0215062bc | ||
![]() |
b551a4dec4 | ||
![]() |
5a56a7f055 | ||
![]() |
ab6dd9c01d | ||
![]() |
dfcb74b427 | ||
![]() |
a910ccc273 | ||
![]() |
d868699bab | ||
![]() |
d6adfbc6c1 | ||
![]() |
687bd8585f | ||
![]() |
b212bfc47e | ||
![]() |
bbc90be217 | ||
![]() |
2d321bf257 | ||
![]() |
8bfeae4eed | ||
![]() |
3ed71e8ee0 | ||
![]() |
fa8e8c6dbb | ||
![]() |
8d05b5779d | ||
![]() |
e1dbdff1da | ||
![]() |
a1402d5b6b | ||
![]() |
51f432be6b | ||
![]() |
69234507c4 | ||
![]() |
94fad78f94 | ||
![]() |
66600f32dc | ||
![]() |
d41f604969 | ||
![]() |
6d269572ae | ||
![]() |
c7ba9ee75c | ||
![]() |
418b68d4ca | ||
![]() |
9ba95b5c20 | ||
![]() |
b043c97c41 | ||
![]() |
4a48a2551b | ||
![]() |
56663023f5 | ||
![]() |
53a952235c | ||
![]() |
d2652ef6cd | ||
![]() |
a6aa66f925 |
88 changed files with 2621 additions and 1515 deletions
4
.github/ISSUE_TEMPLATE/bug_report_linux.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report_linux.yml
vendored
|
@ -6,7 +6,7 @@ body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Hi, and thanks for taking the time to open this bug report.
|
Hi, and thanks for taking the time to open this bug report.
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: what-happened
|
id: what-happened
|
||||||
attributes:
|
attributes:
|
||||||
|
@ -21,7 +21,7 @@ body:
|
||||||
label: Linux distribution
|
label: Linux distribution
|
||||||
description: |
|
description: |
|
||||||
What is the name and version of your Linux distribution? You can find it out with `cat /etc/os-release`
|
What is the name and version of your Linux distribution? You can find it out with `cat /etc/os-release`
|
||||||
placeholder: Ubuntu 20.04.6 LTS
|
placeholder: Ubuntu 22.04.5 LTS
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|
248
.github/workflows/build-push-image.yml
vendored
Normal file
248
.github/workflows/build-push-image.yml
vendored
Normal file
|
@ -0,0 +1,248 @@
|
||||||
|
name: Build and push multi-arch container image
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
registry:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
registry_user:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
image_name:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
reproduce:
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
secrets:
|
||||||
|
registry_token:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install dev. dependencies
|
||||||
|
run: |-
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y git python3-poetry --no-install-recommends
|
||||||
|
poetry install --only package
|
||||||
|
|
||||||
|
- name: Verify that the Dockerfile matches the commited template and params
|
||||||
|
run: |-
|
||||||
|
cp Dockerfile Dockerfile.orig
|
||||||
|
make Dockerfile
|
||||||
|
diff Dockerfile.orig Dockerfile
|
||||||
|
|
||||||
|
prepare:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
debian_archive_date: ${{ steps.params.outputs.debian_archive_date }}
|
||||||
|
source_date_epoch: ${{ steps.params.outputs.source_date_epoch }}
|
||||||
|
image: ${{ steps.params.outputs.full_image_name }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Compute image parameters
|
||||||
|
id: params
|
||||||
|
run: |
|
||||||
|
source Dockerfile.env
|
||||||
|
DEBIAN_ARCHIVE_DATE=$(date -u +'%Y%m%d')
|
||||||
|
SOURCE_DATE_EPOCH=$(date -u -d ${DEBIAN_ARCHIVE_DATE} +"%s")
|
||||||
|
TAG=${DEBIAN_ARCHIVE_DATE}-$(git describe --long --first-parent | tail -c +2)
|
||||||
|
FULL_IMAGE_NAME=${{ inputs.registry }}/${{ inputs.image_name }}:${TAG}
|
||||||
|
|
||||||
|
echo "debian_archive_date=${DEBIAN_ARCHIVE_DATE}" >> $GITHUB_OUTPUT
|
||||||
|
echo "source_date_epoch=${SOURCE_DATE_EPOCH}" >> $GITHUB_OUTPUT
|
||||||
|
echo "tag=${DEBIAN_ARCHIVE_DATE}-${TAG}" >> $GITHUB_OUTPUT
|
||||||
|
echo "full_image_name=${FULL_IMAGE_NAME}" >> $GITHUB_OUTPUT
|
||||||
|
echo "buildkit_image=${BUILDKIT_IMAGE}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build ${{ matrix.platform.name }} image
|
||||||
|
runs-on: ${{ matrix.platform.runs-on }}
|
||||||
|
needs:
|
||||||
|
- prepare
|
||||||
|
outputs:
|
||||||
|
debian_archive_date: ${{ needs.prepare.outputs.debian_archive_date }}
|
||||||
|
source_date_epoch: ${{ needs.prepare.outputs.source_date_epoch }}
|
||||||
|
image: ${{ needs.prepare.outputs.image }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
platform:
|
||||||
|
- runs-on: "ubuntu-24.04"
|
||||||
|
name: "linux/amd64"
|
||||||
|
- runs-on: "ubuntu-24.04-arm"
|
||||||
|
name: "linux/arm64"
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Prepare
|
||||||
|
run: |
|
||||||
|
platform=${{ matrix.platform.name }}
|
||||||
|
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Login to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ inputs.registry_user }}
|
||||||
|
password: ${{ secrets.registry_token }}
|
||||||
|
|
||||||
|
# Instructions for reproducibly building a container image are taken from:
|
||||||
|
# https://github.com/freedomofpress/repro-build?tab=readme-ov-file#build-and-push-a-container-image-on-github-actions
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
with:
|
||||||
|
driver-opts: image=${{ needs.prepare.outputs.buildkit_image }}
|
||||||
|
|
||||||
|
- name: Build and push by digest
|
||||||
|
id: build
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: ./dangerzone/
|
||||||
|
file: Dockerfile
|
||||||
|
build-args: |
|
||||||
|
DEBIAN_ARCHIVE_DATE=${{ needs.prepare.outputs.debian_archive_date }}
|
||||||
|
SOURCE_DATE_EPOCH=${{ needs.prepare.outputs.source_date_epoch }}
|
||||||
|
provenance: false
|
||||||
|
outputs: type=image,"name=${{ inputs.registry }}/${{ inputs.image_name }}",push-by-digest=true,push=true,rewrite-timestamp=true,name-canonical=true
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
- name: Export digest
|
||||||
|
run: |
|
||||||
|
mkdir -p ${{ runner.temp }}/digests
|
||||||
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
|
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||||
|
echo "Image digest is: ${digest}"
|
||||||
|
|
||||||
|
- name: Upload digest
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: digests-${{ env.PLATFORM_PAIR }}
|
||||||
|
path: ${{ runner.temp }}/digests/*
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
merge:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- build
|
||||||
|
outputs:
|
||||||
|
debian_archive_date: ${{ needs.build.outputs.debian_archive_date }}
|
||||||
|
source_date_epoch: ${{ needs.build.outputs.source_date_epoch }}
|
||||||
|
image: ${{ needs.build.outputs.image }}
|
||||||
|
digest_root: ${{ steps.image.outputs.digest_root }}
|
||||||
|
digest_amd64: ${{ steps.image.outputs.digest_amd64 }}
|
||||||
|
digest_arm64: ${{ steps.image.outputs.digest_arm64 }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download digests
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: ${{ runner.temp }}/digests
|
||||||
|
pattern: digests-*
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Login to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ inputs.registry_user }}
|
||||||
|
password: ${{ secrets.registry_token }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
with:
|
||||||
|
driver-opts: image=${{ env.BUILDKIT_IMAGE }}
|
||||||
|
|
||||||
|
- name: Create manifest list and push
|
||||||
|
working-directory: ${{ runner.temp }}/digests
|
||||||
|
run: |
|
||||||
|
DIGESTS=$(printf '${{ needs.build.outputs.image }}@sha256:%s ' *)
|
||||||
|
docker buildx imagetools create -t ${{ needs.build.outputs.image }} ${DIGESTS}
|
||||||
|
|
||||||
|
- name: Inspect image
|
||||||
|
id: image
|
||||||
|
run: |
|
||||||
|
# Inspect the image
|
||||||
|
docker buildx imagetools inspect ${{ needs.build.outputs.image }}
|
||||||
|
docker buildx imagetools inspect ${{ needs.build.outputs.image }} --format "{{json .Manifest}}" > manifest
|
||||||
|
|
||||||
|
# Calculate and print the digests
|
||||||
|
digest_root=$(jq -r .digest manifest)
|
||||||
|
digest_amd64=$(jq -r '.manifests[] | select(.platform.architecture=="amd64") | .digest' manifest)
|
||||||
|
digest_arm64=$(jq -r '.manifests[] | select(.platform.architecture=="arm64") | .digest' manifest)
|
||||||
|
|
||||||
|
echo "The image digests are:"
|
||||||
|
echo " Root: $digest_root"
|
||||||
|
echo " linux/amd64: $digest_amd64"
|
||||||
|
echo " linux/arm64: $digest_arm64"
|
||||||
|
|
||||||
|
# NOTE: Set the digests as an output because the `env` context is not
|
||||||
|
# available to the inputs of a reusable workflow call.
|
||||||
|
echo "digest_root=$digest_root" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "digest_amd64=$digest_amd64" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "digest_arm64=$digest_arm64" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# This step calls the container workflow to generate provenance and push it to
|
||||||
|
# the container registry.
|
||||||
|
provenance:
|
||||||
|
needs:
|
||||||
|
- merge
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
manifest_type:
|
||||||
|
- root
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
permissions:
|
||||||
|
actions: read # for detecting the Github Actions environment.
|
||||||
|
id-token: write # for creating OIDC tokens for signing.
|
||||||
|
packages: write # for uploading attestations.
|
||||||
|
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
|
||||||
|
with:
|
||||||
|
digest: ${{ needs.merge.outputs[format('digest_{0}', matrix.manifest_type)] }}
|
||||||
|
image: ${{ needs.merge.outputs.image }}
|
||||||
|
registry-username: ${{ inputs.registry_user }}
|
||||||
|
secrets:
|
||||||
|
registry-password: ${{ secrets.registry_token }}
|
||||||
|
|
||||||
|
# This step ensures that the image is reproducible
|
||||||
|
check-reproducibility:
|
||||||
|
if: ${{ inputs.reproduce }}
|
||||||
|
needs:
|
||||||
|
- merge
|
||||||
|
runs-on: ${{ matrix.platform.runs-on }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
platform:
|
||||||
|
- runs-on: "ubuntu-24.04"
|
||||||
|
name: "amd64"
|
||||||
|
- runs-on: "ubuntu-24.04-arm"
|
||||||
|
name: "arm64"
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Reproduce the same container image
|
||||||
|
run: |
|
||||||
|
./dev_scripts/reproduce-image.py \
|
||||||
|
--runtime \
|
||||||
|
docker \
|
||||||
|
--debian-archive-date \
|
||||||
|
${{ needs.merge.outputs.debian_archive_date }} \
|
||||||
|
--platform \
|
||||||
|
linux/${{ matrix.platform.name }} \
|
||||||
|
${{ needs.merge.outputs[format('digest_{0}', matrix.platform.name)] }}
|
19
.github/workflows/build.yml
vendored
19
.github/workflows/build.yml
vendored
|
@ -33,14 +33,14 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- distro: ubuntu
|
|
||||||
version: "20.04"
|
|
||||||
- distro: ubuntu
|
- distro: ubuntu
|
||||||
version: "22.04"
|
version: "22.04"
|
||||||
- distro: ubuntu
|
- distro: ubuntu
|
||||||
version: "24.04"
|
version: "24.04"
|
||||||
- distro: ubuntu
|
- distro: ubuntu
|
||||||
version: "24.10"
|
version: "24.10"
|
||||||
|
- distro: ubuntu
|
||||||
|
version: "25.04"
|
||||||
- distro: debian
|
- distro: debian
|
||||||
version: bullseye
|
version: bullseye
|
||||||
- distro: debian
|
- distro: debian
|
||||||
|
@ -51,6 +51,8 @@ jobs:
|
||||||
version: "40"
|
version: "40"
|
||||||
- distro: fedora
|
- distro: fedora
|
||||||
version: "41"
|
version: "41"
|
||||||
|
- distro: fedora
|
||||||
|
version: "42"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
@ -85,19 +87,12 @@ jobs:
|
||||||
id: cache-container-image
|
id: cache-container-image
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
key: v4-${{ steps.date.outputs.date }}-${{ hashFiles('Dockerfile', 'dangerzone/conversion/*.py', 'dangerzone/container_helpers/*', 'install/common/build-image.py') }}
|
key: v5-${{ steps.date.outputs.date }}-${{ hashFiles('Dockerfile', 'dangerzone/conversion/*.py', 'dangerzone/container_helpers/*', 'install/common/build-image.py') }}
|
||||||
path: |
|
path: |
|
||||||
share/container.tar.gz
|
share/container.tar
|
||||||
share/image-id.txt
|
share/image-id.txt
|
||||||
|
|
||||||
- name: Build and push Dangerzone image
|
- name: Build Dangerzone image
|
||||||
if: ${{ steps.cache-container-image.outputs.cache-hit != 'true' }}
|
if: ${{ steps.cache-container-image.outputs.cache-hit != 'true' }}
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get install -y python3-poetry
|
|
||||||
python3 ./install/common/build-image.py
|
python3 ./install/common/build-image.py
|
||||||
echo ${{ github.token }} | podman login ghcr.io -u USERNAME --password-stdin
|
|
||||||
gunzip -c share/container.tar.gz | podman load
|
|
||||||
tag=$(cat share/image-id.txt)
|
|
||||||
podman push \
|
|
||||||
dangerzone.rocks/dangerzone:$tag \
|
|
||||||
${{ env.IMAGE_REGISTRY }}/dangerzone/dangerzone:tag
|
|
||||||
|
|
17
.github/workflows/check_pr.yml
vendored
17
.github/workflows/check_pr.yml
vendored
|
@ -1,6 +1,7 @@
|
||||||
name: Check branch conformity
|
name: Check branch conformity
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
types: ["opened", "labeled", "unlabeled", "reopened", "synchronize"]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
prevent-fixup-commits:
|
prevent-fixup-commits:
|
||||||
|
@ -20,16 +21,10 @@ jobs:
|
||||||
|
|
||||||
check-changelog:
|
check-changelog:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
name: Ensure CHANGELOG.md is populated for user-visible changes
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
# Pin the GitHub action to a specific commit that we have audited and know
|
||||||
uses: actions/checkout@v4
|
# how it works.
|
||||||
|
- uses: tarides/changelog-check-action@509965da3b8ac786a5e2da30c2ccf9661189121f
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
changelog: CHANGELOG.md
|
||||||
- name: ensure CHANGELOG.md is populated
|
|
||||||
env:
|
|
||||||
BASE_REF: ${{ github.event.pull_request.base.ref }}
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
if git diff --exit-code "origin/${BASE_REF}" -- CHANGELOG.md; then
|
|
||||||
echo "::warning::No CHANGELOG.md modifications were found in this pull request."
|
|
||||||
fi
|
|
||||||
|
|
18
.github/workflows/check_repos.yml
vendored
18
.github/workflows/check_repos.yml
vendored
|
@ -19,14 +19,14 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
|
- distro: ubuntu
|
||||||
|
version: "25.04" # plucky
|
||||||
- distro: ubuntu
|
- distro: ubuntu
|
||||||
version: "24.10" # oracular
|
version: "24.10" # oracular
|
||||||
- distro: ubuntu
|
- distro: ubuntu
|
||||||
version: "24.04" # noble
|
version: "24.04" # noble
|
||||||
- distro: ubuntu
|
- distro: ubuntu
|
||||||
version: "22.04" # jammy
|
version: "22.04" # jammy
|
||||||
- distro: ubuntu
|
|
||||||
version: "20.04" # focal
|
|
||||||
- distro: debian
|
- distro: debian
|
||||||
version: "trixie" # 13
|
version: "trixie" # 13
|
||||||
- distro: debian
|
- distro: debian
|
||||||
|
@ -34,18 +34,6 @@ jobs:
|
||||||
- distro: debian
|
- distro: debian
|
||||||
version: "11" # bullseye
|
version: "11" # bullseye
|
||||||
steps:
|
steps:
|
||||||
- name: Add Podman repo for Ubuntu Focal
|
|
||||||
if: matrix.distro == 'ubuntu' && matrix.version == 20.04
|
|
||||||
run: |
|
|
||||||
apt-get update && apt-get -y install curl wget gnupg2
|
|
||||||
. /etc/os-release
|
|
||||||
sh -c "echo 'deb http://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_${VERSION_ID}/ /' \
|
|
||||||
> /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list"
|
|
||||||
wget -nv https://download.opensuse.org/repositories/devel:kubic:libcontainers:stable/xUbuntu_${VERSION_ID}/Release.key -O- \
|
|
||||||
| apt-key add -
|
|
||||||
apt update
|
|
||||||
apt-get install python-all -y
|
|
||||||
|
|
||||||
- name: Add packages.freedom.press PGP key (gpg)
|
- name: Add packages.freedom.press PGP key (gpg)
|
||||||
if: matrix.version != 'trixie'
|
if: matrix.version != 'trixie'
|
||||||
run: |
|
run: |
|
||||||
|
@ -93,6 +81,8 @@ jobs:
|
||||||
version: 40
|
version: 40
|
||||||
- distro: fedora
|
- distro: fedora
|
||||||
version: 41
|
version: 41
|
||||||
|
- distro: fedora
|
||||||
|
version: 42
|
||||||
steps:
|
steps:
|
||||||
- name: Add packages.freedom.press to our YUM sources
|
- name: Add packages.freedom.press to our YUM sources
|
||||||
run: |
|
run: |
|
||||||
|
|
73
.github/workflows/ci.yml
vendored
73
.github/workflows/ci.yml
vendored
|
@ -59,9 +59,9 @@ jobs:
|
||||||
id: cache-container-image
|
id: cache-container-image
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
key: v4-${{ steps.date.outputs.date }}-${{ hashFiles('Dockerfile', 'dangerzone/conversion/*.py', 'dangerzone/container_helpers/*', 'install/common/build-image.py') }}
|
key: v5-${{ steps.date.outputs.date }}-${{ hashFiles('Dockerfile', 'dangerzone/conversion/*.py', 'dangerzone/container_helpers/*', 'install/common/build-image.py') }}
|
||||||
path: |-
|
path: |-
|
||||||
share/container.tar.gz
|
share/container.tar
|
||||||
share/image-id.txt
|
share/image-id.txt
|
||||||
|
|
||||||
- name: Build Dangerzone container image
|
- name: Build Dangerzone container image
|
||||||
|
@ -72,8 +72,8 @@ jobs:
|
||||||
- name: Upload container image
|
- name: Upload container image
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: container.tar.gz
|
name: container.tar
|
||||||
path: share/container.tar.gz
|
path: share/container.tar
|
||||||
|
|
||||||
download-tessdata:
|
download-tessdata:
|
||||||
name: Download and cache Tesseract data
|
name: Download and cache Tesseract data
|
||||||
|
@ -125,9 +125,9 @@ jobs:
|
||||||
with:
|
with:
|
||||||
dotnet-version: "8.x"
|
dotnet-version: "8.x"
|
||||||
- name: Install WiX Toolset
|
- name: Install WiX Toolset
|
||||||
run: dotnet tool install --global wix
|
run: dotnet tool install --global wix --version 5.0.2
|
||||||
- name: Add WiX UI extension
|
- name: Add WiX UI extension
|
||||||
run: wix extension add --global WixToolset.UI.wixext
|
run: wix extension add --global WixToolset.UI.wixext/5.0.2
|
||||||
- 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
|
||||||
|
@ -186,14 +186,14 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- distro: ubuntu
|
|
||||||
version: "20.04"
|
|
||||||
- distro: ubuntu
|
- distro: ubuntu
|
||||||
version: "22.04"
|
version: "22.04"
|
||||||
- distro: ubuntu
|
- distro: ubuntu
|
||||||
version: "24.04"
|
version: "24.04"
|
||||||
- distro: ubuntu
|
- distro: ubuntu
|
||||||
version: "24.10"
|
version: "24.10"
|
||||||
|
- distro: ubuntu
|
||||||
|
version: "25.04"
|
||||||
- distro: debian
|
- distro: debian
|
||||||
version: bullseye
|
version: bullseye
|
||||||
- distro: debian
|
- distro: debian
|
||||||
|
@ -226,9 +226,9 @@ jobs:
|
||||||
- name: Restore container cache
|
- name: Restore container cache
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
key: v4-${{ steps.date.outputs.date }}-${{ hashFiles('Dockerfile', 'dangerzone/conversion/*.py', 'dangerzone/container_helpers/*', 'install/common/build-image.py') }}
|
key: v5-${{ steps.date.outputs.date }}-${{ hashFiles('Dockerfile', 'dangerzone/conversion/*.py', 'dangerzone/container_helpers/*', 'install/common/build-image.py') }}
|
||||||
path: |-
|
path: |-
|
||||||
share/container.tar.gz
|
share/container.tar
|
||||||
share/image-id.txt
|
share/image-id.txt
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
|
|
||||||
|
@ -255,14 +255,14 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- distro: ubuntu
|
|
||||||
version: "20.04"
|
|
||||||
- distro: ubuntu
|
- distro: ubuntu
|
||||||
version: "22.04"
|
version: "22.04"
|
||||||
- distro: ubuntu
|
- distro: ubuntu
|
||||||
version: "24.04"
|
version: "24.04"
|
||||||
- distro: ubuntu
|
- distro: ubuntu
|
||||||
version: "24.10"
|
version: "24.10"
|
||||||
|
- distro: ubuntu
|
||||||
|
version: "25.04"
|
||||||
- distro: debian
|
- distro: debian
|
||||||
version: bullseye
|
version: bullseye
|
||||||
- distro: debian
|
- distro: debian
|
||||||
|
@ -310,7 +310,7 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
distro: ["fedora"]
|
distro: ["fedora"]
|
||||||
version: ["40", "41"]
|
version: ["40", "41", "42"]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
@ -333,9 +333,9 @@ jobs:
|
||||||
- name: Restore container image
|
- name: Restore container image
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
key: v4-${{ steps.date.outputs.date }}-${{ hashFiles('Dockerfile', 'dangerzone/conversion/*.py', 'dangerzone/container_helpers/*', 'install/common/build-image.py') }}
|
key: v5-${{ steps.date.outputs.date }}-${{ hashFiles('Dockerfile', 'dangerzone/conversion/*.py', 'dangerzone/container_helpers/*', 'install/common/build-image.py') }}
|
||||||
path: |-
|
path: |-
|
||||||
share/container.tar.gz
|
share/container.tar
|
||||||
share/image-id.txt
|
share/image-id.txt
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
|
|
||||||
|
@ -383,14 +383,14 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- distro: ubuntu
|
|
||||||
version: "20.04"
|
|
||||||
- distro: ubuntu
|
- distro: ubuntu
|
||||||
version: "22.04"
|
version: "22.04"
|
||||||
- distro: ubuntu
|
- distro: ubuntu
|
||||||
version: "24.04"
|
version: "24.04"
|
||||||
- distro: ubuntu
|
- distro: ubuntu
|
||||||
version: "24.10"
|
version: "24.10"
|
||||||
|
- distro: ubuntu
|
||||||
|
version: "25.04"
|
||||||
- distro: debian
|
- distro: debian
|
||||||
version: bullseye
|
version: bullseye
|
||||||
- distro: debian
|
- distro: debian
|
||||||
|
@ -401,6 +401,8 @@ jobs:
|
||||||
version: "40"
|
version: "40"
|
||||||
- distro: fedora
|
- distro: fedora
|
||||||
version: "41"
|
version: "41"
|
||||||
|
- distro: fedora
|
||||||
|
version: "42"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
@ -428,9 +430,9 @@ jobs:
|
||||||
- name: Restore container image
|
- name: Restore container image
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
key: v4-${{ steps.date.outputs.date }}-${{ hashFiles('Dockerfile', 'dangerzone/conversion/*.py', 'dangerzone/container_helpers/*', 'install/common/build-image.py') }}
|
key: v5-${{ steps.date.outputs.date }}-${{ hashFiles('Dockerfile', 'dangerzone/conversion/*.py', 'dangerzone/container_helpers/*', 'install/common/build-image.py') }}
|
||||||
path: |-
|
path: |-
|
||||||
share/container.tar.gz
|
share/container.tar
|
||||||
share/image-id.txt
|
share/image-id.txt
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
|
|
||||||
|
@ -471,30 +473,11 @@ jobs:
|
||||||
# file successfully.
|
# file successfully.
|
||||||
xvfb-run -s '-ac' ./dev_scripts/env.py --distro ${{ matrix.distro }} --version ${{ matrix.version }} run --dev \
|
xvfb-run -s '-ac' ./dev_scripts/env.py --distro ${{ matrix.distro }} --version ${{ matrix.version }} run --dev \
|
||||||
bash -c 'cd dangerzone; poetry run make test'
|
bash -c 'cd dangerzone; poetry run make test'
|
||||||
|
|
||||||
check-reproducibility:
|
- name: Upload PDF diffs
|
||||||
runs-on: ubuntu-latest
|
uses: actions/upload-artifact@v4
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
name: pdf-diffs-${{ matrix.distro }}-${{ matrix.version }}
|
||||||
|
path: tests/test_docs/diffs/*.jpeg
|
||||||
- name: Install dev. dependencies
|
# Always run this step to publish test results, even on failures
|
||||||
run: |-
|
if: ${{ always() }}
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y git python3-poetry --no-install-recommends
|
|
||||||
poetry install --only package
|
|
||||||
|
|
||||||
- name: Verify that the Dockerfile matches the commited template and params
|
|
||||||
run: |-
|
|
||||||
cp Dockerfile Dockerfile.orig
|
|
||||||
make Dockerfile
|
|
||||||
diff Dockerfile.orig Dockerfile
|
|
||||||
|
|
||||||
- name: Build Dangerzone container image
|
|
||||||
run: |
|
|
||||||
python3 ./install/common/build-image.py --no-save
|
|
||||||
|
|
||||||
- name: Reproduce the same container image
|
|
||||||
run: |
|
|
||||||
./dev_scripts/reproduce-image.py
|
|
||||||
|
|
160
.github/workflows/release-container-image.yml
vendored
160
.github/workflows/release-container-image.yml
vendored
|
@ -9,160 +9,14 @@ on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 0 * * *" # Run every day at 00:00 UTC.
|
- cron: "0 0 * * *" # Run every day at 00:00 UTC.
|
||||||
|
|
||||||
env:
|
|
||||||
REGISTRY: ghcr.io/${{ github.repository_owner }}
|
|
||||||
REGISTRY_USER: ${{ github.actor }}
|
|
||||||
REGISTRY_PASSWORD: ${{ github.token }}
|
|
||||||
IMAGE_NAME: dangerzone/dangerzone
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build-push-image:
|
||||||
runs-on: ubuntu-latest
|
uses: ./.github/workflows/build-push-image.yml
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
platform:
|
|
||||||
- linux/amd64
|
|
||||||
- linux/arm64
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Get current date
|
|
||||||
id: date
|
|
||||||
run: echo "date=$(date +'%Y%m%d')" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Prepare
|
|
||||||
run: |
|
|
||||||
platform=${{ matrix.platform }}
|
|
||||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Docker meta
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
images: |
|
|
||||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
||||||
|
|
||||||
- name: Login to GHCR
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.repository_owner }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Build and push by digest
|
|
||||||
id: build
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: ./dangerzone/
|
|
||||||
file: Dockerfile
|
|
||||||
build-args: |
|
|
||||||
DEBIAN_ARCHIVE_DATE=${{ steps.date.outputs.date }}
|
|
||||||
## Remove potentially incorrect Docker provenance.
|
|
||||||
#provenance: false
|
|
||||||
platforms: ${{ matrix.platform }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
outputs: type=image,"name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}",push-by-digest=true,name-canonical=true,push=true
|
|
||||||
|
|
||||||
- name: Export digest
|
|
||||||
run: |
|
|
||||||
mkdir -p ${{ runner.temp }}/digests
|
|
||||||
digest="${{ steps.build.outputs.digest }}"
|
|
||||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
|
||||||
|
|
||||||
- name: Upload digest
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: digests-${{ env.PLATFORM_PAIR }}
|
|
||||||
path: ${{ runner.temp }}/digests/*
|
|
||||||
if-no-files-found: error
|
|
||||||
retention-days: 1
|
|
||||||
|
|
||||||
merge:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs:
|
|
||||||
- build
|
|
||||||
outputs:
|
|
||||||
digest: ${{ steps.image.outputs.digest }}
|
|
||||||
image: ${{ steps.image.outputs.image }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Compute image tag
|
|
||||||
id: tag
|
|
||||||
run: |
|
|
||||||
DATE=$(date +'%Y%m%d')
|
|
||||||
TAG=$(git describe --long --first-parent | tail -c +2)
|
|
||||||
echo "tag=${DATE}-${TAG}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Download digests
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
path: ${{ runner.temp }}/digests
|
|
||||||
pattern: digests-*
|
|
||||||
merge-multiple: true
|
|
||||||
|
|
||||||
- name: Login to GHCR
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.repository_owner }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
#- name: Docker meta
|
|
||||||
# id: meta
|
|
||||||
# uses: docker/metadata-action@v5
|
|
||||||
# with:
|
|
||||||
# images: |
|
|
||||||
# ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
||||||
# tags: |
|
|
||||||
# type=ref,event=branch
|
|
||||||
# type=ref,event=pr
|
|
||||||
# type=semver,pattern={{version}}
|
|
||||||
# type=semver,pattern={{major}}.{{minor}}
|
|
||||||
|
|
||||||
- name: Create manifest list and push
|
|
||||||
working-directory: ${{ runner.temp }}/digests
|
|
||||||
run: |
|
|
||||||
IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}
|
|
||||||
DIGESTS=$(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
|
|
||||||
docker buildx imagetools create -t ${IMAGE} ${DIGESTS}
|
|
||||||
|
|
||||||
- name: Inspect image
|
|
||||||
id: image
|
|
||||||
run: |
|
|
||||||
# NOTE: Set the image as an output because the `env` context is not
|
|
||||||
# available to the inputs of a reusable workflow call.
|
|
||||||
image_name="${REGISTRY}/${IMAGE_NAME}"
|
|
||||||
echo "image=$image_name" >> "$GITHUB_OUTPUT"
|
|
||||||
docker buildx imagetools inspect ${image_name}:${{ steps.tag.outputs.tag }}
|
|
||||||
digest=$(docker buildx imagetools inspect ${image_name}:${{ steps.tag.outputs.tag }} --format "{{json .Manifest}}" | jq -r '.digest')
|
|
||||||
echo "digest=$digest" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
# This step calls the container workflow to generate provenance and push it to
|
|
||||||
# the container registry.
|
|
||||||
provenance:
|
|
||||||
needs:
|
|
||||||
- merge
|
|
||||||
permissions:
|
|
||||||
actions: read # for detecting the Github Actions environment.
|
|
||||||
id-token: write # for creating OIDC tokens for signing.
|
|
||||||
packages: write # for uploading attestations.
|
|
||||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0
|
|
||||||
with:
|
with:
|
||||||
digest: ${{ needs.merge.outputs.digest }}
|
registry: ghcr.io/${{ github.repository_owner }}
|
||||||
image: ${{ needs.merge.outputs.image }}
|
registry_user: ${{ github.actor }}
|
||||||
registry-username: ${{ github.actor }}
|
image_name: dangerzone/dangerzone
|
||||||
|
reproduce: true
|
||||||
secrets:
|
secrets:
|
||||||
registry-password: ${{ secrets.GITHUB_TOKEN }}
|
registry_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
31
.github/workflows/scan.yml
vendored
31
.github/workflows/scan.yml
vendored
|
@ -10,25 +10,23 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
security-scan-container:
|
security-scan-container:
|
||||||
runs-on: ubuntu-latest
|
strategy:
|
||||||
|
matrix:
|
||||||
|
runs-on:
|
||||||
|
- ubuntu-24.04
|
||||||
|
- ubuntu-24.04-arm
|
||||||
|
runs-on: ${{ matrix.runs-on }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Install container build dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt install pipx
|
|
||||||
pipx install poetry
|
|
||||||
pipx inject poetry poetry-plugin-export
|
|
||||||
poetry install --only package
|
|
||||||
- name: Bump date of Debian snapshot archive
|
|
||||||
run: |
|
|
||||||
date=$(date "+%Y%m%d")
|
|
||||||
sed -i "s/DEBIAN_ARCHIVE_DATE=[0-9]\+/DEBIAN_ARCHIVE_DATE=${date}/" Dockerfile.env
|
|
||||||
make Dockerfile
|
|
||||||
- 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 \
|
||||||
|
--debian-archive-date $(date "+%Y%m%d") \
|
||||||
|
--runtime docker
|
||||||
|
docker load -i share/container.tar
|
||||||
- name: Get image tag
|
- name: Get image tag
|
||||||
id: tag
|
id: tag
|
||||||
run: echo "tag=$(cat share/image-id.txt)" >> $GITHUB_OUTPUT
|
run: echo "tag=$(cat share/image-id.txt)" >> $GITHUB_OUTPUT
|
||||||
|
@ -58,7 +56,12 @@ jobs:
|
||||||
severity-cutoff: critical
|
severity-cutoff: critical
|
||||||
|
|
||||||
security-scan-app:
|
security-scan-app:
|
||||||
runs-on: ubuntu-latest
|
strategy:
|
||||||
|
matrix:
|
||||||
|
runs-on:
|
||||||
|
- ubuntu-24.04
|
||||||
|
- ubuntu-24.04-arm
|
||||||
|
runs-on: ${{ matrix.runs-on }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
14
.github/workflows/scan_released.yml
vendored
14
.github/workflows/scan_released.yml
vendored
|
@ -9,11 +9,10 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- runs-on: ubuntu-latest
|
- runs-on: ubuntu-24.04
|
||||||
arch: i686
|
arch: i686
|
||||||
# Do not scan Silicon mac for now to avoid masking release scan results for other plaforms.
|
- runs-on: ubuntu-24.04-arm
|
||||||
# - runs-on: macos-latest
|
arch: arm64
|
||||||
# arch: arm64
|
|
||||||
runs-on: ${{ matrix.runs-on }}
|
runs-on: ${{ matrix.runs-on }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
@ -55,7 +54,12 @@ jobs:
|
||||||
severity-cutoff: critical
|
severity-cutoff: critical
|
||||||
|
|
||||||
security-scan-app:
|
security-scan-app:
|
||||||
runs-on: ubuntu-latest
|
strategy:
|
||||||
|
matrix:
|
||||||
|
runs-on:
|
||||||
|
- ubuntu-24.04
|
||||||
|
- ubuntu-24.04-arm
|
||||||
|
runs-on: ${{ matrix.runs-on }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
1
.well-known/funding-manifest-urls
Normal file
1
.well-known/funding-manifest-urls
Normal file
|
@ -0,0 +1 @@
|
||||||
|
https://dangerzone.rocks/assets/json/funding.json
|
77
BUILD.md
77
BUILD.md
|
@ -34,29 +34,6 @@ Install dependencies:
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<details>
|
|
||||||
<summary><i>:memo: Expand this section if you are on Ubuntu 20.04 (Focal).</i></summary>
|
|
||||||
</br>
|
|
||||||
|
|
||||||
The default Python version that ships with Ubuntu Focal (3.8) is not
|
|
||||||
compatible with PySide6, which requires Python 3.9 or greater.
|
|
||||||
|
|
||||||
You can install Python 3.9 using the `python3.9` package.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt install -y python3.9
|
|
||||||
```
|
|
||||||
|
|
||||||
Poetry will automatically pick up the correct version when running.
|
|
||||||
</details>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo apt install -y podman dh-python build-essential make libqt6gui6 \
|
sudo apt install -y podman dh-python build-essential make libqt6gui6 \
|
||||||
pipx python3 python3-dev
|
pipx python3 python3-dev
|
||||||
|
@ -132,33 +109,11 @@ sudo dnf install -y rpm-build podman python3 python3-devel python3-poetry-core \
|
||||||
pipx qt6-qtbase-gui
|
pipx qt6-qtbase-gui
|
||||||
```
|
```
|
||||||
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<details>
|
|
||||||
<summary><i>:memo: Expand this section if you are on Fedora 41.</i></summary>
|
|
||||||
</br>
|
|
||||||
|
|
||||||
The default Python version that ships with Fedora 41 (3.13) is not
|
|
||||||
compatible with PySide6, which requires Python 3.12 or earlier.
|
|
||||||
|
|
||||||
You can install Python 3.12 using the `python3.12` package.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo dnf install -y python3.12
|
|
||||||
```
|
|
||||||
|
|
||||||
Poetry will automatically pick up the correct version when running.
|
|
||||||
</details>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
Install Poetry using `pipx`:
|
Install Poetry using `pipx`:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pipx install poetry
|
pipx install poetry
|
||||||
pipx inject poetry poetry-plugin-export
|
pipx inject poetry
|
||||||
```
|
```
|
||||||
|
|
||||||
Clone this repository:
|
Clone this repository:
|
||||||
|
@ -232,27 +187,27 @@ Overview of the qubes you'll create:
|
||||||
|--------------|----------|---------|
|
|--------------|----------|---------|
|
||||||
| dz | app qube | Dangerzone development |
|
| dz | app qube | Dangerzone development |
|
||||||
| dz-dvm | app qube | offline disposable template for performing conversions |
|
| dz-dvm | app qube | offline disposable template for performing conversions |
|
||||||
| fedora-40-dz | template | template for the other two qubes |
|
| fedora-41-dz | template | template for the other two qubes |
|
||||||
|
|
||||||
#### In `dom0`:
|
#### In `dom0`:
|
||||||
|
|
||||||
The following instructions require typing commands in a terminal in dom0.
|
The following instructions require typing commands in a terminal in dom0.
|
||||||
|
|
||||||
1. Create a new Fedora **template** (`fedora-40-dz`) for Dangerzone development:
|
1. Create a new Fedora **template** (`fedora-41-dz`) for Dangerzone development:
|
||||||
|
|
||||||
```
|
```
|
||||||
qvm-clone fedora-40 fedora-40-dz
|
qvm-clone fedora-41 fedora-41-dz
|
||||||
```
|
```
|
||||||
|
|
||||||
> :bulb: Alternatively, you can use your base Fedora 40 template in the
|
> :bulb: Alternatively, you can use your base Fedora 40 template in the
|
||||||
> following instructions. In that case, skip this step and replace
|
> following instructions. In that case, skip this step and replace
|
||||||
> `fedora-40-dz` with `fedora-40` in the steps below.
|
> `fedora-41-dz` with `fedora-41` in the steps below.
|
||||||
|
|
||||||
2. Create an offline disposable template (app qube) called `dz-dvm`, based on the `fedora-40-dz`
|
2. Create an offline disposable template (app qube) called `dz-dvm`, based on the `fedora-41-dz`
|
||||||
template. This will be the qube where the documents will be sanitized:
|
template. This will be the qube where the documents will be sanitized:
|
||||||
|
|
||||||
```
|
```
|
||||||
qvm-create --class AppVM --label red --template fedora-40-dz \
|
qvm-create --class AppVM --label red --template fedora-41-dz \
|
||||||
--prop netvm="" --prop template_for_dispvms=True \
|
--prop netvm="" --prop template_for_dispvms=True \
|
||||||
--prop default_dispvm='' dz-dvm
|
--prop default_dispvm='' dz-dvm
|
||||||
```
|
```
|
||||||
|
@ -261,7 +216,7 @@ The following instructions require typing commands in a terminal in dom0.
|
||||||
and initiating the sanitization process:
|
and initiating the sanitization process:
|
||||||
|
|
||||||
```
|
```
|
||||||
qvm-create --class AppVM --label red --template fedora-40-dz dz
|
qvm-create --class AppVM --label red --template fedora-41-dz dz
|
||||||
qvm-volume resize dz:private $(numfmt --from=auto 20Gi)
|
qvm-volume resize dz:private $(numfmt --from=auto 20Gi)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -306,12 +261,12 @@ test it.
|
||||||
./install/linux/build-rpm.py --qubes
|
./install/linux/build-rpm.py --qubes
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Copy the produced `.rpm` file into `fedora-40-dz`
|
4. Copy the produced `.rpm` file into `fedora-41-dz`
|
||||||
```sh
|
```sh
|
||||||
qvm-copy dist/*.x86_64.rpm
|
qvm-copy dist/*.x86_64.rpm
|
||||||
```
|
```
|
||||||
|
|
||||||
#### In the `fedora-40-dz` template
|
#### In the `fedora-41-dz` template
|
||||||
|
|
||||||
1. Install the `.rpm` package you just copied
|
1. Install the `.rpm` package you just copied
|
||||||
|
|
||||||
|
@ -319,7 +274,7 @@ test it.
|
||||||
sudo dnf install ~/QubesIncoming/dz/*.rpm
|
sudo dnf install ~/QubesIncoming/dz/*.rpm
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Shutdown the `fedora-40-dz` template
|
2. Shutdown the `fedora-41-dz` template
|
||||||
|
|
||||||
### Developing Dangerzone
|
### Developing Dangerzone
|
||||||
|
|
||||||
|
@ -350,7 +305,7 @@ For changes in the server side components, you can simply edit them locally,
|
||||||
and they will be mirrored to the disposable qube through the `dz.ConvertDev`
|
and they will be mirrored to the disposable qube through the `dz.ConvertDev`
|
||||||
RPC call.
|
RPC call.
|
||||||
|
|
||||||
The only reason to build a new Qubes RPM and install it in the `fedora-40-dz`
|
The only reason to build a new Qubes RPM and install it in the `fedora-41-dz`
|
||||||
template for development is if:
|
template for development is if:
|
||||||
1. The project requires new server-side components.
|
1. The project requires new server-side components.
|
||||||
2. The code for `qubes/dz.ConvertDev` needs to be updated.
|
2. The code for `qubes/dz.ConvertDev` needs to be updated.
|
||||||
|
@ -371,7 +326,7 @@ cd dangerzone
|
||||||
Install Python dependencies:
|
Install Python dependencies:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
python3 -m pip install poetry poetry-plugin-export
|
python3 -m pip install poetry
|
||||||
poetry install
|
poetry install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -432,7 +387,7 @@ Install Microsoft Visual C++ 14.0 or greater. Get it with ["Microsoft C++ Build
|
||||||
Install [poetry](https://python-poetry.org/). Open PowerShell, and run:
|
Install [poetry](https://python-poetry.org/). Open PowerShell, and run:
|
||||||
|
|
||||||
```
|
```
|
||||||
python -m pip install poetry poetry-plugin-export
|
python -m pip install poetry
|
||||||
```
|
```
|
||||||
|
|
||||||
Install git from [here](https://git-scm.com/download/win), open a Windows terminal (`cmd.exe`) and clone this repository:
|
Install git from [here](https://git-scm.com/download/win), open a Windows terminal (`cmd.exe`) and clone this repository:
|
||||||
|
@ -478,13 +433,13 @@ poetry shell
|
||||||
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:
|
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:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
dotnet tool install --global wix --version 5.*
|
dotnet tool install --global wix --version 5.0.2
|
||||||
```
|
```
|
||||||
|
|
||||||
Install the WiX UI extension. You may need to open a new terminal in order to use the newly installed `wix` .NET tool:
|
Install the WiX UI extension. You may need to open a new terminal in order to use the newly installed `wix` .NET tool:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
wix extension add --global WixToolset.UI.wixext/5.x.y
|
wix extension add --global WixToolset.UI.wixext/5.0.2
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
|
|
69
CHANGELOG.md
69
CHANGELOG.md
|
@ -5,9 +5,70 @@ All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
||||||
since 0.4.1, and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
since 0.4.1, and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased](https://github.com/freedomofpress/dangerzone/compare/v0.8.1...HEAD)
|
## [Unreleased](https://github.com/freedomofpress/dangerzone/compare/v0.9.0...HEAD)
|
||||||
|
|
||||||
-
|
## [0.9.0](https://github.com/freedomofpress/dangerzone/compare/v0.9.0...0.8.1)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Platform support: Add support for Fedora 42 ([#1091](https://github.com/freedomofpress/dangerzone/issues/1091))
|
||||||
|
- Platform support: Add support for Ubuntu 25.04 (Plucky Puffin) ([#1090](https://github.com/freedomofpress/dangerzone/issues/1090))
|
||||||
|
- (experimental): It is now possible to specify a custom container runtime in
|
||||||
|
the settings, by using the `container_runtime` key. It should contain the path
|
||||||
|
to the container runtime you want to use. Please note that this doesn't mean
|
||||||
|
we support more container runtimes than Podman and Docker for the time being,
|
||||||
|
but enables you to chose which one you want to use, independently of your
|
||||||
|
platform. ([#925](https://github.com/freedomofpress/dangerzone/issues/925))
|
||||||
|
- Document Operating System support [#986](https://github.com/freedomofpress/dangerzone/issues/986)
|
||||||
|
- Tests: Look for regressions when converting PDFs [#321](https://github.com/freedomofpress/dangerzone/issues/321)
|
||||||
|
- Ensure container image reproducibilty across different container runtimes and versions ([#1074](https://github.com/freedomofpress/dangerzone/issues/1074))
|
||||||
|
- Implement container image attestations ([#1035](https://github.com/freedomofpress/dangerzone/issues/1035))
|
||||||
|
- Inform user of outdated Docker Desktop Version ([#693](https://github.com/freedomofpress/dangerzone/issues/693))
|
||||||
|
- Add support for Python 3.13 ([#992](https://github.com/freedomofpress/dangerzone/issues/992))
|
||||||
|
- Publish the built artifacts in our CI pipelines ([#972](https://github.com/freedomofpress/dangerzone/pull/972))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix our Debian Trixie installation instructions using Sequoia PGP ([#1052](https://github.com/freedomofpress/dangerzone/issues/1052))
|
||||||
|
- Fix the way multiprocessing works on macOS ([#873](https://github.com/freedomofpress/dangerzone/issues/873))
|
||||||
|
- Update minimum Docker Desktop version to fix an stdout truncation issue ([#1101](https://github.com/freedomofpress/dangerzone/issues/1101))
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Platform support: Drop support for Ubuntu Focal, since it's nearing end-of-life ([#1018](https://github.com/freedomofpress/dangerzone/issues/1018))
|
||||||
|
- Platform support: Drop support for Fedora 39 ([#999](https://github.com/freedomofpress/dangerzone/issues/999))
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- Switch base image to Debian Stable ([#1046](https://github.com/freedomofpress/dangerzone/issues/1046))
|
||||||
|
- Track image tags instead of image IDs in `image-id.txt` ([#1020](https://github.com/freedomofpress/dangerzone/issues/1020))
|
||||||
|
- Migrate to Wix 4 (windows building tool) ([#602](https://github.com/freedomofpress/dangerzone/issues/602)).
|
||||||
|
Thanks [@jkarasti](https://github.com/jkarasti) for the contribution.
|
||||||
|
- Add a `--debug` flag to the CLI to help retrieve more logs ([#941](https://github.com/freedomofpress/dangerzone/pull/941))
|
||||||
|
- The `debian` base image is now fetched by digest. As a result, your local
|
||||||
|
container storage will no longer show a tag for this dependency
|
||||||
|
([#1116](https://github.com/freedomofpress/dangerzone/pull/1116)).
|
||||||
|
Thanks [@sudoforge](https://github.com/sudoforge) for the contribution.
|
||||||
|
- The `debian` base image is now referenced with a fully qualified URI,
|
||||||
|
including the registry hostname ([#1118](https://github.com/freedomofpress/dangerzone/pull/1118)).
|
||||||
|
Thanks [@sudoforge](https://github.com/sudoforge) for the contribution.
|
||||||
|
- Update the Dangerzone container image and its dependencies (gVisor, Debian base image, H2Orestart) to the latest versions:
|
||||||
|
* Debian image release: `bookworm-20250317-slim@sha256:1209d8fd77def86ceb6663deef7956481cc6c14a25e1e64daec12c0ceffcc19d`
|
||||||
|
* Debian snapshots date: `2025-03-31`
|
||||||
|
* gVisor release date: `2025-03-26`
|
||||||
|
* H2Orestart plugin: `v0.7.2` (`d09bc5c93fe2483a7e4a57985d2a8d0e4efae2efb04375fe4b59a68afd7241e2`)
|
||||||
|
|
||||||
|
### Development changes
|
||||||
|
|
||||||
|
- Make container image scanning work for Silicon macOS ([#1008](https://github.com/freedomofpress/dangerzone/issues/1008))
|
||||||
|
- Automate the main bulk of our release tasks ([#1016](https://github.com/freedomofpress/dangerzone/issues/1016))
|
||||||
|
- CI: Enforce updating the CHANGELOG in the CI ([#1108](https://github.com/freedomofpress/dangerzone/pull/1108))
|
||||||
|
- Add reference to funding.json (required by floss.fund application) ([#1092](https://github.com/freedomofpress/dangerzone/pull/1092))
|
||||||
|
- Lint: add ruff for linting and formatting ([#1029](https://github.com/freedomofpress/dangerzone/pull/1029)).
|
||||||
|
Thanks [@jkarasti](https://github.com/jkarasti) for the contribution.
|
||||||
|
- Work around a `cx_freeze` build issue ([#974](https://github.com/freedomofpress/dangerzone/issues/974))
|
||||||
|
- tests: mark the hancom office suite tests for rerun on failures ([#991](https://github.com/freedomofpress/dangerzone/pull/991))
|
||||||
|
- Update reference template for Qubes to Fedora 41 ([#1078](https://github.com/freedomofpress/dangerzone/issues/1078))
|
||||||
|
|
||||||
## [0.8.1](https://github.com/freedomofpress/dangerzone/compare/v0.8.1...0.8.0)
|
## [0.8.1](https://github.com/freedomofpress/dangerzone/compare/v0.8.1...0.8.0)
|
||||||
|
|
||||||
|
@ -22,6 +83,10 @@ since 0.4.1, and this project adheres to [Semantic Versioning](https://semver.or
|
||||||
|
|
||||||
- Platform support: Drop support for Fedora 39, since it's end-of-life ([#999](https://github.com/freedomofpress/dangerzone/pull/999))
|
- Platform support: Drop support for Fedora 39, since it's end-of-life ([#999](https://github.com/freedomofpress/dangerzone/pull/999))
|
||||||
|
|
||||||
|
## Updated
|
||||||
|
|
||||||
|
- Bump `slsa-framework/slsa-github-generator` from 2.0.0 to 2.1.0 ([#1109](https://github.com/freedomofpress/dangerzone/pull/1109))
|
||||||
|
|
||||||
### Development changes
|
### Development changes
|
||||||
|
|
||||||
Thanks [@jkarasti](https://github.com/jkarasti) for the contribution.
|
Thanks [@jkarasti](https://github.com/jkarasti) for the contribution.
|
||||||
|
|
79
Dockerfile
79
Dockerfile
|
@ -2,14 +2,14 @@
|
||||||
# Dockerfile args below. For more info about this file, read
|
# Dockerfile args below. For more info about this file, read
|
||||||
# docs/developer/reproducibility.md.
|
# docs/developer/reproducibility.md.
|
||||||
|
|
||||||
ARG DEBIAN_IMAGE_DATE=20250113
|
ARG DEBIAN_IMAGE_DIGEST=sha256:1209d8fd77def86ceb6663deef7956481cc6c14a25e1e64daec12c0ceffcc19d
|
||||||
|
|
||||||
FROM debian:bookworm-${DEBIAN_IMAGE_DATE}-slim as dangerzone-image
|
FROM docker.io/library/debian@${DEBIAN_IMAGE_DIGEST} AS dangerzone-image
|
||||||
|
|
||||||
ARG GVISOR_ARCHIVE_DATE=20250120
|
ARG GVISOR_ARCHIVE_DATE=20250326
|
||||||
ARG DEBIAN_ARCHIVE_DATE=20250127
|
ARG DEBIAN_ARCHIVE_DATE=20250331
|
||||||
ARG H2ORESTART_CHECKSUM=7760dc2963332c50d15eee285933ec4b48d6a1de9e0c0f6082946f93090bd132
|
ARG H2ORESTART_CHECKSUM=935e68671bde4ca63a364128077f1c733349bbcc90b7e6973bc7a2306494ec54
|
||||||
ARG H2ORESTART_VERSION=v0.7.0
|
ARG H2ORESTART_VERSION=v0.7.2
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
@ -22,8 +22,8 @@ RUN \
|
||||||
--mount=type=bind,source=./container_helpers/repro-sources-list.sh,target=/usr/local/bin/repro-sources-list.sh \
|
--mount=type=bind,source=./container_helpers/repro-sources-list.sh,target=/usr/local/bin/repro-sources-list.sh \
|
||||||
--mount=type=bind,source=./container_helpers/gvisor.key,target=/tmp/gvisor.key \
|
--mount=type=bind,source=./container_helpers/gvisor.key,target=/tmp/gvisor.key \
|
||||||
: "Hacky way to set a date for the Debian snapshot repos" && \
|
: "Hacky way to set a date for the Debian snapshot repos" && \
|
||||||
touch -d ${DEBIAN_ARCHIVE_DATE} /etc/apt/sources.list.d/debian.sources && \
|
touch -d ${DEBIAN_ARCHIVE_DATE}Z /etc/apt/sources.list.d/debian.sources && \
|
||||||
touch -d ${DEBIAN_ARCHIVE_DATE} /etc/apt/sources.list && \
|
touch -d ${DEBIAN_ARCHIVE_DATE}Z /etc/apt/sources.list && \
|
||||||
repro-sources-list.sh && \
|
repro-sources-list.sh && \
|
||||||
: "Setup APT to install gVisor from its separate APT repo" && \
|
: "Setup APT to install gVisor from its separate APT repo" && \
|
||||||
apt-get update && \
|
apt-get update && \
|
||||||
|
@ -52,9 +52,13 @@ RUN mkdir /opt/libreoffice_ext && cd /opt/libreoffice_ext \
|
||||||
&& rm /root/.wget-hsts
|
&& rm /root/.wget-hsts
|
||||||
|
|
||||||
# Create an unprivileged user both for gVisor and for running Dangerzone.
|
# Create an unprivileged user both for gVisor and for running Dangerzone.
|
||||||
|
# XXX: Make the shadow field "date of last password change" a constant
|
||||||
|
# number.
|
||||||
RUN addgroup --gid 1000 dangerzone
|
RUN addgroup --gid 1000 dangerzone
|
||||||
RUN adduser --uid 1000 --ingroup dangerzone --shell /bin/true \
|
RUN adduser --uid 1000 --ingroup dangerzone --shell /bin/true \
|
||||||
--disabled-password --home /home/dangerzone dangerzone
|
--disabled-password --home /home/dangerzone dangerzone \
|
||||||
|
&& chage -d 99999 dangerzone \
|
||||||
|
&& rm /etc/shadow-
|
||||||
|
|
||||||
# Copy Dangerzone's conversion logic under /opt/dangerzone, and allow Python to
|
# Copy Dangerzone's conversion logic under /opt/dangerzone, and allow Python to
|
||||||
# import it.
|
# import it.
|
||||||
|
@ -165,20 +169,50 @@ RUN mkdir /home/dangerzone/.containers
|
||||||
# The `ln` binary, even if you specify it by its full path, cannot run
|
# The `ln` binary, even if you specify it by its full path, cannot run
|
||||||
# (probably because `ld-linux.so` can't be found). For this reason, we have
|
# (probably because `ld-linux.so` can't be found). For this reason, we have
|
||||||
# to create the symlinks beforehand, in a previous build stage. Then, in an
|
# to create the symlinks beforehand, in a previous build stage. Then, in an
|
||||||
# empty contianer image (scratch images), we can copy these symlinks and the
|
# empty container image (scratch images), we can copy these symlinks and the
|
||||||
# /usr, and stich everything together.
|
# /usr, and stitch everything together.
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
# Create the filesystem hierarchy that will be used to symlink /usr.
|
# Create the filesystem hierarchy that will be used to symlink /usr.
|
||||||
|
|
||||||
RUN mkdir /new_root
|
RUN mkdir -p \
|
||||||
RUN mkdir /new_root/root /new_root/run /new_root/tmp
|
/new_root \
|
||||||
RUN chmod 777 /new_root/tmp
|
/new_root/root \
|
||||||
|
/new_root/run \
|
||||||
|
/new_root/tmp \
|
||||||
|
/new_root/home/dangerzone/dangerzone-image/rootfs
|
||||||
|
|
||||||
|
# Copy the /etc and /var directories under the new root directory. Also,
|
||||||
|
# copy /etc/, /opt, and /usr to the Dangerzone image rootfs.
|
||||||
|
#
|
||||||
|
# NOTE: We also have to remove the resolv.conf file, in order to not leak any
|
||||||
|
# DNS servers added there during image build time.
|
||||||
|
RUN cp -r /etc /var /new_root/ \
|
||||||
|
&& rm /new_root/etc/resolv.conf
|
||||||
|
RUN cp -r /etc /opt /usr /new_root/home/dangerzone/dangerzone-image/rootfs \
|
||||||
|
&& rm /new_root/home/dangerzone/dangerzone-image/rootfs/etc/resolv.conf
|
||||||
|
|
||||||
RUN ln -s /home/dangerzone/dangerzone-image/rootfs/usr /new_root/usr
|
RUN ln -s /home/dangerzone/dangerzone-image/rootfs/usr /new_root/usr
|
||||||
RUN ln -s usr/bin /new_root/bin
|
RUN ln -s usr/bin /new_root/bin
|
||||||
RUN ln -s usr/lib /new_root/lib
|
RUN ln -s usr/lib /new_root/lib
|
||||||
RUN ln -s usr/lib64 /new_root/lib64
|
RUN ln -s usr/lib64 /new_root/lib64
|
||||||
RUN ln -s usr/sbin /new_root/sbin
|
RUN ln -s usr/sbin /new_root/sbin
|
||||||
|
RUN ln -s usr/bin /new_root/home/dangerzone/dangerzone-image/rootfs/bin
|
||||||
|
RUN ln -s usr/lib /new_root/home/dangerzone/dangerzone-image/rootfs/lib
|
||||||
|
RUN ln -s usr/lib64 /new_root/home/dangerzone/dangerzone-image/rootfs/lib64
|
||||||
|
|
||||||
|
# Fix permissions in /home/dangerzone, so that our entrypoint script can make
|
||||||
|
# changes in the following folders.
|
||||||
|
RUN chown dangerzone:dangerzone \
|
||||||
|
/new_root/home/dangerzone \
|
||||||
|
/new_root/home/dangerzone/dangerzone-image/
|
||||||
|
# Fix permissions in /tmp, so that it can be used by unprivileged users.
|
||||||
|
RUN chmod 777 /new_root/tmp
|
||||||
|
|
||||||
|
COPY container_helpers/entrypoint.py /new_root
|
||||||
|
# HACK: For reasons that we are not sure yet, we need to explicitly specify the
|
||||||
|
# modification time of this file.
|
||||||
|
RUN touch -d ${DEBIAN_ARCHIVE_DATE}Z /new_root/entrypoint.py
|
||||||
|
|
||||||
## Final image
|
## Final image
|
||||||
|
|
||||||
|
@ -188,24 +222,7 @@ FROM scratch
|
||||||
# /usr can be a symlink.
|
# /usr can be a symlink.
|
||||||
COPY --from=dangerzone-image /new_root/ /
|
COPY --from=dangerzone-image /new_root/ /
|
||||||
|
|
||||||
# Copy the bare minimum to run Dangerzone in the inner container image.
|
|
||||||
COPY --from=dangerzone-image /etc/ /home/dangerzone/dangerzone-image/rootfs/etc/
|
|
||||||
COPY --from=dangerzone-image /opt/ /home/dangerzone/dangerzone-image/rootfs/opt/
|
|
||||||
COPY --from=dangerzone-image /usr/ /home/dangerzone/dangerzone-image/rootfs/usr/
|
|
||||||
RUN ln -s usr/bin /home/dangerzone/dangerzone-image/rootfs/bin
|
|
||||||
RUN ln -s usr/lib /home/dangerzone/dangerzone-image/rootfs/lib
|
|
||||||
RUN ln -s usr/lib64 /home/dangerzone/dangerzone-image/rootfs/lib64
|
|
||||||
|
|
||||||
# Copy the bare minimum to let the security scanner find vulnerabilities.
|
|
||||||
COPY --from=dangerzone-image /etc/ /etc/
|
|
||||||
COPY --from=dangerzone-image /var/ /var/
|
|
||||||
|
|
||||||
# Allow our entrypoint script to make changes in the following folders.
|
|
||||||
RUN chown dangerzone:dangerzone /home/dangerzone /home/dangerzone/dangerzone-image/
|
|
||||||
|
|
||||||
# Switch to the dangerzone user for the rest of the script.
|
# Switch to the dangerzone user for the rest of the script.
|
||||||
USER dangerzone
|
USER dangerzone
|
||||||
|
|
||||||
COPY container_helpers/entrypoint.py /
|
|
||||||
|
|
||||||
ENTRYPOINT ["/entrypoint.py"]
|
ENTRYPOINT ["/entrypoint.py"]
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
# Can be bumped to the latest date in https://hub.docker.com/_/debian/tags?name=bookworm-
|
# Should be the INDEX DIGEST from an image tagged `bookworm-<DATE>-slim`:
|
||||||
DEBIAN_IMAGE_DATE=20250113
|
# https://hub.docker.com/_/debian/tags?name=bookworm-
|
||||||
|
#
|
||||||
|
# Tag for this digest: bookworm-20250317-slim
|
||||||
|
DEBIAN_IMAGE_DIGEST=sha256:1209d8fd77def86ceb6663deef7956481cc6c14a25e1e64daec12c0ceffcc19d
|
||||||
# Can be bumped to today's date
|
# Can be bumped to today's date
|
||||||
DEBIAN_ARCHIVE_DATE=20250127
|
DEBIAN_ARCHIVE_DATE=20250331
|
||||||
# Can be bumped to the latest date in https://github.com/google/gvisor/tags
|
# Can be bumped to the latest date in https://github.com/google/gvisor/tags
|
||||||
GVISOR_ARCHIVE_DATE=20250120
|
GVISOR_ARCHIVE_DATE=20250326
|
||||||
# Can be bumped to the latest version and checksum from https://github.com/ebandal/H2Orestart/releases
|
# Can be bumped to the latest version and checksum from https://github.com/ebandal/H2Orestart/releases
|
||||||
H2ORESTART_CHECKSUM=7760dc2963332c50d15eee285933ec4b48d6a1de9e0c0f6082946f93090bd132
|
H2ORESTART_CHECKSUM=935e68671bde4ca63a364128077f1c733349bbcc90b7e6973bc7a2306494ec54
|
||||||
H2ORESTART_VERSION=v0.7.0
|
H2ORESTART_VERSION=v0.7.2
|
||||||
|
|
||||||
|
# Buildkit image (taken from freedomofpress/repro-build)
|
||||||
|
BUILDKIT_IMAGE="docker.io/moby/buildkit:v19.0@sha256:14aa1b4dd92ea0a4cd03a54d0c6079046ea98cd0c0ae6176bdd7036ba370cbbe"
|
||||||
|
BUILDKIT_IMAGE_ROOTLESS="docker.io/moby/buildkit:v0.19.0-rootless@sha256:e901cffdad753892a7c3afb8b9972549fca02c73888cf340c91ed801fdd96d71"
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
# Dockerfile args below. For more info about this file, read
|
# Dockerfile args below. For more info about this file, read
|
||||||
# docs/developer/reproducibility.md.
|
# docs/developer/reproducibility.md.
|
||||||
|
|
||||||
ARG DEBIAN_IMAGE_DATE={{DEBIAN_IMAGE_DATE}}
|
ARG DEBIAN_IMAGE_DIGEST={{DEBIAN_IMAGE_DIGEST}}
|
||||||
|
|
||||||
FROM debian:bookworm-${DEBIAN_IMAGE_DATE}-slim as dangerzone-image
|
FROM docker.io/library/debian@${DEBIAN_IMAGE_DIGEST} AS dangerzone-image
|
||||||
|
|
||||||
ARG GVISOR_ARCHIVE_DATE={{GVISOR_ARCHIVE_DATE}}
|
ARG GVISOR_ARCHIVE_DATE={{GVISOR_ARCHIVE_DATE}}
|
||||||
ARG DEBIAN_ARCHIVE_DATE={{DEBIAN_ARCHIVE_DATE}}
|
ARG DEBIAN_ARCHIVE_DATE={{DEBIAN_ARCHIVE_DATE}}
|
||||||
|
@ -22,8 +22,8 @@ RUN \
|
||||||
--mount=type=bind,source=./container_helpers/repro-sources-list.sh,target=/usr/local/bin/repro-sources-list.sh \
|
--mount=type=bind,source=./container_helpers/repro-sources-list.sh,target=/usr/local/bin/repro-sources-list.sh \
|
||||||
--mount=type=bind,source=./container_helpers/gvisor.key,target=/tmp/gvisor.key \
|
--mount=type=bind,source=./container_helpers/gvisor.key,target=/tmp/gvisor.key \
|
||||||
: "Hacky way to set a date for the Debian snapshot repos" && \
|
: "Hacky way to set a date for the Debian snapshot repos" && \
|
||||||
touch -d ${DEBIAN_ARCHIVE_DATE} /etc/apt/sources.list.d/debian.sources && \
|
touch -d ${DEBIAN_ARCHIVE_DATE}Z /etc/apt/sources.list.d/debian.sources && \
|
||||||
touch -d ${DEBIAN_ARCHIVE_DATE} /etc/apt/sources.list && \
|
touch -d ${DEBIAN_ARCHIVE_DATE}Z /etc/apt/sources.list && \
|
||||||
repro-sources-list.sh && \
|
repro-sources-list.sh && \
|
||||||
: "Setup APT to install gVisor from its separate APT repo" && \
|
: "Setup APT to install gVisor from its separate APT repo" && \
|
||||||
apt-get update && \
|
apt-get update && \
|
||||||
|
@ -52,9 +52,13 @@ RUN mkdir /opt/libreoffice_ext && cd /opt/libreoffice_ext \
|
||||||
&& rm /root/.wget-hsts
|
&& rm /root/.wget-hsts
|
||||||
|
|
||||||
# Create an unprivileged user both for gVisor and for running Dangerzone.
|
# Create an unprivileged user both for gVisor and for running Dangerzone.
|
||||||
|
# XXX: Make the shadow field "date of last password change" a constant
|
||||||
|
# number.
|
||||||
RUN addgroup --gid 1000 dangerzone
|
RUN addgroup --gid 1000 dangerzone
|
||||||
RUN adduser --uid 1000 --ingroup dangerzone --shell /bin/true \
|
RUN adduser --uid 1000 --ingroup dangerzone --shell /bin/true \
|
||||||
--disabled-password --home /home/dangerzone dangerzone
|
--disabled-password --home /home/dangerzone dangerzone \
|
||||||
|
&& chage -d 99999 dangerzone \
|
||||||
|
&& rm /etc/shadow-
|
||||||
|
|
||||||
# Copy Dangerzone's conversion logic under /opt/dangerzone, and allow Python to
|
# Copy Dangerzone's conversion logic under /opt/dangerzone, and allow Python to
|
||||||
# import it.
|
# import it.
|
||||||
|
@ -165,20 +169,50 @@ RUN mkdir /home/dangerzone/.containers
|
||||||
# The `ln` binary, even if you specify it by its full path, cannot run
|
# The `ln` binary, even if you specify it by its full path, cannot run
|
||||||
# (probably because `ld-linux.so` can't be found). For this reason, we have
|
# (probably because `ld-linux.so` can't be found). For this reason, we have
|
||||||
# to create the symlinks beforehand, in a previous build stage. Then, in an
|
# to create the symlinks beforehand, in a previous build stage. Then, in an
|
||||||
# empty contianer image (scratch images), we can copy these symlinks and the
|
# empty container image (scratch images), we can copy these symlinks and the
|
||||||
# /usr, and stich everything together.
|
# /usr, and stitch everything together.
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
# Create the filesystem hierarchy that will be used to symlink /usr.
|
# Create the filesystem hierarchy that will be used to symlink /usr.
|
||||||
|
|
||||||
RUN mkdir /new_root
|
RUN mkdir -p \
|
||||||
RUN mkdir /new_root/root /new_root/run /new_root/tmp
|
/new_root \
|
||||||
RUN chmod 777 /new_root/tmp
|
/new_root/root \
|
||||||
|
/new_root/run \
|
||||||
|
/new_root/tmp \
|
||||||
|
/new_root/home/dangerzone/dangerzone-image/rootfs
|
||||||
|
|
||||||
|
# Copy the /etc and /var directories under the new root directory. Also,
|
||||||
|
# copy /etc/, /opt, and /usr to the Dangerzone image rootfs.
|
||||||
|
#
|
||||||
|
# NOTE: We also have to remove the resolv.conf file, in order to not leak any
|
||||||
|
# DNS servers added there during image build time.
|
||||||
|
RUN cp -r /etc /var /new_root/ \
|
||||||
|
&& rm /new_root/etc/resolv.conf
|
||||||
|
RUN cp -r /etc /opt /usr /new_root/home/dangerzone/dangerzone-image/rootfs \
|
||||||
|
&& rm /new_root/home/dangerzone/dangerzone-image/rootfs/etc/resolv.conf
|
||||||
|
|
||||||
RUN ln -s /home/dangerzone/dangerzone-image/rootfs/usr /new_root/usr
|
RUN ln -s /home/dangerzone/dangerzone-image/rootfs/usr /new_root/usr
|
||||||
RUN ln -s usr/bin /new_root/bin
|
RUN ln -s usr/bin /new_root/bin
|
||||||
RUN ln -s usr/lib /new_root/lib
|
RUN ln -s usr/lib /new_root/lib
|
||||||
RUN ln -s usr/lib64 /new_root/lib64
|
RUN ln -s usr/lib64 /new_root/lib64
|
||||||
RUN ln -s usr/sbin /new_root/sbin
|
RUN ln -s usr/sbin /new_root/sbin
|
||||||
|
RUN ln -s usr/bin /new_root/home/dangerzone/dangerzone-image/rootfs/bin
|
||||||
|
RUN ln -s usr/lib /new_root/home/dangerzone/dangerzone-image/rootfs/lib
|
||||||
|
RUN ln -s usr/lib64 /new_root/home/dangerzone/dangerzone-image/rootfs/lib64
|
||||||
|
|
||||||
|
# Fix permissions in /home/dangerzone, so that our entrypoint script can make
|
||||||
|
# changes in the following folders.
|
||||||
|
RUN chown dangerzone:dangerzone \
|
||||||
|
/new_root/home/dangerzone \
|
||||||
|
/new_root/home/dangerzone/dangerzone-image/
|
||||||
|
# Fix permissions in /tmp, so that it can be used by unprivileged users.
|
||||||
|
RUN chmod 777 /new_root/tmp
|
||||||
|
|
||||||
|
COPY container_helpers/entrypoint.py /new_root
|
||||||
|
# HACK: For reasons that we are not sure yet, we need to explicitly specify the
|
||||||
|
# modification time of this file.
|
||||||
|
RUN touch -d ${DEBIAN_ARCHIVE_DATE}Z /new_root/entrypoint.py
|
||||||
|
|
||||||
## Final image
|
## Final image
|
||||||
|
|
||||||
|
@ -188,24 +222,7 @@ FROM scratch
|
||||||
# /usr can be a symlink.
|
# /usr can be a symlink.
|
||||||
COPY --from=dangerzone-image /new_root/ /
|
COPY --from=dangerzone-image /new_root/ /
|
||||||
|
|
||||||
# Copy the bare minimum to run Dangerzone in the inner container image.
|
|
||||||
COPY --from=dangerzone-image /etc/ /home/dangerzone/dangerzone-image/rootfs/etc/
|
|
||||||
COPY --from=dangerzone-image /opt/ /home/dangerzone/dangerzone-image/rootfs/opt/
|
|
||||||
COPY --from=dangerzone-image /usr/ /home/dangerzone/dangerzone-image/rootfs/usr/
|
|
||||||
RUN ln -s usr/bin /home/dangerzone/dangerzone-image/rootfs/bin
|
|
||||||
RUN ln -s usr/lib /home/dangerzone/dangerzone-image/rootfs/lib
|
|
||||||
RUN ln -s usr/lib64 /home/dangerzone/dangerzone-image/rootfs/lib64
|
|
||||||
|
|
||||||
# Copy the bare minimum to let the security scanner find vulnerabilities.
|
|
||||||
COPY --from=dangerzone-image /etc/ /etc/
|
|
||||||
COPY --from=dangerzone-image /var/ /var/
|
|
||||||
|
|
||||||
# Allow our entrypoint script to make changes in the following folders.
|
|
||||||
RUN chown dangerzone:dangerzone /home/dangerzone /home/dangerzone/dangerzone-image/
|
|
||||||
|
|
||||||
# Switch to the dangerzone user for the rest of the script.
|
# Switch to the dangerzone user for the rest of the script.
|
||||||
USER dangerzone
|
USER dangerzone
|
||||||
|
|
||||||
COPY container_helpers/entrypoint.py /
|
|
||||||
|
|
||||||
ENTRYPOINT ["/entrypoint.py"]
|
ENTRYPOINT ["/entrypoint.py"]
|
||||||
|
|
104
INSTALL.md
104
INSTALL.md
|
@ -1,7 +1,41 @@
|
||||||
|
## Operating System support
|
||||||
|
|
||||||
|
Dangerzone can run on various Operating Systems (OS), and has automated tests
|
||||||
|
for most of them.
|
||||||
|
This section explains which OS we support, how long we support each version, and
|
||||||
|
how do we test Dangerzone against these.
|
||||||
|
|
||||||
|
You can find general support information in this table, and more details in the
|
||||||
|
following sections.
|
||||||
|
|
||||||
|
(Unless specified, the architecture of the OS is AMD64)
|
||||||
|
|
||||||
|
| Distribution | Supported releases | Automated tests | Manual QA |
|
||||||
|
| ------------ | ------------------------- | ---------------------- | --------- |
|
||||||
|
| Windows | 2 last releases | 🗹 (`windows-latest`) ◎ | 🗹 |
|
||||||
|
| macOS intel | 3 last releases | 🗹 (`macos-13`) ◎ | 🗹 |
|
||||||
|
| macOS silicon | 3 last releases | 🗹 (`macos-latest`) ◎ | 🗹 |
|
||||||
|
| Ubuntu | Follow upstream support ✰ | 🗹 | 🗹 |
|
||||||
|
| Debian | Current stable, Oldstable and LTS releases | 🗹 | 🗹 |
|
||||||
|
| Fedora | Follow upstream support | 🗹 | 🗹 |
|
||||||
|
| Qubes OS | [Beta support](https://github.com/freedomofpress/dangerzone/issues/413) ✢ | 🗷 | Latest Fedora template |
|
||||||
|
| Tails | Only the last release | 🗷 | Last release only |
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
✰ Support for Ubuntu Focal [was dropped](https://github.com/freedomofpress/dangerzone/issues/1018)
|
||||||
|
|
||||||
|
✢ Qubes OS support assumes the use of a Fedora template. The supported releases follow our general support for Fedora.
|
||||||
|
|
||||||
|
◎ More information about where that points [in the runner-images repository](https://github.com/actions/runner-images/tree/main)
|
||||||
|
|
||||||
## MacOS
|
## MacOS
|
||||||
|
|
||||||
- Download [Dangerzone 0.8.1 for Mac (Apple Silicon CPU)](https://github.com/freedomofpress/dangerzone/releases/download/v0.8.1/Dangerzone-0.8.1-arm64.dmg)
|
- Download [Dangerzone 0.9.0 for Mac (Apple Silicon CPU)](https://github.com/freedomofpress/dangerzone/releases/download/v0.9.0/Dangerzone-0.9.0-arm64.dmg)
|
||||||
- Download [Dangerzone 0.8.1 for Mac (Intel CPU)](https://github.com/freedomofpress/dangerzone/releases/download/v0.8.1/Dangerzone-0.8.1-i686.dmg)
|
- Download [Dangerzone 0.9.0 for Mac (Intel CPU)](https://github.com/freedomofpress/dangerzone/releases/download/v0.9.0/Dangerzone-0.9.0-i686.dmg)
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> We support the releases of macOS that are still within Apple's servicing timeline. Apple usually provides security updates for the latest 3 releases, but this isn’t consistently applied and security fixes aren’t guaranteed for the non-latest releases. We are also dependent on [Docker Desktop windows support](https://docs.docker.com/desktop/setup/install/mac-install/)
|
||||||
|
|
||||||
You can also install Dangerzone for Mac using [Homebrew](https://brew.sh/): `brew install --cask dangerzone`
|
You can also install Dangerzone for Mac using [Homebrew](https://brew.sh/): `brew install --cask dangerzone`
|
||||||
|
|
||||||
|
@ -11,24 +45,44 @@ You can also install Dangerzone for Mac using [Homebrew](https://brew.sh/): `bre
|
||||||
|
|
||||||
## Windows
|
## Windows
|
||||||
|
|
||||||
- Download [Dangerzone 0.8.1 for Windows](https://github.com/freedomofpress/dangerzone/releases/download/v0.8.1/Dangerzone-0.8.1.msi)
|
- Download [Dangerzone 0.9.0 for Windows](https://github.com/freedomofpress/dangerzone/releases/download/v0.9.0/Dangerzone-0.9.0.msi)
|
||||||
|
|
||||||
> **Note**: you will also need to install [Docker Desktop](https://www.docker.com/products/docker-desktop/).
|
> **Note**: you will also need to install [Docker Desktop](https://www.docker.com/products/docker-desktop/).
|
||||||
> This program needs to run alongside Dangerzone at all times, since it is what allows Dangerzone to
|
> This program needs to run alongside Dangerzone at all times, since it is what allows Dangerzone to
|
||||||
> create the secure environment.
|
> create the secure environment.
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> We generally support Windows releases that are still within [Microsoft’s servicing timeline](https://support.microsoft.com/en-us/help/13853/windows-lifecycle-fact-sheet).
|
||||||
|
>
|
||||||
|
> Docker sets the bottom line:
|
||||||
|
>
|
||||||
|
> > Docker only supports Docker Desktop on Windows for those versions of Windows that are still within [Microsoft’s servicing timeline](https://support.microsoft.com/en-us/help/13853/windows-lifecycle-fact-sheet). Docker Desktop is not supported on server versions of Windows, such as Windows Server 2019 or Windows Server 2022.
|
||||||
|
|
||||||
|
|
||||||
## Linux
|
## Linux
|
||||||
|
|
||||||
On Linux, Dangerzone uses [Podman](https://podman.io/) instead of Docker Desktop for creating
|
On Linux, Dangerzone uses [Podman](https://podman.io/) instead of Docker Desktop for creating
|
||||||
an isolated environment. It will be installed automatically when installing Dangerzone.
|
an isolated environment. It will be installed automatically when installing Dangerzone.
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> We support Ubuntu, Debian, and Fedora releases that are still within
|
||||||
|
> their respective servicing timelines, with a few twists:
|
||||||
|
>
|
||||||
|
> - Ubuntu: We follow upstream support with an extra cutoff date. No support for
|
||||||
|
> versions prior to the second oldest LTS release.
|
||||||
|
> - Fedora: We follow upstream support
|
||||||
|
> - Debian: current stable, oldstable and LTS releases.
|
||||||
|
|
||||||
Dangerzone is available for:
|
Dangerzone is available for:
|
||||||
|
|
||||||
|
- Ubuntu 25.04 (plucky)
|
||||||
- Ubuntu 24.10 (oracular)
|
- Ubuntu 24.10 (oracular)
|
||||||
- Ubuntu 24.04 (noble)
|
- Ubuntu 24.04 (noble)
|
||||||
- Ubuntu 22.04 (jammy)
|
- Ubuntu 22.04 (jammy)
|
||||||
- Ubuntu 20.04 (focal)
|
|
||||||
- Debian 13 (trixie)
|
- Debian 13 (trixie)
|
||||||
- Debian 12 (bookworm)
|
- Debian 12 (bookworm)
|
||||||
- Debian 11 (bullseye)
|
- Debian 11 (bullseye)
|
||||||
|
- Fedora 42
|
||||||
- Fedora 41
|
- Fedora 41
|
||||||
- Fedora 40
|
- Fedora 40
|
||||||
- Tails
|
- Tails
|
||||||
|
@ -40,35 +94,7 @@ Dangerzone is available for:
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<details>
|
<details>
|
||||||
<summary><i>:memo: Expand this section if you are on Ubuntu 20.04 (Focal).</i></summary>
|
<summary><i>:information_source: Backport notice for Ubuntu 22.04 (Jammy) users regarding the <code>conmon</code> package</i></summary>
|
||||||
</br>
|
|
||||||
|
|
||||||
Dangerzone requires [Podman](https://podman.io/), which is not available
|
|
||||||
through the official Ubuntu Focal repos. To proceed with the Dangerzone
|
|
||||||
installation, you need to add an extra OpenSUSE repo that provides Podman to
|
|
||||||
Ubuntu Focal users. You can follow the instructions below, which have been
|
|
||||||
copied from the [official Podman blog](https://podman.io/new/2021/06/16/new.html):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt-get update && sudo apt-get install curl wget gnupg2 -y
|
|
||||||
. /etc/os-release
|
|
||||||
sudo sh -c "echo 'deb http://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_${VERSION_ID}/ /' \
|
|
||||||
> /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list"
|
|
||||||
wget -nv https://download.opensuse.org/repositories/devel:kubic:libcontainers:stable/xUbuntu_${VERSION_ID}/Release.key -O- \
|
|
||||||
| sudo apt-key add -
|
|
||||||
sudo apt update
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<details>
|
|
||||||
<summary><i>:information_source: Backport notice for Ubuntu 24.04 (Noble) users regarding the <code>conmon</code> package</i></summary>
|
|
||||||
</br>
|
</br>
|
||||||
|
|
||||||
The `conmon` version that Podman uses and Ubuntu Jammy ships, has a bug
|
The `conmon` version that Podman uses and Ubuntu Jammy ships, has a bug
|
||||||
|
@ -205,8 +231,8 @@ After confirming that it matches, type `y` (for yes) and the installation should
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> This section will install Dangerzone in your **default template**
|
> This section will install Dangerzone in your **default template**
|
||||||
> (`fedora-40` as of writing this). If you want to install it in a different
|
> (`fedora-41` as of writing this). If you want to install it in a different
|
||||||
> one, make sure to replace `fedora-40` with the template of your choice.
|
> one, make sure to replace `fedora-41` with the template of your choice.
|
||||||
|
|
||||||
The following steps must be completed once. Make sure you run them in the
|
The following steps must be completed once. Make sure you run them in the
|
||||||
specified qubes.
|
specified qubes.
|
||||||
|
@ -223,7 +249,7 @@ Create a **disposable**, offline app qube (`dz-dvm`), based on your default
|
||||||
template. This will be the qube where the documents will be sanitized:
|
template. This will be the qube where the documents will be sanitized:
|
||||||
|
|
||||||
```
|
```
|
||||||
qvm-create --class AppVM --label red --template fedora-40 \
|
qvm-create --class AppVM --label red --template fedora-41 \
|
||||||
--prop netvm="" --prop template_for_dispvms=True \
|
--prop netvm="" --prop template_for_dispvms=True \
|
||||||
--prop default_dispvm='' dz-dvm
|
--prop default_dispvm='' dz-dvm
|
||||||
```
|
```
|
||||||
|
@ -236,7 +262,7 @@ document, with the following contents:
|
||||||
dz.Convert * @anyvm @dispvm:dz-dvm allow
|
dz.Convert * @anyvm @dispvm:dz-dvm allow
|
||||||
```
|
```
|
||||||
|
|
||||||
#### In the `fedora-40` template
|
#### In the `fedora-41` template
|
||||||
|
|
||||||
Install Dangerzone:
|
Install Dangerzone:
|
||||||
|
|
||||||
|
@ -297,7 +323,7 @@ Our [GitHub Releases page](https://github.com/freedomofpress/dangerzone/releases
|
||||||
hosts the following files:
|
hosts the following files:
|
||||||
* Windows installer (`Dangerzone-<version>.msi`)
|
* Windows installer (`Dangerzone-<version>.msi`)
|
||||||
* macOS archives (`Dangerzone-<version>-<arch>.dmg`)
|
* macOS archives (`Dangerzone-<version>-<arch>.dmg`)
|
||||||
* Container images (`container-<version>-<arch>.tar.gz`)
|
* Container images (`container-<version>-<arch>.tar`)
|
||||||
* Source package (`dangerzone-<version>.tar.gz`)
|
* Source package (`dangerzone-<version>.tar.gz`)
|
||||||
|
|
||||||
All these files are accompanied by signatures (as `.asc` files). We'll explain
|
All these files are accompanied by signatures (as `.asc` files). We'll explain
|
||||||
|
@ -325,7 +351,7 @@ gpg --verify Dangerzone-0.6.1-i686.dmg.asc Dangerzone-0.6.1-i686.dmg
|
||||||
For the container images:
|
For the container images:
|
||||||
|
|
||||||
```
|
```
|
||||||
gpg --verify container-0.6.1-i686.tar.gz.asc container-0.6.1-i686.tar.gz
|
gpg --verify container-0.6.1-i686.tar.asc container-0.6.1-i686.tar
|
||||||
```
|
```
|
||||||
|
|
||||||
For the source package:
|
For the source package:
|
||||||
|
|
25
Makefile
25
Makefile
|
@ -22,7 +22,7 @@ fix: ## apply all the suggestions from ruff
|
||||||
ruff format
|
ruff format
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test:
|
test: ## Run the tests
|
||||||
# Make each GUI test run as a separate process, to avoid segfaults due to
|
# Make each GUI test run as a separate process, to avoid segfaults due to
|
||||||
# shared state.
|
# shared state.
|
||||||
# See more in https://github.com/freedomofpress/dangerzone/issues/493
|
# See more in https://github.com/freedomofpress/dangerzone/issues/493
|
||||||
|
@ -47,25 +47,32 @@ 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)
|
||||||
|
|
||||||
Dockerfile: Dockerfile.env Dockerfile.in
|
Dockerfile: Dockerfile.env Dockerfile.in ## Regenerate the Dockerfile from its template
|
||||||
poetry run jinja2 Dockerfile.in Dockerfile.env > Dockerfile
|
poetry run jinja2 Dockerfile.in Dockerfile.env > Dockerfile
|
||||||
|
|
||||||
|
.PHONY: poetry-install
|
||||||
|
poetry-install: ## Install project dependencies
|
||||||
|
poetry install
|
||||||
|
|
||||||
.PHONY: build-clean
|
.PHONY: build-clean
|
||||||
build-clean:
|
build-clean:
|
||||||
doit clean
|
poetry run doit clean
|
||||||
|
|
||||||
.PHONY: build-macos-intel
|
.PHONY: build-macos-intel
|
||||||
build-macos-intel: build-clean
|
build-macos-intel: build-clean poetry-install ## Build macOS intel package (.dmg)
|
||||||
doit -n 8
|
poetry run doit -n 8
|
||||||
|
|
||||||
.PHONY: build-macos-arm
|
.PHONY: build-macos-arm
|
||||||
build-macos-arm: build-clean
|
build-macos-arm: build-clean poetry-install ## Build macOS Apple Silicon package (.dmg)
|
||||||
doit -n 8 macos_build_dmg
|
poetry run doit -n 8 macos_build_dmg
|
||||||
|
|
||||||
.PHONY: build-linux
|
.PHONY: build-linux
|
||||||
build-linux: build-clean
|
build-linux: build-clean poetry-install ## Build linux packages (.rpm and .deb)
|
||||||
doit -n 8 fedora_rpm debian_deb
|
poetry run doit -n 8 fedora_rpm debian_deb
|
||||||
|
|
||||||
|
.PHONY: regenerate-reference-pdfs
|
||||||
|
regenerate-reference-pdfs: ## Regenerate the reference PDFs
|
||||||
|
pytest tests/test_cli.py -k regenerate --generate-reference-pdfs
|
||||||
# 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 ##
|
||||||
|
|
20
README.md
20
README.md
|
@ -14,13 +14,15 @@ _Read more about Dangerzone in the [official site](https://dangerzone.rocks/abou
|
||||||
|
|
||||||
Follow the instructions for each platform:
|
Follow the instructions for each platform:
|
||||||
|
|
||||||
* [macOS](https://github.com/freedomofpress/dangerzone/blob/v0.8.1/INSTALL.md#macos)
|
* [macOS](https://github.com/freedomofpress/dangerzone/blob/v0.9.0/INSTALL.md#macos)
|
||||||
* [Windows](https://github.com/freedomofpress/dangerzone/blob/v0.8.1//INSTALL.md#windows)
|
* [Windows](https://github.com/freedomofpress/dangerzone/blob/v0.9.0//INSTALL.md#windows)
|
||||||
* [Ubuntu Linux](https://github.com/freedomofpress/dangerzone/blob/v0.8.1/INSTALL.md#ubuntu-debian)
|
* [Ubuntu Linux](https://github.com/freedomofpress/dangerzone/blob/v0.9.0/INSTALL.md#ubuntu-debian)
|
||||||
* [Debian Linux](https://github.com/freedomofpress/dangerzone/blob/v0.8.1/INSTALL.md#ubuntu-debian)
|
* [Debian Linux](https://github.com/freedomofpress/dangerzone/blob/v0.9.0/INSTALL.md#ubuntu-debian)
|
||||||
* [Fedora Linux](https://github.com/freedomofpress/dangerzone/blob/v0.8.1/INSTALL.md#fedora)
|
* [Fedora Linux](https://github.com/freedomofpress/dangerzone/blob/v0.9.0/INSTALL.md#fedora)
|
||||||
* [Qubes OS (beta)](https://github.com/freedomofpress/dangerzone/blob/v0.8.0/INSTALL.md#qubes-os)
|
* [Qubes OS (beta)](https://github.com/freedomofpress/dangerzone/blob/v0.9.0/INSTALL.md#qubes-os)
|
||||||
* [Tails](https://github.com/freedomofpress/dangerzone/blob/v0.8.1/INSTALL.md#tails)
|
* [Tails](https://github.com/freedomofpress/dangerzone/blob/v0.9.0/INSTALL.md#tails)
|
||||||
|
|
||||||
|
You can read more about our operating system support [here](https://github.com/freedomofpress/dangerzone/blob/v0.9.0/INSTALL.md#operating-system-support).
|
||||||
|
|
||||||
## Some features
|
## Some features
|
||||||
|
|
||||||
|
@ -80,3 +82,7 @@ Dangerzone gets updates to improve its features _and_ to fix problems. So, updat
|
||||||
1. Check which version of Dangerzone you are currently using: run Dangerzone, then look for a series of numbers to the right of the logo within the app. The format of the numbers will look similar to `0.4.1`
|
1. Check which version of Dangerzone you are currently using: run Dangerzone, then look for a series of numbers to the right of the logo within the app. The format of the numbers will look similar to `0.4.1`
|
||||||
2. Now find the latest available version of Dangerzone: go to the [download page](https://dangerzone.rocks/#downloads). Look for the version number displayed. The number will be using the same format as in Step 1.
|
2. Now find the latest available version of Dangerzone: go to the [download page](https://dangerzone.rocks/#downloads). Look for the version number displayed. The number will be using the same format as in Step 1.
|
||||||
3. Is the version on the Dangerzone download page higher than the version of your installed app? Go ahead and update.
|
3. Is the version on the Dangerzone download page higher than the version of your installed app? Go ahead and update.
|
||||||
|
|
||||||
|
### Can I use Podman Desktop?
|
||||||
|
|
||||||
|
Yes! We've introduced [experimental support for Podman Desktop](https://github.com/freedomofpress/dangerzone/blob/main/docs/podman-desktop.md) on Windows and macOS.
|
||||||
|
|
20
RELEASE.md
20
RELEASE.md
|
@ -10,15 +10,18 @@ Here is a list of tasks that should be done before issuing the release:
|
||||||
You can generate its content with the the `poetry run ./dev_scripts/generate-release-tasks.py` command.
|
You can generate its content with the the `poetry run ./dev_scripts/generate-release-tasks.py` command.
|
||||||
- [ ] [Add new Linux platforms and remove obsolete ones](https://github.com/freedomofpress/dangerzone/blob/main/RELEASE.md#add-new-linux-platforms-and-remove-obsolete-ones)
|
- [ ] [Add new Linux platforms and remove obsolete ones](https://github.com/freedomofpress/dangerzone/blob/main/RELEASE.md#add-new-linux-platforms-and-remove-obsolete-ones)
|
||||||
- [ ] Bump the Python dependencies using `poetry lock`
|
- [ ] Bump the Python dependencies using `poetry lock`
|
||||||
|
- [ ] Check for new [WiX releases](https://github.com/wixtoolset/wix/releases) and update it if needed
|
||||||
- [ ] Update `version` in `pyproject.toml`
|
- [ ] Update `version` in `pyproject.toml`
|
||||||
- [ ] Update `share/version.txt`
|
- [ ] Update `share/version.txt`
|
||||||
- [ ] Update the "Version" field in `install/linux/dangerzone.spec`
|
- [ ] Update the "Version" field in `install/linux/dangerzone.spec`
|
||||||
- [ ] Bump the Debian version by adding a new changelog entry in `debian/changelog`
|
- [ ] Bump the Debian version by adding a new changelog entry in `debian/changelog`
|
||||||
- [ ] [Bump the minimum Docker Desktop versions](https://github.com/freedomofpress/dangerzone/blob/main/RELEASE.md#bump-the-minimum-docker-desktop-version) in `isolation_provider/container.py`
|
- [ ] [Bump the minimum Docker Desktop versions](https://github.com/freedomofpress/dangerzone/blob/main/RELEASE.md#bump-the-minimum-docker-desktop-version) in `isolation_provider/container.py`
|
||||||
- [ ] Bump the dates and versions in the `Dockerfile`
|
- [ ] Bump the dates and versions in the `Dockerfile`
|
||||||
|
- [ ] Update the download links in our `INSTALL.md` page to point to the new version (the download links will be populated after the release)
|
||||||
- [ ] Update screenshot in `README.md`, if necessary
|
- [ ] Update screenshot in `README.md`, if necessary
|
||||||
- [ ] CHANGELOG.md should be updated to include a list of all major changes since the last release
|
- [ ] CHANGELOG.md should be updated to include a list of all major changes since the last release
|
||||||
- [ ] A draft release should be created. Copy the release notes text from the template at [`docs/templates/release-notes`](https://github.com/freedomofpress/dangerzone/tree/main/docs/templates/)
|
- [ ] A draft release should be created. Copy the release notes text from the template at [`docs/templates/release-notes`](https://github.com/freedomofpress/dangerzone/tree/main/docs/templates/)
|
||||||
|
- [ ] Send the release notes to editorial for review
|
||||||
- [ ] Do the QA tasks
|
- [ ] Do the QA tasks
|
||||||
|
|
||||||
## Add new Linux platforms and remove obsolete ones
|
## Add new Linux platforms and remove obsolete ones
|
||||||
|
@ -121,7 +124,7 @@ Here is what you need to do:
|
||||||
|
|
||||||
# In case of a new Python installation or minor version upgrade, e.g., from
|
# In case of a new Python installation or minor version upgrade, e.g., from
|
||||||
# 3.11 to 3.12, reinstall Poetry
|
# 3.11 to 3.12, reinstall Poetry
|
||||||
python3 -m pip install poetry poetry-plugin-export
|
python3 -m pip install poetry
|
||||||
|
|
||||||
# You can verify the correct Python version is used
|
# You can verify the correct Python version is used
|
||||||
poetry debug info
|
poetry debug info
|
||||||
|
@ -139,7 +142,7 @@ Here is what you need to do:
|
||||||
poetry env remove --all
|
poetry env remove --all
|
||||||
|
|
||||||
# Install the dependencies
|
# Install the dependencies
|
||||||
poetry install --sync
|
poetry sync
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] Build the container image and the OCR language data
|
- [ ] Build the container image and the OCR language data
|
||||||
|
@ -149,7 +152,7 @@ Here is what you need to do:
|
||||||
poetry run ./install/common/download-tessdata.py
|
poetry run ./install/common/download-tessdata.py
|
||||||
|
|
||||||
# Copy the container image to the assets folder
|
# Copy the container image to the assets folder
|
||||||
cp share/container.tar.gz ~dz/release-assets/$VERSION/dangerzone-$VERSION-arm64.tar.gz
|
cp share/container.tar ~dz/release-assets/$VERSION/dangerzone-$VERSION-arm64.tar
|
||||||
cp share/image-id.txt ~dz/release-assets/$VERSION/.
|
cp share/image-id.txt ~dz/release-assets/$VERSION/.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -203,7 +206,7 @@ The Windows release is performed in a Windows 11 virtual machine (as opposed to
|
||||||
```bash
|
```bash
|
||||||
# In case of a new Python installation or minor version upgrade, e.g., from
|
# In case of a new Python installation or minor version upgrade, e.g., from
|
||||||
# 3.11 to 3.12, reinstall Poetry
|
# 3.11 to 3.12, reinstall Poetry
|
||||||
python3 -m pip install poetry poetry-plugin-export
|
python3 -m pip install poetry
|
||||||
|
|
||||||
# You can verify the correct Python version is used
|
# You can verify the correct Python version is used
|
||||||
poetry debug info
|
poetry debug info
|
||||||
|
@ -221,12 +224,12 @@ The Windows release is performed in a Windows 11 virtual machine (as opposed to
|
||||||
poetry env remove --all
|
poetry env remove --all
|
||||||
|
|
||||||
# Install the dependencies
|
# Install the dependencies
|
||||||
poetry install --sync
|
poetry sync
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] Copy the container image into the VM
|
- [ ] Copy the container image into the VM
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> Instead of running `python .\install\windows\build-image.py` in the VM, run the build image script on the host (making sure to build for `linux/amd64`). Copy `share/container.tar.gz` and `share/image-id.txt` from the host into the `share` folder in the VM.
|
> Instead of running `python .\install\windows\build-image.py` in the VM, run the build image script on the host (making sure to build for `linux/amd64`). Copy `share/container.tar` and `share/image-id.txt` from the host into the `share` folder in the VM.
|
||||||
- [ ] Run `poetry run .\install\windows\build-app.bat`
|
- [ ] Run `poetry run .\install\windows\build-app.bat`
|
||||||
- [ ] When you're done you will have `dist\Dangerzone.msi`
|
- [ ] When you're done you will have `dist\Dangerzone.msi`
|
||||||
|
|
||||||
|
@ -317,9 +320,8 @@ To publish the release, you can follow these steps:
|
||||||
|
|
||||||
- [ ] Run container scan on the produced container images (some time may have passed since the artifacts were built)
|
- [ ] Run container scan on the produced container images (some time may have passed since the artifacts were built)
|
||||||
```bash
|
```bash
|
||||||
gunzip --keep -c ./share/container.tar.gz > /tmp/container.tar
|
|
||||||
docker pull anchore/grype:latest
|
docker pull anchore/grype:latest
|
||||||
docker run --rm -v /tmp/container.tar:/container.tar anchore/grype:latest /container.tar
|
docker run --rm -v ./share/container.tar:/container.tar anchore/grype:latest /container.tar
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] Collect the assets in a single directory, calculate their SHA-256 hashes, and sign them.
|
- [ ] Collect the assets in a single directory, calculate their SHA-256 hashes, and sign them.
|
||||||
|
@ -340,7 +342,7 @@ To publish the release, you can follow these steps:
|
||||||
|
|
||||||
- [ ] Update the [Dangerzone website](https://github.com/freedomofpress/dangerzone.rocks) to link to the new installers.
|
- [ ] Update the [Dangerzone website](https://github.com/freedomofpress/dangerzone.rocks) to link to the new installers.
|
||||||
- [ ] Update the brew cask release of Dangerzone with a [PR like this one](https://github.com/Homebrew/homebrew-cask/pull/116319)
|
- [ ] Update the brew cask release of Dangerzone with a [PR like this one](https://github.com/Homebrew/homebrew-cask/pull/116319)
|
||||||
- [ ] Update version and download links in `README.md`
|
- [ ] Update version and links to our installation instructions (`INSTALL.md`) in `README.md`
|
||||||
|
|
||||||
## Post-release
|
## Post-release
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,12 @@ import sys
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Call freeze_support() to avoid passing unknown options to the subprocess.
|
||||||
|
# See https://github.com/freedomofpress/dangerzone/issues/873
|
||||||
|
import multiprocessing
|
||||||
|
|
||||||
|
multiprocessing.freeze_support()
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from . import vendor # type: ignore [attr-defined]
|
from . import vendor # type: ignore [attr-defined]
|
||||||
|
|
|
@ -11,6 +11,7 @@ from .isolation_provider.container import Container
|
||||||
from .isolation_provider.dummy import Dummy
|
from .isolation_provider.dummy import Dummy
|
||||||
from .isolation_provider.qubes import Qubes, is_qubes_native_conversion
|
from .isolation_provider.qubes import Qubes, is_qubes_native_conversion
|
||||||
from .logic import DangerzoneCore
|
from .logic import DangerzoneCore
|
||||||
|
from .settings import Settings
|
||||||
from .util import get_version, replace_control_chars
|
from .util import get_version, replace_control_chars
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,7 +38,7 @@ def print_header(s: str) -> None:
|
||||||
)
|
)
|
||||||
@click.argument(
|
@click.argument(
|
||||||
"filenames",
|
"filenames",
|
||||||
required=True,
|
required=False,
|
||||||
nargs=-1,
|
nargs=-1,
|
||||||
type=click.UNPROCESSED,
|
type=click.UNPROCESSED,
|
||||||
callback=args.validate_input_filenames,
|
callback=args.validate_input_filenames,
|
||||||
|
@ -48,17 +49,43 @@ def print_header(s: str) -> None:
|
||||||
flag_value=True,
|
flag_value=True,
|
||||||
help="Run Dangerzone in debug mode, to get logs from gVisor.",
|
help="Run Dangerzone in debug mode, to get logs from gVisor.",
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--set-container-runtime",
|
||||||
|
required=False,
|
||||||
|
help=(
|
||||||
|
"The name or full path of the container runtime you want Dangerzone to use."
|
||||||
|
" You can specify the value 'default' if you want to take back your choice, and"
|
||||||
|
" let Dangerzone use the default runtime for this OS"
|
||||||
|
),
|
||||||
|
)
|
||||||
@click.version_option(version=get_version(), message="%(version)s")
|
@click.version_option(version=get_version(), message="%(version)s")
|
||||||
@errors.handle_document_errors
|
@errors.handle_document_errors
|
||||||
def cli_main(
|
def cli_main(
|
||||||
output_filename: Optional[str],
|
output_filename: Optional[str],
|
||||||
ocr_lang: Optional[str],
|
ocr_lang: Optional[str],
|
||||||
filenames: List[str],
|
filenames: Optional[List[str]],
|
||||||
archive: bool,
|
archive: bool,
|
||||||
dummy_conversion: bool,
|
dummy_conversion: bool,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
|
set_container_runtime: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
setup_logging()
|
setup_logging()
|
||||||
|
display_banner()
|
||||||
|
if set_container_runtime:
|
||||||
|
settings = Settings()
|
||||||
|
if set_container_runtime == "default":
|
||||||
|
settings.unset_custom_runtime()
|
||||||
|
click.echo(
|
||||||
|
"Instructed Dangerzone to use the default container runtime for this OS"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
container_runtime = settings.set_custom_runtime(
|
||||||
|
set_container_runtime, autosave=True
|
||||||
|
)
|
||||||
|
click.echo(f"Set the settings container_runtime to {container_runtime}")
|
||||||
|
sys.exit(0)
|
||||||
|
elif not filenames:
|
||||||
|
raise click.UsageError("Missing argument 'FILENAMES...'")
|
||||||
|
|
||||||
if getattr(sys, "dangerzone_dev", False) and dummy_conversion:
|
if getattr(sys, "dangerzone_dev", False) and dummy_conversion:
|
||||||
dangerzone = DangerzoneCore(Dummy())
|
dangerzone = DangerzoneCore(Dummy())
|
||||||
|
@ -67,7 +94,6 @@ def cli_main(
|
||||||
else:
|
else:
|
||||||
dangerzone = DangerzoneCore(Container(debug=debug))
|
dangerzone = DangerzoneCore(Container(debug=debug))
|
||||||
|
|
||||||
display_banner()
|
|
||||||
if len(filenames) == 1 and output_filename:
|
if len(filenames) == 1 and output_filename:
|
||||||
dangerzone.add_document_from_filename(filenames[0], output_filename, archive)
|
dangerzone.add_document_from_filename(filenames[0], output_filename, archive)
|
||||||
elif len(filenames) > 1 and output_filename:
|
elif len(filenames) > 1 and output_filename:
|
||||||
|
@ -320,4 +346,10 @@ def display_banner() -> None:
|
||||||
+ Style.DIM
|
+ Style.DIM
|
||||||
+ "│"
|
+ "│"
|
||||||
)
|
)
|
||||||
print(Back.BLACK + Fore.YELLOW + Style.DIM + "╰──────────────────────────╯")
|
print(
|
||||||
|
Back.BLACK
|
||||||
|
+ Fore.YELLOW
|
||||||
|
+ Style.DIM
|
||||||
|
+ "╰──────────────────────────╯"
|
||||||
|
+ Style.RESET_ALL
|
||||||
|
)
|
||||||
|
|
|
@ -1,30 +1,69 @@
|
||||||
import gzip
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import platform
|
import platform
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
from typing import IO, Callable, List, Optional, Tuple
|
from typing import IO, Callable, List, Optional, Tuple
|
||||||
|
|
||||||
from . import errors
|
from . import errors
|
||||||
|
from .settings import Settings
|
||||||
from .util import get_resource_path, get_subprocess_startupinfo
|
from .util import get_resource_path, get_subprocess_startupinfo
|
||||||
|
|
||||||
OLD_CONTAINER_NAME = "dangerzone.rocks/dangerzone"
|
OLD_CONTAINER_NAME = "dangerzone.rocks/dangerzone"
|
||||||
CONTAINER_NAME = "ghcr.io/almet/dangerzone/dangerzone" # FIXME: Change this to the correct container name
|
CONTAINER_NAME = (
|
||||||
RUNTIME_NAME = "podman" if platform.system() == "Linux" else "docker"
|
"ghcr.io/almet/dangerzone/dangerzone"
|
||||||
|
) # FIXME: Change this to the correct container name
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Runtime(object):
|
||||||
|
"""Represents the container runtime to use.
|
||||||
|
|
||||||
|
- It can be specified via the settings, using the "container_runtime" key,
|
||||||
|
which should point to the full path of the runtime;
|
||||||
|
- If the runtime is not specified via the settings, it defaults
|
||||||
|
to "podman" on Linux and "docker" on macOS and Windows.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
settings = Settings()
|
||||||
|
|
||||||
|
if settings.custom_runtime_specified():
|
||||||
|
self.path = Path(settings.get("container_runtime"))
|
||||||
|
if not self.path.exists():
|
||||||
|
raise errors.UnsupportedContainerRuntime(self.path)
|
||||||
|
self.name = self.path.stem
|
||||||
|
else:
|
||||||
|
self.name = self.get_default_runtime_name()
|
||||||
|
self.path = Runtime.path_from_name(self.name)
|
||||||
|
|
||||||
|
if self.name not in ("podman", "docker"):
|
||||||
|
raise errors.UnsupportedContainerRuntime(self.name)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def path_from_name(name: str) -> Path:
|
||||||
|
name_path = Path(name)
|
||||||
|
if name_path.is_file():
|
||||||
|
return name_path
|
||||||
|
else:
|
||||||
|
runtime = shutil.which(name_path)
|
||||||
|
if runtime is None:
|
||||||
|
raise errors.NoContainerTechException(name)
|
||||||
|
return Path(runtime)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_default_runtime_name() -> str:
|
||||||
|
return "podman" if platform.system() == "Linux" else "docker"
|
||||||
|
|
||||||
|
|
||||||
def subprocess_run(*args, **kwargs) -> subprocess.CompletedProcess:
|
def subprocess_run(*args, **kwargs) -> subprocess.CompletedProcess:
|
||||||
"""subprocess.run with the correct startupinfo for Windows."""
|
"""subprocess.run with the correct startupinfo for Windows."""
|
||||||
return subprocess.run(*args, startupinfo=get_subprocess_startupinfo(), **kwargs)
|
return subprocess.run(*args, startupinfo=get_subprocess_startupinfo(), **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def get_runtime_name() -> str:
|
def get_runtime_version(runtime: Optional[Runtime] = None) -> Tuple[int, int]:
|
||||||
return RUNTIME_NAME
|
|
||||||
|
|
||||||
|
|
||||||
def get_runtime_version() -> Tuple[int, int]:
|
|
||||||
"""Get the major/minor parts of the Docker/Podman version.
|
"""Get the major/minor parts of the Docker/Podman version.
|
||||||
|
|
||||||
Some of the operations we perform in this module rely on some Podman features
|
Some of the operations we perform in this module rely on some Podman features
|
||||||
|
@ -33,14 +72,15 @@ def get_runtime_version() -> Tuple[int, int]:
|
||||||
just knowing the major and minor version, since writing/installing a full-blown
|
just knowing the major and minor version, since writing/installing a full-blown
|
||||||
semver parser is an overkill.
|
semver parser is an overkill.
|
||||||
"""
|
"""
|
||||||
|
runtime = runtime or Runtime()
|
||||||
|
|
||||||
# Get the Docker/Podman version, using a Go template.
|
# Get the Docker/Podman version, using a Go template.
|
||||||
runtime = get_runtime_name()
|
if runtime.name == "podman":
|
||||||
if runtime == "podman":
|
|
||||||
query = "{{.Client.Version}}"
|
query = "{{.Client.Version}}"
|
||||||
else:
|
else:
|
||||||
query = "{{.Server.Version}}"
|
query = "{{.Server.Version}}"
|
||||||
|
|
||||||
cmd = [runtime, "version", "-f", query]
|
cmd = [str(runtime.path), "version", "-f", query]
|
||||||
try:
|
try:
|
||||||
version = subprocess_run(
|
version = subprocess_run(
|
||||||
cmd,
|
cmd,
|
||||||
|
@ -48,7 +88,7 @@ def get_runtime_version() -> Tuple[int, int]:
|
||||||
check=True,
|
check=True,
|
||||||
).stdout.decode()
|
).stdout.decode()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
msg = f"Could not get the version of the {runtime.capitalize()} tool: {e}"
|
msg = f"Could not get the version of the {runtime.name.capitalize()} tool: {e}"
|
||||||
raise RuntimeError(msg) from e
|
raise RuntimeError(msg) from e
|
||||||
|
|
||||||
# Parse this version and return the major/minor parts, since we don't need the
|
# Parse this version and return the major/minor parts, since we don't need the
|
||||||
|
@ -58,20 +98,12 @@ def get_runtime_version() -> Tuple[int, int]:
|
||||||
return (int(major), int(minor))
|
return (int(major), int(minor))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
msg = (
|
msg = (
|
||||||
f"Could not parse the version of the {runtime.capitalize()} tool"
|
f"Could not parse the version of the {runtime.name.capitalize()} tool"
|
||||||
f" (found: '{version}') due to the following error: {e}"
|
f" (found: '{version}') due to the following error: {e}"
|
||||||
)
|
)
|
||||||
raise RuntimeError(msg)
|
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]:
|
def list_image_tags() -> List[str]:
|
||||||
"""Get the tags of all loaded Dangerzone images.
|
"""Get the tags of all loaded Dangerzone images.
|
||||||
|
|
||||||
|
@ -79,10 +111,11 @@ def list_image_tags() -> List[str]:
|
||||||
images. This can be useful when we want to find which are the local image tags,
|
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.
|
and which image ID does the "latest" tag point to.
|
||||||
"""
|
"""
|
||||||
|
runtime = Runtime()
|
||||||
return (
|
return (
|
||||||
subprocess.check_output(
|
subprocess.check_output(
|
||||||
[
|
[
|
||||||
get_runtime(),
|
str(runtime.path),
|
||||||
"image",
|
"image",
|
||||||
"list",
|
"list",
|
||||||
"--format",
|
"--format",
|
||||||
|
@ -97,65 +130,83 @@ def list_image_tags() -> List[str]:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_image_tag(image_id: str, new_tag: str) -> None:
|
||||||
|
"""Add a tag to the Dangerzone image."""
|
||||||
|
runtime = Runtime()
|
||||||
|
log.debug(f"Adding tag '{new_tag}' to image '{image_id}'")
|
||||||
|
subprocess.check_output(
|
||||||
|
[str(runtime.path), "tag", image_id, new_tag],
|
||||||
|
startupinfo=get_subprocess_startupinfo(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def delete_image_tag(tag: str) -> None:
|
def delete_image_tag(tag: str) -> None:
|
||||||
"""Delete a Dangerzone image tag."""
|
"""Delete a Dangerzone image tag."""
|
||||||
name = CONTAINER_NAME + ":" + tag
|
runtime = Runtime()
|
||||||
log.warning(f"Deleting old container image: {name}")
|
log.warning(f"Deleting old container image: {tag}")
|
||||||
try:
|
try:
|
||||||
subprocess.check_output(
|
subprocess.check_output(
|
||||||
[get_runtime(), "rmi", "--force", name],
|
[str(runtime.name), "rmi", "--force", tag],
|
||||||
startupinfo=get_subprocess_startupinfo(),
|
startupinfo=get_subprocess_startupinfo(),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning(
|
log.warning(
|
||||||
f"Couldn't delete old container image '{name}', so leaving it there."
|
f"Couldn't delete old container image '{tag}', so leaving it there."
|
||||||
f" Original error: {e}"
|
f" Original error: {e}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def load_image_tarball_from_gzip() -> None:
|
def load_image_tarball() -> None:
|
||||||
|
runtime = Runtime()
|
||||||
log.info("Installing Dangerzone container image...")
|
log.info("Installing Dangerzone container image...")
|
||||||
p = subprocess.Popen(
|
tarball_path = get_resource_path("container.tar")
|
||||||
[get_runtime(), "load"],
|
try:
|
||||||
stdin=subprocess.PIPE,
|
res = subprocess.run(
|
||||||
startupinfo=get_subprocess_startupinfo(),
|
[str(runtime.path), "load", "-i", str(tarball_path)],
|
||||||
)
|
startupinfo=get_subprocess_startupinfo(),
|
||||||
|
capture_output=True,
|
||||||
chunk_size = 4 << 20
|
check=True,
|
||||||
compressed_container_path = get_resource_path("container.tar.gz")
|
)
|
||||||
with gzip.open(compressed_container_path) as f:
|
except subprocess.CalledProcessError as e:
|
||||||
while True:
|
if e.stderr:
|
||||||
chunk = f.read(chunk_size)
|
error = e.stderr.decode()
|
||||||
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:
|
else:
|
||||||
error = "No output"
|
error = "No output"
|
||||||
raise errors.ImageInstallationException(
|
raise errors.ImageInstallationException(
|
||||||
f"Could not install container image: {error}"
|
f"Could not install container image: {error}"
|
||||||
)
|
)
|
||||||
|
|
||||||
log.info("Successfully installed container image from")
|
# Loading an image built with Buildkit in Podman 3.4 messes up its name. The tag
|
||||||
|
# somehow becomes the name of the loaded image [1].
|
||||||
|
#
|
||||||
|
# We know that older Podman versions are not generally affected, since Podman v3.0.1
|
||||||
|
# on Debian Bullseye works properly. Also, Podman v4.0 is not affected, so it makes
|
||||||
|
# sense to target only Podman v3.4 for a fix.
|
||||||
|
#
|
||||||
|
# The fix is simple, tag the image properly based on the expected tag from
|
||||||
|
# `share/image-id.txt` and delete the incorrect tag.
|
||||||
|
#
|
||||||
|
# [1] https://github.com/containers/podman/issues/16490
|
||||||
|
if runtime.name == "podman" and get_runtime_version(runtime) == (3, 4):
|
||||||
|
expected_tag = get_expected_tag()
|
||||||
|
bad_tag = f"localhost/{expected_tag}:latest"
|
||||||
|
good_tag = f"{CONTAINER_NAME}:{expected_tag}"
|
||||||
|
|
||||||
|
log.debug(
|
||||||
def load_image_tarball_from_tar(tarball_path: str) -> None:
|
f"Dangerzone images loaded in Podman v3.4 usually have an invalid tag."
|
||||||
cmd = [get_runtime(), "load", "-i", tarball_path]
|
" Fixing it..."
|
||||||
subprocess_run(cmd, check=True)
|
)
|
||||||
log.info("Successfully installed container image from %s", tarball_path)
|
add_image_tag(bad_tag, good_tag)
|
||||||
|
delete_image_tag(bad_tag)
|
||||||
|
|
||||||
|
|
||||||
def tag_image_by_digest(digest: str, tag: str) -> None:
|
def tag_image_by_digest(digest: str, tag: str) -> None:
|
||||||
"""Tag a container image by digest.
|
"""Tag a container image by digest.
|
||||||
The sha256: prefix should be omitted from the digest.
|
The sha256: prefix should be omitted from the digest.
|
||||||
"""
|
"""
|
||||||
|
runtime = Runtime()
|
||||||
image_id = get_image_id_by_digest(digest)
|
image_id = get_image_id_by_digest(digest)
|
||||||
cmd = [get_runtime(), "tag", image_id, tag]
|
cmd = [str(runtime.path), "tag", image_id, tag]
|
||||||
log.debug(" ".join(cmd))
|
log.debug(" ".join(cmd))
|
||||||
subprocess_run(cmd, check=True)
|
subprocess_run(cmd, check=True)
|
||||||
|
|
||||||
|
@ -164,8 +215,9 @@ def get_image_id_by_digest(digest: str) -> str:
|
||||||
"""Get an image ID from a digest.
|
"""Get an image ID from a digest.
|
||||||
The sha256: prefix should be omitted from the digest.
|
The sha256: prefix should be omitted from the digest.
|
||||||
"""
|
"""
|
||||||
|
runtime = Runtime()
|
||||||
cmd = [
|
cmd = [
|
||||||
get_runtime(),
|
str(runtime.path),
|
||||||
"images",
|
"images",
|
||||||
"-f",
|
"-f",
|
||||||
f"digest=sha256:{digest}",
|
f"digest=sha256:{digest}",
|
||||||
|
@ -180,7 +232,8 @@ def get_image_id_by_digest(digest: str) -> str:
|
||||||
|
|
||||||
def container_pull(image: str, manifest_digest: str, callback: Callable):
|
def container_pull(image: str, manifest_digest: str, callback: Callable):
|
||||||
"""Pull a container image from a registry."""
|
"""Pull a container image from a registry."""
|
||||||
cmd = [get_runtime_name(), "pull", f"{image}@sha256:{manifest_digest}"]
|
runtime = Runtime()
|
||||||
|
cmd = [str(runtime.path), "pull", f"{image}@sha256:{manifest_digest}"]
|
||||||
process = subprocess.Popen(
|
process = subprocess.Popen(
|
||||||
cmd,
|
cmd,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
|
@ -206,7 +259,8 @@ def get_local_image_digest(image: str) -> str:
|
||||||
# Get the image hash from the "podman images" command.
|
# Get the image hash from the "podman images" command.
|
||||||
# It's not possible to use "podman inspect" here as it
|
# It's not possible to use "podman inspect" here as it
|
||||||
# returns the digest of the architecture-bound image
|
# returns the digest of the architecture-bound image
|
||||||
cmd = [get_runtime_name(), "images", image, "--format", "{{.Digest}}"]
|
runtime = Runtime()
|
||||||
|
cmd = [str(runtime.path), "images", image, "--format", "{{.Digest}}"]
|
||||||
log.debug(" ".join(cmd))
|
log.debug(" ".join(cmd))
|
||||||
try:
|
try:
|
||||||
result = subprocess_run(
|
result = subprocess_run(
|
||||||
|
|
|
@ -150,5 +150,9 @@ class NotAvailableContainerTechException(ContainerException):
|
||||||
super().__init__(f"{container_tech} is not available")
|
super().__init__(f"{container_tech} is not available")
|
||||||
|
|
||||||
|
|
||||||
|
class UnsupportedContainerRuntime(ContainerException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ContainerPullException(ContainerException):
|
class ContainerPullException(ContainerException):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -53,7 +53,7 @@ class Application(QtWidgets.QApplication):
|
||||||
def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
|
def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
|
||||||
super(Application, self).__init__(*args, **kwargs)
|
super(Application, self).__init__(*args, **kwargs)
|
||||||
self.setQuitOnLastWindowClosed(False)
|
self.setQuitOnLastWindowClosed(False)
|
||||||
with open(get_resource_path("dangerzone.css"), "r") as f:
|
with get_resource_path("dangerzone.css").open("r") as f:
|
||||||
style = f.read()
|
style = f.read()
|
||||||
self.setStyleSheet(style)
|
self.setStyleSheet(style)
|
||||||
|
|
||||||
|
|
|
@ -63,7 +63,7 @@ class DangerzoneGui(DangerzoneCore):
|
||||||
path = get_resource_path("dangerzone.ico")
|
path = get_resource_path("dangerzone.ico")
|
||||||
else:
|
else:
|
||||||
path = get_resource_path("icon.png")
|
path = get_resource_path("icon.png")
|
||||||
return QtGui.QIcon(path)
|
return QtGui.QIcon(str(path))
|
||||||
|
|
||||||
def open_pdf_viewer(self, filename: str) -> None:
|
def open_pdf_viewer(self, filename: str) -> None:
|
||||||
if platform.system() == "Darwin":
|
if platform.system() == "Darwin":
|
||||||
|
@ -252,7 +252,7 @@ class Alert(Dialog):
|
||||||
def create_layout(self) -> QtWidgets.QBoxLayout:
|
def create_layout(self) -> QtWidgets.QBoxLayout:
|
||||||
logo = QtWidgets.QLabel()
|
logo = QtWidgets.QLabel()
|
||||||
logo.setPixmap(
|
logo.setPixmap(
|
||||||
QtGui.QPixmap.fromImage(QtGui.QImage(get_resource_path("icon.png")))
|
QtGui.QPixmap.fromImage(QtGui.QImage(str(get_resource_path("icon.png"))))
|
||||||
)
|
)
|
||||||
|
|
||||||
label = QtWidgets.QLabel()
|
label = QtWidgets.QLabel()
|
||||||
|
|
|
@ -58,20 +58,13 @@ about updates.</p>
|
||||||
HAMBURGER_MENU_SIZE = 30
|
HAMBURGER_MENU_SIZE = 30
|
||||||
|
|
||||||
|
|
||||||
WARNING_MESSAGE = """\
|
|
||||||
<p><b>Warning:</b> Ubuntu Focal systems and their derivatives will
|
|
||||||
stop being supported in subsequent Dangerzone releases. We encourage you to upgrade to a
|
|
||||||
more recent version of your operating system in order to get security updates.</p>
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def load_svg_image(filename: str, width: int, height: int) -> QtGui.QPixmap:
|
def load_svg_image(filename: str, width: int, height: int) -> QtGui.QPixmap:
|
||||||
"""Load an SVG image from a filename.
|
"""Load an SVG image from a filename.
|
||||||
|
|
||||||
This answer is basically taken from: https://stackoverflow.com/a/25689790
|
This answer is basically taken from: https://stackoverflow.com/a/25689790
|
||||||
"""
|
"""
|
||||||
path = get_resource_path(filename)
|
path = get_resource_path(filename)
|
||||||
svg_renderer = QtSvg.QSvgRenderer(path)
|
svg_renderer = QtSvg.QSvgRenderer(str(path))
|
||||||
image = QtGui.QImage(width, height, QtGui.QImage.Format_ARGB32)
|
image = QtGui.QImage(width, height, QtGui.QImage.Format_ARGB32)
|
||||||
# Set the ARGB to 0 to prevent rendering artifacts
|
# Set the ARGB to 0 to prevent rendering artifacts
|
||||||
image.fill(0x00000000)
|
image.fill(0x00000000)
|
||||||
|
@ -139,9 +132,8 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
|
|
||||||
# Header
|
# Header
|
||||||
logo = QtWidgets.QLabel()
|
logo = QtWidgets.QLabel()
|
||||||
logo.setPixmap(
|
icon_path = str(get_resource_path("icon.png"))
|
||||||
QtGui.QPixmap.fromImage(QtGui.QImage(get_resource_path("icon.png")))
|
logo.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(icon_path)))
|
||||||
)
|
|
||||||
header_label = QtWidgets.QLabel("Dangerzone")
|
header_label = QtWidgets.QLabel("Dangerzone")
|
||||||
header_label.setFont(self.dangerzone.fixed_font)
|
header_label.setFont(self.dangerzone.fixed_font)
|
||||||
header_label.setStyleSheet("QLabel { font-weight: bold; font-size: 50px; }")
|
header_label.setStyleSheet("QLabel { font-weight: bold; font-size: 50px; }")
|
||||||
|
@ -195,6 +187,9 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
header_layout.addWidget(self.hamburger_button)
|
header_layout.addWidget(self.hamburger_button)
|
||||||
header_layout.addSpacing(15)
|
header_layout.addSpacing(15)
|
||||||
|
|
||||||
|
# Content widget, contains all the window content except waiting widget
|
||||||
|
self.content_widget = ContentWidget(self.dangerzone)
|
||||||
|
|
||||||
if self.dangerzone.isolation_provider.should_wait_install():
|
if self.dangerzone.isolation_provider.should_wait_install():
|
||||||
# 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)
|
||||||
|
@ -204,9 +199,6 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
self.waiting_widget = WaitingWidget()
|
self.waiting_widget = WaitingWidget()
|
||||||
self.dangerzone.is_waiting_finished = True
|
self.dangerzone.is_waiting_finished = True
|
||||||
|
|
||||||
# Content widget, contains all the window content except waiting widget
|
|
||||||
self.content_widget = ContentWidget(self.dangerzone)
|
|
||||||
|
|
||||||
# Only use the waiting widget if container runtime isn't available
|
# Only use the waiting widget if container runtime isn't available
|
||||||
if self.dangerzone.is_waiting_finished:
|
if self.dangerzone.is_waiting_finished:
|
||||||
self.waiting_widget.hide()
|
self.waiting_widget.hide()
|
||||||
|
@ -231,11 +223,16 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
self.setProperty("OSColorMode", self.dangerzone.app.os_color_mode.value)
|
self.setProperty("OSColorMode", self.dangerzone.app.os_color_mode.value)
|
||||||
|
|
||||||
if hasattr(self.dangerzone.isolation_provider, "check_docker_desktop_version"):
|
if hasattr(self.dangerzone.isolation_provider, "check_docker_desktop_version"):
|
||||||
is_version_valid, version = (
|
try:
|
||||||
self.dangerzone.isolation_provider.check_docker_desktop_version()
|
is_version_valid, version = (
|
||||||
)
|
self.dangerzone.isolation_provider.check_docker_desktop_version()
|
||||||
if not is_version_valid:
|
)
|
||||||
self.handle_docker_desktop_version_check(is_version_valid, version)
|
if not is_version_valid:
|
||||||
|
self.handle_docker_desktop_version_check(is_version_valid, version)
|
||||||
|
except errors.UnsupportedContainerRuntime as e:
|
||||||
|
pass # It's caught later in the flow.
|
||||||
|
except errors.NoContainerTechException as e:
|
||||||
|
pass # It's caught later in the flow.
|
||||||
|
|
||||||
self.show()
|
self.show()
|
||||||
|
|
||||||
|
@ -599,8 +596,15 @@ class WaitingWidgetContainer(WaitingWidget):
|
||||||
self.finished.emit()
|
self.finished.emit()
|
||||||
|
|
||||||
def state_change(self, state: str, error: Optional[str] = None) -> None:
|
def state_change(self, state: str, error: Optional[str] = None) -> None:
|
||||||
|
custom_runtime = self.dangerzone.settings.custom_runtime_specified()
|
||||||
|
|
||||||
if state == "not_installed":
|
if state == "not_installed":
|
||||||
if platform.system() == "Linux":
|
if custom_runtime:
|
||||||
|
self.show_error(
|
||||||
|
"<strong>We could not find the container runtime defined in your settings</strong><br><br>"
|
||||||
|
"Please check your settings, install it if needed, and retry."
|
||||||
|
)
|
||||||
|
elif platform.system() == "Linux":
|
||||||
self.show_error(
|
self.show_error(
|
||||||
"<strong>Dangerzone requires Podman</strong><br><br>"
|
"<strong>Dangerzone requires Podman</strong><br><br>"
|
||||||
"Install it and retry."
|
"Install it and retry."
|
||||||
|
@ -613,19 +617,25 @@ class WaitingWidgetContainer(WaitingWidget):
|
||||||
)
|
)
|
||||||
|
|
||||||
elif state == "not_running":
|
elif state == "not_running":
|
||||||
if platform.system() == "Linux":
|
if custom_runtime:
|
||||||
|
self.show_error(
|
||||||
|
"<strong>We were unable to start the container runtime defined in your settings</strong><br><br>"
|
||||||
|
"Please check your settings, install it if needed, and retry."
|
||||||
|
)
|
||||||
|
elif platform.system() == "Linux":
|
||||||
# "not_running" here means that the `podman image ls` command failed.
|
# "not_running" here means that the `podman image ls` command failed.
|
||||||
message = (
|
self.show_error(
|
||||||
"<strong>Dangerzone requires Podman</strong><br><br>"
|
"<strong>Dangerzone requires Podman</strong><br><br>"
|
||||||
"Podman is installed but cannot run properly. See errors below"
|
"Podman is installed but cannot run properly. See errors below",
|
||||||
|
error,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
message = (
|
self.show_error(
|
||||||
"<strong>Dangerzone requires Docker Desktop</strong><br><br>"
|
"<strong>Dangerzone requires Docker Desktop</strong><br><br>"
|
||||||
"Docker is installed but isn't running.<br><br>"
|
"Docker is installed but isn't running.<br><br>"
|
||||||
"Open Docker and make sure it's running in the background."
|
"Open Docker and make sure it's running in the background.",
|
||||||
|
error,
|
||||||
)
|
)
|
||||||
self.show_error(message, error)
|
|
||||||
else:
|
else:
|
||||||
self.show_message(
|
self.show_message(
|
||||||
"Installing the Dangerzone container image.<br><br>"
|
"Installing the Dangerzone container image.<br><br>"
|
||||||
|
@ -650,17 +660,6 @@ class ContentWidget(QtWidgets.QWidget):
|
||||||
self.dangerzone = dangerzone
|
self.dangerzone = dangerzone
|
||||||
self.conversion_started = False
|
self.conversion_started = False
|
||||||
|
|
||||||
self.warning_label = None
|
|
||||||
if platform.system() == "Linux":
|
|
||||||
# Add the warning message only for ubuntu focal
|
|
||||||
os_release_path = Path("/etc/os-release")
|
|
||||||
if os_release_path.exists():
|
|
||||||
os_release = os_release_path.read_text()
|
|
||||||
if "Ubuntu 20.04" in os_release or "focal" in os_release:
|
|
||||||
self.warning_label = QtWidgets.QLabel(WARNING_MESSAGE)
|
|
||||||
self.warning_label.setWordWrap(True)
|
|
||||||
self.warning_label.setProperty("style", "warning")
|
|
||||||
|
|
||||||
# Doc selection widget
|
# Doc selection widget
|
||||||
self.doc_selection_widget = DocSelectionWidget(self.dangerzone)
|
self.doc_selection_widget = DocSelectionWidget(self.dangerzone)
|
||||||
self.doc_selection_widget.documents_selected.connect(self.documents_selected)
|
self.doc_selection_widget.documents_selected.connect(self.documents_selected)
|
||||||
|
@ -686,8 +685,6 @@ class ContentWidget(QtWidgets.QWidget):
|
||||||
|
|
||||||
# Layout
|
# Layout
|
||||||
layout = QtWidgets.QVBoxLayout()
|
layout = QtWidgets.QVBoxLayout()
|
||||||
if self.warning_label:
|
|
||||||
layout.addWidget(self.warning_label) # Add warning at the top
|
|
||||||
layout.addWidget(self.settings_widget, stretch=1)
|
layout.addWidget(self.settings_widget, stretch=1)
|
||||||
layout.addWidget(self.documents_list, stretch=1)
|
layout.addWidget(self.documents_list, stretch=1)
|
||||||
layout.addWidget(self.doc_selection_wrapper, stretch=1)
|
layout.addWidget(self.doc_selection_wrapper, stretch=1)
|
||||||
|
@ -918,22 +915,16 @@ class SettingsWidget(QtWidgets.QWidget):
|
||||||
self.safe_extension_name_layout.setSpacing(0)
|
self.safe_extension_name_layout.setSpacing(0)
|
||||||
self.safe_extension_name_layout.addWidget(self.safe_extension_filename)
|
self.safe_extension_name_layout.addWidget(self.safe_extension_filename)
|
||||||
self.safe_extension_name_layout.addWidget(self.safe_extension)
|
self.safe_extension_name_layout.addWidget(self.safe_extension)
|
||||||
# FIXME: Workaround for https://github.com/freedomofpress/dangerzone/issues/339.
|
self.dot_pdf_validator = QtGui.QRegularExpressionValidator(
|
||||||
# We should drop this once we drop Ubuntu Focal support.
|
QtCore.QRegularExpression(r".*\.[Pp][Dd][Ff]")
|
||||||
if hasattr(QtGui, "QRegularExpressionValidator"):
|
)
|
||||||
QRegEx = QtCore.QRegularExpression
|
|
||||||
QRegExValidator = QtGui.QRegularExpressionValidator
|
|
||||||
else:
|
|
||||||
QRegEx = QtCore.QRegExp # type: ignore [assignment]
|
|
||||||
QRegExValidator = QtGui.QRegExpValidator # type: ignore [assignment]
|
|
||||||
self.dot_pdf_validator = QRegExValidator(QRegEx(r".*\.[Pp][Dd][Ff]"))
|
|
||||||
if platform.system() == "Linux":
|
if platform.system() == "Linux":
|
||||||
illegal_chars_regex = r"[/]"
|
illegal_chars_regex = r"[/]"
|
||||||
elif platform.system() == "Darwin":
|
elif platform.system() == "Darwin":
|
||||||
illegal_chars_regex = r"[\\]"
|
illegal_chars_regex = r"[\\]"
|
||||||
else:
|
else:
|
||||||
illegal_chars_regex = r"[\"*/:<>?\\|]"
|
illegal_chars_regex = r"[\"*/:<>?\\|]"
|
||||||
self.illegal_chars_regex = QRegEx(illegal_chars_regex)
|
self.illegal_chars_regex = QtCore.QRegularExpression(illegal_chars_regex)
|
||||||
self.safe_extension_layout = QtWidgets.QHBoxLayout()
|
self.safe_extension_layout = QtWidgets.QHBoxLayout()
|
||||||
self.safe_extension_layout.addWidget(self.save_checkbox)
|
self.safe_extension_layout.addWidget(self.save_checkbox)
|
||||||
self.safe_extension_layout.addWidget(self.safe_extension_label)
|
self.safe_extension_layout.addWidget(self.safe_extension_label)
|
||||||
|
@ -1352,7 +1343,7 @@ class DocumentWidget(QtWidgets.QWidget):
|
||||||
|
|
||||||
def load_status_image(self, filename: str) -> QtGui.QPixmap:
|
def load_status_image(self, filename: str) -> QtGui.QPixmap:
|
||||||
path = get_resource_path(filename)
|
path = get_resource_path(filename)
|
||||||
img = QtGui.QImage(path)
|
img = QtGui.QImage(str(path))
|
||||||
image = QtGui.QPixmap.fromImage(img)
|
image = QtGui.QPixmap.fromImage(img)
|
||||||
return image.scaled(QtCore.QSize(15, 15))
|
return image.scaled(QtCore.QSize(15, 15))
|
||||||
|
|
||||||
|
|
|
@ -6,14 +6,15 @@ import subprocess
|
||||||
from typing import Callable, List, Tuple
|
from typing import Callable, List, Tuple
|
||||||
|
|
||||||
from .. import container_utils, errors, updater
|
from .. import container_utils, errors, updater
|
||||||
|
from ..container_utils import Runtime
|
||||||
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
|
||||||
|
|
||||||
TIMEOUT_KILL = 5 # Timeout in seconds until the kill command returns.
|
TIMEOUT_KILL = 5 # Timeout in seconds until the kill command returns.
|
||||||
MINIMUM_DOCKER_DESKTOP = {
|
MINIMUM_DOCKER_DESKTOP = {
|
||||||
"Darwin": "4.36.0",
|
"Darwin": "4.40.0",
|
||||||
"Windows": "4.36.0",
|
"Windows": "4.40.0",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Define startupinfo for subprocesses
|
# Define startupinfo for subprocesses
|
||||||
|
@ -50,11 +51,19 @@ class Container(IsolationProvider):
|
||||||
* 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)
|
||||||
"""
|
"""
|
||||||
if container_utils.get_runtime_name() == "podman":
|
runtime = Runtime()
|
||||||
|
if 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):
|
if container_utils.get_runtime_version() >= (4, 1):
|
||||||
security_args += ["--userns", "nomap"]
|
# We perform a platform check to avoid the following Podman Desktop
|
||||||
|
# error on Windows:
|
||||||
|
#
|
||||||
|
# Error: nomap is only supported in rootless mode
|
||||||
|
#
|
||||||
|
# See also: https://github.com/freedomofpress/dangerzone/issues/1127
|
||||||
|
if platform.system() != "Windows":
|
||||||
|
security_args += ["--userns", "nomap"]
|
||||||
else:
|
else:
|
||||||
security_args = ["--security-opt=no-new-privileges:true"]
|
security_args = ["--security-opt=no-new-privileges:true"]
|
||||||
|
|
||||||
|
@ -64,8 +73,16 @@ class Container(IsolationProvider):
|
||||||
#
|
#
|
||||||
# [1] https://github.com/freedomofpress/dangerzone/issues/846
|
# [1] https://github.com/freedomofpress/dangerzone/issues/846
|
||||||
# [2] https://github.com/containers/common/blob/d3283f8401eeeb21f3c59a425b5461f069e199a7/pkg/seccomp/seccomp.json
|
# [2] https://github.com/containers/common/blob/d3283f8401eeeb21f3c59a425b5461f069e199a7/pkg/seccomp/seccomp.json
|
||||||
seccomp_json_path = get_resource_path("seccomp.gvisor.json")
|
seccomp_json_path = str(get_resource_path("seccomp.gvisor.json"))
|
||||||
security_args += ["--security-opt", f"seccomp={seccomp_json_path}"]
|
# We perform a platform check to avoid the following Podman Desktop
|
||||||
|
# error on Windows:
|
||||||
|
#
|
||||||
|
# Error: opening seccomp profile failed: open
|
||||||
|
# C:\[...]\dangerzone\share\seccomp.gvisor.json: no such file or directory
|
||||||
|
#
|
||||||
|
# See also: https://github.com/freedomofpress/dangerzone/issues/1127
|
||||||
|
if runtime.name == "podman" and platform.system() != "Windows":
|
||||||
|
security_args += ["--security-opt", f"seccomp={seccomp_json_path}"]
|
||||||
|
|
||||||
security_args += ["--cap-drop", "all"]
|
security_args += ["--cap-drop", "all"]
|
||||||
security_args += ["--cap-add", "SYS_CHROOT"]
|
security_args += ["--cap-add", "SYS_CHROOT"]
|
||||||
|
@ -118,12 +135,11 @@ class Container(IsolationProvider):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_available() -> bool:
|
def is_available() -> bool:
|
||||||
container_runtime = container_utils.get_runtime()
|
runtime = 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"],
|
[str(runtime.path), "image", "ls"],
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
startupinfo=get_subprocess_startupinfo(),
|
startupinfo=get_subprocess_startupinfo(),
|
||||||
|
@ -131,14 +147,18 @@ class Container(IsolationProvider):
|
||||||
_, stderr = p.communicate()
|
_, stderr = p.communicate()
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise errors.NotAvailableContainerTechException(
|
raise errors.NotAvailableContainerTechException(
|
||||||
runtime_name, stderr.decode()
|
runtime.name, stderr.decode()
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def check_docker_desktop_version(self) -> Tuple[bool, str]:
|
def check_docker_desktop_version(self) -> Tuple[bool, str]:
|
||||||
# On windows and darwin, check that the minimum version is met
|
# On windows and darwin, check that the minimum version is met
|
||||||
version = ""
|
version = ""
|
||||||
if platform.system() != "Linux":
|
runtime = Runtime()
|
||||||
|
runtime_is_docker = runtime.name == "docker"
|
||||||
|
platform_is_not_linux = platform.system() != "Linux"
|
||||||
|
|
||||||
|
if runtime_is_docker and platform_is_not_linux:
|
||||||
with subprocess.Popen(
|
with subprocess.Popen(
|
||||||
["docker", "version", "--format", "{{.Server.Platform.Name}}"],
|
["docker", "version", "--format", "{{.Server.Platform.Name}}"],
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
|
@ -188,7 +208,7 @@ class Container(IsolationProvider):
|
||||||
command: List[str],
|
command: List[str],
|
||||||
name: str,
|
name: str,
|
||||||
) -> subprocess.Popen:
|
) -> subprocess.Popen:
|
||||||
container_runtime = container_utils.get_runtime()
|
runtime = Runtime()
|
||||||
|
|
||||||
image_digest = container_utils.get_local_image_digest(
|
image_digest = container_utils.get_local_image_digest(
|
||||||
container_utils.CONTAINER_NAME
|
container_utils.CONTAINER_NAME
|
||||||
|
@ -216,7 +236,7 @@ class Container(IsolationProvider):
|
||||||
+ image_name
|
+ image_name
|
||||||
+ command
|
+ command
|
||||||
)
|
)
|
||||||
return self.exec([container_runtime] + args)
|
return self.exec([str(runtime.path)] + args)
|
||||||
|
|
||||||
def kill_container(self, name: str) -> None:
|
def kill_container(self, name: str) -> None:
|
||||||
"""Terminate a spawned container.
|
"""Terminate a spawned container.
|
||||||
|
@ -228,8 +248,8 @@ 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()
|
runtime = Runtime()
|
||||||
cmd = [container_runtime, "kill", name]
|
cmd = [str(runtime.path), "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
|
||||||
# have stopped right before invoking this command. In that case, the
|
# have stopped right before invoking this command. In that case, the
|
||||||
|
@ -285,10 +305,10 @@ 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()
|
runtime = 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"],
|
[str(runtime.path), "ps", "-a"],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
startupinfo=get_subprocess_startupinfo(),
|
startupinfo=get_subprocess_startupinfo(),
|
||||||
)
|
)
|
||||||
|
@ -299,19 +319,20 @@ class Container(IsolationProvider):
|
||||||
# FIXME hardcoded 1 until length conversions are better handled
|
# FIXME hardcoded 1 until length conversions are better handled
|
||||||
# https://github.com/freedomofpress/dangerzone/issues/257
|
# https://github.com/freedomofpress/dangerzone/issues/257
|
||||||
return 1
|
return 1
|
||||||
|
runtime = Runtime() # type: ignore [unreachable]
|
||||||
|
|
||||||
n_cpu = 1 # type: ignore [unreachable]
|
n_cpu = 1
|
||||||
if platform.system() == "Linux":
|
if platform.system() == "Linux":
|
||||||
# if on linux containers run natively
|
# if on linux containers run natively
|
||||||
cpu_count = os.cpu_count()
|
cpu_count = os.cpu_count()
|
||||||
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 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}}"],
|
[str(runtime.path), "info", "--format", "{{.NCPU}}"],
|
||||||
text=True,
|
text=True,
|
||||||
startupinfo=get_subprocess_startupinfo(),
|
startupinfo=get_subprocess_startupinfo(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -130,7 +130,6 @@ def is_qubes_native_conversion() -> bool:
|
||||||
# This disambiguates if it is running a Qubes targetted build or not
|
# This disambiguates if it is running a Qubes targetted build or not
|
||||||
# (Qubes-specific builds don't ship the container image)
|
# (Qubes-specific builds don't ship the container image)
|
||||||
|
|
||||||
compressed_container_path = get_resource_path("container.tar.gz")
|
return not get_resource_path("container.tar").exists()
|
||||||
return not os.path.exists(compressed_container_path)
|
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -23,16 +23,13 @@ class DangerzoneCore(object):
|
||||||
# Initialize terminal colors
|
# Initialize terminal colors
|
||||||
colorama.init(autoreset=True)
|
colorama.init(autoreset=True)
|
||||||
|
|
||||||
# App data folder
|
|
||||||
self.appdata_path = util.get_config_dir()
|
|
||||||
|
|
||||||
# Languages supported by tesseract
|
# Languages supported by tesseract
|
||||||
with open(get_resource_path("ocr-languages.json"), "r") as f:
|
with get_resource_path("ocr-languages.json").open("r") as f:
|
||||||
unsorted_ocr_languages = json.load(f)
|
unsorted_ocr_languages = json.load(f)
|
||||||
self.ocr_languages = dict(sorted(unsorted_ocr_languages.items()))
|
self.ocr_languages = dict(sorted(unsorted_ocr_languages.items()))
|
||||||
|
|
||||||
# Load settings
|
# Load settings
|
||||||
self.settings = Settings(self)
|
self.settings = Settings()
|
||||||
self.documents: List[Document] = []
|
self.documents: List[Document] = []
|
||||||
self.isolation_provider = isolation_provider
|
self.isolation_provider = isolation_provider
|
||||||
|
|
||||||
|
|
|
@ -1,29 +1,25 @@
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Any, Dict
|
from typing import TYPE_CHECKING, Any, Dict
|
||||||
|
|
||||||
from packaging import version
|
from packaging import version
|
||||||
|
|
||||||
from .document import SAFE_EXTENSION
|
from .document import SAFE_EXTENSION
|
||||||
from .util import get_version
|
from .util import get_config_dir, get_version
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .logic import DangerzoneCore
|
|
||||||
|
|
||||||
SETTINGS_FILENAME: str = "settings.json"
|
SETTINGS_FILENAME: str = "settings.json"
|
||||||
|
|
||||||
|
|
||||||
class Settings:
|
class Settings:
|
||||||
settings: Dict[str, Any]
|
settings: Dict[str, Any]
|
||||||
|
|
||||||
def __init__(self, dangerzone: "DangerzoneCore") -> None:
|
def __init__(self) -> None:
|
||||||
self.dangerzone = dangerzone
|
self.settings_filename = get_config_dir() / SETTINGS_FILENAME
|
||||||
self.settings_filename = os.path.join(
|
|
||||||
self.dangerzone.appdata_path, SETTINGS_FILENAME
|
|
||||||
)
|
|
||||||
self.default_settings: Dict[str, Any] = self.generate_default_settings()
|
self.default_settings: Dict[str, Any] = self.generate_default_settings()
|
||||||
self.load()
|
self.load()
|
||||||
|
|
||||||
|
@ -45,6 +41,22 @@ class Settings:
|
||||||
"updater_errors": 0,
|
"updater_errors": 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def custom_runtime_specified(self) -> bool:
|
||||||
|
return "container_runtime" in self.settings
|
||||||
|
|
||||||
|
def set_custom_runtime(self, runtime: str, autosave: bool = False) -> Path:
|
||||||
|
from .container_utils import Runtime # Avoid circular import
|
||||||
|
|
||||||
|
container_runtime = Runtime.path_from_name(runtime)
|
||||||
|
self.settings["container_runtime"] = str(container_runtime)
|
||||||
|
if autosave:
|
||||||
|
self.save()
|
||||||
|
return container_runtime
|
||||||
|
|
||||||
|
def unset_custom_runtime(self) -> None:
|
||||||
|
self.settings.pop("container_runtime")
|
||||||
|
self.save()
|
||||||
|
|
||||||
def get(self, key: str) -> Any:
|
def get(self, key: str) -> Any:
|
||||||
return self.settings[key]
|
return self.settings[key]
|
||||||
|
|
||||||
|
@ -91,6 +103,6 @@ class Settings:
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def save(self) -> None:
|
def save(self) -> None:
|
||||||
os.makedirs(self.dangerzone.appdata_path, exist_ok=True)
|
self.settings_filename.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with open(self.settings_filename, "w") as settings_file:
|
with self.settings_filename.open("w") as settings_file:
|
||||||
json.dump(self.settings, settings_file, indent=4)
|
json.dump(self.settings, settings_file, indent=4)
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import pathlib
|
|
||||||
import platform
|
import platform
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
import unicodedata
|
import unicodedata
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import platformdirs
|
import platformdirs
|
||||||
|
@ -11,40 +11,39 @@ except ImportError:
|
||||||
import appdirs as platformdirs # type: ignore[no-redef]
|
import appdirs as platformdirs # type: ignore[no-redef]
|
||||||
|
|
||||||
|
|
||||||
def get_config_dir() -> str:
|
def get_config_dir() -> Path:
|
||||||
return platformdirs.user_config_dir("dangerzone")
|
return Path(platformdirs.user_config_dir("dangerzone"))
|
||||||
|
|
||||||
|
|
||||||
def get_resource_path(filename: str) -> str:
|
def get_resource_path(filename: str) -> Path:
|
||||||
if getattr(sys, "dangerzone_dev", False):
|
if getattr(sys, "dangerzone_dev", False):
|
||||||
# Look for resources directory relative to python file
|
# Look for resources directory relative to python file
|
||||||
project_root = pathlib.Path(__file__).parent.parent
|
project_root = Path(__file__).parent.parent
|
||||||
prefix = project_root / "share"
|
prefix = project_root / "share"
|
||||||
else:
|
else:
|
||||||
if platform.system() == "Darwin":
|
if platform.system() == "Darwin":
|
||||||
bin_path = pathlib.Path(sys.executable)
|
bin_path = Path(sys.executable)
|
||||||
app_path = bin_path.parent.parent
|
app_path = bin_path.parent.parent
|
||||||
prefix = app_path / "Resources" / "share"
|
prefix = app_path / "Resources" / "share"
|
||||||
elif platform.system() == "Linux":
|
elif platform.system() == "Linux":
|
||||||
prefix = pathlib.Path(sys.prefix) / "share" / "dangerzone"
|
prefix = Path(sys.prefix) / "share" / "dangerzone"
|
||||||
elif platform.system() == "Windows":
|
elif platform.system() == "Windows":
|
||||||
exe_path = pathlib.Path(sys.executable)
|
exe_path = Path(sys.executable)
|
||||||
dz_install_path = exe_path.parent
|
dz_install_path = exe_path.parent
|
||||||
prefix = dz_install_path / "share"
|
prefix = dz_install_path / "share"
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(f"Unsupported system {platform.system()}")
|
raise NotImplementedError(f"Unsupported system {platform.system()}")
|
||||||
resource_path = prefix / filename
|
return prefix / filename
|
||||||
return str(resource_path)
|
|
||||||
|
|
||||||
|
|
||||||
def get_tessdata_dir() -> pathlib.Path:
|
def get_tessdata_dir() -> Path:
|
||||||
if getattr(sys, "dangerzone_dev", False) or platform.system() in (
|
if getattr(sys, "dangerzone_dev", False) or platform.system() in (
|
||||||
"Windows",
|
"Windows",
|
||||||
"Darwin",
|
"Darwin",
|
||||||
):
|
):
|
||||||
# Always use the tessdata path from the Dangerzone ./share directory, for
|
# Always use the tessdata path from the Dangerzone ./share directory, for
|
||||||
# development builds, or in Windows/macOS platforms.
|
# development builds, or in Windows/macOS platforms.
|
||||||
return pathlib.Path(get_resource_path("tessdata"))
|
return get_resource_path("tessdata")
|
||||||
|
|
||||||
# In case of Linux systems, grab the Tesseract data from any of the following
|
# In case of Linux systems, grab the Tesseract data from any of the following
|
||||||
# locations. We have found some of the locations through trial and error, whereas
|
# locations. We have found some of the locations through trial and error, whereas
|
||||||
|
@ -55,11 +54,11 @@ def get_tessdata_dir() -> pathlib.Path:
|
||||||
#
|
#
|
||||||
# [1] https://tesseract-ocr.github.io/tessdoc/Installation.html
|
# [1] https://tesseract-ocr.github.io/tessdoc/Installation.html
|
||||||
tessdata_dirs = [
|
tessdata_dirs = [
|
||||||
pathlib.Path("/usr/share/tessdata/"), # on some Debian
|
Path("/usr/share/tessdata/"), # on some Debian
|
||||||
pathlib.Path("/usr/share/tesseract/tessdata/"), # on Fedora
|
Path("/usr/share/tesseract/tessdata/"), # on Fedora
|
||||||
pathlib.Path("/usr/share/tesseract-ocr/tessdata/"), # ? (documented)
|
Path("/usr/share/tesseract-ocr/tessdata/"), # ? (documented)
|
||||||
pathlib.Path("/usr/share/tesseract-ocr/4.00/tessdata/"), # on Ubuntu Focal
|
Path("/usr/share/tesseract-ocr/4.00/tessdata/"), # on Debian Bullseye
|
||||||
pathlib.Path("/usr/share/tesseract-ocr/5/tessdata/"), # on Debian Trixie
|
Path("/usr/share/tesseract-ocr/5/tessdata/"), # on Debian Trixie
|
||||||
]
|
]
|
||||||
|
|
||||||
for dir in tessdata_dirs:
|
for dir in tessdata_dirs:
|
||||||
|
@ -72,7 +71,7 @@ def get_tessdata_dir() -> pathlib.Path:
|
||||||
def get_version() -> str:
|
def get_version() -> str:
|
||||||
"""Returns the Dangerzone version string."""
|
"""Returns the Dangerzone version string."""
|
||||||
try:
|
try:
|
||||||
with open(get_resource_path("version.txt")) as f:
|
with get_resource_path("version.txt").open() as f:
|
||||||
version = f.read().strip()
|
version = f.read().strip()
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
# In dev mode, in Windows, get_resource_path doesn't work properly for the container, but luckily
|
# In dev mode, in Windows, get_resource_path doesn't work properly for the container, but luckily
|
||||||
|
|
8
debian/changelog
vendored
8
debian/changelog
vendored
|
@ -1,8 +1,14 @@
|
||||||
|
dangerzone (0.9.0) unstable; urgency=low
|
||||||
|
|
||||||
|
* Released Dangerzone 0.9.0
|
||||||
|
|
||||||
|
-- Freedom of the Press Foundation <info@freedom.press> Mon, 31 Mar 2025 15:57:18 +0300
|
||||||
|
|
||||||
dangerzone (0.8.1) unstable; urgency=low
|
dangerzone (0.8.1) unstable; urgency=low
|
||||||
|
|
||||||
* Released Dangerzone 0.8.1
|
* Released Dangerzone 0.8.1
|
||||||
|
|
||||||
-- Freedom of the Press Foundation <info@freedom.press> Tue, 22 December 2024 22:03:28 +0300
|
-- Freedom of the Press Foundation <info@freedom.press> Tue, 22 Dec 2024 22:03:28 +0300
|
||||||
|
|
||||||
dangerzone (0.8.0) unstable; urgency=low
|
dangerzone (0.8.0) unstable; urgency=low
|
||||||
|
|
||||||
|
|
|
@ -60,24 +60,6 @@ Run Dangerzone in the end-user environment:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# NOTE: For Ubuntu 20.04 specifically, we need to install some extra deps, mainly for
|
|
||||||
# Podman. This needs to take place both in our dev and end-user environment. See the
|
|
||||||
# corresponding note in our Installation section:
|
|
||||||
#
|
|
||||||
# https://github.com/freedomofpress/dangerzone/blob/main/INSTALL.md#ubuntu-debian
|
|
||||||
DOCKERFILE_UBUNTU_2004_DEPS = r"""
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
|
||||||
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y python-all python3.9 curl wget gnupg2 \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
RUN . /etc/os-release \
|
|
||||||
&& sh -c "echo 'deb http://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_$VERSION_ID/ /' \
|
|
||||||
> /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list" \
|
|
||||||
&& wget -nv https://download.opensuse.org/repositories/devel:kubic:libcontainers:stable/xUbuntu_$VERSION_ID/Release.key -O- \
|
|
||||||
| apt-key add -
|
|
||||||
"""
|
|
||||||
|
|
||||||
# XXX: overcome the fact that ubuntu images (starting on 23.04) ship with the 'ubuntu'
|
# XXX: overcome the fact that ubuntu images (starting on 23.04) ship with the 'ubuntu'
|
||||||
# user by default https://bugs.launchpad.net/cloud-images/+bug/2005129
|
# user by default https://bugs.launchpad.net/cloud-images/+bug/2005129
|
||||||
# Related issue https://github.com/freedomofpress/dangerzone/pull/461
|
# Related issue https://github.com/freedomofpress/dangerzone/pull/461
|
||||||
|
@ -114,33 +96,18 @@ RUN apt-get update \
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends dh-python make build-essential \
|
&& apt-get install -y --no-install-recommends dh-python make build-essential \
|
||||||
git {qt_deps} pipx python3 python3-pip python3-venv dpkg-dev debhelper python3-setuptools \
|
git {qt_deps} pipx python3 python3-pip python3-venv dpkg-dev debhelper python3-setuptools \
|
||||||
|
python3-dev \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
# NOTE: `pipx install poetry` fails on Ubuntu Focal, when installed through APT. By
|
RUN pipx install poetry
|
||||||
# installing the latest version, we sidestep this issue.
|
|
||||||
RUN bash -c 'if [[ "$(pipx --version)" < "1" ]]; then \
|
|
||||||
apt-get update \
|
|
||||||
&& apt-get remove -y pipx \
|
|
||||||
&& apt-get install -y --no-install-recommends python3-pip \
|
|
||||||
&& pip install pipx \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*; \
|
|
||||||
else true; fi'
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends mupdf thunar \
|
&& apt-get install -y --no-install-recommends mupdf thunar \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# NOTE: Fedora 41 comes with Python 3.13 installed. Our Python project is not compatible
|
|
||||||
# yet with Python 3.13, because PySide6 cannot work with this Python version. To
|
|
||||||
# sidestep this, install Python 3.12 *only* in dev environments.
|
|
||||||
DOCKERFILE_BUILD_DEV_FEDORA_41_DEPS = r"""
|
|
||||||
# Install Python 3.12 since our project is not compatible yet with Python 3.13.
|
|
||||||
RUN dnf install -y python3.12
|
|
||||||
"""
|
|
||||||
|
|
||||||
# FIXME: Install Poetry on Fedora via package manager.
|
# FIXME: Install Poetry on Fedora via package manager.
|
||||||
DOCKERFILE_BUILD_DEV_FEDORA_DEPS = r"""
|
DOCKERFILE_BUILD_DEV_FEDORA_DEPS = r"""
|
||||||
RUN dnf install -y git rpm-build podman python3 python3-devel python3-poetry-core \
|
RUN dnf install -y git rpm-build podman python3 python3-devel python3-poetry-core \
|
||||||
pipx make qt6-qtbase-gui \
|
pipx make qt6-qtbase-gui gcc gcc-c++\
|
||||||
&& dnf clean all
|
&& dnf clean all
|
||||||
|
|
||||||
# FIXME: Drop this fix after it's resolved upstream.
|
# FIXME: Drop this fix after it's resolved upstream.
|
||||||
|
@ -564,8 +531,6 @@ class Env:
|
||||||
|
|
||||||
if self.distro == "fedora":
|
if self.distro == "fedora":
|
||||||
install_deps = DOCKERFILE_BUILD_DEV_FEDORA_DEPS
|
install_deps = DOCKERFILE_BUILD_DEV_FEDORA_DEPS
|
||||||
if self.version == "41":
|
|
||||||
install_deps += DOCKERFILE_BUILD_DEV_FEDORA_41_DEPS
|
|
||||||
else:
|
else:
|
||||||
# Use Qt6 in all of our Linux dev environments, and add a missing
|
# Use Qt6 in all of our Linux dev environments, and add a missing
|
||||||
# libxcb-cursor0 dependency
|
# libxcb-cursor0 dependency
|
||||||
|
@ -573,12 +538,7 @@ class Env:
|
||||||
# See https://github.com/freedomofpress/dangerzone/issues/482
|
# See https://github.com/freedomofpress/dangerzone/issues/482
|
||||||
qt_deps = "libqt6gui6 libxcb-cursor0"
|
qt_deps = "libqt6gui6 libxcb-cursor0"
|
||||||
install_deps = DOCKERFILE_BUILD_DEV_DEBIAN_DEPS
|
install_deps = DOCKERFILE_BUILD_DEV_DEBIAN_DEPS
|
||||||
if self.distro == "ubuntu" and self.version in ("20.04", "focal"):
|
if self.distro == "ubuntu" and self.version in ("22.04", "jammy"):
|
||||||
qt_deps = "libqt5gui5 libxcb-cursor0" # Ubuntu Focal has only Qt5.
|
|
||||||
install_deps = (
|
|
||||||
DOCKERFILE_UBUNTU_2004_DEPS + DOCKERFILE_BUILD_DEV_DEBIAN_DEPS
|
|
||||||
)
|
|
||||||
elif self.distro == "ubuntu" and self.version in ("22.04", "jammy"):
|
|
||||||
# Ubuntu Jammy misses a dependency to `libxkbcommon-x11-0`, which we can
|
# Ubuntu Jammy misses a dependency to `libxkbcommon-x11-0`, which we can
|
||||||
# install indirectly via `qt6-qpa-plugins`.
|
# install indirectly via `qt6-qpa-plugins`.
|
||||||
qt_deps += " qt6-qpa-plugins"
|
qt_deps += " qt6-qpa-plugins"
|
||||||
|
@ -592,6 +552,8 @@ class Env:
|
||||||
"noble",
|
"noble",
|
||||||
"24.10",
|
"24.10",
|
||||||
"ocular",
|
"ocular",
|
||||||
|
"25.04",
|
||||||
|
"plucky",
|
||||||
):
|
):
|
||||||
install_deps = (
|
install_deps = (
|
||||||
DOCKERFILE_UBUNTU_REM_USER + DOCKERFILE_BUILD_DEV_DEBIAN_DEPS
|
DOCKERFILE_UBUNTU_REM_USER + DOCKERFILE_BUILD_DEV_DEBIAN_DEPS
|
||||||
|
@ -642,11 +604,7 @@ class Env:
|
||||||
install_cmd = "dnf install -y"
|
install_cmd = "dnf install -y"
|
||||||
else:
|
else:
|
||||||
install_deps = DOCKERFILE_BUILD_DEBIAN_DEPS
|
install_deps = DOCKERFILE_BUILD_DEBIAN_DEPS
|
||||||
if self.distro == "ubuntu" and self.version in ("20.04", "focal"):
|
if self.distro == "ubuntu" and self.version in ("22.04", "jammy"):
|
||||||
install_deps = (
|
|
||||||
DOCKERFILE_UBUNTU_2004_DEPS + DOCKERFILE_BUILD_DEBIAN_DEPS
|
|
||||||
)
|
|
||||||
elif self.distro == "ubuntu" and self.version in ("22.04", "jammy"):
|
|
||||||
# Ubuntu Jammy requires a more up-to-date conmon
|
# Ubuntu Jammy requires a more up-to-date conmon
|
||||||
# package (see https://github.com/freedomofpress/dangerzone/issues/685)
|
# package (see https://github.com/freedomofpress/dangerzone/issues/685)
|
||||||
install_deps = DOCKERFILE_CONMON_UPDATE + DOCKERFILE_BUILD_DEBIAN_DEPS
|
install_deps = DOCKERFILE_CONMON_UPDATE + DOCKERFILE_BUILD_DEBIAN_DEPS
|
||||||
|
@ -655,6 +613,8 @@ class Env:
|
||||||
"noble",
|
"noble",
|
||||||
"24.10",
|
"24.10",
|
||||||
"ocular",
|
"ocular",
|
||||||
|
"25.04",
|
||||||
|
"plucky",
|
||||||
):
|
):
|
||||||
install_deps = DOCKERFILE_UBUNTU_REM_USER + DOCKERFILE_BUILD_DEBIAN_DEPS
|
install_deps = DOCKERFILE_UBUNTU_REM_USER + DOCKERFILE_BUILD_DEBIAN_DEPS
|
||||||
package_pattern = f"dangerzone_{version}-*_*.deb"
|
package_pattern = f"dangerzone_{version}-*_*.deb"
|
||||||
|
|
|
@ -251,29 +251,6 @@ Install dependencies:
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<details>
|
|
||||||
<summary><i>:memo: Expand this section if you are on Ubuntu 20.04 (Focal).</i></summary>
|
|
||||||
</br>
|
|
||||||
|
|
||||||
The default Python version that ships with Ubuntu Focal (3.8) is not
|
|
||||||
compatible with PySide6, which requires Python 3.9 or greater.
|
|
||||||
|
|
||||||
You can install Python 3.9 using the `python3.9` package.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt install -y python3.9
|
|
||||||
```
|
|
||||||
|
|
||||||
Poetry will automatically pick up the correct version when running.
|
|
||||||
</details>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo apt install -y podman dh-python build-essential make libqt6gui6 \
|
sudo apt install -y podman dh-python build-essential make libqt6gui6 \
|
||||||
pipx python3 python3-dev
|
pipx python3 python3-dev
|
||||||
|
@ -350,33 +327,11 @@ sudo dnf install -y rpm-build podman python3 python3-devel python3-poetry-core \
|
||||||
pipx qt6-qtbase-gui
|
pipx qt6-qtbase-gui
|
||||||
```
|
```
|
||||||
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<details>
|
|
||||||
<summary><i>:memo: Expand this section if you are on Fedora 41.</i></summary>
|
|
||||||
</br>
|
|
||||||
|
|
||||||
The default Python version that ships with Fedora 41 (3.13) is not
|
|
||||||
compatible with PySide6, which requires Python 3.12 or earlier.
|
|
||||||
|
|
||||||
You can install Python 3.12 using the `python3.12` package.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo dnf install -y python3.12
|
|
||||||
```
|
|
||||||
|
|
||||||
Poetry will automatically pick up the correct version when running.
|
|
||||||
</details>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
Install Poetry using `pipx`:
|
Install Poetry using `pipx`:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pipx install poetry
|
pipx install poetry
|
||||||
pipx inject poetry poetry-plugin-export
|
pipx inject poetry
|
||||||
```
|
```
|
||||||
|
|
||||||
Clone this repository:
|
Clone this repository:
|
||||||
|
@ -442,7 +397,7 @@ Install Microsoft Visual C++ 14.0 or greater. Get it with ["Microsoft C++ Build
|
||||||
Install [poetry](https://python-poetry.org/). Open PowerShell, and run:
|
Install [poetry](https://python-poetry.org/). Open PowerShell, and run:
|
||||||
|
|
||||||
```
|
```
|
||||||
python -m pip install poetry poetry-plugin-export
|
python -m pip install poetry
|
||||||
```
|
```
|
||||||
|
|
||||||
Install git from [here](https://git-scm.com/download/win), open a Windows terminal (`cmd.exe`) and clone this repository:
|
Install git from [here](https://git-scm.com/download/win), open a Windows terminal (`cmd.exe`) and clone this repository:
|
||||||
|
@ -880,8 +835,8 @@ class QAWindows(QABase):
|
||||||
"Install Poetry and the project's dependencies", ref=REF_BUILD, auto=True
|
"Install Poetry and the project's dependencies", ref=REF_BUILD, auto=True
|
||||||
)
|
)
|
||||||
def install_poetry(self):
|
def install_poetry(self):
|
||||||
self.run("python", "-m", "pip", "install", "poetry", "poetry-plugin-export")
|
self.run("python", "-m", "pip", "install", "poetry")
|
||||||
self.run("poetry", "install", "--sync")
|
self.run("poetry", "sync")
|
||||||
|
|
||||||
@QABase.task("Build Dangerzone container image", ref=REF_BUILD, auto=True)
|
@QABase.task("Build Dangerzone container image", ref=REF_BUILD, auto=True)
|
||||||
def build_image(self):
|
def build_image(self):
|
||||||
|
@ -1035,11 +990,6 @@ class QADebianTrixie(QADebianBased):
|
||||||
VERSION = "trixie"
|
VERSION = "trixie"
|
||||||
|
|
||||||
|
|
||||||
class QAUbuntu2004(QADebianBased):
|
|
||||||
DISTRO = "ubuntu"
|
|
||||||
VERSION = "20.04"
|
|
||||||
|
|
||||||
|
|
||||||
class QAUbuntu2204(QADebianBased):
|
class QAUbuntu2204(QADebianBased):
|
||||||
DISTRO = "ubuntu"
|
DISTRO = "ubuntu"
|
||||||
VERSION = "22.04"
|
VERSION = "22.04"
|
||||||
|
@ -1055,6 +1005,11 @@ class QAUbuntu2410(QADebianBased):
|
||||||
VERSION = "24.10"
|
VERSION = "24.10"
|
||||||
|
|
||||||
|
|
||||||
|
class QAUbuntu2504(QADebianBased):
|
||||||
|
DISTRO = "ubuntu"
|
||||||
|
VERSION = "25.04"
|
||||||
|
|
||||||
|
|
||||||
class QAFedora(QALinux):
|
class QAFedora(QALinux):
|
||||||
"""Base class for Fedora distros.
|
"""Base class for Fedora distros.
|
||||||
|
|
||||||
|
@ -1072,6 +1027,10 @@ class QAFedora(QALinux):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class QAFedora42(QAFedora):
|
||||||
|
VERSION = "42"
|
||||||
|
|
||||||
|
|
||||||
class QAFedora41(QAFedora):
|
class QAFedora41(QAFedora):
|
||||||
VERSION = "41"
|
VERSION = "41"
|
||||||
|
|
||||||
|
|
680
dev_scripts/repro-build.py
Executable file
680
dev_scripts/repro-build.py
Executable file
|
@ -0,0 +1,680 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import datetime
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import pprint
|
||||||
|
import shlex
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tarfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
MEDIA_TYPE_INDEX_V1_JSON = "application/vnd.oci.image.index.v1+json"
|
||||||
|
MEDIA_TYPE_MANIFEST_V1_JSON = "application/vnd.oci.image.manifest.v1+json"
|
||||||
|
|
||||||
|
ENV_RUNTIME = "REPRO_RUNTIME"
|
||||||
|
ENV_DATETIME = "REPRO_DATETIME"
|
||||||
|
ENV_SDE = "REPRO_SOURCE_DATE_EPOCH"
|
||||||
|
ENV_CACHE = "REPRO_CACHE"
|
||||||
|
ENV_BUILDKIT = "REPRO_BUILDKIT_IMAGE"
|
||||||
|
ENV_ROOTLESS = "REPRO_ROOTLESS"
|
||||||
|
|
||||||
|
DEFAULT_BUILDKIT_IMAGE = "moby/buildkit:v0.19.0@sha256:14aa1b4dd92ea0a4cd03a54d0c6079046ea98cd0c0ae6176bdd7036ba370cbbe"
|
||||||
|
DEFAULT_BUILDKIT_IMAGE_ROOTLESS = "moby/buildkit:v0.19.0-rootless@sha256:e901cffdad753892a7c3afb8b9972549fca02c73888cf340c91ed801fdd96d71"
|
||||||
|
|
||||||
|
MSG_BUILD_CTX = """Build environment:
|
||||||
|
- Container runtime: {runtime}
|
||||||
|
- BuildKit image: {buildkit_image}
|
||||||
|
- Rootless support: {rootless}
|
||||||
|
- Caching enabled: {use_cache}
|
||||||
|
- Build context: {context}
|
||||||
|
- Dockerfile: {dockerfile}
|
||||||
|
- Output: {output}
|
||||||
|
|
||||||
|
Build parameters:
|
||||||
|
- SOURCE_DATE_EPOCH: {sde}
|
||||||
|
- Build args: {build_args}
|
||||||
|
- Tag: {tag}
|
||||||
|
- Platform: {platform}
|
||||||
|
|
||||||
|
Podman-only arguments:
|
||||||
|
- BuildKit arguments: {buildkit_args}
|
||||||
|
|
||||||
|
Docker-only arguments:
|
||||||
|
- Docker Buildx arguments: {buildx_args}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def pretty_error(obj: dict, msg: str):
|
||||||
|
raise Exception(f"{msg}\n{pprint.pprint(obj)}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_key(obj: dict, key: str) -> object:
|
||||||
|
if key not in obj:
|
||||||
|
pretty_error(f"Could not find key '{key}' in the dictionary:", obj)
|
||||||
|
return obj[key]
|
||||||
|
|
||||||
|
|
||||||
|
def run(cmd, dry=False, check=True):
|
||||||
|
action = "Would have run" if dry else "Running"
|
||||||
|
logger.debug(f"{action}: {shlex.join(cmd)}")
|
||||||
|
if not dry:
|
||||||
|
subprocess.run(cmd, check=check)
|
||||||
|
|
||||||
|
|
||||||
|
def snip_contents(contents: str, num: int) -> str:
|
||||||
|
contents = contents.replace("\n", "")
|
||||||
|
if len(contents) > num:
|
||||||
|
return (
|
||||||
|
contents[:num]
|
||||||
|
+ f" [... {len(contents) - num} characters omitted."
|
||||||
|
+ " Pass --show-contents to print them in their entirety]"
|
||||||
|
)
|
||||||
|
return contents
|
||||||
|
|
||||||
|
|
||||||
|
def detect_container_runtime() -> str:
|
||||||
|
"""Auto-detect the installed container runtime in the system."""
|
||||||
|
if shutil.which("docker"):
|
||||||
|
return "docker"
|
||||||
|
elif shutil.which("podman"):
|
||||||
|
return "podman"
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_runtime(args) -> str:
|
||||||
|
if args.runtime is not None:
|
||||||
|
return args.runtime
|
||||||
|
|
||||||
|
runtime = os.environ.get(ENV_RUNTIME)
|
||||||
|
if runtime is None:
|
||||||
|
raise RuntimeError("No container runtime detected in your system")
|
||||||
|
if runtime not in ("docker", "podman"):
|
||||||
|
raise RuntimeError(
|
||||||
|
"Only 'docker' or 'podman' container runtimes"
|
||||||
|
" are currently supported by this script"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_use_cache(args) -> bool:
|
||||||
|
if args.no_cache:
|
||||||
|
return False
|
||||||
|
return bool(int(os.environ.get(ENV_CACHE, "1")))
|
||||||
|
|
||||||
|
|
||||||
|
def parse_rootless(args, runtime: str) -> bool:
|
||||||
|
rootless = args.rootless or bool(int(os.environ.get(ENV_ROOTLESS, "0")))
|
||||||
|
if runtime != "podman" and rootless:
|
||||||
|
raise RuntimeError("Rootless mode is only supported with Podman runtime")
|
||||||
|
return rootless
|
||||||
|
|
||||||
|
|
||||||
|
def parse_sde(args) -> str:
|
||||||
|
sde = os.environ.get(ENV_SDE, args.source_date_epoch)
|
||||||
|
dt = os.environ.get(ENV_DATETIME, args.datetime)
|
||||||
|
|
||||||
|
if (sde is not None and dt is not None) or (sde is None and dt is None):
|
||||||
|
raise RuntimeError("You need to pass either a source date epoch or a datetime")
|
||||||
|
|
||||||
|
if sde is not None:
|
||||||
|
return str(sde)
|
||||||
|
|
||||||
|
if dt is not None:
|
||||||
|
d = datetime.datetime.fromisoformat(dt)
|
||||||
|
# If the datetime is naive, assume its timezone is UTC. The check is
|
||||||
|
# taken from:
|
||||||
|
# https://docs.python.org/3/library/datetime.html#determining-if-an-object-is-aware-or-naive
|
||||||
|
if d.tzinfo is None or d.tzinfo.utcoffset(d) is None:
|
||||||
|
d = d.replace(tzinfo=datetime.timezone.utc)
|
||||||
|
return int(d.timestamp())
|
||||||
|
|
||||||
|
|
||||||
|
def parse_buildkit_image(args, rootless: bool, runtime: str) -> str:
|
||||||
|
default = DEFAULT_BUILDKIT_IMAGE_ROOTLESS if rootless else DEFAULT_BUILDKIT_IMAGE
|
||||||
|
img = args.buildkit_image or os.environ.get(ENV_BUILDKIT, default)
|
||||||
|
|
||||||
|
if runtime == "podman" and not img.startswith("docker.io/"):
|
||||||
|
img = "docker.io/" + img
|
||||||
|
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
def parse_build_args(args) -> str:
|
||||||
|
return args.build_arg or []
|
||||||
|
|
||||||
|
|
||||||
|
def parse_buildkit_args(args, runtime: str) -> str:
|
||||||
|
if not args.buildkit_args:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if runtime != "podman":
|
||||||
|
raise RuntimeError("Cannot specify BuildKit arguments using the Podman runtime")
|
||||||
|
|
||||||
|
return shlex.split(args.buildkit_args)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_buildx_args(args, runtime: str) -> str:
|
||||||
|
if not args.buildx_args:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if runtime != "docker":
|
||||||
|
raise RuntimeError(
|
||||||
|
"Cannot specify Docker Buildx arguments using the Podman runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
return shlex.split(args.buildx_args)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_image_digest(args) -> str | None:
|
||||||
|
if not args.expected_image_digest:
|
||||||
|
return None
|
||||||
|
parsed = args.expected_image_digest.split(":", 1)
|
||||||
|
if len(parsed) == 1:
|
||||||
|
return parsed[0]
|
||||||
|
else:
|
||||||
|
return parsed[1]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_path(path: str | None) -> str | None:
|
||||||
|
return path and str(Path(path).absolute())
|
||||||
|
|
||||||
|
|
||||||
|
##########################
|
||||||
|
# OCI parsing logic
|
||||||
|
#
|
||||||
|
# Compatible with:
|
||||||
|
# * https://github.com/opencontainers/image-spec/blob/main/image-layout.md
|
||||||
|
|
||||||
|
|
||||||
|
def oci_print_info(parsed: dict, full: bool) -> None:
|
||||||
|
print(f"The OCI tarball contains an index and {len(parsed) - 1} manifest(s):")
|
||||||
|
print()
|
||||||
|
print(f"Image digest: {parsed[1]['digest']}")
|
||||||
|
for i, info in enumerate(parsed):
|
||||||
|
print()
|
||||||
|
if i == 0:
|
||||||
|
print(f"Index ({info['path']}):")
|
||||||
|
else:
|
||||||
|
print(f"Manifest {i} ({info['path']}):")
|
||||||
|
print(f" Digest: {info['digest']}")
|
||||||
|
print(f" Media type: {info['media_type']}")
|
||||||
|
print(f" Platform: {info['platform'] or '-'}")
|
||||||
|
contents = info["contents"] if full else snip_contents(info["contents"], 600)
|
||||||
|
print(f" Contents: {contents}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def oci_normalize_path(path):
|
||||||
|
if path.startswith("sha256:"):
|
||||||
|
hash_algo, checksum = path.split(":")
|
||||||
|
path = f"blobs/{hash_algo}/{checksum}"
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def oci_get_file_from_tarball(tar: tarfile.TarFile, path: str) -> dict:
|
||||||
|
"""Get file from an OCI tarball.
|
||||||
|
|
||||||
|
If the filename cannot be found, search again by prefixing it with "./", since we
|
||||||
|
have encountered path names in OCI tarballs prefixed with "./".
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return tar.extractfile(path).read().decode()
|
||||||
|
except KeyError:
|
||||||
|
if not path.startswith("./") and not path.startswith("/"):
|
||||||
|
path = "./" + path
|
||||||
|
try:
|
||||||
|
return tar.extractfile(path).read().decode()
|
||||||
|
except KeyError:
|
||||||
|
# Do not raise here, so that we can raise the original exception below.
|
||||||
|
pass
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def oci_parse_manifest(tar: tarfile.TarFile, path: str, platform: dict | None) -> dict:
|
||||||
|
"""Parse manifest information in JSON format.
|
||||||
|
|
||||||
|
Interestingly, the platform info for a manifest is not included in the
|
||||||
|
manifest itself, but in the descriptor that points to it. So, we have to
|
||||||
|
carry it from the previous manifest and include in the info here.
|
||||||
|
"""
|
||||||
|
path = oci_normalize_path(path)
|
||||||
|
contents = oci_get_file_from_tarball(tar, path)
|
||||||
|
digest = "sha256:" + hashlib.sha256(contents.encode()).hexdigest()
|
||||||
|
contents_dict = json.loads(contents)
|
||||||
|
media_type = get_key(contents_dict, "mediaType")
|
||||||
|
manifests = contents_dict.get("manifests", [])
|
||||||
|
|
||||||
|
if platform:
|
||||||
|
os = get_key(platform, "os")
|
||||||
|
arch = get_key(platform, "architecture")
|
||||||
|
platform = f"{os}/{arch}"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"path": path,
|
||||||
|
"contents": contents,
|
||||||
|
"digest": digest,
|
||||||
|
"media_type": media_type,
|
||||||
|
"platform": platform,
|
||||||
|
"manifests": manifests,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def oci_parse_manifests_dfs(
|
||||||
|
tar: tarfile.TarFile, path: str, parsed: list, platform: dict | None = None
|
||||||
|
) -> None:
|
||||||
|
info = oci_parse_manifest(tar, path, platform)
|
||||||
|
parsed.append(info)
|
||||||
|
for m in info["manifests"]:
|
||||||
|
oci_parse_manifests_dfs(tar, m["digest"], parsed, m.get("platform"))
|
||||||
|
|
||||||
|
|
||||||
|
def oci_parse_tarball(path: Path) -> dict:
|
||||||
|
parsed = []
|
||||||
|
with tarfile.TarFile.open(path) as tar:
|
||||||
|
oci_parse_manifests_dfs(tar, "index.json", parsed)
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
##########################
|
||||||
|
# Image building logic
|
||||||
|
|
||||||
|
|
||||||
|
def podman_build(
|
||||||
|
context: str,
|
||||||
|
dockerfile: str | None,
|
||||||
|
tag: str | None,
|
||||||
|
buildkit_image: str,
|
||||||
|
sde: int,
|
||||||
|
rootless: bool,
|
||||||
|
use_cache: bool,
|
||||||
|
output: Path,
|
||||||
|
build_args: list,
|
||||||
|
platform: str,
|
||||||
|
buildkit_args: list,
|
||||||
|
dry: bool,
|
||||||
|
):
|
||||||
|
rootless_args = []
|
||||||
|
rootful_args = []
|
||||||
|
if rootless:
|
||||||
|
rootless_args = [
|
||||||
|
"--userns",
|
||||||
|
"keep-id:uid=1000,gid=1000",
|
||||||
|
"--security-opt",
|
||||||
|
"seccomp=unconfined",
|
||||||
|
"--security-opt",
|
||||||
|
"apparmor=unconfined",
|
||||||
|
"-e",
|
||||||
|
"BUILDKITD_FLAGS=--oci-worker-no-process-sandbox",
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
rootful_args = ["--privileged"]
|
||||||
|
|
||||||
|
dockerfile_args_podman = []
|
||||||
|
dockerfile_args_buildkit = []
|
||||||
|
if dockerfile:
|
||||||
|
dockerfile_args_podman = ["-v", f"{dockerfile}:/tmp/Dockerfile"]
|
||||||
|
dockerfile_args_buildkit = ["--local", "dockerfile=/tmp"]
|
||||||
|
else:
|
||||||
|
dockerfile_args_buildkit = ["--local", "dockerfile=/tmp/work"]
|
||||||
|
|
||||||
|
tag_args = f",name={tag}" if tag else ""
|
||||||
|
|
||||||
|
cache_args = []
|
||||||
|
if use_cache:
|
||||||
|
cache_args = [
|
||||||
|
"--export-cache",
|
||||||
|
"type=local,mode=max,dest=/tmp/cache",
|
||||||
|
"--import-cache",
|
||||||
|
"type=local,src=/tmp/cache",
|
||||||
|
]
|
||||||
|
|
||||||
|
_build_args = []
|
||||||
|
for arg in build_args:
|
||||||
|
_build_args.append("--opt")
|
||||||
|
_build_args.append(f"build-arg:{arg}")
|
||||||
|
platform_args = ["--opt", f"platform={platform}"] if platform else []
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
"podman",
|
||||||
|
"run",
|
||||||
|
"-it",
|
||||||
|
"--rm",
|
||||||
|
"-v",
|
||||||
|
"buildkit_cache:/tmp/cache",
|
||||||
|
"-v",
|
||||||
|
f"{output.parent}:/tmp/image",
|
||||||
|
"-v",
|
||||||
|
f"{context}:/tmp/work",
|
||||||
|
"--entrypoint",
|
||||||
|
"buildctl-daemonless.sh",
|
||||||
|
*rootless_args,
|
||||||
|
*rootful_args,
|
||||||
|
*dockerfile_args_podman,
|
||||||
|
buildkit_image,
|
||||||
|
"build",
|
||||||
|
"--frontend",
|
||||||
|
"dockerfile.v0",
|
||||||
|
"--local",
|
||||||
|
"context=/tmp/work",
|
||||||
|
"--opt",
|
||||||
|
f"build-arg:SOURCE_DATE_EPOCH={sde}",
|
||||||
|
*_build_args,
|
||||||
|
"--output",
|
||||||
|
f"type=docker,dest=/tmp/image/{output.name},rewrite-timestamp=true{tag_args}",
|
||||||
|
*cache_args,
|
||||||
|
*dockerfile_args_buildkit,
|
||||||
|
*platform_args,
|
||||||
|
*buildkit_args,
|
||||||
|
]
|
||||||
|
|
||||||
|
run(cmd, dry)
|
||||||
|
|
||||||
|
|
||||||
|
def docker_build(
|
||||||
|
context: str,
|
||||||
|
dockerfile: str | None,
|
||||||
|
tag: str | None,
|
||||||
|
buildkit_image: str,
|
||||||
|
sde: int,
|
||||||
|
use_cache: bool,
|
||||||
|
output: Path,
|
||||||
|
build_args: list,
|
||||||
|
platform: str,
|
||||||
|
buildx_args: list,
|
||||||
|
dry: bool,
|
||||||
|
):
|
||||||
|
builder_id = hashlib.sha256(buildkit_image.encode()).hexdigest()
|
||||||
|
builder_name = f"repro-build-{builder_id}"
|
||||||
|
tag_args = ["-t", tag] if tag else []
|
||||||
|
cache_args = [] if use_cache else ["--no-cache", "--pull"]
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
"docker",
|
||||||
|
"buildx",
|
||||||
|
"create",
|
||||||
|
"--name",
|
||||||
|
builder_name,
|
||||||
|
"--driver-opt",
|
||||||
|
f"image={buildkit_image}",
|
||||||
|
]
|
||||||
|
run(cmd, dry, check=False)
|
||||||
|
|
||||||
|
dockerfile_args = ["-f", dockerfile] if dockerfile else []
|
||||||
|
_build_args = []
|
||||||
|
for arg in build_args:
|
||||||
|
_build_args.append("--build-arg")
|
||||||
|
_build_args.append(arg)
|
||||||
|
platform_args = ["--platform", platform] if platform else []
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
"docker",
|
||||||
|
"buildx",
|
||||||
|
"--builder",
|
||||||
|
builder_name,
|
||||||
|
"build",
|
||||||
|
"--build-arg",
|
||||||
|
f"SOURCE_DATE_EPOCH={sde}",
|
||||||
|
*_build_args,
|
||||||
|
"--provenance",
|
||||||
|
"false",
|
||||||
|
"--output",
|
||||||
|
f"type=docker,dest={output},rewrite-timestamp=true",
|
||||||
|
*cache_args,
|
||||||
|
*tag_args,
|
||||||
|
*dockerfile_args,
|
||||||
|
*platform_args,
|
||||||
|
*buildx_args,
|
||||||
|
context,
|
||||||
|
]
|
||||||
|
run(cmd, dry)
|
||||||
|
|
||||||
|
|
||||||
|
##########################
|
||||||
|
# Command logic
|
||||||
|
|
||||||
|
|
||||||
|
def build(args):
|
||||||
|
runtime = parse_runtime(args)
|
||||||
|
use_cache = parse_use_cache(args)
|
||||||
|
sde = parse_sde(args)
|
||||||
|
rootless = parse_rootless(args, runtime)
|
||||||
|
buildkit_image = parse_buildkit_image(args, rootless, runtime)
|
||||||
|
build_args = parse_build_args(args)
|
||||||
|
platform = args.platform
|
||||||
|
buildkit_args = parse_buildkit_args(args, runtime)
|
||||||
|
buildx_args = parse_buildx_args(args, runtime)
|
||||||
|
tag = args.tag
|
||||||
|
dockerfile = parse_path(args.file)
|
||||||
|
output = Path(parse_path(args.output))
|
||||||
|
dry = args.dry
|
||||||
|
context = parse_path(args.context)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
MSG_BUILD_CTX.format(
|
||||||
|
runtime=runtime,
|
||||||
|
buildkit_image=buildkit_image,
|
||||||
|
sde=sde,
|
||||||
|
rootless=rootless,
|
||||||
|
use_cache=use_cache,
|
||||||
|
context=context,
|
||||||
|
dockerfile=dockerfile or "(not provided)",
|
||||||
|
tag=tag or "(not provided)",
|
||||||
|
output=output,
|
||||||
|
build_args=",".join(build_args) or "(not provided)",
|
||||||
|
platform=platform or "(default)",
|
||||||
|
buildkit_args=" ".join(buildkit_args) or "(not provided)",
|
||||||
|
buildx_args=" ".join(buildx_args) or "(not provided)",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if runtime == "docker":
|
||||||
|
docker_build(
|
||||||
|
context,
|
||||||
|
dockerfile,
|
||||||
|
tag,
|
||||||
|
buildkit_image,
|
||||||
|
sde,
|
||||||
|
use_cache,
|
||||||
|
output,
|
||||||
|
build_args,
|
||||||
|
platform,
|
||||||
|
buildx_args,
|
||||||
|
dry,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
podman_build(
|
||||||
|
context,
|
||||||
|
dockerfile,
|
||||||
|
tag,
|
||||||
|
buildkit_image,
|
||||||
|
sde,
|
||||||
|
rootless,
|
||||||
|
use_cache,
|
||||||
|
output,
|
||||||
|
build_args,
|
||||||
|
platform,
|
||||||
|
buildkit_args,
|
||||||
|
dry,
|
||||||
|
)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
logger.error(f"Failed with {e.returncode}")
|
||||||
|
sys.exit(e.returncode)
|
||||||
|
|
||||||
|
|
||||||
|
def analyze(args) -> None:
|
||||||
|
expected_image_digest = parse_image_digest(args)
|
||||||
|
tarball_path = Path(args.tarball)
|
||||||
|
|
||||||
|
parsed = oci_parse_tarball(tarball_path)
|
||||||
|
oci_print_info(parsed, args.show_contents)
|
||||||
|
|
||||||
|
if expected_image_digest:
|
||||||
|
cur_digest = parsed[1]["digest"].split(":")[1]
|
||||||
|
if cur_digest != expected_image_digest:
|
||||||
|
raise Exception(
|
||||||
|
f"The image does not have the expected digest: {cur_digest} != {expected_image_digest}"
|
||||||
|
)
|
||||||
|
print(f"✅ Image digest matches {expected_image_digest}")
|
||||||
|
|
||||||
|
|
||||||
|
def define_build_cmd_args(parser: argparse.ArgumentParser) -> None:
|
||||||
|
parser.add_argument(
|
||||||
|
"--runtime",
|
||||||
|
choices=["docker", "podman"],
|
||||||
|
default=detect_container_runtime(),
|
||||||
|
help="The container runtime for building the image (default: %(default)s)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--datetime",
|
||||||
|
metavar="YYYY-MM-DD",
|
||||||
|
default=None,
|
||||||
|
help=(
|
||||||
|
"Provide a date and (optionally) a time in ISO format, which will"
|
||||||
|
" be used as the timestamp of the image layers"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--buildkit-image",
|
||||||
|
metavar="NAME:TAG@DIGEST",
|
||||||
|
default=None,
|
||||||
|
help=(
|
||||||
|
"The BuildKit container image which will be used for building the"
|
||||||
|
" reproducible container image. Make sure to pass the '-rootless'"
|
||||||
|
" variant if you are using rootless Podman"
|
||||||
|
" (default: docker.io/moby/buildkit:v0.19.0)"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--source-date-epoch",
|
||||||
|
"--sde",
|
||||||
|
metavar="SECONDS",
|
||||||
|
type=int,
|
||||||
|
default=None,
|
||||||
|
help="Provide a Unix timestamp for the image layers",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-cache",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="Do not use existing cached images for the container build. Build from the start with a new set of cached layers.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--rootless",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="Run BuildKit in rootless mode (Podman only)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-f",
|
||||||
|
"--file",
|
||||||
|
metavar="FILE",
|
||||||
|
default=None,
|
||||||
|
help="Pathname of a Dockerfile",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-o",
|
||||||
|
"--output",
|
||||||
|
metavar="FILE",
|
||||||
|
default=Path.cwd() / "image.tar",
|
||||||
|
help="Path to save OCI tarball (default: %(default)s)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-t",
|
||||||
|
"--tag",
|
||||||
|
metavar="TAG",
|
||||||
|
default=None,
|
||||||
|
help="Tag the built image with the name %(metavar)s",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--build-arg",
|
||||||
|
metavar="ARG=VALUE",
|
||||||
|
action="append",
|
||||||
|
default=None,
|
||||||
|
help="Set build-time variables",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--platform",
|
||||||
|
metavar="PLAT1,PLAT2",
|
||||||
|
default=None,
|
||||||
|
help="Set platform for the image",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--buildkit-args",
|
||||||
|
metavar="'ARG1 ARG2'",
|
||||||
|
default=None,
|
||||||
|
help="Extra arguments for BuildKit (Podman only)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--buildx-args",
|
||||||
|
metavar="'ARG1 ARG2'",
|
||||||
|
default=None,
|
||||||
|
help="Extra arguments for Docker Buildx (Docker only)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--dry",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="Do not run any commands, just print what would happen",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"context",
|
||||||
|
metavar="CONTEXT",
|
||||||
|
help="Path to the build context",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> dict:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
||||||
|
|
||||||
|
build_parser = subparsers.add_parser("build", help="Perform a build operation")
|
||||||
|
build_parser.set_defaults(func=build)
|
||||||
|
define_build_cmd_args(build_parser)
|
||||||
|
|
||||||
|
analyze_parser = subparsers.add_parser("analyze", help="Analyze an OCI tarball")
|
||||||
|
analyze_parser.set_defaults(func=analyze)
|
||||||
|
analyze_parser.add_argument(
|
||||||
|
"tarball",
|
||||||
|
metavar="FILE",
|
||||||
|
help="Path to OCI image in .tar format",
|
||||||
|
)
|
||||||
|
analyze_parser.add_argument(
|
||||||
|
"--expected-image-digest",
|
||||||
|
metavar="DIGEST",
|
||||||
|
default=None,
|
||||||
|
help="The expected digest for the provided image",
|
||||||
|
)
|
||||||
|
analyze_parser.add_argument(
|
||||||
|
"--show-contents",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="Show full file contents",
|
||||||
|
)
|
||||||
|
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.DEBUG,
|
||||||
|
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||||
|
datefmt="%Y-%m-%d %H:%M:%S",
|
||||||
|
)
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
|
if not hasattr(args, "func"):
|
||||||
|
args.func = build
|
||||||
|
args.func(args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
|
@ -12,164 +12,72 @@ import urllib.request
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
DIFFOCI_VERSION = "v0.1.5"
|
if platform.system() in ["Darwin", "Windows"]:
|
||||||
# https://github.com/reproducible-containers/diffoci/releases/download/v0.1.5/SHA256SUMS
|
CONTAINER_RUNTIME = "docker"
|
||||||
DIFFOCI_CHECKSUMS = """
|
elif platform.system() == "Linux":
|
||||||
ae171821b18c3b9e5cd1953323e79fe5ec1e972e9586474b18227b2cd052e695 diffoci-v0.1.5.darwin-amd64
|
CONTAINER_RUNTIME = "podman"
|
||||||
fadabdac9be45fb3dfe2a53986422e53dcc6e1fdc8062713c5760e8959a37c2b diffoci-v0.1.5.darwin-arm64
|
|
||||||
01d25fe690196945a6bd510d30559338aa489c034d3a1b895a0d82a4b860698f diffoci-v0.1.5.linux-amd64
|
|
||||||
5cbc5d13b51183e2988ee0f406d428eb846d51b7c2c12ae17d0775371f43103e diffoci-v0.1.5.linux-arm-v7
|
|
||||||
2d067bd1af8a26b2c206c6bf2bde9bcb21062ddb5dc575e110e0e1a93d0d065f diffoci-v0.1.5.linux-arm64
|
|
||||||
0923f0c01f270c596fea9f84e529af958d6caba3fa0f6bf4f03df2a12f23b3fc diffoci-v0.1.5.linux-ppc64le
|
|
||||||
5821cbc299a90caa167c3a91465292907077ca1123375f88165a842b8970e710 diffoci-v0.1.5.linux-riscv64
|
|
||||||
917d7f23d2bd8fcc755cb2f722fc50ffd83389e04838c3b6e9c3463ea96a9be1 diffoci-v0.1.5.linux-s390x
|
|
||||||
"""
|
|
||||||
DIFFOCI_URL = "https://github.com/reproducible-containers/diffoci/releases/download/{version}/diffoci-{version}.{arch}"
|
|
||||||
|
|
||||||
DIFFOCI_PATH = (
|
|
||||||
pathlib.Path.home() / ".local" / "share" / "dangerzone-dev" / "helpers" / "diffoci"
|
|
||||||
)
|
|
||||||
IMAGE_NAME = "dangerzone.rocks/dangerzone"
|
|
||||||
|
|
||||||
|
|
||||||
def run(*args):
|
def run(*args):
|
||||||
"""Simple function that runs a command, validates it, and returns the output"""
|
"""Simple function that runs a command and checks the result."""
|
||||||
logger.debug(f"Running command: {' '.join(args)}")
|
logger.debug(f"Running command: {' '.join(args)}")
|
||||||
return subprocess.run(
|
return subprocess.run(args, check=True)
|
||||||
args,
|
|
||||||
check=True,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
).stdout
|
|
||||||
|
|
||||||
|
|
||||||
def git_commit_get():
|
def build_image(
|
||||||
return run("git", "rev-parse", "--short", "HEAD").decode().strip()
|
platform=None,
|
||||||
|
runtime=None,
|
||||||
|
cache=True,
|
||||||
def git_determine_tag():
|
date=None,
|
||||||
return run("git", "describe", "--long", "--first-parent").decode().strip()[1:]
|
):
|
||||||
|
|
||||||
|
|
||||||
def git_verify(commit, source):
|
|
||||||
if not commit in source:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Image '{source}' does not seem to be built from commit '{commit}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_platform_arch():
|
|
||||||
system = platform.system().lower()
|
|
||||||
arch = platform.machine().lower()
|
|
||||||
if arch == "x86_64":
|
|
||||||
arch = "amd64"
|
|
||||||
return f"{system}-{arch}"
|
|
||||||
|
|
||||||
|
|
||||||
def parse_checksums():
|
|
||||||
lines = [
|
|
||||||
line.replace(f"diffoci-{DIFFOCI_VERSION}.", "").split(" ")
|
|
||||||
for line in DIFFOCI_CHECKSUMS.split("\n")
|
|
||||||
if line
|
|
||||||
]
|
|
||||||
return {arch: checksum for checksum, arch in lines}
|
|
||||||
|
|
||||||
|
|
||||||
def diffoci_hash_matches(diffoci):
|
|
||||||
"""Check if the hash of the downloaded diffoci bin matches the expected one."""
|
|
||||||
arch = get_platform_arch()
|
|
||||||
expected_checksum = parse_checksums().get(arch)
|
|
||||||
m = hashlib.sha256()
|
|
||||||
m.update(diffoci)
|
|
||||||
diffoci_checksum = m.hexdigest()
|
|
||||||
return diffoci_checksum == expected_checksum
|
|
||||||
|
|
||||||
|
|
||||||
def diffoci_is_installed():
|
|
||||||
"""Determine if diffoci has been installed.
|
|
||||||
|
|
||||||
Determine if diffoci has been installed, by checking if the binary exists, and if
|
|
||||||
its hash is the expected one. If the binary exists but the hash is different, then
|
|
||||||
this is a sign that we need to update the local diffoci binary.
|
|
||||||
"""
|
|
||||||
if not DIFFOCI_PATH.exists():
|
|
||||||
return False
|
|
||||||
return diffoci_hash_matches(DIFFOCI_PATH.open("rb").read())
|
|
||||||
|
|
||||||
|
|
||||||
def diffoci_download():
|
|
||||||
"""Download the diffoci tool, based on a URL and its checksum."""
|
|
||||||
download_url = DIFFOCI_URL.format(version=DIFFOCI_VERSION, arch=get_platform_arch())
|
|
||||||
logger.info(f"Downloading diffoci helper from {download_url}")
|
|
||||||
with urllib.request.urlopen(download_url) as f:
|
|
||||||
diffoci_bin = f.read()
|
|
||||||
|
|
||||||
if not diffoci_hash_matches(diffoci_bin):
|
|
||||||
raise ValueError(
|
|
||||||
"Unexpected checksum for downloaded diffoci binary:"
|
|
||||||
f" {diffoci_checksum} !={DIFFOCI_CHECKSUM}"
|
|
||||||
)
|
|
||||||
|
|
||||||
DIFFOCI_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
DIFFOCI_PATH.open("wb+").write(diffoci_bin)
|
|
||||||
DIFFOCI_PATH.chmod(DIFFOCI_PATH.stat().st_mode | stat.S_IEXEC)
|
|
||||||
|
|
||||||
|
|
||||||
def diffoci_diff(source, local_target):
|
|
||||||
"""Diff the source image against the recently built target image using diffoci."""
|
|
||||||
target = f"podman://{local_target}"
|
|
||||||
try:
|
|
||||||
return run(
|
|
||||||
str(DIFFOCI_PATH),
|
|
||||||
"diff",
|
|
||||||
source,
|
|
||||||
target,
|
|
||||||
"--semantic",
|
|
||||||
"--verbose",
|
|
||||||
)
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
error = e.stdout.decode()
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Could not rebuild an identical image to {source}. Diffoci report:\n{error}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def build_image(tag, use_cache=False):
|
|
||||||
"""Build the Dangerzone container image with a special tag."""
|
"""Build the Dangerzone container image with a special tag."""
|
||||||
|
platform_args = [] if not platform else ["--platform", platform]
|
||||||
|
runtime_args = [] if not runtime else ["--runtime", runtime]
|
||||||
|
cache_args = [] if cache else ["--use-cache", "no"]
|
||||||
|
date_args = [] if not date else ["--debian-archive-date", date]
|
||||||
run(
|
run(
|
||||||
"python3",
|
"python3",
|
||||||
"./install/common/build-image.py",
|
"./install/common/build-image.py",
|
||||||
"--no-save",
|
*platform_args,
|
||||||
"--use-cache",
|
*runtime_args,
|
||||||
str(use_cache),
|
*cache_args,
|
||||||
"--tag",
|
*date_args,
|
||||||
tag,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def parse_args():
|
def parse_args():
|
||||||
image_tag = git_determine_tag()
|
|
||||||
# TODO: Remove the local "podman://" prefix once we have started pushing images to a
|
|
||||||
# remote.
|
|
||||||
default_image_name = f"podman://{IMAGE_NAME}:{image_tag}"
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
prog=sys.argv[0],
|
prog=sys.argv[0],
|
||||||
description="Dev script for verifying container image reproducibility",
|
description="Dev script for verifying container image reproducibility",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--source",
|
"--platform",
|
||||||
default=default_image_name,
|
default=None,
|
||||||
|
help=f"The platform for building the image (default: current platform)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--runtime",
|
||||||
|
choices=["docker", "podman"],
|
||||||
|
default=CONTAINER_RUNTIME,
|
||||||
|
help=f"The container runtime for building the image (default: {CONTAINER_RUNTIME})",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-cache",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
help=(
|
help=(
|
||||||
"The name of the image that you want to reproduce. If the image resides in"
|
"Do not use existing cached images for the container build."
|
||||||
" the local Docker / Podman engine, you can prefix it with podman:// or"
|
" Build from the start with a new set of cached layers."
|
||||||
f" docker:// accordingly (default: {default_image_name})"
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--use-cache",
|
"--debian-archive-date",
|
||||||
default=False,
|
default=None,
|
||||||
action="store_true",
|
help="Use a specific Debian snapshot archive, by its date",
|
||||||
help="Whether to reuse the build cache (off by default for better reproducibility)",
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"digest",
|
||||||
|
help="The digest of the image that you want to reproduce",
|
||||||
)
|
)
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
@ -182,31 +90,25 @@ def main():
|
||||||
)
|
)
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
|
|
||||||
logger.info(f"Ensuring that current Git commit matches image '{args.source}'")
|
logger.info(f"Building container image")
|
||||||
commit = git_commit_get()
|
build_image(
|
||||||
git_verify(commit, args.source)
|
args.platform,
|
||||||
|
args.runtime,
|
||||||
if not diffoci_is_installed():
|
not args.no_cache,
|
||||||
diffoci_download()
|
args.debian_archive_date,
|
||||||
|
)
|
||||||
tag = f"reproduce-{commit}"
|
|
||||||
target = f"{IMAGE_NAME}:{tag}"
|
|
||||||
logger.info(f"Building container image and tagging it as '{target}'")
|
|
||||||
build_image(tag, args.use_cache)
|
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Ensuring that source image '{args.source}' is semantically identical with"
|
f"Check that the reproduced image has the expected digest: {args.digest}"
|
||||||
f" built image '{target}'"
|
)
|
||||||
|
run(
|
||||||
|
"./dev_scripts/repro-build.py",
|
||||||
|
"analyze",
|
||||||
|
"--show-contents",
|
||||||
|
"share/container.tar",
|
||||||
|
"--expected-image-digest",
|
||||||
|
args.digest,
|
||||||
)
|
)
|
||||||
try:
|
|
||||||
diffoci_diff(args.source, target)
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Could not reproduce image {args.source} for commit {commit}"
|
|
||||||
)
|
|
||||||
breakpoint()
|
|
||||||
|
|
||||||
logger.info(f"Successfully reproduced image '{args.source}' from commit '{commit}'")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
@ -11,8 +11,8 @@ log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
DZ_ASSETS = [
|
DZ_ASSETS = [
|
||||||
"container-{version}-i686.tar.gz",
|
"container-{version}-i686.tar",
|
||||||
"container-{version}-arm64.tar.gz",
|
"container-{version}-arm64.tar",
|
||||||
"Dangerzone-{version}.msi",
|
"Dangerzone-{version}.msi",
|
||||||
"Dangerzone-{version}-arm64.dmg",
|
"Dangerzone-{version}-arm64.dmg",
|
||||||
"Dangerzone-{version}-i686.dmg",
|
"Dangerzone-{version}-i686.dmg",
|
||||||
|
|
|
@ -42,7 +42,8 @@ doit <task>
|
||||||
## Tips and tricks
|
## Tips and tricks
|
||||||
|
|
||||||
* You can run `doit list --all -s` to see the full list of tasks, their
|
* You can run `doit list --all -s` to see the full list of tasks, their
|
||||||
dependencies, and whether they are up to date.
|
dependencies, and whether they are up to date (U) or will run (R). Note that
|
||||||
|
certain small tasks are always configured to run.
|
||||||
* You can run `doit info <task>` to see which dependencies are missing.
|
* You can run `doit info <task>` to see which dependencies are missing.
|
||||||
* You can pass the following environment variables to the script, in order to
|
* You can pass the following environment variables to the script, in order to
|
||||||
affect some global parameters:
|
affect some global parameters:
|
||||||
|
|
|
@ -27,7 +27,7 @@ This means that rebuilding the image without updating our Dockerfile will
|
||||||
|
|
||||||
Here are the necessary variables that make up our image in the `Dockerfile.env`
|
Here are the necessary variables that make up our image in the `Dockerfile.env`
|
||||||
file:
|
file:
|
||||||
* `DEBIAN_IMAGE_DATE`: The date that the Debian container image was released
|
* `DEBIAN_IMAGE_DIGEST`: The index digest for the Debian container image
|
||||||
* `DEBIAN_ARCHIVE_DATE`: The Debian snapshot repo that we want to use
|
* `DEBIAN_ARCHIVE_DATE`: The Debian snapshot repo that we want to use
|
||||||
* `GVISOR_ARCHIVE_DATE`: The gVisor APT repo that we want to use
|
* `GVISOR_ARCHIVE_DATE`: The gVisor APT repo that we want to use
|
||||||
* `H2ORESTART_CHECKSUM`: The SHA-256 checksum of the H2ORestart plugin
|
* `H2ORESTART_CHECKSUM`: The SHA-256 checksum of the H2ORestart plugin
|
||||||
|
@ -47,21 +47,21 @@ trigger a CI error.
|
||||||
|
|
||||||
For a simple way to reproduce a Dangerzone container image, you can checkout the
|
For a simple way to reproduce a Dangerzone container image, you can checkout the
|
||||||
commit this image was built from (you can find it from the image tag in its
|
commit this image was built from (you can find it from the image tag in its
|
||||||
`g<commit>` portion), and run the following command in a Linux environment:
|
`g<commit>` portion), retrieve the date it was built (also included in the image
|
||||||
|
tag), and run the following command in any environment:
|
||||||
|
|
||||||
```
|
```
|
||||||
./dev_scripts/reproduce-image.py --source <image>
|
./dev_scripts/reproduce-image.py \
|
||||||
|
--debian-archive-date <date> \
|
||||||
|
<digest>
|
||||||
```
|
```
|
||||||
|
|
||||||
This command will download the `diffoci` helper, build a container image from
|
where:
|
||||||
the current Git commit, and ensure that the built image matches the source one,
|
* `<date>` should be given in YYYYMMDD format, e.g, 20250226
|
||||||
with the exception of image names and file timestamps.
|
* `<digest>` is the SHA-256 hash of the image for the **current platform**, with
|
||||||
|
or without the `sha256:` prefix.
|
||||||
|
|
||||||
> [!TIP]
|
This command will build a container image from the current Git commit and the
|
||||||
> If the source image is not pushed to a registry, and is local instead, you
|
provided date for the Debian archives. Then, it will compare the digest of the
|
||||||
> can prefix it with `docker://` or `podman://` accordingly, so that `diffoci`
|
manifest against the provided one. This is a simple way to ensure that the
|
||||||
> can load it from the local Docker / Podman container engine. For example:
|
created image is bit-for-bit reproducible.
|
||||||
>
|
|
||||||
> ```
|
|
||||||
> ./dev_scripts/reproduce.py --source podman://dangerzone.rocks/dangerzone:0.8.0-125-g725ce3b
|
|
||||||
> ```
|
|
||||||
|
|
53
docs/podman-desktop.md
Normal file
53
docs/podman-desktop.md
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
# Podman Desktop support
|
||||||
|
|
||||||
|
Starting with Dangerzone 0.9.0, it is possible to use Podman Desktop on
|
||||||
|
Windows and macOS. The support for this container runtime is currently only
|
||||||
|
experimental. If you try it out and encounter issues, please reach to us, we'll
|
||||||
|
be glad to help.
|
||||||
|
|
||||||
|
With [Podman Desktop](https://podman-desktop.io/) installed on your machine,
|
||||||
|
here are the required steps to change the dangerzone container runtime.
|
||||||
|
|
||||||
|
You will be required to open a terminal and follow these steps:
|
||||||
|
|
||||||
|
## On macOS
|
||||||
|
|
||||||
|
You will need to configure podman to access the shared Dangerzone resources:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
podman machine stop
|
||||||
|
podman machine rm
|
||||||
|
cat > ~/.config/containers/containers.conf <<EOF
|
||||||
|
[machine]
|
||||||
|
volumes = ["/Users:/Users", "/private:/private", "/var/folders:/var/folders", "/Applications/Dangerzone.app:/Applications/Dangerzone.app"]
|
||||||
|
EOF
|
||||||
|
podman machine init
|
||||||
|
podman machine set --rootful=false
|
||||||
|
podman machine start
|
||||||
|
```
|
||||||
|
Then, set the container runtime to podman using this command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/Applications/Dangerzone.app/Contents/MacOS/dangerzone-cli --set-container-runtime podman
|
||||||
|
```
|
||||||
|
|
||||||
|
In order to get back to the default behaviour (Docker Desktop on macOS), pass
|
||||||
|
the `default` value instead:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/Applications/Dangerzone.app/Contents/MacOS/dangerzone-cli --set-container-runtime default
|
||||||
|
```
|
||||||
|
|
||||||
|
## On Windows
|
||||||
|
|
||||||
|
To set the container runtime to podman, use this command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
'C:\Program Files\Dangerzone\dangerzone-cli.exe' --set-container-runtime podman
|
||||||
|
```
|
||||||
|
|
||||||
|
To revert back to the default behavior, pass the `default` value:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
'C:\Program Files\Dangerzone\dangerzone-cli.exe' --set-container-runtime podman
|
||||||
|
```
|
14
dodo.py
14
dodo.py
|
@ -8,8 +8,7 @@ from doit.action import CmdAction
|
||||||
|
|
||||||
ARCH = "arm64" if platform.machine() == "arm64" else "i686"
|
ARCH = "arm64" if platform.machine() == "arm64" else "i686"
|
||||||
VERSION = open("share/version.txt").read().strip()
|
VERSION = open("share/version.txt").read().strip()
|
||||||
FEDORA_VERSIONS = ["40", "41"]
|
FEDORA_VERSIONS = ["40", "41", "42"]
|
||||||
DEBIAN_VERSIONS = ["bullseye", "focal", "jammy", "mantic", "noble", "trixie"]
|
|
||||||
|
|
||||||
### Global parameters
|
### Global parameters
|
||||||
|
|
||||||
|
@ -44,7 +43,6 @@ def list_language_data():
|
||||||
tessdata_dir = Path("share") / "tessdata"
|
tessdata_dir = Path("share") / "tessdata"
|
||||||
langs = json.loads(open(tessdata_dir.parent / "ocr-languages.json").read()).values()
|
langs = json.loads(open(tessdata_dir.parent / "ocr-languages.json").read()).values()
|
||||||
targets = [tessdata_dir / f"{lang}.traineddata" for lang in langs]
|
targets = [tessdata_dir / f"{lang}.traineddata" for lang in langs]
|
||||||
targets.append(tessdata_dir)
|
|
||||||
return targets
|
return targets
|
||||||
|
|
||||||
|
|
||||||
|
@ -57,7 +55,7 @@ IMAGE_DEPS = [
|
||||||
*list_files("dangerzone/container_helpers"),
|
*list_files("dangerzone/container_helpers"),
|
||||||
"install/common/build-image.py",
|
"install/common/build-image.py",
|
||||||
]
|
]
|
||||||
IMAGE_TARGETS = ["share/container.tar.gz", "share/image-id.txt"]
|
IMAGE_TARGETS = ["share/container.tar", "share/image-id.txt"]
|
||||||
|
|
||||||
SOURCE_DEPS = [
|
SOURCE_DEPS = [
|
||||||
*list_files("assets"),
|
*list_files("assets"),
|
||||||
|
@ -124,7 +122,7 @@ def build_deb(cwd):
|
||||||
|
|
||||||
def build_rpm(version, cwd, qubes=False):
|
def build_rpm(version, cwd, qubes=False):
|
||||||
"""Build an .rpm package on the requested Fedora distro."""
|
"""Build an .rpm package on the requested Fedora distro."""
|
||||||
return build_linux_pkg(distro="Fedora", version=version, cwd=cwd, qubes=qubes)
|
return build_linux_pkg(distro="fedora", version=version, cwd=cwd, qubes=qubes)
|
||||||
|
|
||||||
|
|
||||||
### Tasks
|
### Tasks
|
||||||
|
@ -188,8 +186,8 @@ def task_download_tessdata():
|
||||||
|
|
||||||
def task_build_image():
|
def task_build_image():
|
||||||
"""Build the container image using ./install/common/build-image.py"""
|
"""Build the container image using ./install/common/build-image.py"""
|
||||||
img_src = "share/container.tar.gz"
|
img_src = "share/container.tar"
|
||||||
img_dst = RELEASE_DIR / f"container-{VERSION}-{ARCH}.tar.gz" # FIXME: Add arch
|
img_dst = RELEASE_DIR / f"container-{VERSION}-{ARCH}.tar" # FIXME: Add arch
|
||||||
img_id_src = "share/image-id.txt"
|
img_id_src = "share/image-id.txt"
|
||||||
img_id_dst = RELEASE_DIR / "image-id.txt" # FIXME: Add arch
|
img_id_dst = RELEASE_DIR / "image-id.txt" # FIXME: Add arch
|
||||||
|
|
||||||
|
@ -208,7 +206,7 @@ def task_build_image():
|
||||||
|
|
||||||
def task_poetry_install():
|
def task_poetry_install():
|
||||||
"""Setup the Poetry environment"""
|
"""Setup the Poetry environment"""
|
||||||
return {"actions": ["poetry install --sync"], "clean": ["poetry env remove --all"]}
|
return {"actions": ["poetry sync"], "clean": ["poetry env remove --all"]}
|
||||||
|
|
||||||
|
|
||||||
def task_macos_build_dmg():
|
def task_macos_build_dmg():
|
||||||
|
|
|
@ -1,20 +1,17 @@
|
||||||
import argparse
|
import argparse
|
||||||
import gzip
|
|
||||||
import platform
|
import platform
|
||||||
import secrets
|
import secrets
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
BUILD_CONTEXT = "dangerzone/"
|
BUILD_CONTEXT = "dangerzone"
|
||||||
IMAGE_NAME = "dangerzone.rocks/dangerzone"
|
IMAGE_NAME = "dangerzone.rocks/dangerzone"
|
||||||
if platform.system() in ["Darwin", "Windows"]:
|
if platform.system() in ["Darwin", "Windows"]:
|
||||||
CONTAINER_RUNTIME = "docker"
|
CONTAINER_RUNTIME = "docker"
|
||||||
elif platform.system() == "Linux":
|
elif platform.system() == "Linux":
|
||||||
CONTAINER_RUNTIME = "podman"
|
CONTAINER_RUNTIME = "podman"
|
||||||
|
|
||||||
ARCH = platform.machine()
|
|
||||||
|
|
||||||
|
|
||||||
def str2bool(v):
|
def str2bool(v):
|
||||||
if isinstance(v, bool):
|
if isinstance(v, bool):
|
||||||
|
@ -50,6 +47,16 @@ def determine_git_tag():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def determine_debian_archive_date():
|
||||||
|
"""Get the date of the Debian archive from Dockerfile.env."""
|
||||||
|
for env in Path("Dockerfile.env").read_text().split("\n"):
|
||||||
|
if env.startswith("DEBIAN_ARCHIVE_DATE"):
|
||||||
|
return env.split("=")[1]
|
||||||
|
raise Exception(
|
||||||
|
"Could not find 'DEBIAN_ARCHIVE_DATE' build argument in Dockerfile.env"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
|
@ -59,16 +66,15 @@ def main():
|
||||||
help=f"The container runtime for building the image (default: {CONTAINER_RUNTIME})",
|
help=f"The container runtime for building the image (default: {CONTAINER_RUNTIME})",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--no-save",
|
"--platform",
|
||||||
action="store_true",
|
default=None,
|
||||||
help="Do not save the container image as a tarball in share/container.tar.gz",
|
help=f"The platform for building the image (default: current platform)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--compress-level",
|
"--output",
|
||||||
type=int,
|
"-o",
|
||||||
choices=range(0, 10),
|
default=str(Path("share") / "container.tar"),
|
||||||
default=9,
|
help="Path to store the container image",
|
||||||
help="The Gzip compression level, from 0 (lowest) to 9 (highest, default)",
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--use-cache",
|
"--use-cache",
|
||||||
|
@ -83,63 +89,63 @@ def main():
|
||||||
default=None,
|
default=None,
|
||||||
help="Provide a custom tag for the image (for development only)",
|
help="Provide a custom tag for the image (for development only)",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--debian-archive-date",
|
||||||
|
"-d",
|
||||||
|
default=determine_debian_archive_date(),
|
||||||
|
help="Use a specific Debian snapshot archive, by its date (default %(default)s)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--dry",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="Do not run any commands, just print what would happen",
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
tarball_path = Path("share") / "container.tar.gz"
|
tag = args.tag or f"{args.debian_archive_date}-{determine_git_tag()}"
|
||||||
image_id_path = Path("share") / "image-id.txt"
|
image_name_tagged = f"{IMAGE_NAME}:{tag}"
|
||||||
|
|
||||||
print(f"Building for architecture '{ARCH}'")
|
|
||||||
|
|
||||||
tag = args.tag or determine_git_tag()
|
|
||||||
image_name_tagged = IMAGE_NAME + ":" + tag
|
|
||||||
|
|
||||||
print(f"Will tag the container image as '{image_name_tagged}'")
|
print(f"Will tag the container image as '{image_name_tagged}'")
|
||||||
with open(image_id_path, "w") as f:
|
image_id_path = Path("share") / "image-id.txt"
|
||||||
f.write(tag)
|
if not args.dry:
|
||||||
|
with open(image_id_path, "w") as f:
|
||||||
|
f.write(tag)
|
||||||
|
|
||||||
# Build the container image, and tag it with the calculated tag
|
# Build the container image, and tag it with the calculated 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"]
|
||||||
|
platform_args = [] if not args.platform else ["--platform", args.platform]
|
||||||
|
rootless_args = [] if args.runtime == "docker" else ["--rootless"]
|
||||||
|
rootless_args = []
|
||||||
|
dry_args = [] if not args.dry else ["--dry"]
|
||||||
|
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
[
|
[
|
||||||
args.runtime,
|
sys.executable,
|
||||||
|
str(Path("dev_scripts") / "repro-build.py"),
|
||||||
"build",
|
"build",
|
||||||
BUILD_CONTEXT,
|
"--runtime",
|
||||||
|
args.runtime,
|
||||||
|
"--build-arg",
|
||||||
|
f"DEBIAN_ARCHIVE_DATE={args.debian_archive_date}",
|
||||||
|
"--datetime",
|
||||||
|
args.debian_archive_date,
|
||||||
|
*dry_args,
|
||||||
*cache_args,
|
*cache_args,
|
||||||
"-f",
|
*platform_args,
|
||||||
"Dockerfile",
|
*rootless_args,
|
||||||
"--tag",
|
"--tag",
|
||||||
image_name_tagged,
|
image_name_tagged,
|
||||||
|
"--output",
|
||||||
|
args.output,
|
||||||
|
"-f",
|
||||||
|
"Dockerfile",
|
||||||
|
BUILD_CONTEXT,
|
||||||
],
|
],
|
||||||
check=True,
|
check=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not args.no_save:
|
|
||||||
print("Saving container image")
|
|
||||||
cmd = subprocess.Popen(
|
|
||||||
[
|
|
||||||
CONTAINER_RUNTIME,
|
|
||||||
"save",
|
|
||||||
image_name_tagged,
|
|
||||||
],
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
)
|
|
||||||
|
|
||||||
print("Compressing container image")
|
|
||||||
chunk_size = 4 << 20
|
|
||||||
with gzip.open(
|
|
||||||
tarball_path,
|
|
||||||
"wb",
|
|
||||||
compresslevel=args.compress_level,
|
|
||||||
) as gzip_f:
|
|
||||||
while True:
|
|
||||||
chunk = cmd.stdout.read(chunk_size)
|
|
||||||
if len(chunk) > 0:
|
|
||||||
gzip_f.write(chunk)
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
cmd.wait(5)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
|
|
@ -51,6 +51,8 @@ def main():
|
||||||
if files == expected_files:
|
if files == expected_files:
|
||||||
logger.info("Skipping tessdata download, language data already exists")
|
logger.info("Skipping tessdata download, language data already exists")
|
||||||
return
|
return
|
||||||
|
elif not files:
|
||||||
|
logger.info("Tesseract dir is empty, proceeding to download language data")
|
||||||
else:
|
else:
|
||||||
logger.info(f"Found {tessdata_dir} but contents do not match")
|
logger.info(f"Found {tessdata_dir} but contents do not match")
|
||||||
return 1
|
return 1
|
||||||
|
|
|
@ -66,14 +66,14 @@ def build(build_dir, qubes=False):
|
||||||
print("* Creating a Python sdist")
|
print("* Creating a Python sdist")
|
||||||
tessdata = root / "share" / "tessdata"
|
tessdata = root / "share" / "tessdata"
|
||||||
tessdata_bak = root / "tessdata.bak"
|
tessdata_bak = root / "tessdata.bak"
|
||||||
container_tar_gz = root / "share" / "container.tar.gz"
|
container_tar = root / "share" / "container.tar"
|
||||||
container_tar_gz_bak = root / "container.tar.gz.bak"
|
container_tar_bak = root / "container.tar.bak"
|
||||||
|
|
||||||
if tessdata.exists():
|
if tessdata.exists():
|
||||||
tessdata.rename(tessdata_bak)
|
tessdata.rename(tessdata_bak)
|
||||||
stash_container = qubes and container_tar_gz.exists()
|
stash_container = qubes and container_tar.exists()
|
||||||
if stash_container and container_tar_gz.exists():
|
if stash_container and container_tar.exists():
|
||||||
container_tar_gz.rename(container_tar_gz_bak)
|
container_tar.rename(container_tar_bak)
|
||||||
try:
|
try:
|
||||||
subprocess.run(["poetry", "build", "-f", "sdist"], cwd=root, check=True)
|
subprocess.run(["poetry", "build", "-f", "sdist"], cwd=root, check=True)
|
||||||
# Copy and unlink the Dangerzone sdist, instead of just renaming it. If the
|
# Copy and unlink the Dangerzone sdist, instead of just renaming it. If the
|
||||||
|
@ -84,8 +84,8 @@ def build(build_dir, qubes=False):
|
||||||
finally:
|
finally:
|
||||||
if tessdata_bak.exists():
|
if tessdata_bak.exists():
|
||||||
tessdata_bak.rename(tessdata)
|
tessdata_bak.rename(tessdata)
|
||||||
if stash_container and container_tar_gz_bak.exists():
|
if stash_container and container_tar_bak.exists():
|
||||||
container_tar_gz_bak.rename(container_tar_gz)
|
container_tar_bak.rename(container_tar)
|
||||||
|
|
||||||
print("* Building RPM package")
|
print("* Building RPM package")
|
||||||
cmd = [
|
cmd = [
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
#
|
#
|
||||||
# * Qubes packages include some extra files under /etc/qubes-rpc, whereas
|
# * Qubes packages include some extra files under /etc/qubes-rpc, whereas
|
||||||
# regular RPM packages include the container image under
|
# regular RPM packages include the container image under
|
||||||
# /usr/share/container.tar.gz
|
# /usr/share/container.tar
|
||||||
# * Qubes packages have some extra dependencies.
|
# * Qubes packages have some extra dependencies.
|
||||||
# 3. It is best to consume this SPEC file using the `install/linux/build-rpm.py`
|
# 3. It is best to consume this SPEC file using the `install/linux/build-rpm.py`
|
||||||
# script, which handles the necessary scaffolding for building the package.
|
# script, which handles the necessary scaffolding for building the package.
|
||||||
|
@ -32,7 +32,7 @@ Name: dangerzone-qubes
|
||||||
Name: dangerzone
|
Name: dangerzone
|
||||||
%endif
|
%endif
|
||||||
|
|
||||||
Version: 0.8.1
|
Version: 0.9.0
|
||||||
Release: 1%{?dist}
|
Release: 1%{?dist}
|
||||||
Summary: Take potentially dangerous PDFs, office documents, or images and convert them to safe PDFs
|
Summary: Take potentially dangerous PDFs, office documents, or images and convert them to safe PDFs
|
||||||
|
|
||||||
|
@ -216,12 +216,6 @@ convert the documents within a secure sandbox.
|
||||||
%prep
|
%prep
|
||||||
%autosetup -p1 -n dangerzone-%{version}
|
%autosetup -p1 -n dangerzone-%{version}
|
||||||
|
|
||||||
# Bypass the version pin for Fedora as the 6.8.1.1 package is causing trouble
|
|
||||||
# A 6.8.1.1 package was only released with a wheel for macOS, but was picked by
|
|
||||||
# Fedora packagers. We cannot use "*" when PyPI is involved as it will fail to download the latest version.
|
|
||||||
# For Fedora, we can pick any of the released versions.
|
|
||||||
sed -i '/shiboken6 = \[/,/\]/c\shiboken6 = "*"' pyproject.toml
|
|
||||||
|
|
||||||
%generate_buildrequires
|
%generate_buildrequires
|
||||||
%pyproject_buildrequires -R
|
%pyproject_buildrequires -R
|
||||||
|
|
||||||
|
|
|
@ -31,23 +31,6 @@ def main():
|
||||||
cmd = ["poetry", "export", "--only", "debian"]
|
cmd = ["poetry", "export", "--only", "debian"]
|
||||||
container_requirements_txt = subprocess.check_output(cmd)
|
container_requirements_txt = subprocess.check_output(cmd)
|
||||||
|
|
||||||
# XXX: Hack for Ubuntu Focal.
|
|
||||||
#
|
|
||||||
# The `requirements.txt` file is generated from our `pyproject.toml` file, and thus
|
|
||||||
# specifies that the minimum Python version is 3.9. This was to accommodate to
|
|
||||||
# PySide6, which is installed in macOS / Windows via `poetry` and works with Python
|
|
||||||
# 3.9+. [1]
|
|
||||||
#
|
|
||||||
# The Python version in Ubuntu Focal though is 3.8. This generally was not much of
|
|
||||||
# an issue, since we used the package manager to install dependencies. However, it
|
|
||||||
# becomes an issue when we want to vendor the PyMuPDF package, using `pip`. In order
|
|
||||||
# to sidestep this virtual limitation, we can just change the Python version in the
|
|
||||||
# generated `requirements.txt` file in Ubuntu Focal from 3.9 to 3.8.
|
|
||||||
#
|
|
||||||
# [1] https://github.com/freedomofpress/dangerzone/pull/818
|
|
||||||
if sys.version.startswith("3.8"):
|
|
||||||
container_requirements_txt = container_requirements_txt.replace(b"3.9", b"3.8")
|
|
||||||
|
|
||||||
logger.info(f"Vendoring PyMuPDF under '{args.dest}'")
|
logger.info(f"Vendoring PyMuPDF under '{args.dest}'")
|
||||||
# We prefer to call the CLI version of `pip`, instead of importing it directly, as
|
# We prefer to call the CLI version of `pip`, instead of importing it directly, as
|
||||||
# instructed here:
|
# instructed here:
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Development script for installing Podman on Ubuntu Focal. Mainly to be used as
|
|
||||||
# part of our CI pipelines, where we may install Podman on environments that
|
|
||||||
# don't have sudo.
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
if [[ "$EUID" -ne 0 ]]; then
|
|
||||||
SUDO=sudo
|
|
||||||
else
|
|
||||||
SUDO=
|
|
||||||
fi
|
|
||||||
|
|
||||||
provide() {
|
|
||||||
$SUDO apt-get update
|
|
||||||
$SUDO apt-get install curl wget gnupg2 -y
|
|
||||||
source /etc/os-release
|
|
||||||
$SUDO sh -c "echo 'deb http://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_${VERSION_ID}/ /' \
|
|
||||||
> /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list"
|
|
||||||
wget -nv https://download.opensuse.org/repositories/devel:kubic:libcontainers:stable/xUbuntu_${VERSION_ID}/Release.key -O- \
|
|
||||||
| $SUDO apt-key add -
|
|
||||||
$SUDO apt-get update -qq -y
|
|
||||||
}
|
|
||||||
|
|
||||||
install() {
|
|
||||||
$SUDO apt-get -qq --yes install podman
|
|
||||||
podman --version
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ "$1" == "--repo-only" ]]; then
|
|
||||||
provide
|
|
||||||
elif [[ "$1" == "" ]]; then
|
|
||||||
provide
|
|
||||||
install
|
|
||||||
else
|
|
||||||
echo "Unexpected argument: $1"
|
|
||||||
echo "Usage: $0 [--repo-only]"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
|
@ -193,7 +193,7 @@ def main():
|
||||||
Path="C:\\Program Files (x86)\\Dangerzone",
|
Path="C:\\Program Files (x86)\\Dangerzone",
|
||||||
)
|
)
|
||||||
ET.SubElement(directory_search_el, "FileSearch", Name="dangerzone.exe")
|
ET.SubElement(directory_search_el, "FileSearch", Name="dangerzone.exe")
|
||||||
registry_search_el = ET.SubElement(package_el, "Property", Id="DANGERZONE080FOUND")
|
registry_search_el = ET.SubElement(package_el, "Property", Id="DANGERZONE08FOUND")
|
||||||
ET.SubElement(
|
ET.SubElement(
|
||||||
registry_search_el,
|
registry_search_el,
|
||||||
"RegistrySearch",
|
"RegistrySearch",
|
||||||
|
@ -202,11 +202,19 @@ def main():
|
||||||
Name="DisplayName",
|
Name="DisplayName",
|
||||||
Type="raw",
|
Type="raw",
|
||||||
)
|
)
|
||||||
|
ET.SubElement(
|
||||||
|
registry_search_el,
|
||||||
|
"RegistrySearch",
|
||||||
|
Root="HKLM",
|
||||||
|
Key="SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{8AAC0808-3556-4164-9D15-6EC1FB673AB2}",
|
||||||
|
Name="DisplayName",
|
||||||
|
Type="raw",
|
||||||
|
)
|
||||||
ET.SubElement(
|
ET.SubElement(
|
||||||
package_el,
|
package_el,
|
||||||
"Launch",
|
"Launch",
|
||||||
Condition="NOT OLDDANGERZONEFOUND AND NOT DANGERZONE080FOUND",
|
Condition="NOT OLDDANGERZONEFOUND AND NOT DANGERZONE08FOUND",
|
||||||
Message="A previous version of [ProductName] is already installed. Please uninstall it from Programs and Features before proceeding with the installation.",
|
Message='A previous version of [ProductName] is already installed. Please uninstall it from "Apps & Features" before proceeding with the installation.',
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add the ProgramMenuFolder StandardDirectory
|
# Add the ProgramMenuFolder StandardDirectory
|
||||||
|
|
888
poetry.lock
generated
888
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "dangerzone"
|
name = "dangerzone"
|
||||||
version = "0.8.1"
|
version = "0.9.0"
|
||||||
description = "Take potentially dangerous PDFs, office documents, or images and convert them to safe PDFs"
|
description = "Take potentially dangerous PDFs, office documents, or images and convert them to safe PDFs"
|
||||||
authors = ["Freedom of the Press Foundation <info@freedom.press>", "Micah Lee <micah.lee@theintercept.com>"]
|
authors = ["Freedom of the Press Foundation <info@freedom.press>", "Micah Lee <micah.lee@theintercept.com>"]
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
|
@ -23,13 +23,6 @@ pyxdg = {version = "*", platform = "linux"}
|
||||||
requests = "*"
|
requests = "*"
|
||||||
markdown = "*"
|
markdown = "*"
|
||||||
packaging = "*"
|
packaging = "*"
|
||||||
# shiboken6 released a 6.8.1.1 version only for macOS
|
|
||||||
# and it's getting picked by poetry, so pin it instead.
|
|
||||||
shiboken6 = [
|
|
||||||
{version = "*", platform = "darwin"},
|
|
||||||
{version = "<6.8.1.1", platform = "linux"},
|
|
||||||
{version = "<6.8.1.1", platform = "win32"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
dangerzone = 'dangerzone:main'
|
dangerzone = 'dangerzone:main'
|
||||||
|
@ -65,9 +58,10 @@ pytest-cov = "^5.0.0"
|
||||||
strip-ansi = "*"
|
strip-ansi = "*"
|
||||||
pytest-subprocess = "^1.5.2"
|
pytest-subprocess = "^1.5.2"
|
||||||
pytest-rerunfailures = "^14.0"
|
pytest-rerunfailures = "^14.0"
|
||||||
|
numpy = "2.0" # bump when we remove python 3.9 support
|
||||||
|
|
||||||
[tool.poetry.group.debian.dependencies]
|
[tool.poetry.group.debian.dependencies]
|
||||||
pymupdf = "1.24.11" # Last version to support python 3.8 (needed for Ubuntu Focal support)
|
pymupdf = "^1.24.11"
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
httpx = "^0.27.2"
|
httpx = "^0.27.2"
|
||||||
|
|
|
@ -13,7 +13,7 @@ setup(
|
||||||
description="Dangerzone",
|
description="Dangerzone",
|
||||||
options={
|
options={
|
||||||
"build_exe": {
|
"build_exe": {
|
||||||
"packages": ["dangerzone", "dangerzone.gui"],
|
"packages": ["dangerzone", "dangerzone.gui", "pymupdf._wxcolors"],
|
||||||
"excludes": ["test", "tkinter"],
|
"excludes": ["test", "tkinter"],
|
||||||
"include_files": [("share", "share"), ("LICENSE", "LICENSE")],
|
"include_files": [("share", "share"), ("LICENSE", "LICENSE")],
|
||||||
"include_msvcr": True,
|
"include_msvcr": True,
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
0.8.1
|
0.9.0
|
||||||
|
|
|
@ -123,7 +123,7 @@ test_docs_compressed_dir = Path(__file__).parent.joinpath(SAMPLE_COMPRESSED_DIRE
|
||||||
|
|
||||||
test_docs = [
|
test_docs = [
|
||||||
p
|
p
|
||||||
for p in test_docs_dir.rglob("*")
|
for p in test_docs_dir.glob("*")
|
||||||
if p.is_file()
|
if p.is_file()
|
||||||
and not (p.name.endswith(SAFE_EXTENSION) or p.name.startswith("sample_bad"))
|
and not (p.name.endswith(SAFE_EXTENSION) or p.name.startswith("sample_bad"))
|
||||||
]
|
]
|
||||||
|
@ -162,3 +162,31 @@ def for_each_external_doc(glob_pattern: str = "*") -> Callable:
|
||||||
|
|
||||||
class TestBase:
|
class TestBase:
|
||||||
sample_doc = str(test_docs_dir.joinpath(BASIC_SAMPLE_PDF))
|
sample_doc = str(test_docs_dir.joinpath(BASIC_SAMPLE_PDF))
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_configure(config: pytest.Config) -> None:
|
||||||
|
config.addinivalue_line(
|
||||||
|
"markers",
|
||||||
|
"reference_generator: Used to mark the test cases that regenerate reference documents",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_addoption(parser: pytest.Parser) -> None:
|
||||||
|
parser.addoption(
|
||||||
|
"--generate-reference-pdfs",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Regenerate reference PDFs",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_collection_modifyitems(
|
||||||
|
config: pytest.Config, items: List[pytest.Item]
|
||||||
|
) -> None:
|
||||||
|
if not config.getoption("--generate-reference-pdfs"):
|
||||||
|
skip_generator = pytest.mark.skip(
|
||||||
|
reason="Only run when --generate-reference-pdfs is provided"
|
||||||
|
)
|
||||||
|
for item in items:
|
||||||
|
if "reference_generator" in item.keywords:
|
||||||
|
item.add_marker(skip_generator)
|
||||||
|
|
|
@ -21,34 +21,25 @@ def get_qt_app() -> Application:
|
||||||
|
|
||||||
def generate_isolated_updater(
|
def generate_isolated_updater(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
monkeypatch: MonkeyPatch,
|
mocker: MockerFixture,
|
||||||
app_mocker: Optional[MockerFixture] = None,
|
mock_app: bool = False,
|
||||||
) -> UpdaterThread:
|
) -> UpdaterThread:
|
||||||
"""Generate an Updater class with its own settings."""
|
"""Generate an Updater class with its own settings."""
|
||||||
if app_mocker:
|
app = mocker.MagicMock() if mock_app else get_qt_app()
|
||||||
app = app_mocker.MagicMock()
|
|
||||||
else:
|
|
||||||
app = get_qt_app()
|
|
||||||
|
|
||||||
dummy = Dummy()
|
dummy = Dummy()
|
||||||
# XXX: We can monkey-patch global state without wrapping it in a context manager, or
|
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
|
||||||
# worrying that it will leak between tests, for two reasons:
|
|
||||||
#
|
|
||||||
# 1. Parallel tests in PyTest take place in different processes.
|
|
||||||
# 2. The monkeypatch fixture tears down the monkey-patch after each test ends.
|
|
||||||
monkeypatch.setattr(util, "get_config_dir", lambda: tmp_path)
|
|
||||||
dangerzone = DangerzoneGui(app, isolation_provider=dummy)
|
dangerzone = DangerzoneGui(app, isolation_provider=dummy)
|
||||||
updater = UpdaterThread(dangerzone)
|
updater = UpdaterThread(dangerzone)
|
||||||
return updater
|
return updater
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def updater(
|
def updater(tmp_path: Path, mocker: MockerFixture) -> UpdaterThread:
|
||||||
tmp_path: Path, monkeypatch: MonkeyPatch, mocker: MockerFixture
|
return generate_isolated_updater(tmp_path, mocker, mock_app=True)
|
||||||
) -> UpdaterThread:
|
|
||||||
return generate_isolated_updater(tmp_path, monkeypatch, mocker)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def qt_updater(tmp_path: Path, monkeypatch: MonkeyPatch) -> UpdaterThread:
|
def qt_updater(tmp_path: Path, mocker: MockerFixture) -> UpdaterThread:
|
||||||
return generate_isolated_updater(tmp_path, monkeypatch)
|
return generate_isolated_updater(tmp_path, mocker, mock_app=False)
|
||||||
|
|
|
@ -50,9 +50,7 @@ def test_default_updater_settings(updater: UpdaterThread) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_pre_0_4_2_settings(
|
def test_pre_0_4_2_settings(tmp_path: Path, mocker: MockerFixture) -> None:
|
||||||
tmp_path: Path, monkeypatch: MonkeyPatch, mocker: MockerFixture
|
|
||||||
) -> None:
|
|
||||||
"""Check settings of installations prior to 0.4.2.
|
"""Check settings of installations prior to 0.4.2.
|
||||||
|
|
||||||
Check that installations that have been upgraded from a version < 0.4.2 to >= 0.4.2
|
Check that installations that have been upgraded from a version < 0.4.2 to >= 0.4.2
|
||||||
|
@ -60,7 +58,7 @@ def test_pre_0_4_2_settings(
|
||||||
in their settings.json file.
|
in their settings.json file.
|
||||||
"""
|
"""
|
||||||
save_settings(tmp_path, default_settings_0_4_1())
|
save_settings(tmp_path, default_settings_0_4_1())
|
||||||
updater = generate_isolated_updater(tmp_path, monkeypatch, mocker)
|
updater = generate_isolated_updater(tmp_path, mocker, mock_app=True)
|
||||||
assert (
|
assert (
|
||||||
updater.dangerzone.settings.get_updater_settings() == default_updater_settings()
|
updater.dangerzone.settings.get_updater_settings() == default_updater_settings()
|
||||||
)
|
)
|
||||||
|
@ -85,12 +83,10 @@ def test_post_0_4_2_settings(
|
||||||
# version is 0.4.3.
|
# version is 0.4.3.
|
||||||
expected_settings = default_updater_settings()
|
expected_settings = default_updater_settings()
|
||||||
expected_settings["updater_latest_version"] = "0.4.3"
|
expected_settings["updater_latest_version"] = "0.4.3"
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(settings, "get_version", lambda: "0.4.3")
|
||||||
settings, "get_version", lambda: expected_settings["updater_latest_version"]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Ensure that the Settings class will correct the latest version field to 0.4.3.
|
# Ensure that the Settings class will correct the latest version field to 0.4.3.
|
||||||
updater = generate_isolated_updater(tmp_path, monkeypatch, mocker)
|
updater = generate_isolated_updater(tmp_path, mocker, mock_app=True)
|
||||||
assert updater.dangerzone.settings.get_updater_settings() == expected_settings
|
assert updater.dangerzone.settings.get_updater_settings() == expected_settings
|
||||||
|
|
||||||
# Simulate an updater check that found a newer Dangerzone version (e.g., 0.4.4).
|
# Simulate an updater check that found a newer Dangerzone version (e.g., 0.4.4).
|
||||||
|
@ -120,9 +116,7 @@ def test_linux_no_check(updater: UpdaterThread, monkeypatch: MonkeyPatch) -> Non
|
||||||
assert updater.dangerzone.settings.get_updater_settings() == expected_settings
|
assert updater.dangerzone.settings.get_updater_settings() == expected_settings
|
||||||
|
|
||||||
|
|
||||||
def test_user_prompts(
|
def test_user_prompts(updater: UpdaterThread, mocker: MockerFixture) -> None:
|
||||||
updater: UpdaterThread, monkeypatch: MonkeyPatch, mocker: MockerFixture
|
|
||||||
) -> None:
|
|
||||||
"""Test prompting users to ask them if they want to enable update checks."""
|
"""Test prompting users to ask them if they want to enable update checks."""
|
||||||
settings = updater.dangerzone.settings
|
settings = updater.dangerzone.settings
|
||||||
# First run
|
# First run
|
||||||
|
@ -375,8 +369,6 @@ def test_update_errors(
|
||||||
def test_update_check_prompt(
|
def test_update_check_prompt(
|
||||||
qtbot: QtBot,
|
qtbot: QtBot,
|
||||||
qt_updater: UpdaterThread,
|
qt_updater: UpdaterThread,
|
||||||
monkeypatch: MonkeyPatch,
|
|
||||||
mocker: MockerFixture,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test that the prompt to enable update checks works properly."""
|
"""Test that the prompt to enable update checks works properly."""
|
||||||
# Force Dangerzone to check immediately for updates
|
# Force Dangerzone to check immediately for updates
|
||||||
|
|
|
@ -5,9 +5,11 @@ 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 import errors
|
||||||
|
from dangerzone.container_utils import Runtime
|
||||||
from dangerzone.isolation_provider.container import Container
|
from dangerzone.isolation_provider.container import Container
|
||||||
from dangerzone.isolation_provider.qubes import is_qubes_native_conversion
|
from dangerzone.isolation_provider.qubes import is_qubes_native_conversion
|
||||||
|
from dangerzone.util import get_resource_path
|
||||||
|
|
||||||
from .base import IsolationProviderTermination, IsolationProviderTest
|
from .base import IsolationProviderTermination, IsolationProviderTest
|
||||||
|
|
||||||
|
@ -23,42 +25,51 @@ def provider() -> Container:
|
||||||
return Container()
|
return Container()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def runtime_path() -> str:
|
||||||
|
return str(Runtime().path)
|
||||||
|
|
||||||
|
|
||||||
class TestContainer(IsolationProviderTest):
|
class TestContainer(IsolationProviderTest):
|
||||||
def test_is_available_raises(self, provider: Container, fp: FakeProcess) -> None:
|
def test_is_available_raises(
|
||||||
|
self, provider: Container, fp: FakeProcess, runtime_path: str
|
||||||
|
) -> 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"],
|
[runtime_path, "image", "ls"],
|
||||||
returncode=-1,
|
returncode=-1,
|
||||||
stderr="podman image ls logs",
|
stderr="podman image ls logs",
|
||||||
)
|
)
|
||||||
with pytest.raises(errors.NotAvailableContainerTechException):
|
with pytest.raises(errors.NotAvailableContainerTechException):
|
||||||
provider.is_available()
|
provider.is_available()
|
||||||
|
|
||||||
def test_is_available_works(self, provider: Container, fp: FakeProcess) -> None:
|
def test_is_available_works(
|
||||||
|
self, provider: Container, fp: FakeProcess, runtime_path: str
|
||||||
|
) -> 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"],
|
[runtime_path, "image", "ls"],
|
||||||
)
|
)
|
||||||
provider.is_available()
|
provider.is_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, provider: Container, fp: FakeProcess, runtime_path: str
|
||||||
) -> None:
|
) -> None:
|
||||||
"""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"],
|
[runtime_path, "image", "ls"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# First check should return nothing.
|
# First check should return nothing.
|
||||||
fp.register_subprocess(
|
fp.register_subprocess(
|
||||||
[
|
[
|
||||||
container_utils.get_runtime(),
|
runtime_path,
|
||||||
"image",
|
"image",
|
||||||
"list",
|
"list",
|
||||||
"--format",
|
"--format",
|
||||||
|
@ -68,11 +79,13 @@ class TestContainer(IsolationProviderTest):
|
||||||
occurrences=2,
|
occurrences=2,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Make podman load fail
|
|
||||||
mocker.patch("gzip.open", mocker.mock_open(read_data=""))
|
|
||||||
|
|
||||||
fp.register_subprocess(
|
fp.register_subprocess(
|
||||||
[container_utils.get_runtime(), "load"],
|
[
|
||||||
|
runtime_path,
|
||||||
|
"load",
|
||||||
|
"-i",
|
||||||
|
get_resource_path("container.tar").absolute(),
|
||||||
|
],
|
||||||
returncode=-1,
|
returncode=-1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -80,18 +93,22 @@ class TestContainer(IsolationProviderTest):
|
||||||
provider.install()
|
provider.install()
|
||||||
|
|
||||||
def test_install_raises_if_still_not_installed(
|
def test_install_raises_if_still_not_installed(
|
||||||
self, mocker: MockerFixture, provider: Container, fp: FakeProcess
|
self, provider: Container, fp: FakeProcess, runtime_path: str
|
||||||
) -> None:
|
) -> None:
|
||||||
"""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(
|
||||||
|
[runtime_path, "version", "-f", "{{.Client.Version}}"],
|
||||||
|
stdout="4.0.0",
|
||||||
|
)
|
||||||
|
|
||||||
fp.register_subprocess(
|
fp.register_subprocess(
|
||||||
[container_utils.get_runtime(), "image", "ls"],
|
[runtime_path, "image", "ls"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# First check should return nothing.
|
# First check should return nothing.
|
||||||
fp.register_subprocess(
|
fp.register_subprocess(
|
||||||
[
|
[
|
||||||
container_utils.get_runtime(),
|
runtime_path,
|
||||||
"image",
|
"image",
|
||||||
"list",
|
"list",
|
||||||
"--format",
|
"--format",
|
||||||
|
@ -101,10 +118,13 @@ class TestContainer(IsolationProviderTest):
|
||||||
occurrences=2,
|
occurrences=2,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Patch gzip.open and podman load so that it works
|
|
||||||
mocker.patch("gzip.open", mocker.mock_open(read_data=""))
|
|
||||||
fp.register_subprocess(
|
fp.register_subprocess(
|
||||||
[container_utils.get_runtime(), "load"],
|
[
|
||||||
|
runtime_path,
|
||||||
|
"load",
|
||||||
|
"-i",
|
||||||
|
get_resource_path("container.tar").absolute(),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
with pytest.raises(errors.ImageNotPresentException):
|
with pytest.raises(errors.ImageNotPresentException):
|
||||||
provider.install()
|
provider.install()
|
||||||
|
@ -191,7 +211,7 @@ class TestContainer(IsolationProviderTest):
|
||||||
reason="Linux specific",
|
reason="Linux specific",
|
||||||
)
|
)
|
||||||
def test_linux_skips_desktop_version_check_returns_true(
|
def test_linux_skips_desktop_version_check_returns_true(
|
||||||
self, mocker: MockerFixture, provider: Container
|
self, provider: Container
|
||||||
) -> None:
|
) -> None:
|
||||||
assert (True, "") == provider.check_docker_desktop_version()
|
assert (True, "") == provider.check_docker_desktop_version()
|
||||||
|
|
||||||
|
|
|
@ -7,10 +7,13 @@ import platform
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Sequence
|
from typing import Optional, Sequence
|
||||||
|
|
||||||
|
import fitz
|
||||||
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
from click.testing import CliRunner, Result
|
from click.testing import CliRunner, Result
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
|
@ -190,11 +193,68 @@ class TestCliConversion(TestCliBasic):
|
||||||
result = self.run_cli([sample_pdf, "--ocr-lang", "piglatin"])
|
result = self.run_cli([sample_pdf, "--ocr-lang", "piglatin"])
|
||||||
result.assert_failure()
|
result.assert_failure()
|
||||||
|
|
||||||
|
@pytest.mark.reference_generator
|
||||||
@for_each_doc
|
@for_each_doc
|
||||||
def test_formats(self, doc: Path) -> None:
|
def test_regenerate_reference(self, doc: Path) -> None:
|
||||||
result = self.run_cli(str(doc))
|
reference = (doc.parent / "reference" / doc.stem).with_suffix(".pdf")
|
||||||
|
|
||||||
|
result = self.run_cli([str(doc), "--output-filename", str(reference)])
|
||||||
result.assert_success()
|
result.assert_success()
|
||||||
|
|
||||||
|
@for_each_doc
|
||||||
|
def test_formats(self, doc: Path, tmp_path_factory: pytest.TempPathFactory) -> None:
|
||||||
|
reference = (doc.parent / "reference" / doc.stem).with_suffix(".pdf")
|
||||||
|
destination = tmp_path_factory.mktemp(doc.stem).with_suffix(".pdf")
|
||||||
|
|
||||||
|
result = self.run_cli([str(doc), "--output-filename", str(destination)])
|
||||||
|
result.assert_success()
|
||||||
|
|
||||||
|
# Do not check against reference versions when using a dummy isolation provider
|
||||||
|
if os.environ.get("DUMMY_CONVERSION", False):
|
||||||
|
return
|
||||||
|
|
||||||
|
converted = fitz.open(destination)
|
||||||
|
ref = fitz.open(reference)
|
||||||
|
errors = []
|
||||||
|
if len(converted) != len(ref):
|
||||||
|
errors.append("different number of pages")
|
||||||
|
|
||||||
|
diffs = doc.parent / "diffs"
|
||||||
|
diffs.mkdir(parents=True, exist_ok=True)
|
||||||
|
for page, ref_page in zip(converted, ref):
|
||||||
|
curr_pixmap = page.get_pixmap(dpi=150)
|
||||||
|
ref_pixmap = ref_page.get_pixmap(dpi=150)
|
||||||
|
if curr_pixmap.tobytes() != ref_pixmap.tobytes():
|
||||||
|
errors.append(f"page {page.number} differs")
|
||||||
|
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
|
||||||
|
arr_ref = np.frombuffer(ref_pixmap.samples, dtype=np.uint8).reshape(
|
||||||
|
ref_pixmap.height, ref_pixmap.width, ref_pixmap.n
|
||||||
|
)
|
||||||
|
arr_curr = np.frombuffer(curr_pixmap.samples, dtype=np.uint8).reshape(
|
||||||
|
curr_pixmap.height, curr_pixmap.width, curr_pixmap.n
|
||||||
|
)
|
||||||
|
|
||||||
|
# Find differences (any channel differs)
|
||||||
|
diff = (arr_ref != arr_curr).any(axis=2)
|
||||||
|
|
||||||
|
# Get coordinates of differences
|
||||||
|
diff_coords = np.where(diff)
|
||||||
|
# Mark differences in red
|
||||||
|
for y, x in zip(diff_coords[0], diff_coords[1]):
|
||||||
|
# Note: PyMuPDF's set_pixel takes (x, y) not (y, x)
|
||||||
|
ref_pixmap.set_pixel(int(x), int(y), (255, 0, 0)) # Red
|
||||||
|
|
||||||
|
t1 = time.perf_counter()
|
||||||
|
print(f"diff took {t1 - t0} seconds")
|
||||||
|
ref_pixmap.save(diffs / f"{destination.stem}_{page.number}.jpeg")
|
||||||
|
|
||||||
|
if len(errors) > 0:
|
||||||
|
raise AssertionError(
|
||||||
|
f"The resulting document differs from the reference. See {str(diffs)} for a visual diff."
|
||||||
|
)
|
||||||
|
|
||||||
def test_output_filename(self, sample_pdf: str) -> None:
|
def test_output_filename(self, sample_pdf: str) -> None:
|
||||||
temp_dir = tempfile.mkdtemp(prefix="dangerzone-")
|
temp_dir = tempfile.mkdtemp(prefix="dangerzone-")
|
||||||
output_filename = str(Path(temp_dir) / "safe.pdf")
|
output_filename = str(Path(temp_dir) / "safe.pdf")
|
||||||
|
|
60
tests/test_container_utils.py
Normal file
60
tests/test_container_utils.py
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
|
from dangerzone import errors
|
||||||
|
from dangerzone.container_utils import Runtime
|
||||||
|
from dangerzone.settings import Settings
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_runtime_name_from_settings(mocker: MockerFixture, tmp_path: Path) -> None:
|
||||||
|
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
|
||||||
|
mocker.patch("dangerzone.container_utils.Path.exists", return_value=True)
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
|
settings.set("container_runtime", "/opt/somewhere/docker", autosave=True)
|
||||||
|
|
||||||
|
assert Runtime().name == "docker"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_runtime_name_linux(mocker: MockerFixture, tmp_path: Path) -> None:
|
||||||
|
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
|
||||||
|
mocker.patch("platform.system", return_value="Linux")
|
||||||
|
mocker.patch(
|
||||||
|
"dangerzone.container_utils.shutil.which", return_value="/usr/bin/podman"
|
||||||
|
)
|
||||||
|
mocker.patch("dangerzone.container_utils.os.path.exists", return_value=True)
|
||||||
|
runtime = Runtime()
|
||||||
|
assert runtime.name == "podman"
|
||||||
|
assert runtime.path == Path("/usr/bin/podman")
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_runtime_name_non_linux(mocker: MockerFixture, tmp_path: Path) -> None:
|
||||||
|
mocker.patch("platform.system", return_value="Windows")
|
||||||
|
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
|
||||||
|
mocker.patch(
|
||||||
|
"dangerzone.container_utils.shutil.which", return_value="/usr/bin/docker"
|
||||||
|
)
|
||||||
|
mocker.patch("dangerzone.container_utils.os.path.exists", return_value=True)
|
||||||
|
runtime = Runtime()
|
||||||
|
assert runtime.name == "docker"
|
||||||
|
assert runtime.path == Path("/usr/bin/docker")
|
||||||
|
|
||||||
|
mocker.patch("platform.system", return_value="Something else")
|
||||||
|
|
||||||
|
runtime = Runtime()
|
||||||
|
assert runtime.name == "docker"
|
||||||
|
assert runtime.path == Path("/usr/bin/docker")
|
||||||
|
assert Runtime().name == "docker"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_unsupported_runtime_name(mocker: MockerFixture, tmp_path: Path) -> None:
|
||||||
|
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
|
||||||
|
settings = Settings()
|
||||||
|
settings.set(
|
||||||
|
"container_runtime", "/opt/somewhere/new-kid-on-the-block", autosave=True
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(errors.UnsupportedContainerRuntime):
|
||||||
|
assert Runtime().name == "new-kid-on-the-block"
|
BIN
tests/test_docs/reference/sample-bmp.pdf
Normal file
BIN
tests/test_docs/reference/sample-bmp.pdf
Normal file
Binary file not shown.
BIN
tests/test_docs/reference/sample-doc.pdf
Normal file
BIN
tests/test_docs/reference/sample-doc.pdf
Normal file
Binary file not shown.
BIN
tests/test_docs/reference/sample-docm.pdf
Normal file
BIN
tests/test_docs/reference/sample-docm.pdf
Normal file
Binary file not shown.
BIN
tests/test_docs/reference/sample-docx.pdf
Normal file
BIN
tests/test_docs/reference/sample-docx.pdf
Normal file
Binary file not shown.
BIN
tests/test_docs/reference/sample-epub.pdf
Normal file
BIN
tests/test_docs/reference/sample-epub.pdf
Normal file
Binary file not shown.
BIN
tests/test_docs/reference/sample-gif.pdf
Normal file
BIN
tests/test_docs/reference/sample-gif.pdf
Normal file
Binary file not shown.
BIN
tests/test_docs/reference/sample-jpg.pdf
Normal file
BIN
tests/test_docs/reference/sample-jpg.pdf
Normal file
Binary file not shown.
BIN
tests/test_docs/reference/sample-mime-application-zip.pdf
Normal file
BIN
tests/test_docs/reference/sample-mime-application-zip.pdf
Normal file
Binary file not shown.
BIN
tests/test_docs/reference/sample-mime-octet-stream.pdf
Normal file
BIN
tests/test_docs/reference/sample-mime-octet-stream.pdf
Normal file
Binary file not shown.
BIN
tests/test_docs/reference/sample-mime-spreadsheet-template.pdf
Normal file
BIN
tests/test_docs/reference/sample-mime-spreadsheet-template.pdf
Normal file
Binary file not shown.
BIN
tests/test_docs/reference/sample-mime-text-template.pdf
Normal file
BIN
tests/test_docs/reference/sample-mime-text-template.pdf
Normal file
Binary file not shown.
BIN
tests/test_docs/reference/sample-mime-x-ole-storage.pdf
Normal file
BIN
tests/test_docs/reference/sample-mime-x-ole-storage.pdf
Normal file
Binary file not shown.
BIN
tests/test_docs/reference/sample-odg.pdf
Normal file
BIN
tests/test_docs/reference/sample-odg.pdf
Normal file
Binary file not shown.
BIN
tests/test_docs/reference/sample-odp.pdf
Normal file
BIN
tests/test_docs/reference/sample-odp.pdf
Normal file
Binary file not shown.
BIN
tests/test_docs/reference/sample-ods.pdf
Normal file
BIN
tests/test_docs/reference/sample-ods.pdf
Normal file
Binary file not shown.
BIN
tests/test_docs/reference/sample-odt-mp4.pdf
Normal file
BIN
tests/test_docs/reference/sample-odt-mp4.pdf
Normal file
Binary file not shown.
BIN
tests/test_docs/reference/sample-odt.pdf
Normal file
BIN
tests/test_docs/reference/sample-odt.pdf
Normal file
Binary file not shown.
BIN
tests/test_docs/reference/sample-pbm.pdf
Normal file
BIN
tests/test_docs/reference/sample-pbm.pdf
Normal file
Binary file not shown.
BIN
tests/test_docs/reference/sample-pdf.pdf
Normal file
BIN
tests/test_docs/reference/sample-pdf.pdf
Normal file
Binary file not shown.
BIN
tests/test_docs/reference/sample-png.pdf
Normal file
BIN
tests/test_docs/reference/sample-png.pdf
Normal file
Binary file not shown.
BIN
tests/test_docs/reference/sample-pnm.pdf
Normal file
BIN
tests/test_docs/reference/sample-pnm.pdf
Normal file
Binary file not shown.
BIN
tests/test_docs/reference/sample-ppm.pdf
Normal file
BIN
tests/test_docs/reference/sample-ppm.pdf
Normal file
Binary file not shown.
BIN
tests/test_docs/reference/sample-ppt.pdf
Normal file
BIN
tests/test_docs/reference/sample-ppt.pdf
Normal file
Binary file not shown.
BIN
tests/test_docs/reference/sample-pptx.pdf
Normal file
BIN
tests/test_docs/reference/sample-pptx.pdf
Normal file
Binary file not shown.
BIN
tests/test_docs/reference/sample-svg.pdf
Normal file
BIN
tests/test_docs/reference/sample-svg.pdf
Normal file
Binary file not shown.
BIN
tests/test_docs/reference/sample-xls.pdf
Normal file
BIN
tests/test_docs/reference/sample-xls.pdf
Normal file
Binary file not shown.
BIN
tests/test_docs/reference/sample-xlsx.pdf
Normal file
BIN
tests/test_docs/reference/sample-xlsx.pdf
Normal file
Binary file not shown.
|
@ -1,5 +1,4 @@
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import PropertyMock
|
from unittest.mock import PropertyMock
|
||||||
|
|
||||||
|
@ -22,13 +21,6 @@ def default_settings_0_4_1() -> dict:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def settings(tmp_path: Path, mocker: MockerFixture) -> Settings:
|
|
||||||
dz_core = mocker.MagicMock()
|
|
||||||
type(dz_core).appdata_path = PropertyMock(return_value=tmp_path)
|
|
||||||
return Settings(dz_core)
|
|
||||||
|
|
||||||
|
|
||||||
def save_settings(tmp_path: Path, settings: dict) -> None:
|
def save_settings(tmp_path: Path, settings: dict) -> None:
|
||||||
"""Mimic the way Settings save a dictionary to a settings.json file."""
|
"""Mimic the way Settings save a dictionary to a settings.json file."""
|
||||||
settings_filename = tmp_path / "settings.json"
|
settings_filename = tmp_path / "settings.json"
|
||||||
|
@ -36,26 +28,31 @@ def save_settings(tmp_path: Path, settings: dict) -> None:
|
||||||
json.dump(settings, settings_file, indent=4)
|
json.dump(settings, settings_file, indent=4)
|
||||||
|
|
||||||
|
|
||||||
def test_no_settings_file_creates_new_one(settings: Settings) -> None:
|
def test_no_settings_file_creates_new_one(
|
||||||
|
tmp_path: Path,
|
||||||
|
mocker: MockerFixture,
|
||||||
|
) -> None:
|
||||||
"""Default settings file is created on first run"""
|
"""Default settings file is created on first run"""
|
||||||
assert os.path.isfile(settings.settings_filename)
|
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
|
||||||
new_settings_dict = json.load(open(settings.settings_filename))
|
settings = Settings()
|
||||||
assert sorted(new_settings_dict.items()) == sorted(
|
|
||||||
settings.generate_default_settings().items()
|
assert settings.settings_filename.is_file()
|
||||||
)
|
with settings.settings_filename.open() as settings_file:
|
||||||
|
new_settings_dict = json.load(settings_file)
|
||||||
|
assert sorted(new_settings_dict.items()) == sorted(
|
||||||
|
settings.generate_default_settings().items()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_corrupt_settings(tmp_path: Path, mocker: MockerFixture) -> None:
|
def test_corrupt_settings(tmp_path: Path, mocker: MockerFixture) -> None:
|
||||||
# Set some broken settings file
|
# Set some broken settings file
|
||||||
corrupt_settings_dict = "{:}"
|
corrupt_settings_dict = "{:}"
|
||||||
with open(tmp_path / SETTINGS_FILENAME, "w") as settings_file:
|
with (tmp_path / SETTINGS_FILENAME).open("w") as settings_file:
|
||||||
settings_file.write(corrupt_settings_dict)
|
settings_file.write(corrupt_settings_dict)
|
||||||
|
|
||||||
# Initialize settings
|
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
|
||||||
dz_core = mocker.MagicMock()
|
settings = Settings()
|
||||||
type(dz_core).appdata_path = PropertyMock(return_value=tmp_path)
|
assert settings.settings_filename.is_file()
|
||||||
settings = Settings(dz_core)
|
|
||||||
assert os.path.isfile(settings.settings_filename)
|
|
||||||
|
|
||||||
# Check if settings file was reset to the default
|
# Check if settings file was reset to the default
|
||||||
new_settings_dict = json.load(open(settings.settings_filename))
|
new_settings_dict = json.load(open(settings.settings_filename))
|
||||||
|
@ -66,10 +63,7 @@ def test_corrupt_settings(tmp_path: Path, mocker: MockerFixture) -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_new_default_setting(tmp_path: Path, mocker: MockerFixture) -> None:
|
def test_new_default_setting(tmp_path: Path, mocker: MockerFixture) -> None:
|
||||||
# Initialize settings
|
settings = Settings()
|
||||||
dz_core = mocker.MagicMock()
|
|
||||||
type(dz_core).appdata_path = PropertyMock(return_value=tmp_path)
|
|
||||||
settings = Settings(dz_core)
|
|
||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
# Ensure new default setting is imported into settings
|
# Ensure new default setting is imported into settings
|
||||||
|
@ -78,15 +72,12 @@ def test_new_default_setting(tmp_path: Path, mocker: MockerFixture) -> None:
|
||||||
return_value={"mock_setting": 1},
|
return_value={"mock_setting": 1},
|
||||||
)
|
)
|
||||||
|
|
||||||
settings2 = Settings(dz_core)
|
settings2 = Settings()
|
||||||
assert settings2.get("mock_setting") == 1
|
assert settings2.get("mock_setting") == 1
|
||||||
|
|
||||||
|
|
||||||
def test_new_settings_added(tmp_path: Path, mocker: MockerFixture) -> None:
|
def test_new_settings_added(tmp_path: Path, mocker: MockerFixture) -> None:
|
||||||
# Initialize settings
|
settings = Settings()
|
||||||
dz_core = mocker.MagicMock()
|
|
||||||
type(dz_core).appdata_path = PropertyMock(return_value=tmp_path)
|
|
||||||
settings = Settings(dz_core)
|
|
||||||
|
|
||||||
# Add new setting
|
# Add new setting
|
||||||
settings.set("new_setting_autosaved", 20, autosave=True)
|
settings.set("new_setting_autosaved", 20, autosave=True)
|
||||||
|
@ -95,7 +86,7 @@ def test_new_settings_added(tmp_path: Path, mocker: MockerFixture) -> None:
|
||||||
) # XXX has to be afterwards; otherwise this will be saved
|
) # XXX has to be afterwards; otherwise this will be saved
|
||||||
|
|
||||||
# Simulate new app startup (settings recreation)
|
# Simulate new app startup (settings recreation)
|
||||||
settings2 = Settings(dz_core)
|
settings2 = Settings()
|
||||||
|
|
||||||
# Check if new setting persisted
|
# Check if new setting persisted
|
||||||
assert 20 == settings2.get("new_setting_autosaved")
|
assert 20 == settings2.get("new_setting_autosaved")
|
||||||
|
|
|
@ -265,6 +265,68 @@ def test_stores_signatures_updates_last_log_index(valid_signature, mocker, tmp_p
|
||||||
return_value=100,
|
return_value=100,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Call store_signatures
|
||||||
|
with pytest.raises(errors.SignatureMismatch):
|
||||||
|
store_signatures(signatures, image_digest, TEST_PUBKEY_PATH)
|
||||||
|
("dangerzone.updater.signatures.get_last_log_index",)
|
||||||
|
# Verify that the signatures file was not created
|
||||||
|
assert not (signatures_path / f"{image_digest}.json").exists()
|
||||||
|
|
||||||
|
# Verify that the log index file was not updated
|
||||||
|
assert not (signatures_path / "last_log_index").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_stores_signatures_updates_last_log_index(valid_signature, mocker, tmp_path):
|
||||||
|
"""Test that store_signatures updates the last log index file."""
|
||||||
|
signatures = [valid_signature]
|
||||||
|
# Extract the digest from the signature
|
||||||
|
image_digest = Signature(valid_signature).manifest_digest
|
||||||
|
signatures = [valid_signature, signature_other_digest]
|
||||||
|
breakpoint()
|
||||||
|
valid_signature, signature_other_digest, mocker, tmp_path
|
||||||
|
|
||||||
|
"""Test that store_signatures raises an error when a signature's digest doesn't match."""
|
||||||
|
|
||||||
|
image_digest = "sha256:123456"
|
||||||
|
|
||||||
|
# Mock the signatures path
|
||||||
|
signatures_path = tmp_path / "signatures"
|
||||||
|
signatures_path.mkdir()
|
||||||
|
mocker.patch("dangerzone.updater.signatures.SIGNATURES_PATH", signatures_path)
|
||||||
|
|
||||||
|
# Mock get_log_index_from_signatures
|
||||||
|
mocker.patch(
|
||||||
|
"dangerzone.updater.signatures.get_log_index_from_signatures",
|
||||||
|
return_value=100,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mock get_last_log_index
|
||||||
|
mocker.patch(
|
||||||
|
"dangerzone.updater.signatures.get_last_log_index",
|
||||||
|
return_value=50,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stores_signatures_updates_last_log_index():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_file_digest():
|
||||||
|
# Mock the signatures path
|
||||||
|
signatures_path = tmp_path / "signatures"
|
||||||
|
signatures_path.mkdir()
|
||||||
|
mocker.patch("dangerzone.updater.signatures.SIGNATURES_PATH", signatures_path)
|
||||||
|
|
||||||
|
# Create an existing last_log_index file with a lower value
|
||||||
|
with open(signatures_path / "last_log_index", "w") as f:
|
||||||
|
f.write("50")
|
||||||
|
|
||||||
|
# Mock get_log_index_from_signatures to return a higher value
|
||||||
|
mocker.patch(
|
||||||
|
"dangerzone.updater.signatures.get_log_index_from_signatures",
|
||||||
|
return_value=100,
|
||||||
|
)
|
||||||
|
|
||||||
# Call store_signatures
|
# Call store_signatures
|
||||||
store_signatures(signatures, image_digest, TEST_PUBKEY_PATH)
|
store_signatures(signatures, image_digest, TEST_PUBKEY_PATH)
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ VERSION_FILE_NAME = "version.txt"
|
||||||
|
|
||||||
def test_get_resource_path() -> None:
|
def test_get_resource_path() -> None:
|
||||||
share_dir = Path("share").resolve()
|
share_dir = Path("share").resolve()
|
||||||
resource_path = Path(util.get_resource_path(VERSION_FILE_NAME)).parent
|
resource_path = util.get_resource_path(VERSION_FILE_NAME).parent
|
||||||
assert share_dir.samefile(resource_path), (
|
assert share_dir.samefile(resource_path), (
|
||||||
f"{share_dir} is not the same file as {resource_path}"
|
f"{share_dir} is not the same file as {resource_path}"
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue