package

github.com/OrelSokolov/carat.cr

0.13.1 / published Nov 1, 2026 / repository

Package manager for Crystal — RubyGems-style workflow for shard binaries

carat logo — a faceted diamond

carat

All-in-one package manager for Crystal.

Extends shard.yml with a binaries: section — fully compatible with standard Crystal installs: shards install / shards build / shard.lock keep working untouched (shards simply ignores the extra key).

  • Build and share your Crystal shards like Ruby gems — a package can be a tool you use right now, not just a library you require.
  • System-wide install as a system binary — carat install owner/repo puts a ready-to-run release binary into ~/.carat/bin (on your PATH). One command, immediately useful. No manual installs, no PATH pain.
  • Compile flags per target system — Windows, Linux and Darwin often need different (link) flags; binaries: in shard.yml declares them per OS, so the same shard builds everywhere.
  • Real-world builds — custom main: files, native prebuild: steps (rake/cmake/make), explicit archives and link flags: GUI and app shards install as easily as hello-world CLIs.
  • Project mode too — carat install in a project delegates to shards install and builds binaries into ./.carat/bin; carat exec runs them like bundle exec.
  • Desktop apps — give a binary launchers: {menu: true, desktop: true} and add assets:appicon: (PNG/SVG), and carat ships it as a real GUI app: application-menu entry and/or desktop shortcut per platform (XDG .desktop / Start Menu .lnk / macOS .app), icon wrapped for the platform — no conversion pipeline (see Desktop apps).
  • Self-hosting — carat installs and updates itself through carat.
  • Shell autocompletion out of the box — bash, fish and zsh: carat exec <TAB> completes your project binaries (see Shell autocompletion).

In Ruby, a package can be a tool: gem install gives you a working binary in one command, immediately useful. Crystal shards never had that — a CLI shard meant git clone, crystal build, and hand-editing your PATH. carat fixes it:

$ carat install OrelSokolov/hello_binary
$ hello_binary
hello_binary 1.1.0: Hello from a carat-installed shard!

One command: the shard is fetched from GitHub, its binaries: are built with crystal build --release, and self-contained binaries land in ~/.carat/bin — on your PATH, ready to use. No manual installs, no PATH pain, no "first clone five repos and read the README".

The idea

A package should bring value the moment it's downloaded. If a shard ships a binary — a CLI tool, a code generator, a formatter — installing it should be as simple as installing a gem:

RubyCrystal with carat
gem install rubocopcarat install crystal-tools/linter
rubocop just workslinter just works

carat treats binaries: in shard.yml as a first-class delivery format: the binary is the package. Libraries keep working the usual way — carat install inside a project delegates to shards install.

Quick start

carat install owner/repo        # from GitHub: owner/repo shorthand
carat install name              # bare name → github.com/<name>/<name>
carat install https://github.com/owner/repo.git
carat install git@gitlab.com:owner/repo.git   # any git host (GitLab, Gitea,
                                              # sr.ht, ...), https or ssh
carat install ssh://git@git.example.com/owner/repo.git
carat install ~/code/my-tool    # local checkout
carat install .                 # the shard in the current directory — global
                                 # install while developing it locally

Sources are resolved in that order; the built binaries are the same in every case.

Private repositories need no special carat options: authentication is delegated to git itself, exactly like a shards git: dependency — ssh keys, a credential helper or an https token all work. For an https repo where git would prompt for a username/token, carat passes the prompt through to your terminal.

Commands

CommandBehavior
carat install <src>Global install: clone → release build → binaries in ~/.carat/bin. Resolves like shards: the latest release tag when the repo has tags, otherwise the default branch HEAD
carat install head <src>Always the default branch HEAD (fresh master, experimental): pinned per commit (version+git.commit.<sha>), so carat update head follows every push
carat install .Global install of the shard in the current dir (local development)
carat installIn a project: delegates to shards install, builds binaries: into ./.carat/bin
carat update <src>Update if the remote identity is newer (tag: semver decides; head: any new commit)
carat update head <src>Re-resolve the default branch HEAD and rebuild
carat updateIn a project: delegates to shards update, rebuilds ./.carat/bin
carat dev installLike install, but builds with a plain crystal build (no --release): faster codegen for development iteration
carat dev updateSame for update
carat only installLike install, but only runs shards install — no binary build (drop-in shards install compatibility)
carat only updateSame for update (shards update only)
carat shards installSynonym of carat only install
carat shards updateSynonym of carat only update
carat exec <cmd> [args]Like bundle exec: runs <cmd> with ./.carat/bin prepended to PATH
carat self installInstall-or-update carat itself from the canonical repo (OrelSokolov/carat.cr); carat self update is the same thing
carat completion installInstall shell autocompletion (bash / fish / zsh)

Both install and update accept:

  • --with-examples — also build the optional examples: section (see below)
  • --system — install binaries into the system directory instead of ~/.carat/bin: /usr/local/bin on Linux/macOS (on PATH by default), %ProgramFiles%\carat\bin + the machine PATH on Windows. Requires root/Administrator; without the rights carat fails fast — before any clone or build — with a clear error. The version store stays in ~/.carat/shards, so carat update keeps working.

Make your shard instantly installable

Add a binaries: section to shard.yml — carat's delivery contract. The simplest form is a list (entrypoints default to src/<name>.cr):

binaries:
  - hello_binary

The full form covers real-world builds — custom main files, native prebuild steps, compiler and linker flags that differ per OS:

binaries:
  widgets_gallery:
    main: examples/widgets_gallery.cr    # any file, not just src/
    prebuild: rake build:native          # native build step, run in the shard root first
    flags:                               # crystal build flags
      "*": [--something]
      darwin: [-DUSE_METAL=0]
    link_flags:                          # linker flags, per-OS selectors
      "*": [-Llib]
      linux: [lib/libfoo.a]              # explicit archive — ld order safe
      windows: [/LIBPATH:lib]

Shared keys: all: and each:

In the mapping form two reserved keys control the whole section — no need to repeat the same spec on every binary:

binaries:
  all:                      # ONCE per section, before any binary is built
    prebuild: rake build:native
  each:                     # defaults for EVERY binary (own keys override)
    flags: [-DSOME=1]
  hello: {}                 # gets each:'s flags, all:'s prebuild already ran
  widgets_gallery:
    main: examples/widgets_gallery.cr
  tool:
    flags: [-DOTHER=1]      # own flags win over each:'s
  • all: — the section-wide step: its prebuild runs one time before the section's builds (the shared native step — not once per binary), its flags/link_flags are the base for every binary.
  • each: — per-binary defaults: prebuild runs before each binary, flags/link_flags apply to each unless the binary overrides them.
  • Precedence: the binary's own keys > each: > all:. Neither key is a binary itself.

Dependencies declared in dependencies: are resolved automatically: a global install runs shards install in the stored copy before building, so shards with dependencies install as smoothly as dep-free ones. prebuild: stays for native build steps (rake/cmake/make), not for dependency resolution.

Selector resolution follows crosspack's hosts: rules: exact OS family (linux / darwin / windows), then * — first hit wins. A plain list instead of a mapping applies to every OS. Relative paths in link_flags are resolved against the shard root (mold/ld do not open relative --link-flags paths).

Version resolution mirrors shards: a carat install/carat update picks the latest release tag of the repository (annotate your tags: git tag v1.2.0 && git push --tags); untagged repos ride the default branch HEAD. carat install head <src> / carat update head <src> always track fresh master — useful for experimental work-in-progress — with the install pinned per commit (version+git.commit.<sha>, the shard.lock notation), so updates follow every push even without a version bump. carat self tracks master this way (the canonical repo isn't tagged per release). No releases page, no tarballs, no install scripts.

shards ignores the extra binaries: key, so the shard stays a perfectly normal shard — shards install / shards build keep working (verified with shards 0.20).

Optional examples: examples:

examples: is the same contract as binaries: (list or full mapping form), but for demo/sample binaries. It is never built by default — recursive installs would balloon quickly — only on demand:

carat install owner/repo --with-examples   # global: binaries in ~/.carat/bin,
                                           # examples in ~/.carat/examples
carat install --with-examples              # project: examples in ./.carat/examples
examples:
  widgets_gallery:
    main: examples/widgets_gallery.cr

Example binaries land in a separate dir (~/.carat/examples / ./.carat/examples), so they never pollute your PATH.

Desktop apps: launchers: and assets:appicon:

A binary with a launchers: mapping is a desktop application, not just a CLI tool — carat gives it desktop presence beyond the PATH entry:

assets:
  appicon: assets/icon.png     # or .svg — the app icon, any path in the shard

binaries:
  myapp:
    launchers:
      menu: true               # application menu entry
      desktop: true            # shortcut on the desktop (in addition)
  helper: {}                   # regular CLI binary, business as usual

The three launcher flags:

FlagDefaultMeaning
terminaltrueThe usual carat install: binary in ~/.carat/bin, on PATH. false moves it off the PATH into its own app dir (~/.carat/apps/myapp/bin/) — for pure-GUI apps
menufalseEntry in the application menu: XDG .desktop on Linux, Start Menu shortcut on Windows, .app bundle in ~/Applications on macOS
desktopfalseShortcut on the user's desktop, in addition to the menu entry — not instead of it

On install, icons land in ~/.carat/apps/myapp/icons/ and the native launchers are generated for the platform:

PlatformmenudesktopIcon
Linux (any distro)~/.local/share/applications/myapp.desktop~/Desktop/myapp.desktop (XDG_DESKTOP_DIR)PNG/SVG as-is
WindowsStart Menu .lnkDesktop .lnk.ico built from the PNG
macOS~/Applications/Myapp.appsymlink on the Desktop.icns built from the PNG

Two things keep this simple:

  • The icon format is verified by magic bytes (marcel.cr port) — PNG or SVG only, a mislabeled file fails the install with a clear error.
  • No conversion pipeline: .ico and .icns are containers that accept a PNG payload as-is (Vista+ / modern macOS), so carat wraps the bytes itself; on Linux .desktop accepts PNG/SVG paths directly. SVG sources work on Linux as-is; on Windows/macOS they need a rasterizer carat doesn't ship — the shortcut is created without an icon.

The application knows nothing about carat: its read-only assets (if any) live next to the binary in the app dir — the portable-app convention, resolved relative to the executable — and writable data belongs in the platform user dirs (XDG_DATA_HOME, %APPDATA%), not in carat's layout. Launchers reference absolute paths, so carat update regenerates them on every version change.

Build flags from dependencies

A shard's binaries often link C libraries through binding shards. A dependency may declare top-level flags: / link_flags: in its own shard.yml (same per-OS selector format), and carat applies them when building binaries of any shard that depends on it:

# lib/some_binding/shard.yml
name: some_binding
version: 1.0.0
flags:
  "*": [-DSOME_BINDING=1]
link_flags:
  linux: [-lsome]

Flags are collected recursively from lib/**/shard.yml (populated by shards install; in global installs — by a prebuild: step), merged with the binary's own flags and deduplicated (own flags first, order preserved). The merged list is printed during the build, so what the compiler actually received is always visible. shards ignores these keys, so the dependency stays a normal shard.

What happens on install

  1. Fetch — the source is resolved like shards resolves a git dependency: the latest release tag when the repo has one (cloned at that tag, --depth 1), otherwise the default branch HEAD; local directories are copied as-is.
  2. Store — sources are archived at ~/.carat/shards/<name>/<version>/ with a current version pointer, so installed versions are inspectable and switchable later.
  3. Resolve — shards with dependencies: get a shards install run in the stored copy (the compiler resolves bare requires from lib/).
  4. Build — every binaries: entry is compiled with crystal build --release from the stored copy and validated by magic bytes (ELF / Mach-O / PE) before it's allowed into the bin dir. Entries build in a worker pool: concurrent crystal build processes scale ~N in wall-clock, and the pool width is derived from the machine's current state (CPU cores, MemAvailable / the peak compiler RSS measured while the first build warms its cache slot): workers = min(cores, MemAvailable * 0.7 / peak_rss, 8). Before handing out each next job a gate re-checks free RAM and stalls the queue when it runs low, and a build that still died (an OOM is exit 137 / SIGKILL) is retried once sequentially. Each worker keeps its own persistent compiler cache (<shard>/.carat/cache/jN) — concurrent compilers race on the shared ~/.cache/crystal. Set CARAT_JOBS=N to bypass the adaptive sizing (benchmarks, A/B runs).
  5. Link — the binary is renamed into ~/.carat/bin/. Overwriting an existing binary always prints WARNING: OVERWRITING WITH NEW BINARY and is recorded in the audit log (~/.carat/logs/carat.log), so silent hijacking of your PATH is impossible. (Global installs only — project rebuilds into ./.carat/bin stay quiet: overwriting your own build is the normal dev loop.)

Layout

~/.carat/bin/                    global binaries (entry points only)
~/.carat/examples/               global example binaries (--with-examples)
~/.carat/apps/<name>/            GUI apps (app:): binary + icons + manifest
~/.carat/shards/<name>/<identity>/ source store (tag: 1.2.3; HEAD: 1.2.3+git.commit.a1b2c3d4e5)
~/.carat/shards/<name>/current   installed version pointer
~/.carat/logs/carat.log          install/overwrite/update audit log
~/.carat/tmp/                    staging for clones
./.carat/bin/                    project binaries (carat install / carat exec)
./.carat/examples/               project example binaries (--with-examples)
./.carat/cache/jN/               per-worker crystal compiler caches (build pool)

Only self-contained binaries ever go into the bin dir — no assets, no wrappers, no PATH pollution.

Shell autocompletion

carat completion install
  • bash — registers complete -C carat carat in the XDG bash-completion dir (~/.local/share/bash-completion/completions/): no rc edits, live on the first TAB in a new shell.
  • fish — writes ~/.config/fish/completions/carat.fish (when fish is installed).
  • zsh — prints a two-line bashcompinit snippet for ~/.zshrc.

What completes:

  • carat <TAB> — subcommands (install, update, exec, completion, --help, --version)
  • carat exec <TAB> — the binaries of the current project (./.carat/bin): carat itself is excluded, and only the first argument is completed — the words after it belong to the launched program, and a fully typed word is never re-offered (no duplicate inserts on repeated TABs)
  • carat update <TAB> — globally installed shards

Installing carat

No packages, no sudo — carat is a plain console binary:

git clone https://github.com/OrelSokolov/carat.cr.git && cd carat.cr
rake install

rake install builds the release binary, copies it to ~/.carat/bin (the first binary there is carat itself) and appends ~/.carat/bin to your PATH — ~/.bashrc / ~/.zshrc on Unix, the user PATH on Windows. Idempotent: run it as many times as you like.

On a fresh host without a toolchain, crosspack sets it up first: gem install crosspack && crosspack deps.

After that, carat updates itself like any other shard:

carat self update        # = carat update OrelSokolov/carat.cr;
                          # carat self install does the same (install-or-update)

Rebuilding over the running binary is safe (build-to-temp + rename).

Status and roadmap

v0.1 focuses on the core promise: gems-style install of binary shards from GitHub and local checkouts.

Next:

  • Shardbox lookup for bare names, carat use <name>@<ver> version switching
  • carat list / carat uninstall
  • Full dependency resolution on a fork of shards — carat as the single package manager replacing it

License

MIT — see LICENSE.

API