Provide *-full packages for Linux, and remove container.tar from default ones#1423
Provide *-full packages for Linux, and remove container.tar from default ones#1423almet wants to merge 22 commits into
*-full packages for Linux, and remove container.tar from default ones#1423Conversation
*-full packages for Linux, and remove container.tar from default ones
b4a9169 to
4f5fdfc
Compare
3225051 to
067c4bd
Compare
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.
067c4bd to
f578304
Compare
| ./dev_scripts/env.py --distro ${{ matrix.distro }} --version ${{ matrix.version }} \ | ||
| run dangerzone --help | ||
|
|
||
| install-rpm-full: |
There was a problem hiding this comment.
What I mentioned in https://github.com/freedomofpress/dangerzone/pull/1423/changes#r3219532167 also applies here, with the added benefit that:
- This job will no longer need to wait for
build-install-rpmto finish. - 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).
There was a problem hiding this comment.
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)
| req.reply(False) | ||
| QtCore.QTimer.singleShot(0, self._handle_download_declined) |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
(Optional, solely because of the extra level of indentation which made things a bit more confusing to me)
| 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() |
| 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I haven't seen a comment about this elsewhere. Let me know how you'd prefer to do things :-)
| ask = Settings().get("updater_ask_before_download") | ||
| if startup.ContainerInstallTask.skip_prompt_once: | ||
| startup.ContainerInstallTask.skip_prompt_once = False | ||
| ask = False | ||
| if ask: |
There was a problem hiding this comment.
Could we rely on this check instead?
| 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: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Does this file replace build-app.bat, or is it an experiment?
This removes the container.tar from the Linux packages, providing
-fullvariants 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.