bundler 4.0.14 → 4.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/bundler.gemspec +2 -2
- data/lib/bundler/build_metadata.rb +1 -1
- data/lib/bundler/cli/gem.rb +1 -2
- data/lib/bundler/definition.rb +23 -8
- data/lib/bundler/dsl.rb +4 -1
- data/lib/bundler/fetcher/base.rb +3 -0
- data/lib/bundler/fetcher/compact_index.rb +13 -0
- data/lib/bundler/fetcher/gem_remote_fetcher.rb +2 -0
- data/lib/bundler/fetcher.rb +5 -1
- data/lib/bundler/installer/parallel_installer.rb +48 -3
- data/lib/bundler/installer.rb +7 -1
- data/lib/bundler/mirror.rb +7 -7
- data/lib/bundler/plugin/unloaded_source.rb +25 -0
- data/lib/bundler/plugin.rb +12 -6
- data/lib/bundler/resolver/base.rb +9 -0
- data/lib/bundler/resolver.rb +17 -13
- data/lib/bundler/rubygems_gem_installer.rb +34 -1
- data/lib/bundler/source/git/git_proxy.rb +6 -0
- data/lib/bundler/source/rubygems.rb +27 -7
- data/lib/bundler/source_list.rb +4 -0
- data/lib/bundler/spec_set.rb +2 -2
- data/lib/bundler/templates/newgem/newgem.gemspec.tt +1 -1
- data/lib/bundler/vendored_tsort.rb +19 -2
- data/lib/bundler/version.rb +1 -1
- metadata +4 -5
- data/lib/bundler/vendor/tsort/LICENSE.txt +0 -22
- data/lib/bundler/vendor/tsort/lib/tsort.rb +0 -455
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d4ec7b35f89e61eb8c603099869df9c107accc057741b52f03b849d31ae0e734
|
|
4
|
+
data.tar.gz: 13f346287c2ee66bb2baa3ddfaff121fce05c0f428ae73aa199b148559a80525
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6e70a1fb438a201e9864383eba6a47727102581979a7e15902d219cca5939e623e933b728a812308b0cf7e2210237bfcee2be2d99e36eabb24296360be296607
|
|
7
|
+
data.tar.gz: 105f3457b4a7f4e69a252ee7857fcbeb7c4c91406c1ad76a693f09e2a3cf2a741bf407a91053fbe57d29afe8c88ded24553b6de33deb76ce11c9abff20d117ec
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.0.16 / 2026-07-10
|
|
4
|
+
|
|
5
|
+
### Enhancements:
|
|
6
|
+
|
|
7
|
+
* Bundler: Fix Bundler::Fetcher for PQC support, adding integration connection tests. Pull request [#9637](https://github.com/ruby/rubygems/pull/9637) by junaruga
|
|
8
|
+
* Reuse RubyGems' vendored tsort in Bundler. Pull request [#9647](https://github.com/ruby/rubygems/pull/9647) by hsbt
|
|
9
|
+
|
|
10
|
+
### Bug fixes:
|
|
11
|
+
|
|
12
|
+
* Initialize the new gem's git repo without a subshell. Pull request [#9670](https://github.com/ruby/rubygems/pull/9670) by hsbt
|
|
13
|
+
* Preserve CRLF lockfile line endings on Windows. Pull request [#9669](https://github.com/ruby/rubygems/pull/9669) by hsbt
|
|
14
|
+
* Fix the gemspec error snippet on Windows drive-letter paths. Pull request [#9668](https://github.com/ruby/rubygems/pull/9668) by hsbt
|
|
15
|
+
|
|
16
|
+
### Documentation:
|
|
17
|
+
|
|
18
|
+
* Point Bundler gemspec metadata at the moved docs. Pull request [#9648](https://github.com/ruby/rubygems/pull/9648) by hsbt
|
|
19
|
+
|
|
20
|
+
## 4.0.15 / 2026-06-24
|
|
21
|
+
|
|
22
|
+
### Enhancements:
|
|
23
|
+
|
|
24
|
+
* Resolve Git LFS files in git sources from the real remote. Pull request [#9632](https://github.com/ruby/rubygems/pull/9632) by hsbt
|
|
25
|
+
* Suggest access issues, not only yanking, for missing locked gems. Pull request [#9631](https://github.com/ruby/rubygems/pull/9631) by hsbt
|
|
26
|
+
* Implement a make jobserver (continuation of #9210). Pull request [#9625](https://github.com/ruby/rubygems/pull/9625) by hsbt
|
|
27
|
+
* Reduce peak memory usage of full index loading and bundle install. Pull request [#9618](https://github.com/ruby/rubygems/pull/9618) by hsbt
|
|
28
|
+
* Bump up to rb-sys 0.9.128. Pull request [#9569](https://github.com/ruby/rubygems/pull/9569) by hsbt
|
|
29
|
+
|
|
30
|
+
### Bug fixes:
|
|
31
|
+
|
|
32
|
+
* Skip the make jobserver on Windows. Pull request [#9630](https://github.com/ruby/rubygems/pull/9630) by hsbt
|
|
33
|
+
* Don't require source plugins to be installed to parse a lockfile: 4.0.x. Pull request [#9621](https://github.com/ruby/rubygems/pull/9621) by hsbt
|
|
34
|
+
* Exempt lockfile versions from cooldown on every resolution path. Pull request [#9619](https://github.com/ruby/rubygems/pull/9619) by hsbt
|
|
35
|
+
* Set `Bundler.settings[:ssl_ca_cert]` to download gems. Pull request [#9610](https://github.com/ruby/rubygems/pull/9610) by junaruga
|
|
36
|
+
|
|
3
37
|
## 4.0.14 / 2026-06-10
|
|
4
38
|
|
|
5
39
|
### Bug fixes:
|
data/bundler.gemspec
CHANGED
|
@@ -24,9 +24,9 @@ Gem::Specification.new do |s|
|
|
|
24
24
|
|
|
25
25
|
s.metadata = {
|
|
26
26
|
"bug_tracker_uri" => "https://github.com/ruby/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3ABundler",
|
|
27
|
-
"changelog_uri" => "https://github.com/ruby/rubygems/blob/master/bundler
|
|
27
|
+
"changelog_uri" => "https://github.com/ruby/rubygems/blob/master/CHANGELOG-bundler.md",
|
|
28
28
|
"homepage_uri" => "https://bundler.io/",
|
|
29
|
-
"source_code_uri" => "https://github.com/ruby/rubygems
|
|
29
|
+
"source_code_uri" => "https://github.com/ruby/rubygems",
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
s.required_ruby_version = ">= 3.2.0"
|
data/lib/bundler/cli/gem.rb
CHANGED
|
@@ -256,8 +256,7 @@ module Bundler
|
|
|
256
256
|
|
|
257
257
|
if use_git
|
|
258
258
|
Bundler.ui.info "\nInitializing git repo in #{target}"
|
|
259
|
-
|
|
260
|
-
`git init #{target.to_s.shellescape}`
|
|
259
|
+
IO.popen(["git", "init", target.to_s], &:read)
|
|
261
260
|
|
|
262
261
|
config[:git_default_branch] = File.read("#{target}/.git/HEAD").split("/").last.chomp
|
|
263
262
|
end
|
data/lib/bundler/definition.rb
CHANGED
|
@@ -230,6 +230,16 @@ module Bundler
|
|
|
230
230
|
sources.prefer_local!
|
|
231
231
|
end
|
|
232
232
|
|
|
233
|
+
# Releases memory only needed during resolution, such as remote spec
|
|
234
|
+
# indexes and resolver state. Only safe to call once resolution is
|
|
235
|
+
# complete and the result has been materialized, since any further
|
|
236
|
+
# resolution will need to refetch remote specs.
|
|
237
|
+
def release_resolution_memory!
|
|
238
|
+
@resolver = nil
|
|
239
|
+
@resolution_base = nil
|
|
240
|
+
sources.release_resolution_memory!
|
|
241
|
+
end
|
|
242
|
+
|
|
233
243
|
# For given dependency list returns a SpecSet with Gemspec of all the required
|
|
234
244
|
# dependencies.
|
|
235
245
|
# 1. The method first resolves the dependencies specified in Gemfile
|
|
@@ -387,10 +397,6 @@ module Bundler
|
|
|
387
397
|
|
|
388
398
|
contents = to_lock
|
|
389
399
|
|
|
390
|
-
# Convert to \r\n if the existing lock has them
|
|
391
|
-
# i.e., Windows with `git config core.autocrlf=true`
|
|
392
|
-
contents.gsub!(/\n/, "\r\n") if @lockfile_contents.match?("\r\n")
|
|
393
|
-
|
|
394
400
|
if @locked_bundler_version
|
|
395
401
|
locked_major = @locked_bundler_version.segments.first
|
|
396
402
|
current_major = bundler_version_to_lock.segments.first
|
|
@@ -411,6 +417,14 @@ module Bundler
|
|
|
411
417
|
return
|
|
412
418
|
end
|
|
413
419
|
|
|
420
|
+
# Convert to \r\n if the existing lock has them, i.e., Windows with
|
|
421
|
+
# `git config core.autocrlf=true`. Detect from the bytes on disk because
|
|
422
|
+
# reading in text mode strips carriage returns on Windows, which would
|
|
423
|
+
# otherwise defeat this check and rewrite a `\r\n` lockfile with `\n`.
|
|
424
|
+
if File.exist?(file) && SharedHelpers.filesystem_access(file, :read) {|p| File.binread(p).include?("\r\n") }
|
|
425
|
+
contents.gsub!(/\n/, "\r\n")
|
|
426
|
+
end
|
|
427
|
+
|
|
414
428
|
begin
|
|
415
429
|
SharedHelpers.filesystem_access(file) do |p|
|
|
416
430
|
File.open(p, "wb") {|f| f.puts(contents) }
|
|
@@ -688,9 +702,10 @@ module Bundler
|
|
|
688
702
|
"available locally before rerunning Bundler."
|
|
689
703
|
else
|
|
690
704
|
"Your bundle is locked to #{locked_gem} from #{locked_gem.source}, but that version can " \
|
|
691
|
-
"no longer be found in that source. That means the author of #{locked_gem} has removed it
|
|
692
|
-
"You'll need to update your bundle to a version other
|
|
693
|
-
"removed
|
|
705
|
+
"no longer be found in that source. That means either the author of #{locked_gem} has removed it, " \
|
|
706
|
+
"or you no longer have access to that source. You'll need to update your bundle to a version other " \
|
|
707
|
+
"than #{locked_gem} that hasn't been removed, or check your credentials and access rights for " \
|
|
708
|
+
"#{locked_gem.source}, in order to install."
|
|
694
709
|
end
|
|
695
710
|
|
|
696
711
|
raise GemNotFound, message
|
|
@@ -1283,7 +1298,7 @@ module Bundler
|
|
|
1283
1298
|
|
|
1284
1299
|
def new_resolution_base(last_resolve:, unlock:)
|
|
1285
1300
|
new_resolution_platforms = @current_platform_missing ? @new_platforms + [Bundler.local_platform] : @new_platforms
|
|
1286
|
-
Resolver::Base.new(source_requirements, expanded_dependencies, last_resolve, @platforms, locked_specs: @originally_locked_specs, unlock: unlock, prerelease: gem_version_promoter.pre?, prefer_local: @prefer_local, new_platforms: new_resolution_platforms)
|
|
1301
|
+
Resolver::Base.new(source_requirements, expanded_dependencies, last_resolve, @platforms, locked_specs: @originally_locked_specs, unlock: unlock, prerelease: gem_version_promoter.pre?, prefer_local: @prefer_local, new_platforms: new_resolution_platforms, explicit_unlocks: @explicit_unlocks)
|
|
1287
1302
|
end
|
|
1288
1303
|
|
|
1289
1304
|
def new_resolver(base)
|
data/lib/bundler/dsl.rb
CHANGED
|
@@ -602,8 +602,11 @@ module Bundler
|
|
|
602
602
|
|
|
603
603
|
trace_line = backtrace.find {|l| l.include?(dsl_path) } || trace_line
|
|
604
604
|
return m unless trace_line
|
|
605
|
-
|
|
605
|
+
# Match the line number right before `:in` or the end of the line so a
|
|
606
|
+
# Windows drive letter like `C:` does not get mistaken for the number.
|
|
607
|
+
line_number = trace_line[/:(\d+)(?::in\b|\z)/, 1]
|
|
606
608
|
return m unless line_number
|
|
609
|
+
line_number = line_number.to_i - 1
|
|
607
610
|
|
|
608
611
|
lines = contents.lines.to_a
|
|
609
612
|
indent = " # "
|
data/lib/bundler/fetcher/base.rb
CHANGED
|
@@ -63,6 +63,13 @@ module Bundler
|
|
|
63
63
|
true
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
# The client holds the parsed checksums of all info files in the
|
|
67
|
+
# index. Dropping it is always safe because it is rebuilt from the
|
|
68
|
+
# local cache on demand.
|
|
69
|
+
def release_resolution_memory!
|
|
70
|
+
@compact_index_client = nil
|
|
71
|
+
end
|
|
72
|
+
|
|
66
73
|
private
|
|
67
74
|
|
|
68
75
|
def compact_index_client
|
|
@@ -73,6 +80,12 @@ module Bundler
|
|
|
73
80
|
end
|
|
74
81
|
|
|
75
82
|
def fetch_gem_infos(names)
|
|
83
|
+
# Create the client and update the versions file on this thread.
|
|
84
|
+
# Otherwise the workers race to lazily create the client and update
|
|
85
|
+
# the versions file concurrently, e.g. when the client was released
|
|
86
|
+
# after resolution and is being rebuilt for `bundle cache`.
|
|
87
|
+
compact_index_client.available?
|
|
88
|
+
|
|
76
89
|
in_parallel(names) {|name| compact_index_client.info(name) }
|
|
77
90
|
rescue TooManyRequestsError # rubygems.org is rate limiting us, slow down.
|
|
78
91
|
@bundle_worker&.stop
|
data/lib/bundler/fetcher.rb
CHANGED
|
@@ -243,6 +243,10 @@ module Bundler
|
|
|
243
243
|
fetchers.first.api_fetcher?
|
|
244
244
|
end
|
|
245
245
|
|
|
246
|
+
def release_resolution_memory!
|
|
247
|
+
@fetchers&.each(&:release_resolution_memory!)
|
|
248
|
+
end
|
|
249
|
+
|
|
246
250
|
def gem_remote_fetcher
|
|
247
251
|
@gem_remote_fetcher ||= begin
|
|
248
252
|
require_relative "fetcher/gem_remote_fetcher"
|
|
@@ -314,7 +318,7 @@ module Bundler
|
|
|
314
318
|
if ssl_client_cert
|
|
315
319
|
pem = File.read(ssl_client_cert)
|
|
316
320
|
con.cert = OpenSSL::X509::Certificate.new(pem)
|
|
317
|
-
con.key = OpenSSL::PKey
|
|
321
|
+
con.key = OpenSSL::PKey.read(pem)
|
|
318
322
|
end
|
|
319
323
|
|
|
320
324
|
con.read_timeout = Fetcher.api_timeout
|
|
@@ -110,10 +110,55 @@ module Bundler
|
|
|
110
110
|
end
|
|
111
111
|
|
|
112
112
|
def install_with_worker
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
with_jobserver do
|
|
114
|
+
installed_specs = {}
|
|
115
|
+
enqueue_specs(installed_specs)
|
|
115
116
|
|
|
116
|
-
|
|
117
|
+
process_specs(installed_specs) until finished_installing?
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def with_jobserver
|
|
122
|
+
# The jobserver hands tokens to child `make` processes through MAKEFLAGS
|
|
123
|
+
# using the GNU make `--jobserver-auth` protocol. nmake, the default make
|
|
124
|
+
# on mswin, instead reads MAKEFLAGS as bare option letters and aborts
|
|
125
|
+
# every native extension build with `fatal error U1065: invalid option
|
|
126
|
+
# '-'`. Skip the jobserver when nmake is in use. Other Windows toolchains
|
|
127
|
+
# such as mingw use GNU make and keep working through the inherited pipe.
|
|
128
|
+
return yield if nmake? || bsd_make?
|
|
129
|
+
|
|
130
|
+
begin
|
|
131
|
+
r, w = IO.pipe
|
|
132
|
+
r.close_on_exec = false
|
|
133
|
+
w.close_on_exec = false
|
|
134
|
+
w.write("*" * @size)
|
|
135
|
+
|
|
136
|
+
old_makeflags = ENV["MAKEFLAGS"]
|
|
137
|
+
ENV["MAKEFLAGS"] = [old_makeflags, "--jobserver-auth=#{r.fileno},#{w.fileno}"].compact.join(" ")
|
|
138
|
+
|
|
139
|
+
yield
|
|
140
|
+
ensure
|
|
141
|
+
# Restore MAKEFLAGS before closing the pipe so a close failure can't
|
|
142
|
+
# leave the process with descriptors that point at a closed pipe.
|
|
143
|
+
old_makeflags ? ENV["MAKEFLAGS"] = old_makeflags : ENV.delete("MAKEFLAGS")
|
|
144
|
+
|
|
145
|
+
r&.close
|
|
146
|
+
w&.close
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Mirror how RubyGems' extension builder picks the make program so the
|
|
151
|
+
# jobserver is only set up when a GNU-compatible make will consume it.
|
|
152
|
+
def nmake?
|
|
153
|
+
make = ENV["MAKE"] || ENV["make"]
|
|
154
|
+
make ||= "nmake" if RUBY_PLATFORM.include?("mswin")
|
|
155
|
+
/\bnmake/i.match?(make.to_s)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def bsd_make?
|
|
159
|
+
return false unless Gem.freebsd_platform?
|
|
160
|
+
make = ENV["MAKE"] || ENV["make"] || "make"
|
|
161
|
+
!/\bgmake/i.match?(make)
|
|
117
162
|
end
|
|
118
163
|
|
|
119
164
|
def install_serially
|
data/lib/bundler/installer.rb
CHANGED
|
@@ -195,7 +195,13 @@ module Bundler
|
|
|
195
195
|
force = options[:force]
|
|
196
196
|
local = options[:local] || options[:"prefer-local"]
|
|
197
197
|
jobs = Bundler.settings.installation_parallelization
|
|
198
|
-
|
|
198
|
+
specs = @definition.specs
|
|
199
|
+
# Installing default gems may need the remote index again to cache
|
|
200
|
+
# their .gem files, so keep resolution memory around in that case.
|
|
201
|
+
# The bundler spec itself is excluded because it comes from the
|
|
202
|
+
# metadata source and never goes through that path.
|
|
203
|
+
@definition.release_resolution_memory! if specs.none? {|s| s.default_gem? && s.source.is_a?(Source::Rubygems) }
|
|
204
|
+
spec_installations = ParallelInstaller.call(self, specs, jobs, standalone, force, local: local)
|
|
199
205
|
spec_installations.each do |installation|
|
|
200
206
|
post_install_messages[installation.name] = installation.post_install_message if installation.has_post_install_message?
|
|
201
207
|
end
|
data/lib/bundler/mirror.rb
CHANGED
|
@@ -160,18 +160,18 @@ module Bundler
|
|
|
160
160
|
|
|
161
161
|
def wait_for_writtable_socket(socket, address, timeout)
|
|
162
162
|
if IO.select(nil, [socket], nil, timeout)
|
|
163
|
-
probe_writtable_socket(socket
|
|
163
|
+
probe_writtable_socket(socket)
|
|
164
164
|
else # TCP Handshake timed out, or there is something dropping packets
|
|
165
165
|
false
|
|
166
166
|
end
|
|
167
167
|
end
|
|
168
168
|
|
|
169
|
-
def probe_writtable_socket(socket
|
|
170
|
-
socket
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
169
|
+
def probe_writtable_socket(socket)
|
|
170
|
+
# Check the pending error on the socket rather than calling
|
|
171
|
+
# +connect_nonblock+ a second time. On BSD-based systems such as macOS a
|
|
172
|
+
# second connect returns +EISCONN+ even when the asynchronous connect
|
|
173
|
+
# failed, which would make a down mirror look reachable.
|
|
174
|
+
socket.getsockopt(Socket::SOL_SOCKET, Socket::SO_ERROR).int == 0
|
|
175
175
|
end
|
|
176
176
|
end
|
|
177
177
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bundler
|
|
4
|
+
module Plugin
|
|
5
|
+
# Stands in for a source handled by a plugin that is not loaded yet, so
|
|
6
|
+
# that the lockfile can still be parsed during the plugin install pass,
|
|
7
|
+
# and by external tools reading a lockfile without the plugin installed.
|
|
8
|
+
class UnloadedSource
|
|
9
|
+
include API::Source
|
|
10
|
+
|
|
11
|
+
# Unlike real plugin sources, where the handling class encodes the
|
|
12
|
+
# source type, all unloaded sources share this class, so the type must
|
|
13
|
+
# be compared explicitly.
|
|
14
|
+
def ==(other)
|
|
15
|
+
super && options["type"] == other.options["type"]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
alias_method :eql?, :==
|
|
19
|
+
|
|
20
|
+
def hash
|
|
21
|
+
[super, options["type"]].hash
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/bundler/plugin.rb
CHANGED
|
@@ -4,11 +4,12 @@ require_relative "plugin/api"
|
|
|
4
4
|
|
|
5
5
|
module Bundler
|
|
6
6
|
module Plugin
|
|
7
|
-
autoload :DSL,
|
|
8
|
-
autoload :Events,
|
|
9
|
-
autoload :Index,
|
|
10
|
-
autoload :Installer,
|
|
11
|
-
autoload :SourceList,
|
|
7
|
+
autoload :DSL, File.expand_path("plugin/dsl", __dir__)
|
|
8
|
+
autoload :Events, File.expand_path("plugin/events", __dir__)
|
|
9
|
+
autoload :Index, File.expand_path("plugin/index", __dir__)
|
|
10
|
+
autoload :Installer, File.expand_path("plugin/installer", __dir__)
|
|
11
|
+
autoload :SourceList, File.expand_path("plugin/source_list", __dir__)
|
|
12
|
+
autoload :UnloadedSource, File.expand_path("plugin/unloaded_source", __dir__)
|
|
12
13
|
|
|
13
14
|
class MalformattedPlugin < PluginError; end
|
|
14
15
|
class UndefinedCommandError < PluginError; end
|
|
@@ -199,9 +200,14 @@ module Bundler
|
|
|
199
200
|
# @return [API::Source] the instance of the class that handles the source
|
|
200
201
|
# type passed in locked_opts
|
|
201
202
|
def from_lock(locked_opts)
|
|
203
|
+
opts = locked_opts.merge("uri" => locked_opts["remote"])
|
|
204
|
+
# use an inert placeholder when the plugin handling this source is not
|
|
205
|
+
# installed, so that the lockfile can still be parsed
|
|
206
|
+
return UnloadedSource.new(opts) unless source?(locked_opts["type"])
|
|
207
|
+
|
|
202
208
|
src = source(locked_opts["type"])
|
|
203
209
|
|
|
204
|
-
src.new(
|
|
210
|
+
src.new(opts)
|
|
205
211
|
end
|
|
206
212
|
|
|
207
213
|
# To be called via the API to register a hooks and corresponding block that
|
|
@@ -8,6 +8,7 @@ module Bundler
|
|
|
8
8
|
attr_reader :packages, :requirements, :source_requirements, :locked_specs
|
|
9
9
|
|
|
10
10
|
def initialize(source_requirements, dependencies, base, platforms, options)
|
|
11
|
+
@explicit_unlocks = options.delete(:explicit_unlocks) || []
|
|
11
12
|
@source_requirements = source_requirements
|
|
12
13
|
@locked_specs = options[:locked_specs]
|
|
13
14
|
|
|
@@ -43,6 +44,14 @@ module Bundler
|
|
|
43
44
|
@packages[name]
|
|
44
45
|
end
|
|
45
46
|
|
|
47
|
+
# Gems the user named on a `bundle update GEM` / `bundle lock --update GEM`
|
|
48
|
+
# command line. These are the only ones meant to move off their locked
|
|
49
|
+
# version, so cooldown keeps applying to them while every other locked gem
|
|
50
|
+
# stays exempt.
|
|
51
|
+
def explicitly_unlocked?(name)
|
|
52
|
+
@explicit_unlocks.include?(name)
|
|
53
|
+
end
|
|
54
|
+
|
|
46
55
|
def base_requirements
|
|
47
56
|
@base_requirements ||= build_base_requirements
|
|
48
57
|
end
|
data/lib/bundler/resolver.rb
CHANGED
|
@@ -437,25 +437,29 @@ module Bundler
|
|
|
437
437
|
def cooldown_excluded?(spec)
|
|
438
438
|
return false unless spec.respond_to?(:created_at) && spec.created_at
|
|
439
439
|
return false unless spec.respond_to?(:remote) && spec.remote
|
|
440
|
-
return false if
|
|
440
|
+
return false if locked_by_lockfile?(spec)
|
|
441
441
|
days = spec.remote.effective_cooldown
|
|
442
442
|
return false if days.nil? || days <= 0
|
|
443
443
|
(cooldown_now - spec.created_at) < (days * 86_400)
|
|
444
444
|
end
|
|
445
445
|
|
|
446
|
-
# A
|
|
447
|
-
#
|
|
448
|
-
#
|
|
449
|
-
#
|
|
450
|
-
#
|
|
451
|
-
#
|
|
452
|
-
#
|
|
453
|
-
#
|
|
454
|
-
|
|
446
|
+
# A version already written to the lockfile has been adopted, and cooldown
|
|
447
|
+
# only governs the adoption of *new* versions, so it must never retract one
|
|
448
|
+
# the lockfile already pins. Keying this off the locked specs rather than the
|
|
449
|
+
# prevent-downgrade floor matters because that floor is absent on resolutions
|
|
450
|
+
# that re-pick a gem from scratch: the auxiliary full update run to compute
|
|
451
|
+
# `--update` targets, and the from-scratch retries after a conflict unlocks a
|
|
452
|
+
# gem. In those passes the locked version is the only candidate, so filtering
|
|
453
|
+
# it out makes an unrelated operation impossible whenever every published
|
|
454
|
+
# version matching the requirement sits inside the cooldown window.
|
|
455
|
+
#
|
|
456
|
+
# Gems named on a `bundle update GEM` command are the exception: the user
|
|
457
|
+
# asked to move them, so they stay subject to cooldown and a locked-but-fresh
|
|
458
|
+
# release is pushed back to an older one (or fails loudly when none exists).
|
|
459
|
+
def locked_by_lockfile?(spec)
|
|
455
460
|
return false unless defined?(@base) && @base
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
requirement.requirements.any? {|op, version| op == ">=" && version == spec.version }
|
|
461
|
+
return false if @base.explicitly_unlocked?(spec.name)
|
|
462
|
+
@base.locked_specs[spec.name].any? {|locked| locked.version == spec.version }
|
|
459
463
|
end
|
|
460
464
|
|
|
461
465
|
def cooldown_now
|
|
@@ -4,6 +4,11 @@ require "rubygems/installer"
|
|
|
4
4
|
|
|
5
5
|
module Bundler
|
|
6
6
|
class RubyGemsGemInstaller < Gem::Installer
|
|
7
|
+
# Cap how many jobserver slots a single gem's `make` may grab so that one
|
|
8
|
+
# gem with many recipes doesn't starve the others sharing the pool. Beyond
|
|
9
|
+
# a handful of jobs the extra parallelism rarely pays off in practice.
|
|
10
|
+
MAX_JOBS_PER_GEM = 3
|
|
11
|
+
|
|
7
12
|
def check_executable_overwrite(filename)
|
|
8
13
|
# Bundler needs to install gems regardless of binstub overwriting
|
|
9
14
|
end
|
|
@@ -101,10 +106,18 @@ module Bundler
|
|
|
101
106
|
end
|
|
102
107
|
|
|
103
108
|
def build_jobs
|
|
104
|
-
|
|
109
|
+
@jobserver_read_io&.read_nonblock(MAX_JOBS_PER_GEM, @jobserver_tokens)
|
|
110
|
+
acquired_jobs = @jobserver_tokens.empty? ? nil : @jobserver_tokens.size
|
|
111
|
+
|
|
112
|
+
acquired_jobs || Bundler.settings[:jobs] || super
|
|
113
|
+
rescue IO::WaitReadable, EOFError
|
|
114
|
+
1
|
|
105
115
|
end
|
|
106
116
|
|
|
107
117
|
def build_extensions
|
|
118
|
+
@jobserver_tokens = +""
|
|
119
|
+
@jobserver_read_io, @jobserver_write_io = connect_to_jobserver
|
|
120
|
+
|
|
108
121
|
extension_cache_path = options[:bundler_extension_cache_path]
|
|
109
122
|
extension_dir = spec.extension_dir
|
|
110
123
|
unless extension_cache_path && extension_dir
|
|
@@ -128,6 +141,11 @@ module Bundler
|
|
|
128
141
|
FileUtils.cp_r extension_dir, extension_cache_path
|
|
129
142
|
end
|
|
130
143
|
end
|
|
144
|
+
ensure
|
|
145
|
+
unless @jobserver_tokens.empty?
|
|
146
|
+
@jobserver_write_io.write(@jobserver_tokens)
|
|
147
|
+
@jobserver_write_io.flush
|
|
148
|
+
end
|
|
131
149
|
end
|
|
132
150
|
|
|
133
151
|
def spec
|
|
@@ -144,6 +162,21 @@ module Bundler
|
|
|
144
162
|
|
|
145
163
|
private
|
|
146
164
|
|
|
165
|
+
def connect_to_jobserver
|
|
166
|
+
return unless ENV["MAKEFLAGS"]
|
|
167
|
+
# We append our own --jobserver-auth, so read the last one. Otherwise a
|
|
168
|
+
# parent jobserver's descriptors (e.g. `bundle install` run under
|
|
169
|
+
# `make -j`) would be picked up instead of the pool ParallelInstaller created.
|
|
170
|
+
read_fd, write_fd = ENV["MAKEFLAGS"].scan(/--jobserver-auth=(\d+),(\d+)/).last
|
|
171
|
+
|
|
172
|
+
return unless read_fd && write_fd
|
|
173
|
+
|
|
174
|
+
# Pass explicit modes. On POSIX, IO.new detects the descriptor's access
|
|
175
|
+
# mode, but on Windows it can't, so the write end would default to read
|
|
176
|
+
# mode and raise "IOError: not opened for writing" when releasing slots.
|
|
177
|
+
[IO.new(read_fd.to_i, "r", autoclose: false), IO.new(write_fd.to_i, "w", autoclose: false)]
|
|
178
|
+
end
|
|
179
|
+
|
|
147
180
|
def prepare_extension_build(extension_dir)
|
|
148
181
|
SharedHelpers.filesystem_access(extension_dir, :create) do
|
|
149
182
|
FileUtils.mkdir_p extension_dir
|
|
@@ -144,6 +144,12 @@ module Bundler
|
|
|
144
144
|
FileUtils.rm_rf(p)
|
|
145
145
|
end
|
|
146
146
|
git "clone", "--no-checkout", "--quiet", path.to_s, destination.to_s
|
|
147
|
+
# The copy is cloned from the local bare cache, which holds no Git LFS
|
|
148
|
+
# objects, so point origin back at the real remote and let git-lfs derive
|
|
149
|
+
# its endpoint from there when checking out. Use the credential-filtered
|
|
150
|
+
# URI to avoid persisting secrets in the copy's .git/config; auth is left
|
|
151
|
+
# to git's credential helper.
|
|
152
|
+
git "remote", "set-url", "origin", credential_filtered_uri, dir: destination
|
|
147
153
|
File.chmod((File.stat(destination).mode | 0o777) & ~File.umask, destination)
|
|
148
154
|
rescue Errno::EEXIST => e
|
|
149
155
|
file_path = e.message[%r{.*?((?:[a-zA-Z]:)?/.*)}, 1]
|
|
@@ -25,6 +25,7 @@ module Bundler
|
|
|
25
25
|
@checksum_store = Checksum::Store.new
|
|
26
26
|
@gem_installers = {}
|
|
27
27
|
@gem_installers_mutex = Mutex.new
|
|
28
|
+
@remote_specs_mutex = Mutex.new
|
|
28
29
|
|
|
29
30
|
cooldown = options["cooldown"]
|
|
30
31
|
Array(options["remotes"]).reverse_each {|r| add_remote(r, cooldown: cooldown) }
|
|
@@ -243,7 +244,7 @@ module Bundler
|
|
|
243
244
|
def cached_built_in_gem(spec, local: false)
|
|
244
245
|
cached_path = cached_gem(spec)
|
|
245
246
|
if cached_path.nil? && !local
|
|
246
|
-
remote_spec =
|
|
247
|
+
remote_spec = remote_spec_for(spec)
|
|
247
248
|
if remote_spec
|
|
248
249
|
cached_path = fetch_gem(remote_spec)
|
|
249
250
|
spec.remote = remote_spec.remote
|
|
@@ -337,6 +338,12 @@ module Bundler
|
|
|
337
338
|
@cached_specs = nil
|
|
338
339
|
end
|
|
339
340
|
|
|
341
|
+
def release_resolution_memory!
|
|
342
|
+
@specs = nil
|
|
343
|
+
@remote_specs_mutex.synchronize { @remote_specs = nil }
|
|
344
|
+
@fetchers&.each(&:release_resolution_memory!)
|
|
345
|
+
end
|
|
346
|
+
|
|
340
347
|
protected
|
|
341
348
|
|
|
342
349
|
def remote_names
|
|
@@ -414,17 +421,30 @@ module Bundler
|
|
|
414
421
|
end
|
|
415
422
|
|
|
416
423
|
def remote_specs
|
|
417
|
-
@remote_specs ||=
|
|
418
|
-
|
|
424
|
+
@remote_specs ||= @remote_specs_mutex.synchronize do
|
|
425
|
+
@remote_specs ||= Index.build do |idx|
|
|
426
|
+
index_fetchers = fetchers - api_fetchers
|
|
419
427
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
428
|
+
if index_fetchers.empty?
|
|
429
|
+
fetch_names(api_fetchers, dependency_names, idx)
|
|
430
|
+
else
|
|
431
|
+
fetch_names(fetchers, nil, idx)
|
|
432
|
+
end
|
|
424
433
|
end
|
|
425
434
|
end
|
|
426
435
|
end
|
|
427
436
|
|
|
437
|
+
# Looks up a single spec in the remote sources, fetching only its own
|
|
438
|
+
# name when the full remote index is not already materialized.
|
|
439
|
+
def remote_spec_for(spec)
|
|
440
|
+
return remote_specs.search(spec).first if @remote_specs || api_fetchers.empty?
|
|
441
|
+
|
|
442
|
+
index = Index.build do |idx|
|
|
443
|
+
fetch_names(api_fetchers, [spec.name], idx)
|
|
444
|
+
end
|
|
445
|
+
index.search(spec).first
|
|
446
|
+
end
|
|
447
|
+
|
|
428
448
|
def fetch_names(fetchers, dependency_names, index)
|
|
429
449
|
fetchers.each do |f|
|
|
430
450
|
if dependency_names
|
data/lib/bundler/source_list.rb
CHANGED
data/lib/bundler/spec_set.rb
CHANGED
|
@@ -5,7 +5,7 @@ require_relative "vendored_tsort"
|
|
|
5
5
|
module Bundler
|
|
6
6
|
class SpecSet
|
|
7
7
|
include Enumerable
|
|
8
|
-
include TSort
|
|
8
|
+
include Gem::TSort
|
|
9
9
|
|
|
10
10
|
def initialize(specs)
|
|
11
11
|
@specs = specs
|
|
@@ -315,7 +315,7 @@ module Bundler
|
|
|
315
315
|
|
|
316
316
|
def sorted
|
|
317
317
|
@sorted ||= ([@specs.find {|s| s.name == "rake" }] + tsort).compact.uniq
|
|
318
|
-
rescue TSort::Cyclic => error
|
|
318
|
+
rescue Gem::TSort::Cyclic => error
|
|
319
319
|
cgems = extract_circular_gems(error)
|
|
320
320
|
raise CyclicDependencyError, "Your bundle requires gems that depend" \
|
|
321
321
|
" on each other, creating an infinite loop. Please remove either" \
|
|
@@ -47,7 +47,7 @@ Gem::Specification.new do |spec|
|
|
|
47
47
|
# Uncomment to register a new dependency of your gem
|
|
48
48
|
# spec.add_dependency "example-gem", "~> 1.0"
|
|
49
49
|
<%- if config[:ext] == 'rust' -%>
|
|
50
|
-
spec.add_dependency "rb_sys", "~> 0.9.
|
|
50
|
+
spec.add_dependency "rb_sys", "~> 0.9.128"
|
|
51
51
|
<%- end -%>
|
|
52
52
|
<%- if config[:ext] == 'go' -%>
|
|
53
53
|
spec.add_dependency "go_gem", "~> 0.2"
|
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
# The defined? guard avoids reopening Gem::TSort when an old RubyGems has
|
|
4
|
+
# already loaded its own copy, e.g. through rubygems/request_set from
|
|
5
|
+
# Gem.activate_bin_path in binstubs.
|
|
6
|
+
#
|
|
7
|
+
unless defined?(Gem::TSort)
|
|
8
|
+
begin
|
|
9
|
+
require "rubygems/vendored_tsort"
|
|
10
|
+
rescue LoadError
|
|
11
|
+
begin
|
|
12
|
+
# RubyGems 3.4 and 3.5 ship the same file under its pre-3.6 name.
|
|
13
|
+
# Requiring the real tsort here instead would activate the tsort
|
|
14
|
+
# default gem, and `bundler/setup` must not activate any gems.
|
|
15
|
+
require "rubygems/tsort"
|
|
16
|
+
rescue LoadError
|
|
17
|
+
require "tsort"
|
|
18
|
+
Gem::TSort = TSort
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/bundler/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bundler
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.0.
|
|
4
|
+
version: 4.0.16
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- André Arko
|
|
@@ -194,6 +194,7 @@ files:
|
|
|
194
194
|
- lib/bundler/plugin/installer/path.rb
|
|
195
195
|
- lib/bundler/plugin/installer/rubygems.rb
|
|
196
196
|
- lib/bundler/plugin/source_list.rb
|
|
197
|
+
- lib/bundler/plugin/unloaded_source.rb
|
|
197
198
|
- lib/bundler/process_lock.rb
|
|
198
199
|
- lib/bundler/remote_specification.rb
|
|
199
200
|
- lib/bundler/resolver.rb
|
|
@@ -349,8 +350,6 @@ files:
|
|
|
349
350
|
- lib/bundler/vendor/thor/lib/thor/shell/wrapped_printer.rb
|
|
350
351
|
- lib/bundler/vendor/thor/lib/thor/util.rb
|
|
351
352
|
- lib/bundler/vendor/thor/lib/thor/version.rb
|
|
352
|
-
- lib/bundler/vendor/tsort/LICENSE.txt
|
|
353
|
-
- lib/bundler/vendor/tsort/lib/tsort.rb
|
|
354
353
|
- lib/bundler/vendor/uri/COPYING
|
|
355
354
|
- lib/bundler/vendor/uri/lib/uri.rb
|
|
356
355
|
- lib/bundler/vendor/uri/lib/uri/common.rb
|
|
@@ -385,9 +384,9 @@ licenses:
|
|
|
385
384
|
- MIT
|
|
386
385
|
metadata:
|
|
387
386
|
bug_tracker_uri: https://github.com/ruby/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3ABundler
|
|
388
|
-
changelog_uri: https://github.com/ruby/rubygems/blob/master/bundler
|
|
387
|
+
changelog_uri: https://github.com/ruby/rubygems/blob/master/CHANGELOG-bundler.md
|
|
389
388
|
homepage_uri: https://bundler.io/
|
|
390
|
-
source_code_uri: https://github.com/ruby/rubygems
|
|
389
|
+
source_code_uri: https://github.com/ruby/rubygems
|
|
391
390
|
rdoc_options: []
|
|
392
391
|
require_paths:
|
|
393
392
|
- lib
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved.
|
|
2
|
-
|
|
3
|
-
Redistribution and use in source and binary forms, with or without
|
|
4
|
-
modification, are permitted provided that the following conditions
|
|
5
|
-
are met:
|
|
6
|
-
1. Redistributions of source code must retain the above copyright
|
|
7
|
-
notice, this list of conditions and the following disclaimer.
|
|
8
|
-
2. Redistributions in binary form must reproduce the above copyright
|
|
9
|
-
notice, this list of conditions and the following disclaimer in the
|
|
10
|
-
documentation and/or other materials provided with the distribution.
|
|
11
|
-
|
|
12
|
-
THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
|
13
|
-
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
14
|
-
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
15
|
-
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
|
16
|
-
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
17
|
-
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
|
18
|
-
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
19
|
-
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
20
|
-
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
|
21
|
-
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
22
|
-
SUCH DAMAGE.
|
|
@@ -1,455 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
#--
|
|
4
|
-
# tsort.rb - provides a module for topological sorting and strongly connected components.
|
|
5
|
-
#++
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
#
|
|
9
|
-
# Bundler::TSort implements topological sorting using Tarjan's algorithm for
|
|
10
|
-
# strongly connected components.
|
|
11
|
-
#
|
|
12
|
-
# Bundler::TSort is designed to be able to be used with any object which can be
|
|
13
|
-
# interpreted as a directed graph.
|
|
14
|
-
#
|
|
15
|
-
# Bundler::TSort requires two methods to interpret an object as a graph,
|
|
16
|
-
# tsort_each_node and tsort_each_child.
|
|
17
|
-
#
|
|
18
|
-
# * tsort_each_node is used to iterate for all nodes over a graph.
|
|
19
|
-
# * tsort_each_child is used to iterate for child nodes of a given node.
|
|
20
|
-
#
|
|
21
|
-
# The equality of nodes are defined by eql? and hash since
|
|
22
|
-
# Bundler::TSort uses Hash internally.
|
|
23
|
-
#
|
|
24
|
-
# == A Simple Example
|
|
25
|
-
#
|
|
26
|
-
# The following example demonstrates how to mix the Bundler::TSort module into an
|
|
27
|
-
# existing class (in this case, Hash). Here, we're treating each key in
|
|
28
|
-
# the hash as a node in the graph, and so we simply alias the required
|
|
29
|
-
# #tsort_each_node method to Hash's #each_key method. For each key in the
|
|
30
|
-
# hash, the associated value is an array of the node's child nodes. This
|
|
31
|
-
# choice in turn leads to our implementation of the required #tsort_each_child
|
|
32
|
-
# method, which fetches the array of child nodes and then iterates over that
|
|
33
|
-
# array using the user-supplied block.
|
|
34
|
-
#
|
|
35
|
-
# require 'bundler/vendor/tsort/lib/tsort'
|
|
36
|
-
#
|
|
37
|
-
# class Hash
|
|
38
|
-
# include Bundler::TSort
|
|
39
|
-
# alias tsort_each_node each_key
|
|
40
|
-
# def tsort_each_child(node, &block)
|
|
41
|
-
# fetch(node).each(&block)
|
|
42
|
-
# end
|
|
43
|
-
# end
|
|
44
|
-
#
|
|
45
|
-
# {1=>[2, 3], 2=>[3], 3=>[], 4=>[]}.tsort
|
|
46
|
-
# #=> [3, 2, 1, 4]
|
|
47
|
-
#
|
|
48
|
-
# {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}.strongly_connected_components
|
|
49
|
-
# #=> [[4], [2, 3], [1]]
|
|
50
|
-
#
|
|
51
|
-
# == A More Realistic Example
|
|
52
|
-
#
|
|
53
|
-
# A very simple `make' like tool can be implemented as follows:
|
|
54
|
-
#
|
|
55
|
-
# require 'bundler/vendor/tsort/lib/tsort'
|
|
56
|
-
#
|
|
57
|
-
# class Make
|
|
58
|
-
# def initialize
|
|
59
|
-
# @dep = {}
|
|
60
|
-
# @dep.default = []
|
|
61
|
-
# end
|
|
62
|
-
#
|
|
63
|
-
# def rule(outputs, inputs=[], &block)
|
|
64
|
-
# triple = [outputs, inputs, block]
|
|
65
|
-
# outputs.each {|f| @dep[f] = [triple]}
|
|
66
|
-
# @dep[triple] = inputs
|
|
67
|
-
# end
|
|
68
|
-
#
|
|
69
|
-
# def build(target)
|
|
70
|
-
# each_strongly_connected_component_from(target) {|ns|
|
|
71
|
-
# if ns.length != 1
|
|
72
|
-
# fs = ns.delete_if {|n| Array === n}
|
|
73
|
-
# raise Bundler::TSort::Cyclic.new("cyclic dependencies: #{fs.join ', '}")
|
|
74
|
-
# end
|
|
75
|
-
# n = ns.first
|
|
76
|
-
# if Array === n
|
|
77
|
-
# outputs, inputs, block = n
|
|
78
|
-
# inputs_time = inputs.map {|f| File.mtime f}.max
|
|
79
|
-
# begin
|
|
80
|
-
# outputs_time = outputs.map {|f| File.mtime f}.min
|
|
81
|
-
# rescue Errno::ENOENT
|
|
82
|
-
# outputs_time = nil
|
|
83
|
-
# end
|
|
84
|
-
# if outputs_time == nil ||
|
|
85
|
-
# inputs_time != nil && outputs_time <= inputs_time
|
|
86
|
-
# sleep 1 if inputs_time != nil && inputs_time.to_i == Time.now.to_i
|
|
87
|
-
# block.call
|
|
88
|
-
# end
|
|
89
|
-
# end
|
|
90
|
-
# }
|
|
91
|
-
# end
|
|
92
|
-
#
|
|
93
|
-
# def tsort_each_child(node, &block)
|
|
94
|
-
# @dep[node].each(&block)
|
|
95
|
-
# end
|
|
96
|
-
# include Bundler::TSort
|
|
97
|
-
# end
|
|
98
|
-
#
|
|
99
|
-
# def command(arg)
|
|
100
|
-
# print arg, "\n"
|
|
101
|
-
# system arg
|
|
102
|
-
# end
|
|
103
|
-
#
|
|
104
|
-
# m = Make.new
|
|
105
|
-
# m.rule(%w[t1]) { command 'date > t1' }
|
|
106
|
-
# m.rule(%w[t2]) { command 'date > t2' }
|
|
107
|
-
# m.rule(%w[t3]) { command 'date > t3' }
|
|
108
|
-
# m.rule(%w[t4], %w[t1 t3]) { command 'cat t1 t3 > t4' }
|
|
109
|
-
# m.rule(%w[t5], %w[t4 t2]) { command 'cat t4 t2 > t5' }
|
|
110
|
-
# m.build('t5')
|
|
111
|
-
#
|
|
112
|
-
# == Bugs
|
|
113
|
-
#
|
|
114
|
-
# * 'tsort.rb' is wrong name because this library uses
|
|
115
|
-
# Tarjan's algorithm for strongly connected components.
|
|
116
|
-
# Although 'strongly_connected_components.rb' is correct but too long.
|
|
117
|
-
#
|
|
118
|
-
# == References
|
|
119
|
-
#
|
|
120
|
-
# R. E. Tarjan, "Depth First Search and Linear Graph Algorithms",
|
|
121
|
-
# <em>SIAM Journal on Computing</em>, Vol. 1, No. 2, pp. 146-160, June 1972.
|
|
122
|
-
#
|
|
123
|
-
|
|
124
|
-
module Bundler::TSort
|
|
125
|
-
|
|
126
|
-
VERSION = "0.2.0"
|
|
127
|
-
|
|
128
|
-
class Cyclic < StandardError
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
# Returns a topologically sorted array of nodes.
|
|
132
|
-
# The array is sorted from children to parents, i.e.
|
|
133
|
-
# the first element has no child and the last node has no parent.
|
|
134
|
-
#
|
|
135
|
-
# If there is a cycle, Bundler::TSort::Cyclic is raised.
|
|
136
|
-
#
|
|
137
|
-
# class G
|
|
138
|
-
# include Bundler::TSort
|
|
139
|
-
# def initialize(g)
|
|
140
|
-
# @g = g
|
|
141
|
-
# end
|
|
142
|
-
# def tsort_each_child(n, &b) @g[n].each(&b) end
|
|
143
|
-
# def tsort_each_node(&b) @g.each_key(&b) end
|
|
144
|
-
# end
|
|
145
|
-
#
|
|
146
|
-
# graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]})
|
|
147
|
-
# p graph.tsort #=> [4, 2, 3, 1]
|
|
148
|
-
#
|
|
149
|
-
# graph = G.new({1=>[2], 2=>[3, 4], 3=>[2], 4=>[]})
|
|
150
|
-
# p graph.tsort # raises Bundler::TSort::Cyclic
|
|
151
|
-
#
|
|
152
|
-
def tsort
|
|
153
|
-
each_node = method(:tsort_each_node)
|
|
154
|
-
each_child = method(:tsort_each_child)
|
|
155
|
-
Bundler::TSort.tsort(each_node, each_child)
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
# Returns a topologically sorted array of nodes.
|
|
159
|
-
# The array is sorted from children to parents, i.e.
|
|
160
|
-
# the first element has no child and the last node has no parent.
|
|
161
|
-
#
|
|
162
|
-
# The graph is represented by _each_node_ and _each_child_.
|
|
163
|
-
# _each_node_ should have +call+ method which yields for each node in the graph.
|
|
164
|
-
# _each_child_ should have +call+ method which takes a node argument and yields for each child node.
|
|
165
|
-
#
|
|
166
|
-
# If there is a cycle, Bundler::TSort::Cyclic is raised.
|
|
167
|
-
#
|
|
168
|
-
# g = {1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}
|
|
169
|
-
# each_node = lambda {|&b| g.each_key(&b) }
|
|
170
|
-
# each_child = lambda {|n, &b| g[n].each(&b) }
|
|
171
|
-
# p Bundler::TSort.tsort(each_node, each_child) #=> [4, 2, 3, 1]
|
|
172
|
-
#
|
|
173
|
-
# g = {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}
|
|
174
|
-
# each_node = lambda {|&b| g.each_key(&b) }
|
|
175
|
-
# each_child = lambda {|n, &b| g[n].each(&b) }
|
|
176
|
-
# p Bundler::TSort.tsort(each_node, each_child) # raises Bundler::TSort::Cyclic
|
|
177
|
-
#
|
|
178
|
-
def self.tsort(each_node, each_child)
|
|
179
|
-
tsort_each(each_node, each_child).to_a
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
# The iterator version of the #tsort method.
|
|
183
|
-
# <tt><em>obj</em>.tsort_each</tt> is similar to <tt><em>obj</em>.tsort.each</tt>, but
|
|
184
|
-
# modification of _obj_ during the iteration may lead to unexpected results.
|
|
185
|
-
#
|
|
186
|
-
# #tsort_each returns +nil+.
|
|
187
|
-
# If there is a cycle, Bundler::TSort::Cyclic is raised.
|
|
188
|
-
#
|
|
189
|
-
# class G
|
|
190
|
-
# include Bundler::TSort
|
|
191
|
-
# def initialize(g)
|
|
192
|
-
# @g = g
|
|
193
|
-
# end
|
|
194
|
-
# def tsort_each_child(n, &b) @g[n].each(&b) end
|
|
195
|
-
# def tsort_each_node(&b) @g.each_key(&b) end
|
|
196
|
-
# end
|
|
197
|
-
#
|
|
198
|
-
# graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]})
|
|
199
|
-
# graph.tsort_each {|n| p n }
|
|
200
|
-
# #=> 4
|
|
201
|
-
# # 2
|
|
202
|
-
# # 3
|
|
203
|
-
# # 1
|
|
204
|
-
#
|
|
205
|
-
def tsort_each(&block) # :yields: node
|
|
206
|
-
each_node = method(:tsort_each_node)
|
|
207
|
-
each_child = method(:tsort_each_child)
|
|
208
|
-
Bundler::TSort.tsort_each(each_node, each_child, &block)
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
# The iterator version of the Bundler::TSort.tsort method.
|
|
212
|
-
#
|
|
213
|
-
# The graph is represented by _each_node_ and _each_child_.
|
|
214
|
-
# _each_node_ should have +call+ method which yields for each node in the graph.
|
|
215
|
-
# _each_child_ should have +call+ method which takes a node argument and yields for each child node.
|
|
216
|
-
#
|
|
217
|
-
# g = {1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}
|
|
218
|
-
# each_node = lambda {|&b| g.each_key(&b) }
|
|
219
|
-
# each_child = lambda {|n, &b| g[n].each(&b) }
|
|
220
|
-
# Bundler::TSort.tsort_each(each_node, each_child) {|n| p n }
|
|
221
|
-
# #=> 4
|
|
222
|
-
# # 2
|
|
223
|
-
# # 3
|
|
224
|
-
# # 1
|
|
225
|
-
#
|
|
226
|
-
def self.tsort_each(each_node, each_child) # :yields: node
|
|
227
|
-
return to_enum(__method__, each_node, each_child) unless block_given?
|
|
228
|
-
|
|
229
|
-
each_strongly_connected_component(each_node, each_child) {|component|
|
|
230
|
-
if component.size == 1
|
|
231
|
-
yield component.first
|
|
232
|
-
else
|
|
233
|
-
raise Cyclic.new("topological sort failed: #{component.inspect}")
|
|
234
|
-
end
|
|
235
|
-
}
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
# Returns strongly connected components as an array of arrays of nodes.
|
|
239
|
-
# The array is sorted from children to parents.
|
|
240
|
-
# Each elements of the array represents a strongly connected component.
|
|
241
|
-
#
|
|
242
|
-
# class G
|
|
243
|
-
# include Bundler::TSort
|
|
244
|
-
# def initialize(g)
|
|
245
|
-
# @g = g
|
|
246
|
-
# end
|
|
247
|
-
# def tsort_each_child(n, &b) @g[n].each(&b) end
|
|
248
|
-
# def tsort_each_node(&b) @g.each_key(&b) end
|
|
249
|
-
# end
|
|
250
|
-
#
|
|
251
|
-
# graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]})
|
|
252
|
-
# p graph.strongly_connected_components #=> [[4], [2], [3], [1]]
|
|
253
|
-
#
|
|
254
|
-
# graph = G.new({1=>[2], 2=>[3, 4], 3=>[2], 4=>[]})
|
|
255
|
-
# p graph.strongly_connected_components #=> [[4], [2, 3], [1]]
|
|
256
|
-
#
|
|
257
|
-
def strongly_connected_components
|
|
258
|
-
each_node = method(:tsort_each_node)
|
|
259
|
-
each_child = method(:tsort_each_child)
|
|
260
|
-
Bundler::TSort.strongly_connected_components(each_node, each_child)
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
# Returns strongly connected components as an array of arrays of nodes.
|
|
264
|
-
# The array is sorted from children to parents.
|
|
265
|
-
# Each elements of the array represents a strongly connected component.
|
|
266
|
-
#
|
|
267
|
-
# The graph is represented by _each_node_ and _each_child_.
|
|
268
|
-
# _each_node_ should have +call+ method which yields for each node in the graph.
|
|
269
|
-
# _each_child_ should have +call+ method which takes a node argument and yields for each child node.
|
|
270
|
-
#
|
|
271
|
-
# g = {1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}
|
|
272
|
-
# each_node = lambda {|&b| g.each_key(&b) }
|
|
273
|
-
# each_child = lambda {|n, &b| g[n].each(&b) }
|
|
274
|
-
# p Bundler::TSort.strongly_connected_components(each_node, each_child)
|
|
275
|
-
# #=> [[4], [2], [3], [1]]
|
|
276
|
-
#
|
|
277
|
-
# g = {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}
|
|
278
|
-
# each_node = lambda {|&b| g.each_key(&b) }
|
|
279
|
-
# each_child = lambda {|n, &b| g[n].each(&b) }
|
|
280
|
-
# p Bundler::TSort.strongly_connected_components(each_node, each_child)
|
|
281
|
-
# #=> [[4], [2, 3], [1]]
|
|
282
|
-
#
|
|
283
|
-
def self.strongly_connected_components(each_node, each_child)
|
|
284
|
-
each_strongly_connected_component(each_node, each_child).to_a
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
# The iterator version of the #strongly_connected_components method.
|
|
288
|
-
# <tt><em>obj</em>.each_strongly_connected_component</tt> is similar to
|
|
289
|
-
# <tt><em>obj</em>.strongly_connected_components.each</tt>, but
|
|
290
|
-
# modification of _obj_ during the iteration may lead to unexpected results.
|
|
291
|
-
#
|
|
292
|
-
# #each_strongly_connected_component returns +nil+.
|
|
293
|
-
#
|
|
294
|
-
# class G
|
|
295
|
-
# include Bundler::TSort
|
|
296
|
-
# def initialize(g)
|
|
297
|
-
# @g = g
|
|
298
|
-
# end
|
|
299
|
-
# def tsort_each_child(n, &b) @g[n].each(&b) end
|
|
300
|
-
# def tsort_each_node(&b) @g.each_key(&b) end
|
|
301
|
-
# end
|
|
302
|
-
#
|
|
303
|
-
# graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]})
|
|
304
|
-
# graph.each_strongly_connected_component {|scc| p scc }
|
|
305
|
-
# #=> [4]
|
|
306
|
-
# # [2]
|
|
307
|
-
# # [3]
|
|
308
|
-
# # [1]
|
|
309
|
-
#
|
|
310
|
-
# graph = G.new({1=>[2], 2=>[3, 4], 3=>[2], 4=>[]})
|
|
311
|
-
# graph.each_strongly_connected_component {|scc| p scc }
|
|
312
|
-
# #=> [4]
|
|
313
|
-
# # [2, 3]
|
|
314
|
-
# # [1]
|
|
315
|
-
#
|
|
316
|
-
def each_strongly_connected_component(&block) # :yields: nodes
|
|
317
|
-
each_node = method(:tsort_each_node)
|
|
318
|
-
each_child = method(:tsort_each_child)
|
|
319
|
-
Bundler::TSort.each_strongly_connected_component(each_node, each_child, &block)
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
# The iterator version of the Bundler::TSort.strongly_connected_components method.
|
|
323
|
-
#
|
|
324
|
-
# The graph is represented by _each_node_ and _each_child_.
|
|
325
|
-
# _each_node_ should have +call+ method which yields for each node in the graph.
|
|
326
|
-
# _each_child_ should have +call+ method which takes a node argument and yields for each child node.
|
|
327
|
-
#
|
|
328
|
-
# g = {1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}
|
|
329
|
-
# each_node = lambda {|&b| g.each_key(&b) }
|
|
330
|
-
# each_child = lambda {|n, &b| g[n].each(&b) }
|
|
331
|
-
# Bundler::TSort.each_strongly_connected_component(each_node, each_child) {|scc| p scc }
|
|
332
|
-
# #=> [4]
|
|
333
|
-
# # [2]
|
|
334
|
-
# # [3]
|
|
335
|
-
# # [1]
|
|
336
|
-
#
|
|
337
|
-
# g = {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}
|
|
338
|
-
# each_node = lambda {|&b| g.each_key(&b) }
|
|
339
|
-
# each_child = lambda {|n, &b| g[n].each(&b) }
|
|
340
|
-
# Bundler::TSort.each_strongly_connected_component(each_node, each_child) {|scc| p scc }
|
|
341
|
-
# #=> [4]
|
|
342
|
-
# # [2, 3]
|
|
343
|
-
# # [1]
|
|
344
|
-
#
|
|
345
|
-
def self.each_strongly_connected_component(each_node, each_child) # :yields: nodes
|
|
346
|
-
return to_enum(__method__, each_node, each_child) unless block_given?
|
|
347
|
-
|
|
348
|
-
id_map = {}
|
|
349
|
-
stack = []
|
|
350
|
-
each_node.call {|node|
|
|
351
|
-
unless id_map.include? node
|
|
352
|
-
each_strongly_connected_component_from(node, each_child, id_map, stack) {|c|
|
|
353
|
-
yield c
|
|
354
|
-
}
|
|
355
|
-
end
|
|
356
|
-
}
|
|
357
|
-
nil
|
|
358
|
-
end
|
|
359
|
-
|
|
360
|
-
# Iterates over strongly connected component in the subgraph reachable from
|
|
361
|
-
# _node_.
|
|
362
|
-
#
|
|
363
|
-
# Return value is unspecified.
|
|
364
|
-
#
|
|
365
|
-
# #each_strongly_connected_component_from doesn't call #tsort_each_node.
|
|
366
|
-
#
|
|
367
|
-
# class G
|
|
368
|
-
# include Bundler::TSort
|
|
369
|
-
# def initialize(g)
|
|
370
|
-
# @g = g
|
|
371
|
-
# end
|
|
372
|
-
# def tsort_each_child(n, &b) @g[n].each(&b) end
|
|
373
|
-
# def tsort_each_node(&b) @g.each_key(&b) end
|
|
374
|
-
# end
|
|
375
|
-
#
|
|
376
|
-
# graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]})
|
|
377
|
-
# graph.each_strongly_connected_component_from(2) {|scc| p scc }
|
|
378
|
-
# #=> [4]
|
|
379
|
-
# # [2]
|
|
380
|
-
#
|
|
381
|
-
# graph = G.new({1=>[2], 2=>[3, 4], 3=>[2], 4=>[]})
|
|
382
|
-
# graph.each_strongly_connected_component_from(2) {|scc| p scc }
|
|
383
|
-
# #=> [4]
|
|
384
|
-
# # [2, 3]
|
|
385
|
-
#
|
|
386
|
-
def each_strongly_connected_component_from(node, id_map={}, stack=[], &block) # :yields: nodes
|
|
387
|
-
Bundler::TSort.each_strongly_connected_component_from(node, method(:tsort_each_child), id_map, stack, &block)
|
|
388
|
-
end
|
|
389
|
-
|
|
390
|
-
# Iterates over strongly connected components in a graph.
|
|
391
|
-
# The graph is represented by _node_ and _each_child_.
|
|
392
|
-
#
|
|
393
|
-
# _node_ is the first node.
|
|
394
|
-
# _each_child_ should have +call+ method which takes a node argument
|
|
395
|
-
# and yields for each child node.
|
|
396
|
-
#
|
|
397
|
-
# Return value is unspecified.
|
|
398
|
-
#
|
|
399
|
-
# #Bundler::TSort.each_strongly_connected_component_from is a class method and
|
|
400
|
-
# it doesn't need a class to represent a graph which includes Bundler::TSort.
|
|
401
|
-
#
|
|
402
|
-
# graph = {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}
|
|
403
|
-
# each_child = lambda {|n, &b| graph[n].each(&b) }
|
|
404
|
-
# Bundler::TSort.each_strongly_connected_component_from(1, each_child) {|scc|
|
|
405
|
-
# p scc
|
|
406
|
-
# }
|
|
407
|
-
# #=> [4]
|
|
408
|
-
# # [2, 3]
|
|
409
|
-
# # [1]
|
|
410
|
-
#
|
|
411
|
-
def self.each_strongly_connected_component_from(node, each_child, id_map={}, stack=[]) # :yields: nodes
|
|
412
|
-
return to_enum(__method__, node, each_child, id_map, stack) unless block_given?
|
|
413
|
-
|
|
414
|
-
minimum_id = node_id = id_map[node] = id_map.size
|
|
415
|
-
stack_length = stack.length
|
|
416
|
-
stack << node
|
|
417
|
-
|
|
418
|
-
each_child.call(node) {|child|
|
|
419
|
-
if id_map.include? child
|
|
420
|
-
child_id = id_map[child]
|
|
421
|
-
minimum_id = child_id if child_id && child_id < minimum_id
|
|
422
|
-
else
|
|
423
|
-
sub_minimum_id =
|
|
424
|
-
each_strongly_connected_component_from(child, each_child, id_map, stack) {|c|
|
|
425
|
-
yield c
|
|
426
|
-
}
|
|
427
|
-
minimum_id = sub_minimum_id if sub_minimum_id < minimum_id
|
|
428
|
-
end
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
if node_id == minimum_id
|
|
432
|
-
component = stack.slice!(stack_length .. -1)
|
|
433
|
-
component.each {|n| id_map[n] = nil}
|
|
434
|
-
yield component
|
|
435
|
-
end
|
|
436
|
-
|
|
437
|
-
minimum_id
|
|
438
|
-
end
|
|
439
|
-
|
|
440
|
-
# Should be implemented by a extended class.
|
|
441
|
-
#
|
|
442
|
-
# #tsort_each_node is used to iterate for all nodes over a graph.
|
|
443
|
-
#
|
|
444
|
-
def tsort_each_node # :yields: node
|
|
445
|
-
raise NotImplementedError.new
|
|
446
|
-
end
|
|
447
|
-
|
|
448
|
-
# Should be implemented by a extended class.
|
|
449
|
-
#
|
|
450
|
-
# #tsort_each_child is used to iterate for child nodes of _node_.
|
|
451
|
-
#
|
|
452
|
-
def tsort_each_child(node) # :yields: child
|
|
453
|
-
raise NotImplementedError.new
|
|
454
|
-
end
|
|
455
|
-
end
|