Skip to main content
·
Arun Babu Neelicattu
·13 min read

Automated RPM Repackaging with COPR: Bridging Standalone Binary Releases to DNF Repositories

Modern app frameworks make standalone RPMs easy to build, but without a native package repository Linux users must manually download and reinstall every update. This guide builds an automated downstream pipeline with Fedora COPR, Tito, and GitHub Actions.

Republication note: Republished with the author’s permission from the original post on abn.is, published 14 August 2026. The body prose, headings, links, lists, commands, and code examples are unchanged apart from normalizing trailing whitespace in code. Nowledge Labs added the summary and tags as editorial metadata, self-hosts the original hero, renders both diagrams from the author’s Mermaid source, presents the original callout boxes as blockquotes, and added the community-repository notice below.

Community repository notice: The COPR repository described in this article is community-maintained and unofficial. It is not an official Nowledge Labs DNF repository.

Modern app frameworks like Tauri, Electron, and cargo-packager make it straightforward to build desktop applications and CLI binaries into standalone .rpm and .deb files. However, publishing standalone package files creates a distinct distribution problem for Linux users. Without a native package repository (dnf / zypper / apt), end users must manually download and re-install packages for every update. For fast-moving software projects releasing updates daily or multiple times per day, this manual workflow becomes unsustainable for users.

Getting a new application accepted into official Linux distribution repositories (such as Fedora or Debian official package repos) requires significant maintainer overhead: submission reviews, compliance with strict source-build guidelines, unbundling dependencies, and long approval queues. Small teams and independent developers cannot easily maintain official packages across every Linux distribution.

This guide details how to build an automated downstream packaging pipeline using Fedora COPR, Tito, and GitHub Actions. This pattern ingests upstream standalone binary RPMs, extracts and modularizes their components, tags releases, and publishes an updated COPR repository automatically.

The Linux Packaging Bottleneck

When developers package applications with framework tools, the resulting output is often a single standalone .rpm or .deb file uploaded to GitHub Releases or an HTTP download endpoint.

Upstream Build Pipeline Default User Experience (No Repository) New Release Published App Code: Tauri / Electron / Rust Build Tooling: cargo-packager / electron-builder Standalone Binary RPM Artifact User manual download via Browser Manual shell command: sudo dnf install ./app.rpm Stale local installation

UX Failures of Standalone Binary Releases

  1. No Automatic System Updates: dnf update or system update GUI applications (GNOME Software, KDE Discover) cannot discover new versions.
  2. High Update Friction: Users must re-visit release pages, download new files, and run local installation commands manually. When release cadences reach 1 to 2 releases per day, user update adoption drops significantly.
  3. Monolithic Dependency Graphs: Framework packagers frequently bundle desktop GUI, CLI tools, and background server daemons into a single heavy package, preventing modular server-only or CLI-only deployments.

Desktop Distribution Alternatives: Flatpak and AetherPak

For desktop applications, Flatpak provides a sandboxed distribution mechanism with automatic updates. However, submitting applications to Flathub involves strict submission requirements and manual reviewer approvals.

Self Managed Flatpak Repositories: AetherPak

For developers seeking automated Flatpak distribution outside Flathub, projects can publish via alternative indexes like AetherPak.

A concrete reference implementation is the Lemonade Tauri App Flatpak repository, which packages Tauri desktop applications into standalone Flatpak bundles that support automatic updates as upstream releases appear.

For server daemons, CLI binaries, and systemd integration, native RPM repositories remain the standard approach for Fedora, RHEL, CentOS Stream, and openSUSE ecosystems.

Architecture of an Automated Repackaging Pipeline

Fedora COPR (originally Cool Other Package Repositories, and now Community Projects) allows maintainers and community contributors to host automatic RPM repositories. By pairing COPR with Tito (a tool for managing RPM package lifecycles in Git) and GitHub Actions, you can transform standalone binary RPM downloads into a fully automated repository pipeline.

COPR Dist-Git and Source Asset Management

A common misconception when repackaging pre-compiled binaries is that large binary files (x86_64-unknown-linux-gnu.rpm) must be committed directly to Git or tracked with Git LFS. In Fedora COPR, large binary source files do not belong in Git history.

Instead, Fedora COPR leverages a dist-git lookaside cache mechanism:

  • The Git repository tracks only lightweight text files: the .spec file, configuration metadata, and a sources file containing MD5/SHA256 checksums of binary payload files.
  • During build execution, COPR’s dist-git-client sources tool fetches the corresponding binary artifact from the lookaside cache or specified HTTP endpoint before launching rpmbuild inside the isolated Mock build chroot.
  • When dynamic downloads occur during Mock build execution, ensure enable_net: True (or net_access: true) is configured in COPR project settings. The corresponding setting is a checkbox “Allow internet access”.
alt [New Version Detected] Run version check & download binary RPM Query latest download URL / headers Return version metadata (e.g. 0.10.59) Download binary RPM artifact (x86_64-unknown-linux-gnu.rpm) Update Version: tag & reset Release: in spec file Run tito tag --use-version <version> Commit changes, generate changelog & Git tag Push commits & tags to GitHub Trigger SRPM build via copr-cli Fetch lookaside source, extract RPM payload, build sub-packages dnf update (fetches new release automatically) GitHub Actions (4h Cron) Upstream HTTP Endpoint update_version.py Tito Release Manager Fedora COPR Service End User (DNF)

Design Rationale (COPR Dist-Git Lookaside Cache vs Git LFS)

Committing compiled binary RPMs directly to Git repositories causes rapid repository bloat. Utilizing COPR’s dist-git lookaside cache keeps the packaging Git repository lightweight (containing only text spec files and version scripts). Git LFS remains an alternative for self-hosted or air-gapped CI environments where external lookaside storage is unavailable.

Design Rationale (rpm2cpio Repackaging vs Source Compilation)

Re-building proprietary or complex multi-architecture binaries from source inside standard RPM build environments often requires reproducing complex toolchains. Repackaging pre-compiled upstream binary RPMs via rpm2cpio in the spec %prep phase isolates the distribution mechanism from the build environment while preserving native system integration (systemd service units, desktop menu entry caches, binary symlinks).

Implementation Walkthrough

1. Designing the RPM Spec File

The spec file accepts the pre-fetched upstream binary .rpm file, unpacks its contents using rpm2cpio and cpio, and distributes the files into appropriate package paths and sub-packages using standard RPM macros (%{_bindir}, %{_datadir}, %{_prefix}/lib).

Below is an example spec file (nowledge-mem.spec) supporting four sub-packages: a metapackage (nowledge-mem), a CLI package (nowledge-mem-cli), a GUI package (nowledge-mem-desktop), and a headless daemon package (nowledge-mem-server).

spec
Name: nowledge-mem Version: 0.10.59 Release: 1%{?dist} Summary: Personal memory and context management system (Metapackage) License: LicenseRef-Proprietary URL: https://download-mem.nowledge.co Source0: x86_64-unknown-linux-gnu.rpm ExclusiveArch: x86_64 # AutoReqProv: no prevents RPM build helpers from failing on unbundled internal libraries in pre-compiled binaries AutoReqProv: no %global debug_package %{nil} BuildRequires: cpio BuildRequires: rpm BuildRequires: systemd-rpm-macros Provides: nmem = %{version}-%{release} Requires: nowledge-mem-cli = %{version}-%{release} Requires: nowledge-mem-desktop = %{version}-%{release} Requires: nowledge-mem-server = %{version}-%{release} %description Metapackage installing desktop GUI, CLI tools, and local backend server. %package cli Summary: Nowledge Mem CLI and TUI tools AutoReqProv: no %description cli Command-line interface (nmem) and Terminal UI (nmem-tui). %package desktop Summary: Nowledge Mem Desktop GUI client AutoReqProv: no Requires: nowledge-mem-cli = %{version}-%{release} Requires: gtk3 Requires: (webkit2gtk4.1 or webkit2gtk4.0 or webkit2gtk3) %description desktop Desktop GUI client and desktop entry icons. %package server Summary: Nowledge Mem backend server daemon AutoReqProv: no %{?systemd_requires} %description server Headless server daemon with systemd user service units. %prep %setup -q -c -T # Multi-path search for binary payload across local directory, Mock chroot, and SRPM sources RPM_FILE="" for f in \ "/sources/build/x86_64-unknown-linux-gnu.rpm" \ "/sources/x86_64-unknown-linux-gnu.rpm" \ "/builddir/build/SOURCES/x86_64-unknown-linux-gnu.rpm" \ "%{_sourcedir}/x86_64-unknown-linux-gnu.rpm" \ "%{SOURCE0}"; do if [ -f "$f" ] && rpm2cpio "$f" >/dev/null 2>&1; then RPM_FILE="$f" break fi done if [ -n "$RPM_FILE" ]; then echo "Extracting binary payload from: $RPM_FILE" rpm2cpio "$RPM_FILE" | cpio -idmv else echo "ERROR: Upstream binary RPM not found in build paths." >&2 exit 1 fi %build # Pre-compiled binary package; no build step required. %install rm -rf %{buildroot} mkdir -p %{buildroot}%{_bindir} mkdir -p %{buildroot}%{_prefix}/lib mkdir -p %{buildroot}%{_datadir}/applications mkdir -p %{buildroot}%{_datadir}/icons mkdir -p %{buildroot}%{_userunitdir} cp -a usr/bin/* %{buildroot}%{_bindir}/ 2>/dev/null || true cp -a "usr/lib/Nowledge Mem" %{buildroot}%{_prefix}/lib/ cp -a usr/share/applications/* %{buildroot}%{_datadir}/applications/ 2>/dev/null || true cp -a usr/share/icons/* %{buildroot}%{_datadir}/icons/ 2>/dev/null || true # Install Systemd User Service Unit cat << 'EOF' > %{buildroot}%{_userunitdir}/nowledge-mem.service [Unit] Description=Nowledge Mem Server Daemon (User Service) After=network.target [Service] Type=simple ExecStart=/usr/bin/nmem-server Restart=always RestartSec=5 [Install] WantedBy=default.target EOF %post desktop if command -v gtk-update-icon-cache >/dev/null 2>&1; then gtk-update-icon-cache -q -t -f %{_datadir}/icons/hicolor 2>/dev/null || true fi %post server %systemd_user_post nowledge-mem.service %preun server %systemd_user_preun nowledge-mem.service %files cli %{_bindir}/nmem "%{_prefix}/lib/Nowledge Mem/_up_/rust-backend/nmem" %files desktop %{_bindir}/nowledge-mem "%{_datadir}/applications/Nowledge Mem.desktop" %{_datadir}/icons/hicolor/* %files server %{_bindir}/nmem-server %{_userunitdir}/nowledge-mem.service "%{_prefix}/lib/Nowledge Mem/_up_/rust-backend/nmem-server"

Design Rationale (Sub-package Modularization)

Splitting a monolithic application into discrete sub-packages (cli, desktop, server) allows headless servers and remote container environments to install nowledge-mem-server without pulling heavy desktop dependencies like gtk3 or webkit2gtk.

2. Upstream Version Resolution & Downloader Script

To automate version checks and pre-fetch the binary payload for Tito SRPM builds, write a Python script (update_version.py) to inspect the upstream redirect URL, update the spec file Version:, reset Release: to 1, and save the binary artifact to x86_64-unknown-linux-gnu.rpm.

python
#!/usr/bin/env python3 """ Upstream Version Resolver and Downloader for Repackaging Pipelines. Note: Adjust regex patterns in resolve_latest_version() to match your project's specific upstream release endpoint or redirect URL structure. """ import argparse import os import re import urllib.request import sys from pathlib import Path VERSION_REDIRECT_URL = "https://download-mem.nowledge.co/download-mem-rpm" SPEC_FILE_PATH = Path("nowledge-mem.spec") BINARY_OUTPUT_PATH = Path("x86_64-unknown-linux-gnu.rpm") def resolve_latest_version(): req = urllib.request.Request( VERSION_REDIRECT_URL, headers={"User-Agent": "Mozilla/5.0 (X11; Linux x86_64)"} ) with urllib.request.urlopen(req) as resp: final_url = resp.geturl() # Adapt regex pattern to match upstream URL scheme (e.g., /app/<version>/ or /releases/v<version>/) match = re.search(r"/app/([^/]+)/", final_url) if not match: raise RuntimeError(f"Could not parse version from URL: {final_url}") return match.group(1), final_url def update_spec_version(new_version, is_new=True): content = SPEC_FILE_PATH.read_text() updated = re.sub(r"^(Version:\s*)[^\s]+", f"\\g<1>{new_version}", content, flags=re.MULTILINE) if is_new: updated = re.sub(r"^(Release:\s*)[^\s]+", r"\g<1>1%{?dist}", updated, flags=re.MULTILINE) if content != updated: SPEC_FILE_PATH.write_text(updated) def download_upstream_rpm(download_url): print(f"Downloading upstream binary artifact from {download_url}...") req = urllib.request.Request( download_url, headers={"User-Agent": "Mozilla/5.0 (X11; Linux x86_64)"} ) with urllib.request.urlopen(req) as resp, open(BINARY_OUTPUT_PATH, "wb") as f: while chunk := resp.read(8192): f.write(chunk) print(f"Saved binary payload to {BINARY_OUTPUT_PATH}") def main(): parser = argparse.ArgumentParser() parser.add_argument("--check-only", action="store_true") args = parser.parse_args() latest_version, download_url = resolve_latest_version() current_version = None if SPEC_FILE_PATH.exists(): match = re.search(r"^Version:\s*([^\s]+)", SPEC_FILE_PATH.read_text(), re.MULTILINE) if match: current_version = match.group(1) is_new = (current_version != latest_version) if args.check_only: print(f"is_new={'true' if is_new else 'false'}") sys.exit(0) if is_new or not BINARY_OUTPUT_PATH.exists(): update_spec_version(latest_version, is_new=is_new) download_upstream_rpm(download_url) if __name__ == "__main__": main()

3. Automated Version Tagging with Tito

Tito simplifies Git-based RPM management by maintaining package versions, generating changelogs, and managing Git tags automatically.

Git Hygiene Rule

Avoid editing %changelog manually in the spec file when using Tito. Tito manages %changelog during tito tag unless otherwise specified explicitly. Uncommitted manual changes in the working tree will cause tito tag to fail.

Initialize Tito in the project root directory:

bash
tito init

To create a new release tag automatically when the spec file updates:

bash
tito tag --use-version "0.10.59" --accept-auto-changelog

4. Continuous Integration Workflow (GitHub Actions)

Configure a GitHub Actions workflow (.github/workflows/check-updates.yml) to poll the upstream endpoint every 4 hours. When a new version is detected, tito tag generates clean Git release tags, while SRPM generation and copr-cli leverage the dist-git source specification.

yaml
name: Automated Update Check & Tito Release on: schedule: - cron: '0 */4 * * *' workflow_dispatch: permissions: contents: write jobs: check-and-release: runs-on: ubuntu-latest container: image: fedora:latest options: --user root steps: - name: Install Dependencies run: | dnf install -y git tito python3 copr-cli rpmdevtools git config --global user.name "github-actions[bot]" git config --global user.email "github-actions[bot]@users.noreply.github.com" git config --global --add safe.directory "*" - name: Checkout Code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Check Upstream Version and Update Spec id: updater run: | python3 update_version.py - name: Tag Release with Tito id: tito_release run: | if [ -n "$(git status --porcelain nowledge-mem.spec)" ]; then NEW_VER=$(python3 -c "import re; print(re.search(r'^Version:\s*([^\s]+)', open('nowledge-mem.spec').read(), re.MULTILINE).group(1))") # Tito automatically stages modified spec files, updates changelog, and creates release commit tito tag --use-version "${NEW_VER}" --accept-auto-changelog echo "CHANGED=true" >> $GITHUB_OUTPUT fi - name: Push Commits and Tags if: steps.tito_release.outputs.CHANGED == 'true' run: | git push origin main --follow-tags - name: Dispatch Build to COPR if: steps.tito_release.outputs.CHANGED == 'true' env: COPR_CONFIG: ${{ secrets.COPR_CONFIG }} run: | mkdir -p ~/.config echo "$COPR_CONFIG" > ~/.config/copr tito build --srpm --output=build/ SRPM_FILE=$(ls build/*.src.rpm | head -n 1) copr-cli build user/nowledge-mem "$SRPM_FILE" --nowait

Design Rationale (Automated Tito Tagging in CI)

Automating tito tag inside a containerized GitHub Actions runner ensures consistent release versioning and changelog entries while keeping local developer setup minimal.

Security Model and Trust Caveats

Repackaging third-party binary artifacts introduces security responsibilities that repository maintainers and end users must acknowledge.

Security Caveat (Third-Party Repositories and Pre-compiled Binaries)

When enabling a community COPR repository containing repackaged binary RPMs, end users delegate complete trust to both the upstream project maintainers and the repackaging repository maintainer.

  • No Source Build Verification: Repackaging pipelines extract pre-compiled binaries (rpm2cpio). The COPR builder does not compile source code, meaning COPR build logs only verify extraction and packaging steps, not binary compilation integrity.
  • Repository Isolation: Users should avoid adding untrusted community COPR repositories to production machines. Always inspect the underlying spec file and GitHub Actions pipeline before executing sudo dnf copr enable <user>/<repo>.

Summary Command Reference

For users consuming packages published through this pipeline:

bash
# Enable the community COPR repository sudo dnf copr enable user/nowledge-mem # Install the full metapackage (Desktop + Server + CLI) sudo dnf install nowledge-mem # Or install headless server daemon only sudo dnf install nowledge-mem-server # Enable and start the systemd user service systemctl --user enable --now nowledge-mem.service

With this automation in place, project maintainers can publish standalone binaries via their standard framework build tooling, while Linux users retain native dnf update capabilities and systemd integration.

Project Source Code

The source code for this work is available on GitHub.

© 2026 Nowledge Labs. Building the knowledge layer.