Hermetic Ruby packages from Gemfile.lock. Import a lockfile, build every gem once, cache forever.
$ onix import ~/src/rails # parse Gemfile.lock → packagesets/rails.jsonl
$ onix generate # prefetch hashes, write nix derivations
$ onix build # build everything
Bundler solves dependency resolution but not hermetic builds. Nix solves hermetic builds but doesn't understand lockfiles. onix bridges them:
- Lockfile in, nix derivations out. Your existing
Gemfile.lockbecomes the source of truth. - System libraries only. Native extensions link against nixpkgs — no vendored copies of openssl, libxml2, sqlite, etc.
- One derivation per gem. Individually cacheable, parallel builds, content-addressed store paths.
- Build once, cache forever. Same lockfile + same nixpkgs = same store paths. CI and dev share the cache.
gem install https://github.com/tobi/onixRequires Ruby ≥ 3.1 and Nix.
mkdir my-packages && cd my-packages
onix initCreates the directory structure: packagesets/, overlays/, nix/ruby/.
onix import ~/src/myapp # reads myapp/Gemfile.lock
onix import --name blog Gemfile.lock # explicit nameParses the lockfile and writes a hermetic JSONL packageset to packagesets/<name>.jsonl. For git-sourced gems, clones the repo to discover monorepo subdirectories.
onix generate # default: 20 parallel prefetch jobs
onix generate -j 8 # fewer jobsPrefetches SHA256 hashes for all gems via nix-prefetch-url and nix-prefetch-git, then writes:
nix/ruby/<name>.nix— one file per gem with all versions and hashesnix/<project>.nix— per-project gem selection, bundlePath, and devShellnix/build-gem.nix— wrapper around nixpkgsbuildRubyGemnix/gem-config.nix— overlay loader
onix build # build all projects
onix build myapp # build all gems for one project
onix build myapp nokogiri # build a single gem
onix build -k # keep going past failuresPipes through nix-output-monitor when available. On failure, tells you exactly what to do:
✗ sqlite3 → create overlays/sqlite3.nix
nix log /nix/store/...-sqlite3-2.8.0.drv
onix checkRuns nix-eval, packageset-complete, and secrets checks in parallel.
Each project nix file exports individual gems, a merged bundlePath, and a devShell:
{ pkgs ? import <nixpkgs> {}, ruby ? pkgs.ruby_3_4 }:
let
project = import ./nix/rails.nix { inherit pkgs ruby; };
in project.devShell {
buildInputs = with pkgs; [ sqlite postgresql ];
}Sets BUNDLE_PATH, BUNDLE_GEMFILE, and GEM_PATH automatically. Ruby can require any gem in the bundle without bundler/setup.
For CI scripts, Docker images, or custom derivations:
project.bundlePath # → /nix/store/...-rails-bundle
# contains gems/*, specifications/*, extensions/*When a gem needs system libraries or custom build steps, create overlays/<gem-name>.nix.
# overlays/pg.nix
{ pkgs, ruby, ... }: with pkgs; [ libpq pkg-config ]# overlays/sqlite3.nix
{ pkgs, ruby, ... }: {
deps = with pkgs; [ sqlite pkg-config ];
extconfFlags = "--enable-system-libraries";
}Some gems need other gems during extconf.rb. Use buildGems with the buildGem function:
# overlays/nokogiri.nix
{ pkgs, ruby, buildGem, ... }: {
deps = with pkgs; [ libxml2 libxslt pkg-config zlib ];
extconfFlags = "--use-system-libraries";
buildGems = [
(buildGem "mini_portile2")
];
}# overlays/tiktoken_ruby.nix
{ pkgs, ruby, buildGem, ... }: {
deps = with pkgs; [ rustc cargo libclang ];
buildGems = [ (buildGem "rb_sys") ];
preBuild = ''
export CARGO_HOME="$TMPDIR/cargo"
mkdir -p "$CARGO_HOME"
export LIBCLANG_PATH="${pkgs.libclang.lib}/lib"
'';
}| Field | Type | Effect |
|---|---|---|
deps |
list | Added to nativeBuildInputs |
extconfFlags |
string | Appended to ruby extconf.rb |
buildGems |
list | Gems needed at build time (GEM_PATH set automatically) |
preBuild |
string | Runs before the build phase |
postBuild |
string | Runs after the build phase |
buildPhase |
string | Replaces the default build entirely |
postInstall |
string | Runs after install |
# overlays/therubyracer.nix — abandoned, use mini_racer
{ pkgs, ruby, ... }: { buildPhase = "true"; }my-packages/
├── packagesets/ # JSONL packagesets (one per project)
│ ├── rails.jsonl
│ └── liquid.jsonl
├── overlays/ # Hand-written build overrides
│ ├── nokogiri.nix
│ └── sqlite3.nix
└── nix/ # Generated — never edit
├── ruby/ # Per-gem derivations
├── rails.nix # Per-project entry point
├── build-gem.nix
└── gem-config.nix
- Nix-native fetch.
generateprefetches hashes;buildusesfetchurl/builtins.fetchGit. No local cache. - System libraries only. Native extensions link against nixpkgs. Vendored copies are replaced.
- Lockfile is truth. Packagesets are hermetic JSONL parsed once during import.
- One derivation per gem. Individually cacheable, parallel, content-addressed.
- Overlays win. Manual overrides always take precedence over auto-detection.
- Parameterized runtime.
rubyflows through every derivation — one argument changes the whole build.