Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
name: Bug report
about: A gem fails to build or load
---

**What happened?**
(Paste the error message)

**Gemfile.lock** (attach or paste the relevant section)

**System:** (e.g., aarch64-darwin, x86_64-linux)

**Bundler version:** (output of `bundle --version`)

**PLATFORMS section of your Gemfile.lock:**
(paste it -- this is often the key to diagnosing platform issues)

**Did you run `bundle lock --add-platform`?**
(yes/no, and which platforms)
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
flake-check:
name: nix flake check (x86_64-linux)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install Nix
uses: DeterminateSystems/nix-installer-action@main

- name: Enable Magic Nix Cache
uses: DeterminateSystems/magic-nix-cache-action@main

- name: nix flake check
run: nix flake check --print-build-logs
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
.direnv
result
result-*
tmp
60 changes: 60 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Architecture

## Pipeline

```mermaid
flowchart TD
input([Gemfile + Gemfile.lock])
parse["<b>parse.nix</b> — pure Nix<br/>reads lockfile text<br/>emits { gemName, version, platform, source, sha256, groups }"]
resolve["<b>resolve.nix</b> — pure Nix<br/>filters by group and platform<br/>expands transitive deps<br/>picks one variant per name"]
build["<b>default.nix</b> — nixpkgs<br/>applies gemConfig<br/>calls buildRubyGem<br/>combines into buildEnv"]
output([Derivation with all gems on GEM_PATH])

input --> parse --> resolve --> build --> output
```

## Key Design Decisions

1. Parse the lockfile in pure Nix (no bundix, no gemset.nix). Group extraction
uses a Ruby IFD because Gemfile semantics are Bundler's domain.
2. Prefer precompiled native gems over source compilation.
3. Only apply gemConfig overrides to ruby-platform gems (precompiled gems
should not need source build patches).
4. Delegate to nixpkgs' `buildRubyGem` and `defaultGemConfig` rather than
maintaining custom builders.

## What We Use from Nixpkgs

- `buildRubyGem` -- builds individual gem derivations
- `defaultGemConfig` -- per-gem build overrides (nokogiri, grpc, etc.)
- `buildEnv` -- combines gem derivations into a single environment path

We reimplement lockfile parsing, group filtering, and platform resolution
because upstream assumes a `gemset.nix` generated by bundix. We parse the
lockfile directly.

## File Map

```
lib/gemfile-env/
default.nix Orchestrator. Chains parse -> resolve -> build.
parse.nix Lockfile parsing. Pure Nix, takes { lib }.
resolve.nix Filtering and resolution. Pure Nix, takes { lib }.
parse-gemfile-and-lockfile.nix IO shell: readFile, runCommand, calls parse.nix.
gem-configs.nix Local per-gem build overrides.
gem-groups.rb Ruby IFD script for Gemfile group extraction.

test/
helpers.nix Shared assertEq, assertThrows, fixtures.
unit/test-parse-logic.nix Unit tests for parse.nix.
unit/test-resolve-logic.nix Unit tests for resolve.nix.
unit/test-pipeline-logic.nix End-to-end parse + resolve tests.
unit/test-{parse,resolve,pipeline}.nix Wrappers that import logic + lib.
fixtures/lockfiles/ Lockfile corpus for testing.
integration/ Integration tests with real gem builds.

examples/
simple/ Pure-ruby gems (rack, rake).
medium/ Native gems (nokogiri, puma, ethon).
complex/ Rails 8, git/path sources.
```
116 changes: 116 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Contributing to gems4nix

## Architecture Overview

gems4nix is a three-stage pipeline: **parse** the lockfile, **resolve** gems
for the target system, and **build** derivations via nixpkgs' `buildRubyGem`.
All parsing and resolution logic is pure Nix (no IO, no nixpkgs deps), making
it directly testable with `nix eval`. See [ARCHITECTURE.md](ARCHITECTURE.md)
for the full pipeline diagram and file map.

## How to Run Tests

```sh
# All checks (unit + integration) via flake
nix flake check

# Unit tests individually (returns true or throws on failure)
nix eval --file test/unit/test-parse.nix --json
nix eval --file test/unit/test-resolve.nix --json
nix eval --file test/unit/test-pipeline.nix --json

# Integration examples
cd examples/simple && nix flake check --no-write-lock-file
cd examples/medium && nix flake check --no-write-lock-file
cd examples/complex && nix flake check --no-write-lock-file

# Everything at once
nix eval --file test/unit/test-parse.nix --json && \
nix eval --file test/unit/test-resolve.nix --json && \
nix eval --file test/unit/test-pipeline.nix --json && \
echo "unit tests passed" && \
for ex in simple medium complex; do
(cd examples/$ex && nix flake check --no-write-lock-file) || exit 1
done && \
echo "all tests passed"
```

## How to Add a Test

1. Identify which layer the test belongs to:
- **Parsing** (lockfile text to gem attrs) -- `test/unit/test-parse-logic.nix`
- **Resolution** (filtering, platform matching) -- `test/unit/test-resolve-logic.nix`
- **Pipeline** (end-to-end parse + resolve) -- `test/unit/test-pipeline-logic.nix`

2. Add a test case. Each test is an `assertEq` call with a descriptive name:
```nix
(assertEq "test_my_new_case"
(someFunction someInput)
expectedOutput)
```

3. Add it to the `allTests` list at the bottom of the file.

4. Run the test:
```sh
nix eval --file test/unit/test-parse.nix --json
```
It returns `true` on success or throws with the test name and diff on failure.

## How to Contribute a Lockfile Fixture (Bug Reports)

If you have a `Gemfile.lock` that causes a build failure or incorrect behavior:

1. Add your `Gemfile.lock` to `test/fixtures/lockfiles/` with a descriptive
name (e.g., `nokogiri-ruby-only.lock`).
2. Add a unit test in the appropriate `test-*-logic.nix` file that parses or
resolves against your lockfile content and demonstrates the bug.
3. Open a PR or issue with both files.

Minimal lockfile excerpts are preferred over full lockfiles. Include at least
the `GEM`, `PLATFORMS`, and `CHECKSUMS` sections relevant to the bug.

## How to Add a Gem Config Override

Gem config overrides live in `lib/gemfile-env/gem-configs.nix`. Each entry is a
function that receives the gem's attributes and returns an attrset to merge:

```nix
{
my-gem = attrs: {
buildInputs = [ some-package ];
NIX_CFLAGS_COMPILE = "-I${some-package}/include";
};
}
```

Notes:
- Gem configs are only applied to ruby-platform gems (precompiled native gems
skip config, since they do not need source build overrides).
- Check `nixpkgs.defaultGemConfig` first -- it may already handle your gem.
- Our local overrides in `gem-configs.nix` layer on top of `defaultGemConfig`.

## Test Philosophy

Tests follow a **red-green-refactor** loop:

1. **Red:** Write a test that captures expected behavior. Run it. Watch it fail
(or verify it passes if testing existing correct behavior).
2. **Green:** Make the minimal change to pass the test.
3. **Refactor:** With passing tests as a safety net, clean up the code. Re-run
tests to confirm nothing broke.

Key principles:
- Unit tests are pure Nix evaluations -- no network, no builds, no IO.
- Unit tests use synthetic lockfile content (inline strings), not real files.
- Integration tests (`examples/`) use real `Gemfile.lock` files from rubygems.org
and build actual gem derivations.
- Shared assertion helpers (`assertEq`, `assertThrows`) live in `test/helpers.nix`.

## Code Style

- Format with `nixfmt`.
- Pure logic files (`parse.nix`, `resolve.nix`) take only `{ lib }:` as input
for testability. No nixpkgs build dependencies.
- IO (file reads, IFD) is isolated in `parse-gemfile-and-lockfile.nix`.
- Comments explain "why", not "what".
Loading
Loading