gel 0.2.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 (75) hide show
  1. checksums.yaml +7 -0
  2. data/CODE_OF_CONDUCT.md +74 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +39 -0
  5. data/exe/gel +13 -0
  6. data/lib/gel.rb +22 -0
  7. data/lib/gel/catalog.rb +153 -0
  8. data/lib/gel/catalog/common.rb +82 -0
  9. data/lib/gel/catalog/compact_index.rb +152 -0
  10. data/lib/gel/catalog/dependency_index.rb +125 -0
  11. data/lib/gel/catalog/legacy_index.rb +157 -0
  12. data/lib/gel/catalog/marshal_hacks.rb +16 -0
  13. data/lib/gel/command.rb +86 -0
  14. data/lib/gel/command/config.rb +11 -0
  15. data/lib/gel/command/env.rb +7 -0
  16. data/lib/gel/command/exec.rb +66 -0
  17. data/lib/gel/command/help.rb +7 -0
  18. data/lib/gel/command/install.rb +7 -0
  19. data/lib/gel/command/install_gem.rb +16 -0
  20. data/lib/gel/command/lock.rb +34 -0
  21. data/lib/gel/command/ruby.rb +10 -0
  22. data/lib/gel/command/shell_setup.rb +25 -0
  23. data/lib/gel/command/stub.rb +12 -0
  24. data/lib/gel/command/update.rb +11 -0
  25. data/lib/gel/compatibility.rb +4 -0
  26. data/lib/gel/compatibility/bundler.rb +54 -0
  27. data/lib/gel/compatibility/bundler/cli.rb +6 -0
  28. data/lib/gel/compatibility/bundler/friendly_errors.rb +3 -0
  29. data/lib/gel/compatibility/bundler/setup.rb +4 -0
  30. data/lib/gel/compatibility/rubygems.rb +192 -0
  31. data/lib/gel/compatibility/rubygems/command.rb +4 -0
  32. data/lib/gel/compatibility/rubygems/dependency_installer.rb +0 -0
  33. data/lib/gel/compatibility/rubygems/gem_runner.rb +6 -0
  34. data/lib/gel/config.rb +80 -0
  35. data/lib/gel/db.rb +294 -0
  36. data/lib/gel/direct_gem.rb +29 -0
  37. data/lib/gel/environment.rb +592 -0
  38. data/lib/gel/error.rb +104 -0
  39. data/lib/gel/gemfile_parser.rb +144 -0
  40. data/lib/gel/gemspec_parser.rb +95 -0
  41. data/lib/gel/git_catalog.rb +38 -0
  42. data/lib/gel/git_depot.rb +119 -0
  43. data/lib/gel/httpool.rb +148 -0
  44. data/lib/gel/installer.rb +251 -0
  45. data/lib/gel/lock_loader.rb +164 -0
  46. data/lib/gel/lock_parser.rb +64 -0
  47. data/lib/gel/locked_store.rb +126 -0
  48. data/lib/gel/multi_store.rb +96 -0
  49. data/lib/gel/package.rb +156 -0
  50. data/lib/gel/package/inspector.rb +23 -0
  51. data/lib/gel/package/installer.rb +267 -0
  52. data/lib/gel/path_catalog.rb +44 -0
  53. data/lib/gel/pinboard.rb +140 -0
  54. data/lib/gel/pub_grub/preference_strategy.rb +82 -0
  55. data/lib/gel/pub_grub/source.rb +153 -0
  56. data/lib/gel/runtime.rb +27 -0
  57. data/lib/gel/store.rb +205 -0
  58. data/lib/gel/store_catalog.rb +31 -0
  59. data/lib/gel/store_gem.rb +80 -0
  60. data/lib/gel/stub_set.rb +51 -0
  61. data/lib/gel/support/gem_platform.rb +225 -0
  62. data/lib/gel/support/gem_requirement.rb +264 -0
  63. data/lib/gel/support/gem_version.rb +398 -0
  64. data/lib/gel/support/tar.rb +13 -0
  65. data/lib/gel/support/tar/tar_header.rb +229 -0
  66. data/lib/gel/support/tar/tar_reader.rb +123 -0
  67. data/lib/gel/support/tar/tar_reader/entry.rb +154 -0
  68. data/lib/gel/support/tar/tar_writer.rb +339 -0
  69. data/lib/gel/tail_file.rb +205 -0
  70. data/lib/gel/version.rb +5 -0
  71. data/lib/gel/work_pool.rb +143 -0
  72. data/man/man1/gel-exec.1 +16 -0
  73. data/man/man1/gel-install.1 +16 -0
  74. data/man/man1/gel.1 +30 -0
  75. metadata +131 -0
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gel::ReportableError
4
+ # Include this module into any exception that should be treated as a
5
+ # "user facing error" -- that means an error that's user _reportable_,
6
+ # not necessarily that it's their fault.
7
+ #
8
+ # Examples of things that are user-facing errors:
9
+ # * unknown subcommand / arguments
10
+ # * requesting an unknown gem, or one with a malformed name
11
+ # * problems talking to a gem source
12
+ # * syntax errors inside a Gemfile or Gemfile.lock
13
+ # * dependency resolution failure
14
+ # * problems compiling or installing a gem
15
+ #
16
+ # In general, anything that's expected to possibly go wrong should at
17
+ # some point be wrapped in a user error describing the problem in
18
+ # terms of what they wanted to do. An unwrapped exception reaching the
19
+ # top in Command#run is a bug: either the exception is itself
20
+ # reporting a bug (nil reference, typo on a method name, etc), or if
21
+ # it's a legitimate failure, then the bug is a missing rescue.
22
+
23
+ def details
24
+ end
25
+
26
+ def exit_code
27
+ 1
28
+ end
29
+ end
30
+
31
+ # Base class for user-facing errors. Errors _can_ directly include
32
+ # ReportableError to bypass this, but absent a specific reason, they
33
+ # should subclass UserError.
34
+ #
35
+ # Prefer narrow-purpose error classes that receive context parameters
36
+ # over raising generic classes with pre-rendered message parameters. The
37
+ # former can do a better job of fully describing the problem when
38
+ # producing detailed CLI output, without filling real code with long
39
+ # message heredocs.
40
+ #
41
+ # Define all UserError subclasses in this file. (Non-reportable errors,
42
+ # which describe errors in interaction between internal components, can
43
+ # and should be defined whereever they're used.)
44
+ class Gel::UserError < StandardError
45
+ include Gel::ReportableError
46
+
47
+ def initialize(**context)
48
+ @context = context
49
+
50
+ super message
51
+ end
52
+
53
+ def [](key)
54
+ @context.fetch(key)
55
+ end
56
+
57
+ def message
58
+ self.class.name
59
+ end
60
+
61
+ def inner_backtrace
62
+ return [] unless cause
63
+
64
+ bt = cause.backtrace_locations
65
+ ignored_bt = backtrace_locations
66
+
67
+ while bt.last.to_s == ignored_bt.last.to_s
68
+ bt.pop
69
+ ignored_bt.pop
70
+ end
71
+
72
+ while bt.last.path == ignored_bt.last.path
73
+ bt.pop
74
+ end
75
+
76
+ bt
77
+ end
78
+ end
79
+
80
+ module Gel::Error
81
+ class GemfileEvaluationError < Gel::UserError
82
+ def initialize(filename:)
83
+ super
84
+ end
85
+
86
+ def message
87
+ "Failed to evaluate #{self[:filename].inspect}: #{cause&.message}"
88
+ end
89
+
90
+ def details
91
+ inner_backtrace.join("\n")
92
+ end
93
+ end
94
+
95
+ class BrokenStubError < Gel::UserError
96
+ def initialize(name:)
97
+ super
98
+ end
99
+
100
+ def message
101
+ "No available gem supplies a #{self[:name].inspect} executable"
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gel::GemfileParser
4
+ def self.parse(content, filename = nil, lineno = nil)
5
+ result = GemfileContent.new(filename)
6
+ context = ParseContext.new(result, filename)
7
+ if filename
8
+ context.instance_eval(content, filename, lineno)
9
+ else
10
+ context.instance_eval(content)
11
+ end
12
+ result
13
+ rescue ScriptError, StandardError
14
+ raise Gel::Error::GemfileEvaluationError.new(filename: filename)
15
+ end
16
+
17
+ class ParseContext
18
+ def initialize(result, filename)
19
+ @result = result
20
+
21
+ @stack = []
22
+ end
23
+
24
+ def source(uri)
25
+ if block_given?
26
+ begin
27
+ @stack << { source: uri }
28
+ yield
29
+ ensure
30
+ @stack.pop
31
+ end
32
+ else
33
+ @result.sources << uri
34
+ end
35
+ end
36
+
37
+ def git_source(name, &block)
38
+ @result.git_sources[name] = block
39
+ end
40
+
41
+ def ruby(version, engine: nil, engine_version: nil)
42
+ req = Gel::Support::GemRequirement.new(version)
43
+ raise "Running ruby version #{RUBY_VERSION} does not match requested #{version.inspect}" unless req.satisfied_by?(Gel::Support::GemVersion.new(RUBY_VERSION))
44
+ raise "Running ruby engine #{RUBY_ENGINE} does not match requested #{engine.inspect}" unless !engine || RUBY_ENGINE == engine
45
+ if engine_version
46
+ raise "Cannot specify :engine_version without :engine" unless engine
47
+ req = Gel::Support::GemRequirement.new(version)
48
+ raise "Running ruby engine version #{RUBY_ENGINE_VERSION} does not match requested #{engine_version.inspect}" unless req.satisfied_by?(Gel::Support::GemVersion.new(RUBY_ENGINE_VERSION))
49
+ end
50
+ @result.ruby << [version, engine: engine, engine_version: engine_version]
51
+ end
52
+
53
+ def gem(name, *requirements, **options)
54
+ options = @result.flatten(options, @stack)
55
+ @result.add_gem(name, requirements, options)
56
+ end
57
+
58
+ def gemspec
59
+ if file = Dir["#{File.dirname(@result.filename)}/*.gemspec"].first
60
+ spec = Gel::GemspecParser.parse(File.read(file), file)
61
+ gem spec.name, path: "."
62
+ spec.development_dependencies.each do |name, constraints|
63
+ gem name, constraints, group: :development
64
+ end
65
+ end
66
+ end
67
+
68
+ def group(*names)
69
+ @stack << { group: names }
70
+ yield
71
+ ensure
72
+ @stack.pop
73
+ end
74
+
75
+ def platforms(*names)
76
+ @stack << { platforms: names }
77
+ yield
78
+ ensure
79
+ @stack.pop
80
+ end
81
+ end
82
+
83
+ class GemfileContent
84
+ attr_reader :filename
85
+
86
+ attr_reader :sources
87
+ attr_reader :git_sources
88
+ attr_reader :ruby
89
+
90
+ attr_reader :gems
91
+
92
+ def initialize(filename)
93
+ @filename = filename
94
+ @sources = []
95
+ @git_sources = {}
96
+ @ruby = []
97
+ @gems = []
98
+ end
99
+
100
+ def flatten(options, stack)
101
+ options = options.dup
102
+ stack.reverse_each do |layer|
103
+ options.update(layer) { |_, current, outer| current }
104
+ end
105
+ @git_sources.each do |key, block|
106
+ next unless options.key?(key)
107
+ raise "Multiple git sources specified" if options.key?(:git)
108
+ options[:git] = block.call(options.delete(key))
109
+ end
110
+ options
111
+ end
112
+
113
+ def add_gem(name, requirements, options)
114
+ return if name == "bundler"
115
+ raise "Only git sources can specify a :branch" if options[:branch] && !options[:git]
116
+ raise "Duplicate entry for gem #{name.inspect}" if @gems.assoc(name)
117
+
118
+ @gems << [name, requirements, options]
119
+ end
120
+
121
+ def autorequire(target, gems = self.gems)
122
+ gems.each do |name, _version, options|
123
+ next if options[:require] == false
124
+
125
+ if [nil, true].include?(options[:require])
126
+ alt_name = name.include?("-") && name.tr("-", "/")
127
+ if target.gem_has_file?(name, name)
128
+ target.scoped_require name, name
129
+ elsif alt_name && target.gem_has_file?(name, alt_name)
130
+ target.scoped_require name, alt_name
131
+ elsif options[:require] == true
132
+ target.scoped_require name, name
133
+ end
134
+ elsif options[:require].is_a?(Array)
135
+ options[:require].each do |path|
136
+ target.scoped_require name, path
137
+ end
138
+ else
139
+ target.scoped_require name, options[:require]
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ostruct"
4
+
5
+ class Gel::GemspecParser
6
+ module Context
7
+ def self.context
8
+ binding
9
+ end
10
+
11
+ module Gem
12
+ Version = Gel::Support::GemVersion
13
+ Requirement = Gel::Support::GemRequirement
14
+
15
+ VERSION = "2.99.0"
16
+
17
+ module Platform
18
+ RUBY = "ruby"
19
+ end
20
+
21
+ module Specification
22
+ def self.new(name = nil, version = nil, &block)
23
+ result = Result.new
24
+ result.name = name
25
+ result.version = version
26
+ block.call result
27
+ result
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ class Result < OpenStruct
34
+ def initialize
35
+ super
36
+ self.specification_version = nil
37
+ self.metadata = {}
38
+ self.requirements = []
39
+ self.rdoc_options = []
40
+ self.development_dependencies = []
41
+ self.runtime_dependencies = []
42
+ self.executables = []
43
+ end
44
+
45
+ def add_development_dependency(name, *versions)
46
+ development_dependencies << [name, versions.flatten]
47
+ end
48
+
49
+ def add_runtime_dependency(name, *versions)
50
+ runtime_dependencies << [name, versions.flatten]
51
+ end
52
+ alias add_dependency add_runtime_dependency
53
+ end
54
+
55
+ def self.parse(content, filename, lineno = 1, root: File.dirname(filename), isolate: true)
56
+ filename = File.expand_path(filename)
57
+ root = File.expand_path(root)
58
+
59
+ if isolate
60
+ in_read, in_write = IO.pipe
61
+ out_read, out_write = IO.pipe
62
+
63
+ pid = spawn({ "RUBYLIB" => Gel::Environment.modified_rubylib, "GEL_GEMFILE" => "", "GEL_LOCKFILE" => "" },
64
+ RbConfig.ruby,
65
+ "-r", File.expand_path("compatibility", __dir__),
66
+ "-r", File.expand_path("gemspec_parser", __dir__),
67
+ "-e", "puts Marshal.dump(Gel::GemspecParser.parse($stdin.read, ARGV.shift, ARGV.shift.to_i, root: ARGV.shift, isolate: false))",
68
+ filename, lineno.to_s, root,
69
+ in: in_read, out: out_write)
70
+
71
+ in_read.close
72
+ out_write.close
73
+
74
+ write_thread = Thread.new do
75
+ in_write.write content
76
+ in_write.close
77
+ end
78
+
79
+ read_thread = Thread.new do
80
+ out_read.read
81
+ end
82
+
83
+ _, status = Process.waitpid2(pid)
84
+ raise "Gemspec parse failed" unless status.success?
85
+
86
+ write_thread.join
87
+ Marshal.load read_thread.value
88
+
89
+ else
90
+ Dir.chdir(root) do
91
+ Context.context.eval(content, filename, lineno)
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "path_catalog"
4
+
5
+ class Gel::GitCatalog
6
+ attr_reader :git_depot, :remote, :ref_type, :ref
7
+
8
+ def initialize(git_depot, remote, ref_type, ref)
9
+ @git_depot = git_depot
10
+ @remote = remote
11
+ @ref_type = ref_type
12
+ @ref = ref
13
+
14
+ @monitor = Monitor.new
15
+ @result = nil
16
+ end
17
+
18
+ def checkout_result
19
+ @result ||
20
+ @monitor.synchronize { @result ||= git_depot.resolve_and_checkout(remote, ref) }
21
+ end
22
+
23
+ def revision
24
+ checkout_result[0]
25
+ end
26
+
27
+ def gem_info(name)
28
+ path_catalog.gem_info(name)
29
+ end
30
+
31
+ def path_catalog
32
+ @path_catalog ||= Gel::PathCatalog.new(checkout_result[1])
33
+ end
34
+
35
+ def prepare
36
+ checkout_result
37
+ end
38
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Gel::GitDepot
4
+ attr_reader :mirror_root
5
+
6
+ require "logger"
7
+ Logger = ::Logger.new($stderr)
8
+ Logger.level = $DEBUG ? ::Logger::DEBUG : ::Logger::WARN
9
+
10
+ def initialize(store, mirror_root = (ENV["GEL_CACHE"] || "~/.cache/gel") + "/git")
11
+ @store = store
12
+ @mirror_root = File.expand_path(mirror_root)
13
+ end
14
+
15
+ def git_path(remote, revision)
16
+ short = File.basename(remote, ".git")
17
+ File.join(@store.root, "git", "#{short}-#{revision[0..12]}")
18
+ end
19
+
20
+ # Returns a path containing a local mirror of the given remote.
21
+ #
22
+ # If the mirror already exists, yields the path (same as the return
23
+ # value); if the block returns false the mirror will be updated.
24
+ def remote(remote)
25
+ cache_dir = "#{@mirror_root}/#{ident(remote)}"
26
+
27
+ if Dir.exist?(cache_dir)
28
+ if block_given? && !yield(cache_dir)
29
+ # The block didn't like what it saw; try updating the mirror
30
+ # from upstream
31
+ status = git(remote, "remote", "update", chdir: cache_dir)
32
+ raise "git remote update failed" unless status.success?
33
+ end
34
+ else
35
+ status = git(remote, "clone", "--mirror", remote, cache_dir)
36
+ raise "git clone --mirror failed" unless status.success?
37
+ end
38
+
39
+ cache_dir
40
+ end
41
+
42
+ def resolve(remote, ref)
43
+ mirror = remote(remote) { false } # always update mirror
44
+
45
+ r, w = IO.pipe
46
+ status = git(remote, "rev-parse", ref || "HEAD", chdir: mirror, out: w)
47
+ raise "git rev-parse failed" unless status.success?
48
+
49
+ w.close
50
+
51
+ r.read.chomp
52
+ end
53
+
54
+ def resolve_and_checkout(remote, ref)
55
+ revision = resolve(remote, ref)
56
+ [revision, checkout(remote, revision)]
57
+ end
58
+
59
+ def checkout(remote, revision)
60
+ destination = git_path(remote, revision)
61
+ return destination if Dir.exist?(destination)
62
+
63
+ mirror = remote(remote) do |cache_dir|
64
+ # Check whether the revision is already in our mirror
65
+ status = git(remote, "rev-list", "--quiet", revision, chdir: cache_dir)
66
+ status.success?
67
+ end
68
+
69
+ status = git(remote, "clone", mirror, destination)
70
+ raise "git clone --local failed" unless status.success?
71
+
72
+ status = git(remote, "checkout", "--detach", "--force", revision, chdir: destination)
73
+ raise "git checkout failed" unless status.success?
74
+
75
+ destination
76
+ end
77
+
78
+ private
79
+
80
+ def git(remote, *arguments, **kwargs)
81
+ kwargs[:in] ||= IO::NULL
82
+ kwargs[:out] ||= IO::NULL
83
+ kwargs[:err] ||= IO::NULL
84
+
85
+ t = Time.now
86
+ pid = spawn("git", *arguments, **kwargs)
87
+ logger.debug { "#{remote} [#{pid}] #{command_for_log("git", *arguments)}" }
88
+
89
+ _, status = Process.waitpid2(pid)
90
+ logger.debug { "#{remote} [#{pid}] process exited #{status.exitstatus} (#{status.success? ? "success" : "failure"}) after #{Time.now - t}s" }
91
+
92
+ status
93
+ end
94
+
95
+ def ident(remote)
96
+ short = File.basename(remote, ".git")
97
+ digest = Digest(:SHA256).hexdigest(remote)[0..12]
98
+ "#{short}-#{digest}"
99
+ end
100
+
101
+ require "shellwords"
102
+ def shellword(word)
103
+ if word =~ /\A[A-Za-z0-9=+\/,.-]+\z/
104
+ word
105
+ elsif word =~ /'/
106
+ "\"#{Shellwords.shellescape(word).gsub(/\\\s/, "\\1")}\""
107
+ else
108
+ "'#{word}'"
109
+ end
110
+ end
111
+
112
+ def command_for_log(*parts)
113
+ parts.map { |part| shellword(part) }.join(" ")
114
+ end
115
+
116
+ def logger
117
+ Logger
118
+ end
119
+ end