summary refs log tree commit diff
diff options
context:
space:
mode:
authorCharles Strahan <charles@cstrahan.com>2017-07-28 23:22:21 -0400
committerGitHub <noreply@github.com>2017-07-28 23:22:21 -0400
commit2b57cb91691d7fef08266f292108c8f2bd00f490 (patch)
treeb08c18df18f9c24cccdd0b0c6849d2774aa96794
parente8d8633f4e9bb67db56cafdbfa658b57bcac7211 (diff)
parent5142e8f2b2057ec02c59f4406019d41bebaff59f (diff)
downloadnixpkgs-2b57cb91691d7fef08266f292108c8f2bd00f490.tar
nixpkgs-2b57cb91691d7fef08266f292108c8f2bd00f490.tar.gz
nixpkgs-2b57cb91691d7fef08266f292108c8f2bd00f490.tar.bz2
nixpkgs-2b57cb91691d7fef08266f292108c8f2bd00f490.tar.lz
nixpkgs-2b57cb91691d7fef08266f292108c8f2bd00f490.tar.xz
nixpkgs-2b57cb91691d7fef08266f292108c8f2bd00f490.tar.zst
nixpkgs-2b57cb91691d7fef08266f292108c8f2bd00f490.zip
Merge pull request #25980 from nyarly/bundlerenv_usecases
BundlerEnv, now with groups and paths
-rw-r--r--doc/languages-frameworks/ruby.xml25
-rw-r--r--lib/maintainers.nix1
-rw-r--r--pkgs/development/ruby-modules/bundled-common/default.nix156
-rw-r--r--pkgs/development/ruby-modules/bundled-common/functions.nix75
-rw-r--r--pkgs/development/ruby-modules/bundled-common/gen-bin-stubs.rb (renamed from pkgs/development/ruby-modules/bundler-env/gen-bin-stubs.rb)0
-rw-r--r--pkgs/development/ruby-modules/bundled-common/test.nix50
-rw-r--r--pkgs/development/ruby-modules/bundler-app/default.nix48
-rw-r--r--pkgs/development/ruby-modules/bundler-env/default.nix164
-rw-r--r--pkgs/development/ruby-modules/bundler-env/test.nix33
-rw-r--r--pkgs/development/ruby-modules/bundler-env/test/Gemfile0
-rw-r--r--pkgs/development/ruby-modules/bundler-env/test/Gemfile.lock0
-rw-r--r--pkgs/development/ruby-modules/bundler-env/test/gemset.nix10
-rw-r--r--pkgs/development/ruby-modules/gem/default.nix1
-rwxr-xr-xpkgs/development/ruby-modules/runtests.sh6
-rw-r--r--pkgs/development/ruby-modules/testing/assertions.nix28
-rw-r--r--pkgs/development/ruby-modules/testing/driver.nix20
-rw-r--r--pkgs/development/ruby-modules/testing/stubs.nix33
-rw-r--r--pkgs/development/ruby-modules/testing/tap-support.nix21
-rw-r--r--pkgs/development/ruby-modules/testing/testing.nix62
-rw-r--r--pkgs/development/tools/corundum/Gemfile3
-rw-r--r--pkgs/development/tools/corundum/Gemfile.lock56
-rw-r--r--pkgs/development/tools/corundum/default.nix15
-rw-r--r--pkgs/development/tools/corundum/gemset.nix154
-rw-r--r--pkgs/top-level/all-packages.nix3
24 files changed, 834 insertions, 130 deletions
diff --git a/doc/languages-frameworks/ruby.xml b/doc/languages-frameworks/ruby.xml
index b13da92dcc4..eb1696ad224 100644
--- a/doc/languages-frameworks/ruby.xml
+++ b/doc/languages-frameworks/ruby.xml
@@ -41,7 +41,29 @@ bundlerEnv rec {
 <para>Please check in the <filename>Gemfile</filename>, <filename>Gemfile.lock</filename> and the <filename>gemset.nix</filename> so future updates can be run easily.
 </para>
 
-<para>Resulting derivations also have two helpful items, <literal>env</literal> and <literal>wrapper</literal>. The first one allows one to quickly drop into
+<para>For tools written in Ruby - i.e. where the desire is to install a package and then execute e.g. <command>rake</command> at the command line, there is an alternative builder called <literal>bundlerApp</literal>. Set up the <filename>gemset.nix</filename> the same way, and then, for example:
+</para>
+
+<screen>
+  <![CDATA[{ lib, bundlerApp }:
+
+bundlerApp {
+  pname = "corundum";
+  gemdir = ./.;
+  exes = [ "corundum-skel" ];
+
+  meta = with lib; {
+    description = "Tool and libraries for maintaining Ruby gems.";
+    homepage    = https://github.com/nyarly/corundum;
+    license     = licenses.mit;
+    maintainers = [ maintainers.nyarly ];
+    platforms   = platforms.unix;
+  };
+}]]>
+
+<para>The chief advantage of <literal>bundlerApp</literal> over <literal>bundlerEnv</literal> is the executables introduced in the environment are precisely those selected in the <literal>exes</literal> list, as opposed to <literal>bundlerEnv</literal> which adds all the executables made available by gems in the gemset, which can mean e.g. <command>rspec</command> or <command>rake</command> in unpredictable versions available from various packages.
+
+<para>Resulting derivations for both builders also have two helpful attributes, <literal>env</literal> and <literal>wrapper</literal>. The first one allows one to quickly drop into
 <command>nix-shell</command> with the specified environment present. E.g. <command>nix-shell -A sensu.env</command> would give you an environment with Ruby preset
 so it has all the libraries necessary for <literal>sensu</literal> in its paths. The second one can be used to make derivations from custom Ruby scripts which have
 <filename>Gemfile</filename>s with their dependencies specified. It is a derivation with <command>ruby</command> wrapped so it can find all the needed dependencies.
@@ -74,4 +96,3 @@ in stdenv.mkDerivation {
 </programlisting>
 
 </section>
-
diff --git a/lib/maintainers.nix b/lib/maintainers.nix
index 31299006083..31760c94eb0 100644
--- a/lib/maintainers.nix
+++ b/lib/maintainers.nix
@@ -407,6 +407,7 @@
   np = "Nicolas Pouillard <np.nix@nicolaspouillard.fr>";
   nslqqq = "Nikita Mikhailov <nslqqq@gmail.com>";
   nthorne = "Niklas Thörne <notrupertthorne@gmail.com>";
+  nyarly = "Judson Lester <nyarly@gmail.com>";
   obadz = "obadz <obadz-nixos@obadz.com>";
   ocharles = "Oliver Charles <ollie@ocharles.org.uk>";
   odi = "Oliver Dunkl <oliver.dunkl@gmail.com>";
diff --git a/pkgs/development/ruby-modules/bundled-common/default.nix b/pkgs/development/ruby-modules/bundled-common/default.nix
new file mode 100644
index 00000000000..1bf6257f655
--- /dev/null
+++ b/pkgs/development/ruby-modules/bundled-common/default.nix
@@ -0,0 +1,156 @@
+{ stdenv, runCommand, ruby, lib
+, defaultGemConfig, buildRubyGem, buildEnv
+, makeWrapper
+, bundler
+}@defs:
+
+{
+  name ? null
+, pname ? null
+, mainGemName ? null
+, gemdir ? null
+, gemfile ? null
+, lockfile ? null
+, gemset ? null
+, ruby ? defs.ruby
+, gemConfig ? defaultGemConfig
+, postBuild ? null
+, document ? []
+, meta ? {}
+, groups ? ["default"]
+, ignoreCollisions ? false
+, ...
+}@args:
+
+assert name == null -> pname != null;
+
+with  import ./functions.nix { inherit lib gemConfig; };
+
+let
+  gemFiles = bundlerFiles args;
+
+  importedGemset = import gemFiles.gemset;
+
+  filteredGemset = filterGemset { inherit ruby groups; } importedGemset;
+
+  configuredGemset = lib.flip lib.mapAttrs filteredGemset (name: attrs:
+    applyGemConfigs (attrs // { inherit ruby; gemName = name; })
+  );
+
+  hasBundler = builtins.hasAttr "bundler" filteredGemset;
+
+  bundler =
+    if hasBundler then gems.bundler
+    else defs.bundler.override (attrs: { inherit ruby; });
+
+  gems = lib.flip lib.mapAttrs configuredGemset (name: attrs: buildGem name attrs);
+
+  name' = if name != null then
+    name
+  else
+    let
+      gem = gems."${pname}";
+      version = gem.version;
+    in
+      "${pname}-${version}";
+
+  pname' = if pname != null then
+    pname
+  else
+    name;
+
+  copyIfBundledByPath = { bundledByPath ? false, ...}@main:
+  (if bundledByPath then
+      assert gemFiles.gemdir != null; "cp -a ${gemFiles.gemdir}/* $out/"
+    else ""
+  );
+
+  maybeCopyAll = pkgname: if pkgname == null then "" else
+  let
+    mainGem = gems."${pkgname}" or (throw "bundlerEnv: gem ${pkgname} not found");
+  in
+    copyIfBundledByPath mainGem;
+
+  # We have to normalize the Gemfile.lock, otherwise bundler tries to be
+  # helpful by doing so at run time, causing executables to immediately bail
+  # out. Yes, I'm serious.
+  confFiles = runCommand "gemfile-and-lockfile" {} ''
+    mkdir -p $out
+    ${maybeCopyAll mainGemName}
+    cp ${gemFiles.gemfile} $out/Gemfile || ls -l $out/Gemfile
+    cp ${gemFiles.lockfile} $out/Gemfile.lock || ls -l $out/Gemfile.lock
+  '';
+
+  buildGem = name: attrs: (
+    let
+      gemAttrs = composeGemAttrs ruby gems name attrs;
+    in
+    if gemAttrs.type == "path" then
+      pathDerivation gemAttrs
+    else
+      buildRubyGem gemAttrs
+  );
+
+  envPaths = lib.attrValues gems ++ lib.optional (!hasBundler) bundler;
+
+  basicEnv = buildEnv {
+    inherit  ignoreCollisions;
+
+    name = name';
+
+    paths = envPaths;
+    pathsToLink = [ "/lib" ];
+
+    postBuild = genStubsScript (defs // args // {
+      inherit confFiles bundler groups;
+      binPaths = envPaths;
+    }) + lib.optionalString (postBuild != null) postBuild;
+
+    meta = { platforms = ruby.meta.platforms; } // meta;
+
+    passthru = rec {
+      inherit ruby bundler gems mainGem confFiles envPaths;
+
+      wrappedRuby =
+      stdenv.mkDerivation {
+        name = "wrapped-ruby-${pname}";
+        nativeBuildInputs = [ makeWrapper ];
+        buildCommand = ''
+          mkdir -p $out/bin
+          for i in ${ruby}/bin/*; do
+            makeWrapper "$i" $out/bin/$(basename "$i") \
+              --set BUNDLE_GEMFILE ${confFiles}/Gemfile \
+              --set BUNDLE_PATH ${basicEnv}/${ruby.gemPath} \
+              --set BUNDLE_FROZEN 1 \
+              --set GEM_HOME ${basicEnv}/${ruby.gemPath} \
+              --set GEM_PATH ${basicEnv}/${ruby.gemPath}
+          done
+        '';
+      };
+
+      env = let
+        irbrc = builtins.toFile "irbrc" ''
+          if !(ENV["OLD_IRBRC"].nil? || ENV["OLD_IRBRC"].empty?)
+            require ENV["OLD_IRBRC"]
+          end
+          require 'rubygems'
+          require 'bundler/setup'
+        '';
+        in stdenv.mkDerivation {
+          name = "${pname}-interactive-environment";
+          nativeBuildInputs = [ wrappedRuby basicEnv ];
+          shellHook = ''
+            export OLD_IRBRC=$IRBRC
+            export IRBRC=${irbrc}
+          '';
+          buildCommand = ''
+            echo >&2 ""
+            echo >&2 "*** Ruby 'env' attributes are intended for interactive nix-shell sessions, not for building! ***"
+            echo >&2 ""
+            exit 1
+          '';
+        };
+    };
+  };
+in
+  basicEnv
diff --git a/pkgs/development/ruby-modules/bundled-common/functions.nix b/pkgs/development/ruby-modules/bundled-common/functions.nix
new file mode 100644
index 00000000000..b17a4639e77
--- /dev/null
+++ b/pkgs/development/ruby-modules/bundled-common/functions.nix
@@ -0,0 +1,75 @@
+{ lib, gemConfig, ... }:
+rec {
+  bundlerFiles = {
+    gemfile ? null
+  , lockfile ? null
+  , gemset ? null
+  , gemdir ? null
+  , ...
+  }: {
+    inherit gemdir;
+
+    gemfile =
+    if gemfile == null then assert gemdir != null; gemdir + "/Gemfile"
+    else gemfile;
+
+    lockfile =
+    if lockfile == null then assert gemdir != null; gemdir + "/Gemfile.lock"
+    else lockfile;
+
+    gemset =
+    if gemset == null then assert gemdir != null; gemdir + "/gemset.nix"
+    else gemset;
+  };
+
+  filterGemset = {ruby, groups,...}@env: gemset: lib.filterAttrs (name: attrs: platformMatches ruby attrs && groupMatches groups attrs) gemset;
+
+  platformMatches = {rubyEngine, version, ...}@ruby: attrs: (
+  !(attrs ? "platforms") ||
+  builtins.length attrs.platforms == 0 ||
+    builtins.any (platform:
+      platform.engine == rubyEngine &&
+        (!(platform ? "version") || platform.version == version.majMin)
+    ) attrs.platforms
+  );
+
+  groupMatches = groups: attrs: (
+  !(attrs ? "groups") ||
+    builtins.any (gemGroup: builtins.any (group: group == gemGroup) groups) attrs.groups
+  );
+
+  applyGemConfigs = attrs:
+    (if gemConfig ? "${attrs.gemName}"
+    then attrs // gemConfig."${attrs.gemName}" attrs
+    else attrs);
+
+  genStubsScript = { lib, ruby, confFiles, bundler, groups, binPaths, ... }: ''
+      ${ruby}/bin/ruby ${./gen-bin-stubs.rb} \
+        "${ruby}/bin/ruby" \
+        "${confFiles}/Gemfile" \
+        "$out/${ruby.gemPath}" \
+        "${bundler}/${ruby.gemPath}" \
+        ${lib.escapeShellArg binPaths} \
+        ${lib.escapeShellArg groups}
+    '';
+
+  pathDerivation = { gemName, version, path, ...  }:
+    let
+      res = {
+          type = "derivation";
+          bundledByPath = true;
+          name = gemName;
+          version = version;
+          outPath = path;
+          outputs = [ "out" ];
+          out = res;
+          outputName = "out";
+        };
+    in res;
+
+  composeGemAttrs = ruby: gems: name: attrs: ((removeAttrs attrs ["source" "platforms"]) // attrs.source // {
+    inherit ruby;
+    gemName = name;
+    gemPath = map (gemName: gems."${gemName}") (attrs.dependencies or []);
+  });
+}
diff --git a/pkgs/development/ruby-modules/bundler-env/gen-bin-stubs.rb b/pkgs/development/ruby-modules/bundled-common/gen-bin-stubs.rb
index 92321d6427d..92321d6427d 100644
--- a/pkgs/development/ruby-modules/bundler-env/gen-bin-stubs.rb
+++ b/pkgs/development/ruby-modules/bundled-common/gen-bin-stubs.rb
diff --git a/pkgs/development/ruby-modules/bundled-common/test.nix b/pkgs/development/ruby-modules/bundled-common/test.nix
new file mode 100644
index 00000000000..ee3754595f3
--- /dev/null
+++ b/pkgs/development/ruby-modules/bundled-common/test.nix
@@ -0,0 +1,50 @@
+{ stdenv, writeText, lib, ruby, defaultGemConfig, callPackage, test, stubs, should }@defs:
+let
+  testConfigs = {
+    inherit lib;
+    gemConfig =  defaultGemConfig;
+  };
+  functions = (import ./functions.nix testConfigs);
+in
+  builtins.concatLists [
+    ( test.run "All set, no gemdir" (functions.bundlerFiles {
+      gemfile  = test/Gemfile;
+      lockfile = test/Gemfile.lock;
+      gemset   = test/gemset.nix;
+    }) {
+      gemfile  = should.equal test/Gemfile;
+      lockfile = should.equal test/Gemfile.lock;
+      gemset   = should.equal test/gemset.nix;
+    })
+
+    ( test.run "Just gemdir" (functions.bundlerFiles {
+      gemdir = test/.;
+    }) {
+      gemfile  = should.equal test/Gemfile;
+      lockfile = should.equal test/Gemfile.lock;
+      gemset   = should.equal test/gemset.nix;
+    })
+
+    ( test.run "Gemset and dir" (functions.bundlerFiles {
+      gemdir = test/.;
+      gemset = test/extraGemset.nix;
+    }) {
+      gemfile  = should.equal test/Gemfile;
+      lockfile = should.equal test/Gemfile.lock;
+      gemset   = should.equal test/extraGemset.nix;
+    })
+
+    ( test.run "Filter empty gemset" {} (set: functions.filterGemset {inherit ruby; groups = ["default"]; } set == {}))
+    ( let gemSet = { test = { groups = ["x" "y"]; }; };
+      in
+      test.run "Filter matches a group" gemSet (set: functions.filterGemset {inherit ruby; groups = ["y" "z"];} set == gemSet))
+    ( let gemSet = { test = { platforms = []; }; };
+      in
+      test.run "Filter matches empty platforms list" gemSet (set: functions.filterGemset {inherit ruby; groups = [];} set == gemSet))
+    ( let gemSet = { test = { platforms = [{engine = ruby.rubyEngine; version = ruby.version.majMin;}]; }; };
+      in
+      test.run "Filter matches on platform" gemSet (set: functions.filterGemset {inherit ruby; groups = [];} set == gemSet))
+    ( let gemSet = { test = { groups = ["x" "y"]; }; };
+      in
+      test.run "Filter excludes based on groups" gemSet (set: functions.filterGemset {inherit ruby; groups = ["a" "b"];} set == {}))
+  ]
diff --git a/pkgs/development/ruby-modules/bundler-app/default.nix b/pkgs/development/ruby-modules/bundler-app/default.nix
new file mode 100644
index 00000000000..99d1dd64dc4
--- /dev/null
+++ b/pkgs/development/ruby-modules/bundler-app/default.nix
@@ -0,0 +1,48 @@
+{ lib, stdenv, callPackage, runCommand, ruby }@defs:
+
+# Use for simple installation of Ruby tools shipped in a Gem.
+# Start with a Gemfile that includes `gem <toolgem>`
+# > nix-shell -p bundler bundix
+# (shell)> bundle lock
+# (shell)> bundix
+# Then use rubyTool in the default.nix:
+
+# rubyTool { pname = "gemifiedTool"; gemdir = ./.; exes = ["gemified-tool"]; }
+# The 'exes' parameter ensures that a copy of e.g. rake doesn't polute the system.
+{
+  # use the name of the name in question; its version will be picked up from the gemset
+  pname
+  # gemdir is the location of the Gemfile{,.lock} and gemset.nix; usually ./.
+, gemdir
+  # Exes is the list of executables provided by the gems in the Gemfile
+, exes ? []
+  # Scripts are ruby programs depend on gems in the Gemfile (e.g. scripts/rails)
+, scripts ? []
+, ruby ? defs.ruby
+, gemfile ? null
+, lockfile ? null
+, gemset ? null
+, preferLocalBuild ? false
+, allowSubstitutes ? false
+, meta ? {}
+, postBuild ? ""
+}@args:
+
+let
+  basicEnv = (callPackage ../bundled-common {}) args;
+
+  cmdArgs = removeAttrs args [ "pname" "postBuild" ]
+  // { inherit preferLocalBuild allowSubstitutes; }; # pass the defaults
+in
+   runCommand basicEnv.name cmdArgs ''
+    mkdir -p $out/bin;
+      ${(lib.concatMapStrings (x: "ln -s '${basicEnv}/bin/${x}' $out/bin/${x};\n") exes)}
+      ${(lib.concatMapStrings (s: "makeWrapper $out/bin/$(basename ${s}) $srcdir/${s} " +
+              "--set BUNDLE_GEMFILE ${basicEnv.confFiles}/Gemfile "+
+              "--set BUNDLE_PATH ${basicEnv}/${ruby.gemPath} "+
+              "--set BUNDLE_FROZEN 1 "+
+              "--set GEM_HOME ${basicEnv}/${ruby.gemPath} "+
+              "--set GEM_PATH ${basicEnv}/${ruby.gemPath} "+
+              "--run \"cd $srcdir\";\n") scripts)}
+    ${postBuild}
+  ''
diff --git a/pkgs/development/ruby-modules/bundler-env/default.nix b/pkgs/development/ruby-modules/bundler-env/default.nix
index 57ca23d4143..2e2653621a7 100644
--- a/pkgs/development/ruby-modules/bundler-env/default.nix
+++ b/pkgs/development/ruby-modules/bundler-env/default.nix
@@ -1,9 +1,6 @@
 { stdenv, runCommand, writeText, writeScript, writeScriptBin, ruby, lib
 , callPackage, defaultGemConfig, fetchurl, fetchgit, buildRubyGem, buildEnv
-, git
-, makeWrapper
-, bundler
-, tree
+, linkFarm, git, makeWrapper, bundler, tree
 }@defs:
 
 { name ? null
@@ -12,143 +9,54 @@
 , gemfile ? null
 , lockfile ? null
 , gemset ? null
+, groups ? ["default"]
 , ruby ? defs.ruby
 , gemConfig ? defaultGemConfig
 , postBuild ? null
 , document ? []
 , meta ? {}
-, groups ? ["default"]
 , ignoreCollisions ? false
 , ...
 }@args:
 
 let
-  drvName =
-    if name != null then name
-    else if pname != null then "${toString pname}-${mainGem.version}"
-    else throw "bundlerEnv: either pname or name must be set";
-
-  mainGem =
-    if pname == null then null
-    else gems."${pname}" or (throw "bundlerEnv: gem ${pname} not found");
-
-  gemfile' =
-    if gemfile == null then gemdir + "/Gemfile"
-    else gemfile;
-
-  lockfile' =
-    if lockfile == null then gemdir + "/Gemfile.lock"
-    else lockfile;
-
-  gemset' =
-    if gemset == null then gemdir + "/gemset.nix"
-    else gemset;
-
-  importedGemset = import gemset';
-
-  filteredGemset = (lib.filterAttrs (name: attrs:
-    if (builtins.hasAttr "groups" attrs)
-    then (builtins.any (gemGroup: builtins.any (group: group == gemGroup) groups) attrs.groups)
-    else true
-  ) importedGemset);
-
-  applyGemConfigs = attrs:
-    (if gemConfig ? "${attrs.gemName}"
-    then attrs // gemConfig."${attrs.gemName}" attrs
-    else attrs);
-
-  configuredGemset = lib.flip lib.mapAttrs filteredGemset (name: attrs:
-    applyGemConfigs (attrs // { inherit ruby; gemName = name; })
-  );
-
-  hasBundler = builtins.hasAttr "bundler" filteredGemset;
+  inherit (import ../bundled-common/functions.nix {inherit lib ruby gemConfig groups; }) genStubsScript;
 
-  bundler =
-    if hasBundler then gems.bundler
-    else defs.bundler.override (attrs: { inherit ruby; });
+  basicEnv = (callPackage ../bundled-common {}) (args // { inherit pname name; mainGemName = pname; });
 
-  gems = lib.flip lib.mapAttrs configuredGemset (name: attrs:
-    buildRubyGem ((removeAttrs attrs ["source"]) // attrs.source // {
-      inherit ruby;
-      gemName = name;
-      gemPath = map (gemName: gems."${gemName}") (attrs.dependencies or []);
-    }));
+  inherit (basicEnv) envPaths;
+  # Idea here is a mkDerivation that gen-bin-stubs new stubs "as specified" -
+  # either specific executables or the bin/ for certain gem(s), but
+  # incorporates the basicEnv as a requirement so that its $out is in our path.
 
-  # We have to normalize the Gemfile.lock, otherwise bundler tries to be
-  # helpful by doing so at run time, causing executables to immediately bail
-  # out. Yes, I'm serious.
-  confFiles = runCommand "gemfile-and-lockfile" {} ''
-    mkdir -p $out
-    cp ${gemfile'} $out/Gemfile
-    cp ${lockfile'} $out/Gemfile.lock
-  '';
+  # When stubbing the bins for a gem, we should use the gem expression
+  # directly, which means that basicEnv should somehow make it available.
 
-  envPaths = lib.attrValues gems ++ lib.optional (!hasBundler) bundler;
+  # Different use cases should use different variations on this file, rather
+  # than the expression trying to deduce a use case.
 
-  binPaths = if mainGem != null then [ mainGem ] else envPaths;
-
-  bundlerEnv = buildEnv {
-    inherit ignoreCollisions;
-
-    name = drvName;
-
-    paths = envPaths;
-    pathsToLink = [ "/lib" ];
-
-    postBuild = ''
-      ${ruby}/bin/ruby ${./gen-bin-stubs.rb} \
-        "${ruby}/bin/ruby" \
-        "${confFiles}/Gemfile" \
-        "$out/${ruby.gemPath}" \
-        "${bundler}/${ruby.gemPath}" \
-        ${lib.escapeShellArg binPaths} \
-        ${lib.escapeShellArg groups}
-    '' + lib.optionalString (postBuild != null) postBuild;
-
-    meta = { platforms = ruby.meta.platforms; } // meta;
-
-    passthru = rec {
-      inherit ruby bundler gems;
-
-      wrappedRuby = stdenv.mkDerivation {
-        name = "wrapped-ruby-${drvName}";
-        nativeBuildInputs = [ makeWrapper ];
-        buildCommand = ''
-          mkdir -p $out/bin
-          for i in ${ruby}/bin/*; do
-            makeWrapper "$i" $out/bin/$(basename "$i") \
-              --set BUNDLE_GEMFILE ${confFiles}/Gemfile \
-              --set BUNDLE_PATH ${bundlerEnv}/${ruby.gemPath} \
-              --set BUNDLE_FROZEN 1 \
-              --set GEM_HOME ${bundlerEnv}/${ruby.gemPath} \
-              --set GEM_PATH ${bundlerEnv}/${ruby.gemPath}
-          done
-        '';
-      };
-
-      env = let
-        irbrc = builtins.toFile "irbrc" ''
-          if !(ENV["OLD_IRBRC"].nil? || ENV["OLD_IRBRC"].empty?)
-            require ENV["OLD_IRBRC"]
-          end
-          require 'rubygems'
-          require 'bundler/setup'
-        '';
-        in stdenv.mkDerivation {
-          name = "interactive-${drvName}-environment";
-          nativeBuildInputs = [ wrappedRuby bundlerEnv ];
-          shellHook = ''
-            export OLD_IRBRC="$IRBRC"
-            export IRBRC=${irbrc}
-          '';
-          buildCommand = ''
-            echo >&2 ""
-            echo >&2 "*** Ruby 'env' attributes are intended for interactive nix-shell sessions, not for building! ***"
-            echo >&2 ""
-            exit 1
-          '';
-        };
-    };
-  };
+  # The basicEnv should be put into passthru so that e.g. nix-shell can use it.
 in
-  bundlerEnv
+  if pname == null then
+    basicEnv // { inherit name basicEnv; }
+  else
+    (buildEnv {
+      inherit ignoreCollisions;
+
+      name = basicEnv.name;
+
+      paths = envPaths;
+      pathsToLink = [ "/lib" ];
+
+      postBuild = genStubsScript {
+        inherit lib ruby bundler groups;
+        confFiles = basicEnv.confFiles;
+        binPaths = [ basicEnv.gems."${pname}" ];
+      } + lib.optionalString (postBuild != null) postBuild;
+
+      meta = { platforms = ruby.meta.platforms; } // meta;
+      passthru = basicEnv.passthru // {
+        inherit basicEnv;
+        inherit (basicEnv) env;
+      };
+    })
diff --git a/pkgs/development/ruby-modules/bundler-env/test.nix b/pkgs/development/ruby-modules/bundler-env/test.nix
new file mode 100644
index 00000000000..63da7044c0c
--- /dev/null
+++ b/pkgs/development/ruby-modules/bundler-env/test.nix
@@ -0,0 +1,33 @@
+{ stdenv, writeText, lib, ruby, defaultGemConfig, callPackage, test, stubs, should}@defs:
+let
+  bundlerEnv = callPackage ./default.nix stubs // {
+    basicEnv = callPackage ../bundled-common stubs;
+  };
+
+  justName = bundlerEnv {
+    name = "test-0.1.2";
+    gemset = ./test/gemset.nix;
+  };
+
+  pnamed = bundlerEnv {
+    pname = "test";
+    gemdir = ./test;
+    gemset = ./test/gemset.nix;
+    gemfile = ./test/Gemfile;
+    lockfile = ./test/Gemfile.lock;
+  };
+in
+  builtins.concatLists [
+    (test.run "bundlerEnv { name }" justName {
+      name = should.equal "test-0.1.2";
+    })
+    (test.run "bundlerEnv { pname }" pnamed
+    [
+      (should.haveKeys [ "name" "env" "postBuild" ])
+      {
+        name = should.equal "test-0.1.2";
+        env = should.beASet;
+        postBuild = should.havePrefix "/nix/store";
+      }
+    ])
+  ]
diff --git a/pkgs/development/ruby-modules/bundler-env/test/Gemfile b/pkgs/development/ruby-modules/bundler-env/test/Gemfile
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/pkgs/development/ruby-modules/bundler-env/test/Gemfile
diff --git a/pkgs/development/ruby-modules/bundler-env/test/Gemfile.lock b/pkgs/development/ruby-modules/bundler-env/test/Gemfile.lock
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/pkgs/development/ruby-modules/bundler-env/test/Gemfile.lock
diff --git a/pkgs/development/ruby-modules/bundler-env/test/gemset.nix b/pkgs/development/ruby-modules/bundler-env/test/gemset.nix
new file mode 100644
index 00000000000..53f15f96bc6
--- /dev/null
+++ b/pkgs/development/ruby-modules/bundler-env/test/gemset.nix
@@ -0,0 +1,10 @@
+{
+  test = {
+    source = {
+      remotes = ["https://rubygems.org"];
+      sha256 = "1j5r0anj8m4qlf2psnldip4b8ha2bsscv11lpdgnfh4nnchzjnxw";
+      type = "gem";
+    };
+    version = "0.1.2";
+  };
+}
diff --git a/pkgs/development/ruby-modules/gem/default.nix b/pkgs/development/ruby-modules/gem/default.nix
index ade6659c400..62a9d60686f 100644
--- a/pkgs/development/ruby-modules/gem/default.nix
+++ b/pkgs/development/ruby-modules/gem/default.nix
@@ -87,6 +87,7 @@ stdenv.mkDerivation (attrs // {
     ++ lib.optional stdenv.isDarwin darwin.libobjc
     ++ buildInputs;
 
+  #name = builtins.trace (attrs.name or "no attr.name" ) "${namePrefix}${gemName}-${version}";
   name = attrs.name or "${namePrefix}${gemName}-${version}";
 
   inherit src;
diff --git a/pkgs/development/ruby-modules/runtests.sh b/pkgs/development/ruby-modules/runtests.sh
new file mode 100755
index 00000000000..8bb8c8a5462
--- /dev/null
+++ b/pkgs/development/ruby-modules/runtests.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+set -o xtrace
+cd $(dirname $0)
+find . -name text.nix
+testfiles=$(find . -name test.nix)
+nix-build -E "with import <nixpkgs> {}; callPackage testing/driver.nix { testFiles = [ $testfiles ]; }" --show-trace && cat result
diff --git a/pkgs/development/ruby-modules/testing/assertions.nix b/pkgs/development/ruby-modules/testing/assertions.nix
new file mode 100644
index 00000000000..f28cfcd508d
--- /dev/null
+++ b/pkgs/development/ruby-modules/testing/assertions.nix
@@ -0,0 +1,28 @@
+{ test, lib, ...}:
+{
+  equal = expected: actual:
+    if actual == expected then
+      (test.passed "= ${toString expected}") else
+      (test.failed (
+      "expected '${toString expected}'(${builtins.typeOf expected})"
+      + " !=  "+
+      "actual '${toString actual}'(${builtins.typeOf actual})"
+      ));
+
+  beASet = actual:
+    if builtins.isAttrs actual then
+      (test.passed "is a set") else
+      (test.failed "is not a set, was ${builtins.typeOf actual}: ${toString actual}");
+
+  haveKeys = expected: actual:
+    if builtins.all
+    (ex: builtins.any (ac: ex == ac) (builtins.attrNames actual))
+    expected then
+      (test.passed "has expected keys") else
+      (test.failed "keys differ: expected: [${lib.concatStringsSep ";" expected}] actual: [${lib.concatStringsSep ";" (builtins.attrNames actual)}]");
+
+  havePrefix = expected: actual:
+    if lib.hasPrefix expected actual then
+      (test.passed "has prefix '${expected}'") else
+      (test.failed "prefix '${expected}' not found in '${actual}'");
+}
diff --git a/pkgs/development/ruby-modules/testing/driver.nix b/pkgs/development/ruby-modules/testing/driver.nix
new file mode 100644
index 00000000000..65e7c8d4416
--- /dev/null
+++ b/pkgs/development/ruby-modules/testing/driver.nix
@@ -0,0 +1,20 @@
+/*
+Run with:
+nix-build -E 'with import <nixpkgs> { }; callPackage ./test.nix {}' --show-trace; and cat result
+
+Confusingly, the ideal result ends with something like:
+error: build of ‘/nix/store/3245f3dcl2wxjs4rci7n069zjlz8qg85-test-results.tap.drv’ failed
+*/
+{ writeText, lib, callPackage, testFiles, stdenv, ruby }@defs:
+let
+  testTools = rec {
+    test = import ./testing.nix;
+    stubs = import ./stubs.nix defs;
+    should = import ./assertions.nix { inherit test lib; };
+  };
+
+  tap = import ./tap-support.nix;
+
+  results = builtins.concatLists (map (file: callPackage file testTools) testFiles);
+in
+  writeText "test-results.tap" (tap.output results)
diff --git a/pkgs/development/ruby-modules/testing/stubs.nix b/pkgs/development/ruby-modules/testing/stubs.nix
new file mode 100644
index 00000000000..3585681478c
--- /dev/null
+++ b/pkgs/development/ruby-modules/testing/stubs.nix
@@ -0,0 +1,33 @@
+{ stdenv, lib, ruby, callPackage, ... }:
+let
+  real = {
+    inherit (stdenv) mkDerivation;
+  };
+  mkDerivation = {name, ...}@argSet:
+  derivation {
+    inherit name;
+    text = (builtins.toJSON (lib.filterAttrs ( n: v: builtins.any (x: x == n) ["name" "system"]) argSet));
+    builder = stdenv.shell;
+    args = [ "-c" "echo  $(<$textPath) > $out"];
+    system = stdenv.system;
+    passAsFile = ["text"];
+  };
+  fetchurl = {url?"", urls ? [],...}: "fetchurl:${if urls == [] then url else builtins.head urls}";
+
+  stdenv' = stdenv // {
+    inherit mkDerivation;
+    stubbed = true;
+  };
+  ruby' = ruby // {
+    stdenv = stdenv';
+    stubbed = true;
+  };
+in
+  {
+    ruby = ruby';
+    buildRubyGem = callPackage ../gem {
+      inherit fetchurl;
+      ruby = ruby';
+    };
+    stdenv = stdenv';
+  }
diff --git a/pkgs/development/ruby-modules/testing/tap-support.nix b/pkgs/development/ruby-modules/testing/tap-support.nix
new file mode 100644
index 00000000000..74fcceebaa0
--- /dev/null
+++ b/pkgs/development/ruby-modules/testing/tap-support.nix
@@ -0,0 +1,21 @@
+with builtins;
+let
+  withIndexes = list: genList (idx: (elemAt list idx) // {index = idx;}) (length list);
+
+  testLine = report: "${okStr report} ${toString (report.index + 1)} ${report.description}" + testDirective report + testYaml report;
+
+  # These are part of the TAP spec, not yet implemented.
+  #c.f.  https://github.com/NixOS/nixpkgs/issues/27071
+  testDirective = report: "";
+  testYaml = report: "";
+
+  okStr = { result, ...}: if result == "pass" then "ok" else "not ok";
+in
+  {
+    output = reports: ''
+      TAP version 13
+      1..${toString (length reports)}'' + (foldl' (l: r: l + "\n" + r) "" (map testLine (withIndexes reports))) + ''
+
+      # Finished at ${toString currentTime}
+      '';
+  }
diff --git a/pkgs/development/ruby-modules/testing/testing.nix b/pkgs/development/ruby-modules/testing/testing.nix
new file mode 100644
index 00000000000..43d10fca044
--- /dev/null
+++ b/pkgs/development/ruby-modules/testing/testing.nix
@@ -0,0 +1,62 @@
+with builtins;
+let
+  /*
+  underTest = {
+    x = {
+      a = 1;
+      b = "2";
+    };
+  };
+
+  tests = [
+    (root: false)
+    {
+      x = [
+        (set: true)
+        {
+          a = (a: a > 1);
+          b = (b: b == "3");
+        }
+      ];
+    }
+  ];
+
+  results = run "Examples" underTest tests;
+  */
+
+  passed = desc: {
+    result = "pass";
+    description = desc;
+  };
+
+  failed = desc: {
+    result = "failed";
+    description = desc;
+  };
+
+  prefixName = name: res: {
+    inherit (res) result;
+    description = "${name}: ${res.description}";
+  };
+
+  run = name: under: tests: if isList tests then
+    (concatLists (map (run name under) tests))
+  else if isAttrs tests then
+    (concatLists (map (
+    subName: run (name + "." + subName) (if hasAttr subName under then getAttr subName under else "<MISSING!>") (getAttr subName tests)
+    ) (attrNames tests)))
+  else if isFunction tests then
+    let
+      res = tests under;
+    in
+      if isBool res then
+        [
+          (prefixName name (if tests under then passed "passed" else failed "failed"))
+        ]
+      else
+        [ (prefixName name res) ]
+  else [
+    failed (name ": not a function, list or set")
+  ];
+in
+  { inherit run passed failed; }
diff --git a/pkgs/development/tools/corundum/Gemfile b/pkgs/development/tools/corundum/Gemfile
new file mode 100644
index 00000000000..5f817ae498a
--- /dev/null
+++ b/pkgs/development/tools/corundum/Gemfile
@@ -0,0 +1,3 @@
+source "https://rubygems.org"
+
+gem "corundum", "=0.6.2"
diff --git a/pkgs/development/tools/corundum/Gemfile.lock b/pkgs/development/tools/corundum/Gemfile.lock
new file mode 100644
index 00000000000..40ad1948394
--- /dev/null
+++ b/pkgs/development/tools/corundum/Gemfile.lock
@@ -0,0 +1,56 @@
+GEM
+  remote: https://rubygems.org/
+  specs:
+    calibrate (0.0.1)
+    caliph (0.3.1)
+    corundum (0.6.2)
+      bundler (~> 1.10)
+      caliph (~> 0.3)
+      mattock (~> 0.9)
+      paint (~> 0.8)
+      rspec (>= 2.0, < 4)
+      simplecov (>= 0.5)
+      simplecov-json (~> 0.2)
+    diff-lcs (1.3)
+    docile (1.1.5)
+    json (2.1.0)
+    mattock (0.10.1)
+      calibrate (~> 0.0.1)
+      caliph (~> 0.3)
+      rake (~> 10.0)
+      tilt (> 0)
+      valise (~> 1.1)
+    paint (0.9.0)
+    rake (10.5.0)
+    rspec (3.6.0)
+      rspec-core (~> 3.6.0)
+      rspec-expectations (~> 3.6.0)
+      rspec-mocks (~> 3.6.0)
+    rspec-core (3.6.0)
+      rspec-support (~> 3.6.0)
+    rspec-expectations (3.6.0)
+      diff-lcs (>= 1.2.0, < 2.0)
+      rspec-support (~> 3.6.0)
+    rspec-mocks (3.6.0)
+      diff-lcs (>= 1.2.0, < 2.0)
+      rspec-support (~> 3.6.0)
+    rspec-support (3.6.0)
+    simplecov (0.14.1)
+      docile (~> 1.1.0)
+      json (>= 1.8, < 3)
+      simplecov-html (~> 0.10.0)
+    simplecov-html (0.10.1)
+    simplecov-json (0.2)
+      json
+      simplecov
+    tilt (2.0.7)
+    valise (1.2.1)
+
+PLATFORMS
+  ruby
+
+DEPENDENCIES
+  corundum (= 0.6.2)
+
+BUNDLED WITH
+   1.14.4
diff --git a/pkgs/development/tools/corundum/default.nix b/pkgs/development/tools/corundum/default.nix
new file mode 100644
index 00000000000..22d7b236ffa
--- /dev/null
+++ b/pkgs/development/tools/corundum/default.nix
@@ -0,0 +1,15 @@
+{ lib, bundlerApp }:
+
+bundlerApp {
+  pname = "corundum";
+  gemdir = ./.;
+  exes = [ "corundum-skel" ];
+
+  meta = with lib; {
+    description = "Tool and libraries for maintaining Ruby gems.";
+    homepage    = https://github.com/nyarly/corundum;
+    license     = licenses.mit;
+    maintainers = [ maintainers.nyarly ];
+    platforms   = platforms.unix;
+  };
+}
diff --git a/pkgs/development/tools/corundum/gemset.nix b/pkgs/development/tools/corundum/gemset.nix
new file mode 100644
index 00000000000..e395e098e6d
--- /dev/null
+++ b/pkgs/development/tools/corundum/gemset.nix
@@ -0,0 +1,154 @@
+{
+  calibrate = {
+    source = {
+      remotes = ["https://rubygems.org"];
+      sha256 = "17kmlss7db70pjwdbbhag7mnixh8wasdq6n1v8663x50z9c7n2ng";
+      type = "gem";
+    };
+    version = "0.0.1";
+  };
+  caliph = {
+    source = {
+      remotes = ["https://rubygems.org"];
+      sha256 = "08d07n4m4yh1h9icq6n9dkw4jwgdmgd638f15mxr2pvqp4wycsnr";
+      type = "gem";
+    };
+    version = "0.3.1";
+  };
+  corundum = {
+    source = {
+      remotes = ["https://rubygems.org"];
+      sha256 = "1y6shjrqaqyh14a1r4ic660g6jnq4abdrx9imglyalzyrlrwbsxq";
+      type = "gem";
+    };
+    version = "0.6.2";
+  };
+  diff-lcs = {
+    source = {
+      remotes = ["https://rubygems.org"];
+      sha256 = "18w22bjz424gzafv6nzv98h0aqkwz3d9xhm7cbr1wfbyas8zayza";
+      type = "gem";
+    };
+    version = "1.3";
+  };
+  docile = {
+    source = {
+      remotes = ["https://rubygems.org"];
+      sha256 = "0m8j31whq7bm5ljgmsrlfkiqvacrw6iz9wq10r3gwrv5785y8gjx";
+      type = "gem";
+    };
+    version = "1.1.5";
+  };
+  json = {
+    source = {
+      remotes = ["https://rubygems.org"];
+      sha256 = "01v6jjpvh3gnq6sgllpfqahlgxzj50ailwhj9b3cd20hi2dx0vxp";
+      type = "gem";
+    };
+    version = "2.1.0";
+  };
+  mattock = {
+    source = {
+      remotes = ["https://rubygems.org"];
+      sha256 = "02d6igwr4sfj4jnky8d5h0rm2cc665k1bqz7sj4khzvr18nk3ai6";
+      type = "gem";
+    };
+    version = "0.10.1";
+  };
+  paint = {
+    source = {
+      remotes = ["https://rubygems.org"];
+      sha256 = "1fcn7cfrhbl4nl95fmcd67q33h7bl3iafsafs6w9yj4nqzagz1yc";
+      type = "gem";
+    };
+    version = "0.9.0";
+  };
+  rake = {
+    source = {
+      remotes = ["https://rubygems.org"];
+      sha256 = "0jcabbgnjc788chx31sihc5pgbqnlc1c75wakmqlbjdm8jns2m9b";
+      type = "gem";
+    };
+    version = "10.5.0";
+  };
+  rspec = {
+    source = {
+      remotes = ["https://rubygems.org"];
+      sha256 = "1nd50hycab2a2vdah9lxi585g8f63jxjvmzmxqyln51grxwx9hzb";
+      type = "gem";
+    };
+    version = "3.6.0";
+  };
+  rspec-core = {
+    source = {
+      remotes = ["https://rubygems.org"];
+      sha256 = "18np8wyw2g79waclpaacba6nd7x60ixg07ncya0j0qj1z9b37grd";
+      type = "gem";
+    };
+    version = "3.6.0";
+  };
+  rspec-expectations = {
+    source = {
+      remotes = ["https://rubygems.org"];
+      sha256 = "028ifzf9mqp3kxx40q1nbwj40g72g9zk0wr78l146phblkv96w0a";
+      type = "gem";
+    };
+    version = "3.6.0";
+  };
+  rspec-mocks = {
+    source = {
+      remotes = ["https://rubygems.org"];
+      sha256 = "0nv6jkxy24sag1i9w9wi3850k6skk2fm6yhcrgnmlz6vmwxvizp8";
+      type = "gem";
+    };
+    version = "3.6.0";
+  };
+  rspec-support = {
+    source = {
+      remotes = ["https://rubygems.org"];
+      sha256 = "050paqqpsml8w88nf4a15zbbj3vvm471zpv73sjfdnz7w21wnypb";
+      type = "gem";
+    };
+    version = "3.6.0";
+  };
+  simplecov = {
+    source = {
+      remotes = ["https://rubygems.org"];
+      sha256 = "1r9fnsnsqj432cmrpafryn8nif3x0qg9mdnvrcf0wr01prkdlnww";
+      type = "gem";
+    };
+    version = "0.14.1";
+  };
+  simplecov-html = {
+    source = {
+      remotes = ["https://rubygems.org"];
+      sha256 = "0f3psphismgp6jp1fxxz09zbswh7m2xxxr6gqlzdh7sgv415clvm";
+      type = "gem";
+    };
+    version = "0.10.1";
+  };
+  simplecov-json = {
+    source = {
+      remotes = ["https://rubygems.org"];
+      sha256 = "0x9hr08pkj5d14nfzsn5h8b7ayl6q0xir45dcx5rv2a7g10kzlpp";
+      type = "gem";
+    };
+    version = "0.2";
+  };
+  tilt = {
+    source = {
+      remotes = ["https://rubygems.org"];
+      sha256 = "1is1ayw5049z8pd7slsk870bddyy5g2imp4z78lnvl8qsl8l0s7b";
+      type = "gem";
+    };
+    version = "2.0.7";
+  };
+  valise = {
+    source = {
+      remotes = ["https://rubygems.org"];
+      sha256 = "1arsbmk2gifrhv244qrld7s3202xrnxy6vlc5gqklg70dpsinbn5";
+      type = "gem";
+    };
+    version = "1.2.1";
+  };
+}
\ No newline at end of file
diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix
index 9cbb0b15873..d3c012b37a8 100644
--- a/pkgs/top-level/all-packages.nix
+++ b/pkgs/top-level/all-packages.nix
@@ -6412,6 +6412,7 @@ with pkgs;
   bundix = callPackage ../development/ruby-modules/bundix { };
   bundler = callPackage ../development/ruby-modules/bundler { };
   bundlerEnv = callPackage ../development/ruby-modules/bundler-env { };
+  bundlerApp = callPackage ../development/ruby-modules/bundler-app { };
 
   inherit (callPackage ../development/interpreters/ruby {})
     ruby_2_0_0
@@ -6727,6 +6728,8 @@ with pkgs;
 
   cookiecutter = pythonPackages.cookiecutter;
 
+  corundum = callPackage ../development/tools/corundum { };
+
   ctags = callPackage ../development/tools/misc/ctags { };
 
   ctagsWrapped = callPackage ../development/tools/misc/ctags/wrapped.nix {};