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
16 changes: 16 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,22 @@ gemfileEnv {
};
```

**"gems4nix: Gemfile uses the `gemspec` directive but no gemspec was supplied"**
Your Gemfile calls `gemspec` (the default `bundle gem` layout). Group inference
runs Bundler against a sandboxed Gemfile, so the `.gemspec` — and anything it
`require_relative`s — must be handed to `gemfileEnv` explicitly:
```nix
gemfileEnv {
# ...
gemspec = ./my-gem.gemspec;
extraFiles = {
"lib/my_gem/version.rb" = ./lib/my_gem/version.rb;
};
};
```
Alternatively, pass an explicit `gemGroups = { name = [ "default" ]; ... }`
mapping to skip Bundler group inference entirely.

## How It Works

The pipeline has three stages:
Expand Down
8 changes: 4 additions & 4 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 25 additions & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
description = "Bundle Ruby gems into an environment, using Bundler checksums";

inputs = {
nixpkgs.url = "github:NixOS/nixpkgs?ref=24.11";
nixpkgs.url = "github:NixOS/nixpkgs?ref=26.05";
};

outputs =
Expand Down Expand Up @@ -58,6 +58,17 @@
gemfileLock = ./test/integration/platform-gems/Gemfile.lock;
groups = [ "default" ];
};

gemspecDirectiveGems = gemfileEnv {
name = "gemspec-directive-test";
gemfile = ./test/integration/gemspec-directive/Gemfile;
gemfileLock = ./test/integration/gemspec-directive/Gemfile.lock;
gemspec = ./test/integration/gemspec-directive/foo.gemspec;
extraFiles = {
"lib/foo/version.rb" = ./test/integration/gemspec-directive/lib/foo/version.rb;
};
groups = [ "default" ];
};
in
{
unit-resolve = nixEvalCheck "resolve" ./test/unit/test-resolve-logic.nix;
Expand All @@ -76,6 +87,19 @@
ruby ${./test/integration/platform-gems/validate.rb}
touch $out
'';

integration-gemspec-directive =
pkgs.runCommand "integration-gemspec-directive"
{
buildInputs = [
pkgs.ruby
gemspecDirectiveGems
];
}
''
ruby ${./test/integration/gemspec-directive/validate.rb}
touch $out
'';
}
);

Expand Down
12 changes: 11 additions & 1 deletion lib/gemfile-env/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ in
"test"
],
gemGroups ? null, # null = auto-detect via gem-groups.rb IFD; attrset = override
gemspec ? null, # path to *.gemspec when the Gemfile uses the `gemspec` directive
extraFiles ? { }, # { "relative/dest" = ./src; } — files the gemspec require_relatives
gemConfig ? defaultGemConfig,
ruby ? defaultRuby,
debug ? false, # when true, builtins.trace each gem being built
Expand All @@ -39,7 +41,15 @@ let

# ── parsing ──────────────────────────────────────────────────
parseGemfileAndLockfile = callPackage ./parse-gemfile-and-lockfile.nix { };
parsed = parseGemfileAndLockfile { inherit gemfile gemfileLock gemGroups; };
parsed = parseGemfileAndLockfile {
inherit
gemfile
gemfileLock
gemGroups
gemspec
extraFiles
;
};
gemMetadata = parsed.gems;
depGraph = parsed.depGraph;

Expand Down
75 changes: 63 additions & 12 deletions lib/gemfile-env/parse-gemfile-and-lockfile.nix
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
runCommand,
ruby,
bundler,
git,
fetchurl,
stdenv,
defaultGemConfig,
Expand All @@ -42,6 +43,8 @@
gemfile,
gemfileLock,
gemGroups ? null, # null = auto-detect via gem-groups.rb; attrset = override
gemspec ? null, # path to *.gemspec when the Gemfile uses the `gemspec` directive
extraFiles ? { }, # { "relative/dest" = ./src; } — files the gemspec require_relatives
}:

let
Expand All @@ -56,21 +59,69 @@ let

# ── IO ───────────────────────────────────────────────────────

# Detect the Bundler `gemspec` directive in the Gemfile. When present, the
# group-detection IFD sandbox must include the gemspec (and anything it
# require_relatives) or Bundler aborts with `Bundler::InvalidOption: There
# are no gemspecs`. See github.com/omc/gems4nix issue #2.
gemfileText = builtins.readFile gemfile;
gemfileUsesGemspec =
let
isGemspecLine = line: builtins.match "[[:space:]]*gemspec([[:space:]#(].*)?" line != null;
in
lib.any isGemspecLine (lib.splitString "\n" gemfileText);

needsGemspecButMissing = gemfileUsesGemspec && gemspec == null && gemGroups == null;

gemspecError = throw ''
gems4nix: Gemfile uses the `gemspec` directive but no gemspec was supplied.

Bundler needs the .gemspec (and any files it require_relatives) available
at group-detection time. Add these arguments to your gemfileEnv call:

gemspec = ./<your-name>.gemspec;
extraFiles = { "lib/<your-name>/version.rb" = ./lib/<your-name>/version.rb; };

Alternatively, pass an explicit `gemGroups = { ... }` mapping to skip
Bundler group inference entirely.
'';

# Copy the caller-supplied gemspec into the sandbox as `project.gemspec`.
# The filename doesn't matter to Bundler — it globs `*.gemspec` — but the
# gemspec's require_relative calls resolve from the sandbox root, so
# extraFiles must land at their declared destinations.
copyGemspecCmd = lib.optionalString (gemspec != null) ''
cp ${gemspec} project.gemspec
'';

copyExtraFilesCmd = lib.concatMapStringsSep "\n" (dest: ''
mkdir -p "$(dirname ${lib.escapeShellArg dest})"
cp ${extraFiles.${dest}} ${lib.escapeShellArg dest}
'') (builtins.attrNames extraFiles);

# use the Gemfile to produce group information for each gem
# (skipped when the caller supplies an explicit gemGroups override)
gemGroupsJson =
runCommand "gem-groups-json"
{
buildInputs = [
ruby
bundler
];
}
''
cp ${gemfile} Gemfile
cp ${gemfileLock} Gemfile.lock
ruby ${./gem-groups.rb} > $out
'';
if needsGemspecButMissing then
gemspecError
else
runCommand "gem-groups-json"
{
# git is needed because the canonical `bundle gem` gemspec computes
# spec.files via `IO.popen(%w[git ls-files -z])`. Without git on PATH
# that popen raises Errno::ENOENT and gemspec evaluation aborts.
buildInputs = [
ruby
bundler
git
];
}
''
cp ${gemfile} Gemfile
cp ${gemfileLock} Gemfile.lock
${copyGemspecCmd}
${copyExtraFilesCmd}
ruby ${./gem-groups.rb} > $out
'';

# ── pure assembly (delegated to helpers) ─────────────────────

Expand Down
7 changes: 7 additions & 0 deletions test/integration/gemspec-directive/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
source "https://rubygems.org"

gemspec

group :test do
gem "rspec", "~> 3.0"
end
46 changes: 46 additions & 0 deletions test/integration/gemspec-directive/Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
PATH
remote: .
specs:
foo (0.1.0)
rake (~> 13.0)

GEM
remote: https://rubygems.org/
specs:
diff-lcs (1.6.2)
rake (13.4.2)
rspec (3.13.2)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
rspec-mocks (~> 3.13.0)
rspec-core (3.13.6)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.8)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-support (3.13.7)

PLATFORMS
arm64-darwin-25
ruby
universal-darwin

DEPENDENCIES
foo!
rspec (~> 3.0)

CHECKSUMS
diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
foo (0.1.0)
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47
rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c

BUNDLED WITH
2.7.2
14 changes: 14 additions & 0 deletions test/integration/gemspec-directive/foo.gemspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

require_relative "lib/foo/version"

Gem::Specification.new do |spec|
spec.name = "foo"
spec.version = Foo::VERSION
spec.authors = ["gems4nix test"]
spec.summary = "Fixture exercising the `gemspec` directive"
spec.files = ["lib/foo/version.rb"]
spec.required_ruby_version = ">= 3.0.0"

spec.add_dependency "rake", "~> 13.0"
end
5 changes: 5 additions & 0 deletions test/integration/gemspec-directive/lib/foo/version.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true

module Foo
VERSION = "0.1.0"
end
16 changes: 16 additions & 0 deletions test/integration/gemspec-directive/validate.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

# Verifies group inference succeeds against a Gemfile using the `gemspec`
# directive (default `bundle gem` layout). See github.com/omc/gems4nix
# issue #2. The regression is that the group-detection IFD sandbox
# previously omitted the gemspec, so Bundler aborted before any gems
# could be resolved.

require "rake"

unless defined?(Rake::VERSION)
warn "expected Rake constant to be defined after require"
exit 1
end

puts "gemspec-directive: rake #{Rake::VERSION} loaded from #{Rake.method(:application).source_location.first}"
Loading