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.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/FEATURES.md +13 -0
  3. data/README.md +216 -0
  4. data/bin/bundler-vs-scint +233 -0
  5. data/bin/scint +35 -0
  6. data/bin/scint-io-summary +46 -0
  7. data/bin/scint-syscall-trace +41 -0
  8. data/lib/bundler/setup.rb +5 -0
  9. data/lib/bundler.rb +168 -0
  10. data/lib/scint/cache/layout.rb +131 -0
  11. data/lib/scint/cache/metadata_store.rb +75 -0
  12. data/lib/scint/cache/prewarm.rb +192 -0
  13. data/lib/scint/cli/add.rb +85 -0
  14. data/lib/scint/cli/cache.rb +316 -0
  15. data/lib/scint/cli/exec.rb +150 -0
  16. data/lib/scint/cli/install.rb +1047 -0
  17. data/lib/scint/cli/remove.rb +60 -0
  18. data/lib/scint/cli.rb +77 -0
  19. data/lib/scint/commands/exec.rb +17 -0
  20. data/lib/scint/commands/install.rb +17 -0
  21. data/lib/scint/credentials.rb +153 -0
  22. data/lib/scint/debug/io_trace.rb +218 -0
  23. data/lib/scint/debug/sampler.rb +138 -0
  24. data/lib/scint/downloader/fetcher.rb +113 -0
  25. data/lib/scint/downloader/pool.rb +112 -0
  26. data/lib/scint/errors.rb +63 -0
  27. data/lib/scint/fs.rb +119 -0
  28. data/lib/scint/gem/extractor.rb +86 -0
  29. data/lib/scint/gem/package.rb +62 -0
  30. data/lib/scint/gemfile/dependency.rb +30 -0
  31. data/lib/scint/gemfile/editor.rb +93 -0
  32. data/lib/scint/gemfile/parser.rb +275 -0
  33. data/lib/scint/index/cache.rb +166 -0
  34. data/lib/scint/index/client.rb +301 -0
  35. data/lib/scint/index/parser.rb +142 -0
  36. data/lib/scint/installer/extension_builder.rb +264 -0
  37. data/lib/scint/installer/linker.rb +226 -0
  38. data/lib/scint/installer/planner.rb +140 -0
  39. data/lib/scint/installer/preparer.rb +207 -0
  40. data/lib/scint/lockfile/parser.rb +251 -0
  41. data/lib/scint/lockfile/writer.rb +178 -0
  42. data/lib/scint/platform.rb +71 -0
  43. data/lib/scint/progress.rb +579 -0
  44. data/lib/scint/resolver/provider.rb +230 -0
  45. data/lib/scint/resolver/resolver.rb +249 -0
  46. data/lib/scint/runtime/exec.rb +141 -0
  47. data/lib/scint/runtime/setup.rb +45 -0
  48. data/lib/scint/scheduler.rb +392 -0
  49. data/lib/scint/source/base.rb +46 -0
  50. data/lib/scint/source/git.rb +92 -0
  51. data/lib/scint/source/path.rb +70 -0
  52. data/lib/scint/source/rubygems.rb +79 -0
  53. data/lib/scint/vendor/pub_grub/assignment.rb +20 -0
  54. data/lib/scint/vendor/pub_grub/basic_package_source.rb +169 -0
  55. data/lib/scint/vendor/pub_grub/failure_writer.rb +182 -0
  56. data/lib/scint/vendor/pub_grub/incompatibility.rb +150 -0
  57. data/lib/scint/vendor/pub_grub/package.rb +43 -0
  58. data/lib/scint/vendor/pub_grub/partial_solution.rb +121 -0
  59. data/lib/scint/vendor/pub_grub/rubygems.rb +45 -0
  60. data/lib/scint/vendor/pub_grub/solve_failure.rb +19 -0
  61. data/lib/scint/vendor/pub_grub/static_package_source.rb +61 -0
  62. data/lib/scint/vendor/pub_grub/strategy.rb +42 -0
  63. data/lib/scint/vendor/pub_grub/term.rb +105 -0
  64. data/lib/scint/vendor/pub_grub/version.rb +3 -0
  65. data/lib/scint/vendor/pub_grub/version_constraint.rb +129 -0
  66. data/lib/scint/vendor/pub_grub/version_range.rb +423 -0
  67. data/lib/scint/vendor/pub_grub/version_solver.rb +236 -0
  68. data/lib/scint/vendor/pub_grub/version_union.rb +178 -0
  69. data/lib/scint/vendor/pub_grub.rb +32 -0
  70. data/lib/scint/worker_pool.rb +114 -0
  71. data/lib/scint.rb +87 -0
  72. metadata +116 -0
@@ -0,0 +1,251 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../source/rubygems"
4
+ require_relative "../source/git"
5
+ require_relative "../source/path"
6
+
7
+ module Scint
8
+ module Lockfile
9
+ # Data returned by the lockfile parser.
10
+ LockfileData = Struct.new(
11
+ :specs, :dependencies, :platforms, :sources,
12
+ :bundler_version, :ruby_version, :checksums,
13
+ keyword_init: true,
14
+ )
15
+
16
+ # Parses a standard Gemfile.lock file into structured data.
17
+ # Compatible with the format produced by stock bundler.
18
+ class Parser
19
+ BUNDLED = "BUNDLED WITH"
20
+ DEPENDENCIES = "DEPENDENCIES"
21
+ CHECKSUMS = "CHECKSUMS"
22
+ PLATFORMS = "PLATFORMS"
23
+ RUBY = "RUBY VERSION"
24
+ GIT = "GIT"
25
+ GEM = "GEM"
26
+ PATH = "PATH"
27
+ SPECS = " specs:"
28
+
29
+ OPTIONS = /^ ([a-z]+): (.*)$/i
30
+ SOURCE_TYPES = [GIT, GEM, PATH].freeze
31
+
32
+ # Regex for spec/dependency lines:
33
+ # 2 spaces = dependency, 4 spaces = spec, 6 spaces = spec dependency
34
+ NAME_VERSION = /
35
+ ^(\s{2}|\s{4}|\s{6})(?!\s) # exactly 2, 4, or 6 leading spaces
36
+ (.*?) # name
37
+ (?:\s\(([^-]*) # version in parens
38
+ (?:-(.*))?\))? # optional platform after dash
39
+ (!)? # optional pinned marker
40
+ (?:\s([^\s]+))? # optional checksum
41
+ $
42
+ /x
43
+
44
+ # Accepts either lockfile contents or a file path.
45
+ def self.parse(lockfile_or_contents)
46
+ contents =
47
+ if lockfile_or_contents.is_a?(String) && File.exist?(lockfile_or_contents)
48
+ File.read(lockfile_or_contents)
49
+ else
50
+ lockfile_or_contents.to_s
51
+ end
52
+ new(contents).parse
53
+ end
54
+
55
+ def initialize(lockfile_contents)
56
+ @contents = lockfile_contents
57
+ @specs = []
58
+ @dependencies = {}
59
+ @platforms = []
60
+ @sources = []
61
+ @bundler_version = nil
62
+ @ruby_version = nil
63
+ @checksums = nil
64
+
65
+ @parse_method = nil
66
+ @current_source = nil
67
+ @current_spec = nil
68
+ @source_type = nil
69
+ @source_opts = {}
70
+ end
71
+
72
+ def parse
73
+ if @contents.match?(/(<<<<<<<|=======|>>>>>>>|\|\|\|\|\|\|\|)/)
74
+ raise LockfileError, "Lockfile contains merge conflicts"
75
+ end
76
+
77
+ @contents.each_line do |line|
78
+ line.chomp!
79
+
80
+ # Blank lines reset nothing; skip them
81
+ next if line.strip.empty?
82
+
83
+ if SOURCE_TYPES.include?(line)
84
+ @parse_method = :parse_source
85
+ @source_type = line
86
+ @source_opts = {}
87
+ @current_source = nil
88
+ next
89
+ elsif line == DEPENDENCIES
90
+ @parse_method = :parse_dependency
91
+ next
92
+ elsif line == CHECKSUMS
93
+ @checksums = {}
94
+ @parse_method = :parse_checksum
95
+ next
96
+ elsif line == PLATFORMS
97
+ @parse_method = :parse_platform
98
+ next
99
+ elsif line == RUBY
100
+ @parse_method = :parse_ruby
101
+ next
102
+ elsif line == BUNDLED
103
+ @parse_method = :parse_bundled_with
104
+ next
105
+ elsif line =~ /^[^\s]/
106
+ # Unknown section header
107
+ @parse_method = nil
108
+ next
109
+ end
110
+
111
+ next unless @parse_method
112
+
113
+ case @parse_method
114
+ when :parse_source then parse_source(line)
115
+ when :parse_dependency then parse_dependency(line)
116
+ when :parse_checksum then parse_checksum(line)
117
+ when :parse_platform then parse_platform(line)
118
+ when :parse_ruby then parse_ruby(line)
119
+ when :parse_bundled_with then parse_bundled_with(line)
120
+ end
121
+ end
122
+
123
+ LockfileData.new(
124
+ specs: @specs,
125
+ dependencies: @dependencies,
126
+ platforms: @platforms,
127
+ sources: @sources,
128
+ bundler_version: @bundler_version,
129
+ ruby_version: @ruby_version,
130
+ checksums: @checksums,
131
+ )
132
+ end
133
+
134
+ private
135
+
136
+ def parse_source(line)
137
+ case line
138
+ when SPECS
139
+ @current_source = build_source(@source_type, @source_opts)
140
+ @sources << @current_source if @current_source
141
+ when OPTIONS
142
+ key = $1
143
+ value = $2
144
+ value = true if value == "true"
145
+ value = false if value == "false"
146
+
147
+ if @source_opts[key]
148
+ @source_opts[key] = Array(@source_opts[key])
149
+ @source_opts[key] << value
150
+ else
151
+ @source_opts[key] = value
152
+ end
153
+ else
154
+ parse_spec(line)
155
+ end
156
+ end
157
+
158
+ def parse_spec(line)
159
+ return unless line =~ NAME_VERSION
160
+ spaces = $1
161
+ name = $2.freeze
162
+ version = $3
163
+ platform = $4
164
+
165
+ if spaces.length == 4
166
+ # This is a spec line (top-level spec under a source)
167
+ spec = {
168
+ name: name,
169
+ version: version,
170
+ platform: platform || "ruby",
171
+ dependencies: [],
172
+ source: @current_source,
173
+ checksum: nil,
174
+ }
175
+ @specs << spec
176
+ @current_spec = spec
177
+ elsif spaces.length == 6 && @current_spec
178
+ # This is a dependency of the current spec
179
+ dep_versions = version ? version.split(",").map(&:strip) : [">= 0"]
180
+ @current_spec[:dependencies] << { name: name, version_reqs: dep_versions }
181
+ end
182
+ end
183
+
184
+ def parse_dependency(line)
185
+ return unless line =~ NAME_VERSION
186
+ spaces = $1
187
+ return unless spaces.length == 2
188
+
189
+ name = $2.freeze
190
+ version = $3
191
+ pinned = $5
192
+
193
+ version_reqs = version ? version.split(",").map(&:strip) : [">= 0"]
194
+
195
+ @dependencies[name] = {
196
+ name: name,
197
+ version_reqs: version_reqs,
198
+ pinned: pinned == "!",
199
+ }
200
+ end
201
+
202
+ def parse_checksum(line)
203
+ return unless line =~ NAME_VERSION
204
+ spaces = $1
205
+ return unless spaces.length == 2
206
+
207
+ name = $2
208
+ version = $3
209
+ platform = $4 || "ruby"
210
+ checksums_str = $6
211
+
212
+ key = platform == "ruby" ? "#{name}-#{version}" : "#{name}-#{version}-#{platform}"
213
+
214
+ if checksums_str
215
+ @checksums[key] = checksums_str.split(",").map(&:strip)
216
+ else
217
+ @checksums[key] = []
218
+ end
219
+ end
220
+
221
+ def parse_platform(line)
222
+ stripped = line.strip
223
+ return if stripped.empty?
224
+ @platforms << stripped
225
+ end
226
+
227
+ def parse_ruby(line)
228
+ stripped = line.strip
229
+ return if stripped.empty?
230
+ @ruby_version = stripped
231
+ end
232
+
233
+ def parse_bundled_with(line)
234
+ stripped = line.strip
235
+ return if stripped.empty?
236
+ @bundler_version = stripped
237
+ end
238
+
239
+ def build_source(type, opts)
240
+ case type
241
+ when GEM
242
+ Source::Rubygems.from_lock(opts.dup)
243
+ when GIT
244
+ Source::Git.from_lock(opts.dup)
245
+ when PATH
246
+ Source::Path.from_lock(opts.dup)
247
+ end
248
+ end
249
+ end
250
+ end
251
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Scint
4
+ module Lockfile
5
+ # Writes a standard Gemfile.lock file from structured data.
6
+ # Produces output compatible with stock bundler.
7
+ #
8
+ # Sections in order: source blocks (GEM/GIT/PATH), PLATFORMS,
9
+ # DEPENDENCIES, CHECKSUMS (if present), RUBY VERSION, BUNDLED WITH.
10
+ class Writer
11
+ def self.write(lockfile_data)
12
+ new(lockfile_data).generate
13
+ end
14
+
15
+ def initialize(lockfile_data)
16
+ @data = lockfile_data
17
+ end
18
+
19
+ def generate
20
+ out = String.new
21
+
22
+ add_sources(out)
23
+ add_platforms(out)
24
+ add_dependencies(out)
25
+ add_checksums(out)
26
+ add_ruby_version(out)
27
+ add_bundled_with(out)
28
+
29
+ out
30
+ end
31
+
32
+ private
33
+
34
+ def add_sources(out)
35
+ # Group specs by source, preserving source order.
36
+ # Specs store source as a URI string; sources are Source objects.
37
+ # Match by checking if the spec's source URI matches any remote.
38
+ specs_by_source = {}
39
+ @data.sources.each { |s| specs_by_source[s] = [] }
40
+
41
+ @data.specs.each do |spec|
42
+ spec_src = spec.is_a?(Hash) ? spec[:source] : spec.source
43
+ spec_uri = normalize_source_uri(spec_src)
44
+
45
+ matched = @data.sources.find do |source|
46
+ if source.respond_to?(:remotes)
47
+ source.remotes.any? { |r| normalize_source_uri(r) == spec_uri }
48
+ elsif source.respond_to?(:uri)
49
+ normalize_source_uri(source.uri) == spec_uri
50
+ else
51
+ source == spec_src
52
+ end
53
+ end
54
+
55
+ target = matched || @data.sources.first
56
+ specs_by_source[target] ||= []
57
+ specs_by_source[target] << spec
58
+ end
59
+
60
+ first = true
61
+ @data.sources.each do |source|
62
+ out << "\n" unless first
63
+ first = false
64
+
65
+ out << source.to_lock
66
+ add_specs(out, specs_by_source[source] || [])
67
+ end
68
+ end
69
+
70
+ def normalize_source_uri(uri)
71
+ s = uri.to_s.chomp("/")
72
+ s.sub(%r{^https?://}, "").downcase
73
+ end
74
+
75
+ def add_specs(out, specs)
76
+ # Sort by full name (name-version-platform) for consistency
77
+ sorted = specs.sort_by do |s|
78
+ if s.is_a?(Hash)
79
+ n = s[:name]
80
+ v = s[:version]
81
+ p = s[:platform]
82
+ p == "ruby" ? "#{n}-#{v}" : "#{n}-#{v}-#{p}"
83
+ else
84
+ "#{s.name}-#{s.version}#{"-#{s.platform}" if s.platform != "ruby"}"
85
+ end
86
+ end
87
+
88
+ sorted.each do |spec|
89
+ name, version, platform, deps = if spec.is_a?(Hash)
90
+ [spec[:name], spec[:version], spec[:platform], spec[:dependencies] || []]
91
+ else
92
+ [spec.name, spec.version, spec.platform, spec.dependencies || []]
93
+ end
94
+
95
+ # Format: " name (version)" or " name (version-platform)"
96
+ version_str = platform && platform != "ruby" ? "#{version}-#{platform}" : version.to_s
97
+ out << " #{name} (#{version_str})\n"
98
+
99
+ # Dependencies of this spec (6-space indent)
100
+ dep_list = deps.sort_by { |d| d.is_a?(Hash) ? d[:name] : d.name }
101
+ dep_list.each do |dep|
102
+ dep_name, dep_reqs = if dep.is_a?(Hash)
103
+ [dep[:name], dep[:version_reqs]]
104
+ else
105
+ [dep.name, dep.version_reqs]
106
+ end
107
+
108
+ if dep_reqs && dep_reqs != [">= 0"]
109
+ out << " #{dep_name} (#{Array(dep_reqs).join(", ")})\n"
110
+ else
111
+ out << " #{dep_name}\n"
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ def add_platforms(out)
118
+ return if @data.platforms.empty?
119
+ out << "\nPLATFORMS\n"
120
+ @data.platforms.sort.each do |p|
121
+ out << " #{p}\n"
122
+ end
123
+ end
124
+
125
+ def add_dependencies(out)
126
+ return if @data.dependencies.empty?
127
+ out << "\nDEPENDENCIES\n"
128
+
129
+ deps = @data.dependencies
130
+ dep_list = if deps.is_a?(Hash)
131
+ deps.values
132
+ else
133
+ deps
134
+ end
135
+
136
+ dep_list.sort_by { |d| d.is_a?(Hash) ? d[:name] : d.name }.each do |dep|
137
+ name, reqs, pinned = if dep.is_a?(Hash)
138
+ [dep[:name], dep[:version_reqs], dep[:pinned]]
139
+ else
140
+ [dep.name, dep.version_reqs, dep.pinned]
141
+ end
142
+
143
+ out << " #{name}"
144
+ if reqs && reqs != [">= 0"]
145
+ out << " (#{Array(reqs).join(", ")})"
146
+ end
147
+ out << "!" if pinned
148
+ out << "\n"
149
+ end
150
+ end
151
+
152
+ def add_checksums(out)
153
+ return unless @data.checksums
154
+ out << "\nCHECKSUMS\n"
155
+
156
+ @data.checksums.sort.each do |key, values|
157
+ if values && !values.empty?
158
+ out << " #{key} #{values.join(",")}\n"
159
+ else
160
+ out << " #{key}\n"
161
+ end
162
+ end
163
+ end
164
+
165
+ def add_ruby_version(out)
166
+ return unless @data.ruby_version
167
+ out << "\nRUBY VERSION\n"
168
+ out << " #{@data.ruby_version}\n"
169
+ end
170
+
171
+ def add_bundled_with(out)
172
+ return unless @data.bundler_version
173
+ out << "\nBUNDLED WITH\n"
174
+ out << " #{@data.bundler_version}\n"
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "etc"
4
+ require "rbconfig"
5
+
6
+ module Scint
7
+ module Platform
8
+ module_function
9
+
10
+ def cpu_count
11
+ @cpu_count ||= Etc.nprocessors
12
+ end
13
+
14
+ def abi_key
15
+ @abi_key ||= begin
16
+ engine = RUBY_ENGINE # "ruby", "jruby", etc.
17
+ version = RUBY_VERSION # "3.3.0"
18
+ arch = RbConfig::CONFIG["arch"] # "arm64-darwin24"
19
+ "#{engine}-#{version}-#{arch}"
20
+ end
21
+ end
22
+
23
+ def local_platform
24
+ @local_platform ||= ::Gem::Platform.local
25
+ end
26
+
27
+ def match_platform?(spec_platform)
28
+ return true if spec_platform.nil?
29
+ return true if spec_platform == "ruby"
30
+
31
+ spec_plat = spec_platform.is_a?(::Gem::Platform) ? spec_platform : ::Gem::Platform.new(spec_platform)
32
+ spec_plat === local_platform
33
+ end
34
+
35
+ def ruby_engine
36
+ RUBY_ENGINE
37
+ end
38
+
39
+ def ruby_version
40
+ RUBY_VERSION
41
+ end
42
+
43
+ def extension_api_version
44
+ ::Gem.extension_api_version
45
+ end
46
+
47
+ def arch
48
+ RbConfig::CONFIG["arch"]
49
+ end
50
+
51
+ def gem_arch
52
+ local_platform.to_s
53
+ end
54
+
55
+ def os
56
+ RbConfig::CONFIG["host_os"]
57
+ end
58
+
59
+ def windows?
60
+ !!(os =~ /mswin|mingw|cygwin/)
61
+ end
62
+
63
+ def macos?
64
+ !!(os =~ /darwin/)
65
+ end
66
+
67
+ def linux?
68
+ !!(os =~ /linux/)
69
+ end
70
+ end
71
+ end