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