gemsolver 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +48 -0
- data/Rakefile +3 -0
- data/lib/gemsolver/errors.rb +28 -0
- data/lib/gemsolver/parsers/info_parser.rb +91 -0
- data/lib/gemsolver/parsers/sem_ver_parser.rb +79 -0
- data/lib/gemsolver/railtie.rb +4 -0
- data/lib/gemsolver/version.rb +3 -0
- data/lib/gemsolver.rb +61 -0
- data/lib/generators/gemcache/install_generator.rb +24 -0
- data/lib/tasks/gemcache_tasks.rake +4 -0
- metadata +71 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: bd1bc00574ad277bb5ecd22d84c7c8de6fc57140009245bc75dfd2ca9d186e02
|
4
|
+
data.tar.gz: 10f8060f235b69be05a7ba35885a3840d327fddf28d18ca4f1d752b091a534b9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 560a4b12aff71e5f38425cc9c84b454919d766cb58632f857377103032d701c39d991ac81667007b36f9b4d64df3da950960652cc512d8f5c46b3069e76c0f92
|
7
|
+
data.tar.gz: 32d0e043c9f8224c9135f9e09b5b931c178e7223ff24d2dc9137a56cfd8c6810c41bcb32754277d073f369501c6689eb828cadfbc493b1d4dfc7cb29e7a790b4
|
data/README.md
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# Gem Solver
|
2
|
+
Gem Solver is a plugin for Rails that caches Ruby gems from any [rubygems.org](https://rubygems.org) compatible host.
|
3
|
+
It is built primarly to work with [GemCache](https://github.com/pinecat/gemcache).
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
To use GemSolver in your rails project, first:
|
7
|
+
```
|
8
|
+
bundle add gemsolver
|
9
|
+
```
|
10
|
+
|
11
|
+
then:
|
12
|
+
```
|
13
|
+
rails g gemsolver:install
|
14
|
+
```
|
15
|
+
|
16
|
+
## Usage
|
17
|
+
```ruby
|
18
|
+
class Gemfu
|
19
|
+
include GemSolver
|
20
|
+
|
21
|
+
attr_reader :name, :requirements, :info_file, :stories, :versions, :version, :info, :quick_file, :gem_file
|
22
|
+
|
23
|
+
def initialize(gem)
|
24
|
+
@name = gem.name
|
25
|
+
@requirements = gem.requirements_list
|
26
|
+
|
27
|
+
@info_file = fetch_info
|
28
|
+
@stories = InfoParser.parse(@info_file)
|
29
|
+
@versions = SemVerParser.new(@stories, @requirements, @name)
|
30
|
+
@version = @versions.available.first
|
31
|
+
@info = InfoParser.info(@version, @stories)
|
32
|
+
|
33
|
+
@quick_file = fetch_quick
|
34
|
+
@gem_file = fetch_gem
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
gem = Gem::Dependency.new("colorize", Gem::Requirement.new(["~> 1.0"]))
|
39
|
+
gemfu = Gemfu.new(gem)
|
40
|
+
|
41
|
+
File.binwrite("path/to/colorize.gem", gemfu.gem_file)
|
42
|
+
```
|
43
|
+
|
44
|
+
## Contributing
|
45
|
+
Bug reports and pull requests are welcome on Github at https://github.com/pinecat/gemsolver/issues and https://github.com/pinecat/gemsolver/pulls, respectively.
|
46
|
+
|
47
|
+
## License
|
48
|
+
The gem is available as open source under the terms of the [BSD 3-Clause License](https://opensource.org/license/bsd-3-clause/).
|
data/Rakefile
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
module GemSolver
|
2
|
+
#
|
3
|
+
# Raised when /info/<gem> cannot be found on the remote gem host.
|
4
|
+
#
|
5
|
+
class GemNotFoundError < StandardError
|
6
|
+
def initialize(name)
|
7
|
+
super("The '#{name}' gem could not be found on '#{@@host}'")
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
#
|
12
|
+
# Raised when an unrecognized version constraint is passed in a dependency.
|
13
|
+
#
|
14
|
+
class InvalidVersionConstraintError < StandardError
|
15
|
+
def initialize(operator)
|
16
|
+
super("The semantic versioning contraint '#{operator}' is not recognized")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# Raised when the version constraints are not inclusive enough to return a version.
|
22
|
+
#
|
23
|
+
class NoAvailableVersionsError < StandardError
|
24
|
+
def initialize(name)
|
25
|
+
super("There are no versions of '#{name}' that meet the specified version constraints")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module GemSolver
|
2
|
+
class InfoParser
|
3
|
+
# The version of the gem from the info entry.
|
4
|
+
attr_reader :version
|
5
|
+
|
6
|
+
# A list of Gem::Dependencies from the info entry.
|
7
|
+
attr_reader :dependencies
|
8
|
+
|
9
|
+
# The SHA256 checksum of the gem from the info entry.
|
10
|
+
attr_reader :checksum
|
11
|
+
|
12
|
+
# The required Ruby version of the gem from the info entry.
|
13
|
+
attr_reader :ruby
|
14
|
+
|
15
|
+
# The required rubygems version of the gem from the info entry.
|
16
|
+
attr_reader :rubygems
|
17
|
+
|
18
|
+
def initialize(entry)
|
19
|
+
# Split line into two parts:
|
20
|
+
# (0) Semantic version and runtime dependencies
|
21
|
+
# (1) Checksum and required ruby/rubygems versions
|
22
|
+
split = entry.split("|")
|
23
|
+
ver_and_deps = split[0].strip
|
24
|
+
sha_and_ruby = split[1].strip
|
25
|
+
|
26
|
+
# Parse each part of the entry in the info file
|
27
|
+
parts = parse_version(ver_and_deps)
|
28
|
+
parse_dependencies(parts)
|
29
|
+
parts = parse_checksum(sha_and_ruby)
|
30
|
+
parts = parse_ruby(parts)
|
31
|
+
parse_rubygems(parts)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.info(version, stories)
|
35
|
+
stories.each do |s|
|
36
|
+
return s if s.version == version
|
37
|
+
end
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.parse(raw)
|
42
|
+
stories = []
|
43
|
+
raw.lines.drop(1).each do |l|
|
44
|
+
stories << new(l)
|
45
|
+
end
|
46
|
+
stories.reverse
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def parse_version(raw)
|
52
|
+
return if raw.blank?
|
53
|
+
|
54
|
+
parts = raw.split(" ", 2)
|
55
|
+
@version = Gem::Version.new(parts[0])
|
56
|
+
parts.drop(1)
|
57
|
+
end
|
58
|
+
|
59
|
+
def parse_dependencies(parts)
|
60
|
+
return if parts.blank?
|
61
|
+
|
62
|
+
parts = parts.split(",").flatten
|
63
|
+
@dependencies = []
|
64
|
+
parts.each do |p|
|
65
|
+
name, requirements = p.split(":")
|
66
|
+
@dependencies << Gem::Dependency.new(name, requirements)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def parse_checksum(raw)
|
71
|
+
return if raw.blank?
|
72
|
+
|
73
|
+
parts = raw.split(",")
|
74
|
+
@checksum = parts[0].split(":")[1]
|
75
|
+
parts.drop(1)
|
76
|
+
end
|
77
|
+
|
78
|
+
def parse_ruby(parts)
|
79
|
+
return if parts.blank?
|
80
|
+
|
81
|
+
@ruby = Gem::Dependency.new("ruby", parts[0].split(":")[1].split("&"))
|
82
|
+
parts.drop(1)
|
83
|
+
end
|
84
|
+
|
85
|
+
def parse_rubygems(parts)
|
86
|
+
return if parts.blank?
|
87
|
+
|
88
|
+
@rubygems = Gem::Dependency.new("rubygems", parts[0].split(":")[1].split("&"))
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module GemSolver
|
2
|
+
#
|
3
|
+
# Creates a list of appropriate gem and dependency versions
|
4
|
+
# based on semantic versioning requirements. The latest
|
5
|
+
# possible verions will always come first in the list.
|
6
|
+
#
|
7
|
+
class SemVerParser
|
8
|
+
class Constraint
|
9
|
+
def initialize(operator, version)
|
10
|
+
@operator = operator.intern
|
11
|
+
@version = Gem::Version.new(version)
|
12
|
+
end
|
13
|
+
|
14
|
+
def appropriate?(version)
|
15
|
+
version.send(@operator, @version)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# List of appropriate semantic versions, with the latest versions at the front of the list.
|
20
|
+
attr_reader :available
|
21
|
+
|
22
|
+
def initialize(stories, requirements, name)
|
23
|
+
constraints = constrain(requirements)
|
24
|
+
|
25
|
+
@available = []
|
26
|
+
stories.each do |s|
|
27
|
+
one_constraint_failed = false
|
28
|
+
constraints.each do |c|
|
29
|
+
unless c.appropriate?(s.version)
|
30
|
+
one_constraint_failed = true
|
31
|
+
break
|
32
|
+
end
|
33
|
+
end
|
34
|
+
@available << s.version unless one_constraint_failed
|
35
|
+
end
|
36
|
+
|
37
|
+
raise NoAvailableVersionsError.new(name) if @available.blank?
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def constrain(requirements)
|
43
|
+
constraints = []
|
44
|
+
requirements.each do |r|
|
45
|
+
operator, version = r.split(" ")
|
46
|
+
|
47
|
+
case operator
|
48
|
+
when "~>"
|
49
|
+
# Pessimistic versioning (i.e. approximately greater than)
|
50
|
+
constraints << Constraint.new(">=", version)
|
51
|
+
|
52
|
+
if version.include?("-")
|
53
|
+
version = version.split("-")[0]
|
54
|
+
end
|
55
|
+
|
56
|
+
if version.include?("+")
|
57
|
+
version = version.split("+")[0]
|
58
|
+
end
|
59
|
+
|
60
|
+
mmp = version.split(".").map { |s| s.to_i }
|
61
|
+
mmp.pop unless mmp.length == 1
|
62
|
+
mmp[-1] += 1
|
63
|
+
version = mmp.join(".")
|
64
|
+
|
65
|
+
constraints << Constraint.new("<", version)
|
66
|
+
when ">", ">=", "<", "<="
|
67
|
+
constraints << Constraint.new(operator, version)
|
68
|
+
when "="
|
69
|
+
constraints << Constraint.new("==", version)
|
70
|
+
else
|
71
|
+
raise InvalidVersionConstraintError.new(operator)
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
constraints
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
data/lib/gemsolver.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
require "gemsolver/version"
|
2
|
+
require "gemsolver/railtie"
|
3
|
+
require "gemsolver/errors"
|
4
|
+
require "gemsolver/parsers/info_parser"
|
5
|
+
require "gemsolver/parsers/sem_ver_parser"
|
6
|
+
require "net/http"
|
7
|
+
require "stringio"
|
8
|
+
require "rubygems/indexer"
|
9
|
+
|
10
|
+
module GemSolver
|
11
|
+
#
|
12
|
+
# The host of the remote gem server.
|
13
|
+
# The default is https://rubygems.org.
|
14
|
+
#
|
15
|
+
mattr_accessor :host
|
16
|
+
@@host = "https://rubygems.org"
|
17
|
+
|
18
|
+
#
|
19
|
+
# Set configuration options.
|
20
|
+
# This is typically done in the initializer ('rails g gemcache:install')
|
21
|
+
#
|
22
|
+
def self.setup
|
23
|
+
yield self
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# Fetch required gem info file.
|
28
|
+
#
|
29
|
+
# @return [String] The (YAML based?) info file.
|
30
|
+
#
|
31
|
+
def fetch_info
|
32
|
+
info_uri = URI.parse("#{@@host}/info/#{@name}")
|
33
|
+
response = Net::HTTP.get_response(info_uri)
|
34
|
+
raise GemNotFoundError.new(@name) unless response.is_a? Net::HTTPSuccess
|
35
|
+
response.body
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Fetch required gem quick file.
|
40
|
+
#
|
41
|
+
# @return [String] The binary (compressed) quick file.
|
42
|
+
#
|
43
|
+
def fetch_quick
|
44
|
+
quick_uri = URI.parse("#{@@host}/quick/Marshal.4.8/#{@name}-#{@version}.gemspec.rz")
|
45
|
+
response = Net::HTTP.get_response(quick_uri)
|
46
|
+
raise GemNotFoundError.new(@name) unless response.is_a? Net::HTTPSuccess
|
47
|
+
response.body
|
48
|
+
end
|
49
|
+
|
50
|
+
#
|
51
|
+
# Fetch required gem file.
|
52
|
+
#
|
53
|
+
# @return [String] The binary (compressed) gem file.
|
54
|
+
#
|
55
|
+
def fetch_gem
|
56
|
+
gem_uri = URI.parse("#{@@host}/gems/#{@name}-#{@version}.gem")
|
57
|
+
response = Net::HTTP.get_response(gem_uri)
|
58
|
+
raise GemNotFoundError(@name) unless response.is_a? Net::HTTPSuccess
|
59
|
+
response.body
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators/base"
|
4
|
+
|
5
|
+
module Gemsolver
|
6
|
+
class InstallGenerator < Rails::Generators::Base
|
7
|
+
desc "Create an initializer file for Gem Solver configuration options"
|
8
|
+
def create_initializer_file
|
9
|
+
create_file "config/initializers/gemsolver.rb", <<~RUBY
|
10
|
+
# frozen_string_literal: true
|
11
|
+
|
12
|
+
# GemCache configuration options.
|
13
|
+
GemSolver.setup do |config|
|
14
|
+
# The remote host to cache ruby gems from. This option may be overriden
|
15
|
+
# directly or by setting the 'RAILS_GEMCACHE_HOST' environment
|
16
|
+
# variable. The default option is https://rubygems.org. The remote host
|
17
|
+
# must follow the distrubution pattern of rubygems.org, otherwise
|
18
|
+
# GemCache will be unable to fetch gems from it.
|
19
|
+
config.host = ENV["RAILS_GEMSOLVE_HOST"] || "https://rubygems.org"
|
20
|
+
end
|
21
|
+
RUBY
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gemsolver
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Rory Dudley
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-08-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 7.0.7.2
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 7.0.7.2
|
27
|
+
description: Cache Ruby gems from a rubygems.org compatible host.
|
28
|
+
email:
|
29
|
+
- rory.dudley@gmail.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- README.md
|
35
|
+
- Rakefile
|
36
|
+
- lib/gemsolver.rb
|
37
|
+
- lib/gemsolver/errors.rb
|
38
|
+
- lib/gemsolver/parsers/info_parser.rb
|
39
|
+
- lib/gemsolver/parsers/sem_ver_parser.rb
|
40
|
+
- lib/gemsolver/railtie.rb
|
41
|
+
- lib/gemsolver/version.rb
|
42
|
+
- lib/generators/gemcache/install_generator.rb
|
43
|
+
- lib/tasks/gemcache_tasks.rake
|
44
|
+
homepage: https://github.com/pinecat/gemcache
|
45
|
+
licenses:
|
46
|
+
- BSD 3-Clause
|
47
|
+
metadata:
|
48
|
+
allowed_push_host: https://rubygems.org
|
49
|
+
homepage_uri: https://github.com/pinecat/gemcache
|
50
|
+
source_code_uri: https://github.com/pinecat/gemcache
|
51
|
+
changelog_uri: https://github.com/pinecat/gemcache/blob/master/CHANGELOG.md
|
52
|
+
post_install_message:
|
53
|
+
rdoc_options: []
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
requirements: []
|
67
|
+
rubygems_version: 3.4.19
|
68
|
+
signing_key:
|
69
|
+
specification_version: 4
|
70
|
+
summary: Cache Ruby gems.
|
71
|
+
test_files: []
|