summaryrefslogtreecommitdiff
path: root/lib/rubygems/ext/builder.rb
blob: 6d32be6515b91e7300db4c40aefec13ac8666fda (plain)
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
# frozen_string_literal: true
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++

require_relative '../user_interaction'

class Gem::Ext::Builder
  include Gem::UserInteraction

  attr_accessor :build_args # :nodoc:

  def self.class_name
    name =~ /Ext::(.*)Builder/
    $1.downcase
  end

  def self.make(dest_path, results, make_dir = Dir.pwd)
    unless File.exist? File.join(make_dir, 'Makefile')
      raise Gem::InstallError, 'Makefile not found'
    end

    # try to find make program from Ruby configure arguments first
    RbConfig::CONFIG['configure_args'] =~ /with-make-prog\=(\w+)/
    make_program_name = ENV['MAKE'] || ENV['make'] || $1
    unless make_program_name
      make_program_name = (/mswin/ =~ RUBY_PLATFORM) ? 'nmake' : 'make'
    end
    make_program = Shellwords.split(make_program_name)

    # The installation of the bundled gems is failed when DESTDIR is empty in mswin platform.
    destdir = (/\bnmake/i !~ make_program_name || ENV['DESTDIR'] && ENV['DESTDIR'] != "") ? 'DESTDIR=%s' % ENV['DESTDIR'] : ''

    ['clean', '', 'install'].each do |target|
      # Pass DESTDIR via command line to override what's in MAKEFLAGS
      cmd = [
        *make_program,
        destdir,
        target,
      ].reject(&:empty?)
      begin
        run(cmd, results, "make #{target}".rstrip, make_dir)
      rescue Gem::InstallError
        raise unless target == 'clean' # ignore clean failure
      end
    end
  end

  def self.run(command, results, command_name = nil, dir = Dir.pwd)
    verbose = Gem.configuration.really_verbose

    begin
      rubygems_gemdeps, ENV['RUBYGEMS_GEMDEPS'] = ENV['RUBYGEMS_GEMDEPS'], nil
      if verbose
        puts("current directory: #{dir}")
        p(command)
      end
      results << "current directory: #{dir}"
      require "shellwords"
      results << command.shelljoin

      require "open3"
      # Set $SOURCE_DATE_EPOCH for the subprocess.
      env = {'SOURCE_DATE_EPOCH' => Gem.source_date_epoch_string}
      output, status = begin
                         Open3.capture2e(env, *command, :chdir => dir)
                       rescue => error
                         raise Gem::InstallError, "#{command_name || class_name} failed#{error.message}"
                       end
      if verbose
        puts output
      else
        results << output
      end
    ensure
      ENV['RUBYGEMS_GEMDEPS'] = rubygems_gemdeps
    end

    unless status.success?
      results << "Building has failed. See above output for more information on the failure." if verbose
    end

    yield(status, results) if block_given?

    unless status.success?
      exit_reason =
        if status.exited?
          ", exit code #{status.exitstatus}"
        elsif status.signaled?
          ", uncaught signal #{status.termsig}"
        end

      raise Gem::InstallError, "#{command_name || class_name} failed#{exit_reason}"
    end
  end

  ##
  # Creates a new extension builder for +spec+.  If the +spec+ does not yet
  # have build arguments, saved, set +build_args+ which is an ARGV-style
  # array.

  def initialize(spec, build_args = spec.build_args)
    @spec       = spec
    @build_args = build_args
    @gem_dir    = spec.full_gem_path

    @ran_rake = false
  end

  ##
  # Chooses the extension builder class for +extension+

  def builder_for(extension) # :nodoc:
    case extension
    when /extconf/ then
      Gem::Ext::ExtConfBuilder
    when /configure/ then
      Gem::Ext::ConfigureBuilder
    when /rakefile/i, /mkrf_conf/i then
      @ran_rake = true
      Gem::Ext::RakeBuilder
    when /CMakeLists.txt/ then
      Gem::Ext::CmakeBuilder
    else
      build_error("No builder for extension '#{extension}'")
    end
  end

  ##
  # Logs the build +output+, then raises Gem::Ext::BuildError.

  def build_error(output, backtrace = nil) # :nodoc:
    gem_make_out = write_gem_make_out output

    message = <<-EOF
ERROR: Failed to build gem native extension.

    #{output}

Gem files will remain installed in #{@gem_dir} for inspection.
Results logged to #{gem_make_out}
EOF

    raise Gem::Ext::BuildError, message, backtrace
  end

  def build_extension(extension, dest_path) # :nodoc:
    results = []

    builder = builder_for(extension)

    extension_dir =
      File.expand_path File.join(@gem_dir, File.dirname(extension))
    lib_dir = File.join @spec.full_gem_path, @spec.raw_require_paths.first

    begin
      FileUtils.mkdir_p dest_path

      results = builder.build(extension, dest_path,
                              results, @build_args, lib_dir, extension_dir)

      verbose { results.join("\n") }

      write_gem_make_out results.join "\n"
    rescue => e
      results << e.message
      build_error(results.join("\n"), $@)
    end
  end

  ##
  # Builds extensions.  Valid types of extensions are extconf.rb files,
  # configure scripts and rakefiles or mkrf_conf files.

  def build_extensions
    return if @spec.extensions.empty?

    if @build_args.empty?
      say "Building native extensions. This could take a while..."
    else
      say "Building native extensions with: '#{@build_args.join ' '}'"
      say "This could take a while..."
    end

    dest_path = @spec.extension_dir

    require "fileutils"
    FileUtils.rm_f @spec.gem_build_complete_path

    @spec.extensions.each do |extension|
      break if @ran_rake

      build_extension extension, dest_path
    end

    FileUtils.touch @spec.gem_build_complete_path
  end

  ##
  # Writes +output+ to gem_make.out in the extension install directory.

  def write_gem_make_out(output) # :nodoc:
    destination = File.join @spec.extension_dir, 'gem_make.out'

    FileUtils.mkdir_p @spec.extension_dir

    File.open destination, 'wb' do |io|
      io.puts output
    end

    destination
  end
end