scint 0.1.0 → 0.7.0
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/README.md +90 -41
- data/VERSION +1 -0
- data/bin/scint +9 -0
- data/lib/bundler.rb +106 -0
- data/lib/scint/cache/layout.rb +16 -14
- data/lib/scint/cache/metadata_store.rb +4 -11
- data/lib/scint/cli/cache.rb +2 -1
- data/lib/scint/cli/exec.rb +12 -24
- data/lib/scint/cli/install.rb +1214 -134
- data/lib/scint/credentials.rb +78 -15
- data/lib/scint/debug/io_trace.rb +26 -7
- data/lib/scint/downloader/fetcher.rb +25 -1
- data/lib/scint/downloader/pool.rb +67 -15
- data/lib/scint/errors.rb +10 -0
- data/lib/scint/fs.rb +90 -3
- data/lib/scint/gemfile/parser.rb +31 -4
- data/lib/scint/index/client.rb +1 -1
- data/lib/scint/installer/extension_builder.rb +95 -30
- data/lib/scint/installer/linker.rb +9 -25
- data/lib/scint/installer/planner.rb +37 -13
- data/lib/scint/installer/preparer.rb +2 -9
- data/lib/scint/lockfile/parser.rb +2 -1
- data/lib/scint/lockfile/writer.rb +78 -35
- data/lib/scint/platform.rb +8 -0
- data/lib/scint/progress.rb +128 -73
- data/lib/scint/resolver/provider.rb +71 -7
- data/lib/scint/runtime/exec.rb +52 -26
- data/lib/scint/runtime/setup.rb +29 -1
- data/lib/scint/scheduler.rb +6 -1
- data/lib/scint/spec_utils.rb +58 -0
- data/lib/scint/vendor/pub_grub/version.rb +5 -1
- data/lib/scint/version.rb +5 -0
- data/lib/scint.rb +3 -2
- metadata +5 -7
- data/bin/bundler-vs-scint +0 -233
- data/bin/scint-io-summary +0 -46
- data/bin/scint-syscall-trace +0 -41
data/lib/scint/credentials.rb
CHANGED
|
@@ -13,9 +13,10 @@ module Scint
|
|
|
13
13
|
# Lookup order (first match wins):
|
|
14
14
|
# 1. Inline credentials in the request URI itself (user:pass@host)
|
|
15
15
|
# 2. Credentials registered at runtime (from Gemfile source: URIs)
|
|
16
|
-
# 3.
|
|
17
|
-
# 4.
|
|
18
|
-
# 5.
|
|
16
|
+
# 3. Bundler local config: $BUNDLE_APP_CONFIG/config or ./.bundle/config
|
|
17
|
+
# 4. Scint config: $XDG_CONFIG_HOME/scint/credentials
|
|
18
|
+
# 5. Bundler global config: ~/.bundle/config
|
|
19
|
+
# 6. Environment variables (BUNDLE_HOST__NAME / BUNDLE_HTTPS://... format)
|
|
19
20
|
#
|
|
20
21
|
# All config files use Bundler's key format:
|
|
21
22
|
# BUNDLE_PKGS__SHOPIFY__IO: "token:secret"
|
|
@@ -80,7 +81,7 @@ module Scint
|
|
|
80
81
|
end
|
|
81
82
|
|
|
82
83
|
# 2–5. Registered + config files + env
|
|
83
|
-
auth =
|
|
84
|
+
auth = lookup_uri(uri)
|
|
84
85
|
return nil unless auth
|
|
85
86
|
|
|
86
87
|
user, password = auth.split(":", 2)
|
|
@@ -99,27 +100,35 @@ module Scint
|
|
|
99
100
|
|
|
100
101
|
private
|
|
101
102
|
|
|
102
|
-
def
|
|
103
|
-
return nil unless host
|
|
103
|
+
def lookup_uri(uri)
|
|
104
|
+
return nil unless uri&.host
|
|
104
105
|
|
|
105
106
|
# 2. Runtime-registered (from Gemfile inline URIs)
|
|
106
|
-
registered = @mutex.synchronize { @registered[host] }
|
|
107
|
+
registered = @mutex.synchronize { @registered[uri.host] }
|
|
107
108
|
return registered if registered
|
|
108
109
|
|
|
109
|
-
# 3–
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
# 3–5. Config files (bundler local, scint, bundler global)
|
|
111
|
+
keys = self.class.keys_for_uri_lookup(uri)
|
|
112
|
+
keys.each do |key|
|
|
113
|
+
val = @file_config[key]
|
|
114
|
+
return val if val
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# 6. Environment variable
|
|
118
|
+
keys.each do |key|
|
|
119
|
+
val = ENV[key]
|
|
120
|
+
return val if val
|
|
121
|
+
end
|
|
113
122
|
|
|
114
|
-
|
|
115
|
-
ENV[key]
|
|
123
|
+
nil
|
|
116
124
|
end
|
|
117
125
|
|
|
118
126
|
def load_config_files
|
|
119
127
|
config = {}
|
|
120
128
|
# Load in reverse priority (later overrides earlier)
|
|
121
|
-
load_yaml_into(config,
|
|
129
|
+
load_yaml_into(config, bundler_global_config_path)
|
|
122
130
|
load_yaml_into(config, scint_credentials_path)
|
|
131
|
+
load_yaml_into(config, bundler_local_config_path)
|
|
123
132
|
config
|
|
124
133
|
end
|
|
125
134
|
|
|
@@ -137,7 +146,17 @@ module Scint
|
|
|
137
146
|
File.join(xdg, "scint", "credentials")
|
|
138
147
|
end
|
|
139
148
|
|
|
140
|
-
def
|
|
149
|
+
def bundler_local_config_path
|
|
150
|
+
app_config = ENV["BUNDLE_APP_CONFIG"]
|
|
151
|
+
dir = if app_config && !app_config.empty?
|
|
152
|
+
app_config
|
|
153
|
+
else
|
|
154
|
+
File.join(Dir.pwd, ".bundle")
|
|
155
|
+
end
|
|
156
|
+
File.join(dir, "config")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def bundler_global_config_path
|
|
141
160
|
File.join(Dir.home, ".bundle", "config")
|
|
142
161
|
end
|
|
143
162
|
|
|
@@ -149,5 +168,49 @@ module Scint
|
|
|
149
168
|
key.upcase!
|
|
150
169
|
"BUNDLE_#{key}"
|
|
151
170
|
end
|
|
171
|
+
|
|
172
|
+
def self.key_for_uri_string(uri_string)
|
|
173
|
+
key = uri_string.to_s.dup
|
|
174
|
+
key.gsub!(".", "__")
|
|
175
|
+
key.gsub!("-", "___")
|
|
176
|
+
key.upcase!
|
|
177
|
+
"BUNDLE_#{key}"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def self.key_for_source_uri(uri)
|
|
181
|
+
uri = URI.parse(uri.to_s) unless uri.is_a?(URI)
|
|
182
|
+
keys_for_uri_lookup(uri).find { |key| key != key_for_host(uri.host) }
|
|
183
|
+
rescue StandardError
|
|
184
|
+
nil
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def self.keys_for_uri_lookup(uri)
|
|
188
|
+
uri = URI.parse(uri.to_s) unless uri.is_a?(URI)
|
|
189
|
+
keys = []
|
|
190
|
+
|
|
191
|
+
normalized = "#{uri.scheme || 'https'}://#{uri.host}"
|
|
192
|
+
default_port = (uri.scheme == "http" ? 80 : 443)
|
|
193
|
+
normalized += ":#{uri.port}" if uri.port && uri.port != default_port
|
|
194
|
+
|
|
195
|
+
path = uri.path.to_s
|
|
196
|
+
path = "/" if path.empty?
|
|
197
|
+
path = "/#{path}" unless path.start_with?("/")
|
|
198
|
+
path += "/" unless path.end_with?("/")
|
|
199
|
+
|
|
200
|
+
segments = path.split("/").reject(&:empty?)
|
|
201
|
+
candidate_paths = ["/"]
|
|
202
|
+
unless segments.empty?
|
|
203
|
+
1.upto(segments.length) do |i|
|
|
204
|
+
candidate_paths << "/#{segments.first(i).join('/')}/"
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
candidate_paths.reverse_each do |candidate_path|
|
|
209
|
+
keys << key_for_uri_string("#{normalized}#{candidate_path}")
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
keys << key_for_host(uri.host) if uri.host
|
|
213
|
+
keys.compact.uniq
|
|
214
|
+
end
|
|
152
215
|
end
|
|
153
216
|
end
|
data/lib/scint/debug/io_trace.rb
CHANGED
|
@@ -110,7 +110,9 @@ module Scint
|
|
|
110
110
|
|
|
111
111
|
next unless container.method_defined?(original_name) || container.private_method_defined?(original_name)
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
with_silenced_warnings do
|
|
114
|
+
container.send(:alias_method, method_name, original_name)
|
|
115
|
+
end
|
|
114
116
|
container.send(:remove_method, original_name)
|
|
115
117
|
|
|
116
118
|
visibility = entry[:visibility]
|
|
@@ -146,12 +148,21 @@ module Scint
|
|
|
146
148
|
visibility = method_visibility(container, method_name)
|
|
147
149
|
|
|
148
150
|
container.send(:alias_method, original_name, method_name)
|
|
149
|
-
container.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
151
|
+
if container.instance_methods(false).include?(method_name) ||
|
|
152
|
+
container.private_instance_methods(false).include?(method_name) ||
|
|
153
|
+
container.protected_instance_methods(false).include?(method_name)
|
|
154
|
+
container.send(:remove_method, method_name)
|
|
155
|
+
elsif container.method_defined?(method_name) || container.private_method_defined?(method_name) || container.protected_method_defined?(method_name)
|
|
156
|
+
container.send(:undef_method, method_name)
|
|
157
|
+
end
|
|
158
|
+
with_silenced_warnings do
|
|
159
|
+
container.send(:define_method, method_name) do |*args, **kwargs, &block|
|
|
160
|
+
Scint::Debug::IOTrace.log(op_name, args: args, kwargs: kwargs)
|
|
161
|
+
if kwargs.empty?
|
|
162
|
+
send(original_name, *args, &block)
|
|
163
|
+
else
|
|
164
|
+
send(original_name, *args, **kwargs, &block)
|
|
165
|
+
end
|
|
155
166
|
end
|
|
156
167
|
end
|
|
157
168
|
|
|
@@ -173,6 +184,14 @@ module Scint
|
|
|
173
184
|
nil
|
|
174
185
|
end
|
|
175
186
|
|
|
187
|
+
def with_silenced_warnings
|
|
188
|
+
verbose = $VERBOSE
|
|
189
|
+
$VERBOSE = nil
|
|
190
|
+
yield
|
|
191
|
+
ensure
|
|
192
|
+
$VERBOSE = verbose
|
|
193
|
+
end
|
|
194
|
+
|
|
176
195
|
def disable_unlocked
|
|
177
196
|
return unless @enabled || @log_io
|
|
178
197
|
|
|
@@ -54,7 +54,13 @@ module Scint
|
|
|
54
54
|
current_uri = URI.parse(location)
|
|
55
55
|
next
|
|
56
56
|
else
|
|
57
|
-
raise NetworkError
|
|
57
|
+
raise NetworkError.new(
|
|
58
|
+
http_error_message(current_uri, response),
|
|
59
|
+
uri: current_uri.to_s,
|
|
60
|
+
http_status: response.code.to_i,
|
|
61
|
+
response_headers: response_headers_hash(response),
|
|
62
|
+
response_body: response.body.to_s,
|
|
63
|
+
)
|
|
58
64
|
end
|
|
59
65
|
end
|
|
60
66
|
|
|
@@ -108,6 +114,24 @@ module Scint
|
|
|
108
114
|
http
|
|
109
115
|
end
|
|
110
116
|
end
|
|
117
|
+
|
|
118
|
+
def http_error_message(uri, response)
|
|
119
|
+
message = "HTTP #{response.code} for #{uri}: #{response.message}"
|
|
120
|
+
body = response.body.to_s
|
|
121
|
+
return message if body.empty?
|
|
122
|
+
|
|
123
|
+
excerpt = body.gsub(/\s+/, " ").strip
|
|
124
|
+
return message if excerpt.empty?
|
|
125
|
+
|
|
126
|
+
excerpt = "#{excerpt[0, 277]}..." if excerpt.length > 280
|
|
127
|
+
"#{message} -- #{excerpt}"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def response_headers_hash(response)
|
|
131
|
+
headers = {}
|
|
132
|
+
response.each_header { |k, v| headers[k] = v }
|
|
133
|
+
headers
|
|
134
|
+
end
|
|
111
135
|
end
|
|
112
136
|
end
|
|
113
137
|
end
|
|
@@ -4,41 +4,59 @@ require_relative "fetcher"
|
|
|
4
4
|
require_relative "../worker_pool"
|
|
5
5
|
require_relative "../platform"
|
|
6
6
|
require_relative "../errors"
|
|
7
|
+
require "uri"
|
|
7
8
|
|
|
8
9
|
module Scint
|
|
9
10
|
module Downloader
|
|
10
11
|
class Pool
|
|
11
12
|
MAX_RETRIES = 3
|
|
12
13
|
BACKOFF_BASE = 0.5 # seconds
|
|
14
|
+
DEFAULT_PER_HOST_LIMIT = 4
|
|
13
15
|
|
|
14
16
|
attr_reader :size
|
|
15
17
|
|
|
16
|
-
def initialize(size: nil, on_progress: nil, credentials: nil)
|
|
18
|
+
def initialize(size: nil, on_progress: nil, credentials: nil, per_host_limit: DEFAULT_PER_HOST_LIMIT)
|
|
17
19
|
@size = size || [Platform.cpu_count * 2, 50].min
|
|
18
20
|
@on_progress = on_progress
|
|
19
21
|
@credentials = credentials
|
|
22
|
+
@per_host_limit = [per_host_limit.to_i, 1].max
|
|
20
23
|
@fetchers = {} # thread_id => Fetcher
|
|
21
24
|
@fetcher_mutex = Thread::Mutex.new
|
|
25
|
+
@host_slots = Hash.new(0)
|
|
26
|
+
@host_waiters = {}
|
|
27
|
+
@host_mutex = Thread::Mutex.new
|
|
22
28
|
end
|
|
23
29
|
|
|
24
30
|
# Download a single URI to dest_path with retry logic.
|
|
25
31
|
# Returns { path:, size: }
|
|
26
32
|
def download(uri, dest_path, checksum: nil)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
with_host_slot(uri) do
|
|
34
|
+
retries = 0
|
|
35
|
+
begin
|
|
36
|
+
fetcher = thread_fetcher
|
|
37
|
+
fetcher.fetch(uri, dest_path, checksum: checksum)
|
|
38
|
+
rescue NetworkError, Errno::ECONNRESET, Errno::ECONNREFUSED,
|
|
39
|
+
Errno::ETIMEDOUT, Net::ReadTimeout, Net::OpenTimeout,
|
|
40
|
+
SocketError, IOError => e
|
|
41
|
+
retries += 1
|
|
42
|
+
if retries <= MAX_RETRIES
|
|
43
|
+
sleep(BACKOFF_BASE * (2**(retries - 1)))
|
|
44
|
+
# Reset connection on retry
|
|
45
|
+
reset_thread_fetcher
|
|
46
|
+
retry
|
|
47
|
+
end
|
|
48
|
+
if e.is_a?(NetworkError)
|
|
49
|
+
raise NetworkError.new(
|
|
50
|
+
"Failed to download #{uri} after #{MAX_RETRIES} retries: #{e.message}",
|
|
51
|
+
uri: (e.uri || uri.to_s),
|
|
52
|
+
http_status: e.http_status,
|
|
53
|
+
response_headers: e.response_headers,
|
|
54
|
+
response_body: e.response_body,
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
raise NetworkError, "Failed to download #{uri} after #{MAX_RETRIES} retries: #{e.message}"
|
|
40
59
|
end
|
|
41
|
-
raise NetworkError, "Failed to download #{uri} after #{MAX_RETRIES} retries: #{e.message}"
|
|
42
60
|
end
|
|
43
61
|
end
|
|
44
62
|
|
|
@@ -107,6 +125,40 @@ module Scint
|
|
|
107
125
|
old&.close
|
|
108
126
|
end
|
|
109
127
|
end
|
|
128
|
+
|
|
129
|
+
def with_host_slot(uri)
|
|
130
|
+
key = host_slot_key(uri)
|
|
131
|
+
return yield unless key
|
|
132
|
+
|
|
133
|
+
waiter = nil
|
|
134
|
+
@host_mutex.synchronize do
|
|
135
|
+
waiter = (@host_waiters[key] ||= Thread::ConditionVariable.new)
|
|
136
|
+
while @host_slots[key] >= @per_host_limit
|
|
137
|
+
waiter.wait(@host_mutex)
|
|
138
|
+
end
|
|
139
|
+
@host_slots[key] += 1
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
begin
|
|
143
|
+
yield
|
|
144
|
+
ensure
|
|
145
|
+
@host_mutex.synchronize do
|
|
146
|
+
@host_slots[key] -= 1 if @host_slots[key] > 0
|
|
147
|
+
waiter.broadcast
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def host_slot_key(uri)
|
|
153
|
+
parsed = uri.is_a?(URI) ? uri : URI.parse(uri.to_s)
|
|
154
|
+
return nil unless parsed.host
|
|
155
|
+
|
|
156
|
+
scheme = parsed.scheme || "https"
|
|
157
|
+
port = parsed.port || (scheme == "https" ? 443 : 80)
|
|
158
|
+
"#{scheme}://#{parsed.host}:#{port}"
|
|
159
|
+
rescue URI::InvalidURIError
|
|
160
|
+
nil
|
|
161
|
+
end
|
|
110
162
|
end
|
|
111
163
|
end
|
|
112
164
|
end
|
data/lib/scint/errors.rb
CHANGED
|
@@ -26,6 +26,16 @@ module Scint
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
class NetworkError < BundlerError
|
|
29
|
+
attr_reader :uri, :http_status, :response_headers, :response_body
|
|
30
|
+
|
|
31
|
+
def initialize(message = nil, uri: nil, http_status: nil, response_headers: nil, response_body: nil)
|
|
32
|
+
super(message)
|
|
33
|
+
@uri = uri
|
|
34
|
+
@http_status = http_status
|
|
35
|
+
@response_headers = response_headers
|
|
36
|
+
@response_body = response_body
|
|
37
|
+
end
|
|
38
|
+
|
|
29
39
|
def status_code
|
|
30
40
|
7
|
|
31
41
|
end
|
data/lib/scint/fs.rb
CHANGED
|
@@ -32,6 +32,11 @@ module Scint
|
|
|
32
32
|
return if system("cp", "-c", src, dst, [:out, :err] => File::NULL)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
# Try Linux reflink copy-on-write where supported (btrfs/xfs/etc).
|
|
36
|
+
if Platform.linux?
|
|
37
|
+
return if system("cp", "--reflink=always", src, dst, [:out, :err] => File::NULL)
|
|
38
|
+
end
|
|
39
|
+
|
|
35
40
|
# Fallback: hardlink
|
|
36
41
|
begin
|
|
37
42
|
File.link(src, dst)
|
|
@@ -44,11 +49,73 @@ module Scint
|
|
|
44
49
|
FileUtils.cp(src, dst)
|
|
45
50
|
end
|
|
46
51
|
|
|
52
|
+
# Recursively clone directory tree from src_dir into dst_dir.
|
|
53
|
+
# On macOS/APFS, prefers CoW clones via `cp -cR`.
|
|
54
|
+
# Falls back to hardlink_tree, then regular copy per-file if needed.
|
|
55
|
+
def clone_tree(src_dir, dst_dir)
|
|
56
|
+
src_dir = src_dir.to_s
|
|
57
|
+
dst_dir = dst_dir.to_s
|
|
58
|
+
raise Errno::ENOENT, src_dir unless Dir.exist?(src_dir)
|
|
59
|
+
mkdir_p(dst_dir)
|
|
60
|
+
|
|
61
|
+
# Fast path on macOS/APFS: copy-on-write clone of full tree.
|
|
62
|
+
if Platform.macos?
|
|
63
|
+
src_contents = File.join(src_dir, ".")
|
|
64
|
+
return if system("cp", "-cR", src_contents, dst_dir, [:out, :err] => File::NULL)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Fast path on Linux filesystems with reflink support.
|
|
68
|
+
if Platform.linux?
|
|
69
|
+
src_contents = File.join(src_dir, ".")
|
|
70
|
+
return if system("cp", "--reflink=always", "-R", src_contents, dst_dir, [:out, :err] => File::NULL)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
hardlink_tree(src_dir, dst_dir)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Clone many source directories into one destination parent directory.
|
|
77
|
+
# This is significantly faster than one process per gem on large warm
|
|
78
|
+
# installs because it batches cp invocations while preserving CoW/reflink.
|
|
79
|
+
# Returns the number of source trees requested.
|
|
80
|
+
def clone_many_trees(src_dirs, dst_parent, chunk_size: 64)
|
|
81
|
+
dst_parent = dst_parent.to_s
|
|
82
|
+
mkdir_p(dst_parent)
|
|
83
|
+
|
|
84
|
+
sources = Array(src_dirs).map(&:to_s).uniq
|
|
85
|
+
sources.select! { |src| Dir.exist?(src) }
|
|
86
|
+
return 0 if sources.empty?
|
|
87
|
+
|
|
88
|
+
copied = 0
|
|
89
|
+
sources.each_slice([chunk_size.to_i, 1].max) do |slice|
|
|
90
|
+
pending = slice.reject do |src|
|
|
91
|
+
Dir.exist?(File.join(dst_parent, File.basename(src)))
|
|
92
|
+
end
|
|
93
|
+
next if pending.empty?
|
|
94
|
+
|
|
95
|
+
ok = false
|
|
96
|
+
if Platform.macos?
|
|
97
|
+
ok = system("cp", "-cR", *pending, dst_parent, [:out, :err] => File::NULL)
|
|
98
|
+
elsif Platform.linux?
|
|
99
|
+
ok = system("cp", "--reflink=always", "-R", *pending, dst_parent, [:out, :err] => File::NULL)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
unless ok
|
|
103
|
+
pending.each do |src|
|
|
104
|
+
clone_tree(src, File.join(dst_parent, File.basename(src)))
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
copied += pending.length
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
copied
|
|
112
|
+
end
|
|
113
|
+
|
|
47
114
|
# Recursively hardlink all files from src_dir into dst_dir.
|
|
48
115
|
# Directory structure is recreated; files are hardlinked.
|
|
49
116
|
def hardlink_tree(src_dir, dst_dir)
|
|
50
|
-
src_dir = src_dir.to_s
|
|
51
|
-
dst_dir = dst_dir.to_s
|
|
117
|
+
src_dir = File.expand_path(src_dir.to_s)
|
|
118
|
+
dst_dir = File.expand_path(dst_dir.to_s)
|
|
52
119
|
raise Errno::ENOENT, src_dir unless Dir.exist?(src_dir)
|
|
53
120
|
mkdir_p(dst_dir)
|
|
54
121
|
|
|
@@ -59,6 +126,10 @@ module Scint
|
|
|
59
126
|
Dir.each_child(src_root) do |entry|
|
|
60
127
|
src_path = File.join(src_root, entry)
|
|
61
128
|
dst_path = File.join(dst_root, entry)
|
|
129
|
+
|
|
130
|
+
# Guard against recursive copy when destination is nested under source.
|
|
131
|
+
next if dst_dir == src_path || dst_dir.start_with?("#{src_path}/")
|
|
132
|
+
|
|
62
133
|
stat = File.lstat(src_path)
|
|
63
134
|
|
|
64
135
|
if stat.directory?
|
|
@@ -68,10 +139,26 @@ module Scint
|
|
|
68
139
|
end
|
|
69
140
|
|
|
70
141
|
mkdir_p(File.dirname(dst_path))
|
|
142
|
+
# Another worker may have already materialized this file.
|
|
143
|
+
next if File.exist?(dst_path)
|
|
144
|
+
|
|
71
145
|
begin
|
|
72
146
|
File.link(src_path, dst_path)
|
|
147
|
+
rescue Errno::EEXIST
|
|
148
|
+
# Lost a race to another concurrent linker; destination is valid.
|
|
149
|
+
next
|
|
73
150
|
rescue SystemCallError
|
|
74
|
-
|
|
151
|
+
# TOCTOU guard: destination may have appeared after File.link failed.
|
|
152
|
+
next if File.exist?(dst_path)
|
|
153
|
+
|
|
154
|
+
begin
|
|
155
|
+
clonefile(src_path, dst_path)
|
|
156
|
+
rescue StandardError
|
|
157
|
+
# If a concurrent worker created destination in the meantime,
|
|
158
|
+
# treat this as success; otherwise bubble up.
|
|
159
|
+
next if File.exist?(dst_path)
|
|
160
|
+
raise
|
|
161
|
+
end
|
|
75
162
|
end
|
|
76
163
|
end
|
|
77
164
|
end
|
data/lib/scint/gemfile/parser.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "dependency"
|
|
4
|
+
require_relative "../source/path"
|
|
4
5
|
|
|
5
6
|
module Scint
|
|
6
7
|
module Gemfile
|
|
@@ -134,6 +135,11 @@ module Scint
|
|
|
134
135
|
source_opts[:path] = path_val
|
|
135
136
|
end
|
|
136
137
|
|
|
138
|
+
# Internal/source metadata used by lockfile generation.
|
|
139
|
+
source_opts[:glob] = options.delete(:glob) if options.key?(:glob)
|
|
140
|
+
source_opts[:gemspec_generated] = options.delete(:gemspec_generated) if options.key?(:gemspec_generated)
|
|
141
|
+
source_opts[:gemspec_primary] = options.delete(:gemspec_primary) if options.key?(:gemspec_primary)
|
|
142
|
+
|
|
137
143
|
if options[:source]
|
|
138
144
|
source_opts[:source] = options.delete(:source)
|
|
139
145
|
end
|
|
@@ -213,24 +219,39 @@ module Scint
|
|
|
213
219
|
instance_eval(contents, expanded, 1)
|
|
214
220
|
end
|
|
215
221
|
|
|
216
|
-
def ruby(
|
|
217
|
-
|
|
222
|
+
def ruby(*versions, **opts)
|
|
223
|
+
version_parts = versions.flatten.compact.map(&:to_s)
|
|
224
|
+
@ruby_version = version_parts.join(", ") unless version_parts.empty?
|
|
218
225
|
end
|
|
219
226
|
|
|
220
227
|
def gemspec(opts = {})
|
|
221
228
|
path = opts[:path] || "."
|
|
222
229
|
name = opts[:name]
|
|
230
|
+
glob = opts[:glob] || Scint::Source::Path::DEFAULT_GLOB
|
|
223
231
|
dir = File.expand_path(path, File.dirname(@gemfile_path))
|
|
224
|
-
gemspecs = Dir.glob(File.join(dir,
|
|
232
|
+
gemspecs = Dir.glob(File.join(dir, glob)).sort
|
|
225
233
|
# Just record we have a gemspec source -- full spec loading is
|
|
226
234
|
# deferred to the resolver/installer.
|
|
227
235
|
gemspecs.each do |gs|
|
|
228
236
|
spec_name = File.basename(gs, ".gemspec")
|
|
229
237
|
next if name && spec_name != name
|
|
230
|
-
gem(
|
|
238
|
+
gem(
|
|
239
|
+
spec_name,
|
|
240
|
+
path: File.dirname(gs),
|
|
241
|
+
glob: glob,
|
|
242
|
+
gemspec_generated: true,
|
|
243
|
+
gemspec_primary: File.expand_path(File.dirname(gs)) == dir,
|
|
244
|
+
)
|
|
231
245
|
end
|
|
232
246
|
end
|
|
233
247
|
|
|
248
|
+
def install_if(*conditions, &blk)
|
|
249
|
+
raise GemfileError, "install_if requires a block" unless block_given?
|
|
250
|
+
return unless conditions.all? { |condition| condition_truthy?(condition) }
|
|
251
|
+
|
|
252
|
+
yield
|
|
253
|
+
end
|
|
254
|
+
|
|
234
255
|
# Silently ignore plugin declarations
|
|
235
256
|
def plugin(*args); end
|
|
236
257
|
|
|
@@ -247,6 +268,12 @@ module Scint
|
|
|
247
268
|
|
|
248
269
|
private
|
|
249
270
|
|
|
271
|
+
def condition_truthy?(condition)
|
|
272
|
+
return condition.call if condition.respond_to?(:call)
|
|
273
|
+
|
|
274
|
+
!!condition
|
|
275
|
+
end
|
|
276
|
+
|
|
250
277
|
def add_default_git_sources
|
|
251
278
|
git_source(:github) do |repo_name|
|
|
252
279
|
if repo_name =~ %r{\Ahttps://github\.com/([^/]+/[^/]+)/pull/(\d+)\z}
|
data/lib/scint/index/client.rb
CHANGED