Public Access
Two things the RPM shipped without, and the reason they arrive together is
that both are the same question — what may this platform ask of the machine,
and what must it not ask of the user.
◆ UPDATES: THIS CLIENT CHECKS AND FETCHES, AND PACKAGEKIT INSTALLS.
ADR 0013 rule 11 said there was no updater at all, and that went too far. It
conflated noticing that a newer build exists with installing it: the second is
what the trust argument is about, and the first is not — a client that checks
a fixed address and tells you what it found has taken no capability from
anybody. Written as a flat refusal, the rule bought its safety by paying a cost
that was avoidable, and the ADR now says so in its own words.
So LinuxUpdateChannel reads the rolling nightly release, compares versions the
way rpm compares them, downloads the package, verifies its digest and hands the
file to PackageKit over gdbus. Polkit asks; the user may say no; nothing is
torn down before the answer, because ApplyingEndsTheProcess is false and the
shell already knows what that means — it is the phone's shape, and declining an
update must not cost somebody their open shells.
Three details that are load-bearing rather than incidental:
- The comparison is rpmvercmp and not SemVer, because rpm is what will accept
or refuse the install. Same argument ADR 0014 makes for versionCode. The
installed side is read out of the rpm database rather than derived from the
assembly, so the SemVer-to-rpm translation exists in exactly one place. All
28 expected values in RpmVersionTests came out of rpm itself rather than out
of a reading of the algorithm.
- The digest is checked and is not a signature. Velopack verifies its own
downloads and Android checks a signature; an RPM over HTTPS has neither. The
manifest names a SHA-256 and a mismatch deletes the file rather than leaving
something installable-by-hand in the profile directory.
- ONLY_TRUSTED is cleared, which is what routes the request to polkit's
package-install-untrusted action. The package is unsigned, so requiring
trust would fail every update at the last step.
◆ THE DEVICE KEY: SEALED IN THE TPM, BEHIND A PIN THE TPM CHECKS.
ADR 0007's amendment is the bar: a gate inside the process is not a gate. The
key is sealed into a TPM 2.0 keyedhash object whose userAuth comes from a PIN,
so unsealing is a command the chip refuses; the blob is encrypted to the storage
hierarchy and is useless on any other machine; nothing here ever compares a PIN.
The sealed object deliberately carries no NoDA attribute, and that one omission
is the whole reason four digits is enough: every wrong answer advances the TPM's
dictionary-attack counter and the chip stops answering entirely once it trips.
Measured at four wrong tries against a software TPM, after which the correct PIN
gets nothing either. It is asserted by a test rather than left to a reading of
the template, because removing the attribute leaves every other assertion
passing.
This is weaker than Hello and Touch ID and the difference is invisible, so it is
written down in three places rather than smoothed over: those raise their prompt
from inside the platform and this one is a window we draw. The prompt is
deliberately plain for that reason — teaching people that a convincing DodoSSH
PIN box is a system dialog is the one thing this design must not do.
Rejected, each for failing the amendment: the Secret Service API, which has no
per-use consent and is DPAPI's shape exactly; fprintd, which answers a question
and holds nothing, so skipping the call skips the gate; and a polkit-gated root
helper, which would work and puts a privileged component in a package that
installs nothing setuid.
Availability is narrower than "has a TPM": on Fedora /dev/tpmrm0 is root:tss and
desktop users are not in tss, so the ordinary case reports unavailable and keeps
asking for the passphrase. Shipping a udev rule to widen that is refused — it is
the administrator's decision, not an application package's.
◆ WHAT IS ACTUALLY VERIFIED, SINCE THAT IS THE PART THAT USUALLY ISN'T.
This is the only device key store in the repository with real coverage, and it
is only possible because the PIN is a delegate and the TPM is a device: seal,
unseal, wrong PIN, lockout, decline and a foreign blob all run unattended
against a software TPM. Windows and macOS cannot be tested at all — their
prompts block a suite forever, which WindowsDeviceKeyStoreTests records.
TSS.Net's own TcpTpmDevice cannot reach swtpm: it opens the platform socket too
and exchanges a handshake only the Microsoft reference simulator answers, so
Connect() blocks with no error and no timeout. SoftwareTpm speaks the data
channel and nothing else.
The suite also lied once and was fixed: the lockout test trips shared state, and
without a DictionaryAttackLockReset in Dispose every later test got null from a
correct PIN — six of them assert null and passed for entirely the wrong reason.
End to end on a real Fedora 44: a genuine linux-x64 publish packages, installs,
and every DT_NEEDED in the installed tree resolves except liblttng-ust, which is
the one this spec deliberately excludes. What no runner covers is /dev/tpmrm0,
PackageKit and polkit, and Phase 19 grew five checks for exactly those.
Also fixed: the magic-bytes literal picked up raw control characters, which made
LinuxDeviceKeyStore.cs binary to git — no diffs and no line-ending normalisation.
311 lines
19 KiB
RPMSpec
311 lines
19 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 replaces nothing and installs nothing, and that is deliberate rather than unfinished.
|
|
# On Linux the package manager is already an updater the user trusts, audits and controls, so putting
|
|
# a second one inside the application — answering to this project instead — would be building the
|
|
# capability ADR 0013 rule 4 spends a page making unsettable, on the one platform where nobody had to.
|
|
#
|
|
# What the client does do is the half a package manager cannot: LinuxUpdateChannel notices that a
|
|
# newer build exists on the project's forge, fetches it, checks its digest, and hands the file to
|
|
# PackageKit — which raises polkit and asks. The user may say no, and nothing is replaced until they
|
|
# say yes. See ADR 0013 rule 11.
|
|
#
|
|
# The consequence to know: discovery is worse here than on the other two desktops, because there is
|
|
# no package repository to subscribe to and the in-app check is what stands in for one.
|
|
# ==================================================================================================
|
|
|
|
# ◆ 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
|
|
|
|
# ◆ RECOMMENDED RATHER THAN REQUIRED, WHICH IS A JUDGEMENT ABOUT WHAT THE PACKAGE IS FOR.
|
|
#
|
|
# PackageKit is what LinuxUpdateChannel hands a downloaded update to, so without it the update path
|
|
# stops at the last step. It is not required, for two reasons that both point the same way: dnf
|
|
# installs weak dependencies by default, so the ordinary Fedora install gets it anyway; and a machine
|
|
# deliberately built without it — a minimal or an immutable one — should still be able to install an
|
|
# SSH client. The failure is not silent, which is what makes this safe: the channel names the missing
|
|
# service and the file it fetched, so the answer is one command the user was going to run anyway.
|
|
#
|
|
# Nothing is required for the TPM. LinuxDeviceKeyStore talks to /dev/tpmrm0 through a managed stack,
|
|
# so there is no library to depend on, and a machine with no TPM — or one whose device node this user
|
|
# may not open, which is the ordinary Fedora case — simply keeps asking for the passphrase.
|
|
Recommends: PackageKit
|
|
|
|
%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. It checks the project's own forge for newer builds and can
|
|
download one, but it installs nothing itself: an update is handed to PackageKit, which asks before it
|
|
replaces anything. The address it checks is fixed and is never a DodoSSH server.
|
|
|
|
%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.
|