Skip to content

Commit de945e1

Browse files
authored
Merge branch 'master' into fix/bundle-exec-prepend-bin-path-for-load-relative-ruby
2 parents 46ef3fa + f72d9d9 commit de945e1

10 files changed

Lines changed: 201 additions & 52 deletions

File tree

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,12 @@ See https://bundler.io/compatibility for known issues.
106106

107107
### Supporting
108108

109-
RubyGems is managed by [Ruby Central](https://rubycentral.org), a non-profit organization that supports the Ruby community through projects like this one, as well as [RubyConf](https://rubyconf.org), [RailsConf](https://railsconf.org), and [RubyGems.org](https://rubygems.org). You can support Ruby Central by attending or [sponsoring](sponsors@rubycentral.org) a conference, or by [joining as a supporting member](https://rubycentral.org/#/portal/signup).
109+
RubyGems is a community project. Please consider sponsoring [individual contributors for their great OSS
110+
work](https://github.com/ruby/rubygems/graphs/contributors).
111+
112+
In addition, Ruby Central administers grant-funded work for improvements to `ruby/rubygems`, as well as running
113+
RubyGems.org (the service). You can support Ruby Central by attending or [sponsoring](mailto:sponsors@rubycentral.org)
114+
a [RubyConf](https://rubyconf.org/), or by [joining as a supporting member](https://rubycentral.org/#/portal/signup).
110115

111116
### Contributing
112117

bundler/README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,12 @@ If you'd like to contribute to Bundler, that's awesome, and we <3 you. We've put
3939

4040
## Supporting
4141

42-
RubyGems is managed by [Ruby Central](https://rubycentral.org), a non-profit organization that supports the Ruby community through projects like this one, as well as [RubyConf](https://rubyconf.org), [RailsConf](https://railsconf.org), and [RubyGems.org](https://rubygems.org). You can support Ruby Central by attending or [sponsoring](sponsors@rubycentral.org) a conference, or by [joining as a supporting member](https://rubycentral.org/#/portal/signup).
42+
RubyGems is a community project. Please consider sponsoring [individual contributors for their great OSS
43+
work](https://github.com/ruby/rubygems/graphs/contributors).
44+
45+
In addition, Ruby Central administers grant-funded work for improvements to `ruby/rubygems`, as well as running
46+
RubyGems.org (the service). You can support Ruby Central by attending or [sponsoring](mailto:sponsors@rubycentral.org)
47+
a [RubyConf](https://rubyconf.org/), or by [joining as a supporting member](https://rubycentral.org/#/portal/signup).
4348

4449
## Code of Conduct
4550

bundler/lib/bundler/definition.rb

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -777,7 +777,25 @@ def start_resolution
777777
end
778778

779779
def precompute_source_requirements_for_indirect_dependencies?
780-
sources.non_global_rubygems_sources.all?(&:dependency_api_available?)
780+
if sources.non_global_rubygems_sources.all?(&:dependency_api_available?)
781+
true
782+
else
783+
non_dependency_api_warning
784+
false
785+
end
786+
end
787+
788+
def non_dependency_api_warning
789+
non_api_sources = sources.non_global_rubygems_sources.reject(&:dependency_api_available?)
790+
non_api_source_names = non_api_sources.map {|d| " * #{d}" }.join("\n")
791+
792+
msg = String.new
793+
msg << "Your Gemfile contains scoped sources that don't implement a dependency API, namely:\n\n"
794+
msg << non_api_source_names
795+
msg << "\n\nUsing the above gem servers may result in installing unexpected gems. " \
796+
"To resolve this warning, make sure you use gem servers that implement dependency APIs, " \
797+
"such as gemstash or geminabox gem servers."
798+
Bundler.ui.warn msg
781799
end
782800

783801
def current_platform_locked?

bundler/lib/bundler/installer/parallel_installer.rb

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
module Bundler
77
class ParallelInstaller
88
class SpecInstallation
9-
attr_accessor :spec, :name, :full_name, :post_install_message, :state, :error
9+
attr_accessor :spec, :name, :full_name, :post_install_message, :state, :error, :dependencies
1010
def initialize(spec)
1111
@spec = spec
1212
@name = spec.name
@@ -46,25 +46,11 @@ def has_post_install_message?
4646
!post_install_message.empty?
4747
end
4848

49-
def ignorable_dependency?(dep)
50-
dep.type == :development || dep.name == @name
51-
end
52-
53-
# Checks installed dependencies against spec's dependencies to make
54-
# sure needed dependencies have been installed.
49+
# Recursively checks that all dependencies (direct and transitive) have been installed.
5550
def dependencies_installed?(installed_specs)
56-
dependencies.all? {|d| installed_specs.include? d.name }
57-
end
58-
59-
# Represents only the non-development dependencies, the ones that are
60-
# itself and are in the total list.
61-
def dependencies
62-
@dependencies ||= all_dependencies.reject {|dep| ignorable_dependency? dep }
63-
end
64-
65-
# Represents all dependencies
66-
def all_dependencies
67-
@spec.dependencies
51+
dependencies.all? do |dep|
52+
installed_specs.include?(dep.name) && dep.dependencies_installed?(installed_specs)
53+
end
6854
end
6955

7056
def to_s
@@ -85,6 +71,12 @@ def initialize(installer, all_specs, size, standalone, force, local: false, skip
8571
@force = force
8672
@local = local
8773
@specs = all_specs.map {|s| SpecInstallation.new(s) }
74+
specs_by_name = @specs.to_h {|s| [s.name, s] }
75+
@specs.each do |spec_install|
76+
spec_install.dependencies = spec_install.spec.dependencies.filter_map do |dep|
77+
specs_by_name[dep.name] unless dep.type == :development || dep.name == spec_install.name
78+
end
79+
end
8880
@specs.each do |spec_install|
8981
spec_install.state = :installed if skip.include?(spec_install.name)
9082
end if skip

bundler/lib/bundler/templates/newgem/newgem.gemspec.tt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ Gem::Specification.new do |spec|
2222
spec.metadata["changelog_uri"] = "<%= config[:changelog_uri] %>"
2323
<%- end -%>
2424

25+
# Uncomment the line below to require MFA for gem pushes.
26+
# This helps protect your gem from supply chain attacks by ensuring
27+
# no one can publish a new version without multi-factor authentication.
28+
# See: https://guides.rubygems.org/mfa-requirement-opt-in/
29+
# spec.metadata["rubygems_mfa_required"] = "true"
30+
2531
# Specify which files should be added to the gem when it is released.
2632
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
2733
gemspec = File.basename(__FILE__)

spec/bundler/definition_spec.rb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,57 @@
289289
end
290290
end
291291

292+
describe "#precompute_source_requirements_for_indirect_dependencies?" do
293+
before do
294+
allow(Bundler::SharedHelpers).to receive(:find_gemfile) { Pathname.new("Gemfile") }
295+
end
296+
297+
let(:sources) { Bundler::SourceList.new }
298+
subject { Bundler::Definition.new(nil, [], sources, []) }
299+
300+
before do
301+
allow(sources).to receive(:non_global_rubygems_sources).and_return(non_global_rubygems_sources)
302+
end
303+
304+
context "when all the scoped sources implement a dependency API" do
305+
let(:non_global_rubygems_sources) do
306+
[
307+
double("non-global-source-0", "dependency_api_available?":true, to_s:"a"),
308+
double("non-global-source-1", "dependency_api_available?":true, to_s:"b"),
309+
]
310+
end
311+
312+
it "returns true without warning" do
313+
expect(subject).not_to receive(:non_dependency_api_warning)
314+
315+
expect(subject.send(:precompute_source_requirements_for_indirect_dependencies?)).to be_truthy
316+
end
317+
end
318+
319+
context "when some scoped sources do not implement a dependency API" do
320+
let(:non_global_rubygems_sources) do
321+
[
322+
double("non-global-source-0", "dependency_api_available?":true, to_s:"a"),
323+
double("non-global-source-1", "dependency_api_available?":false, to_s:"b"),
324+
double("non-global-source-2", "dependency_api_available?":false, to_s:"c"),
325+
]
326+
end
327+
328+
it "returns false and warns about the non-API sources" do
329+
expect(Bundler.ui).to receive(:warn).with(<<-W.strip)
330+
Your Gemfile contains scoped sources that don't implement a dependency API, namely:
331+
332+
* b
333+
* c
334+
335+
Using the above gem servers may result in installing unexpected gems. To resolve this warning, make sure you use gem servers that implement dependency APIs, such as gemstash or geminabox gem servers.
336+
W
337+
338+
expect(subject.send(:precompute_source_requirements_for_indirect_dependencies?)).to be_falsy
339+
end
340+
end
341+
end
342+
292343
def mock_source_list
293344
Class.new do
294345
def all_sources

spec/bundler/installer/spec_installation_spec.rb

Lines changed: 50 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,17 @@
33
require "bundler/installer/parallel_installer"
44

55
RSpec.describe Bundler::ParallelInstaller::SpecInstallation do
6-
let!(:dep) do
7-
a_spec = Object.new
8-
def a_spec.name
9-
"I like tests"
10-
end
11-
12-
def a_spec.full_name
13-
"I really like tests"
14-
end
15-
a_spec
6+
def build_spec(name, extensions: [])
7+
spec = Object.new
8+
spec.define_singleton_method(:name) { name }
9+
spec.define_singleton_method(:full_name) { "#{name}-1.0" }
10+
spec.define_singleton_method(:extensions) { extensions }
11+
spec.define_singleton_method(:dependencies) { [] }
12+
spec
1613
end
1714

15+
let!(:dep) { build_spec("I like tests") }
16+
1817
describe "#ready_to_enqueue?" do
1918
context "when in enqueued state" do
2019
it "is falsey" do
@@ -39,29 +38,51 @@ def a_spec.full_name
3938
end
4039

4140
describe "#dependencies_installed?" do
42-
context "when all dependencies are installed" do
43-
it "returns true" do
44-
dependencies = []
45-
dependencies << instance_double("SpecInstallation", spec: "alpha", name: "alpha", installed?: true, all_dependencies: [], type: :production)
46-
dependencies << instance_double("SpecInstallation", spec: "beta", name: "beta", installed?: true, all_dependencies: [], type: :production)
47-
all_specs = dependencies + [instance_double("SpecInstallation", spec: "gamma", name: "gamma", installed?: false, all_dependencies: [], type: :production)]
41+
it "returns true when all dependencies are installed" do
42+
alpha = described_class.new(build_spec("alpha"))
43+
alpha.dependencies = []
44+
45+
beta = described_class.new(build_spec("beta"))
46+
beta.dependencies = [alpha]
47+
48+
gamma = described_class.new(build_spec("gamma"))
49+
gamma.dependencies = [beta]
50+
51+
expect(gamma.dependencies_installed?({})).to be_falsey
52+
expect(gamma.dependencies_installed?({ "beta" => true })).to be_falsey
53+
expect(gamma.dependencies_installed?({ "alpha" => true, "beta" => true })).to be_truthy
54+
end
55+
end
56+
57+
describe "#ready_to_install?" do
58+
context "when spec has no extensions" do
59+
it "returns true regardless of dependencies" do
60+
beta = described_class.new(build_spec("beta"))
61+
beta.dependencies = []
62+
4863
spec = described_class.new(dep)
49-
allow(spec).to receive(:all_dependencies).and_return(dependencies)
50-
installed_specs = all_specs.select(&:installed?).map {|s| [s.name, true] }.to_h
51-
expect(spec.dependencies_installed?(installed_specs)).to be_truthy
64+
spec.state = :downloaded
65+
spec.dependencies = [beta]
66+
67+
expect(spec.ready_to_install?({})).to be_truthy
5268
end
5369
end
5470

55-
context "when all dependencies are not installed" do
56-
it "returns false" do
57-
dependencies = []
58-
dependencies << instance_double("SpecInstallation", spec: "alpha", name: "alpha", installed?: false, all_dependencies: [], type: :production)
59-
dependencies << instance_double("SpecInstallation", spec: "beta", name: "beta", installed?: true, all_dependencies: [], type: :production)
60-
all_specs = dependencies + [instance_double("SpecInstallation", spec: "gamma", name: "gamma", installed?: false, all_dependencies: [], type: :production)]
61-
spec = described_class.new(dep)
62-
allow(spec).to receive(:all_dependencies).and_return(dependencies)
63-
installed_specs = all_specs.select(&:installed?).map {|s| [s.name, true] }.to_h
64-
expect(spec.dependencies_installed?(installed_specs)).to be_falsey
71+
context "when spec has extensions" do
72+
it "returns true when all dependencies are installed" do
73+
alpha = described_class.new(build_spec("alpha"))
74+
alpha.dependencies = []
75+
76+
beta = described_class.new(build_spec("beta"))
77+
beta.dependencies = [alpha]
78+
79+
gamma = described_class.new(build_spec("gamma", extensions: ["ext/Rakefile"]))
80+
gamma.state = :downloaded
81+
gamma.dependencies = [beta]
82+
83+
expect(gamma.ready_to_install?({})).to be_falsey
84+
expect(gamma.ready_to_install?({ "beta" => true })).to be_falsey
85+
expect(gamma.ready_to_install?({ "alpha" => true, "beta" => true })).to be_truthy
6586
end
6687
end
6788
end

spec/commands/install_spec.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1379,6 +1379,48 @@ def run
13791379
end
13801380
end
13811381

1382+
describe "when a native extension requires a transitive dependency at build time" do
1383+
before do
1384+
build_repo4 do
1385+
build_gem "alpha", "1.0.0" do |s|
1386+
extension = "ext/alpha/extconf.rb"
1387+
s.extensions = extension
1388+
s.write(extension, <<~CODE)
1389+
require "mkmf"
1390+
sleep 1
1391+
create_makefile("alpha")
1392+
CODE
1393+
s.write "lib/alpha.rb", "ALPHA = '1.0.0'"
1394+
end
1395+
1396+
build_gem "beta", "1.0.0" do |s|
1397+
s.add_dependency "alpha"
1398+
s.write "lib/beta.rb", "require 'alpha'\nBETA = '1.0.0'"
1399+
end
1400+
1401+
build_gem "gamma", "1.0.0" do |s|
1402+
s.add_dependency "beta"
1403+
extension = "ext/gamma/extconf.rb"
1404+
s.extensions = extension
1405+
s.write(extension, <<~EXTCONF)
1406+
require "beta"
1407+
require "mkmf"
1408+
create_makefile("gamma")
1409+
EXTCONF
1410+
end
1411+
end
1412+
end
1413+
1414+
it "installs successfully" do
1415+
install_gemfile <<~G
1416+
source "https://gem.repo4"
1417+
gem "gamma"
1418+
G
1419+
1420+
expect(the_bundle).to include_gems "alpha 1.0.0", "beta 1.0.0", "gamma 1.0.0"
1421+
end
1422+
end
1423+
13821424
describe "when configured path is UTF-8 and a file inside a gem package too" do
13831425
let(:app_path) do
13841426
path = tmp("♥")

spec/commands/newgem_spec.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,15 @@ def create_temporary_dir(dir)
650650
to match(/example\.com/)
651651
end
652652

653+
it "includes a commented-out rubygems_mfa_required metadata hint" do
654+
bundle "gem #{gem_name}"
655+
656+
gemspec_contents = bundled_app("#{gem_name}/#{gem_name}.gemspec").read
657+
658+
expect(gemspec_contents).to include('# spec.metadata["rubygems_mfa_required"] = "true"')
659+
expect(gemspec_contents).to include("https://guides.rubygems.org/mfa-requirement-opt-in/")
660+
end
661+
653662
it "sets a minimum ruby version" do
654663
bundle "gem #{gem_name}"
655664

spec/realworld/fixtures/tapioca/Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ GEM
3232
thor (>= 1.2.0)
3333
yard-sorbet
3434
thor (1.4.0)
35-
yard (0.9.37)
35+
yard (0.9.42)
3636
yard-sorbet (0.9.0)
3737
sorbet-runtime
3838
yard

0 commit comments

Comments
 (0)