Public Access
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.
295 lines
18 KiB
RPMSpec
295 lines
18 KiB
RPMSpec
# ==================================================================================================
|
|
# The Linux desktop head, as an RPM.
|
|
#
|
|
# Built by scripts/ci-linux-rpm.sh, which is what supplies every dodo_* macro below and what checks
|
|
# the result. Nothing here is invoked by hand; `rpmbuild -bb` on this file with no --define set will
|
|
# fail on the Version line, which is the intended way round.
|
|
#
|
|
# ◆ WHY THIS IS AN RPM AND NOT A VELOPACK RELEASE, WHICH IS THE FIRST QUESTION.
|
|
#
|
|
# Both desktop platforms that ship today are Velopack: the client polls the project's forge, fetches
|
|
# a newer build and swaps itself over. ADR 0013 rule 4 is what makes that safe to hold — the feed
|
|
# address is a const, so no operator can answer "is there a newer version" on the user's behalf.
|
|
#
|
|
# This package has no updater in it at all, and that is deliberate rather than unfinished.
|
|
# VelopackUpdateChannel.ForThisMachine already refuses every platform that is not Windows or macOS,
|
|
# and it says why in a comment worth not undoing: a client handed an UpdateManager pointed at a feed
|
|
# carrying nothing it can apply is a client that reports itself current forever. On Linux the
|
|
# package manager is the updater — `dnf upgrade` is a mechanism the user already trusts, already
|
|
# audits and already controls — so the honest thing is to be an ordinary package and let it do that,
|
|
# rather than to bolt on a second one that answers to a different authority.
|
|
#
|
|
# The consequence to know: an installed RPM learns about a new version when its repository does, and
|
|
# this project publishes no repository yet. Somebody who downloaded the file installs the next one
|
|
# the same way.
|
|
# ==================================================================================================
|
|
|
|
# ◆ THE THREE THINGS THAT MUST BE TURNED OFF, AND WHAT EACH ONE WOULD DO.
|
|
#
|
|
# __os_install_post is the chain of brp-* scripts rpm runs over the buildroot: strip, strip-static,
|
|
# compress, elfperms. Every one of them is wrong here. The payload is a published .NET tree whose
|
|
# managed PDBs ADR 0013 rule 6 keeps on purpose — a stack with file names and line numbers is this
|
|
# project's entire diagnostic channel — and brp-strip would take the native symbols with them for a
|
|
# saving nobody asked for. It also runs the host's binutils over glibc binaries on a build host that
|
|
# is musl, which is a class of breakage with no good failure message.
|
|
#
|
|
# debug_package would then try to build a -debuginfo subpackage out of what strip extracted, from
|
|
# third-party binaries nobody here will ever step through.
|
|
#
|
|
# _build_id_links is the one that surprises people, because turning the other two off does not turn
|
|
# it off: rpm still writes /usr/lib/.build-id/<xx>/<hash> symlinks for every ELF file in the payload.
|
|
# For a bundled tree that means dozens of links in a system directory this package has no business
|
|
# owning, and two files with the same build id — which a .NET publish does produce — make it a build
|
|
# *warning* about duplicates rather than an error, so it goes unnoticed.
|
|
%global __os_install_post %{nil}
|
|
%global debug_package %{nil}
|
|
%global _build_id_links none
|
|
|
|
# ◆ AUTO-REQUIRES STAYS ON. AUTO-PROVIDES DOES NOT. THE ASYMMETRY IS THE WHOLE POINT.
|
|
#
|
|
# rpm's elfdeps reads DT_NEEDED and the symbol-version table out of every ELF file in the buildroot,
|
|
# and the requires half of that is worth much more than a hand-written list: it produces
|
|
# `libc.so.6(GLIBC_2.38)(64bit)` rather than `glibc`, so the package refuses to install on a host
|
|
# whose glibc is too old instead of installing and dying at the first launch. A hand list cannot
|
|
# express that and would go stale the first time a dependency changed underneath it.
|
|
#
|
|
# It reads ELF directly rather than by loading anything, so it does this correctly for x86_64 glibc
|
|
# binaries on a musl build host — which is what lets this package be built on the same Alpine runner
|
|
# as everything else in ci.yml.
|
|
#
|
|
# The provides half is the opposite. Everything under /opt here is a *bundled* copy: libSkiaSharp,
|
|
# libcoreclr, libe_sqlite3 and the rest belong to this application and to nothing else. Left on,
|
|
# this package would advertise `libcoreclr.so()(64bit)` to the whole system, and some other package's
|
|
# dependency could then be satisfied by a private copy inside a directory `dnf remove dodossh` takes
|
|
# away. Excluding the tree by path is the standard shape for that and costs nothing, because nothing
|
|
# outside this package is meant to link against anything inside it.
|
|
#
|
|
# ◆ WHICH IS WHY THE REQUIRES SIDE NEEDS AN EXCLUSION TOO, AND WHY IT IS COMPUTED.
|
|
#
|
|
# The two halves are not independent. A bundled library with a DT_NEEDED on another bundled library
|
|
# produces a requirement for a soname this package carries and — after the exclusion above — no
|
|
# longer advertises, which no host on earth can satisfy. It is not hypothetical and it is not rare:
|
|
# .NET's own libmscordbi.so links libmscordaccore.so, so the very first realistic payload this was
|
|
# tried against produced a package `dnf install` refuses outright.
|
|
#
|
|
# dodo_bundled is the alternation of every lib*.so* filename in the payload, worked out by
|
|
# scripts/ci-linux-rpm.sh from the tree it was handed rather than listed here. A list in this file
|
|
# would be a list of Microsoft's and SkiaSharp's internal file names, kept in step by hand, and its
|
|
# failure mode is the uninstallable package above.
|
|
#
|
|
# liblttng-ust is excluded on its own terms and not because it is bundled — it is not. It is what
|
|
# libcoreclrtraceptprovider.so links, and that file exists so that CoreCLR can emit LTTng trace
|
|
# events if a host has the tracing stack. Nothing here asks it to. The file is kept and the
|
|
# requirement is dropped, which is the arrangement that works both ways round: where liblttng-ust is
|
|
# present the dlopen succeeds and tracing is available, and where it is absent CoreCLR carries on, as
|
|
# it does on every machine that runs a .NET application without it. Requiring it instead would put
|
|
# the package out of reach on RHEL, where lttng-ust is in EPEL rather than in the base repositories —
|
|
# an install failure on a supported distribution, in exchange for a tracing integration this
|
|
# application does not use.
|
|
#
|
|
# scripts/ci-linux-rpm.sh checks the finished package against its own file list afterwards, because
|
|
# an exclusion that silently stopped matching would put the uninstallable package back with no
|
|
# warning anywhere.
|
|
%global __provides_exclude_from ^/opt/.*$
|
|
%global __requires_exclude ^(liblttng-ust\.so|%{dodo_bundled})
|
|
|
|
Name: %{dodo_name}
|
|
Version: %{dodo_version}
|
|
Release: %{dodo_release}
|
|
Summary: SSH client with an end-to-end encrypted credential vault
|
|
License: MIT
|
|
URL: https://git.dodotech.cloud/DodoTech-Public/DodoSSH
|
|
BuildArch: x86_64
|
|
|
|
# ◆ THE DEPENDENCIES elfdeps CANNOT SEE, WHICH ARE THE ONES THE PRODUCT IS MADE OF.
|
|
#
|
|
# Everything below is reached through dlopen, by a name composed at runtime, so it appears in no
|
|
# DT_NEEDED entry anywhere in the payload. Left to the automatic scan the package would install
|
|
# cleanly on a host with none of it and fail at the first window.
|
|
#
|
|
# GTK and WebKitGTK are the terminal. Avalonia.Controls.WebView P/Invokes a bare `libwebkit2gtk` and
|
|
# a bare `libgtk` and resolves them itself, and MainWindow.OnTerminalEnvironmentRequested documents
|
|
# the backend it was measured against: WebKitGTK 2.52.5 on Fedora 44, which is the 4.1 API line — the
|
|
# assembly's one fully-spelled soname, libjavascriptcoregtk-4.1.so.0, is that line's other half.
|
|
#
|
|
# ◆ THE 4.0 LINE IS DELIBERATELY NOT CLAIMED, AND THE COST OF THAT IS KNOWN.
|
|
#
|
|
# The control's own diagnostic says "webkit2gtk 4.0+", so a dependency permitting either line is
|
|
# tempting and was written first. RHEL 9 and its rebuilds are what it would buy: they ship
|
|
# webkit2gtk3, which is the 4.0 API under the soname libwebkit2gtk-4.0.so.37 — not .so.0, which is
|
|
# what makes this a thing to look up rather than guess. Requiring only 4.1 means `dnf install` on
|
|
# AlmaLinux 9 refuses with "nothing provides libwebkit2gtk-4.1.so.0", by design.
|
|
#
|
|
# It is refused because nobody has run it. This project does not ship an Intel macOS package for the
|
|
# same reason and says so in ADR 0013 decision 10 — the engineering is small and the verification is
|
|
# the scarce thing — and an install that succeeds and then draws a blank terminal is a worse answer
|
|
# than one that refuses in a sentence naming the missing library. The day somebody runs the head on
|
|
# a 4.0 host, this becomes a rich `or` and a line in the README.
|
|
#
|
|
# libX11 is Avalonia's own backend, P/Invoked the same way. On Wayland this is satisfied by XWayland,
|
|
# which every desktop shipping this package has.
|
|
#
|
|
# libicu is the one that looks removable and is not. DodoSSH.Client.App sets InvariantGlobalization
|
|
# to false — deliberately, and the csproj says why: this process prints timestamps and host names to
|
|
# a person who expects their own locale's conventions. A .NET process configured that way and given
|
|
# no ICU does not degrade to invariant, it refuses to start. The bare package name is the Fedora and
|
|
# RHEL spelling; it is the one name here that is a distribution's rather than a soname's, because ICU
|
|
# bumps its soname on every release and pinning one would make this package expire.
|
|
Requires: libwebkit2gtk-4.1.so.0()(64bit)
|
|
Requires: libgtk-3.so.0()(64bit)
|
|
Requires: libgdk-3.so.0()(64bit)
|
|
Requires: libgio-2.0.so.0()(64bit)
|
|
Requires: libglib-2.0.so.0()(64bit)
|
|
Requires: libgobject-2.0.so.0()(64bit)
|
|
Requires: libX11.so.6()(64bit)
|
|
Requires: libicu
|
|
|
|
# The size directories under /usr/share/icons/hicolor belong to hicolor-icon-theme and not to this
|
|
# package, which is the right way round — every application drops icons into them. Depending on the
|
|
# owner is how those directories come to exist at all on a minimal host, and how they get removed
|
|
# again when the last thing using them goes.
|
|
Requires: hicolor-icon-theme
|
|
|
|
%description
|
|
DodoSSH is an SSH and SFTP client for teams whose credentials are shared and whose infrastructure is
|
|
not. Hosts, keys and passphrases live in a vault that is encrypted on this machine and stays
|
|
encrypted on the server, so the deployment holding a team's secrets can never read them.
|
|
|
|
This package carries its own .NET runtime and is updated the way every other package on this system
|
|
is; it contains no self-updater and contacts no update service.
|
|
|
|
%install
|
|
rm -rf %{buildroot}
|
|
|
|
# ◆ /opt, AND NOT /usr/lib64/dodossh, WHICH IS THE MORE CONVENTIONAL PLACE.
|
|
#
|
|
# The Filesystem Hierarchy Standard reserves /opt for exactly this — an add-on package that arrives
|
|
# as one self-contained tree — and that is what a published .NET application is: a runtime, thirty
|
|
# managed assemblies and half a dozen native libraries that only make sense together and are only
|
|
# ever loaded by each other. Scattering them into the system library directory would mix a bundled
|
|
# libSkiaSharp in with the system's shared objects, which is the arrangement the provides exclusion
|
|
# above exists to keep this package out of.
|
|
#
|
|
# The directory name carries the channel, so a release and a nightly are two trees rather than one
|
|
# that overwrites the other. That is the same separation ADR 0013 rule 9 builds on Windows out of
|
|
# pack ids, arrived at here for free: two RPMs with different names owning different paths cannot
|
|
# replace one another, and dnf will say so rather than doing it. The profile directory is separated
|
|
# by the build itself — DodoChannel=nightly is what ClientPaths reads to suffix it — so the two
|
|
# installs share no state at all.
|
|
mkdir -p %{buildroot}/opt/%{dodo_dir}
|
|
cp -a %{dodo_payload}/. %{buildroot}/opt/%{dodo_dir}/
|
|
|
|
# ◆ AND THE MODES ARE SET HERE RATHER THAN INHERITED, WHICH IS NOT TIDINESS.
|
|
#
|
|
# `cp -a` preserves what the publish directory happened to have, and what it happened to have is a
|
|
# property of the filesystem it was written on rather than of anything decided here. rpmlint caught
|
|
# the consequence on the first tree this was tried against: every file in the package was mode 777,
|
|
# because the tree had come off a mount that reports everything that way.
|
|
#
|
|
# For this application that is not a lint warning, it is a hole. A world-writable /opt/DodoSSH lets
|
|
# any local account replace the SSH client belonging to every other account on the machine — the
|
|
# binary that holds a team's infrastructure credentials in plaintext while it runs, which is the
|
|
# thing ADR 0001 spends its whole trust model on. Malware running as nobody-in-particular would not
|
|
# need to attack the crypto; it would edit the client.
|
|
#
|
|
# Stated absolutely and in this order, so the answer does not depend on the publish at all:
|
|
# directories and the things that are executed or loaded get 0755, and everything else — assemblies,
|
|
# PDBs, the runtime config, the terminal's HTML and JavaScript — gets 0644. Owned by root, which is
|
|
# rpm's default for a package that names no %attr user, and is what makes the modes mean anything.
|
|
find %{buildroot}/opt/%{dodo_dir} -type d -exec chmod 0755 {} +
|
|
find %{buildroot}/opt/%{dodo_dir} -type f -exec chmod 0644 {} +
|
|
find %{buildroot}/opt/%{dodo_dir} -type f -name '*.so*' -exec chmod 0755 {} +
|
|
|
|
# The two files a .NET publish means to be executed. createdump is the runtime's crash-dump helper
|
|
# and is only present on some publishes, so it is conditional; the apphost never is, and the check
|
|
# above this section has already refused a publish without one.
|
|
chmod 0755 %{buildroot}/opt/%{dodo_dir}/DodoSSH
|
|
[ -f %{buildroot}/opt/%{dodo_dir}/createdump ] && chmod 0755 %{buildroot}/opt/%{dodo_dir}/createdump
|
|
true
|
|
|
|
# The apphost is the thing to launch and every other file in the tree is loaded relative to it, so
|
|
# what goes on PATH is a symlink rather than a copy or a wrapper script. .NET resolves its own
|
|
# directory through /proc/self/exe, which follows the link, so a symlinked apphost finds its runtime
|
|
# with no help — a wrapper exporting DOTNET_ROOT or cd-ing first would be solving a problem that
|
|
# does not exist here, and would be one more file to keep in step with the layout.
|
|
#
|
|
# Relative — ../../opt from /usr/bin — rather than the absolute /opt path it resolves to. Both work
|
|
# on a running system and only one works inside a chroot or an image being assembled somewhere other
|
|
# than /, which is where absolute symlinks out of a package are checked and warned about.
|
|
mkdir -p %{buildroot}%{_bindir}
|
|
ln -s ../../opt/%{dodo_dir}/DodoSSH %{buildroot}%{_bindir}/%{dodo_bin}
|
|
|
|
# ◆ THE DESKTOP ENTRY IS WRITTEN HERE RATHER THAN SHIPPED AS A SOURCE FILE.
|
|
#
|
|
# Three of its seven meaningful lines differ between the release and the nightly, and the two files
|
|
# that would result are the kind of pair that drifts: somebody adds a MIME type to one of them and
|
|
# nobody notices for a release. Composed from the same macros the rest of the layout uses, they
|
|
# cannot.
|
|
#
|
|
# StartupWMClass is the line that is easy to leave out and visibly wrong without: Avalonia sets
|
|
# WM_CLASS from the assembly name, which is DodoSSH for both channels, so without this a launched
|
|
# window is a second, unlabelled icon in the dock beside the one that launched it. It is the assembly
|
|
# name deliberately and not the package's binary name — the window does not know which package
|
|
# started it.
|
|
mkdir -p %{buildroot}%{_datadir}/applications
|
|
cat > %{buildroot}%{_datadir}/applications/%{dodo_bin}.desktop <<DESKTOP
|
|
[Desktop Entry]
|
|
Type=Application
|
|
Version=1.0
|
|
Name=%{dodo_title}
|
|
GenericName=SSH Client
|
|
Comment=SSH and SFTP with an end-to-end encrypted credential vault
|
|
Exec=%{dodo_bin}
|
|
Icon=%{dodo_bin}
|
|
Terminal=false
|
|
StartupNotify=true
|
|
StartupWMClass=DodoSSH
|
|
Categories=Network;RemoteAccess;System;
|
|
Keywords=SSH;SFTP;Terminal;Remote;Shell;
|
|
DESKTOP
|
|
|
|
# One file per size into the hicolor theme, named for the package so the two channels show different
|
|
# entries rather than fighting over one path. The sizes are the ones the icon script draws and its
|
|
# comment argues for; adding one there means adding it here.
|
|
for size in 48 64 128 256; do
|
|
mkdir -p %{buildroot}%{_datadir}/icons/hicolor/${size}x${size}/apps
|
|
install -m 0644 %{dodo_icons}/dodossh-${size}.png \
|
|
%{buildroot}%{_datadir}/icons/hicolor/${size}x${size}/apps/%{dodo_bin}.png
|
|
done
|
|
|
|
# Installed rather than listed straight out of the source tree. `%license` is a %doc-style marker and
|
|
# resolves its argument against the build directory, which this spec does not have — there is no
|
|
# %prep and no %build, because the payload arrives already published. Putting the file where it
|
|
# belongs first and marking the installed path second is the form that works with neither.
|
|
mkdir -p %{buildroot}%{_licensedir}/%{name}
|
|
install -m 0644 %{dodo_licence} %{buildroot}%{_licensedir}/%{name}/LICENSE
|
|
|
|
# Modern rpm on Fedora fires file triggers that do both of these, so on the distribution this was
|
|
# measured against they are redundant. They are here for the ones that do not, and every line ends in
|
|
# `|| :` so that a host without the tool installed gets a package that installs rather than a
|
|
# scriptlet failure at the end of a 130 MB transaction.
|
|
%post
|
|
/usr/bin/update-desktop-database %{_datadir}/applications &>/dev/null || :
|
|
/usr/bin/gtk-update-icon-cache -qtf %{_datadir}/icons/hicolor &>/dev/null || :
|
|
|
|
%postun
|
|
/usr/bin/update-desktop-database %{_datadir}/applications &>/dev/null || :
|
|
/usr/bin/gtk-update-icon-cache -qtf %{_datadir}/icons/hicolor &>/dev/null || :
|
|
|
|
%files
|
|
# The whole tree, owned by this package, which is what makes `dnf remove` take all of it. The user's
|
|
# vault cache is not in here and is not touched by an uninstall — ClientPaths puts it under
|
|
# $XDG_DATA_HOME, which is the Linux spelling of the same decision ADR 0013's consequences record for
|
|
# Windows: the install moves, the profile does not, and an uninstall is not a sign-out.
|
|
/opt/%{dodo_dir}
|
|
%{_bindir}/%{dodo_bin}
|
|
%{_datadir}/applications/%{dodo_bin}.desktop
|
|
%{_datadir}/icons/hicolor/*/apps/%{dodo_bin}.png
|
|
%license %{_licensedir}/%{name}/LICENSE
|
|
|
|
%changelog
|
|
# Deliberately empty. The changelog of record is the git history and the release notes on the forge,
|
|
# and a %changelog generated per build from a version number carries no information that `rpm -q`
|
|
# does not already answer — while a hand-written one is a third place a release has to remember to
|
|
# edit. rpmlint says W: no-changelog and that warning is accepted.
|