diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml new file mode 100644 index 0000000..b6481a4 --- /dev/null +++ b/.github/workflows/packages.yml @@ -0,0 +1,354 @@ +# Build installable HyprCosmic packages for Fedora, Arch and Debian. +# +# WHY THIS IS A SEPARATE WORKFLOW FROM hyprcosmic.yml +# -------------------------------------------------- +# hyprcosmic.yml beside it answers "does the fork still build and do the assets +# still install where they claim to", on every push, in a few minutes. This one +# compiles 27 Rust components three times over and takes hours. Sharing a file +# would mean either running the slow thing on every push or never running the +# fast thing on a tag, and a `if:` guard threaded through a shared matrix to +# avoid that is harder to read than two files. +# +# WHY IT DOES NOT RUN ON EVERY PUSH +# --------------------------------- +# Three full desktop builds per commit is hours of runner time to produce +# artifacts nobody downloads. Tags get packages because that is when a package +# means something; workflow_dispatch covers wanting one at any other time. +# +# WHY THE WHOLE JOB RUNS IN A CONTAINER +# ------------------------------------- +# Nothing here is statically linked, so a package is only valid on the +# distribution that built it -- an RPM built on Ubuntu's runner would name +# Ubuntu's sonames and refuse to install on Fedora. `container:` puts the +# compile, the staging and the package build all inside an image of the target, +# so the sonames recorded are the ones that will exist on the machine installing +# it. This is also why no step here runs on a developer's workstation: the +# distribution being targeted is rarely the one being typed at. +name: Packages + +on: + push: + tags: ['v*'] + workflow_dispatch: + inputs: + version: + description: 'Version to stamp on the packages' + required: false + default: '0.1.0' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +jobs: + package: + name: ${{ matrix.distro }} + runs-on: ubuntu-latest + container: ${{ matrix.image }} + strategy: + # One distribution failing on a package name is worth seeing on its own, + # and the other two artifacts are still worth having. + fail-fast: false + matrix: + include: + - distro: fedora + image: fedora:44 + - distro: arch + image: archlinux:base-devel + # trixie rather than the bookworm the other workflows use. bookworm has + # no `just` package -- it arrived in trixie -- and its rustc is 1.63, + # so `just` would have to be compiled by a toolchain installed before + # the thing that installs toolchains. CI pays that to prove the crate + # builds on the oldest supported Debian; a package has no such point + # to make. + - distro: debian + image: debian:trixie + + steps: + # Before checkout, deliberately: actions/checkout needs git in the image + # and these are bare. + # + # The library lists are upstream's own, from the components' debian/control + # files, translated per distribution rather than trimmed. + - name: Install build dependencies (fedora) + if: matrix.distro == 'fedora' + run: | + dnf -y install --setopt=install_weak_deps=False \ + git curl ca-certificates just \ + gcc gcc-c++ cmake pkgconf-pkg-config nasm lld mold \ + clang-devel llvm-devel \ + desktop-file-utils rpm-build \ + dbus-devel expat-devel fontconfig-devel freetype-devel \ + libinput-devel libseat-devel libxkbcommon-devel \ + mesa-libgbm-devel libglvnd-devel wayland-devel libdisplay-info-devel \ + pixman-devel cairo-devel pango-devel gtk3-devel gtk4-devel \ + pipewire-devel pulseaudio-libs-devel \ + gstreamer1-devel gstreamer1-plugins-base-devel \ + flatpak-devel systemd-devel libgudev-devel \ + openssl-devel pam-devel libxml2-devel xkeyboard-config-devel + + # Shorter than the others, and not by omission: Arch ships headers in the + # main package rather than splitting a -devel, so `wayland` here is + # `wayland-devel` on Fedora. + - name: Install build dependencies (arch) + if: matrix.distro == 'arch' + run: | + pacman -Syu --noconfirm --needed \ + git curl just cmake pkgconf nasm lld mold clang llvm \ + desktop-file-utils sudo \ + dbus expat fontconfig freetype2 \ + libinput seatd libxkbcommon \ + mesa libglvnd wayland libdisplay-info \ + pixman cairo pango gtk3 gtk4 \ + pipewire libpulse gst-plugins-base-libs \ + flatpak systemd-libs libgudev \ + openssl pam libxml2 xkeyboard-config + + - name: Install build dependencies (debian) + if: matrix.distro == 'debian' + run: | + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + git curl ca-certificates just \ + build-essential cmake pkg-config nasm lld mold \ + clang libclang-dev llvm-dev \ + desktop-file-utils dpkg-dev fakeroot \ + libdbus-1-dev libexpat1-dev libfontconfig-dev libfreetype-dev \ + libinput-dev libseat-dev libxkbcommon-dev \ + libgbm-dev libegl-dev libgles-dev libwayland-dev libdisplay-info-dev \ + libpixman-1-dev libcairo2-dev libpango1.0-dev libgtk-3-dev libgtk-4-dev \ + libpipewire-0.3-dev libpulse-dev \ + libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \ + libflatpak-dev libsystemd-dev libudev-dev libgudev-1.0-dev \ + libssl-dev libpam0g-dev libxml2-dev xkb-data libxcb1-dev + + # submodules: recursive is the whole point -- this repository is 27 + # components plus the two forks, and a checkout without them builds + # nothing. + - uses: actions/checkout@v4 + with: + submodules: recursive + + # --default-toolchain none, then let cosmic-comp's rust-toolchain.toml + # choose. Naming a version here would create a second place to update it + # and a way for the packages to be compiled by something the project does + # not otherwise use. + - name: Install Rust + run: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --default-toolchain none --profile minimal + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + + - name: Show toolchain + run: | + rustup show + cargo --version + just --version + + # Keyed per distribution: the same crate compiled against three different + # glibcs produces three different artifacts, and sharing one cache between + # them would mean each job invalidating the other two. + - uses: Swatinem/rust-cache@v2 + with: + key: pkg-${{ matrix.distro }} + + - name: Determine version + id: ver + run: | + set -eux + # A tag is authoritative; a manual run uses its input; anything else + # falls back so the job is still testable from a branch. + if [ "${GITHUB_REF_TYPE}" = "tag" ]; then + v="${GITHUB_REF_NAME#v}" + else + v="${{ inputs.version || '0.1.0' }}" + fi + echo "version=$v" >> "$GITHUB_OUTPUT" + + - name: Build + run: just build + + # prefix=/usr, not the justfile's /usr/local default. Both .desktop files + # name an absolute Exec under /usr/bin -- a desktop entry cannot + # interpolate a prefix -- so any other prefix stages entries pointing at + # paths this step did not write. + - name: Stage the install + run: just install "$PWD/stage" /usr + + # The staged tree is what all three packages wrap, so it is worth failing + # here rather than shipping a package that is missing the compositor. The + # negative assertion is the one that would rot quietly: nothing may return + # to the private libexec layout this fork used to install into, because a + # copy there is a second compositor that nothing runs and no uninstall + # removes. + - name: Assert the staged tree is a complete desktop + run: | + set -eux + test -x stage/usr/bin/cosmic-comp + test -x stage/usr/bin/cosmic-session + test -x stage/usr/bin/cosmic-conf + test -x stage/usr/bin/start-hyprcosmic + test -x stage/usr/bin/start-cosmic + test -f stage/usr/share/wayland-sessions/hyprcosmic.desktop + test -f stage/usr/share/wayland-sessions/cosmic.desktop + test -f stage/usr/lib/systemd/user/cosmic-session.target + test -f stage/usr/share/cosmic/com.system76.CosmicSettings.Shortcuts/v1/defaults + test -d stage/usr/share/hyprcosmic + test ! -e stage/usr/libexec/hyprcosmic + echo "staged files: $(find stage -type f | wc -l)" + + # ---- Fedora ------------------------------------------------------- + # + # The file list is generated rather than written into the spec. Across 27 + # components a hand-maintained %files would be stale within a week, and + # stale in the direction that omits files nobody misses until a login + # fails. + # + # Directories need care. A %dir line for every staged directory would + # have the package claim /usr, /usr/bin and /usr/share, which the + # `filesystem` package owns -- the RPM would build fine and then refuse to + # install, or worse, take those directories with it on uninstall. So a + # directory is only claimed if no package on the build system already owns + # it, which leaves exactly the ones this fork creates + # (/usr/share/hyprcosmic and friends). + - name: Build the RPM + if: matrix.distro == 'fedora' + run: | + set -eux + mkdir -p rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS} + : > files.list + find stage -mindepth 1 -type d -printf '%P\n' | while read -r d; do + rpm -qf --quiet "/$d" || printf '%%%%dir "/%s"\n' "$d" >> files.list + done + find stage -mindepth 1 \! -type d -printf '"/%P"\n' >> files.list + wc -l files.list + rpmbuild -bb packaging/fedora/hyprcosmic.spec \ + --define "_topdir $PWD/rpmbuild" \ + --define "stagedir $PWD/stage" \ + --define "filelist $PWD/files.list" \ + --define "ver ${{ steps.ver.outputs.version }}" + mkdir -p dist + find rpmbuild/RPMS -name '*.rpm' -exec cp -v {} dist/ \; + + # A package that installs is the claim being made, so it is tested rather + # than assumed. --setopt=tsflags=test does the whole resolution and + # conflict check without writing to the container. + - name: Verify the RPM + if: matrix.distro == 'fedora' + run: | + set -eux + rpm -qpi dist/*.rpm + rpm -qp --requires dist/*.rpm + dnf -y install --setopt=tsflags=test dist/*.rpm + + # ---- Arch --------------------------------------------------------- + # + # makepkg refuses to run as root, and a container is root by default, so + # the build runs as an unprivileged user that owns the tree it reads. + # --nodeps because the depends array names a running system's runtime + # libraries, which this image has no reason to hold, and nothing is being + # compiled at this point anyway. + - name: Build the Arch package + if: matrix.distro == 'arch' + run: | + set -eux + useradd -m builder + mkdir -p dist + cp packaging/arch/PKGBUILD . + chown -R builder:builder . + sudo -u builder \ + HYPRCOSMIC_STAGEDIR="$PWD/stage" \ + HYPRCOSMIC_VERSION="${{ steps.ver.outputs.version }}" \ + PKGDEST="$PWD/dist" \ + makepkg --nodeps --noconfirm + + - name: Verify the Arch package + if: matrix.distro == 'arch' + run: | + set -eux + pacman -Qip dist/*.pkg.tar.zst + # Contents rather than an install: pacman has no dry run that resolves + # dependencies without touching the filesystem, and this image is not + # a desktop, so a real install would fail on runtime libraries that + # say nothing about whether the package is well formed. + pacman -Qlp dist/*.pkg.tar.zst | head -20 + + # ---- Debian ------------------------------------------------------- + # + # dpkg-deb --build over a staged tree, rather than a full source package. + # The Depends line is computed by dpkg-shlibdeps from the binaries + # themselves rather than written by hand -- 27 components link against + # more libraries than anyone will keep an accurate list of, and a hand + # list is wrong in the direction that installs and then fails to start. + # + # dpkg-shlibdeps insists on a debian/control in the working directory even + # when invoked outside a source package, hence the stub. + - name: Build the Debian package + if: matrix.distro == 'debian' + run: | + set -eux + mkdir -p debian + printf 'Source: hyprcosmic\n\nPackage: hyprcosmic\nArchitecture: amd64\n' > debian/control + binaries=$(find stage -type f -perm -100 -exec sh -c 'file -b "$1" | grep -q ELF && echo "$1"' _ {} \;) + dpkg-shlibdeps -O --ignore-missing-info $binaries > shlibdeps.txt + deps=$(sed 's/^shlibs:Depends=//' shlibdeps.txt) + size=$(du -sk stage | cut -f1) + + mkdir -p stage/DEBIAN + sed -e "s|@VERSION@|${{ steps.ver.outputs.version }}|" \ + -e "s|@INSTALLED_SIZE@|$size|" \ + -e "s|@SHLIB_DEPENDS@|$deps|" \ + packaging/debian/control.in > stage/DEBIAN/control + cat stage/DEBIAN/control + + mkdir -p dist + dpkg-deb --build --root-owner-group stage \ + "dist/hyprcosmic_${{ steps.ver.outputs.version }}_amd64.deb" + + - name: Verify the Debian package + if: matrix.distro == 'debian' + run: | + set -eux + dpkg-deb --info dist/*.deb + dpkg-deb --contents dist/*.deb | head -20 + # lintian is not installed and would fail this package on a dozen + # policy points that do not apply to a desktop fork shipped outside + # the archive. What matters here is that dpkg can read it back. + dpkg-deb --fsys-tarfile dist/*.deb | tar -tf - >/dev/null + + - uses: actions/upload-artifact@v4 + with: + name: hyprcosmic-${{ matrix.distro }} + path: dist/ + retention-days: 30 + + # Only on a tag. A dispatch run is for getting artifacts to try, and turning + # one into a public release would make every experiment look like a shipped + # version. + release: + needs: package + if: github.ref_type == 'tag' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + + - name: Attach the packages to the release + uses: softprops/action-gh-release@v2 + with: + files: dist/* + # Draft, deliberately. These packages conflict with the + # distribution's cosmic-comp and cosmic-session, so installing one + # replaces the machine's desktop. That is worth a human reading the + # notes before it is published rather than a tag push making it + # available. + draft: true + generate_release_notes: true diff --git a/README.md b/README.md index c2dee31..0e28f09 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,12 @@ Three things distinguish a HyprCosmic session from a COSMIC one: - **HyDE's shell.** waybar instead of cosmic-panel, rofi instead of cosmic-launcher, `awww` instead of cosmic-bg. HyDE themes are imported directly, palette and wallpapers and all. -- **It installs beside COSMIC, not over it.** The two modified binaries go to - `/usr/libexec/hyprcosmic/`, and the session gets its own entry on the - greeter's menu. A machine with COSMIC from its distribution keeps that session - working, which matters on the day this one does not start. +- **It replaces COSMIC rather than sitting next to it.** The binaries install as + `/usr/bin/cosmic-comp` and `/usr/bin/cosmic-session`, the paths a cosmic-comp + and a cosmic-session go to, and the packages conflict with the distribution's + accordingly. Both session entries are installed, so the greeter still offers a + stock COSMIC shell for the day the HyDE one does not start — now served by + these binaries rather than by a second copy on disk. ## Repository layout @@ -52,8 +54,8 @@ current. deliberately small: `dispatch exec` and `dispatch killactive` are rejected, because this is the surface any process that can open the socket gets. - New windows open *beside* the focused window rather than inside it. -- The install goes to `/usr/libexec/hyprcosmic/cosmic-comp`, and the shortcut - defaults file is left alone. +- The install goes to `/usr/bin/cosmic-comp`, at upstream's paths and alongside + upstream's two `.ron` defaults files, which are carried unmodified. **cosmic-session** — profiles. `HYPRCOSMIC_PROFILE=hyprcosmic` (set by `hyprcosmic.desktop`) skips cosmic-panel, cosmic-launcher, cosmic-app-library, @@ -81,6 +83,27 @@ trixie; `cargo install just --locked` covers it. ## Installing +The easiest route is a package. Every tag builds one for Fedora, Arch and Debian +and attaches it to a draft release; `workflow_dispatch` on **Packages** builds +them at any other time and leaves them as run artifacts. + +```shell +sudo dnf install ./hyprcosmic-*.rpm # Fedora +sudo pacman -U ./hyprcosmic-*.pkg.tar.zst # Arch +sudo dpkg -i ./hyprcosmic_*_amd64.deb # Debian +``` + +Expect this to fail the first time, and read what it says when it does. These +packages provide `/usr/bin/cosmic-comp` and `/usr/bin/cosmic-session`, so they +**conflict with the distribution's `cosmic-comp` and `cosmic-session`** and your +package manager will refuse until those are removed. That refusal is the design: +installing HyprCosmic replaces the machine's desktop, and it should take a +deliberate `dnf remove cosmic-comp cosmic-session` to say so rather than a +resolver deciding on your behalf. Both session entries survive the swap, so the +greeter still offers a stock COSMIC shell afterwards. + +Building it yourself instead: + ```shell sudo just install '' /usr ``` @@ -225,9 +248,18 @@ Two workflows, on purpose: and an `install-assets.sh` round trip into a staging root, verified with `--check`. -The two forks carry a `hyprcosmic.yml` of the same shape, each asserting that -its install landed in `/usr/libexec/hyprcosmic/` and that the files owned by the -distribution's COSMIC packages went unwritten. +The two forks carry a `hyprcosmic.yml` of the same shape, each building on +Fedora, Debian and Arch and asserting that its install landed at upstream's +paths — and that nothing landed in the private `/usr/libexec/hyprcosmic/` this +fork used to use, which is the assertion that would otherwise rot quietly. + +`packages.yml` in this repository builds installable packages for the same three +distributions: an RPM, a `.pkg.tar.zst` and a `.deb`, each compiled inside a +container of the distribution it targets so the sonames it records are the ones +the installing machine will have. It runs on tags and on demand, not on every +push — three full desktop builds is hours of runner time. Tags additionally open +a **draft** release with the packages attached; drafts rather than published, +because installing one of these replaces the machine's desktop. The waybar generator deserves its own note. `config.jsonc` is generated from `config.jsonc.in` and a codepoint table in `generate-config.py`, and is never diff --git a/cosmic-comp b/cosmic-comp index 139a1de..5f17699 160000 --- a/cosmic-comp +++ b/cosmic-comp @@ -1 +1 @@ -Subproject commit 139a1de4e84e76a9e02f93b54d37e0133d2ae390 +Subproject commit 5f176995dc901e7f9a0f351e3091589bdff29173 diff --git a/cosmic-session b/cosmic-session index 8ff1e72..73867a5 160000 --- a/cosmic-session +++ b/cosmic-session @@ -1 +1 @@ -Subproject commit 8ff1e72d3e3943eaf07ec8f546ffac3ba740ac74 +Subproject commit 73867a5d6a31560507ad86a6fb9ab4d929c21258 diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD new file mode 100644 index 0000000..b2679df --- /dev/null +++ b/packaging/arch/PKGBUILD @@ -0,0 +1,60 @@ +# HyprCosmic, as one Arch package. +# +# There is no build() here, and no source array. This PKGBUILD packages a tree +# that `just install` has already staged, which tools/make-packages.sh points at +# through $HYPRCOSMIC_STAGEDIR. The reasoning is in packaging/fedora/hyprcosmic.spec +# and applies identically: building 27 Rust components a second time under +# makepkg, to produce bytes that already exist, costs hours and creates a way +# for the packaged desktop and the built one to drift apart. +# +# The consequence, same as there: this package is only valid on the Arch that +# built it. make-packages.sh builds it inside archlinux:base-devel so that is a +# current Arch rather than whatever the host is. + +pkgname=hyprcosmic +pkgver=${HYPRCOSMIC_VERSION:-0.1.0} +pkgrel=1 +pkgdesc="COSMIC configured in Hyprland's idiom, with a HyDE shell" +arch=('x86_64') +url="https://github.com/outbackdingo/hyprcosmic" +license=('GPL-3.0-only') + +# The HyDE shell. Without these the session starts to a blank screen: no bar, +# no launcher, no wallpaper. +depends=('waybar' 'rofi-wayland' 'wayland' 'libxkbcommon' 'libinput' 'seatd' + 'mesa' 'pixman' 'libdisplay-info' 'systemd-libs') + +# awww is in the AUR rather than in the repositories, so it cannot be a hard +# depends without making the package uninstallable for anyone who has not built +# it. Without it there is no wallpaper, which is recoverable; an unsatisfiable +# dependency is not. +optdepends=('awww: wallpaper daemon, required for HyDE theme wallpapers' + 'ttf-nerd-fonts-symbols: glyphs the waybar config draws with' + 'qt5ct: Qt application theming to match the GTK theme') + +# What "complete replacement" means in packaging terms. Every path this writes +# under /usr/bin is one cosmic-comp and cosmic-session also own, so the two +# cannot coexist -- which is correct, they are two builds of the same programs. +# +# conflicts without replaces/provides-driven auto-removal: pacman stops and +# names the conflict rather than quietly removing the desktop the machine is +# currently running. +conflicts=('cosmic-comp' 'cosmic-session') +provides=("cosmic-comp=$pkgver" "cosmic-session=$pkgver") + +options=('!strip' '!debug') + +package() { + # Set by make-packages.sh. Failing loudly here beats producing an empty + # package, which is what a bare `cp -a "$unset/."` would do. + if [ -z "$HYPRCOSMIC_STAGEDIR" ] || [ ! -d "$HYPRCOSMIC_STAGEDIR/usr" ]; then + echo "HYPRCOSMIC_STAGEDIR unset or has no usr/; see tools/make-packages.sh" >&2 + return 1 + fi + cp -a "$HYPRCOSMIC_STAGEDIR/." "$pkgdir/" + + # The two files a broken install shows up in first: a bad Exec line puts an + # entry on the greeter's menu that fails silently when it is chosen. + desktop-file-validate "$pkgdir/usr/share/wayland-sessions/hyprcosmic.desktop" + desktop-file-validate "$pkgdir/usr/share/wayland-sessions/cosmic.desktop" +} diff --git a/packaging/debian/control.in b/packaging/debian/control.in new file mode 100644 index 0000000..b66f7fa --- /dev/null +++ b/packaging/debian/control.in @@ -0,0 +1,32 @@ +Package: hyprcosmic +Version: @VERSION@ +Architecture: amd64 +Maintainer: dingo +Section: x11 +Priority: optional +Homepage: https://github.com/outbackdingo/hyprcosmic +Installed-Size: @INSTALLED_SIZE@ +Depends: @SHLIB_DEPENDS@ +Recommends: waybar, rofi +Suggests: fonts-hack-ttf, qt5ct +Conflicts: cosmic-comp, cosmic-session +Provides: cosmic-comp, cosmic-session +Replaces: cosmic-comp, cosmic-session +Description: COSMIC configured in Hyprland's idiom, with a HyDE shell + HyprCosmic is a fork of the COSMIC desktop that takes its configuration in + Hyprland's idiom and wears a HyDE-style shell. + . + A single ~/.config/hyprcosmic/cosmic.conf -- with general { } blocks, bind = + lines and $variables -- is compiled into COSMIC's own configuration tree by + cosmic-conf. The file is the source of truth: keys it names are applied at + every login over whatever the settings UI last stored, and keys it does not + name are left alone. + . + The shell is waybar, rofi and awww in place of cosmic-panel, cosmic-launcher + and cosmic-bg, and HyDE themes are imported directly. The compositor is + COSMIC's, with a Hyprland-compatible IPC socket so that HyDE's scripts and + waybar's hyprland modules work unmodified. + . + This package replaces the distribution's COSMIC. It installs both session + entries, so the greeter offers a stock COSMIC shell as well as the HyDE one, + both served by these binaries. diff --git a/packaging/fedora/hyprcosmic.spec b/packaging/fedora/hyprcosmic.spec new file mode 100644 index 0000000..e5b40a8 --- /dev/null +++ b/packaging/fedora/hyprcosmic.spec @@ -0,0 +1,123 @@ +# HyprCosmic, as one RPM. +# +# WHY THIS REPACKS RATHER THAN REBUILDS +# ------------------------------------- +# There is no %build here. The spec packages a tree that `just install` has +# already produced, which .github/workflows/packages.yml stages and passes in as +# --define "stagedir ...". +# +# The alternative -- a spec that runs `just build` itself under rpmbuild -- is +# the more orthodox shape and is wrong for this project. `just build` compiles +# 27 Rust components; doing it a second time inside rpmbuild to produce bytes +# identical to the ones already sitting in the tree costs hours and buys +# nothing. Worse, it would let the packaged desktop and the `just install` +# desktop drift apart, and the whole point of shipping a package is that the +# two are the same thing. +# +# The cost of this choice, stated plainly: the resulting RPM is only valid on +# the distribution it was built on. Nothing here is statically linked, so an RPM +# built on Fedora 44 assumes Fedora 44's glibc, wayland, libinput and mesa. Build +# it on the release you intend to install it on. The workflow does that by +# running the whole job inside a container of the target distribution. +# +# WHY IT IS ONE PACKAGE AND NOT TWENTY-SEVEN +# ------------------------------------------ +# Fedora splits COSMIC into a package per component, which is right for a +# distribution tracking upstream. This is a fork that replaces the desktop as a +# unit: the compositor, the session and the config compiler are versioned and +# tested together, and there is no supported combination in which you take the +# HyprCosmic cosmic-comp and the distribution's cosmic-session. One package is +# an accurate description of what is actually supported. + +%global _hardened_build 1 + +# Debuginfo extraction re-links every binary in the tree and would add an hour +# to a package whose binaries were built elsewhere anyway. There is nothing to +# strip usefully here. +%global debug_package %{nil} + +Name: hyprcosmic +Version: %{?ver}%{!?ver:0.1.0} +Release: %{?rel}%{!?rel:1}%{?dist} +Summary: COSMIC configured in Hyprland's idiom, with a HyDE shell + +License: GPL-3.0-only +URL: https://github.com/outbackdingo/hyprcosmic +BuildArch: x86_64 + +# These are what "complete replacement" means in packaging terms. Every path +# this package writes under /usr/bin and /usr/share/cosmic is owned by one of +# these on a stock Fedora, so the two cannot be installed at once -- which is +# correct, because they are two builds of the same programs. +# +# Conflicts rather than Obsoletes, deliberately. Obsoletes would let a routine +# `dnf install hyprcosmic` quietly remove the desktop the machine is currently +# running. Conflicts stops and says so, and removing the COSMIC packages stays +# something a person decides to do rather than something a resolver does on +# their behalf. +Conflicts: cosmic-comp +Conflicts: cosmic-session + +# What it stands in for, so anything depending on a COSMIC session is satisfied. +Provides: cosmic-comp = %{version}-%{release} +Provides: cosmic-session = %{version}-%{release} + +# The HyDE shell. These are separate programs this fork drives rather than +# builds, and without them the session starts to a blank screen with no bar and +# no launcher. +Requires: waybar +Requires: rofi-wayland + +# The wallpaper daemon. Recommends rather than Requires because it lives in the +# alebastr/sway-extras COPR rather than in Fedora proper, and a hard dependency +# that cannot resolve would make this package uninstallable on a machine that +# has not enabled that repository. Without it you get no wallpaper; with it and +# no theme imported, you also get no wallpaper. Both are recoverable; an +# unsatisfiable dependency is not. +Recommends: awww + +# Nerd Font glyphs are most of what the bar draws. +Recommends: nerd-fonts + +%description +HyprCosmic is a fork of the COSMIC desktop that takes its configuration in +Hyprland's idiom and wears a HyDE-style shell. + +A single ~/.config/hyprcosmic/cosmic.conf -- with general { } blocks, bind = +lines and $variables -- is compiled into COSMIC's own configuration tree by +cosmic-conf. The file is the source of truth: keys it names are applied at every +login over whatever the settings UI last stored, and keys it does not name are +left alone. + +The shell is waybar, rofi and awww in place of cosmic-panel, cosmic-launcher and +cosmic-bg, and HyDE themes are imported directly. The compositor is COSMIC's, +with a Hyprland-compatible IPC socket so that HyDE's scripts and waybar's +hyprland modules work unmodified. + +This package replaces the distribution's COSMIC. It installs both session +entries, so the greeter offers a stock COSMIC shell as well as the HyDE one, +both served by these binaries. + +%prep +# Nothing to unpack. See the note at the top of this file. + +%install +test -n "%{stagedir}" || { echo "define stagedir: see .github/workflows/packages.yml" >&2; exit 1; } +test -d "%{stagedir}/usr" || { echo "%{stagedir}/usr missing; run just install first" >&2; exit 1; } +cp -a "%{stagedir}/." "%{buildroot}/" + +# The desktop entries are the two files a broken install shows up in first, so +# they are validated rather than assumed. A .desktop with a bad Exec line puts +# an entry on the greeter's menu that fails silently when chosen. +desktop-file-validate "%{buildroot}%{_datadir}/wayland-sessions/hyprcosmic.desktop" +desktop-file-validate "%{buildroot}%{_datadir}/wayland-sessions/cosmic.desktop" + +# Generated by the workflow from the staged tree rather than written out here. +# A hand-maintained list across 27 components would be wrong within a week, and +# wrong in the direction that ships a package missing files nobody notices until +# a login fails. +%files -f %{filelist} + +%changelog +* Sun Aug 10 2026 dingo - 0.1.0-1 +- First package of the fork: COSMIC replaced as a unit, HyDE shell, cosmic-conf.