Skip to content
6 changes: 3 additions & 3 deletions lib/bundler/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -627,15 +627,15 @@ def inject(*)
end

desc "lock", "Creates a lockfile without installing"
method_option "update", type: :array, lazy_default: true, banner: "ignore the existing lockfile, update all gems by default, or update list of given gems"
method_option "update", type: :array, lazy_default: true, repeatable: true, banner: "ignore the existing lockfile, update all gems by default, or update list of given gems"
method_option "local", type: :boolean, default: false, banner: "do not attempt to fetch remote gemspecs and use the local gem cache only"
method_option "print", type: :boolean, default: false, banner: "print the lockfile to STDOUT instead of writing to the file system"
method_option "gemfile", type: :string, banner: "Use the specified gemfile instead of Gemfile"
method_option "lockfile", type: :string, default: nil, banner: "the path the lockfile should be written to"
method_option "full-index", type: :boolean, default: false, banner: "Fall back to using the single-file index of all gems"
method_option "add-checksums", type: :boolean, default: false, banner: "Adds checksums to the lockfile"
method_option "add-platform", type: :array, default: [], banner: "Add a new platform to the lockfile"
method_option "remove-platform", type: :array, default: [], banner: "Remove a platform from the lockfile"
method_option "add-platform", type: :array, default: [], repeatable: true, banner: "Add a new platform to the lockfile"
method_option "remove-platform", type: :array, default: [], repeatable: true, banner: "Remove a platform from the lockfile"
method_option "normalize-platforms", type: :boolean, default: false, banner: "Normalize lockfile platforms"
method_option "patch", type: :boolean, banner: "If updating, prefer updating only to next patch version"
method_option "minor", type: :boolean, banner: "If updating, prefer updating only to next minor version"
Expand Down
7 changes: 5 additions & 2 deletions lib/bundler/cli/lock.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ def run
Bundler::Fetcher.disable_endpoint = options["full-index"]

update = options[:update]
# --update is repeatable, so it parses as an array with one entry per
# occurrence, where a bare `--update` produces a `true` entry
update = update.include?(true) ? true : update.flatten if update.is_a?(Array)
conservative = options[:conservative]
bundler = options[:bundler]

Expand All @@ -44,12 +47,12 @@ def run

Bundler::CLI::Common.configure_gem_version_promoter(definition, options) if options[:update]

options["remove-platform"].each do |platform_string|
options["remove-platform"].flatten.each do |platform_string|
platform = Gem::Platform.new(platform_string)
definition.remove_platform(platform)
end

options["add-platform"].each do |platform_string|
options["add-platform"].flatten.each do |platform_string|
platform = Gem::Platform.new(platform_string)
if platform.to_s == "unknown"
Bundler.ui.error "The platform `#{platform_string}` is unknown to RubyGems and can't be added to the lockfile."
Expand Down
8 changes: 5 additions & 3 deletions lib/bundler/definition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ def write_lock(file, preserve_unknown_sections)
updating_major = locked_major < current_major
end

preserve_unknown_sections ||= !updating_major && (Bundler.frozen_bundle? || !(unlocking? || @unlocking_bundler))
preserve_unknown_sections ||= Bundler.frozen_bundle? || (!updating_major && !(unlocking? || @unlocking_bundler))

if File.exist?(file) && lockfiles_equal?(@lockfile_contents, contents, preserve_unknown_sections)
return if Bundler.frozen_bundle?
Expand All @@ -425,8 +425,10 @@ def write_lock(file, preserve_unknown_sections)
end

if Bundler.frozen_bundle?
Bundler.ui.error "Cannot write a changed lockfile while frozen."
return
msg = lockfile_changes_summary("frozen mode is set") ||
"Your lockfile needs to be updated, but it can't be because frozen mode is set.\n\n" \
"Run `bundle install` elsewhere and add the updated #{SharedHelpers.relative_lockfile_path} to version control."
raise ProductionError, msg
end

# Convert to \r\n if the existing lock has them, i.e., Windows with
Expand Down
16 changes: 16 additions & 0 deletions lib/bundler/lockfile_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ def add_section(name, value)
end

def bundler_checksum
# In frozen mode the lockfile can't change, so reproduce whatever bundler
# entry is already locked instead of recording one for the running bundler
# version, which may legitimately differ from the locked one.
return locked_bundler_checksum if Bundler.frozen_bundle?

# `.dev` versions and `SKIP_BUNDLER_CHECKSUM` are deliberate opt-outs (used
# by Bundler/RubyGems' own development and release tasks): never record a
# checksum for Bundler itself in those cases.
Expand Down Expand Up @@ -138,5 +143,16 @@ def bundled_with_changing?

locked_gems.bundler_version != definition.bundler_version_to_lock
end

def locked_bundler_checksum
locked_version = definition.locked_gems&.bundler_version
return [] unless locked_version

metadata_source = definition.sources.metadata_source
locked_spec = LazySpecification.new("bundler", locked_version, Gem::Platform::RUBY, metadata_source)
return [] if metadata_source.checksum_store.missing?(locked_spec)

[metadata_source.checksum_store.to_lock(locked_spec)]
end
end
end
2 changes: 1 addition & 1 deletion spec/bundler/cache/git_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@
G
lockfile <<~L
GIT
remote: #{git_path}/
remote: #{git_path}
revision: #{locked_revision}
specs:
foo (1.0)
Expand Down
4 changes: 2 additions & 2 deletions spec/bundler/commands/cache_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -349,15 +349,15 @@
GEM
remote: https://gem.repo4/
specs:
foo (1.0.0)
bar (1.0.0)
foo (1.0.0)

PLATFORMS
#{lockfile_platforms}

DEPENDENCIES
foo
bar
foo

BUNDLED WITH
#{Bundler::VERSION}
Expand Down
65 changes: 65 additions & 0 deletions spec/bundler/commands/lock_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,49 @@
expect(read_lockfile).to eq(expected_lockfile)
end

it "updates gems given through repeated --update options" do
build_repo4 do
build_gem "foo", "1.0"
build_gem "foo", "2.0"
build_gem "bar", "1.0"
build_gem "bar", "2.0"
build_gem "baz", "1.0"
build_gem "baz", "2.0"
end

gemfile <<-G
source "https://gem.repo4"

gem "foo"
gem "bar"
gem "baz"
G

lockfile <<~L
GEM
remote: https://gem.repo4/
specs:
bar (1.0)
baz (1.0)
foo (1.0)

PLATFORMS
#{lockfile_platforms}

DEPENDENCIES
bar
baz
foo

BUNDLED WITH
#{Bundler::VERSION}
L

bundle "lock --update foo --update bar"

expect(lockfile).to include("foo (2.0)", "bar (2.0)", "baz (1.0)")
end

it "updates specific gems using --update, even if that requires unlocking other top level gems" do
build_repo4 do
build_gem "prism", "0.15.1"
Expand Down Expand Up @@ -853,6 +896,15 @@
expect(the_bundle.locked_platforms).to match_array(default_platform_list("java", "x86-mingw32"))
end

it "supports adding platforms through repeated --add-platform options" do
gemfile_with_rails_weakling_and_foo_from_repo4

bundle "lock --add-platform java --add-platform x86-mingw32"

allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
expect(the_bundle.locked_platforms).to match_array(default_platform_list("java", "x86-mingw32"))
end

it "supports adding new platforms when a previous lockfile exists" do
gemfile_with_rails_weakling_and_foo_from_repo4

Expand Down Expand Up @@ -975,6 +1027,19 @@
expect(the_bundle.locked_platforms).to match_array(default_platform_list("x86-mingw32"))
end

it "supports removing platforms through repeated --remove-platform options" do
gemfile_with_rails_weakling_and_foo_from_repo4

bundle "lock --add-platform java x86-mingw32"

allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
expect(the_bundle.locked_platforms).to match_array(default_platform_list("java", "x86-mingw32"))

bundle "lock --remove-platform java --remove-platform x86-mingw32"

expect(the_bundle.locked_platforms).to match_array(default_platform_list)
end

it "also cleans up redundant platform gems when removing platforms" do
build_repo4 do
build_gem "nokogiri", "1.12.0"
Expand Down
69 changes: 69 additions & 0 deletions spec/bundler/install/deploy_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,75 @@
end.not_to change { bundled_app_lock.mtime }
end

it "explodes if regenerating the lockfile would change it" do
lockfile lockfile.
sub(" myrack (1.0.0)", " myrack-obama (1.0)\n myrack (1.0.0)").
sub(/^ myrack \(1\.0\.0\) sha256=\S+$/) {|line| "#{line}\n #{checksum_to_lock(gem_repo1, "myrack-obama", "1.0")}" }

bundle :install, env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false
expect(err).to include("Your lockfile needs to be updated, but it can't be because frozen mode is set")
expect(last_command).to be_failure
end

it "explodes on `bundle check` if the lockfile contains a gem bundler would prune" do
lockfile lockfile.sub(" myrack (1.0.0)", " myrack (1.0.0)\n myrack-obama (1.0)")

bundle :check, env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false
expect(err).to include("but can't be updated because frozen mode is set")
expect(last_command).to be_failure
end

it "works when the lockfile includes a checksum entry for bundler itself" do
lockfile <<~L
GEM
remote: https://gem.repo1/
specs:
myrack (1.0.0)

PLATFORMS
#{lockfile_platforms}

DEPENDENCIES
myrack

CHECKSUMS
bundler (#{Bundler::VERSION}) sha256=#{"a" * 64}
#{checksum_to_lock gem_repo1, "myrack", "1.0.0"}

BUNDLED WITH
#{Bundler::VERSION}
L

bundle :install, env: { "BUNDLE_FROZEN" => "true" }
expect(err).to be_empty
end

it "explodes if the lockfile checksum entry for bundler does not match the BUNDLED WITH version" do
lockfile <<~L
GEM
remote: https://gem.repo1/
specs:
myrack (1.0.0)

PLATFORMS
#{lockfile_platforms}

DEPENDENCIES
myrack

CHECKSUMS
bundler (4.0.16) sha256=#{"a" * 64}
#{checksum_to_lock gem_repo1, "myrack", "1.0.0"}

BUNDLED WITH
#{Bundler::VERSION}
L

bundle :install, env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false
expect(err).to include("Your lockfile needs to be updated, but it can't be because frozen mode is set")
expect(last_command).to be_failure
end

it "explodes with the `deployment` setting if you make a change and don't check in the lockfile" do
gemfile <<-G
source "https://gem.repo1"
Expand Down
2 changes: 1 addition & 1 deletion spec/bundler/install/gemfile/platform_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
it "pulls the pure ruby version on jruby if the java platform is not present in the lockfile and bundler is run in frozen mode", :jruby_only do
lockfile <<-G
GEM
remote: https://gem.repo1
remote: https://gem.repo1/
specs:
platform_specific (1.0)

Expand Down
2 changes: 1 addition & 1 deletion transcode.c
Original file line number Diff line number Diff line change
Expand Up @@ -3013,7 +3013,7 @@ econv_free(void *ptr)
static size_t
econv_memsize(const void *ptr)
{
return sizeof(rb_econv_t);
return ptr ? rb_econv_memsize((rb_econv_t *)ptr) : 0;
}

static const rb_data_type_t econv_data_type = {
Expand Down
Loading