Crystal packaging in Fedora

Fedora Packaging Update (2026-09-03)

Quick progress update from today’s coordination with Fedora leadership:

  1. Fedora Packaging Committee (FPC) Review:

    • Attended today’s bi-weekly FPC meeting to review the draft packaging guidelines (Meeting Minutes | Full Chat Log).
    • Compiler Source Package: Standardized on crystal-lang for the compiler SRPM/dist-git repo (producing the native crystal binary package via %package -n crystal). This gives us a clean Git history and enables automated SIG co-maintainership via crystal-* patterns.
    • Shard Package Naming: Adopted unconditional crystal-<shard> prefixing for shard library SRPMs (no deduplication) to prevent namespace collisions (e.g., sqlite3 vs crystal-sqlite3), with application shards providing unprefixed subpackages (e.g., dnf install ameba).
    • Parallel Coexistence: The committee validated our unbundled model installing sources to %{_datadir}/crystal/shards/<name>-<version> for conflict-free multi-version coexistence.
  2. Official Guidelines PR Submitted:

    • Submitted the formal draft as Pull Request #1568 against packaging/guidelines on Fedora Forge, incorporating policies aligned with Fedora’s Go and Rust guidelines.
  3. Infrastructure & Wiki Updated:

Yep, you’re right. Because Crystal shards are identified by Git repository URLs rather than a single global registry, two independent upstreams could indeed choose the same name in their shard.yml.

Here is how the Fedora packaging model handles this:

  1. Dist-Git Package Review & Namespace Disambiguation:
    In Fedora, source package names must be globally unique. If two distinct upstreams collide on a generic name (e.g., both named cache), Fedora packaging policy handles this via author/organization disambiguation at review time (e.g., crystal-author1-cache vs crystal-author2-cache), similar to how Fedora handles Go packages with conflicting base names.

  2. Decoupled RPM Macros (%crystal_install_lib -n):
    Our RPM macros explicitly decouple the RPM package name from the on-disk shard directory. If a disambiguated package name is needed, %crystal_install_lib -n <shard_name> can install the files into the canonical directory name expected by dependent shards.

  3. shard.lock Symlink Resolution:
    Because %crystal_setup inspects the project’s shard.lock during %prep, it maps the exact dependency name the project expects into local ./lib/<name>, pointing to the versioned system shard path in %{_datadir}/crystal/shards/.

In practice, this makes conflicts rare and manageable whenever two popular shards share a name.

I’ve been experimenting with how to do this. Mostly because I have so many projects running locally that the amount of copies of my dependencies is becoming problematic (I ran out of disk space!)

Hey everyone,

Quick update on the packaging effort, and a question for the core team on how you see shard discovery evolving.

Following up on the scheme I outlined earlier (installing shards to /usr/share/crystal/shards/<name>-<version> and symlinking them into ./lib), it has solved my offline build requirements completely. Compiler, shards, ameba, flaw, db drivers, and kemal all build cleanly in offline chroots.

Because shards are source-only and RPM packages cannot have file collisions, we install into versioned directories (kemal-1.13.0, kemal-1.12.0) following the compat package pattern (like LLVM). The primary package drops an unversioned symlink (kemal -> kemal-1.13.0), while compat packages omit it so multiple versions can coexist on disk.

This works cleanly for isolated RPM builds, but I’m hitting friction with developer workflows on a live system:

  1. Standalone scripts: If someone writes a quick script like app.cr with require "kemal" without a shard.yml or a lib/ folder, CRYSTAL_PATH looks for kemal. That resolves via the primary package’s unversioned symlink, but only for whichever version is primary. Has there ever been any thought on the compiler natively understanding versioned directories in CRYSTAL_PATH?

  2. Dangling symlinks on patch updates: When a developer links a local project against system shards (e.g. lib/kemal -> /usr/share/crystal/shards/kemal-1.13.0) and runs a routine dnf upgrade, the package updates to 1.13.1. RPM removes the 1.13.0 directory and replaces it with 1.13.1, which breaks the symlink in ./lib.

Here is what I tried to address this, and why it didn’t work:

  • Hybrid SONAME approach: Since we can’t use ELF SONAMEs, I tried emulating them with minor-version symlinks (kemal-1.13 -> kemal-1.13.1) so patch updates wouldn’t break ./lib. But shard.lock pins exact patch versions (1.13.0), so linking against minor versions bypasses the lockfile’s pinned version.
  • shard.override.yml: I tried generating an override file so shards install could manage the links. But running shards install mutates shard.lock with local absolute paths and # Overridden comments, which dirties git and breaks CI for anyone else pulling the repo. Plus, if any transitive dependency is missing from the overrides, shards install tries to hit the network, which fails in offline/air-gapped environments.

Where I landed for now is a small helper tool called shards-link. It reads shard.lock (or falls back to shard.yml), matches the version against /usr/share/crystal/shards/, and symlinks the dependencies into ./lib without touching shard.lock at all. If a system update removes an old patch version, running shards-link again relinks to the newest matching installed version.

It works, but it feels like a downstream hack for something that might be better addressed upstream.

Has there been any discussion around shards having an offline or shared cache mechanism (similar to Cargo or Go), or a native way to consume system-installed shards without dirtying shard.lock?

I’d love to hear your thoughts before I cement too many conventions on the Fedora side.

My first thought on this is that shards is a dependency manager, rpm a package manager. Those are different concerns. Each tool does its job and does it well. But mixing concerns can be quite a challenge.

Packaging shards as RPM source packages works well as dependencies for other RPM packages.

But for non-packaged uses, there’s considerable friction. And I don’t think there’s a clean way to avoid all of that. We can maybe alleviate it a bit, though.

I guess I’m lacking understanding of the RPM ecosystem: What denotes a package as primary?
Anyway, I’m wondering if it’s even a good idea to put all RPM-installed shard sources in the default CRYSTAL_PATH. What’s the point of it? It hides dependencies instead of making them explicit.

I suppose adding support for version suffixes in require could be discussed. But honestly, it feels kinda off. The compiler shouldn’t worry about dependencies.

A generic solution for standalone scripts could be embedded dependency declarations directly in the source file. I think @bcardiff floated this idea internally a while ago.
Then the behaviour for standalone scripts would be the same as for a full repo with a shard.yml.

IMO dependencies should always be explicit about versions. So if you want to use RPM to provide source dependencies for a local project, you should pin the exact version, just like shards would do: install kemal-1.13.0, not floating kemal. That ensures the symlink stays valid.

Fair point on keeping the compiler lean, @straight-shoota. Looking at upstream’s default CRYSTAL_PATH (/usr/share/crystal/src:lib), /usr/share/crystal/shards was indeed our own distro-level injection to make quick scripts work, and I see how implicit resolution hides dependencies.

That said, the core issue here isn’t the compiler; it’s shards.

Right now, shards is strictly git-and-network-first. The only way to consume local source trees is via path: overrides, but running shards install with overrides rewrites shard.lock with local paths and # Overridden comments. That dirties git and breaks CI for anyone else pulling the repo.

In contrast, Cargo (Rust) and Go both use a shared local cache (~/.cargo/registry/src, GOPATH/pkg/mod). When they resolve a lockfile, they look in the local cache first. If the files are there, they build offline immediately without touching git or lockfiles.

If shards had a shared or system cache lookup path (e.g. $SHARDS_PATH or /usr/share/crystal/shards), we wouldn’t have this friction:

  1. shards install could resolve locked dependencies offline during package builds and in air-gapped environments without mutating shard.lock.
  2. Developers with multiple local projects wouldn’t have to duplicate dependencies on disk (which was actually brought up in this thread earlier by @crimson-knight).

As for standalone scripts: @bcardiff’s idea of embedded dependency metadata in the source file header (similar to PEP 723 in Python or Cargo-script in Rust) makes total sense. That would give single-file scripts explicit, reproducible version pins without turning the compiler into a package manager or forcing people to spin up a full git repo just for a 20-line tool.

For now, shards-link is our stopgap in Fedora to wire ./lib against system-installed shards without touching shard.lock. If upstream ever considers a shared cache or script manifest in shards, I’d be happy to share our findings and test cases.

Hello everyone,

We have major, game-changing news regarding the Fedora packaging effort for Crystal:

1. Dual-Track Packaging Model Formally Adopted

The biggest historical hurdle for compiled, lockfile-centric languages (like Go, Rust, and Crystal) in traditional Linux distributions has always been the tension between distribution unbundling orthodoxy and upstream dependency pinning:

  • Traditional unbundling purism required packaging every micro-dependency as an independent RPM. For complex applications, this created an overwhelming barrier to entry (reviewing and maintaining 40-60 separate packages in dist-git just to ship one CLI tool), and led to severe maintainer burnout whenever minor SemVer bumps collided across applications.
  • Uncontrolled bundling was unacceptable for distribution security, CVE tracking, and offline reproducibility.

Following recent precedent approved by the Fedora Engineering Steering Committee (FESCo) for Go, we have updated our official Fedora Packaging Guidelines draft (PR #1568) to formally establish the Dual-Track Packaging Model for Crystal:

  1. Track 1: Shared Shard Libraries (Unbundled)
    • Core libraries and shared building blocks are packaged as unbundled source RPMs installed into versioned paths: %{_datadir}/crystal/shards/<name>-<version>.
    • Supports parallel minor-version coexistence without RPM filesystem collisions.
    • Designed for local development and system administration scripts (crystal run script.cr with require "kemal") executing out of the box via CRYSTAL_PATH without needing local lib/ or Git access.
  2. Track 2: Vendored Dependencies for Leaf Applications
    • For standalone end-user applications, CLIs, and daemons where unbundling transitive shards is disproportionately burdensome, maintainers are permitted to ship an offline companion vendor archive (Source1: %{name}-%{version}-vendor.tar.xz).
    • The archive is unpacked offline during %prep.
    • Security and auditability are preserved via mandatory virtual provides: Provides: bundled(crystal-shard(<name>)) = <version> for every vendored shard, alongside aggregate SPDX license expressions in the License: field.
    • Applications compile into fully hardened, LTO-optimized, debuginfo-complete native ELF binaries in a single spec file.

This gives the Fedora Crystal ecosystem the best of both worlds: unbundled modularity for shared libraries, and a friction-free on-ramp for packaging applications like Kemal apps, CLI tools, and background services.


2. Full Ecosystem Deployed & Verified in Copr (12/12 Succeeded)

To validate this architecture end-to-end under genuine distribution build conditions, we ran a complete mass build and deployment of the Crystal stack in Fedora Rawhide Copr (renich/crystal).

All 12 packages across all four architectural tiers succeeded on both fedora-rawhide-x86_64 and fedora-rawhide-aarch64 with zero chroot skips and zero test failures:

  • Foundation: crystal-lang (1.21.0 compiler), shards (0.20.0), crystal-rpm-macros
  • Tier 1 (Core Shards): crystal-db (0.14.0), crystal-radix (0.4.1), crystal-backtracer (1.2.4)
  • Tier 2 (Database Drivers & Middleware): crystal-sqlite3 (0.23.0), crystal-pg (0.30.0), crystal-mysql (0.17.0), crystal-exception_page (0.5.0)
  • Tier 3 (Core Web Framework): crystal-kemal (1.13.0)
  • Tier 4 (Ecosystem Extensions & Apps): crystal-kemal-session (1.6.0), crystal-kemal-basic-auth (2.0.0), crystal-spec-kemal (1.3.0), and the multi-core load testing binary cryload (6.0.0)

Highlights from the build runs:

  • 100% Hermetic & Offline: Every single shard library resolved its dependencies strictly through /usr/share/crystal/shards/<name> with zero network calls during build time.
  • Upstream Test Suites Pass: The entire test suite (crystal spec) for Kemal, spec-kemal, and the database drivers executed cleanly inside isolated Mock roots against system-installed shards.
  • Native Hardening: Binaries built with full Fedora distribution flags (-specs=/usr/lib/rpm/redhat/redhat-hardened-ld, branch protection on aarch64, full debuginfo/debugsource generation) cleanly passed through the LLVM toolchain.

3. Links & Get Involved

The road is wide open for packaging Crystal applications in Fedora! Feedback, testing, and new packagers are warmly welcome.

Two corrections on that:

Thanks, @straight-shoota. We tested both, but ran into two blockers for distro packaging:

  1. $SHARDS_CACHE_PATH expects Git repositories: It resolves tags and commits from bare git objects. RPM packages install extracted source trees into /usr/share/crystal/shards/<name>-<version> without .git directories or VCS metadata, so shards can’t consume them as a cache.

  2. shard.override.yml mutates shard.lock: Running shards install with overrides rewrites shard.lock with local paths and # Overridden comments, which dirties git and breaks CI on other platforms. Running with --frozen rejects the overrides altogether. Also, path overrides still attempt to fetch un-overridden transitive dependencies over the network.

Directly symlinking from /usr/share/crystal/shards/ into ./lib sidesteps the solver entirely: it needs no Git objects, leaves shard.lock untouched, and works offline in air-gapped/chroot builds.

Wow, really cool!!

I never use fedora on my local laptop, before read this post, I thought this is a post about for fixing def install crystal.

After I read, I know this is create a complete first-class Crystal ecosystem in fedora, this is really a wonderful contribution to Crystal.

Thank you for your kind comments. Indeed, this is about making crystal a first class citizen in fedora.

I think Crystal has huge potential. And I know that it will be very useful in air -ap environments.

If developers take heart to it, which I think it’s very easy, Crystal will thrive in this ecosystem.

I’ve been using Fedora with KDE Plasma (Qt) on my personal laptop for a few years now, and I’m very happy with the system’s stability.
I made this choice after trying various Debian-based distros, which would eventually run into issues and instability, despite having a larger ecosystem.

I am delighted to hear about this valuable contribution to both the Fedora distro and Crystal.

Congratulations on the excellent work.

It’s almost official! We’re part of Fedora 46! \0/

Here’s the ticket: Making sure you're not a bot!

And we have many +1s!!

There is a pushback on vendroing by one of the members but they agree to allow it if things get complex. The good thing is that we have both pathways covered.

Not a single complaint or pushback on the language. I am very happy to say I think we’re almost there.

Approved! Fedora has approved Crystal as a feature for Fedora 46!

\0/

What a journey! Congrats @renich! :clap: :tada: