at 24.11-pre 8.0 kB view raw
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)