ruby/lib/bundler/lockfile_parser.rb
Thong Kuah ad08674d8d
[rubygems/rubygems] Add CHECKSUMS for each gem in lockfile
We lock the checksum for each resolved spec under a new CHECKSUMS
section in the lockfile.

If the locked spec does not resolve for the local platform, we preserve
the locked checksum, similar to how we preserve specs.

Checksum locking only makes sense on install. The compact index
information is only available then.

bde37ca6bf
2023-10-23 13:59:01 +09:00

239 lines
7.4 KiB
Ruby

# frozen_string_literal: true
module Bundler
class LockfileParser
attr_reader :sources, :dependencies, :specs, :platforms, :bundler_version, :ruby_version, :checksums
BUNDLED = "BUNDLED WITH"
DEPENDENCIES = "DEPENDENCIES"
CHECKSUMS = "CHECKSUMS"
PLATFORMS = "PLATFORMS"
RUBY = "RUBY VERSION"
GIT = "GIT"
GEM = "GEM"
PATH = "PATH"
PLUGIN = "PLUGIN SOURCE"
SPECS = " specs:"
OPTIONS = /^ ([a-z]+): (.*)$/i.freeze
SOURCE = [GIT, GEM, PATH, PLUGIN].freeze
SECTIONS_BY_VERSION_INTRODUCED = {
Gem::Version.create("1.0") => [DEPENDENCIES, PLATFORMS, GIT, GEM, PATH].freeze,
Gem::Version.create("1.10") => [BUNDLED].freeze,
Gem::Version.create("1.12") => [RUBY].freeze,
Gem::Version.create("1.13") => [PLUGIN].freeze,
Gem::Version.create("2.4.0") => [CHECKSUMS].freeze,
}.freeze
KNOWN_SECTIONS = SECTIONS_BY_VERSION_INTRODUCED.values.flatten!.freeze
ENVIRONMENT_VERSION_SECTIONS = [BUNDLED, RUBY].freeze
deprecate_constant(:ENVIRONMENT_VERSION_SECTIONS)
def self.sections_in_lockfile(lockfile_contents)
sections = lockfile_contents.scan(/^\w[\w ]*$/)
sections.uniq!
sections
end
def self.unknown_sections_in_lockfile(lockfile_contents)
sections_in_lockfile(lockfile_contents) - KNOWN_SECTIONS
end
def self.sections_to_ignore(base_version = nil)
base_version &&= base_version.release
base_version ||= Gem::Version.create("1.0")
attributes = []
SECTIONS_BY_VERSION_INTRODUCED.each do |version, introduced|
next if version <= base_version
attributes += introduced
end
attributes
end
def self.bundled_with
lockfile = Bundler.default_lockfile
return unless lockfile.file?
lockfile_contents = Bundler.read_file(lockfile)
return unless lockfile_contents.include?(BUNDLED)
lockfile_contents.split(BUNDLED).last.strip
end
def initialize(lockfile)
@platforms = []
@sources = []
@dependencies = {}
@parse_method = nil
@checksums = []
@specs = {}
if lockfile.match?(/<<<<<<<|=======|>>>>>>>|\|\|\|\|\|\|\|/)
raise LockfileError, "Your lockfile contains merge conflicts.\n" \
"Run `git checkout HEAD -- #{SharedHelpers.relative_lockfile_path}` first to get a clean lock."
end
lockfile.split(/(?:\r?\n)+/) do |line|
if SOURCE.include?(line)
@parse_method = :parse_source
parse_source(line)
elsif line == DEPENDENCIES
@parse_method = :parse_dependency
elsif line == CHECKSUMS
@parse_method = :parse_checksum
elsif line == PLATFORMS
@parse_method = :parse_platform
elsif line == RUBY
@parse_method = :parse_ruby
elsif line == BUNDLED
@parse_method = :parse_bundled_with
elsif /^[^\s]/.match?(line)
@parse_method = nil
elsif @parse_method
send(@parse_method, line)
end
end
@specs = @specs.values.sort_by!(&:full_name)
rescue ArgumentError => e
Bundler.ui.debug(e)
raise LockfileError, "Your lockfile is unreadable. Run `rm #{SharedHelpers.relative_lockfile_path}` " \
"and then `bundle install` to generate a new lockfile."
end
def may_include_redundant_platform_specific_gems?
bundler_version.nil? || bundler_version < Gem::Version.new("1.16.2")
end
private
TYPES = {
GIT => Bundler::Source::Git,
GEM => Bundler::Source::Rubygems,
PATH => Bundler::Source::Path,
PLUGIN => Bundler::Plugin,
}.freeze
def parse_source(line)
case line
when SPECS
return unless TYPES.key?(@type)
@current_source = TYPES[@type].from_lock(@opts)
@sources << @current_source
when OPTIONS
value = $2
value = true if value == "true"
value = false if value == "false"
key = $1
if @opts[key]
@opts[key] = Array(@opts[key])
@opts[key] << value
else
@opts[key] = value
end
when *SOURCE
@current_source = nil
@opts = {}
@type = line
else
parse_spec(line)
end
end
space = / /
NAME_VERSION = /
^(#{space}{2}|#{space}{4}|#{space}{6})(?!#{space}) # Exactly 2, 4, or 6 spaces at the start of the line
(.*?) # Name
(?:#{space}\(([^-]*) # Space, followed by version
(?:-(.*))?\))? # Optional platform
(!)? # Optional pinned marker
(?:#{space}(.*))? # Optional checksum
$ # Line end
/xo.freeze
def parse_dependency(line)
return unless line =~ NAME_VERSION
spaces = $1
return unless spaces.size == 2
name = -$2
version = $3
pinned = $5
version = version.split(",").each(&:strip!) if version
dep = Bundler::Dependency.new(name, version)
if pinned && dep.name != "bundler"
spec = @specs.find {|_, v| v.name == dep.name }
dep.source = spec.last.source if spec
# Path sources need to know what the default name / version
# to use in the case that there are no gemspecs present. A fake
# gemspec is created based on the version set on the dependency
# TODO: Use the version from the spec instead of from the dependency
if version && version.size == 1 && version.first =~ /^\s*= (.+)\s*$/ && dep.source.is_a?(Bundler::Source::Path)
dep.source.name = name
dep.source.version = $1
end
end
@dependencies[dep.name] = dep
end
def parse_checksum(line)
if line =~ NAME_VERSION
spaces = $1
return unless spaces.size == 2
name = $2
version = $3
platform = $4
checksum = $6
version = Gem::Version.new(version)
platform = platform ? Gem::Platform.new(platform) : Gem::Platform::RUBY
@checksums << Bundler::Checksum.new(name, version, platform, checksum)
end
end
def parse_spec(line)
return unless line =~ NAME_VERSION
spaces = $1
name = -$2
version = $3
if spaces.size == 4
# only load platform for non-dependency (spec) line
platform = $4
version = Gem::Version.new(version)
platform = platform ? Gem::Platform.new(platform) : Gem::Platform::RUBY
@current_spec = LazySpecification.new(name, version, platform)
@current_spec.source = @current_source
@current_source.add_dependency_names(name)
@specs[@current_spec.full_name] = @current_spec
elsif spaces.size == 6
version = version.split(",").each(&:strip!) if version
dep = Gem::Dependency.new(name, version)
@current_spec.dependencies << dep
end
end
def parse_platform(line)
@platforms << Gem::Platform.new($1) if line =~ /^ (.*)$/
end
def parse_bundled_with(line)
line.strip!
return unless Gem::Version.correct?(line)
@bundler_version = Gem::Version.create(line)
end
def parse_ruby(line)
line.strip!
@ruby_version = line
end
end
end