DistroPack

Builds Linux packages (deb, rpm, Arch, Alpine, tar) from one upload and hosts them in signed repositories.

category
$ product
status
live
active
2024-10 → 2026-04 · 235 commits
made
◧ started by hand then kept building with agents · started by hand, continued with agents
numbers
● 57 users (live)
stack
asp.net core · c# · postgres · rust cli · github actions
links
distropack.dev ↗cli ↗action ↗
timeline
······

What it does

Each Linux distribution family has its own package format and its own repository format. A developer who wants to support all of them must learn and maintain each one.

DistroPack does this work. The developer uploads the files once and selects the targets. DistroPack builds the packages, signs them with GPG and hosts the repositories. Users install with their normal package manager.

The live targets are:

  • Debian and Ubuntu (.deb, with an apt repository).
  • Fedora, RHEL and Rocky (.rpm).
  • Arch Linux (.pkg.tar.zst, with a pacman repository).
  • Alpine (.apk).
  • Plain tar archives, for systems without one of these package managers.

Each package gets an install page. The page shows the commands for each distribution. Power Options uses DistroPack for its own packages.

DistroPack install page for power-options-gtk
fig. 01 · the public install page for power-options-gtk · distropack.dev, 2026-09

How it works

The system has three services, all in C#:

  • The web app. It has the dashboard, the accounts, the billing and the API.
  • The packager. It takes a build job and makes the package for one target.
  • The file server. It hosts the packages and the repository metadata, and it signs them.

They talk over HTTP with a service key. They do not share a database.

webstorage · s3packagerfile serveruploaddashboard · cli · cifilesstoreuser/project/pkgcheck + leaseplan limitsbuild requestqueuein memoryjob idssave jobsrelease leaseeach target, one at a timedownloadpre-signed urlsbuildfpm · abuildsize checktier limituploadsign packageproject keyupdate indexper formatsign indexproject keyget status · every 5 srunning · finished · failedupdate jobsversion history
fig. 02 · one build · the services talk over HTTP with a service key and do not share a database

A build goes like this:

  1. The developer uploads the files from the dashboard, the CLI or the GitHub Action. The files go to S3.
  2. The web app checks the plan limits and takes a lease on each target. The lease stops two builds of the same package and target from starting at the same time. The web app sends one build request to the packager and saves the jobs that come back. Then it releases the lease. From that point, the running job blocks a second build.
  3. The packager keeps the jobs in a queue in memory and runs one at a time. A job downloads the files from S3 with pre-signed URLs, builds the package and checks it against the size limit of the plan.
  4. The packager uploads the package to the file server. The file server signs it, updates the repository index and signs the index.
  5. The web app asks the packager for the status every 5 seconds. A job that runs for more than 30 minutes is marked as failed.

fpm builds the .deb, .rpm, pacman and tar packages. Alpine needs its own tool, abuild, so the packager image is based on Alpine.

A build job runs the pre-install, post-install, upgrade and removal scripts that the developer writes. The developer also selects the architectures: x86, x64, ARM and ARM64.

For CI, there is a CLI in Rust and a GitHub Action. A release workflow can upload the files and start the builds without the dashboard.

Repositories and signing

Each project has its own GPG key. The file server makes the key when the first package of the project arrives. The public key is at a public URL, and the install page uses it.

Each format puts the signatures in a different place:

FormatPackage signatureIndexIndex signature
.debdpkg-sig, inside the packagePackages and ReleaseRelease.gpg and InRelease
.rpmrpm --addsign, inside the packagerepodata/, from createrepo_crepomd.xml.asc
pacmana binary .sig filethe repo-add database.db.sig
.apkabuild-sign, inside the packageAPKINDEX.tar.gz, from apk indexAPKINDEX.tar.gz.asc
taran armored .asc filenonenone

Alpine signs packages with an RSA key, not a GPG key. The file server converts the GPG key of the project to an RSA key with openpgp2ssh, and abuild-sign uses that key.

Pacman does not let repo-add replace a package with the same or a lower version. To rebuild a version, the file server first removes the package from the database with repo-remove.

macOS and Windows

I built both in 2026. The landing page still lists them as coming soon. All of the builds run on Linux, so the system has no Mac and no Windows machine.

  • macOS: .app, .dmg and .pkg. genisoimage and libdmg-hfsplus make the DMG. bomutils and xar make the pkg. rcodesign, a Rust version of Apple's codesign, signs them. Command-line tools go out through Homebrew. The file server writes the formula and serves the tap itself, with a small read-only git server.
  • Windows: a portable zip, manifests for WinGet, Chocolatey and Scoop, an MSI and a setup EXE from WiX, and an MSIX from MakeAppx under Wine. osslsigncode signs them, or SignTool under Wine.

Timeline

DateChange
2024-10First commit. Accounts, projects and pricing.
2025-01RPM and Arch builds, then full Debian repositories.
2025-08First agent config in any of my repos.
2025-11Rust CLI and GitHub Action. 102 commits in this month.
2025-12tar archives and Alpine packages.
2026-02macOS packages, with a new build pipeline.
2026-04Windows support.

What was hard

Each format has its own rules. Some examples from the commit log:

  • RPM builds failed only for packages larger than 256 KB.
  • Pacman does not want an armored signature. I removed it.
  • Alpine needs its own repository layout and index. I verify each .apk before it goes into the repository.

Each new format needs changes in all three services. The commit message for the Alpine work says it: "yeah this is annoying each pkg addition".