1{ stdenv, buildPackages, lib
2, fetchurl, fetchpatch, fetchFromSavannah, fetchFromGitHub
3, zlib, gdbm, ncurses, readline, groff, libyaml, libffi, jemalloc, autoreconfHook, bison
4, autoconf, libiconv, libobjc, libunwind, Foundation
5, buildEnv, bundler, bundix, cargo, rustPlatform, rustc
6, makeBinaryWrapper, buildRubyGem, defaultGemConfig, removeReferencesTo
7, openssl, openssl_1_1
8, linuxPackages, libsystemtap
9} @ args:
10
11let
12 op = lib.optional;
13 ops = lib.optionals;
14 opString = lib.optionalString;
15 config = import ./config.nix { inherit fetchFromSavannah; };
16 rubygems = import ./rubygems { inherit stdenv lib fetchurl; };
17
18 openssl3Gem = fetchFromGitHub {
19 owner = "ruby";
20 repo = "openssl";
21 rev = "v3.0.2";
22 hash = "sha256-KhuKRP1JkMJv7CagGRQ0KKGOd5Oh0FP0fbj0VZ4utGo=";
23 };
24
25 # Contains the ruby version heuristics
26 rubyVersion = import ./ruby-version.nix { inherit lib; };
27
28 generic = { version, sha256, cargoSha256 ? null }: let
29 ver = version;
30 atLeast30 = lib.versionAtLeast ver.majMin "3.0";
31 atLeast31 = lib.versionAtLeast ver.majMin "3.1";
32 atLeast32 = lib.versionAtLeast ver.majMin "3.2";
33 # https://github.com/ruby/ruby/blob/v3_2_2/yjit.h#L21
34 yjitSupported = atLeast32 && (stdenv.hostPlatform.isx86_64 || (!stdenv.hostPlatform.isWindows && stdenv.hostPlatform.isAarch64));
35 self = lib.makeOverridable (
36 { stdenv, buildPackages, lib
37 , fetchurl, fetchpatch, fetchFromSavannah, fetchFromGitHub
38 , rubygemsSupport ? true
39 , zlib, zlibSupport ? true
40 , openssl, openssl_1_1, opensslSupport ? true
41 , gdbm, gdbmSupport ? true
42 , ncurses, readline, cursesSupport ? true
43 , groff, docSupport ? true
44 , libyaml, yamlSupport ? true
45 , libffi, fiddleSupport ? true
46 , jemalloc, jemallocSupport ? false
47 , linuxPackages, systemtap ? linuxPackages.systemtap, libsystemtap, dtraceSupport ? false
48 # By default, ruby has 3 observed references to stdenv.cc:
49 #
50 # - If you run:
51 # ruby -e "puts RbConfig::CONFIG['configure_args']"
52 # - In:
53 # $out/${passthru.libPath}/${stdenv.hostPlatform.system}/rbconfig.rb
54 # Or (usually):
55 # $(nix-build -A ruby)/lib/ruby/2.6.0/x86_64-linux/rbconfig.rb
56 # - In $out/lib/libruby.so and/or $out/lib/libruby.dylib
57 , removeReferencesTo, jitSupport ? yjitSupport
58 , cargo, rustPlatform, rustc, yjitSupport ? yjitSupported
59 , autoreconfHook, bison, autoconf
60 , buildEnv, bundler, bundix
61 , libiconv, libobjc, libunwind, Foundation
62 , makeBinaryWrapper, buildRubyGem, defaultGemConfig
63 , baseRuby ? buildPackages.ruby.override {
64 docSupport = false;
65 rubygemsSupport = false;
66 }
67 , useBaseRuby ? stdenv.hostPlatform != stdenv.buildPlatform
68 }:
69 stdenv.mkDerivation rec {
70 pname = "ruby";
71 inherit version;
72
73 src = fetchurl {
74 url = "https://cache.ruby-lang.org/pub/ruby/${ver.majMin}/ruby-${ver}.tar.gz";
75 inherit sha256;
76 };
77
78 # Have `configure' avoid `/usr/bin/nroff' in non-chroot builds.
79 NROFF = if docSupport then "${groff}/bin/nroff" else null;
80
81 outputs = [ "out" ] ++ lib.optional docSupport "devdoc";
82
83 strictDeps = true;
84
85 nativeBuildInputs = [ autoreconfHook bison ]
86 ++ (op docSupport groff)
87 ++ (ops (dtraceSupport && stdenv.isLinux) [ systemtap libsystemtap ])
88 ++ ops yjitSupport [ rustPlatform.cargoSetupHook cargo rustc ]
89 ++ op useBaseRuby baseRuby;
90 buildInputs = [ autoconf ]
91 ++ (op fiddleSupport libffi)
92 ++ (ops cursesSupport [ ncurses readline ])
93 ++ (op zlibSupport zlib)
94 ++ (op (atLeast30 && opensslSupport) openssl)
95 ++ (op (!atLeast30 && opensslSupport) openssl_1_1)
96 ++ (op gdbmSupport gdbm)
97 ++ (op yamlSupport libyaml)
98 # Looks like ruby fails to build on darwin without readline even if curses
99 # support is not enabled, so add readline to the build inputs if curses
100 # support is disabled (if it's enabled, we already have it) and we're
101 # running on darwin
102 ++ op (!cursesSupport && stdenv.isDarwin) readline
103 ++ ops stdenv.isDarwin [ libiconv libobjc libunwind Foundation ];
104 propagatedBuildInputs = op jemallocSupport jemalloc;
105
106 enableParallelBuilding = true;
107 # /build/ruby-2.7.7/lib/fileutils.rb:882:in `chmod':
108 # No such file or directory @ apply2files - ...-ruby-2.7.7-devdoc/share/ri/2.7.0/system/ARGF/inspect-i.ri (Errno::ENOENT)
109 # make: *** [uncommon.mk:373: do-install-all] Error 1
110 enableParallelInstalling = false;
111
112 patches = op (lib.versionOlder ver.majMin "3.1") ./do-not-regenerate-revision.h.patch
113 ++ op (atLeast30 && useBaseRuby) (
114 if atLeast32 then ./do-not-update-gems-baseruby-3.2.patch
115 else ./do-not-update-gems-baseruby.patch
116 )
117 ++ ops (ver.majMin == "3.0") [
118 # Ruby 3.0 adds `-fdeclspec` to $CC instead of $CFLAGS. Fixed in later versions.
119 (fetchpatch {
120 url = "https://github.com/ruby/ruby/commit/0acc05caf7518cd0d63ab02bfa036455add02346.patch";
121 sha256 = "sha256-43hI9L6bXfeujgmgKFVmiWhg7OXvshPCCtQ4TxqK1zk=";
122 })
123 ]
124 ++ ops (!atLeast30 && rubygemsSupport) [
125 # We upgrade rubygems to a version that isn't compatible with the
126 # ruby 2.7 installer. Backport the upstream fix.
127 ./rbinstall-new-rubygems-compat.patch
128
129 # Ruby prior to 3.0 has a bug the installer (tools/rbinstall.rb) but
130 # the resulting error was swallowed. Newer rubygems no longer swallows
131 # this error. We upgrade rubygems when rubygemsSupport is enabled, so
132 # we have to fix this bug to prevent the install step from failing.
133 # See https://github.com/ruby/ruby/pull/2930
134 (fetchpatch {
135 url = "https://github.com/ruby/ruby/commit/261d8dd20afd26feb05f00a560abd99227269c1c.patch";
136 sha256 = "0wrii25cxcz2v8bgkrf7ibcanjlxwclzhayin578bf0qydxdm9qy";
137 })
138 ]
139 ++ ops atLeast31 [
140 # When using a baseruby, ruby always sets "libdir" to the build
141 # directory, which nix rejects due to a reference in to /build/ in
142 # the final product. Removing this reference doesn't seem to break
143 # anything and fixes cross compliation.
144 ./dont-refer-to-build-dir.patch
145 ];
146
147 cargoRoot = opString yjitSupport "yjit";
148
149 cargoDeps = if yjitSupport then rustPlatform.fetchCargoTarball {
150 inherit src;
151 sourceRoot = "${pname}-${version}/${cargoRoot}";
152 sha256 = cargoSha256;
153 } else null;
154
155 postUnpack = opString rubygemsSupport ''
156 rm -rf $sourceRoot/{lib,test}/rubygems*
157 cp -r ${rubygems}/lib/rubygems* $sourceRoot/lib
158 cp -r ${rubygems}/test/rubygems $sourceRoot/test
159 '' + opString (ver.majMin == "3.0" && opensslSupport) ''
160 # Replace the Gem by a OpenSSL3-compatible one.
161 echo "Hotpatching the OpenSSL gem with a 3.x series for OpenSSL 3 support..."
162 cp -vr ${openssl3Gem}/ext/openssl $sourceRoot/ext/
163 cp -vr ${openssl3Gem}/lib/ $sourceRoot/ext/openssl/
164 cp -vr ${openssl3Gem}/{History.md,openssl.gemspec} $sourceRoot/ext/openssl/
165 '';
166
167 postPatch = ''
168 sed -i configure.ac -e '/config.guess/d'
169 cp --remove-destination ${config}/config.guess tool/
170 cp --remove-destination ${config}/config.sub tool/
171 '' + opString (!atLeast30) ''
172 # Make the build reproducible for ruby <= 2.7
173 # See https://github.com/ruby/io-console/commit/679a941d05d869f5e575730f6581c027203b7b26#diff-d8422f096931c58d4463e2489f62a228b0f24f0492950ba88c8c89a0d741cfe6
174 sed -i ext/io/console/io-console.gemspec -e '/s\.date/d'
175 '';
176
177 configureFlags = [
178 (lib.enableFeature (!stdenv.hostPlatform.isStatic) "shared")
179 (lib.enableFeature true "pthread")
180 (lib.withFeatureAs true "soname" "ruby-${version}")
181 (lib.withFeatureAs useBaseRuby "baseruby" "${baseRuby}/bin/ruby")
182 (lib.enableFeature dtraceSupport "dtrace")
183 (lib.enableFeature jitSupport "jit-support")
184 (lib.enableFeature yjitSupport "yjit")
185 (lib.enableFeature docSupport "install-doc")
186 (lib.withFeature jemallocSupport "jemalloc")
187 (lib.withFeatureAs docSupport "ridir" "${placeholder "devdoc"}/share/ri")
188 # ruby enables -O3 for gcc, however our compiler hardening wrapper
189 # overrides that by enabling `-O2` which is the minimum optimization
190 # needed for `_FORTIFY_SOURCE`.
191 ] ++ lib.optional stdenv.cc.isGNU "CFLAGS=-O3" ++ [
192 ] ++ ops stdenv.isDarwin [
193 # on darwin, we have /usr/include/tk.h -- so the configure script detects
194 # that tk is installed
195 "--with-out-ext=tk"
196 # on yosemite, "generating encdb.h" will hang for a very long time without this flag
197 "--with-setjmp-type=setjmp"
198 ];
199
200 preConfigure = opString docSupport ''
201 # rdoc creates XDG_DATA_DIR (defaulting to $HOME/.local/share) even if
202 # it's not going to be used.
203 export HOME=$TMPDIR
204 '';
205
206 # fails with "16993 tests, 2229489 assertions, 105 failures, 14 errors, 89 skips"
207 # mostly TZ- and patch-related tests
208 # TZ- failures are caused by nix sandboxing, I didn't investigate others
209 doCheck = false;
210
211 preInstall = ''
212 # Ruby installs gems here itself now.
213 mkdir -pv "$out/${passthru.gemPath}"
214 export GEM_HOME="$out/${passthru.gemPath}"
215 '';
216
217 installFlags = lib.optional docSupport "install-doc";
218 # Bundler tries to create this directory
219 postInstall = ''
220 rbConfig=$(find $out/lib/ruby -name rbconfig.rb)
221 # Remove references to the build environment from the closure
222 sed -i '/^ CONFIG\["\(BASERUBY\|SHELL\|GREP\|EGREP\|MKDIR_P\|MAKEDIRS\|INSTALL\)"\]/d' $rbConfig
223 # Remove unnecessary groff reference from runtime closure, since it's big
224 sed -i '/NROFF/d' $rbConfig
225 ${
226 lib.optionalString (!jitSupport) ''
227 # Get rid of the CC runtime dependency
228 ${removeReferencesTo}/bin/remove-references-to \
229 -t ${stdenv.cc} \
230 $out/lib/libruby*
231 ${removeReferencesTo}/bin/remove-references-to \
232 -t ${stdenv.cc} \
233 $rbConfig
234 sed -i '/CC_VERSION_MESSAGE/d' $rbConfig
235 ''
236 }
237
238 # Allow to override compiler. This is important for cross compiling as
239 # we need to set a compiler that is different from the build one.
240 sed -i 's/CONFIG\["CC"\] = "\(.*\)"/CONFIG["CC"] = if ENV["CC"].nil? || ENV["CC"].empty? then "\1" else ENV["CC"] end/' "$rbConfig"
241
242 # Remove unnecessary external intermediate files created by gems
243 extMakefiles=$(find $out/${passthru.gemPath} -name Makefile)
244 for makefile in $extMakefiles; do
245 make -C "$(dirname "$makefile")" distclean
246 done
247 find "$out/${passthru.gemPath}" \( -name gem_make.out -o -name mkmf.log \) -delete
248 # Bundler tries to create this directory
249 mkdir -p $out/nix-support
250 cat > $out/nix-support/setup-hook <<EOF
251 addGemPath() {
252 addToSearchPath GEM_PATH \$1/${passthru.gemPath}
253 }
254 addRubyLibPath() {
255 addToSearchPath RUBYLIB \$1/lib/ruby/site_ruby
256 addToSearchPath RUBYLIB \$1/lib/ruby/site_ruby/${ver.libDir}
257 addToSearchPath RUBYLIB \$1/lib/ruby/site_ruby/${ver.libDir}/${stdenv.hostPlatform.system}
258 }
259
260 addEnvHooks "$hostOffset" addGemPath
261 addEnvHooks "$hostOffset" addRubyLibPath
262 EOF
263 '' + opString docSupport ''
264 # Prevent the docs from being included in the closure
265 sed -i "s|\$(DESTDIR)$devdoc|\$(datarootdir)/\$(RI_BASE_NAME)|" $rbConfig
266 sed -i "s|'--with-ridir=$devdoc/share/ri'||" $rbConfig
267
268 # Add rbconfig shim so ri can find docs
269 mkdir -p $devdoc/lib/ruby/site_ruby
270 cp ${./rbconfig.rb} $devdoc/lib/ruby/site_ruby/rbconfig.rb
271 '' + opString useBaseRuby ''
272 # Prevent the baseruby from being included in the closure.
273 ${removeReferencesTo}/bin/remove-references-to \
274 -t ${baseRuby} \
275 $rbConfig $out/lib/libruby*
276 '';
277
278 installCheckPhase = ''
279 overriden_cc=$(CC=foo $out/bin/ruby -rrbconfig -e 'puts RbConfig::CONFIG["CC"]')
280 if [[ "$overriden_cc" != "foo" ]]; then
281 echo "CC cannot be overwritten: $overriden_cc != foo" >&2
282 false
283 fi
284
285 fallback_cc=$(unset CC; $out/bin/ruby -rrbconfig -e 'puts RbConfig::CONFIG["CC"]')
286 if [[ "$fallback_cc" != "$CC" ]]; then
287 echo "CC='$fallback_cc' should be '$CC' by default" >&2
288 false
289 fi
290 '';
291 doInstallCheck = true;
292
293 disallowedRequisites = op (!jitSupport) stdenv.cc.cc
294 ++ op useBaseRuby baseRuby;
295
296 meta = with lib; {
297 description = "An object-oriented language for quick and easy programming";
298 homepage = "https://www.ruby-lang.org/";
299 license = licenses.ruby;
300 maintainers = with maintainers; [ vrthra manveru marsam ];
301 platforms = platforms.all;
302 knownVulnerabilities = op (lib.versionOlder ver.majMin "3.0") "This Ruby release has reached its end of life. See https://www.ruby-lang.org/en/downloads/branches/.";
303 };
304
305 passthru = rec {
306 version = ver;
307 rubyEngine = "ruby";
308 libPath = "lib/${rubyEngine}/${ver.libDir}";
309 gemPath = "lib/${rubyEngine}/gems/${ver.libDir}";
310 devEnv = import ./dev.nix {
311 inherit buildEnv bundler bundix;
312 ruby = self;
313 };
314
315 inherit rubygems;
316 inherit (import ../../ruby-modules/with-packages {
317 inherit lib stdenv makeBinaryWrapper buildRubyGem buildEnv;
318 gemConfig = defaultGemConfig;
319 ruby = self;
320 }) withPackages buildGems gems;
321
322 } // lib.optionalAttrs useBaseRuby {
323 inherit baseRuby;
324 };
325 }
326 ) args; in self;
327
328in {
329 mkRubyVersion = rubyVersion;
330 mkRuby = generic;
331
332 ruby_2_7 = generic {
333 version = rubyVersion "2" "7" "8" "";
334 sha256 = "sha256-wtq2PLyPKgVSYQitQZ76Y6Z+1AdNu8+fwrHKZky0W6A=";
335 };
336
337 ruby_3_0 = generic {
338 version = rubyVersion "3" "0" "6" "";
339 sha256 = "sha256-bmy9SQAw15EMD/IO3vq0KU380QRvD49H94tZeYesaD4=";
340 };
341
342 ruby_3_1 = generic {
343 version = rubyVersion "3" "1" "4" "";
344 sha256 = "sha256-o9VYeaDfqx1xQf3xDSKgfb+OXNxEFdob3gYSfVzDx7Y=";
345 };
346
347 ruby_3_2 = generic {
348 version = rubyVersion "3" "2" "2" "";
349 sha256 = "sha256-lsV1WIcaZ0jeW8nydOk/S1qtBs2PN776Do2U57ikI7w=";
350 cargoSha256 = "sha256-6du7RJo0DH+eYMOoh3L31F3aqfR5+iG1iKauSV1uNcQ=";
351 };
352}