Files
Cody CutrerandDiogo Fernandes 0e7a46fb3c [ruby/rubygems] Fix plugin installation from gemfile
Several things are fixed:
 * Don't re-install a plugin referenced from the gemfile with every
   call to `bundle install`
 * If the version of a plugin referenced in the gemfile conflicts
   with what's in the plugin index, _do_ re-install it
 * If plugins aren't installed yet, don't throw cryptic errors from
   commands that don't implicitly install gems, such as
   `bundle check` and `bundle info`. This also applies if the
   plugin index references a system gem, and that gem is removed.

This is all accomplished by actuallying including plugins as
regular dependencies in the Gemfile, so that they end up in the
lockfile, and then just using the regular lockfile with the
plugins-only pass of the gemfile in the Plugin infrastructure.
This also means that non-specific version constraints can be
used for plugins, and you can update them with
`bundle update <plugin>` just like any other gem.

https://github.com/ruby/rubygems/commit/5bb065ddab

Co-authored-by: Diogo Fernandes <diogofernandesop@gmail.com>
2026-06-11 08:16:25 +00:00

463 lines
12 KiB
Ruby

# frozen_string_literal: true
RSpec.describe "real source plugins" do
context "with a minimal source plugin" do
before do
build_repo2 do
build_plugin "bundler-source-mpath" do |s|
s.write "plugins.rb", <<-RUBY
require "bundler-source-mpath"
class MPath < Bundler::Plugin::API
source "mpath"
attr_reader :path
def initialize(opts)
super
@path = Pathname.new options["uri"]
end
def fetch_gemspec_files
@spec_files ||= begin
glob = "{,*,*/*}.gemspec"
if installed?
search_path = install_path
else
search_path = path
end
Dir["\#{search_path.to_s}/\#{glob}"]
end
end
def install(spec, opts)
mkdir_p(install_path.parent)
require 'fileutils'
FileUtils.cp_r(path, install_path)
spec_path = install_path.join("\#{spec.full_name}.gemspec")
spec_path.open("wb") {|f| f.write spec.to_ruby }
spec.loaded_from = spec_path.to_s
post_install(spec)
nil
end
end
RUBY
end # build_plugin
end
build_lib "a-path-gem"
gemfile <<-G
source "https://gem.repo2" # plugin source
source "#{lib_path("a-path-gem-1.0")}", :type => :mpath do
gem "a-path-gem"
end
G
end
it "installs" do
bundle "install"
expect(out).to include("Bundle complete!")
expect(the_bundle).to include_gems("a-path-gem 1.0")
end
it "writes to lockfile" do
bundle "install"
checksums = checksums_section_when_enabled do |c|
c.no_checksum "a-path-gem", "1.0"
c.checksum gem_repo2, "bundler-source-mpath", "1.0"
end
expect(lockfile).to eq <<~G
PLUGIN SOURCE
remote: #{lib_path("a-path-gem-1.0")}
type: mpath
specs:
a-path-gem (1.0)
GEM
remote: https://gem.repo2/
specs:
bundler-source-mpath (1.0)
PLATFORMS
#{lockfile_platforms}
DEPENDENCIES
a-path-gem!
bundler-source-mpath
#{checksums}
BUNDLED WITH
#{Bundler::VERSION}
G
end
it "provides correct #full_gem_path" do
bundle "install"
run <<-RUBY
puts Bundler.rubygems.find_name('a-path-gem').first.full_gem_path
RUBY
expect(out).to eq(bundle("info a-path-gem --path"))
end
it "installs the gem executables" do
build_lib "gem_with_bin" do |s|
s.executables = ["foo"]
end
install_gemfile <<-G
source "https://gem.repo2" # plugin source
source "#{lib_path("gem_with_bin-1.0")}", :type => :mpath do
gem "gem_with_bin"
end
G
bundle "exec foo"
expect(out).to eq("1.0")
end
describe "bundle cache/package" do
let(:uri_hash) { Digest(:SHA1).hexdigest(lib_path("a-path-gem-1.0").to_s) }
it "copies repository to vendor cache and uses it" do
bundle "install"
bundle :cache
expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}")).to exist
expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}/.git")).not_to exist
expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}/.bundlecache")).to be_file
FileUtils.rm_r lib_path("a-path-gem-1.0")
expect(the_bundle).to include_gems("a-path-gem 1.0")
end
it "copies repository to vendor cache and uses it even when installed with `path` configured" do
bundle_config "path vendor/bundle"
bundle :install
bundle :cache
expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}")).to exist
FileUtils.rm_r lib_path("a-path-gem-1.0")
expect(the_bundle).to include_gems("a-path-gem 1.0")
end
it "bundler package copies repository to vendor cache" do
bundle_config "path vendor/bundle"
bundle :install
bundle :cache
expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}")).to exist
FileUtils.rm_r lib_path("a-path-gem-1.0")
expect(the_bundle).to include_gems("a-path-gem 1.0")
end
end
context "with lockfile" do
before do
lockfile <<-G
PLUGIN SOURCE
remote: #{lib_path("a-path-gem-1.0")}
type: mpath
specs:
a-path-gem (1.0)
GEM
remote: https://gem.repo2/
specs:
PLATFORMS
#{generic_local_platform}
DEPENDENCIES
a-path-gem!
BUNDLED WITH
#{Bundler::VERSION}
G
end
it "installs" do
bundle "install"
expect(the_bundle).to include_gems("a-path-gem 1.0")
end
end
end
context "with a more elaborate source plugin" do
before do
build_repo2 do
build_plugin "bundler-source-gitp" do |s|
s.write "plugins.rb", <<-RUBY
require "open3"
class SPlugin < Bundler::Plugin::API
source "gitp"
attr_reader :ref
def initialize(opts)
super
@ref = options["ref"] || options["branch"] || options["tag"] || "main"
@unlocked = false
end
def eql?(other)
other.is_a?(self.class) && uri == other.uri && ref == other.ref
end
alias_method :==, :eql?
def fetch_gemspec_files
@spec_files ||= begin
glob = "{,*,*/*}.gemspec"
if !cached?
cache_repo
end
if installed? && !@unlocked
path = install_path
else
path = cache_path
end
Dir["\#{path}/\#{glob}"]
end
end
def install(spec, opts)
mkdir_p(install_path.dirname)
rm_rf(install_path)
`git clone --no-checkout --quiet "\#{cache_path}" "\#{install_path}"`
Open3.capture2e("git reset --hard \#{revision}", :chdir => install_path)
spec_path = install_path.join("\#{spec.full_name}.gemspec")
spec_path.open("wb") {|f| f.write spec.to_ruby }
spec.loaded_from = spec_path.to_s
post_install(spec)
nil
end
def options_to_lock
opts = {"revision" => revision}
opts["ref"] = ref if ref != "main"
opts
end
def unlock!
@unlocked = true
@revision = latest_revision
end
def app_cache_dirname
"\#{base_name}-\#{shortref_for_path(revision)}"
end
private
def cache_path
@cache_path ||= cache_dir.join("gitp", base_name)
end
def cache_repo
`git clone --quiet \#{@options["uri"]} \#{cache_path}`
end
def cached?
File.directory?(cache_path)
end
def locked_revision
options["revision"]
end
def revision
@revision ||= locked_revision || latest_revision
end
def latest_revision
if !cached? || @unlocked
rm_rf(cache_path)
cache_repo
end
output, _status = Open3.capture2e("git rev-parse --verify \#{@ref}", :chdir => cache_path)
output.strip
end
def base_name
File.basename(uri.sub(%r{^(\w+://)?([^/:]+:)?(//\w*/)?(\w*/)*}, ""), ".git")
end
def shortref_for_path(ref)
ref[0..11]
end
def install_path
@install_path ||= begin
git_scope = "\#{base_name}-\#{shortref_for_path(revision)}"
gem_install_dir.join(git_scope)
end
end
def installed?
File.directory?(install_path)
end
end
RUBY
end
end
build_git "ma-gitp-gem"
gemfile <<-G
source "https://gem.repo2" # plugin source
source "#{lib_path("ma-gitp-gem-1.0")}", :type => :gitp do
gem "ma-gitp-gem"
end
G
end
it "handles the source option" do
bundle "install"
expect(out).to include("Bundle complete!")
expect(the_bundle).to include_gems("ma-gitp-gem 1.0")
end
it "writes to lockfile" do
revision = revision_for(lib_path("ma-gitp-gem-1.0"))
bundle "install"
checksums = checksums_section_when_enabled do |c|
c.checksum gem_repo2, "bundler-source-gitp", "1.0"
c.no_checksum "ma-gitp-gem", "1.0"
end
expect(lockfile).to eq <<~G
PLUGIN SOURCE
remote: #{lib_path("ma-gitp-gem-1.0")}
type: gitp
revision: #{revision}
specs:
ma-gitp-gem (1.0)
GEM
remote: https://gem.repo2/
specs:
bundler-source-gitp (1.0)
PLATFORMS
#{lockfile_platforms}
DEPENDENCIES
bundler-source-gitp
ma-gitp-gem!
#{checksums}
BUNDLED WITH
#{Bundler::VERSION}
G
end
context "with lockfile" do
before do
revision = revision_for(lib_path("ma-gitp-gem-1.0"))
lockfile <<-G
PLUGIN SOURCE
remote: #{lib_path("ma-gitp-gem-1.0")}
type: gitp
revision: #{revision}
specs:
ma-gitp-gem (1.0)
GEM
remote: https://gem.repo2/
specs:
PLATFORMS
#{generic_local_platform}
DEPENDENCIES
ma-gitp-gem!
BUNDLED WITH
#{Bundler::VERSION}
G
end
it "installs" do
bundle "install"
expect(the_bundle).to include_gems("ma-gitp-gem 1.0")
end
it "uses the locked ref" do
update_git "ma-gitp-gem"
bundle "install"
run <<-RUBY
require 'ma/gitp/gem'
puts "WIN" unless defined?(MAGITPGEM_PREV_REF)
RUBY
expect(out).to eq("WIN")
end
it "updates the deps on bundler update" do
update_git "ma-gitp-gem"
bundle "update ma-gitp-gem"
run <<-RUBY
require 'ma/gitp/gem'
puts "WIN" if defined?(MAGITPGEM_PREV_REF)
RUBY
expect(out).to eq("WIN")
end
it "updates the deps on change in gemfile" do
update_git "ma-gitp-gem", "1.1", path: lib_path("ma-gitp-gem-1.0"), gemspec: true
gemfile <<-G
source "https://gem.repo2" # plugin source
source "#{lib_path("ma-gitp-gem-1.0")}", :type => :gitp do
gem "ma-gitp-gem", "1.1"
end
G
bundle "install"
expect(the_bundle).to include_gems("ma-gitp-gem 1.1")
end
end
describe "bundle cache with gitp" do
it "copies repository to vendor cache and uses it" do
git = build_git "foo"
ref = git.ref_for("main", 11)
install_gemfile <<-G
source "https://gem.repo2" # plugin source
source '#{lib_path("foo-1.0")}', :type => :gitp do
gem "foo"
end
G
bundle :cache
expect(bundled_app("vendor/cache/foo-1.0-#{ref}")).to exist
expect(bundled_app("vendor/cache/foo-1.0-#{ref}/.git")).not_to exist
expect(bundled_app("vendor/cache/foo-1.0-#{ref}/.bundlecache")).to be_file
FileUtils.rm_r lib_path("foo-1.0")
expect(the_bundle).to include_gems "foo 1.0"
end
end
end
end