Package the Linux desktop head as an RPM, and publish a nightly one
ci / build and test (push) Successful in 3m1s
ci / android head (push) Successful in 3m39s
ci / desktop nightly (push) Successful in 1m7s
ci / api image (push) Successful in 22s

The third desktop platform shipped as nothing: a checkout somebody built.
VelopackUpdateChannel already named the two platforms it serves rather than
negating the one it does not, so that a Linux package would be an addition
there rather than something that had silently half-happened. This is it, and
it is deliberately not a Velopack channel.

There is no updater in the package at all. On this platform dnf is already an
updater the user trusts, audits and controls, and a second one answering to
this project instead is the capability ADR 0013 rule 4 spends a page keeping
unsettable. The cost is real and is written down rather than glossed: nothing
tells a Linux user that a newer build exists, because there is no repository
to tell them.

Rule 3 is untouched. The build job packages the release-shaped RPM on every
main build and every tag and uploads nothing, exactly as it does for Windows
and macOS; the desktop job packages the nightly-shaped one and puts it on the
rolling nightly-desktop release beside the Windows feed. Two RPMs with
different names owning different paths cannot replace one another, so rule 9's
separations come almost free here — the profile split still belongs to the
build, through DodoChannel.

vpk verifies that what it packs actually calls VelopackApp.Build().Run(), and
nothing here does that for us, so scripts/ci-linux-rpm.sh makes its own
assertions: it asks the finished package what it requires, what it provides,
what it holds and what modes those carry. Two of those checks exist because
the first realistic payload tripped them.

  - .NET's libmscordbi.so links libmscordaccore.so. With bundled provides
    excluded — which they must be, or this package would advertise libcoreclr
    to the whole system — that produced a dependency no host on earth can
    satisfy. The requires exclusion is computed from the payload rather than
    listed, because a list of Microsoft's internal file names goes stale in
    the direction that yields an uninstallable package.

  - cp -a inherits the publish directory's modes, and off the wrong filesystem
    that meant a world-writable /opt/DodoSSH: any local account able to
    replace the SSH client every other account runs. For an application
    holding a team's credentials in plaintext that is the trust model walked
    around rather than attacked. Every mode is now set absolutely.

Verified as far as a runner can: both packages install side by side on Fedora
44, resolve webkit2gtk4.1 2.52.5 — the backend MainWindow records as measured
— keep separate launchers, icons and profiles, and dnf remove takes one tree
and leaves the other. 0.0.1~alpha.0.143 sorts below both 0.0.1 and the next
height. What no runner can answer is Phase 19, which is new and unwalked.

RHEL 9 is deliberately not claimed. It ships WebKitGTK's 4.0 line under
libwebkit2gtk-4.0.so.37, and an or-branch for it was written and then removed:
nobody has run the head there, and dnf refusing with "nothing provides
libwebkit2gtk-4.1.so.0" is a better answer than an install that draws a blank
terminal. Same sentence, and the same reason, as the missing Intel Mac.

x86_64 only, for that reason and not a different one.
This commit is contained in:
jaap-jan committed 2026-08-22 16:43:38 +02:00
1 parent 1970e20e56
commit 1bf79c7c26
12 files changed
+1015 -5

No files matched your search

+109
View File
@@ -2841,3 +2841,112 @@ Register a device, sync something, then remove the application.
**Failure means:** an empty directory is the pack id having been changed to `DodoSSH`, which puts Velopack's
install root on top of `ClientPaths.DataDirectory` and makes an uninstall delete a user's un-synced work.
This is the single reason the bundle is named `DodoSSH.Desktop.app`.
## Phase 19 — Installing the Linux client, and the things dnf does not check
The Linux counterpart of phases 16 and 18, and the shortest of the three because the platform takes two of
their questions off the table. There is no updater here, so nothing in this phase asks whether an update
arrives; and there is no signing, because nothing on this platform refuses an unsigned package — a distinct
statement from "unsigned is fine", and `build/linux/dodossh.spec` says what stands in for a signature: the
package comes from the project's own forge over TLS and from nowhere else.
What that leaves is the set nobody else can answer. **CI builds this package and installs nothing**, and
every check in `scripts/ci-linux-rpm.sh` is a check on what the package *says* — its dependencies, its file
list, its modes. None of them start the application, and the failures below are all of the shape "installs
perfectly and then does not work".
Needs a Fedora 44 machine or newer with a graphical session. Build the package first:
```bash
dotnet publish src/DodoSSH.Client.App/DodoSSH.Client.App.csproj \
--configuration Release --runtime linux-x64 --self-contained true \
-p:RestoreLockedMode=false --output /tmp/linux-x64
scripts/ci-linux-rpm.sh --payload /tmp/linux-x64 --version 1.2.3 --out /tmp/rpm
sudo dnf install /tmp/rpm/dodossh-1.2.3-1.x86_64.rpm
```
### 19.1 It launches from the menu, and not only from a shell · **do this one first**
Log out and back in, or wait for the desktop to notice the new `.desktop` file, then start DodoSSH from the
applications menu rather than from a terminal.
**Pass:** the window opens, and the running application's icon in the dock or task list is the same entry
that launched it — one icon, not two.
**Failure means:** a second unlabelled icon appearing beside the launcher is `StartupWMClass` not matching
what Avalonia sets `WM_CLASS` to. The spec sets it to `DodoSSH`, which is the assembly name and is the same
for both channels; if Avalonia's answer has changed, `xprop WM_CLASS` on the window is what to compare
against. Nothing at all in the menu is the desktop database not having been rebuilt — `%post` runs
`update-desktop-database` and swallows its failure on purpose, so try it by hand before concluding anything.
### 19.2 The terminal draws, which is the WebKitGTK question
Connect to a host and use the shell: type, run something that scrolls, resize the window.
**Pass:** the terminal attaches within a second or two and behaves as it does on Windows.
**Failure means:** **a blank pane where everything else works is the known one**, and it is the reason
`MainWindow.OnTerminalEnvironmentRequested` exists — the GTK WebView adapter in its default mode can only
open a window of its own, so a control asked to host it in place has nothing to draw. If that method has
stopped setting `ExperimentalOffscreen`, or the vendor has withdrawn the flag, this is what it looks like.
A pane that reports a renderer timeout instead is the loopback WebSocket rather than the backend.
### 19.3 The icon is the mark, at the sizes the desktop actually asks for
Look at it in the applications menu, in the dock, and in the window switcher.
**Pass:** the accent tile and the `>_` mark, filling its square, legible at the small end.
**Failure means:** a generic icon is `Icon=` in the desktop entry not matching an installed file name — both
are the package's binary name, so they can only disagree if one was edited. A blurry small icon is the
48-pixel file missing and the 256 being scaled down; `dodossh-icon.ps1` draws four sizes and the spec
installs four.
### 19.4 The passphrase is asked for, and no device key is offered
Open the security settings page.
**Pass:** registering a device key is **not** offered, and unlocking asks for the passphrase every time.
**Failure means:** being offered it is `DesktopDeviceKeyStores.ForThisMachine` having returned something
other than `UnavailableDeviceKeyStore` on a platform with nothing to back it. Accepting the offer would put
a wrap on the server that nothing can ever open.
### 19.5 The nightly installs beside the release rather than over it
With the release package installed, install the nightly one too.
**Pass:** `dnf` installs it as a second package rather than as an upgrade. Both entries appear in the menu,
named *DodoSSH* and *DodoSSH Nightly*. Both run at once. Signing into the nightly asks for the passphrase
again and appears on the account as a separate device.
**Failure means:** `dnf` offering to *upgrade* rather than install is the two packages sharing a name. A
nightly that opens the release's vault already unlocked is the profile separation gone — that comes from
`DodoChannel=nightly` reaching `ClientPaths`, not from the packaging, so check
`~/.local/share/dodossh.nightly` exists rather than looking at the spec.
### 19.6 Nothing in the installed tree is writable by anyone but root
```bash
find /opt/DodoSSH ! -user root -o -perm -o+w
```
**Pass:** no output.
**Failure means:** any line here is a local account being able to replace the SSH client every other account
on the machine runs, which for an application holding a team's credentials in plaintext is the whole trust
model walked around rather than attacked. `ci-linux-rpm.sh` refuses to produce such a package and the spec
sets every mode absolutely, so a hit here means one of those two was changed — it cannot arrive by accident
from a publish directory.
### 19.7 Removing it leaves the vault, and leaves the other channel alone
Register a device, sync something, then `sudo dnf remove dodossh`.
**Pass:** `~/.local/share/dodossh` still holds the cache and the outbox. `/opt/DodoSSH` is gone entirely.
`/opt/DodoSSH.Nightly` and its menu entry are untouched.
**Failure means:** a missing cache is `ClientPaths` having been pointed somewhere under `/opt`, which would
make an uninstall delete a user's un-synced work — the Linux spelling of the mistake ADR 0013 decision 2
exists to prevent. Files left behind under `/opt/DodoSSH` are files the package did not own, which means
something wrote into the install directory at runtime and would survive a reinstall.