Skip to content

Provide *-full packages for Linux, and remove container.tar from default ones#1423

Open
almet wants to merge 22 commits into
mainfrom
1069-dangerzone-slim-packages
Open

Provide *-full packages for Linux, and remove container.tar from default ones#1423
almet wants to merge 22 commits into
mainfrom
1069-dangerzone-slim-packages

Conversation

@almet
Copy link
Copy Markdown
Member

@almet almet commented Feb 11, 2026

This removes the container.tar from the Linux packages, providing -full variants for the versions with the container.

This doesn't currently change how windows and macOS installers are created. Currently they continue shipping with the container.tar inside.

@almet almet changed the title Remove container.tar from dangerzone package Provide *-full packages for Linux, and remove container.tar from default ones Feb 12, 2026
Comment thread dangerzone/gui/updater.py Outdated
Comment thread dangerzone/gui/updater.py Outdated
Comment thread dangerzone/updater/releases.py Outdated
Comment thread dangerzone/cli.py
Comment thread install/linux/build-deb.py Outdated
@almet almet force-pushed the 1069-dangerzone-slim-packages branch 2 times, most recently from b4a9169 to 4f5fdfc Compare April 29, 2026 10:02
@almet almet force-pushed the 1069-dangerzone-slim-packages branch 3 times, most recently from 3225051 to 067c4bd Compare May 4, 2026 12:52
almet added 15 commits May 4, 2026 19:31
The conversion code that used to live in this repo now lives in
dangerzone-image. Drop the tests/test_docs_large submodule (moved to
dangerzone-image).
Add the possibility to generate a dangerzone-slim RPM that does not
include the container.tar file. Built with:

    ./install/linux/build-rpm.py --slim
Provide a dangerzone-slim Debian package alongside the full one. The
slim variant ships without container.tar; the container image is
fetched at runtime.

This commit also reshapes the Debian build flow so that debian/rules
drives the packaging (via debian/control), rather than carrying the
full packaging logic inside install/linux/build-deb.py. The Python
script is now a thin driver around dpkg-buildpackage.
Teach the doit task graph about the slim/full split: separate tasks
for building each flavor, correct dependencies between them, and
updated developer docs (docs/developer/doit.md).
Make slim the default on Linux: the dangerzone package no longer
bundles container.tar, and a new dangerzone-full package provides
the bundled-image variant for users who want the legacy behavior.

Updates the RPM spec, Debian control, build scripts, installer docs,
and the updater's expected install paths so they all agree on the new
layout. The Windows installer is adjusted to match.

build-rpm.py preserves artifacts of the other variant in dist/, so a
slim+full build sequence doesn't lose the first artifact. debian/rules
folds the python3-dangerzone pybuild staging tree into the slim deb so
that /usr/bin/dangerzone-image and the other console_scripts ship
with the package. dodo.py builds both .debs in a single
dpkg-buildpackage run.
When the packaged image is absent (slim install), the GUI now:

- Detects that the first-run sandbox download is needed and prompts
  the user for it via the updater flow, with a welcoming first-run
  message.
- Surfaces a clear error state if the download is refused or fails,
  rather than crashing deep in the isolation provider.
- Threads a NoContainerImageError through startup.py so the main
  window can render a recoverable error.
- Suppresses the redundant "A new version of the secure sandbox is
  available..." prompt right after the user has just consented to the
  first-run download (one-shot ContainerInstallTask.skip_prompt_once;
  the persisted updater_ask_before_download setting is unchanged).
- Does not hang on shutdown when the user declines the initial
  download: Runner.run honours raise_on_error=False for
  UpdaterDisabledNoContainer so the QThread can finish cleanly.
The CLI, unlike the GUI, cannot prompt the user to download the
image. If no image is available, exit with a clear error message
pointing at dangerzone-image rather than failing opaquely during
conversion.
On Linux there is no Podman virtual machine to manage, so logging
machine init/start/stop at INFO just clutters the user-visible
output. Drop those messages to DEBUG, and keep INFO for Windows and
macOS where the machine exists.
The apt-tools-prod and yum-tools-prod repos moved to
github.com/freedomofpress/packages. Update release/build docs to
point there.
Add jobs that build and install the new package variants, fetch the
container image via dangerzone-image, and run a smoke conversion
through dangerzone-cli. Build/upload steps are reordered so each
build is immediately followed by its upload.

Clarify in build.md that build-deb.py produces both the slim
(default) and -full .deb in a single run, and that both .rpms must
be published to the packages repo.
Document the slim/full split, the new dangerzone-slim packages, and
the dangerzone-cli breaking change in CHANGELOG.md.
- Mount ~/.config/dangerzone in env.py so settings.json (and the
  updater_check_all toggle) survives container restarts.
- Build the dev image automatically when it is missing, instead of
  failing with an opaque "image not found".
- Apply ruff format to dev_scripts/env.py.
Pin SOURCE_DATE_EPOCH to the latest debian/changelog entry and clamp
file mtimes in the staging trees so two builds of the same source
produce byte-identical .debs. Document the verification steps in
docs/developer/reproducibility.md.
Rather than just checking LAST_LOG_INDEX, also ensure that the
podman image is present on the system. Otherwise, deleting the local
image while LAST_LOG_INDEX is set leaves the GUI thinking the
sandbox is installed.
Microsoft's WSL update servers intermittently return HTTP 403, which
fails CI on a step that has no relevant signal for the dangerzone
code. Retry up to five times with a short backoff before giving up.
@almet almet force-pushed the 1069-dangerzone-slim-packages branch from 067c4bd to f578304 Compare May 4, 2026 17:34
Comment thread .github/workflows/ci.yml Outdated
Comment thread .github/workflows/ci.yml Outdated
./dev_scripts/env.py --distro ${{ matrix.distro }} --version ${{ matrix.version }} \
run dangerzone --help

install-rpm-full:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I mentioned in https://github.com/freedomofpress/dangerzone/pull/1423/changes#r3219532167 also applies here, with the added benefit that:

  1. This job will no longer need to wait for build-install-rpm to finish.
  2. Having a variant: "full" option in the matrix will help build just the packages we need per job, which means we will build the full variant for just one of our the Fedora versions (Fedora 43 works fine).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm actually not sure we want this here. For Debian, because the packages is built once and installed everywhere the story is a bit different, but since here we need to build on each system, I believe we could just build the -full variant and try it on each system.

We could try to depend on some more specific jobs to finish, but I'm not sure the benefit would be worth the cost. Having things run in a separate job is useful to ensure we're not using the local caches (when testing that conversions work)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually implemented this as 430f317

Comment thread dangerzone/gui/updater.py Outdated
Comment thread debian/control Outdated
Comment on lines +708 to +709
req.reply(False)
QtCore.QTimer.singleShot(0, self._handle_download_declined)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did we use this method of exiting the app versus self.begin_shutdown(ret=2), which is used by the other tasks? I recall that there were some weird Python tracebacks if we didn't exit gracefully.

Comment thread dangerzone/gui/updater.py Outdated
Comment thread dangerzone/updater/releases.py
Comment thread dangerzone/startup.py Outdated
Comment on lines +246 to +268
except updater_errors.NeedUserInput as e:
download_required = isinstance(e, updater_errors.NeedUserInputNoContainer)
accepted = self.prompt_user(download_required=download_required)

if download_required:
# No container available: blocking prompt, handle response
if accepted is True:
settings.Settings().set("updater_check_all", True, autosave=True)
# Accepting here is consent for this download, so suppress
# ContainerInstallTask's redundant prompt for this run.
ContainerInstallTask.skip_prompt_once = True
# Proceed with update check immediately so the remote
# container can be downloaded.
return False
elif accepted is False:
# User declined - raise an error to stop startup
raise errors.UpdaterDisabledNoContainer()
# User pressed X - treat as decline
raise errors.UpdaterDisabledNoContainer()
else:
# Container available: non-blocking prompt, handler saves setting
# Skip update check, user will be prompted again next run if needed
return True
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Optional, solely because of the extra level of indentation which made things a bit more confusing to me)

Suggested change
except updater_errors.NeedUserInput as e:
download_required = isinstance(e, updater_errors.NeedUserInputNoContainer)
accepted = self.prompt_user(download_required=download_required)
if download_required:
# No container available: blocking prompt, handle response
if accepted is True:
settings.Settings().set("updater_check_all", True, autosave=True)
# Accepting here is consent for this download, so suppress
# ContainerInstallTask's redundant prompt for this run.
ContainerInstallTask.skip_prompt_once = True
# Proceed with update check immediately so the remote
# container can be downloaded.
return False
elif accepted is False:
# User declined - raise an error to stop startup
raise errors.UpdaterDisabledNoContainer()
# User pressed X - treat as decline
raise errors.UpdaterDisabledNoContainer()
else:
# Container available: non-blocking prompt, handler saves setting
# Skip update check, user will be prompted again next run if needed
return True
except updater_errors.NeedUserInput:
self.prompt_user()
return True
except updater_errors.NeedUserInputNoContainer as e:
if self.prompt_user(download_required=True) is True:
settings.Settings().set("updater_check_all", True, autosave=True)
# Accepting here is consent for this download, so suppress
# ContainerInstallTask's redundant prompt for this run.
ContainerInstallTask.skip_prompt_once = True
# Proceed with update check immediately so the remote
# container can be downloaded.
return False
# User declined or pressed X - raise an error to stop startup
raise errors.UpdaterDisabledNoContainer()

Comment thread dangerzone/startup.py Outdated
settings.Settings().set("updater_check_all", True, autosave=True)
# Accepting here is consent for this download, so suppress
# ContainerInstallTask's redundant prompt for this run.
ContainerInstallTask.skip_prompt_once = True
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly, here we change the skip_prompt_once attribute of the whole ContainerInstallTask class, in order to affect the subsequent of the execution of that task?

Hm, messing with global state is a bit risky, I'd like to avoid this if possible. I think there's a more elegant way to do that though. I'll comment in the relevant task.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't seen a comment about this elsewhere. Let me know how you'd prefer to do things :-)

Comment thread dangerzone/gui/startup.py Outdated
Comment on lines +126 to +130
ask = Settings().get("updater_ask_before_download")
if startup.ContainerInstallTask.skip_prompt_once:
startup.ContainerInstallTask.skip_prompt_once = False
ask = False
if ask:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we rely on this check instead?

Suggested change
ask = Settings().get("updater_ask_before_download")
if startup.ContainerInstallTask.skip_prompt_once:
startup.ContainerInstallTask.skip_prompt_once = False
ask = False
if ask:
ask = Settings().get("updater_ask_before_download")
can_proceed = is_container_tar_bundled() or is_container_image_installed()
if ask and can_proceed:

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this won't actually work, and it will ask the user a second time.

This was done to avoid asking them again. It introduces the in-memory skip_prompt_once variable as a way to remember the choice the user just entered in a previous prompt.

Adding the conditions you propose in can_proceed won't change this, because at this stage it's not already downloaded, unfortunately.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this file replace build-app.bat, or is it an experiment?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants