再掲載に関する注記: 著者の許可を得て、abn.isの元記事(2026年8月14日公開)から再掲載しています。 本文の文章、見出し、リンク、リスト、コマンド、コード例は、コード内の末尾空白を正規化した以外は変更していません。 Nowledge Labsは要約とタグを編集メタデータとして追加し、元のヒーロー画像をセルフホストし、著者のMermaidソースから両方の図をレンダリングし、元のコールアウトボックスを引用ブロックとして提示し、以下のコミュニティリポジトリに関する通知を追加しました。
コミュニティリポジトリに関する通知: 本記事で説明するCOPRリポジトリはコミュニティによって保守されている非公式のものです。 Nowledge Labsの公式DNFリポジトリではありません。

Tauri、Electron、cargo-packagerといった現代のアプリケーションフレームワークは、デスクトップアプリケーションやCLIバイナリをスタンドアロンの.rpmファイルや.debファイルにビルドすることを容易にします。
しかし、スタンドアロンのパッケージファイルを公開することは、Linuxユーザーにとって特有の配布上の問題を生み出します。
ネイティブなパッケージリポジトリ(dnf / zypper / apt)がなければ、エンドユーザーは更新のたびに手動でパッケージをダウンロードして再インストールしなければなりません。
毎日あるいは1日に複数回の更新をリリースするような動きの速いソフトウェアプロジェクトでは、この手動ワークフローはユーザーにとって持続不可能になります。
新しいアプリケーションが公式のLinuxディストリビューションリポジトリ(FedoraやDebianの公式パッケージリポジトリなど)に受け入れられるには、多大なメンテナの負担が必要です。 提出レビュー、厳格なソースビルドガイドラインへの準拠、依存関係のバンドル解除、そして長い承認待ち行列などです。 小規模チームや独立系開発者が、あらゆるLinuxディストリビューションにわたって公式パッケージを容易に保守することはできません。
本ガイドでは、Fedora COPR、Tito、GitHub Actionsを用いて自動化されたダウンストリームパッケージングパイプラインを構築する方法を詳述します。 このパターンは、アップストリームのスタンドアロンバイナリRPMを取り込み、そのコンポーネントを抽出してモジュール化し、リリースにタグを付け、更新されたCOPRリポジトリを自動的に公開します。
Linuxパッケージングのボトルネック
開発者がフレームワークツールでアプリケーションをパッケージングすると、その出力は多くの場合、GitHub ReleasesやHTTPダウンロードエンドポイントにアップロードされた単一のスタンドアロン.rpmファイルまたは.debファイルです。
スタンドアロンバイナリリリースのUX上の失敗
- システムの自動更新がない:
dnf updateやシステム更新GUIアプリケーション(GNOME Software、KDE Discover)は新しいバージョンを検出できません。 - 更新の摩擦が大きい: ユーザーはリリースページを再訪し、新しいファイルをダウンロードし、ローカルのインストールコマンドを手動で実行しなければなりません。 リリースの頻度が1日1〜2回に達すると、ユーザーの更新採用率は著しく低下します。
- モノリシックな依存関係グラフ: フレームワークのパッケージャは、デスクトップGUI、CLIツール、バックグラウンドのサーバーデーモンを単一の重いパッケージにバンドルすることが多く、サーバー専用やCLI専用のモジュール型デプロイを妨げます。
デスクトップ配布の代替手段: FlatpakとAetherPak
デスクトップアプリケーションについては、Flatpakが自動更新を備えたサンドボックス化された配布メカニズムを提供します。 しかし、Flathubへのアプリケーション提出には厳格な提出要件と手動のレビュアー承認が伴います。
セルフマネージドFlatpakリポジトリ: AetherPak
Flathub以外で自動化されたFlatpak配布を求める開発者向けに、プロジェクトはAetherPakのような代替インデックスを通じて公開できます。
具体的なリファレンス実装としてLemonade Tauri App Flatpakリポジトリがあり、これはTauriデスクトップアプリケーションをスタンドアロンのFlatpakバンドルにパッケージングし、アップストリームのリリースが現れると自動更新をサポートします。
サーバーデーモン、CLIバイナリ、systemd統合については、ネイティブRPMリポジトリがFedora、RHEL、CentOS Stream、openSUSEエコシステムにおける標準的なアプローチであり続けています。
自動再パッケージングパイプラインのアーキテクチャ
Fedora COPR(元はCool Other Package Repositories、現在はCommunity Projects)は、メンテナやコミュニティコントリビューターが自動RPMリポジトリをホストすることを可能にします。 COPRをTito(Git内でRPMパッケージのライフサイクルを管理するツール)およびGitHub Actionsと組み合わせることで、スタンドアロンバイナリRPMのダウンロードを完全に自動化されたリポジトリパイプラインへと変換できます。
COPR Dist-Gitとソースアセットの管理
コンパイル済みバイナリを再パッケージングする際のよくある誤解は、大きなバイナリファイル(x86_64-unknown-linux-gnu.rpm)をGitに直接コミットしたり、Git LFSで追跡したりしなければならないというものです。
Fedora COPRでは、大きなバイナリソースファイルはGit履歴に属しません。
代わりに、Fedora COPRはdist-gitルックアサイドキャッシュのメカニズムを活用します。
- Gitリポジトリは軽量なテキストファイルのみを追跡します。
すなわち
.specファイル、設定メタデータ、そしてバイナリペイロードファイルのMD5/SHA256チェックサムを含むsourcesファイルです。 - ビルド実行中、COPRの
dist-git-client sourcesツールは、分離されたMockビルドchroot内でrpmbuildを起動する前に、ルックアサイドキャッシュまたは指定されたHTTPエンドポイントから対応するバイナリアーティファクトを取得します。 - Mockビルド実行中に動的ダウンロードが発生する場合は、COPRプロジェクト設定で
enable_net: True(またはnet_access: true)が構成されていることを確認してください。 対応する設定は「Allow internet access」チェックボックスです。
設計根拠(COPR Dist-Gitルックアサイドキャッシュvs Git LFS)
コンパイル済みバイナリRPMをGitリポジトリに直接コミットすると、リポジトリが急速に肥大化します。 COPRのdist-gitルックアサイドキャッシュを利用することで、パッケージング用Gitリポジトリを軽量に保つことができます(テキストのspecファイルとバージョンスクリプトのみを含む)。 Git LFSは、外部のルックアサイドストレージが利用できないセルフホスト型またはエアギャップ型のCI環境における代替手段であり続けます。
設計根拠(rpm2cpioによる再パッケージングvsソースコンパイル)
プロプライエタリまたは複雑なマルチアーキテクチャバイナリを標準的なRPMビルド環境内でソースから再ビルドするには、多くの場合、複雑なツールチェーンを再現する必要があります。 コンパイル済みのアップストリームバイナリRPMをspecの
%prepフェーズでrpm2cpioを介して再パッケージングすることで、配布メカニズムをビルド環境から分離しつつ、ネイティブなシステム統合(systemdサービスユニット、デスクトップメニューエントリキャッシュ、バイナリシンボリックリンク)を維持できます。
実装ウォークスルー
1. RPM Specファイルの設計
specファイルは、事前に取得されたアップストリームバイナリ.rpmファイルを受け取り、rpm2cpioとcpioを用いてその内容を展開し、標準的なRPMマクロ(%{_bindir}、%{_datadir}、%{_prefix}/lib)を用いてファイルを適切なパッケージパスとサブパッケージに配分します。
以下は、4つのサブパッケージをサポートするspecファイル(nowledge-mem.spec)の例です。
メタパッケージ(nowledge-mem)、CLIパッケージ(nowledge-mem-cli)、GUIパッケージ(nowledge-mem-desktop)、ヘッドレスデーモンパッケージ(nowledge-mem-server)です。
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"設計根拠(サブパッケージのモジュール化)
モノリシックなアプリケーションを個別のサブパッケージ(
cli、desktop、server)に分割することで、ヘッドレスサーバーやリモートコンテナ環境は、gtk3やwebkit2gtkのような重いデスクトップ依存関係を引き込むことなくnowledge-mem-serverをインストールできます。
2. アップストリームバージョンの解決とダウンローダースクリプト
バージョンチェックを自動化し、Tito SRPMビルド用のバイナリペイロードを事前取得するには、Pythonスクリプト(update_version.py)を書いて、アップストリームのリダイレクトURLを検査し、specファイルのVersion:を更新し、Release:を1にリセットし、バイナリアーティファクトをx86_64-unknown-linux-gnu.rpmに保存します。
#!/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. Titoによる自動バージョンタグ付け
Titoは、パッケージバージョンの維持、チェンジログの生成、Gitタグの自動管理を行うことで、GitベースのRPM管理を簡素化します。
Git衛生ルール
Titoを使用する際は、specファイル内の
%changelogを手動で編集することは避けてください。 Titoは明示的に指定されない限り、tito tagの間に%changelogを管理します。 作業ツリー内のコミットされていない手動変更は、tito tagを失敗させます。
プロジェクトのルートディレクトリでTitoを初期化します。
tito initspecファイルが更新されたときに新しいリリースタグを自動的に作成するには、次のようにします。
tito tag --use-version "0.10.59" --accept-auto-changelog4. 継続的インテグレーションワークフロー(GitHub Actions)
GitHub Actionsワークフロー(.github/workflows/check-updates.yml)を構成して、アップストリームエンドポイントを4時間ごとにポーリングします。
新しいバージョンが検出されると、tito tagがクリーンなGitリリースタグを生成し、SRPMの生成とcopr-cliはdist-gitソース仕様を活用します。
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設計根拠(CIにおける自動Titoタグ付け)
コンテナ化されたGitHub Actionsランナー内で
tito tagを自動化することで、一貫したリリースバージョニングとチェンジログエントリを確保しつつ、ローカルの開発者セットアップを最小限に保つことができます。
セキュリティモデルと信頼に関する注意事項
サードパーティのバイナリアーティファクトを再パッケージングすることは、リポジトリのメンテナとエンドユーザーの双方が認識しなければならないセキュリティ上の責任を伴います。
セキュリティに関する注意事項(サードパーティリポジトリとコンパイル済みバイナリ)
再パッケージングされたバイナリRPMを含むコミュニティCOPRリポジトリを有効にする場合、エンドユーザーはアップストリームプロジェクトのメンテナと再パッケージングリポジトリのメンテナの両方に完全な信頼を委ねることになります。
- ソースビルドの検証なし: 再パッケージングパイプラインはコンパイル済みバイナリ(
rpm2cpio)を抽出します。 COPRビルダーはソースコードをコンパイルしないため、COPRビルドログは抽出とパッケージングの手順のみを検証し、バイナリのコンパイルの完全性は検証しません。- リポジトリの分離: ユーザーは信頼できないコミュニティCOPRリポジトリを本番マシンに追加することは避けるべきです。
sudo dnf copr enable <user>/<repo>を実行する前に、必ず基盤となるspecファイルとGitHub Actionsパイプラインを検査してください。
コマンドリファレンスのまとめ
このパイプラインを通じて公開されたパッケージを利用するユーザー向け:
# 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この自動化が整えば、プロジェクトのメンテナは標準的なフレームワークビルドツールでスタンドアロンバイナリを公開でき、Linuxユーザーはネイティブなdnf update機能とsystemd統合を維持できます。
プロジェクトのソースコード
この取り組みのソースコードはGitHubで入手できます。