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
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