1# This builds gems in a way that is compatible with bundler.
2#
3# Bundler installs gems from git sources _very_ differently from how RubyGems
4# installs gem packages, though they both install gem packages similarly.
5#
6# We monkey-patch Bundler to remove any impurities and then drive its internals
7# to install git gems.
8#
9# For the sake of simplicity, gem packages are installed with the standard `gem`
10# program.
11#
12# Note that bundler does not support multiple prefixes; it assumes that all
13# gems are installed in a common prefix, and has no support for specifying
14# otherwise. Therefore, if you want to be able to use the resulting derivations
15# with bundler, you need to create a symlink forrest first, which is what
16# `bundlerEnv` does for you.
17#
18# Normal gem packages can be used outside of bundler; a binstub is created in
19# $out/bin.
20
21{
22 lib,
23 fetchurl,
24 fetchgit,
25 makeWrapper,
26 gitMinimal,
27 ruby,
28 bundler,
29}@defs:
30
31lib.makeOverridable (
32
33 {
34 name ? null,
35 gemName,
36 version ? null,
37 type ? "gem",
38 document ? [ ], # e.g. [ "ri" "rdoc" ]
39 platform ? "ruby",
40 ruby ? defs.ruby,
41 stdenv ? ruby.stdenv,
42 namePrefix ? (
43 let
44 rubyName = builtins.parseDrvName ruby.name;
45 in
46 "${rubyName.name}${lib.versions.majorMinor rubyName.version}-"
47 ),
48 nativeBuildInputs ? [ ],
49 buildInputs ? [ ],
50 meta ? { },
51 patches ? [ ],
52 gemPath ? [ ],
53 dontStrip ? false,
54 # Assume we don't have to build unless strictly necessary (e.g. the source is a
55 # git checkout).
56 # If you need to apply patches, make sure to set `dontBuild = false`;
57 dontBuild ? true,
58 dontInstallManpages ? false,
59 propagatedBuildInputs ? [ ],
60 propagatedUserEnvPkgs ? [ ],
61 buildFlags ? [ ],
62 passthru ? { },
63 # bundler expects gems to be stored in the cache directory for certain actions
64 # such as `bundler install --redownload`.
65 # At the cost of increasing the store size, you can keep the gems to have closer
66 # alignment with what Bundler expects.
67 keepGemCache ? false,
68 ...
69 }@attrs:
70
71 let
72 src =
73 attrs.src or (
74 if type == "gem" then
75 fetchurl {
76 urls = map (remote: "${remote}/gems/${gemName}-${suffix}.gem") (
77 attrs.source.remotes or [ "https://rubygems.org" ]
78 );
79 inherit (attrs.source) sha256;
80 }
81 else if type == "git" then
82 fetchgit {
83 inherit (attrs.source)
84 url
85 rev
86 sha256
87 fetchSubmodules
88 ;
89 }
90 else if type == "url" then
91 fetchurl attrs.source
92 else
93 throw "buildRubyGem: don't know how to build a gem of type \"${type}\""
94 );
95
96 # See: https://github.com/rubygems/rubygems/blob/7a7b234721c375874b7e22b1c5b14925b943f04e/bundler/lib/bundler/source/git.rb#L103
97 suffix =
98 if type == "git" then
99 builtins.substring 0 12 attrs.source.rev
100 else if platform != "ruby" then
101 "${version}-${platform}"
102 else
103 version;
104
105 documentFlag = if document == [ ] then "-N" else "--document ${lib.concatStringsSep "," document}";
106
107 in
108
109 stdenv.mkDerivation (
110 (builtins.removeAttrs attrs [ "source" ])
111 // {
112 inherit ruby;
113 inherit dontBuild;
114 inherit dontStrip;
115 inherit suffix;
116 gemType = type;
117
118 nativeBuildInputs = [
119 ruby
120 makeWrapper
121 ]
122 ++ lib.optionals (type == "git") [ gitMinimal ]
123 ++ lib.optionals (type != "gem") [ bundler ]
124 ++ nativeBuildInputs;
125
126 buildInputs = [
127 ruby
128 ]
129 ++ buildInputs;
130
131 #name = builtins.trace (attrs.name or "no attr.name" ) "${namePrefix}${gemName}-${version}";
132 name = attrs.name or "${namePrefix}${gemName}-${suffix}";
133
134 inherit src;
135
136 unpackPhase =
137 attrs.unpackPhase or ''
138 runHook preUnpack
139
140 if [[ -f $src && $src == *.gem ]]; then
141 if [[ -z "''${dontBuild-}" ]]; then
142 # we won't know the name of the directory that RubyGems creates,
143 # so we'll just use a glob to find it and move it over.
144 gempkg="$src"
145 sourceRoot=source
146 gem unpack $gempkg --target=container
147 cp -r container/* $sourceRoot
148 rm -r container
149
150 # copy out the original gemspec, for convenience during patching /
151 # overrides.
152 gem specification $gempkg --ruby > original.gemspec
153 gemspec=$(readlink -f .)/original.gemspec
154 else
155 gempkg="$src"
156 fi
157 else
158 # Fall back to the original thing for everything else.
159 dontBuild=""
160 preUnpack="" postUnpack="" unpackPhase
161 fi
162
163 runHook postUnpack
164 '';
165
166 # As of ruby 3.0, ruby headers require -fdeclspec when building with clang
167 # Introduced in https://github.com/ruby/ruby/commit/0958e19ffb047781fe1506760c7cbd8d7fe74e57
168 env = lib.optionalAttrs (attrs ? env) attrs.env // {
169 NIX_CFLAGS_COMPILE = toString (
170 lib.optionals
171 (ruby.rubyEngine == "ruby" && stdenv.cc.isClang && lib.versionAtLeast ruby.version.major "3")
172 [
173 "-fdeclspec"
174 ]
175 ++ lib.optional (attrs.env.NIX_CFLAGS_COMPILE or "" != "") attrs.env.NIX_CFLAGS_COMPILE
176 );
177 };
178
179 buildPhase =
180 attrs.buildPhase or ''
181 runHook preBuild
182
183 if [[ "$gemType" == "gem" ]]; then
184 if [[ -z "$gemspec" ]]; then
185 gemspec="$(find . -name '*.gemspec')"
186 echo "found the following gemspecs:"
187 echo "$gemspec"
188 gemspec="$(echo "$gemspec" | head -n1)"
189 fi
190
191 exec 3>&1
192 output="$(gem build $gemspec | tee >(cat - >&3))"
193 exec 3>&-
194
195 gempkg=$(echo "$output" | grep -oP 'File: \K(.*)')
196
197 echo "gem package built: $gempkg"
198 elif [[ "$gemType" == "git" ]]; then
199 git init
200 # remove variations to improve the likelihood of a bit-reproducible output
201 rm -rf .git/logs/ .git/hooks/ .git/index .git/FETCH_HEAD .git/ORIG_HEAD .git/refs/remotes/origin/HEAD .git/config
202 # support `git ls-files`
203 git add .
204 fi
205
206 runHook postBuild
207 '';
208
209 # Note:
210 # We really do need to keep the $out/${ruby.gemPath}/cache.
211 # This is very important in order for many parts of RubyGems/Bundler to not blow up.
212 # See https://github.com/bundler/bundler/issues/3327
213 installPhase =
214 attrs.installPhase or ''
215 runHook preInstall
216
217 export GEM_HOME=$out/${ruby.gemPath}
218 mkdir -p $GEM_HOME
219
220 echo "buildFlags: $buildFlags"
221
222 ${lib.optionalString (type == "url") ''
223 ruby ${./nix-bundle-install.rb} \
224 "path" \
225 '${gemName}' \
226 '${version}' \
227 '${lib.escapeShellArgs buildFlags}'
228 ''}
229 ${lib.optionalString (type == "git") ''
230 ruby ${./nix-bundle-install.rb} \
231 "git" \
232 '${gemName}' \
233 '${version}' \
234 '${lib.escapeShellArgs buildFlags}' \
235 '${attrs.source.url}' \
236 '.' \
237 '${attrs.source.rev}'
238 ''}
239
240 ${lib.optionalString (type == "gem") ''
241 if [[ -z "$gempkg" ]]; then
242 echo "failure: \$gempkg path unspecified" 1>&2
243 exit 1
244 elif [[ ! -f "$gempkg" ]]; then
245 echo "failure: \$gempkg path invalid" 1>&2
246 exit 1
247 fi
248
249 gem install \
250 --local \
251 --force \
252 --http-proxy 'http://nodtd.invalid' \
253 --ignore-dependencies \
254 --install-dir "$GEM_HOME" \
255 --build-root '/' \
256 --backtrace \
257 --no-env-shebang \
258 ${documentFlag} \
259 $gempkg $gemFlags -- $buildFlags
260
261 # looks like useless files which break build repeatability and consume space
262 pushd $out/${ruby.gemPath}
263 find doc/ -iname created.rid -delete -print
264 find gems/*/ext/ extensions/ \( -iname Makefile -o -iname mkmf.log -o -iname gem_make.out \) -delete -print
265 ${lib.optionalString (!keepGemCache) "rm -fvr cache"}
266 popd
267
268 # write out metadata and binstubs
269 spec=$(echo $out/${ruby.gemPath}/specifications/*.gemspec)
270 ruby ${./gem-post-build.rb} "$spec"
271 ''}
272
273 ${lib.optionalString (!dontInstallManpages) ''
274 for section in {1..9}; do
275 mandir="$out/share/man/man$section"
276 find $out/lib \( -wholename "*/man/*.$section" -o -wholename "*/man/man$section/*.$section" \) \
277 -execdir mkdir -p $mandir \; -execdir cp '{}' $mandir \;
278 done
279 ''}
280
281 # For Ruby-generated binstubs, shebang paths are already in Nix store but for
282 # ruby used to build the package. Update them to match the host system. Note
283 # that patchShebangsAuto ignores scripts where shebang line is already in Nix
284 # store.
285 if [[ -d $GEM_HOME/bin ]]; then
286 patchShebangs --update --host -- "$GEM_HOME"/bin
287 fi
288
289 runHook postInstall
290 '';
291
292 propagatedBuildInputs = gemPath ++ propagatedBuildInputs;
293 propagatedUserEnvPkgs = gemPath ++ propagatedUserEnvPkgs;
294
295 passthru = passthru // {
296 isRubyGem = true;
297 };
298 meta = {
299 # default to Ruby's platforms
300 platforms = ruby.meta.platforms;
301 mainProgram = gemName;
302 }
303 // meta;
304 }
305 )
306
307)