at v192 340 lines 9.5 kB view raw
1{ stdenv, runCommand, writeText, writeScript, writeScriptBin, ruby, lib 2, callPackage, defaultGemConfig, fetchurl, fetchgit, buildRubyGem , bundler_HEAD 3, git 4}@defs: 5 6# This is a work-in-progress. 7# The idea is that his will replace load-ruby-env.nix. 8 9{ name, gemset, gemfile, lockfile, ruby ? defs.ruby, gemConfig ? defaultGemConfig 10, enableParallelBuilding ? false # TODO: this might not work, given the env-var shinanigans. 11, postInstall ? null 12, documentation ? false 13, meta ? {} 14, ... 15}@args: 16 17let 18 19 shellEscape = x: "'${lib.replaceChars ["'"] [("'\\'" + "'")] x}'"; 20 const = x: y: x; 21 bundler = bundler_HEAD.override { inherit ruby; }; 22 inherit (builtins) attrValues; 23 24 gemName = attrs: "${attrs.name}-${attrs.version}.gem"; 25 26 fetchers.path = attrs: attrs.source.path; 27 fetchers.gem = attrs: fetchurl { 28 url = "${attrs.source.source or "https://rubygems.org"}/downloads/${gemName attrs}"; 29 inherit (attrs.source) sha256; 30 }; 31 fetchers.git = attrs: fetchgit { 32 inherit (attrs.source) url rev sha256 fetchSubmodules; 33 leaveDotGit = true; 34 }; 35 36 applySrc = attrs: 37 attrs // { 38 src = (fetchers."${attrs.source.type}" attrs); 39 }; 40 41 applyGemConfigs = attrs: 42 if gemConfig ? "${attrs.name}" 43 then attrs // gemConfig."${attrs.name}" attrs 44 else attrs; 45 46 needsPatch = attrs: 47 (attrs ? patches) || (attrs ? prePatch) || (attrs ? postPatch); 48 49 # patch a gem or source tree. 50 # for gems, the gem is unpacked, patched, and then repacked. 51 # see: https://github.com/fedora-ruby/gem-patch/blob/master/lib/rubygems/patcher.rb 52 applyPatches = attrs: 53 if !needsPatch attrs 54 then attrs 55 else attrs // { src = 56 stdenv.mkDerivation { 57 name = gemName attrs; 58 phases = [ "unpackPhase" "patchPhase" "installPhase" ]; 59 buildInputs = [ ruby ] ++ attrs.buildInputs or []; 60 patches = attrs.patches or [ ]; 61 prePatch = attrs.prePatch or "true"; 62 postPatch = attrs.postPatch or "true"; 63 unpackPhase = '' 64 runHook preUnpack 65 66 if [[ -f ${attrs.src} ]]; then 67 isGem=1 68 # we won't know the name of the directory that RubyGems creates, 69 # so we'll just use a glob to find it and move it over. 70 gem unpack ${attrs.src} --target=container 71 cp -r container/* contents 72 rm -r container 73 else 74 cp -r ${attrs.src} contents 75 chmod -R +w contents 76 fi 77 78 cd contents 79 runHook postUnpack 80 ''; 81 installPhase = '' 82 runHook preInstall 83 84 if [[ -n "$isGem" ]]; then 85 ${writeScript "repack.rb" '' 86 #!${ruby}/bin/ruby 87 require 'rubygems' 88 require 'rubygems/package' 89 require 'fileutils' 90 91 if defined?(Encoding.default_internal) 92 Encoding.default_internal = Encoding::UTF_8 93 Encoding.default_external = Encoding::UTF_8 94 end 95 96 if Gem::VERSION < '2.0' 97 load "${./package-1.8.rb}" 98 end 99 100 out = ENV['out'] 101 files = Dir['**/{.[^\.]*,*}'] 102 103 package = Gem::Package.new("${attrs.src}") 104 patched_package = Gem::Package.new(package.spec.file_name) 105 patched_package.spec = package.spec.clone 106 patched_package.spec.files = files 107 108 patched_package.build(false) 109 110 FileUtils.cp(patched_package.spec.file_name, out) 111 ''} 112 else 113 cp -r . $out 114 fi 115 116 runHook postInstall 117 ''; 118 }; 119 }; 120 121 instantiate = (attrs: 122 applyPatches (applyGemConfigs (applySrc attrs)) 123 ); 124 125 instantiated = lib.flip lib.mapAttrs (import gemset) (name: attrs: 126 instantiate (attrs // { inherit name; }) 127 ); 128 129 needsPreInstall = attrs: 130 (attrs ? preInstall) || (attrs ? buildInputs) || (attrs ? nativeBuildInputs); 131 132 # TODO: support cross compilation? look at stdenv/generic/default.nix. 133 runPreInstallers = lib.fold (next: acc: 134 if !needsPreInstall next 135 then acc 136 else acc + '' 137 ${writeScript "${next.name}-pre-install" '' 138 #!${stdenv.shell} 139 140 export nativeBuildInputs="${toString ((next.nativeBuildInputs or []) ++ (next.buildInputs or []))}" 141 142 source ${stdenv}/setup 143 144 header "running pre-install script for ${next.name}" 145 146 ${next.preInstall or ""} 147 148 ${ruby}/bin/ruby -e 'print ENV.inspect' > env/${next.name} 149 150 stopNest 151 ''} 152 '' 153 ) "" (attrValues instantiated); 154 155 # copy *.gem to ./gems 156 copyGems = lib.fold (next: acc: 157 if next.source.type == "gem" 158 then acc + "cp ${next.src} gems/${gemName next}\n" 159 else acc 160 ) "" (attrValues instantiated); 161 162 runRuby = name: env: command: 163 runCommand name env '' 164 ${ruby}/bin/ruby ${writeText name command} 165 ''; 166 167 # TODO: include json_pure, so the version of ruby doesn't matter. 168 # not all rubies have support for JSON built-in, 169 # so we'll convert JSON to ruby expressions. 170 json2rb = writeScript "json2rb" '' 171 #!${ruby}/bin/ruby 172 begin 173 require 'json' 174 rescue LoadError => ex 175 require 'json_pure' 176 end 177 178 puts JSON.parse(STDIN.read).inspect 179 ''; 180 181 # dump the instantiated gemset as a ruby expression. 182 serializedGemset = runCommand "gemset.rb" { json = builtins.toJSON instantiated; } '' 183 printf '%s' "$json" | ${json2rb} > $out 184 ''; 185 186 # this is a mapping from a source type and identifier (uri/path/etc) 187 # to the pure store path. 188 # we'll use this from the patched bundler to make fetching sources pure. 189 sources = runRuby "sources.rb" { gemset = serializedGemset; } '' 190 out = ENV['out'] 191 gemset = eval(File.read(ENV['gemset'])) 192 193 sources = { 194 "git" => { }, 195 "path" => { }, 196 "gem" => { }, 197 "svn" => { } 198 } 199 200 gemset.each_value do |spec| 201 type = spec["source"]["type"] 202 val = spec["src"] 203 key = 204 case type 205 when "gem" 206 spec["name"] 207 when "git" 208 spec["source"]["url"] 209 when "path" 210 spec["source"]["originalPath"] 211 when "svn" 212 nil # TODO 213 end 214 215 sources[type][key] = val if key 216 end 217 218 File.open(out, "wb") do |f| 219 f.print sources.inspect 220 end 221 ''; 222 223 # rewrite PATH sources to point into the nix store. 224 purifiedLockfile = runRuby "purifiedLockfile" {} '' 225 out = ENV['out'] 226 sources = eval(File.read("${sources}")) 227 paths = sources["path"] 228 229 lockfile = File.read("${lockfile}") 230 231 paths.each_pair do |impure, pure| 232 lockfile.gsub!(/^ remote: #{Regexp.escape(impure)}/, " remote: #{pure}") 233 end 234 235 File.open(out, "wb") do |f| 236 f.print lockfile 237 end 238 ''; 239 240 needsBuildFlags = attrs: attrs ? buildFlags; 241 242 mkBuildFlags = spec: 243 "export BUNDLE_BUILD__${lib.toUpper spec.name}='${lib.concatStringsSep " " (map shellEscape spec.buildFlags)}'"; 244 245 allBuildFlags = 246 lib.concatStringsSep "\n" 247 (map mkBuildFlags 248 (lib.filter needsBuildFlags (attrValues instantiated))); 249 250 derivation = stdenv.mkDerivation { 251 inherit name; 252 253 buildInputs = [ 254 ruby 255 bundler 256 git 257 ] ++ args.buildInputs or []; 258 259 phases = [ "installPhase" "fixupPhase" ]; 260 261 outputs = [ 262 "out" # the installed libs/bins 263 "bundle" # supporting files for bundler 264 ]; 265 266 installPhase = '' 267 mkdir -p $bundle 268 export BUNDLE_GEMFILE=$bundle/Gemfile 269 cp ${gemfile} $BUNDLE_GEMFILE 270 cp ${purifiedLockfile} $BUNDLE_GEMFILE.lock 271 272 export NIX_GEM_SOURCES=${sources} 273 export NIX_BUNDLER_GEMPATH=${bundler}/${ruby.gemPath} 274 275 export GEM_HOME=$out/${ruby.gemPath} 276 export GEM_PATH=$NIX_BUNDLER_GEMPATH:$GEM_HOME 277 mkdir -p $GEM_HOME 278 279 ${allBuildFlags} 280 281 mkdir gems 282 cp ${bundler}/${bundler.ruby.gemPath}/cache/bundler-*.gem gems 283 ${copyGems} 284 285 ${lib.optionalString (!documentation) '' 286 mkdir home 287 HOME="$(pwd -P)/home" 288 echo "gem: --no-rdoc --no-ri" > $HOME/.gemrc 289 ''} 290 291 mkdir env 292 ${runPreInstallers} 293 294 mkdir $out/bin 295 cp ${./monkey_patches.rb} monkey_patches.rb 296 export RUBYOPT="-rmonkey_patches.rb -I $(pwd -P)" 297 bundler install --frozen --binstubs ${lib.optionalString enableParallelBuilding "--jobs $NIX_BUILD_CORES"} 298 RUBYOPT="" 299 300 runHook postInstall 301 ''; 302 303 inherit postInstall; 304 305 passthru = { 306 inherit ruby; 307 inherit bundler; 308 309 env = let 310 irbrc = builtins.toFile "irbrc" '' 311 if not ENV["OLD_IRBRC"].empty? 312 require ENV["OLD_IRBRC"] 313 end 314 require 'rubygems' 315 require 'bundler/setup' 316 ''; 317 in stdenv.mkDerivation { 318 name = "interactive-${name}-environment"; 319 nativeBuildInputs = [ ruby derivation ]; 320 shellHook = '' 321 export BUNDLE_GEMFILE=${derivation.bundle}/Gemfile 322 export GEM_HOME=${derivation}/${ruby.gemPath} 323 export NIX_BUNDLER_GEMPATH=${bundler}/${ruby.gemPath} 324 export GEM_PATH=$NIX_BUNDLER_GEMPATH:$GEM_HOME 325 export OLD_IRBRC="$IRBRC" 326 export IRBRC=${irbrc} 327 ''; 328 buildCommand = '' 329 echo >&2 "" 330 echo >&2 "*** Ruby 'env' attributes are intended for interactive nix-shell sessions, not for building! ***" 331 echo >&2 "" 332 exit 1 333 ''; 334 }; 335 }; 336 337 inherit meta; 338 }; 339 340in derivation