summary refs log tree commit diff
path: root/pkgs/development/interpreters/ruby/bundler-env/monkey_patches.rb
blob: f68a20212ceedff4d062ee4bf47519cbab4a7523 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
require 'bundler'

# Undo the RUBYOPT trickery.
opt = ENV['RUBYOPT'].dup
opt.gsub!(/-rmonkey_patches.rb -I [^ ]*/, '')
ENV['RUBYOPT'] = opt

Bundler.module_eval do
  class << self
    # mappings from original uris to store paths.
    def nix_gem_sources
      @nix_gem_sources ||=
        begin
          src = ENV['NIX_GEM_SOURCES']
          eval(Bundler.read_file(src))
        end
    end

    # extract the gemspecs from the gems pulled from Rubygems.
    def nix_gemspecs
      @nix_gemspecs ||= Dir.glob("gems/*.gem").map do |path|
        Bundler.rubygems.spec_from_gem(path)
      end
    end

    # swap out ENV
    def nix_with_env(env, &block)
      if env
        old_env = ENV.to_hash
        begin
          ENV.replace(env)
          block.call
        ensure
          ENV.replace(old_env)
        end
      else
        block.call
      end
    end

    # map a git uri to a fetchgit store path.
    def nix_git(uri)
      Pathname.new(nix_gem_sources["git"][uri])
    end
  end
end

Bundler::Source::Git::GitProxy.class_eval do
  def checkout
    unless path.exist?
      FileUtils.mkdir_p(path.dirname)
      FileUtils.cp_r(Bundler.nix_git(@uri).join(".git"), path)
      system("chmod -R +w #{path}")
    end
  end

  def copy_to(destination, submodules=false)
    unless File.exist?(destination.join(".git"))
      FileUtils.mkdir_p(destination.dirname)
      FileUtils.cp_r(Bundler.nix_git(@uri), destination)
      system("chmod -R +w #{destination}")
    end
  end
end

Bundler::Fetcher.class_eval do
  def use_api
    true
  end

  def fetch_dependency_remote_specs(gem_names)
    Bundler.ui.debug "Query Gemcutter Dependency Endpoint API: #{gem_names.join(',')}"
    deps_list = []

    spec_list = gem_names.map do |name|
      spec = Bundler.nix_gemspecs.detect {|spec| spec.name == name }
      if spec.nil?
        msg = "WARNING: Could not find gemspec for '#{name}'"
        Bundler.ui.warn msg
        nil
      else
        dependencies = spec.dependencies.
          select {|dep| dep.type != :development}.
          map do |dep|
            deps_list << dep.name
            dep
          end

        [spec.name, spec.version, spec.platform, dependencies]
      end
    end

    spec_list.compact!

    [spec_list, deps_list.uniq]
  end
end

Bundler::Source::Rubygems.class_eval do
  # We copy all gems into $PWD/gems, and this allows RubyGems to find those
  # gems during installation.
  def fetchers
    @fetchers ||= [
      Bundler::Fetcher.new(URI.parse("file://#{File.expand_path(Dir.pwd)}"))
    ]
  end

  # Look-up gems that were originally from RubyGems.
  def remote_specs
    @remote_specs ||=
      begin
        lockfile = Bundler::LockfileParser.new(Bundler.read_file(Bundler.default_lockfile))
        gem_names = lockfile.specs.
          select {|spec| spec.source.is_a?(Bundler::Source::Rubygems)}.
          map {|spec| spec.name}
        idx = Bundler::Index.new
        api_fetchers.each do |f|
          Bundler.ui.info "Fetching source index from #{f.uri}"
          idx.use f.specs(gem_names, self)
        end
        idx
      end
  end
end

Bundler::Installer.class_eval do

  # WHY:
  # This allows us to provide a typical Nix experience, where
  # `buildInputs` and/or `preInstall` may set up the $PATH and other env-vars
  # as needed. By swapping out the environment per install, we can have finer
  # grained control than we would have otherwise.
  #
  # HOW:
  # This is a wrapper around the original `install_gem_from_spec`.
  # We expect that a "pre-installer" might exist at `pre-installers/<gem-name>`,
  # and if it does, we execute it.
  # The pre-installer is expected to dump its environment variables as a Ruby
  # hash to `env/<gem-name>`.
  # We then swap out the environment for the duration of the install,
  # and then set it back to what it was originally.
  alias original_install_gem_from_spec install_gem_from_spec
  def install_gem_from_spec(spec, standalone = false, worker = 0)
    env_dump = "env/#{spec.name}"
    if File.exist?(env_dump)
      env = eval(Bundler.read_file(env_dump))
      unless env
        Bundler.ui.error "The environment variables for #{spec.name} could not be loaded!"
        exit 1
      end
      Bundler.nix_with_env(env) do
        original_install_gem_from_spec(spec, standalone, worker)
      end
    else
      original_install_gem_from_spec(spec, standalone, worker)
    end
  end

  def generate_bundler_executable_stubs(spec, options = {})
    return if spec.executables.empty?

    out = ENV['out']

    spec.executables.each do |executable|
      next if executable == "bundle" || executable == "bundler"

      binstub_path = "#{out}/bin/#{executable}"

      File.open(binstub_path, 'w', 0777 & ~File.umask) do |f|
        f.print <<-TEXT
#!#{RbConfig.ruby}

old_gemfile  = ENV["BUNDLE_GEMFILE"]
old_gem_home = ENV["GEM_HOME"]
old_gem_path = ENV["GEM_PATH"]

ENV["BUNDLE_GEMFILE"] =
  "#{ENV["BUNDLE_GEMFILE"]}"
ENV["GEM_HOME"] =
  "#{ENV["GEM_HOME"]}"
ENV["GEM_PATH"] =
  "#{ENV["NIX_BUNDLER_GEMPATH"]}:\#{ENV["GEM_HOME"]}\#{old_gem_path ? ":\#{old_gem_path}" : ""}}"

require 'rubygems'
require 'bundler/setup'

ENV["BUNDLE_GEMFILE"] = old_gemfile
ENV["GEM_HOME"]       = old_gem_home
ENV["GEM_PATH"]       = old_gem_path

load Gem.bin_path('#{spec.name}', '#{executable}')
TEXT
      end
    end
  end
end

Gem::Installer.class_eval do
  # Make the wrappers automagically use bundler.
  #
  # Stage 1.
  #   Set $BUNDLE_GEMFILE so bundler knows what gems to load.
  #   Set $GEM_HOME to the installed gems, because bundler looks there for
  #     non-Rubygems installed gems (e.g. git/svn/path sources).
  #   Set $GEM_PATH to include both bundler and installed gems.
  #
  # Stage 2.
  #   Setup bundler, locking down the gem versions.
  #
  # Stage 3.
  #   Reset $BUNDLE_GEMFILE, $GEM_HOME, $GEM_PATH.
  #
  # Stage 4.
  #   Run the actual executable.
  def app_script_text(bin_file_name)
    return <<-TEXT
#!#{RbConfig.ruby}
#
# This file was generated by Nix's RubyGems.
#
# The application '#{spec.name}' is installed as part of a gem, and
# this file is here to facilitate running it.
#

old_gemfile  = ENV["BUNDLE_GEMFILE"]
old_gem_home = ENV["GEM_HOME"]
old_gem_path = ENV["GEM_PATH"]

ENV["BUNDLE_GEMFILE"] =
  "#{ENV["BUNDLE_GEMFILE"]}"
ENV["GEM_HOME"] =
  "#{ENV["GEM_HOME"]}"
ENV["GEM_PATH"] =
  "#{ENV["NIX_BUNDLER_GEMPATH"]}:\#{ENV["GEM_HOME"]}\#{old_gem_path ? ":\#{old_gem_path}" : ""}}"

require 'rubygems'
require 'bundler/setup'

ENV["BUNDLE_GEMFILE"] = old_gemfile
ENV["GEM_HOME"]       = old_gem_home
ENV["GEM_PATH"]       = old_gem_path

load Gem.bin_path('#{spec.name}', '#{bin_file_name}')
TEXT
  end
end