2018-11-03 02:07:56 +03:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2021-10-19 22:35:35 +03:00
|
|
|
require_relative "vendored_tsort"
|
2018-11-03 02:07:56 +03:00
|
|
|
|
|
|
|
module Bundler
|
|
|
|
class SpecSet
|
2019-01-04 16:10:58 +03:00
|
|
|
include Enumerable
|
|
|
|
include TSort
|
2018-11-03 02:07:56 +03:00
|
|
|
|
2023-03-23 14:31:15 +03:00
|
|
|
attr_reader :incomplete_specs
|
|
|
|
|
|
|
|
def initialize(specs, incomplete_specs = [])
|
2018-11-03 02:07:56 +03:00
|
|
|
@specs = specs
|
2023-03-23 14:31:15 +03:00
|
|
|
@incomplete_specs = incomplete_specs
|
2018-11-03 02:07:56 +03:00
|
|
|
end
|
|
|
|
|
2022-07-26 07:43:48 +03:00
|
|
|
def for(dependencies, check = false, platforms = [nil])
|
|
|
|
handled = ["bundler"].product(platforms).map {|k| [k, true] }.to_h
|
2022-07-23 12:58:53 +03:00
|
|
|
deps = dependencies.product(platforms)
|
2018-11-03 02:07:56 +03:00
|
|
|
specs = []
|
|
|
|
|
|
|
|
loop do
|
|
|
|
break unless dep = deps.shift
|
2021-02-01 18:17:16 +03:00
|
|
|
|
2022-08-22 05:52:51 +03:00
|
|
|
name = dep[0].name
|
|
|
|
platform = dep[1]
|
2023-02-22 00:27:54 +03:00
|
|
|
incomplete = false
|
2022-08-22 05:52:51 +03:00
|
|
|
|
|
|
|
key = [name, platform]
|
2022-07-23 12:58:53 +03:00
|
|
|
next if handled.key?(key)
|
|
|
|
|
|
|
|
handled[key] = true
|
2018-11-03 02:07:56 +03:00
|
|
|
|
2022-07-26 07:43:48 +03:00
|
|
|
specs_for_dep = specs_for_dependency(*dep)
|
2020-12-23 02:45:19 +03:00
|
|
|
if specs_for_dep.any?
|
2022-05-13 16:56:50 +03:00
|
|
|
specs.concat(specs_for_dep)
|
2018-11-03 02:07:56 +03:00
|
|
|
|
2020-12-23 02:45:19 +03:00
|
|
|
specs_for_dep.first.dependencies.each do |d|
|
2018-11-03 02:07:56 +03:00
|
|
|
next if d.type == :development
|
2023-12-22 01:01:12 +03:00
|
|
|
incomplete = true if d.name != "bundler" && lookup[d.name].nil?
|
2022-07-23 12:58:53 +03:00
|
|
|
deps << [d, dep[1]]
|
2018-11-03 02:07:56 +03:00
|
|
|
end
|
2023-02-22 00:27:54 +03:00
|
|
|
else
|
|
|
|
incomplete = true
|
2018-11-03 02:07:56 +03:00
|
|
|
end
|
2023-02-22 00:27:54 +03:00
|
|
|
|
2023-03-01 03:29:58 +03:00
|
|
|
if incomplete && check
|
2023-12-22 01:01:12 +03:00
|
|
|
@incomplete_specs += lookup[name] || [LazySpecification.new(name, nil, nil)]
|
2023-03-01 03:29:58 +03:00
|
|
|
end
|
2018-11-03 02:07:56 +03:00
|
|
|
end
|
|
|
|
|
2023-03-13 20:56:54 +03:00
|
|
|
specs.uniq
|
2018-11-03 02:07:56 +03:00
|
|
|
end
|
|
|
|
|
2023-12-22 20:16:54 +03:00
|
|
|
def add_extra_platforms!(platforms)
|
[rubygems/rubygems] Automatically lock extra ruby platforms
Since we started locking the specific platform in the lockfile, that has
created an annoying situation for users that don't develop on Linux.
They will create a lockfile on their machines, locking their local
platform, for example, darwin. But then that lockfile won't work
automatically when deploying to Heroku for example, because the lockfile
is frozen and the Linux platform is not included.
There's the chance though that resolving against two platforms (Linux +
the local platform) won't succeed while resolving for just the current
platform will. So, instead, we check other platform specific variants
available for the resolution we initially found, and lock those
platforms and specs too if they satisfy the resolution.
This is only done when generating new lockfiles from scratch, existing
lockfiles should keep working as before, and it's only done for "ruby
platforms", i.e., not Java or Windows which have their own complexities,
and so are excluded.
With this change, we expect that MacOS users can bundle locally and
deploy to Heroku without needing to do anything special.
https://github.com/rubygems/rubygems/commit/5f24f06bc5
2023-03-17 16:18:30 +03:00
|
|
|
return platforms.concat([Gem::Platform::RUBY]).uniq if @specs.empty?
|
|
|
|
|
2023-12-22 20:27:39 +03:00
|
|
|
new_platforms = all_platforms.select do |platform|
|
[rubygems/rubygems] Automatically lock extra ruby platforms
Since we started locking the specific platform in the lockfile, that has
created an annoying situation for users that don't develop on Linux.
They will create a lockfile on their machines, locking their local
platform, for example, darwin. But then that lockfile won't work
automatically when deploying to Heroku for example, because the lockfile
is frozen and the Linux platform is not included.
There's the chance though that resolving against two platforms (Linux +
the local platform) won't succeed while resolving for just the current
platform will. So, instead, we check other platform specific variants
available for the resolution we initially found, and lock those
platforms and specs too if they satisfy the resolution.
This is only done when generating new lockfiles from scratch, existing
lockfiles should keep working as before, and it's only done for "ruby
platforms", i.e., not Java or Windows which have their own complexities,
and so are excluded.
With this change, we expect that MacOS users can bundle locally and
deploy to Heroku without needing to do anything special.
https://github.com/rubygems/rubygems/commit/5f24f06bc5
2023-03-17 16:18:30 +03:00
|
|
|
next if platforms.include?(platform)
|
|
|
|
next unless GemHelpers.generic(platform) == Gem::Platform::RUBY
|
|
|
|
|
2023-12-22 20:27:39 +03:00
|
|
|
complete_platform(platform)
|
[rubygems/rubygems] Automatically lock extra ruby platforms
Since we started locking the specific platform in the lockfile, that has
created an annoying situation for users that don't develop on Linux.
They will create a lockfile on their machines, locking their local
platform, for example, darwin. But then that lockfile won't work
automatically when deploying to Heroku for example, because the lockfile
is frozen and the Linux platform is not included.
There's the chance though that resolving against two platforms (Linux +
the local platform) won't succeed while resolving for just the current
platform will. So, instead, we check other platform specific variants
available for the resolution we initially found, and lock those
platforms and specs too if they satisfy the resolution.
This is only done when generating new lockfiles from scratch, existing
lockfiles should keep working as before, and it's only done for "ruby
platforms", i.e., not Java or Windows which have their own complexities,
and so are excluded.
With this change, we expect that MacOS users can bundle locally and
deploy to Heroku without needing to do anything special.
https://github.com/rubygems/rubygems/commit/5f24f06bc5
2023-03-17 16:18:30 +03:00
|
|
|
end
|
|
|
|
return platforms if new_platforms.empty?
|
|
|
|
|
|
|
|
platforms.concat(new_platforms)
|
|
|
|
|
2024-04-15 20:17:54 +03:00
|
|
|
less_specific_platform = new_platforms.find {|platform| platform != Gem::Platform::RUBY && Bundler.local_platform === platform && platform === Bundler.local_platform }
|
[rubygems/rubygems] Automatically lock extra ruby platforms
Since we started locking the specific platform in the lockfile, that has
created an annoying situation for users that don't develop on Linux.
They will create a lockfile on their machines, locking their local
platform, for example, darwin. But then that lockfile won't work
automatically when deploying to Heroku for example, because the lockfile
is frozen and the Linux platform is not included.
There's the chance though that resolving against two platforms (Linux +
the local platform) won't succeed while resolving for just the current
platform will. So, instead, we check other platform specific variants
available for the resolution we initially found, and lock those
platforms and specs too if they satisfy the resolution.
This is only done when generating new lockfiles from scratch, existing
lockfiles should keep working as before, and it's only done for "ruby
platforms", i.e., not Java or Windows which have their own complexities,
and so are excluded.
With this change, we expect that MacOS users can bundle locally and
deploy to Heroku without needing to do anything special.
https://github.com/rubygems/rubygems/commit/5f24f06bc5
2023-03-17 16:18:30 +03:00
|
|
|
platforms.delete(Bundler.local_platform) if less_specific_platform
|
|
|
|
|
|
|
|
platforms
|
|
|
|
end
|
|
|
|
|
2023-12-22 01:01:12 +03:00
|
|
|
def validate_deps(s)
|
|
|
|
s.runtime_dependencies.each do |dep|
|
|
|
|
next if dep.name == "bundler"
|
|
|
|
|
|
|
|
return :missing unless names.include?(dep.name)
|
|
|
|
return :invalid if none? {|spec| dep.matches_spec?(spec) }
|
|
|
|
end
|
|
|
|
|
|
|
|
:valid
|
|
|
|
end
|
|
|
|
|
2018-11-03 02:07:56 +03:00
|
|
|
def [](key)
|
|
|
|
key = key.name if key.respond_to?(:name)
|
2023-12-22 01:01:12 +03:00
|
|
|
lookup[key]&.reverse || []
|
2018-11-03 02:07:56 +03:00
|
|
|
end
|
|
|
|
|
|
|
|
def []=(key, value)
|
|
|
|
@specs << value
|
2023-12-22 20:15:05 +03:00
|
|
|
|
|
|
|
reset!
|
2018-11-03 02:07:56 +03:00
|
|
|
end
|
|
|
|
|
2023-03-16 19:45:54 +03:00
|
|
|
def delete(specs)
|
|
|
|
specs.each {|spec| @specs.delete(spec) }
|
2023-12-22 20:15:05 +03:00
|
|
|
|
|
|
|
reset!
|
2022-08-22 05:52:51 +03:00
|
|
|
end
|
|
|
|
|
2018-11-03 02:07:56 +03:00
|
|
|
def sort!
|
|
|
|
self
|
|
|
|
end
|
|
|
|
|
|
|
|
def to_a
|
|
|
|
sorted.dup
|
|
|
|
end
|
|
|
|
|
|
|
|
def to_hash
|
|
|
|
lookup.dup
|
|
|
|
end
|
|
|
|
|
2023-03-23 14:31:07 +03:00
|
|
|
def materialize(deps)
|
|
|
|
materialized = self.for(deps, true)
|
2021-04-21 14:54:29 +03:00
|
|
|
|
2023-03-23 14:31:15 +03:00
|
|
|
SpecSet.new(materialized, incomplete_specs)
|
2018-11-03 02:07:56 +03:00
|
|
|
end
|
|
|
|
|
|
|
|
# Materialize for all the specs in the spec set, regardless of what platform they're for
|
|
|
|
# This is in contrast to how for does platform filtering (and specifically different from how `materialize` calls `for` only for the current platform)
|
|
|
|
# @return [Array<Gem::Specification>]
|
|
|
|
def materialized_for_all_platforms
|
|
|
|
@specs.map do |s|
|
|
|
|
next s unless s.is_a?(LazySpecification)
|
2021-07-24 19:17:53 +03:00
|
|
|
s.source.remote!
|
2022-07-26 07:43:48 +03:00
|
|
|
spec = s.materialize_for_installation
|
2018-11-03 02:07:56 +03:00
|
|
|
raise GemNotFound, "Could not find #{s.full_name} in any of the sources" unless spec
|
|
|
|
spec
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-10-05 08:53:57 +03:00
|
|
|
def incomplete_for_platform?(deps, platform)
|
2023-03-17 15:39:20 +03:00
|
|
|
return false if @specs.empty?
|
|
|
|
|
2023-03-23 14:31:15 +03:00
|
|
|
@incomplete_specs = []
|
|
|
|
|
2023-10-05 08:53:57 +03:00
|
|
|
self.for(deps, true, [platform])
|
2023-03-23 14:31:07 +03:00
|
|
|
|
2023-03-23 14:31:15 +03:00
|
|
|
@incomplete_specs.any?
|
2022-08-02 18:45:28 +03:00
|
|
|
end
|
|
|
|
|
2021-07-24 00:49:13 +03:00
|
|
|
def missing_specs
|
2021-07-30 12:48:46 +03:00
|
|
|
@specs.select {|s| s.is_a?(LazySpecification) }
|
2021-07-24 00:49:13 +03:00
|
|
|
end
|
|
|
|
|
2022-09-05 03:15:30 +03:00
|
|
|
def -(other)
|
|
|
|
SpecSet.new(to_a - other.to_a)
|
|
|
|
end
|
|
|
|
|
2018-11-03 02:07:56 +03:00
|
|
|
def find_by_name_and_platform(name, platform)
|
|
|
|
@specs.detect {|spec| spec.name == name && spec.match_platform(platform) }
|
|
|
|
end
|
|
|
|
|
2024-07-04 14:55:52 +03:00
|
|
|
def specs_compatible_with(other)
|
|
|
|
select do |spec|
|
|
|
|
other.valid?(spec)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-11-12 00:00:58 +03:00
|
|
|
def delete_by_name(name)
|
|
|
|
@specs.reject! {|spec| spec.name == name }
|
2023-12-22 20:15:05 +03:00
|
|
|
|
|
|
|
reset!
|
2022-09-05 03:15:30 +03:00
|
|
|
end
|
|
|
|
|
2018-11-03 02:07:56 +03:00
|
|
|
def what_required(spec)
|
2023-12-22 01:01:12 +03:00
|
|
|
unless req = find {|s| s.runtime_dependencies.any? {|d| d.name == spec.name } }
|
2018-11-03 02:07:56 +03:00
|
|
|
return [spec]
|
|
|
|
end
|
|
|
|
what_required(req) << spec
|
|
|
|
end
|
|
|
|
|
2019-01-04 16:10:58 +03:00
|
|
|
def <<(spec)
|
|
|
|
@specs << spec
|
|
|
|
end
|
|
|
|
|
|
|
|
def length
|
|
|
|
@specs.length
|
|
|
|
end
|
|
|
|
|
|
|
|
def size
|
|
|
|
@specs.size
|
|
|
|
end
|
|
|
|
|
|
|
|
def empty?
|
|
|
|
@specs.empty?
|
|
|
|
end
|
|
|
|
|
|
|
|
def each(&b)
|
|
|
|
sorted.each(&b)
|
|
|
|
end
|
|
|
|
|
2023-12-22 01:01:12 +03:00
|
|
|
def names
|
|
|
|
lookup.keys
|
|
|
|
end
|
|
|
|
|
2024-07-04 14:55:52 +03:00
|
|
|
def valid?(s)
|
|
|
|
s.matches_current_metadata? && valid_dependencies?(s)
|
|
|
|
end
|
|
|
|
|
2020-10-15 07:20:25 +03:00
|
|
|
private
|
2018-11-03 02:07:56 +03:00
|
|
|
|
2023-12-22 20:15:05 +03:00
|
|
|
def reset!
|
|
|
|
@sorted = nil
|
|
|
|
@lookup = nil
|
|
|
|
end
|
|
|
|
|
2023-12-22 20:27:39 +03:00
|
|
|
def complete_platform(platform)
|
|
|
|
new_specs = []
|
|
|
|
|
|
|
|
valid_platform = lookup.all? do |_, specs|
|
|
|
|
spec = specs.first
|
|
|
|
matching_specs = spec.source.specs.search([spec.name, spec.version])
|
|
|
|
platform_spec = GemHelpers.select_best_platform_match(matching_specs, platform).find do |s|
|
2024-07-04 14:55:52 +03:00
|
|
|
valid?(s)
|
2023-12-22 20:27:39 +03:00
|
|
|
end
|
|
|
|
|
|
|
|
if platform_spec
|
2023-12-22 20:58:55 +03:00
|
|
|
new_specs << LazySpecification.from_spec(platform_spec) unless specs.include?(platform_spec)
|
2023-12-22 20:27:39 +03:00
|
|
|
true
|
|
|
|
else
|
|
|
|
false
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-12-22 20:58:55 +03:00
|
|
|
if valid_platform && new_specs.any?
|
|
|
|
@specs.concat(new_specs)
|
|
|
|
|
|
|
|
reset!
|
|
|
|
end
|
2023-12-22 20:27:39 +03:00
|
|
|
|
|
|
|
valid_platform
|
|
|
|
end
|
|
|
|
|
|
|
|
def all_platforms
|
|
|
|
@specs.flat_map {|spec| spec.source.specs.search([spec.name, spec.version]).map(&:platform) }.uniq
|
|
|
|
end
|
|
|
|
|
2023-12-22 01:01:12 +03:00
|
|
|
def valid_dependencies?(s)
|
|
|
|
validate_deps(s) == :valid
|
|
|
|
end
|
|
|
|
|
2018-11-03 02:07:56 +03:00
|
|
|
def sorted
|
|
|
|
rake = @specs.find {|s| s.name == "rake" }
|
|
|
|
begin
|
|
|
|
@sorted ||= ([rake] + tsort).compact.uniq
|
|
|
|
rescue TSort::Cyclic => error
|
|
|
|
cgems = extract_circular_gems(error)
|
|
|
|
raise CyclicDependencyError, "Your bundle requires gems that depend" \
|
|
|
|
" on each other, creating an infinite loop. Please remove either" \
|
2022-11-12 00:00:58 +03:00
|
|
|
" gem '#{cgems[0]}' or gem '#{cgems[1]}' and try again."
|
2018-11-03 02:07:56 +03:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def extract_circular_gems(error)
|
2019-08-17 23:00:52 +03:00
|
|
|
error.message.scan(/@name="(.*?)"/).flatten
|
2018-11-03 02:07:56 +03:00
|
|
|
end
|
|
|
|
|
|
|
|
def lookup
|
|
|
|
@lookup ||= begin
|
2023-12-22 01:01:12 +03:00
|
|
|
lookup = {}
|
2022-08-24 09:39:00 +03:00
|
|
|
@specs.each do |s|
|
2023-12-22 01:01:12 +03:00
|
|
|
lookup[s.name] ||= []
|
2018-11-03 02:07:56 +03:00
|
|
|
lookup[s.name] << s
|
|
|
|
end
|
|
|
|
lookup
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def tsort_each_node
|
|
|
|
# MUST sort by name for backwards compatibility
|
|
|
|
@specs.sort_by(&:name).each {|s| yield s }
|
|
|
|
end
|
|
|
|
|
2022-07-23 12:58:53 +03:00
|
|
|
def specs_for_dependency(dep, platform)
|
|
|
|
specs_for_name = lookup[dep.name]
|
2023-12-22 01:01:12 +03:00
|
|
|
return [] unless specs_for_name
|
|
|
|
|
2024-07-04 16:48:08 +03:00
|
|
|
if platform
|
|
|
|
GemHelpers.select_best_platform_match(specs_for_name, platform, force_ruby: dep.force_ruby_platform)
|
2023-10-26 21:50:06 +03:00
|
|
|
else
|
2024-07-04 16:48:08 +03:00
|
|
|
GemHelpers.select_best_local_platform_match(specs_for_name, force_ruby: dep.force_ruby_platform)
|
2023-10-26 21:50:06 +03:00
|
|
|
end
|
2018-11-03 02:07:56 +03:00
|
|
|
end
|
|
|
|
|
|
|
|
def tsort_each_child(s)
|
|
|
|
s.dependencies.sort_by(&:name).each do |d|
|
|
|
|
next if d.type == :development
|
2023-12-22 01:01:12 +03:00
|
|
|
|
|
|
|
specs_for_name = lookup[d.name]
|
|
|
|
next unless specs_for_name
|
|
|
|
|
|
|
|
specs_for_name.each {|s2| yield s2 }
|
2018-11-03 02:07:56 +03:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|