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