Commit 2b57cb91 authored by Charles Strahan's avatar Charles Strahan Committed by GitHub
Browse files

Merge pull request #25980 from nyarly/bundlerenv_usecases

BundlerEnv, now with groups and paths
parents e8d8633f 5142e8f2
Loading
Loading
Loading
Loading
+23 −2
Original line number Diff line number Diff line
@@ -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>
+1 −0
Original line number Diff line number Diff line
@@ -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>";
+156 −0
Original line number Diff line number Diff line
{ 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
+75 −0
Original line number Diff line number Diff line
{ 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 []);
  });
}
Loading