{ 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