Repository: flatpak/flatpak (C)

Issue:
[Bug]: `flatpak update --no-deploy` uninstalls eol-rebase apps even though the replacement is not installed
### Checklist

- [X] I agree to follow the [Code of Conduct](https://github.com/flatpak/flatpak/blob/main/CODE_OF_CONDUCT.md) that this project adheres to.
- [X] I have searched the [issue tracker](https://www.github.com/flatpak/flatpak/issues) for a bug that matches the one I want to file, without success.
- [X] If this is an issue with a particular app, I have tried filing it in the appropriate issue tracker for the app (e.g. under https://github.com/flathub/) and determined that it is an issue with Flatpak itself.
- [X] This issue is not a report of a security vulnerability (see [here](https://github.com/flatpak/flatpak/blob/main/SECURITY.md) if you need to report a security issue).

### Flatpak version

1.14.0

### What Linux distribution are you using?

Fedora Linux

### Linux distribution version

37

### What architecture are you using?

x86_64

### How to reproduce

1. Have an app installed which is marked `eol-rebase`. I hit this with `com.github.gi_lom.dialect`, which was at some point superseded by `app.drey.Dialect`. You can reproduce this situation with:

```
flatpak install --system flathub app/com.github.gi_lom.dialect/x86_64/stable
```

You will see this prompt:

> Info: app **com.github.gi_lom.dialect** branch **stable** is end-of-life, in favor of **app.drey.Dialect** branch **stable**
> Replace? [Y/n]: n

Answer no.

2. Run `flatpak update --no-deploy`
3. Answer "yes" when asked about replacing `com.github.gi_lom.dialect` with `app.drey.Dialect` (and the same question for its `.Locale` extension

### Expected Behavior

`--no-deploy` is documented as follows:

> Download the latest version, but don't deploy it.

It exists to be subsequently paired with `flatpak update --no-pull`, to install whatever updated versions are locally available.

So I would expect that this operation would download `app.drey.Dialect` and `app.drey.Dialect.Locale`, but not deploy them, and `com.github.gi_lom.dialect` and `com.github.gi_lom.dialect.Locale` would remain installed. A subsequent invocation of `flatpak update --no-pull` (answering "yes" again) would remove the old ID and install the new.

### Actual Behavior

The installed copy of the app is removed.

As a result, I have now lost an app I had installed, and a subsequent invocation of `flatpak update --no-pull` will not install the new version, because the old version is not installed to be eol-rebased.


```
✘ ~ ➭ flatpak update --no-deploy
Looking for updates…

Info: runtime org.gnome.Sdk branch 41 is end-of-life, with reason:
   The GNOME 41 runtime is no longer supported as of September 17, 2022. Please ask your application developer to migrate to a supported platform.

Info: runtime com.github.gi_lom.dialect.Locale branch stable is end-of-life, in favor of app.drey.Dialect.Locale branch stable
Replace? [Y/n]: y

Info: app com.github.gi_lom.dialect branch stable is end-of-life, in favor of app.drey.Dialect branch stable
Replace? [Y/n]: y

app.drey.Dialect permissions:
    ipc     network     fallback-x11     pulseaudio    wayland    x11    dri



        ID                                         Branch           Op           Remote            Download
 1. [✓] app.drey.Dialect.Locale                    stable           i            flathub           244.4 kB / 227.6 kB
 2. [-] com.github.gi_lom.dialect                  stable           r
 3. [-] com.github.gi_lom.dialect.Locale           stable           r
 4. [✓] app.drey.Dialect                           stable           i            flathub             1.7 MB / 1.5 MB

Changes complete.
✔ ~ ➭ flatpak run com.github.gi_lom.dialect
error: app/com.github.gi_lom.dialect/x86_64/master not installed
✘ ~ ➭ flatpak update
Looking for updates…

Info: runtime org.gnome.Sdk branch 41 is end-of-life, with reason:
   The GNOME 41 runtime is no longer supported as of September 17, 2022. Please ask your application developer to migrate to a supported platform.

Nothing to do.
```

### Additional Information

I initially hit this problem in GNOME Software, which as part of its normal operation performs (the libflatpak API equivalent of) a `flatpak update --no-deploy` followed by `flatpak update --no-pull`. Personally [I think it shouldn't](https://gitlab.gnome.org/GNOME/gnome-software/-/issues/1929) but nonetheless the current behaviour of `flatpak update --no-deploy` is also broken in this situation.

The way that eol-rebase is exposed by `FlatpakTransaction` is that a `end-of-lifed-with-rebase` signal / `end_of_lifed_with_rebase` vfunc is emitted/called. The application is expected to make a policy decision about whether to follow the rebase, and if so, call `flatpak_transaction_add_rebase()` for the new ref and `flatpak_transaction_add_uninstall()` for the old.

https://github.com/flatpak/flatpak/blob/main/common/flatpak-transaction.c#L1271-L1280

Flatpak itself does this in `FlatpakCliTransaction`:

https://github.com/flatpak/flatpak/blob/d83b262dedcd51a0443d6d9d13e3a242d15b4d59/app/flatpak-cli-transaction.c#L988-L1010

And then again in `FlatpakQuietTransaction`, with a copy-paste of some code to handle an edge-case:

https://github.com/flatpak/flatpak/blob/d83b262dedcd51a0443d6d9d13e3a242d15b4d59/app/flatpak-quiet-transaction.c#L249-L269

GNOME Software also does this, except that it doesn't handle that edge case. I think because there is no way to install an app marked eol-rebase from the UI if it is not already installed, it's not a problem that it is not handled, but I'm not sure. https://gitlab.gnome.org/GNOME/gnome-software/-/blob/main/plugins/flatpak/gs-flatpak-transaction.c#L676-686

I couldn't find the code in KDE Discover that handles eol-rebase. Maybe it doesn't handle it?

(The fact that this code is copy-pasted from the docstring into at least 3 places, but with minor, potentially bad, variations, suggests to me that this API is not the right shape. Perhaps it would have been better to have the signal handler return an element of an enum like `{ NO_DECISION, FOLLOW_REBASE, DONT_FOLLOW_REBASE }`, stop once a handler returns something other than `NO_DECISION`, and add the relevant ops in the base class if a handler returns `FOLLOW_REBASE`, but I'm sure there's a reason it was designed this way, and anyway it's too late to change that.)

As on #5171, there seem to be two ways to solve this bug:

1. Adjust every application which handles `end_of_lifed_with_rebase` to check `flatpak_transaction_get_no_deploy (t)` and only add the uninstall op if that returns `FALSE`, and adjust the description of the dance you have to do in the docstring of the signal
2. Make uninstall operations a no-op if no_deploy is set on the transaction (probably by making `flatpak_transaction_add_uninstall()` a no-op in that case)


Discussion: I'm in favour of gently expanding the semantics of NO_DEPLOY to actually also disable uninstallations, but this would probably need a bit of auditing of call sites (GitHub searching?) to check that this won't have any surprising side-effects.

Perhaps we should also create a "make the API for this less bad" ticket?
I did a little auditing of usage of `flatpak_transaction_set_no_deploy` and it's shorter GI version `set_no_deploy` (fortunately almost no one has a similar API).

https://github.com/search?q=flatpak_transaction_set_no_deploy&type=Code
flatpak
gnome-software

https://codesearch.debian.net/search?q=flatpak_transaction_set_no_deploy&literal=1
flatpak
gnome-software

https://github.com/search?q=set_no_deploy&type=code
pardus-flatpak-gui
eos-image-builder

https://codesearch.debian.net/search?q=set_no_deploy&literal=1
flatpak
gnome-software

So, it's primarily flatpak and gnome-software. pardus-flatpak-gui looks to be unmaintained but always sets no deploy to `FALSE` regardless. I wrote eos-image-builder's flatpak integration, which sets no deploy to `TRUE` for a staged install, but it's explicitly installing from scratch and there aren't any apps to uninstall.

The other searching would be for tools that use the CLI with `--no-deploy`. I'll take a look at that next.
`--no-deploy` was trickier to search for. GitHub's search doesn't let you search for that on it's own, so I searched for [`flatpak no-deploy`](https://github.com/search?q=flatpak+no-deploy&type=code). Those were all from within flatpak either in the CLI builtin, the tests, or the fish shell completions.

I was able to search for simply [`--no-deploy`](https://codesearch.debian.net/search?q=--no-deploy&literal=1) on Debian code search. Primarily those are Ruby matches for bundler's `--no-deployment` option. The only ones that were specifically for flatpak were flatpak itself.

So, my conclusion is that there are 3 programs that care about no deploy set to `TRUE`:

- flatpak
- gnome-software
- eos-image-builder

As I said above, eos-image-builder doesn't care what flatpak decides to do about uninstallation as there's nothing installed when it runs.
There's also eos-updater which sets it to TRUE in some cases:

https://github.com/endlessm/eos-updater/blob/master/eos-updater/fetch.c#L495
 ->
https://github.com/endlessm/eos-updater/blob/30a0dc669b61779d8b04abe1b95eac6d9abebc53/libeos-updater-util/flatpak-util.c#L2601

I think the proposed behaviour change would also be correct here.
Don't know how I missed eos-updater. I think you're correct that this wouldn't affect it.



Fix (patch):
diff --git a/common/flatpak-transaction.c b/common/flatpak-transaction.c
index 97d79bbcd5..893b118ddc 100644
--- a/common/flatpak-transaction.c
+++ b/common/flatpak-transaction.c
@@ -2242,6 +2242,13 @@ add_related (FlatpakTransaction          *self,
           if (!rel->delete)
             continue;
 
+          if (priv->no_deploy)
+            {
+              g_info ("Skipping uninstallation of %s for no deploy transaction",
+                      flatpak_decomposed_get_ref (rel->ref));
+              continue;
+            }
+
           related_op = flatpak_transaction_add_op (self, rel->remote, rel->ref,
                                                    NULL, NULL, NULL, NULL,
                                                    FLATPAK_TRANSACTION_OPERATION_UNINSTALL,
@@ -2710,6 +2717,13 @@ flatpak_transaction_add_ref (FlatpakTransaction             *self,
     }
   else if (kind == FLATPAK_TRANSACTION_OPERATION_UNINSTALL)
     {
+      /* Skip uninstall for no deploy transactions. */
+      if (priv->no_deploy)
+        {
+          g_info ("Skipping uninstallation of %s for no deploy transaction", pref);
+          return TRUE;
+        }
+
       if (!dir_ref_is_installed (priv->dir, ref, &origin, NULL))
         return flatpak_fail_error (error, FLATPAK_ERROR_NOT_INSTALLED,
                                    _("%s not installed"), pref);
@@ -2945,7 +2959,8 @@ flatpak_transaction_add_update (FlatpakTransaction *self,
  * @ref: the ref
  * @error: return location for a #GError
  *
- * Adds uninstalling the given ref to this transaction.
+ * Adds uninstalling the given ref to this transaction. If the transaction is
+ * set to not deploy updates, the request is ignored.
  *
  * Returns: %TRUE on success; %FALSE with @error set on failure.
  */
@@ -4957,6 +4972,13 @@ add_uninstall_unused_ops (FlatpakTransaction  *self,
       origin = flatpak_dir_get_origin (priv->dir, unused_ref, NULL, NULL);
       if (origin)
         {
+          if (priv->no_deploy)
+            {
+              g_info ("Skipping uninstallation of %s for no deploy transaction",
+                      unused_ref_str);
+              continue;
+            }
+
           /* These get added last and have no dependencies, so will run last */
           uninstall_op = flatpak_transaction_add_op (self, origin, unused_ref,
                                                      NULL, NULL, NULL, NULL,