Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/rubygems/commands/install_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def description # :nodoc:
[build fails]
Gem files will remain installed in \\
/path/to/gems/some_extension_gem-1.0 for inspection.
Results logged to /path/to/gems/some_extension_gem-1.0/gem_make.out
Results logged to /path/to/build_info/some_extension_gem-1.0.gem_make.out
$ gem install some_extension_gem -- --with-extension-lib=/path/to/lib
[build succeeds]
$ gem list some_extension_gem
Expand All @@ -110,7 +110,7 @@ def description # :nodoc:
[build fails]
Gem files will remain installed in \\
/path/to/gems/some_extension_gem-1.0 for inspection.
Results logged to /path/to/gems/some_extension_gem-1.0/gem_make.out
Results logged to /path/to/build_info/some_extension_gem-1.0.gem_make.out
$ [cd /path/to/gems/some_extension_gem-1.0]
$ [edit files or what-have-you and run make]
$ gem spec ../../cache/some_extension_gem-1.0.gem --ruby > \\
Expand Down
26 changes: 22 additions & 4 deletions lib/rubygems/ext/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -222,9 +222,25 @@ def build_extension(extension, dest_path) # :nodoc:

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

write_gem_make_out results.join "\n"
# mkmf.log is a noisy, non-reproducible build artifact that is not meant
# to be installed. Drop the one left behind by a successful build instead
# of leaving it in the installation tree.
FileUtils.rm_f File.join(extension_dir, "mkmf.log")
rescue StandardError => e
results << e.message

# On failure keep the mkmf.log for inspection, but move it out of the
# installation tree into the build_info directory.
mkmf_log = File.join extension_dir, "mkmf.log"
if File.exist?(mkmf_log)
mkmf_log_dest = File.join @spec.build_info_dir, "#{@spec.full_name}.mkmf.log"
FileUtils.mkdir_p @spec.build_info_dir
FileUtils.mv mkmf_log, mkmf_log_dest

results << "To see why this extension failed to compile, please check the mkmf.log which can be found here:"
results << " #{mkmf_log_dest}"
end

build_error(results.join("\n"), $@)
end
end
Expand Down Expand Up @@ -256,12 +272,14 @@ def build_extensions
end

##
# Writes +output+ to gem_make.out in the extension install directory.
# Writes +output+ to gem_make.out in the build_info directory. Only called
# on failure (via #build_error), to keep build logs out of the installation
# tree.

def write_gem_make_out(output) # :nodoc:
destination = File.join @spec.extension_dir, "gem_make.out"
destination = File.join @spec.build_info_dir, "#{@spec.full_name}.gem_make.out"

FileUtils.mkdir_p @spec.extension_dir
FileUtils.mkdir_p @spec.build_info_dir

File.open destination, "wb" do |io|
io.puts output
Expand Down
15 changes: 4 additions & 11 deletions lib/rubygems/ext/ext_conf_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,10 @@ def self.build(extension, dest_path, results, args = [], lib_dir = nil, extensio
cmd << "--target-rbconfig=#{target_rbconfig.path}" if target_rbconfig.path
cmd.push(*args)

run(cmd, results, class_name, extension_dir) do |s, r|
mkmf_log = File.join(extension_dir, "mkmf.log")
if File.exist? mkmf_log
unless s.success?
r << "To see why this extension failed to compile, please check" \
" the mkmf.log which can be found here:\n"
r << " " + File.join(dest_path, "mkmf.log") + "\n"
end
FileUtils.mv mkmf_log, dest_path
end
end
# Leave mkmf.log in the extension directory. The final placement (dropped
# on success, moved to build_info on failure) is decided by
# Gem::Ext::Builder#build_extension.
run(cmd, results, class_name, extension_dir)

ENV["DESTDIR"] = nil

Expand Down
2 changes: 2 additions & 0 deletions lib/rubygems/uninstaller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,8 @@ def remove(spec)

safe_delete { rm_r full_gem_path, exclusions: exclusions }
safe_delete { FileUtils.rm_r spec.extension_dir }
safe_delete { FileUtils.rm_f File.join(spec.build_info_dir, "#{spec.full_name}.mkmf.log") }
safe_delete { FileUtils.rm_f File.join(spec.build_info_dir, "#{spec.full_name}.gem_make.out") }

old_platform_name = spec.original_name

Expand Down
70 changes: 43 additions & 27 deletions spec/bundler/installer/parallel_installer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -147,34 +147,37 @@
let(:gem_two) { definition.specs.find {|spec| spec.name == "two" } }

it "takes all available slots" do
redefine_build_jobs do
acquired = track_build_jobs(rendezvous: true) do
Bundler::ParallelInstaller.call(installer, definition.specs, 5, false, true)
end

# Take 3 slots out of the 5 available.
expect(File.read(File.join(gem_one.extension_dir, "gem_make.out"))).to include("make -j3")
# Take 3 slots (capped per gem) out of the 5 available.
expect(acquired["one"]).to eq(3)
# Take the remaining 2 slots.
expect(File.read(File.join(gem_two.extension_dir, "gem_make.out"))).to include("make -j2")
expect(acquired["two"]).to eq(2)
end

it "fallback to non parallel when no slots are available" do
redefine_build_jobs do
acquired = track_build_jobs(rendezvous: true) do
Bundler::ParallelInstaller.call(installer, definition.specs, 3, false, true)
end

# Take 3 slots out of the 3 available.
expect(File.read(File.join(gem_one.extension_dir, "gem_make.out"))).to include("make -j3")
expect(acquired["one"]).to eq(3)
# Fallback to one slot (non parallel).
expect(File.read(File.join(gem_two.extension_dir, "gem_make.out"))).to_not include("make -j")
expect(acquired["two"]).to eq(1)
end

it "uses one jobs when installing serially" do
acquired = nil
Bundler.settings.temporary(jobs: 1) do
Bundler::ParallelInstaller.call(installer, definition.specs, 1, false, true)
acquired = track_build_jobs do
Bundler::ParallelInstaller.call(installer, definition.specs, 1, false, true)
end
end

expect(File.read(File.join(gem_one.extension_dir, "gem_make.out"))).to_not include("make -j")
expect(File.read(File.join(gem_two.extension_dir, "gem_make.out"))).to_not include("make -j")
expect(acquired["one"]).to eq(1)
expect(acquired["two"]).to eq(1)
end

it "release the job slots" do
Expand All @@ -186,39 +189,52 @@
end
end

Bundler::ParallelInstaller.call(installer, definition.specs, 3, false, true)
acquired = track_build_jobs do
Bundler::ParallelInstaller.call(installer, definition.specs, 3, false, true)
end

# Take 3 slots out of the 3 available.
expect(File.read(File.join(gem_one.extension_dir, "gem_make.out"))).to include("make -j3")
# Take 3 slots that were released.
expect(File.read(File.join(gem_two.extension_dir, "gem_make.out"))).to include("make -j3")
expect(acquired["one"]).to eq(3)
# Take 3 slots that were released by `one`.
expect(acquired["two"]).to eq(3)
end

def redefine_build_jobs
# Records how many jobserver slots each gem's build acquired. RubyGems turns
# that count directly into `make -jN`, so asserting on it verifies slot
# allocation and release without reading a build log, which a successful
# build no longer writes. With +rendezvous+, "one" grabs its slots first and
# holds them until "two" has grabbed the rest, making the split deterministic.
def track_build_jobs(rendezvous: false)
acquired = {}
old_method = Bundler::RubyGemsGemInstaller.instance_method(:build_jobs)
Bundler::RubyGemsGemInstaller.remove_method(:build_jobs)

# Rendezvous so that "one" grabs its slots first and keeps holding them
# until "two" has grabbed the rest. Blocking on a queue avoids the
# busy-wait and makes the ordering deterministic.
one_acquired = Thread::Queue.new
two_acquired = Thread::Queue.new

Bundler::RubyGemsGemInstaller.define_method(:build_jobs) do
if spec.name == "one"
value = old_method.bind(self).call
one_acquired << true
two_acquired.pop
elsif spec.name == "two"
one_acquired.pop
value = old_method.bind(self).call
two_acquired << true
end
value =
if rendezvous && spec.name == "one"
v = old_method.bind(self).call
one_acquired << true
two_acquired.pop
v
elsif rendezvous && spec.name == "two"
one_acquired.pop
v = old_method.bind(self).call
two_acquired << true
v
else
old_method.bind(self).call
end

acquired[spec.name] = value
value
end

yield

acquired
ensure
Bundler::RubyGemsGemInstaller.remove_method(:build_jobs)
Bundler::RubyGemsGemInstaller.define_method(:build_jobs, old_method)
Expand Down
20 changes: 11 additions & 9 deletions spec/commands/install_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1369,10 +1369,18 @@ def run
s.extensions = extension

s.write(extension, extconf_code)
# A successful build no longer leaves gem_make.out behind. Force the
# build to fail at the make stage so the make command line, including
# the jobserver `-j`, is recorded in build_info for these assertions.
s.write("ext/mypsych/mypsych.c", "#error forced build failure for test")
end
end
end

def gem_make_out
File.read(File.join(@gemspec.build_info_dir, "#{@gemspec.full_name}.gem_make.out"))
end

after do
if @old_makeflags
ENV["MAKEFLAGS"] = @old_makeflags
Expand All @@ -1384,39 +1392,33 @@ def run
it "doesn't pass down -j to make when MAKEFLAGS is set" do
ENV["MAKEFLAGS"] = "-j1"

install_gemfile(<<~G, env: { "BUNDLE_JOBS" => "8" })
install_gemfile(<<~G, env: { "BUNDLE_JOBS" => "8" }, raise_on_error: false)
source "https://gem.repo4"
gem "mypsych"
G

gem_make_out = File.read(File.join(@gemspec.extension_dir, "gem_make.out"))

expect(gem_make_out).not_to include("make -j8")
end

it "uses 3 slots from the available pool when running the compilation of an extension" do
ENV.delete("MAKEFLAGS")

install_gemfile(<<~G, env: { "BUNDLE_JOBS" => "8" })
install_gemfile(<<~G, env: { "BUNDLE_JOBS" => "8" }, raise_on_error: false)
source "https://gem.repo4"
gem "mypsych"
G

gem_make_out = File.read(File.join(@gemspec.extension_dir, "gem_make.out"))

expect(gem_make_out).to include("make -j3")
end

it "consumes 3 slots from the pool when BUNDLE_JOBS isn't set" do
ENV.delete("MAKEFLAGS")

install_gemfile(<<~G)
install_gemfile(<<~G, raise_on_error: false)
source "https://gem.repo4"
gem "mypsych"
G

gem_make_out = File.read(File.join(@gemspec.extension_dir, "gem_make.out"))

expect(gem_make_out).to include("make -j3")
end
end
Expand Down
7 changes: 5 additions & 2 deletions test/rubygems/test_gem_commands_install_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1587,6 +1587,9 @@ def test_pass_down_the_job_option_to_make

write_file(extconf_path) do |io|
io.puts "require 'mkmf'"
# Force the build to fail at the make stage so the build log is
# written. The make command line (including -j) is recorded there.
io.puts "File.write('a.c', '#error forced build failure for test')"
io.puts "create_makefile '#{spec.name}'"
end

Expand All @@ -1595,12 +1598,12 @@ def test_pass_down_the_job_option_to_make
end

use_ui @ui do
assert_raise Gem::MockGemUi::SystemExitException, @ui.error do
assert_raise Gem::MockGemUi::TermError, @ui.error do
@cmd.invoke "a", "-j4"
end
end

gem_make_out = File.read(File.join(gemspec.extension_dir, "gem_make.out"))
gem_make_out = File.read(File.join(gemspec.build_info_dir, "#{gemspec.full_name}.gem_make.out"))
if vc_windows? && nmake_found?
refute_includes(gem_make_out, " -j4")
else
Expand Down
5 changes: 4 additions & 1 deletion test/rubygems/test_gem_commands_update_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,9 @@ def test_pass_down_the_job_option_to_make

write_file(extconf_path) do |io|
io.puts "require 'mkmf'"
# Force the build to fail at the make stage so the build log is
# written. The make command line (including -j) is recorded there.
io.puts "File.write('a.c', '#error forced build failure for test')"
io.puts "create_makefile '#{spec.name}'"
end

Expand All @@ -748,7 +751,7 @@ def test_pass_down_the_job_option_to_make
@cmd.invoke("a", "-j2")
end

gem_make_out = File.read(File.join(gemspec.extension_dir, "gem_make.out"))
gem_make_out = File.read(File.join(gemspec.build_info_dir, "#{gemspec.full_name}.gem_make.out"))
if vc_windows? && nmake_found?
refute_includes(gem_make_out, " -j2")
else
Expand Down
Loading
Loading