scint 0.1.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 +7 -0
- data/FEATURES.md +13 -0
- data/README.md +216 -0
- data/bin/bundler-vs-scint +233 -0
- data/bin/scint +35 -0
- data/bin/scint-io-summary +46 -0
- data/bin/scint-syscall-trace +41 -0
- data/lib/bundler/setup.rb +5 -0
- data/lib/bundler.rb +168 -0
- data/lib/scint/cache/layout.rb +131 -0
- data/lib/scint/cache/metadata_store.rb +75 -0
- data/lib/scint/cache/prewarm.rb +192 -0
- data/lib/scint/cli/add.rb +85 -0
- data/lib/scint/cli/cache.rb +316 -0
- data/lib/scint/cli/exec.rb +150 -0
- data/lib/scint/cli/install.rb +1047 -0
- data/lib/scint/cli/remove.rb +60 -0
- data/lib/scint/cli.rb +77 -0
- data/lib/scint/commands/exec.rb +17 -0
- data/lib/scint/commands/install.rb +17 -0
- data/lib/scint/credentials.rb +153 -0
- data/lib/scint/debug/io_trace.rb +218 -0
- data/lib/scint/debug/sampler.rb +138 -0
- data/lib/scint/downloader/fetcher.rb +113 -0
- data/lib/scint/downloader/pool.rb +112 -0
- data/lib/scint/errors.rb +63 -0
- data/lib/scint/fs.rb +119 -0
- data/lib/scint/gem/extractor.rb +86 -0
- data/lib/scint/gem/package.rb +62 -0
- data/lib/scint/gemfile/dependency.rb +30 -0
- data/lib/scint/gemfile/editor.rb +93 -0
- data/lib/scint/gemfile/parser.rb +275 -0
- data/lib/scint/index/cache.rb +166 -0
- data/lib/scint/index/client.rb +301 -0
- data/lib/scint/index/parser.rb +142 -0
- data/lib/scint/installer/extension_builder.rb +264 -0
- data/lib/scint/installer/linker.rb +226 -0
- data/lib/scint/installer/planner.rb +140 -0
- data/lib/scint/installer/preparer.rb +207 -0
- data/lib/scint/lockfile/parser.rb +251 -0
- data/lib/scint/lockfile/writer.rb +178 -0
- data/lib/scint/platform.rb +71 -0
- data/lib/scint/progress.rb +579 -0
- data/lib/scint/resolver/provider.rb +230 -0
- data/lib/scint/resolver/resolver.rb +249 -0
- data/lib/scint/runtime/exec.rb +141 -0
- data/lib/scint/runtime/setup.rb +45 -0
- data/lib/scint/scheduler.rb +392 -0
- data/lib/scint/source/base.rb +46 -0
- data/lib/scint/source/git.rb +92 -0
- data/lib/scint/source/path.rb +70 -0
- data/lib/scint/source/rubygems.rb +79 -0
- data/lib/scint/vendor/pub_grub/assignment.rb +20 -0
- data/lib/scint/vendor/pub_grub/basic_package_source.rb +169 -0
- data/lib/scint/vendor/pub_grub/failure_writer.rb +182 -0
- data/lib/scint/vendor/pub_grub/incompatibility.rb +150 -0
- data/lib/scint/vendor/pub_grub/package.rb +43 -0
- data/lib/scint/vendor/pub_grub/partial_solution.rb +121 -0
- data/lib/scint/vendor/pub_grub/rubygems.rb +45 -0
- data/lib/scint/vendor/pub_grub/solve_failure.rb +19 -0
- data/lib/scint/vendor/pub_grub/static_package_source.rb +61 -0
- data/lib/scint/vendor/pub_grub/strategy.rb +42 -0
- data/lib/scint/vendor/pub_grub/term.rb +105 -0
- data/lib/scint/vendor/pub_grub/version.rb +3 -0
- data/lib/scint/vendor/pub_grub/version_constraint.rb +129 -0
- data/lib/scint/vendor/pub_grub/version_range.rb +423 -0
- data/lib/scint/vendor/pub_grub/version_solver.rb +236 -0
- data/lib/scint/vendor/pub_grub/version_union.rb +178 -0
- data/lib/scint/vendor/pub_grub.rb +32 -0
- data/lib/scint/worker_pool.rb +114 -0
- data/lib/scint.rb +87 -0
- metadata +116 -0
data/lib/bundler.rb
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "base64"
|
|
5
|
+
require "scint/runtime/setup"
|
|
6
|
+
require "scint/gemfile/parser"
|
|
7
|
+
|
|
8
|
+
# Minimal Bundler compatibility shim for `scint exec`.
|
|
9
|
+
# This intentionally implements only the subset commonly used by apps:
|
|
10
|
+
# - `require "bundler/setup"` (handled by lib/bundler/setup.rb)
|
|
11
|
+
# - `Bundler.setup`
|
|
12
|
+
# - `Bundler.require`
|
|
13
|
+
module Bundler
|
|
14
|
+
RUNTIME_LOCK = "scint.lock.marshal"
|
|
15
|
+
ORIGINAL_ENV = begin
|
|
16
|
+
encoded = ENV["SCINT_ORIGINAL_ENV"]
|
|
17
|
+
if encoded && !encoded.empty?
|
|
18
|
+
Marshal.load(Base64.decode64(encoded))
|
|
19
|
+
else
|
|
20
|
+
ENV.to_hash
|
|
21
|
+
end
|
|
22
|
+
rescue StandardError
|
|
23
|
+
ENV.to_hash
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
def setup(*_groups)
|
|
28
|
+
return @lock_data if @lock_data
|
|
29
|
+
|
|
30
|
+
lock_path = find_runtime_lock
|
|
31
|
+
unless lock_path
|
|
32
|
+
raise LoadError, "Runtime lock not found. Run `scint install` first."
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
ENV["SCINT_RUNTIME_LOCK"] ||= lock_path
|
|
36
|
+
@lock_data = ::Scint::Runtime::Setup.setup(lock_path)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def require(*_groups)
|
|
40
|
+
groups = _groups.flatten.compact.map(&:to_sym)
|
|
41
|
+
groups = [:default] if groups.empty?
|
|
42
|
+
|
|
43
|
+
setup
|
|
44
|
+
|
|
45
|
+
gemfile_dependencies.each do |dep|
|
|
46
|
+
next unless dependency_in_groups?(dep, groups)
|
|
47
|
+
|
|
48
|
+
targets = require_targets_for(dep)
|
|
49
|
+
targets.each { |target| require_one(target) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
true
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def root
|
|
56
|
+
gemfile = ENV["BUNDLE_GEMFILE"]
|
|
57
|
+
return Pathname.new(Dir.pwd) unless gemfile && !gemfile.empty?
|
|
58
|
+
|
|
59
|
+
Pathname.new(File.dirname(gemfile))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def bundle_path
|
|
63
|
+
root.join(".bundle")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def load
|
|
67
|
+
setup
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def with_unbundled_env
|
|
71
|
+
previous = ENV.to_hash
|
|
72
|
+
ENV.replace(unbundled_env)
|
|
73
|
+
yield
|
|
74
|
+
ensure
|
|
75
|
+
ENV.replace(previous) if previous
|
|
76
|
+
end
|
|
77
|
+
alias with_original_env with_unbundled_env
|
|
78
|
+
|
|
79
|
+
def original_env
|
|
80
|
+
ORIGINAL_ENV.dup
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def unbundled_env
|
|
84
|
+
env = original_env
|
|
85
|
+
env["RUBYOPT"] = filter_bundler_setup_from_rubyopt(env["RUBYOPT"])
|
|
86
|
+
env.delete("SCINT_RUNTIME_LOCK")
|
|
87
|
+
env.delete("SCINT_ORIGINAL_ENV")
|
|
88
|
+
env
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def require_one(gem_name)
|
|
94
|
+
name = gem_name.to_s
|
|
95
|
+
candidates = [name, name.tr("-", "_"), name.tr("-", "/")].uniq
|
|
96
|
+
last_error = nil
|
|
97
|
+
|
|
98
|
+
candidates.each do |candidate|
|
|
99
|
+
begin
|
|
100
|
+
Kernel.require(candidate)
|
|
101
|
+
return true
|
|
102
|
+
rescue LoadError => e
|
|
103
|
+
last_error = e
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
raise last_error if last_error
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def gemfile_dependencies
|
|
111
|
+
return @gemfile_dependencies if defined?(@gemfile_dependencies)
|
|
112
|
+
|
|
113
|
+
gemfile = ENV["BUNDLE_GEMFILE"]
|
|
114
|
+
@gemfile_dependencies =
|
|
115
|
+
if gemfile && File.exist?(gemfile)
|
|
116
|
+
::Scint::Gemfile::Parser.parse(gemfile).dependencies
|
|
117
|
+
else
|
|
118
|
+
[]
|
|
119
|
+
end
|
|
120
|
+
rescue StandardError
|
|
121
|
+
@gemfile_dependencies = []
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def dependency_in_groups?(dep, groups)
|
|
125
|
+
dep_groups = Array(dep.groups).map(&:to_sym)
|
|
126
|
+
(dep_groups & groups).any?
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def require_targets_for(dep)
|
|
130
|
+
req = dep.require_paths
|
|
131
|
+
return [] if req == []
|
|
132
|
+
return [dep.name] if req.nil?
|
|
133
|
+
|
|
134
|
+
Array(req)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def find_runtime_lock
|
|
138
|
+
env_path = ENV["SCINT_RUNTIME_LOCK"]
|
|
139
|
+
return env_path if env_path && File.exist?(env_path)
|
|
140
|
+
|
|
141
|
+
gemfile = ENV["BUNDLE_GEMFILE"]
|
|
142
|
+
if gemfile && !gemfile.empty?
|
|
143
|
+
candidate = File.join(File.dirname(gemfile), ".bundle", RUNTIME_LOCK)
|
|
144
|
+
return candidate if File.exist?(candidate)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
dir = Dir.pwd
|
|
148
|
+
loop do
|
|
149
|
+
candidate = File.join(dir, ".bundle", RUNTIME_LOCK)
|
|
150
|
+
return candidate if File.exist?(candidate)
|
|
151
|
+
|
|
152
|
+
parent = File.dirname(dir)
|
|
153
|
+
break if parent == dir
|
|
154
|
+
dir = parent
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
nil
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def filter_bundler_setup_from_rubyopt(value)
|
|
161
|
+
parts = value.to_s.split(/\s+/).reject(&:empty?)
|
|
162
|
+
filtered = parts.reject do |entry|
|
|
163
|
+
entry == "-rbundler/setup" || entry.end_with?("/bundler/setup")
|
|
164
|
+
end
|
|
165
|
+
filtered.join(" ")
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../fs"
|
|
4
|
+
require_relative "../platform"
|
|
5
|
+
require "digest"
|
|
6
|
+
require "uri"
|
|
7
|
+
|
|
8
|
+
module Scint
|
|
9
|
+
module Cache
|
|
10
|
+
class Layout
|
|
11
|
+
attr_reader :root
|
|
12
|
+
|
|
13
|
+
def initialize(root: nil)
|
|
14
|
+
@root = root || default_root
|
|
15
|
+
@ensured_dirs = {}
|
|
16
|
+
@mutex = Thread::Mutex.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# -- Top-level directories -----------------------------------------------
|
|
20
|
+
|
|
21
|
+
def inbound_dir
|
|
22
|
+
File.join(@root, "inbound")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def extracted_dir
|
|
26
|
+
File.join(@root, "extracted")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def ext_dir
|
|
30
|
+
File.join(@root, "ext")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def index_dir
|
|
34
|
+
File.join(@root, "index")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def git_dir
|
|
38
|
+
File.join(@root, "git")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Isolated gem home used while compiling native extensions during install.
|
|
42
|
+
# This keeps build-time gem activation hermetic to scint-managed paths.
|
|
43
|
+
def install_env_dir
|
|
44
|
+
File.join(@root, "install-env")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def install_ruby_dir
|
|
48
|
+
File.join(install_env_dir, "ruby", RUBY_VERSION.split(".")[0, 2].join(".") + ".0")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# -- Per-spec paths ------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
def inbound_path(spec)
|
|
54
|
+
File.join(inbound_dir, "#{full_name(spec)}.gem")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def extracted_path(spec)
|
|
58
|
+
File.join(extracted_dir, full_name(spec))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def spec_cache_path(spec)
|
|
62
|
+
File.join(extracted_dir, "#{full_name(spec)}.spec.marshal")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def ext_path(spec, abi_key = Platform.abi_key)
|
|
66
|
+
File.join(ext_dir, abi_key, full_name(spec))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# -- Per-source paths ----------------------------------------------------
|
|
70
|
+
|
|
71
|
+
def index_path(source)
|
|
72
|
+
slug = if source.respond_to?(:cache_slug)
|
|
73
|
+
source.cache_slug
|
|
74
|
+
else
|
|
75
|
+
slugify_uri(source.to_s)
|
|
76
|
+
end
|
|
77
|
+
File.join(index_dir, slug)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def git_path(uri)
|
|
81
|
+
slug = Digest::SHA256.hexdigest(uri.to_s)[0, 16]
|
|
82
|
+
File.join(git_dir, slug)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# -- Helpers -------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
def full_name(spec)
|
|
88
|
+
name = spec.respond_to?(:name) ? spec.name : spec[:name]
|
|
89
|
+
version = spec.respond_to?(:version) ? spec.version : spec[:version]
|
|
90
|
+
platform = spec.respond_to?(:platform) ? spec.platform : spec[:platform]
|
|
91
|
+
|
|
92
|
+
base = "#{name}-#{version}"
|
|
93
|
+
if platform && platform.to_s != "ruby" && platform.to_s != ""
|
|
94
|
+
"#{base}-#{platform}"
|
|
95
|
+
else
|
|
96
|
+
base
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Ensure a directory exists (thread-safe, cached).
|
|
101
|
+
def ensure_dir(path)
|
|
102
|
+
return if @ensured_dirs[path]
|
|
103
|
+
|
|
104
|
+
@mutex.synchronize do
|
|
105
|
+
return if @ensured_dirs[path]
|
|
106
|
+
FS.mkdir_p(path)
|
|
107
|
+
@ensured_dirs[path] = true
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def default_root
|
|
114
|
+
base = ENV["XDG_CACHE_HOME"] || File.join(Dir.home, ".cache")
|
|
115
|
+
File.join(base, "scint")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def slugify_uri(str)
|
|
119
|
+
uri = URI.parse(str) rescue nil
|
|
120
|
+
if uri && uri.host
|
|
121
|
+
path = uri.path.to_s.gsub("/", "-").sub(/^-/, "")
|
|
122
|
+
slug = uri.host
|
|
123
|
+
slug += path unless path.empty? || path == "-"
|
|
124
|
+
slug
|
|
125
|
+
else
|
|
126
|
+
Digest::SHA256.hexdigest(str)[0, 16]
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../fs"
|
|
4
|
+
|
|
5
|
+
module Scint
|
|
6
|
+
module Cache
|
|
7
|
+
class MetadataStore
|
|
8
|
+
def initialize(path)
|
|
9
|
+
@path = path
|
|
10
|
+
@data = nil
|
|
11
|
+
@mutex = Thread::Mutex.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Load specs hash from disk. Returns {} if file missing or corrupt.
|
|
15
|
+
def load
|
|
16
|
+
@mutex.synchronize do
|
|
17
|
+
return @data if @data
|
|
18
|
+
@data = load_from_disk
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Save specs hash to disk atomically.
|
|
23
|
+
def save(specs_hash)
|
|
24
|
+
@mutex.synchronize do
|
|
25
|
+
@data = specs_hash
|
|
26
|
+
FS.atomic_write(@path, Marshal.dump(specs_hash))
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Check if a gem is installed. specs_hash keys are "name-version" or "name-version-platform".
|
|
31
|
+
def installed?(name, version, platform = "ruby")
|
|
32
|
+
data = load
|
|
33
|
+
key = cache_key(name, version, platform)
|
|
34
|
+
data.key?(key)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Add a single entry.
|
|
38
|
+
def add(name, version, platform = "ruby")
|
|
39
|
+
@mutex.synchronize do
|
|
40
|
+
@data ||= load_from_disk
|
|
41
|
+
key = cache_key(name, version, platform)
|
|
42
|
+
@data[key] = true
|
|
43
|
+
FS.atomic_write(@path, Marshal.dump(@data))
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Remove a single entry.
|
|
48
|
+
def remove(name, version, platform = "ruby")
|
|
49
|
+
@mutex.synchronize do
|
|
50
|
+
@data ||= load_from_disk
|
|
51
|
+
key = cache_key(name, version, platform)
|
|
52
|
+
@data.delete(key)
|
|
53
|
+
FS.atomic_write(@path, Marshal.dump(@data))
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def cache_key(name, version, platform)
|
|
60
|
+
if platform && platform.to_s != "ruby" && platform.to_s != ""
|
|
61
|
+
"#{name}-#{version}-#{platform}"
|
|
62
|
+
else
|
|
63
|
+
"#{name}-#{version}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def load_from_disk
|
|
68
|
+
return {} unless File.exist?(@path)
|
|
69
|
+
Marshal.load(File.binread(@path))
|
|
70
|
+
rescue ArgumentError, TypeError, EOFError
|
|
71
|
+
{}
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require_relative "layout"
|
|
5
|
+
require_relative "../errors"
|
|
6
|
+
require_relative "../downloader/pool"
|
|
7
|
+
require_relative "../gem/package"
|
|
8
|
+
require_relative "../fs"
|
|
9
|
+
require_relative "../platform"
|
|
10
|
+
require_relative "../worker_pool"
|
|
11
|
+
|
|
12
|
+
module Scint
|
|
13
|
+
module Cache
|
|
14
|
+
class Prewarm
|
|
15
|
+
Task = Struct.new(:spec, :download, :extract, keyword_init: true)
|
|
16
|
+
|
|
17
|
+
def initialize(cache_layout: Layout.new, jobs: nil, credentials: nil, force: false,
|
|
18
|
+
downloader_factory: nil)
|
|
19
|
+
@cache = cache_layout
|
|
20
|
+
@jobs = [[jobs || (Platform.cpu_count * 2), 1].max, 50].min
|
|
21
|
+
@credentials = credentials
|
|
22
|
+
@force = force
|
|
23
|
+
@downloader_factory = downloader_factory || lambda { |size, creds|
|
|
24
|
+
Downloader::Pool.new(size: size, credentials: creds)
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Returns summary hash:
|
|
29
|
+
# { warmed:, skipped:, ignored:, failed:, failures: [] }
|
|
30
|
+
def run(specs)
|
|
31
|
+
failures = []
|
|
32
|
+
ignored = 0
|
|
33
|
+
skipped = 0
|
|
34
|
+
|
|
35
|
+
tasks = []
|
|
36
|
+
specs.each do |spec|
|
|
37
|
+
if prewarmable?(spec)
|
|
38
|
+
tasks << task_for(spec)
|
|
39
|
+
else
|
|
40
|
+
ignored += 1
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
tasks.each do |task|
|
|
45
|
+
next unless @force
|
|
46
|
+
|
|
47
|
+
purge_artifacts(task.spec)
|
|
48
|
+
task.download = true
|
|
49
|
+
task.extract = true
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
tasks.each do |task|
|
|
53
|
+
if !task.download && !task.extract
|
|
54
|
+
skipped += 1
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
work_tasks = tasks.select { |task| task.download || task.extract }
|
|
59
|
+
return result_hash(work_tasks.size, skipped, ignored, failures) if work_tasks.empty?
|
|
60
|
+
|
|
61
|
+
download_errors = download_tasks(work_tasks.select(&:download))
|
|
62
|
+
failures.concat(download_errors)
|
|
63
|
+
|
|
64
|
+
remaining = work_tasks.reject do |task|
|
|
65
|
+
failures.any? { |f| f[:spec] == task.spec }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
extract_errors = extract_tasks(remaining.select(&:extract))
|
|
69
|
+
failures.concat(extract_errors)
|
|
70
|
+
|
|
71
|
+
result_hash(work_tasks.size - failures.size, skipped, ignored, failures)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def task_for(spec)
|
|
77
|
+
inbound = @cache.inbound_path(spec)
|
|
78
|
+
extracted = @cache.extracted_path(spec)
|
|
79
|
+
metadata = @cache.spec_cache_path(spec)
|
|
80
|
+
|
|
81
|
+
Task.new(
|
|
82
|
+
spec: spec,
|
|
83
|
+
download: !File.exist?(inbound),
|
|
84
|
+
extract: !Dir.exist?(extracted) || !File.exist?(metadata),
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def prewarmable?(spec)
|
|
89
|
+
source = spec.source
|
|
90
|
+
source_str = source.to_s
|
|
91
|
+
source_str.start_with?("http://", "https://")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def download_tasks(tasks)
|
|
95
|
+
return [] if tasks.empty?
|
|
96
|
+
|
|
97
|
+
downloader = @downloader_factory.call(@jobs, @credentials)
|
|
98
|
+
items = tasks.map do |task|
|
|
99
|
+
spec = task.spec
|
|
100
|
+
{
|
|
101
|
+
spec: spec,
|
|
102
|
+
uri: download_uri_for(spec),
|
|
103
|
+
dest: @cache.inbound_path(spec),
|
|
104
|
+
checksum: spec.respond_to?(:checksum) ? spec.checksum : nil,
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
results = downloader.download_batch(items)
|
|
109
|
+
failures = results.select { |r| r[:error] }.map do |r|
|
|
110
|
+
{ spec: r[:spec], error: r[:error] }
|
|
111
|
+
end
|
|
112
|
+
failures
|
|
113
|
+
ensure
|
|
114
|
+
downloader&.close
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def extract_tasks(tasks)
|
|
118
|
+
return [] if tasks.empty?
|
|
119
|
+
|
|
120
|
+
failures = []
|
|
121
|
+
mutex = Thread::Mutex.new
|
|
122
|
+
done = Thread::Queue.new
|
|
123
|
+
|
|
124
|
+
pool = WorkerPool.new(@jobs, name: "prewarm-extract")
|
|
125
|
+
pool.start do |task|
|
|
126
|
+
spec = task.spec
|
|
127
|
+
inbound = @cache.inbound_path(spec)
|
|
128
|
+
|
|
129
|
+
unless File.exist?(inbound)
|
|
130
|
+
raise CacheError, "Missing downloaded gem for #{spec.name}: #{inbound}"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
extracted = @cache.extracted_path(spec)
|
|
134
|
+
metadata = @cache.spec_cache_path(spec)
|
|
135
|
+
|
|
136
|
+
gemspec = nil
|
|
137
|
+
if task.extract
|
|
138
|
+
FileUtils.rm_rf(extracted)
|
|
139
|
+
FS.mkdir_p(extracted)
|
|
140
|
+
result = GemPkg::Package.new.extract(inbound, extracted)
|
|
141
|
+
gemspec = result[:gemspec]
|
|
142
|
+
elsif !File.exist?(metadata)
|
|
143
|
+
gemspec = GemPkg::Package.new.read_metadata(inbound)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
if gemspec
|
|
147
|
+
FS.atomic_write(metadata, gemspec.to_yaml)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
true
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
tasks.each do |task|
|
|
154
|
+
pool.enqueue(task) do |job|
|
|
155
|
+
mutex.synchronize do
|
|
156
|
+
if job[:state] == :failed
|
|
157
|
+
failures << { spec: job[:payload].spec, error: job[:error] }
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
done.push(true)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
tasks.size.times { done.pop }
|
|
165
|
+
pool.stop
|
|
166
|
+
|
|
167
|
+
failures
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def download_uri_for(spec)
|
|
171
|
+
source = spec.source.to_s.chomp("/")
|
|
172
|
+
"#{source}/gems/#{@cache.full_name(spec)}.gem"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def purge_artifacts(spec)
|
|
176
|
+
FileUtils.rm_f(@cache.inbound_path(spec))
|
|
177
|
+
FileUtils.rm_rf(@cache.extracted_path(spec))
|
|
178
|
+
FileUtils.rm_f(@cache.spec_cache_path(spec))
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def result_hash(warmed, skipped, ignored, failures)
|
|
182
|
+
{
|
|
183
|
+
warmed: warmed,
|
|
184
|
+
skipped: skipped,
|
|
185
|
+
ignored: ignored,
|
|
186
|
+
failed: failures.size,
|
|
187
|
+
failures: failures,
|
|
188
|
+
}
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../errors"
|
|
4
|
+
require_relative "../gemfile/editor"
|
|
5
|
+
require_relative "install"
|
|
6
|
+
|
|
7
|
+
module Scint
|
|
8
|
+
module CLI
|
|
9
|
+
class Add
|
|
10
|
+
def initialize(argv = [])
|
|
11
|
+
@argv = argv.dup
|
|
12
|
+
@skip_install = false
|
|
13
|
+
@requirement = nil
|
|
14
|
+
@group = nil
|
|
15
|
+
@source = nil
|
|
16
|
+
@git = nil
|
|
17
|
+
@path = nil
|
|
18
|
+
parse_options
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def run
|
|
22
|
+
if @gems.empty?
|
|
23
|
+
$stderr.puts "Usage: scint add GEM [GEM...] [options]"
|
|
24
|
+
return 1
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
editor = Gemfile::Editor.new("Gemfile")
|
|
28
|
+
@gems.each do |gem_name|
|
|
29
|
+
result = editor.add(
|
|
30
|
+
gem_name,
|
|
31
|
+
requirement: @requirement,
|
|
32
|
+
group: @group,
|
|
33
|
+
source: @source,
|
|
34
|
+
git: @git,
|
|
35
|
+
path: @path,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
action = result == :updated ? "Updated" : "Added"
|
|
39
|
+
$stdout.puts "#{action} #{gem_name} in Gemfile"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
return 0 if @skip_install
|
|
43
|
+
|
|
44
|
+
CLI::Install.new([]).run
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def parse_options
|
|
50
|
+
@gems = []
|
|
51
|
+
i = 0
|
|
52
|
+
while i < @argv.length
|
|
53
|
+
token = @argv[i]
|
|
54
|
+
|
|
55
|
+
case token
|
|
56
|
+
when "--skip-install"
|
|
57
|
+
@skip_install = true
|
|
58
|
+
i += 1
|
|
59
|
+
when "--version"
|
|
60
|
+
@requirement = @argv[i + 1]
|
|
61
|
+
i += 2
|
|
62
|
+
when "--group"
|
|
63
|
+
@group = @argv[i + 1]
|
|
64
|
+
i += 2
|
|
65
|
+
when "--source"
|
|
66
|
+
@source = @argv[i + 1]
|
|
67
|
+
i += 2
|
|
68
|
+
when "--git"
|
|
69
|
+
@git = @argv[i + 1]
|
|
70
|
+
i += 2
|
|
71
|
+
when "--path"
|
|
72
|
+
@path = @argv[i + 1]
|
|
73
|
+
i += 2
|
|
74
|
+
else
|
|
75
|
+
if token.start_with?("-")
|
|
76
|
+
raise GemfileError, "Unknown option for add: #{token}"
|
|
77
|
+
end
|
|
78
|
+
@gems << token
|
|
79
|
+
i += 1
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|