# OmniPackage documentation > Reference, guides, and CLI docs for OmniPackage — build signed RPM and DEB packages for multiple Linux distros from one YAML config. OmniPackage is a CLI that drives standard Linux packaging tools (rpmbuild, debuild, createrepo_c, dpkg-scanpackages, gpg, podman/docker) from a single YAML config, so one project repo can ship signed RPM and DEB packages to many distros from a single `release` command. # Getting started # OmniPackage documentation Reference and guides for **OmniPackage**, a CLI for building and publishing signed RPM, DEB, and pacman packages to many Linux distributions from one YAML config. For the project overview, see [omnipackage.org](https://omnipackage.org/). ## Where to start - [Getting started](https://docs.omnipackage.org/getting_started/index.md) — install the CLI and ship the bundled C example end-to-end. - [How it works](https://docs.omnipackage.org/guides/how_it_works/index.md) — what `omnipackage release` does, step by step. - [Configuration](https://docs.omnipackage.org/configuration/index.md) — every key in `.omnipackage/config.yml`. - [CLI reference](https://docs.omnipackage.org/cli/index.md) — every subcommand and flag. ## 30-second example ```sh omnipackage init . echo "GPG_KEY=$(omnipackage gpg generate --name 'Your Name' --email you@example.com --format base64)" >> .env # edit .omnipackage/config.yml — point repositories: at your bucket omnipackage release . ``` Full walkthrough: [Getting started](https://docs.omnipackage.org/getting_started/index.md). ## Real-world projects - [`olegantonyan/mpz`](https://github.com/olegantonyan/mpz) — Qt desktop music player. 36 build targets, Qt5/Qt6 split via YAML anchors, per-distro CMake flags, R2 with Cloudflare cache purges. - [`omnipackage/omnipackage-rs`](https://github.com/omnipackage/omnipackage-rs) — OmniPackage built with itself. `before_build_script` installs a current Rust toolchain on older distros; newer ones use distro-packaged Rust. Dual GHCR caches cover org and contributor-fork workflows. - [`omnipackage/examples`](https://github.com/omnipackage/examples) — minimal one-per-language templates (C, C++, CMake, Rust, Go, Python, Ruby, Crystal, Electron, Tauri). See [Examples](https://docs.omnipackage.org/examples/index.md) for details. ## Links - [omnipackage.org](https://omnipackage.org/) — project landing page - [GitHub](https://github.com/omnipackage/omnipackage-rs) — source, issues, releases - Install OmniPackage — apt / dnf / zypper / pacman one-liners: [x86_64](https://repositories.omnipackage.org/omnipackage-rs/stable/install.html) · [aarch64](https://repositories.omnipackage.org/omnipackage-rs/stable-aarch64/install.html) # Getting started Install the CLI and build the bundled C example end-to-end. ## Install the CLI Options: 1. OmniPackage repositories — recommended: [x86_64](https://repositories.omnipackage.org/omnipackage-rs/stable/install.html) · [aarch64](https://repositories.omnipackage.org/omnipackage-rs/stable-aarch64/install.html) 1. [AUR](https://aur.archlinux.org/packages/omnipackage) — Arch alternative, e.g. `yay -S omnipackage` 1. [Source](https://github.com/omnipackage/omnipackage-rs/) — requires Rust 1.95+ Verify: ```text omnipackage --version ``` Prints `omnipackage ` if the binary is on `$PATH`. ## Build the example project Clone the [examples repo](https://github.com/omnipackage/examples) and enter the C/Makefile sample: ```text git clone https://github.com/omnipackage/examples.git && cd examples/c_makefile ``` Generate a GPG signing key. To reuse an existing one, see [Signing packages](https://docs.omnipackage.org/guides/signing/index.md). ```text echo "GPG_KEY=$(omnipackage gpg generate --name 'Your Name' --email 'you@example.com' --format base64)" >> .env ``` The key is ASCII-armored, then base64-encoded so it fits in an env variable without newline escaping. OmniPackage reads `.env` from the project root by default; override with `--env-file`. `.env` now holds your private signing key — keep it out of version control (the examples repo already gitignores it). Run `release` (`.` is the project directory containing `.omnipackage/config.yml`): ```text omnipackage release . ``` The build log streams for each distro in `.omnipackage/config.yml`. The command writes local repositories to `~/omnipackage-examples-repos/c_makefile`. Open `install.html` there in a browser for copy-paste install instructions per distro. The directory also holds `install.sh`, which detects the machine's distro and installs in one step, and `install.json` with the same data for automation (see [Install page](https://docs.omnipackage.org/guides/install_page/index.md)). The path comes from the `repositories` block in `.omnipackage/config.yml`; the first entry is used by default. ## Next steps ### Switch to S3 for production Local repositories are for testing; production usually means S3. The example config already includes an S3 block. Select it by name: ```text omnipackage release . --repository "Example bucket" ``` ### How secrets flow Secrets are declared in `config.yml` and passed from the environment via `${...}`. `config.yml` declares what is needed; `.env` (or any env file, or the process environment) holds the values. There are no hidden env settings. # Guides # How it works A high-level walkthrough of what happens during `omnipackage release`. This page is conceptual — it explains the model, not the flags. ## What it is OmniPackage is a thin wrapper over existing Linux packaging infrastructure. `rpmbuild`, `debuild`, `makepkg`, `createrepo_c`, `dpkg-scanpackages`, `repo-add`, `gpg`, container runtimes (`podman` / `docker`), `apt` / `dnf` / `zypper` / `pacman` — none of it is reinvented. OmniPackage drives these tools in the right order, per distro, with sensible defaults, so one project repo can ship signed packages to many distros from one config file. The motivation is on [About](https://omnipackage.org/about): native Linux packaging works well for distro maintainers, but it is a steep climb for individual developers who want their users to `apt install` their software. OmniPackage closes that gap on both sides — developer UX (one config, one command) and user UX (a generated install page with four copy-paste commands). ## Two flows, one pipeline There's a developer-side flow and a user-side flow. The pipeline produces both. ``` flowchart TD src["source repo"] -->|init| cfg["omnipackage config"] cfg -->|release| build["per-distro container build"] build -->|sign| pkg["signed package"] pkg -->|publish| repo["signed repo on S3"] repo --> page["generated install page"] page --> install["users install"] ``` ### Developer side 1. **Scaffold** *(optional)* — `omnipackage init` detects the project type from marker files (`Cargo.toml`, `go.mod`, `CMakeLists.txt`, `pyproject.toml`, …) and renders a starter `.omnipackage/config.yml` plus per-format template files (RPM `.spec.liquid`, `debian/` directory, pacman `PKGBUILD.liquid`). The generated templates are starting points, not finished configs: expect to edit `config.yml`, the spec, and the `debian/` files to match what your project builds and ships. Skip this step if you would rather hand-write the config from one of the [examples](https://docs.omnipackage.org/examples/index.md). 1. **Release** — `omnipackage release` reads the config and, for each configured distro: - Pulls the distro container image (`opensuse/leap:16.0`, `fedora:42`, `debian:trixie`, `archlinux:latest`, etc.). - Runs the distro's own setup commands inside the container — `zypper install ...`, `apt-get install build-essential debhelper ...`, `dnf install rpmdevtools ...`, `pacman -Syu base-devel ...`. These are not OmniPackage code; they are verbatim distro-native shell commands. - Renders the `.spec` (RPM), `debian/` (DEB), or `PKGBUILD` (pacman) templates with project and distro variables via Liquid, then invokes the distro's native build tool (`rpmbuild`, `debuild`, `makepkg`). - Signs the resulting `.rpm` / `.deb` / `.pkg.tar.zst` with the configured GPG key. The same key signs packages and repo metadata. - Builds repo metadata with the distro-native tool — `createrepo_c` for RPM, `dpkg-scanpackages` for DEB, `repo-add` for pacman. - Uploads the signed packages and metadata to S3 (or any S3-compatible store: R2, GCS, B2, MinIO; see [`s3_repository`](https://docs.omnipackage.org/guides/s3_repository/index.md)). - Generates the [install page](https://docs.omnipackage.org/guides/install_page/index.md) (`install.html`) and, next to it, `install.sh` — a one-line installer that auto-detects the user's distro — and `install.json`, the same per-distro data in machine-readable form for automation. `omnipackage prime` is separate from this flow: it pre-runs the distro setup commands and snapshots the container image to a registry, so later releases skip the slow `apt-get install build-essential` phase. See [`image_caches`](https://docs.omnipackage.org/configuration/image_caches/index.md). `omnipackage` runs anywhere a container runtime does (laptop, VPS, any CI). A common setup is free end-to-end: GitHub Actions is free for public repositories, and Cloudflare R2, Backblaze B2, and Google Cloud Storage have free tiers that cover small-to-mid projects. ### User side What ends up at `//install.html` is what a real end user sees: - For DEB-family distros, four lines: add the apt source, import the GPG key, `apt-get update`, `apt-get install `. - For RPM-family distros, the equivalent `dnf` / `zypper` flow. - For pacman distros (Arch, Manjaro), import the key with `pacman-key`, add the repo `Server` to `/etc/pacman.conf`, then `pacman -Sy `. Users who would rather not pick their distro by hand can run the one-line installer instead. It detects the distro from `/etc/os-release`, checks the CPU architecture, and runs the matching steps: ```text curl -fsSL //install.sh | sh ``` Add `-y` to skip the confirmation prompt, or `--distro ` to override detection. `install.json` next to it exposes the same per-distro data — download URLs, GPG public key, install commands — as a machine-readable array for automation. After install, users receive updates through their distro's normal `apt upgrade` / `dnf upgrade` / `zypper update` / `pacman -Syu`. No opt-in updater, no Electron tray icon, no separate channel. The repo is a normal signed repo served over HTTPS. ## What it does not do - Build other package formats. RPM, DEB, and pacman only. Flatpak/Snap/AppImage/Nix are a different approach — see [About](https://omnipackage.org/about) for why. (OmniPackage builds pacman packages into its own signed repo; it does not publish to the AUR.) - Host your repository. You bring the bucket. The trade-off: no vendor lock-in, and your packages live in storage you control. - Sandbox installed software. Packages run with the same privileges any `apt install` package gets — no Flatpak-style isolation unless you ship it as part of your package (an AppArmor / SELinux profile, a `bwrap` / `firejail` wrapper around your binary). # Signing packages OmniPackage signs every published `.deb` / `.rpm` / `.pkg.tar.zst` and the repository metadata (`Release` / `InRelease` for DEB, `repomd.xml` for RPM, the signed `.db.tar.gz` database plus a detached `.sig` per package for pacman) with a GPG private key. End users import the matching public key once when they add the repository; `apt` / `dnf` / `zypper` / `pacman` reject any package or metadata file whose signature does not verify. The key is what makes a published repository trustable. The key is referenced from `config.yml` as base64-wrapped ASCII armor, normally via `${GPG_KEY}` — substituted from a `.env` file (project root by default, override with `--env-file `) or from the process environment. ```yaml repositories: - name: my-repo provider: s3 gpg_private_key_base64: "${GPG_KEY}" # ... ``` The rest of this page covers producing that `GPG_KEY` value — generated fresh, exported from your existing GPG keyring, or converted between formats. Keep the key secret and back it up The private key is the trust anchor for your repository. Treat it like any other production secret — never commit it, restrict access, and keep at least one backup somewhere safe (password manager, encrypted offline storage). **If you lose it, you cannot sign updates with the same key.** Signing future releases with a *new* key causes every user's `apt` / `dnf` / `zypper` to reject the updates with a signature-mismatch error; they will have to manually import the new public key (or remove and re-add the repository) before updates resume. ## Generate a new key ```sh omnipackage gpg generate --name "Your Name" --email you@example.com --format base64 ``` This prints a single base64 line to stdout — append it to `.env`: ```sh echo "GPG_KEY=$(omnipackage gpg generate --name 'Your Name' --email you@example.com --format base64)" >> .env ``` What the command does: - Generates an RSA 4096-bit keypair with no expiration date. - Sets no passphrase: the build runs unattended, with no interactive prompt. - Prints only the private key. The public key is derived from it on every publish, so you do not need to track them separately. - Runs in a temporary, isolated `GNUPGHOME`; your real `~/.gnupg` is never touched. The same key signs packages and repo metadata for the lifetime of the repository — rotating it forces every existing user to re-import the new public key, so generate once and keep the `.env` value safe. ## Use an existing key To reuse an existing GPG key, export it from your keyring and feed it through `omnipackage gpg convert`. ### 1. Find the key ID ```sh gpg --list-secret-keys --keyid-format=long ``` Note the `sec` line's key ID — the long hex after `rsa/`. ### 2. Remove the passphrase, if any OmniPackage cannot use a passphrased key — there is no interactive prompt during a build. Strip the passphrase first: ```sh gpg --edit-key > passwd # enter the current passphrase, then leave the new passphrase empty > save ``` To keep your everyday key passphrased, **do not** do this on it — instead generate a dedicated unprotected signing subkey, or generate a fresh key with `omnipackage gpg generate` (above) and use it exclusively for package signing. ### 3. Export the private key as ASCII armor ```sh gpg --armor --export-secret-keys > signing-key.asc ``` The result is a multi-line `-----BEGIN PGP PRIVATE KEY BLOCK-----` block. ### 4. Convert to base64 and put it in `.env` ```sh echo "GPG_KEY=$(omnipackage gpg convert signing-key.asc)" >> .env rm signing-key.asc ``` Delete `signing-key.asc` once it is in `.env` — no reason to keep two copies of the secret on disk. ## Convert between formats `omnipackage gpg convert` round-trips between two key encodings: | Format | What it is | Where it is used | | -------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | | `pem` | Plain ASCII armor (`-----BEGIN PGP PRIVATE KEY BLOCK-----`...). Multi-line. | What `gpg --armor --export-secret-keys` produces; human-inspectable. | | `base64` | The PEM block, base64-encoded into one line of ASCII. | What `config.yml` expects in `gpg_private_key_base64`; what you put in `.env`. | Why base64? ASCII-armored PGP keys contain newlines, and newlines do not survive `.env` files, GitHub Actions secrets (the multi-line case works but is fragile), or shell `export VAR=...`. Base64 collapses the whole thing into a single line of `[A-Za-z0-9+/=]`, which round-trips cleanly through every layer between your laptop and the build container. ```sh # pem → base64 (typical: prepare for .env); pem in, base64 out are the defaults omnipackage gpg convert signing-key.asc # base64 → pem (typical: inspect or re-import) omnipackage gpg convert key.b64 --input-format base64 --output-format pem | gpg --import # stdin works too cat signing-key.asc | omnipackage gpg convert --input-format pem --output-format base64 ``` The conversion is loss-free: decoding the base64 form yields exactly the original ASCII-armored block. # Publishing to S3 End-to-end walkthrough for turning an S3 bucket (or any S3-compatible storage) into a public DEB/RPM/pacman repository. **Cloudflare R2 is recommended** if you have no preference: it is the most-tested provider here, egress is free (so serving packages costs nothing), and it includes 10 GB of free storage. ## AWS S3 ### 1. Create the bucket S3 console → **Create bucket**. Pick a region (e.g. `eu-central-1`) and a globally-unique name. Leave "Block all public access" on for now — step 3 turns the right parts of it off. ### 2. Create an IAM user with access keys Access keys come from **IAM**, not S3. Do not use the root account. 1. IAM → **Users** → **Create user** (e.g. `omnipackage-publisher`). Programmatic access only, no console login. 1. Attach an inline policy scoped to this single bucket: ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["s3:ListBucket", "s3:GetBucketLocation"], "Resource": "arn:aws:s3:::" }, { "Effect": "Allow", "Action": ["s3:PutObject", "s3:GetObject", "s3:DeleteObject", "s3:AbortMultipartUpload"], "Resource": "arn:aws:s3:::/*" } ] } ``` 1. **Security credentials** → **Create access key** → "Application running outside AWS". Copy both into your env file as `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` — the secret is shown only once and cannot be retrieved later. From GitHub Actions, prefer OIDC with an IAM role over static keys. ### 3. Make objects publicly readable S3 buckets are private by default behind two independent gates; both must allow public reads. **Block Public Access (BPA)** — bucket-level master switch. **Permissions** → **Block public access (bucket settings)** → Edit. Uncheck: - "Block public access to buckets and objects granted through *new* public bucket or access point policies" - "Block public access to buckets and objects granted through *any* public bucket or access point policies" Leave the two ACL boxes checked — modern buckets use policies, not ACLs. **Bucket policy** — the actual grant. Same Permissions tab → **Bucket policy** → Edit: ```json { "Version": "2012-10-17", "Statement": [ { "Sid": "PublicReadGetObject", "Effect": "Allow", "Principal": "*", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::/*" } ] } ``` The `Resource` ARN ends in `/*` (objects), not the bare bucket. With both gates in place, objects are reachable at `https://.s3..amazonaws.com/`. ### 4. Repository config ```yaml - name: AWS S3 eu-central-1 provider: s3 gpg_private_key_base64: "${GPG_PRIVATE_KEY_BASE64}" package_name: "sample-project" s3: bucket: omnipackage-repositories-891377066957-eu-central-1-an path_in_bucket: "sample-project" bucket_public_url: "https://omnipackage-repositories-891377066957-eu-central-1-an.s3.eu-central-1.amazonaws.com" endpoint: "https://s3.eu-central-1.amazonaws.com" access_key_id: "${AWS_ACCESS_KEY_ID}" secret_access_key: "${AWS_SECRET_ACCESS_KEY}" region: eu-central-1 force_path_style: false ``` Field notes: - `bucket_public_url` — virtual-hosted REST endpoint (`https://.s3..amazonaws.com`). Serves HTTPS once the bucket policy is in place. Do not use the `s3-website` endpoint (HTTP-only). - `endpoint` — regional S3 API endpoint, e.g. `https://s3.eu-central-1.amazonaws.com`. - `region` — actual AWS region. AWS requires it for SigV4 (R2 uses `auto`; AWS does not). - `force_path_style: false` — AWS uses virtual-hosted-style; path-style is deprecated. ### 5. Troubleshooting `AccessDenied` on the public URL almost always means: 1. **BPA still blocking the policy.** Permissions tab should show "Public" once both BPA and the policy are correct. 1. **Bucket policy missing `/*`** on the resource ARN, or wrong bucket name. 1. **Object doesn't exist.** Verify with `aws s3 ls s3:////`. AWS returns `AccessDenied` instead of `NoSuchKey` when `s3:ListBucket` isn't granted, masking missing objects. ## Cloudflare R2 R2 is S3-compatible with a few quirks. ### 1. Create the bucket Cloudflare dashboard → **R2** → **Create bucket**. R2 names are scoped to your account, not globally unique. ### 2. Make it public via a custom subdomain R2 does not expose a public `*.r2.cloudflarestorage.com` URL — that endpoint is API-only and requires signed requests. Public access requires a **custom subdomain** under a Cloudflare-managed zone: Bucket → **Settings** → **Public access** → **Custom Domains** → **Connect Domain** → enter e.g. `repositories-test.omnipackage.org`. Cloudflare provisions DNS and TLS automatically. The `r2.dev` subdomain is rate-limited and meant for development; do not use it for a real repo. ### 3. Create R2 API credentials R2 dashboard → **Manage R2 API Tokens** → **Create API token**. Permissions: **Object Read & Write**, scoped to the bucket. Cloudflare returns an Access Key ID, Secret Access Key, and the S3 API endpoint (`https://.r2.cloudflarestorage.com`). ### 4. Repository config ```yaml - name: test repo on Cloudflare R2 provider: s3 gpg_private_key_base64: "${GPG_PRIVATE_KEY_BASE64}" package_name: "sample-project" s3: bucket: repositories-test path_in_bucket: "sample-project" bucket_public_url: "https://repositories-test.omnipackage.org" endpoint: "${CLOUDFLARE_R2_ENDPOINT}" access_key_id: "${CLOUDFLARE_R2_ACCESS_KEY_ID}" secret_access_key: "${CLOUDFLARE_R2_SECRET_ACCESS_KEY}" region: auto force_path_style: true # Optional — see "CDN cache purge" below cloudflare_zone_id: "${CLOUDFLARE_ZONE_ID}" cloudflare_api_token: "${CLOUDFLARE_API_TOKEN}" ``` Field notes: - `bucket_public_url` — your custom subdomain. Must be the public-facing host, **not** the R2 API endpoint. - `endpoint` — the R2 S3 API endpoint, account-scoped. Used only for uploads. - `region: auto` — R2 ignores region; SigV4 still needs *some* value, and `auto` is what Cloudflare documents. - `force_path_style: true` — required. R2's endpoint is account-scoped, so the bucket goes in the path. ### 5. CDN cache purge (optional) Custom-subdomain R2 traffic flows through Cloudflare's edge, which caches `GET` responses. Without purging, stale repo metadata (`Release`, `Packages.gz`, `repodata/`, the pacman `.db.tar.gz`) can be served until TTL expires. If both `cloudflare_zone_id` and `cloudflare_api_token` are set, OmniPackage purges the affected URL prefix after each upload. They are treated as a pair — if either is missing, the purge step is skipped silently. A purge failure logs a warning but does not fail the publish. To get them: - **Zone ID** — Cloudflare dashboard → your domain → **Overview** sidebar (right side). - **API token** — **My Profile** → **API Tokens** → **Create Token** → custom token with **Zone → Cache Purge → Purge** scoped to the zone. Do not use the global API key. Skip if you can tolerate edge TTL on repo updates. ## Google Cloud Storage GCS speaks an S3-compatible API. ### 1. Create the bucket Console → **Cloud Storage** → **Buckets** → **+ Create**. Names are globally unique. Set **Access control = Uniform bucket-level access**. ### 2. Service account + HMAC keys GCS authenticates the S3 API with **HMAC keys**, not JSON service-account files. Bind the key to a dedicated service account so it can be rotated independently. 1. **IAM & Admin** → **Service Accounts** → create `omnipackage-publisher`. 1. Bucket → **Permissions** → **Grant access**. Principal = service account email; role = **Storage Object Admin**. 1. **Cloud Storage** → **Settings** → **Interoperability** → **+ Create a key for a service account** → pick the publisher SA. 1. Copy the access key and secret into your env file as `GCS_HMAC_ACCESS_KEY_ID` and `GCS_HMAC_SECRET_ACCESS_KEY`. ### 3. Make objects publicly readable - **Public access prevention** (Configuration tab) → **Off**. - **Permissions** → **Grant access**: principal `allUsers`, role **Storage Object Viewer**. The bucket header then shows a "Public to internet" badge. ### 4. Repository config ```yaml - name: GCS europe-southwest1 provider: s3 gpg_private_key_base64: "${GPG_PRIVATE_KEY_BASE64}" package_name: "sample-project" s3: bucket: omnipackage-repos path_in_bucket: "sample-project" bucket_public_url: "https://storage.googleapis.com/omnipackage-repos" endpoint: "https://storage.googleapis.com" access_key_id: "${GCS_HMAC_ACCESS_KEY_ID}" secret_access_key: "${GCS_HMAC_SECRET_ACCESS_KEY}" region: europe-southwest1 force_path_style: true ``` Field notes: - `bucket_public_url` — path-style. Do not use virtual-hosted (`.storage.googleapis.com`). - `endpoint` — single global endpoint; no regional variant. - `region` — must match the bucket's actual location. SigV4 is region-bound; do not use `auto`. - `force_path_style: true` — required; virtual-hosted style trips signature mismatches. ### 5. Cache and custom domains GCS serves public objects with `Cache-Control: public, max-age=3600` by default, so republished repo metadata can be stale for up to an hour. Override the bucket-default `Cache-Control` or set per-object headers if that matters. GCS cannot serve a custom domain over HTTPS on its own. Either put a Google HTTPS Load Balancer with a backend bucket in front, or front it with Cloudflare — in the Cloudflare case the `cloudflare_zone_id` / `cloudflare_api_token` fields are useful for cache purges, same as the R2 setup. # Publishing to a local directory S3 is not mandatory. The `localfs` provider writes the same signed repository tree to a directory on the host instead of uploading it to a bucket. The output is a standard DEB/RPM/pacman repository, identical to what an S3 target produces. Use it to: - inspect exactly what `publish` generates, - install on the same machine without any server, - self-host behind a web server or shared mount you already run. ## Configuration ```yaml - name: Local provider: localfs gpg_private_key_base64: "${GPG_KEY}" package_name: "sample-project" localfs: path: "${HOME}/sample-project-repo" ``` `path` is the only `localfs` key. Environment placeholders such as `${HOME}` are expanded from the [env file](https://docs.omnipackage.org/configuration/secrets/index.md). The directory is created if it does not exist. ## What gets written Each distro lands in its own subdirectory named by [distro id](https://docs.omnipackage.org/distros/index.md), with the package, the signed native repo metadata, and the public key. The generated [install page](https://docs.omnipackage.org/guides/install_page/index.md) and its sibling files sit at the root: ```text sample-project-repo/ ├── install.html # human-facing install page ├── install.sh # one-line installer, detects the distro ├── install.json # per-distro data, machine-readable ├── badge.svg # repo status badge ├── fedora_42/ │ ├── sample-project.repo │ ├── repodata/ │ └── sample-project-1.0-1.x86_64.rpm ├── debian_12/ │ └── stable/ │ ├── Release │ ├── Release.key │ ├── Packages.gz │ └── sample-project_1.0_amd64.deb └── arch/ ├── public.key ├── sample-project.db.tar.gz └── sample-project-1.0-1-x86_64.pkg.tar.zst ``` ## Inspect and install locally Open `install.html` in a browser for the per-distro install snippets, or run `install.sh` to detect the distro and install in one step; `install.json` carries the same data for scripts. Because `localfs` has no public URL, the snippets point at `path` on disk, so they work as-is for installing on the same machine. ## Self-hosting The directory is a complete, standard repository. To serve it over the network, point any static web server (nginx, Caddy, `python -m http.server`) at `path`, or place it on an NFS/SMB share. Package managers consume it like any other repo. The generated `install.html` links use the on-disk `path`, not your server's URL, so for a networked repo give consumers the repository URL directly. For a turnkey public repo with a matching install page, an [S3-compatible backend](https://docs.omnipackage.org/guides/s3_repository/index.md) is simpler — including self-hosted MinIO, which speaks the same S3 API. ## Retention [`retain_packages`](https://docs.omnipackage.org/configuration/repositories/#package-retention) works the same as for S3: each `publish`/`release` keeps the N most recent packages per distro plus the new build and prunes the rest from `path`. # The install page `publish` and `release` write four files to the repository root, next to the per-distro package trees: `install.html`, `install.sh`, `install.json`, and `badge.svg`. Each publish updates the published distro's entry and keeps the rest, so the files stay complete when distros are published one at a time, as in a [CI matrix](https://docs.omnipackage.org/guides/cicd/index.md). `omnipackage info --show-install-page-url` prints the page URL (S3 repositories). ## `install.html` A self-contained static page with install instructions for every published distro, grouped by family. Each entry holds copy-paste shell steps and the repository's GPG public key; the header offers a one-line `curl ... | sh` that runs `install.sh`. Light and dark themes, no external assets. The page stores its data as JSON in a `