nixpkgs/pkgs/development/interpreters/ruby/bundler-env/default.nix
2015-03-18 14:15:50 +01:00

340 lines
9.3 KiB
Nix

{ stdenv, runCommand, writeText, writeScript, writeScriptBin, ruby, lib
, callPackage, defaultGemConfig, fetchurl, fetchgit, buildRubyGem , bundler_HEAD
, git
}@defs:
# This is a work-in-progress.
# The idea is that his will replace load-ruby-env.nix.
{ name, gemset, gemfile, lockfile, ruby ? defs.ruby, gemConfig ? defaultGemConfig
, enableParallelBuilding ? false # TODO: this might not work, given the env-var shinanigans.
, postInstall ? null
, documentation ? false
, meta ? {}
, ...
}@args:
let
shellEscape = x: "'${lib.replaceChars ["'"] [("'\\'" + "'")] x}'";
const = x: y: x;
bundler = bundler_HEAD.override { inherit ruby; };
inherit (builtins) attrValues;
gemName = attrs: "${attrs.name}-${attrs.version}.gem";
fetchers.path = attrs: attrs.source.path;
fetchers.gem = attrs: fetchurl {
url = "${attrs.source.source or "https://rubygems.org"}/downloads/${gemName attrs}";
inherit (attrs.source) sha256;
};
fetchers.git = attrs: fetchgit {
inherit (attrs.source) url rev sha256 fetchSubmodules;
leaveDotGit = true;
};
applySrc = attrs:
attrs // {
src = (fetchers."${attrs.source.type}" attrs);
};
applyGemConfigs = attrs:
if gemConfig ? "${attrs.name}"
then attrs // gemConfig."${attrs.name}" attrs
else attrs;
needsPatch = attrs:
(attrs ? patches) || (attrs ? prePatch) || (attrs ? postPatch);
# patch a gem or source tree.
# for gems, the gem is unpacked, patched, and then repacked.
# see: https://github.com/fedora-ruby/gem-patch/blob/master/lib/rubygems/patcher.rb
applyPatches = attrs:
if !needsPatch attrs
then attrs
else attrs // { src =
stdenv.mkDerivation {
name = gemName attrs;
phases = [ "unpackPhase" "patchPhase" "installPhase" ];
buildInputs = [ ruby ] ++ attrs.buildInputs or [];
patches = attrs.patches or [ ];
prePatch = attrs.prePatch or "true";
postPatch = attrs.postPatch or "true";
unpackPhase = ''
runHook preUnpack
if [[ -f ${attrs.src} ]]; then
isGem=1
# we won't know the name of the directory that RubyGems creates,
# so we'll just use a glob to find it and move it over.
gem unpack ${attrs.src} --target=container
cp -r container/* contents
rm -r container
else
cp -r ${attrs.src} contents
chmod -R +w contents
fi
cd contents
runHook postUnpack
'';
installPhase = ''
runHook preInstall
if [[ -n "$isGem" ]]; then
${writeScript "repack.rb" ''
#!${ruby}/bin/ruby
require 'rubygems'
require 'rubygems/package'
require 'fileutils'
if defined?(Encoding.default_internal)
Encoding.default_internal = Encoding::UTF_8
Encoding.default_external = Encoding::UTF_8
end
if Gem::VERSION < '2.0'
load "${./package-1.8.rb}"
end
out = ENV['out']
files = Dir['**/{.[^\.]*,*}']
package = Gem::Package.new("${attrs.src}")
patched_package = Gem::Package.new(package.spec.file_name)
patched_package.spec = package.spec.clone
patched_package.spec.files = files
patched_package.build(false)
FileUtils.cp(patched_package.spec.file_name, out)
''}
else
cp -r . $out
fi
runHook postInstall
'';
};
};
instantiate = (attrs:
applyPatches (applyGemConfigs (applySrc attrs))
);
instantiated = lib.flip lib.mapAttrs (import gemset) (name: attrs:
instantiate (attrs // { inherit name; })
);
needsPreInstall = attrs:
(attrs ? preInstall) || (attrs ? buildInputs) || (attrs ? nativeBuildInputs);
# TODO: support cross compilation? look at stdenv/generic/default.nix.
runPreInstallers = lib.fold (next: acc:
if !needsPreInstall next
then acc
else acc + ''
${writeScript "${next.name}-pre-install" ''
#!${stdenv.shell}
export nativeBuildInputs="${toString ((next.nativeBuildInputs or []) ++ (next.buildInputs or []))}"
source ${stdenv}/setup
header "running pre-install script for ${next.name}"
${next.preInstall or ""}
${ruby}/bin/ruby -e 'print ENV.inspect' > env/${next.name}
stopNest
''}
''
) "" (attrValues instantiated);
# copy *.gem to ./gems
copyGems = lib.fold (next: acc:
if next.source.type == "gem"
then acc + "cp ${next.src} gems/${gemName next}\n"
else acc
) "" (attrValues instantiated);
runRuby = name: env: command:
runCommand name env ''
${ruby}/bin/ruby ${writeText name command}
'';
# TODO: include json_pure, so the version of ruby doesn't matter.
# not all rubies have support for JSON built-in,
# so we'll convert JSON to ruby expressions.
json2rb = writeScript "json2rb" ''
#!${ruby}/bin/ruby
begin
require 'json'
rescue LoadError => ex
require 'json_pure'
end
puts JSON.parse(STDIN.read).inspect
'';
# dump the instantiated gemset as a ruby expression.
serializedGemset = runCommand "gemset.rb" { json = builtins.toJSON instantiated; } ''
printf '%s' "$json" | ${json2rb} > $out
'';
# this is a mapping from a source type and identifier (uri/path/etc)
# to the pure store path.
# we'll use this from the patched bundler to make fetching sources pure.
sources = runRuby "sources.rb" { gemset = serializedGemset; } ''
out = ENV['out']
gemset = eval(File.read(ENV['gemset']))
sources = {
"git" => { },
"path" => { },
"gem" => { },
"svn" => { }
}
gemset.each_value do |spec|
type = spec["source"]["type"]
val = spec["src"]
key =
case type
when "gem"
spec["name"]
when "git"
spec["source"]["url"]
when "path"
spec["source"]["originalPath"]
when "svn"
nil # TODO
end
sources[type][key] = val if key
end
File.open(out, "wb") do |f|
f.print sources.inspect
end
'';
# rewrite PATH sources to point into the nix store.
purifiedLockfile = runRuby "purifiedLockfile" {} ''
out = ENV['out']
sources = eval(File.read("${sources}"))
paths = sources["path"]
lockfile = File.read("${lockfile}")
paths.each_pair do |impure, pure|
lockfile.gsub!(/^ remote: #{Regexp.escape(impure)}/, " remote: #{pure}")
end
File.open(out, "wb") do |f|
f.print lockfile
end
'';
needsBuildFlags = attrs: attrs ? buildFlags;
mkBuildFlags = spec:
"export BUNDLE_BUILD__${lib.toUpper spec.name}='${lib.concatStringsSep " " (map shellEscape spec.buildFlags)}'";
allBuildFlags =
lib.concatStringsSep "\n"
(map mkBuildFlags
(lib.filter needsBuildFlags (attrValues instantiated)));
derivation = stdenv.mkDerivation {
inherit name;
buildInputs = [
ruby
bundler
git
] ++ args.buildInputs or [];
phases = [ "installPhase" "fixupPhase" ];
outputs = [
"out" # the installed libs/bins
"bundle" # supporting files for bundler
];
installPhase = ''
mkdir -p $bundle
export BUNDLE_GEMFILE=$bundle/Gemfile
cp ${gemfile} $BUNDLE_GEMFILE
cp ${purifiedLockfile} $BUNDLE_GEMFILE.lock
export NIX_GEM_SOURCES=${sources}
export NIX_BUNDLER_GEMPATH=${bundler}/${ruby.gemPath}
export GEM_HOME=$out/${ruby.gemPath}
export GEM_PATH=$NIX_BUNDLER_GEMPATH:$GEM_HOME
mkdir -p $GEM_HOME
${allBuildFlags}
mkdir gems
cp ${bundler}/${bundler.ruby.gemPath}/cache/bundler-*.gem gems
${copyGems}
${lib.optionalString (!documentation) ''
mkdir home
HOME="$(pwd -P)/home"
echo "gem: --no-rdoc --no-ri" > $HOME/.gemrc
''}
mkdir env
${runPreInstallers}
mkdir $out/bin
cp ${./monkey_patches.rb} monkey_patches.rb
export RUBYOPT="-rmonkey_patches.rb -I $(pwd -P)"
bundler install --frozen --binstubs ${lib.optionalString enableParallelBuilding "--jobs $NIX_BUILD_CORES"}
RUBYOPT=""
runHook postInstall
'';
inherit postInstall;
passthru = {
inherit ruby;
inherit bundler;
env = let
irbrc = builtins.toFile "irbrc" ''
if not ENV["OLD_IRBRC"].empty?
require ENV["OLD_IRBRC"]
end
require 'rubygems'
require 'bundler/setup'
'';
in stdenv.mkDerivation {
name = "interactive-${name}-environment";
nativeBuildInputs = [ ruby derivation ];
shellHook = ''
export BUNDLE_GEMFILE=${derivation.bundle}/Gemfile
export GEM_HOME=${derivation}/${ruby.gemPath}
export NIX_BUNDLER_GEMPATH=${bundler}/${ruby.gemPath}
export GEM_PATH=$NIX_BUNDLER_GEMPATH:$GEM_HOME
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
'';
};
};
inherit meta;
};
in derivation