solve 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rbenv-version +1 -0
- data/Gemfile +3 -0
- data/Guardfile +17 -0
- data/LICENSE +201 -0
- data/README.md +43 -0
- data/Thorfile +31 -0
- data/lib/solve.rb +47 -0
- data/lib/solve/artifact.rb +104 -0
- data/lib/solve/constraint.rb +73 -0
- data/lib/solve/core_ext.rb +3 -0
- data/lib/solve/core_ext/kernel.rb +33 -0
- data/lib/solve/demand.rb +38 -0
- data/lib/solve/dependency.rb +40 -0
- data/lib/solve/errors.rb +31 -0
- data/lib/solve/gem_version.rb +3 -0
- data/lib/solve/graph.rb +152 -0
- data/lib/solve/version.rb +47 -0
- data/solve.gemspec +32 -0
- data/spec/acceptance/solutions_spec.rb +25 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/unit/solve/artifact_spec.rb +128 -0
- data/spec/unit/solve/constraint_spec.rb +201 -0
- data/spec/unit/solve/demand_spec.rb +57 -0
- data/spec/unit/solve/dependency_spec.rb +49 -0
- data/spec/unit/solve/graph_spec.rb +273 -0
- data/spec/unit/solve/version_spec.rb +11 -0
- data/spec/unit/solve_spec.rb +19 -0
- metadata +278 -0
@@ -0,0 +1,73 @@
|
|
1
|
+
module Solve
|
2
|
+
class Constraint
|
3
|
+
class << self
|
4
|
+
# @param [#to_s] string
|
5
|
+
#
|
6
|
+
# @return [Array, nil]
|
7
|
+
def parse(string)
|
8
|
+
if string =~ /^[0-9]/
|
9
|
+
op = "="
|
10
|
+
ver = string
|
11
|
+
else
|
12
|
+
_, op, ver = REGEXP.match(string).to_a
|
13
|
+
end
|
14
|
+
|
15
|
+
return nil unless op || ver
|
16
|
+
|
17
|
+
[ op, ver ]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
OPERATORS = [
|
22
|
+
"=",
|
23
|
+
">",
|
24
|
+
"<",
|
25
|
+
">=",
|
26
|
+
"<=",
|
27
|
+
"~>"
|
28
|
+
]
|
29
|
+
REGEXP = /^(#{OPERATORS.join('|')}) (.+)$/
|
30
|
+
|
31
|
+
attr_reader :operator
|
32
|
+
attr_reader :version
|
33
|
+
|
34
|
+
# @param [#to_s] constraint
|
35
|
+
def initialize(constraint = ">= 0.0.0")
|
36
|
+
@operator, ver_str = self.class.parse(constraint)
|
37
|
+
if @operator.nil? || ver_str.nil?
|
38
|
+
raise InvalidConstraintFormat.new(constraint)
|
39
|
+
end
|
40
|
+
|
41
|
+
@version = Version.new(ver_str)
|
42
|
+
@dep_constraint = DepSelector::VersionConstraint.new(constraint)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns true or false if the given version would be satisfied by
|
46
|
+
# the version constraint.
|
47
|
+
#
|
48
|
+
# @param [Version, String] version
|
49
|
+
#
|
50
|
+
# @return [Boolean]
|
51
|
+
def satisfies?(version)
|
52
|
+
dep_constraint.include?(version)
|
53
|
+
end
|
54
|
+
|
55
|
+
# @param [Object] other
|
56
|
+
#
|
57
|
+
# @return [Boolean]
|
58
|
+
def ==(other)
|
59
|
+
other.is_a?(self.class) &&
|
60
|
+
self.operator == other.operator &&
|
61
|
+
self.version == other.version
|
62
|
+
end
|
63
|
+
alias_method :eql?, :==
|
64
|
+
|
65
|
+
def to_s
|
66
|
+
"#{operator} #{version}"
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
attr_reader :dep_constraint
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# From ActiveSupport: https://github.com/rails/rails/blob/c2c8ef57d6f00d1c22743dc43746f95704d67a95/activesupport/lib/active_support/core_ext/kernel/reporting.rb#L39
|
2
|
+
|
3
|
+
require 'rbconfig'
|
4
|
+
|
5
|
+
module Kernel
|
6
|
+
# Silences any stream for the duration of the block.
|
7
|
+
#
|
8
|
+
# silence_stream(STDOUT) do
|
9
|
+
# puts 'This will never be seen'
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# puts 'But this will'
|
13
|
+
def silence_stream(stream)
|
14
|
+
old_stream = stream.dup
|
15
|
+
stream.reopen(RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ ? 'NUL:' : '/dev/null')
|
16
|
+
stream.sync = true
|
17
|
+
yield
|
18
|
+
ensure
|
19
|
+
stream.reopen(old_stream)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Silences both STDOUT and STDERR, even for subprocesses.
|
23
|
+
#
|
24
|
+
# quietly { system 'bundle install' }
|
25
|
+
#
|
26
|
+
def quietly
|
27
|
+
silence_stream(STDOUT) do
|
28
|
+
silence_stream(STDERR) do
|
29
|
+
yield
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/solve/demand.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
module Solve
|
2
|
+
class Demand
|
3
|
+
attr_reader :graph
|
4
|
+
attr_reader :name
|
5
|
+
attr_reader :constraint
|
6
|
+
|
7
|
+
# @param [Solve::Graph] graph
|
8
|
+
# @param [#to_s] name
|
9
|
+
# @param [Solve::Constraint, #to_s] constraint
|
10
|
+
def initialize(graph, name, constraint = nil)
|
11
|
+
@graph = graph
|
12
|
+
@name = name
|
13
|
+
|
14
|
+
if constraint
|
15
|
+
@constraint = if constraint.is_a?(Solve::Constraint)
|
16
|
+
constraint
|
17
|
+
else
|
18
|
+
Constraint.new(constraint.to_s)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# @return [Solve::Demand, nil]
|
24
|
+
def delete
|
25
|
+
unless graph.nil?
|
26
|
+
result = graph.remove_demand(self)
|
27
|
+
@graph = nil
|
28
|
+
result
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_s
|
33
|
+
s = "#{name}"
|
34
|
+
s << "(#{constraint})" if constraint
|
35
|
+
s
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Solve
|
2
|
+
class Dependency
|
3
|
+
attr_reader :artifact
|
4
|
+
attr_reader :name
|
5
|
+
attr_reader :constraint
|
6
|
+
|
7
|
+
# @param [Solve::Artifact] artifact
|
8
|
+
# @param [#to_s] name
|
9
|
+
# @param [Solve::Constraint, #to_s] constraint
|
10
|
+
def initialize(artifact, name, constraint)
|
11
|
+
@artifact = artifact
|
12
|
+
@name = name
|
13
|
+
@constraint = case constraint
|
14
|
+
when Solve::Constraint
|
15
|
+
constraint
|
16
|
+
else
|
17
|
+
Constraint.new(constraint)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return [Solve::Dependency, nil]
|
22
|
+
def delete
|
23
|
+
unless artifact.nil?
|
24
|
+
result = artifact.remove_dependency(self)
|
25
|
+
@artifact = nil
|
26
|
+
result
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# @param [Object] other
|
31
|
+
#
|
32
|
+
# @return [Boolean]
|
33
|
+
def ==(other)
|
34
|
+
other.is_a?(self.class) &&
|
35
|
+
self.artifact == other.artifact &&
|
36
|
+
self.constraint == other.constraint
|
37
|
+
end
|
38
|
+
alias_method :eql?, :==
|
39
|
+
end
|
40
|
+
end
|
data/lib/solve/errors.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
module Solve
|
2
|
+
class SolveError < StandardError; end
|
3
|
+
|
4
|
+
class InvalidVersionFormat < SolveError
|
5
|
+
attr_reader :version
|
6
|
+
|
7
|
+
# @param [#to_s] version
|
8
|
+
def initialize(version)
|
9
|
+
@version = version
|
10
|
+
end
|
11
|
+
|
12
|
+
def message
|
13
|
+
"'#{version}' did not contain a valid version string: 'x.y.z' or 'x.y'."
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class InvalidConstraintFormat < SolveError
|
18
|
+
attr_reader :constraint
|
19
|
+
|
20
|
+
# @param [#to_s] constraint
|
21
|
+
def initialize(constraint)
|
22
|
+
@constraint = constraint
|
23
|
+
end
|
24
|
+
|
25
|
+
def message
|
26
|
+
"'#{constraint}' did not contain a valid operator or a valid version string."
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class NoSolutionError < SolveError; end
|
31
|
+
end
|
data/lib/solve/graph.rb
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
module Solve
|
2
|
+
class Graph
|
3
|
+
def initialize
|
4
|
+
@artifacts = Hash.new
|
5
|
+
@demands = Hash.new
|
6
|
+
@dep_graph = DepSelector::DependencyGraph.new
|
7
|
+
end
|
8
|
+
|
9
|
+
# @overload artifacts(name, version)
|
10
|
+
# Return the Solve::Artifact from the collection of artifacts
|
11
|
+
# with the given name and version.
|
12
|
+
#
|
13
|
+
# @param [#to_s]
|
14
|
+
# @param [Solve::Version, #to_s]
|
15
|
+
#
|
16
|
+
# @return [Solve::Artifact]
|
17
|
+
# @overload artifacts
|
18
|
+
# Return the collection of artifacts
|
19
|
+
#
|
20
|
+
# @return [Array<Solve::Artifact>]
|
21
|
+
def artifacts(*args)
|
22
|
+
if args.empty?
|
23
|
+
return artifact_collection
|
24
|
+
end
|
25
|
+
unless args.length == 2
|
26
|
+
raise ArgumentError, "Unexpected number of arguments. You gave: #{args.length}. Expected: 0 or 2."
|
27
|
+
end
|
28
|
+
|
29
|
+
name, version = args
|
30
|
+
|
31
|
+
if name.nil? || version.nil?
|
32
|
+
raise ArgumentError, "A name and version must be specified. You gave: #{args}."
|
33
|
+
end
|
34
|
+
|
35
|
+
artifact = Artifact.new(self, name, version)
|
36
|
+
add_artifact(artifact)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Add a Solve::Artifact to the collection of artifacts and
|
40
|
+
# return the added Solve::Artifact. No change will be made
|
41
|
+
# if the artifact is already a member of the collection.
|
42
|
+
#
|
43
|
+
# @param [Solve::Artifact] artifact
|
44
|
+
#
|
45
|
+
# @return [Solve::Artifact]
|
46
|
+
def add_artifact(artifact)
|
47
|
+
unless has_artifact?(artifact)
|
48
|
+
@dep_graph.package(artifact.name).add_version(DepSelector::Version.new(artifact.version.to_s))
|
49
|
+
@artifacts[artifact.to_s] = artifact
|
50
|
+
end
|
51
|
+
|
52
|
+
artifact
|
53
|
+
end
|
54
|
+
|
55
|
+
# @param [Solve::Artifact, nil] artifact
|
56
|
+
def remove_artifact(artifact)
|
57
|
+
if has_artifact?(artifact)
|
58
|
+
@dep_graph.packages.delete(artifact.to_s)
|
59
|
+
@artifacts.delete(artifact.to_s)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# @param [Solve::Artifact] artifact
|
64
|
+
#
|
65
|
+
# @return [Boolean]
|
66
|
+
def has_artifact?(artifact)
|
67
|
+
@artifacts.has_key?(artifact.to_s)
|
68
|
+
end
|
69
|
+
|
70
|
+
# @overload demands(name, constraint)
|
71
|
+
# Return the Solve::Demand from the collection of demands
|
72
|
+
# with the given name and constraint.
|
73
|
+
#
|
74
|
+
# @param [#to_s]
|
75
|
+
# @param [Solve::Constraint, #to_s]
|
76
|
+
#
|
77
|
+
# @return [Solve::Demand]
|
78
|
+
# @overload demands(name)
|
79
|
+
# Return the Solve::Demand from the collection of demands
|
80
|
+
# with the given name.
|
81
|
+
#
|
82
|
+
# @param [#to_s]
|
83
|
+
#
|
84
|
+
# @return [Solve::Demand]
|
85
|
+
# @overload demands
|
86
|
+
# Return the collection of demands
|
87
|
+
#
|
88
|
+
# @return [Array<Solve::Demand>]
|
89
|
+
def demands(*args)
|
90
|
+
if args.empty?
|
91
|
+
return demand_collection
|
92
|
+
end
|
93
|
+
if args.length > 2
|
94
|
+
raise ArgumentError, "Unexpected number of arguments. You gave: #{args.length}. Expected: 2 or less."
|
95
|
+
end
|
96
|
+
|
97
|
+
name, constraint = args
|
98
|
+
constraint ||= ">= 0.0.0"
|
99
|
+
|
100
|
+
if name.nil?
|
101
|
+
raise ArgumentError, "A name must be specified. You gave: #{args}."
|
102
|
+
end
|
103
|
+
|
104
|
+
demand = Demand.new(self, name, constraint)
|
105
|
+
add_demand(demand)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Add a Solve::Demand to the collection of demands and
|
109
|
+
# return the added Solve::Demand. No change will be made
|
110
|
+
# if the demand is already a member of the collection.
|
111
|
+
#
|
112
|
+
# @param [Solve::Demand] demand
|
113
|
+
#
|
114
|
+
# @return [Solve::Demand]
|
115
|
+
def add_demand(demand)
|
116
|
+
unless has_demand?(demand)
|
117
|
+
@demands[demand.to_s] = demand
|
118
|
+
end
|
119
|
+
|
120
|
+
demand
|
121
|
+
end
|
122
|
+
alias_method :demand, :add_demand
|
123
|
+
|
124
|
+
# @param [Solve::Demand, nil] demand
|
125
|
+
def remove_demand(demand)
|
126
|
+
if has_demand?(demand)
|
127
|
+
@demands.delete(demand.to_s)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# @param [Solve::Demand] demand
|
132
|
+
#
|
133
|
+
# @return [Boolean]
|
134
|
+
def has_demand?(demand)
|
135
|
+
@demands.has_key?(demand.to_s)
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
attr_reader :dep_graph
|
141
|
+
|
142
|
+
# @return [Array<Solve::Artifact>]
|
143
|
+
def artifact_collection
|
144
|
+
@artifacts.collect { |name, artifact| artifact }
|
145
|
+
end
|
146
|
+
|
147
|
+
# @return [Array<Solve::Demand>]
|
148
|
+
def demand_collection
|
149
|
+
@demands.collect { |name, demand| demand }
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Solve
|
2
|
+
class Version
|
3
|
+
include Comparable
|
4
|
+
|
5
|
+
attr_reader :major
|
6
|
+
attr_reader :minor
|
7
|
+
attr_reader :patch
|
8
|
+
|
9
|
+
# @param [#to_s] ver_str
|
10
|
+
def initialize(ver_str = String.new)
|
11
|
+
@major, @minor, @patch = parse(ver_str)
|
12
|
+
end
|
13
|
+
|
14
|
+
def <=>(other)
|
15
|
+
[:major, :minor, :patch].each do |method|
|
16
|
+
ans = (self.send(method) <=> other.send(method))
|
17
|
+
return ans if ans != 0
|
18
|
+
end
|
19
|
+
0
|
20
|
+
end
|
21
|
+
|
22
|
+
def eql?(other)
|
23
|
+
other.is_a?(Version) && self == other
|
24
|
+
end
|
25
|
+
|
26
|
+
def inspect
|
27
|
+
to_s
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_s
|
31
|
+
"#{major}.#{minor}.#{patch}"
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def parse(ver_str = String.new)
|
37
|
+
@major, @minor, @patch = case ver_str.to_s
|
38
|
+
when /^(\d+)\.(\d+)\.(\d+)$/
|
39
|
+
[ $1.to_i, $2.to_i, $3.to_i ]
|
40
|
+
when /^(\d+)\.(\d+)$/
|
41
|
+
[ $1.to_i, $2.to_i, 0 ]
|
42
|
+
else
|
43
|
+
raise InvalidVersionFormat.new(ver_str)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/solve.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/solve/gem_version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.authors = ["Jamie Winsor"]
|
6
|
+
s.email = ["jamie@vialstudios.com"]
|
7
|
+
s.description = %q{A Ruby constraint solver}
|
8
|
+
s.summary = s.description
|
9
|
+
s.homepage = "https://github.com/reset/solve"
|
10
|
+
|
11
|
+
s.files = `git ls-files`.split($\)
|
12
|
+
s.executables = s.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
s.test_files = s.files.grep(%r{^spec/})
|
14
|
+
s.name = "solve"
|
15
|
+
s.require_paths = ["lib"]
|
16
|
+
s.version = Solve::VERSION
|
17
|
+
s.required_ruby_version = ">= 1.9.1"
|
18
|
+
|
19
|
+
s.add_runtime_dependency 'dep_selector', '~> 0.0.8'
|
20
|
+
|
21
|
+
s.add_development_dependency 'thor'
|
22
|
+
s.add_development_dependency 'rspec'
|
23
|
+
s.add_development_dependency 'fuubar'
|
24
|
+
s.add_development_dependency 'spork'
|
25
|
+
s.add_development_dependency 'yard'
|
26
|
+
s.add_development_dependency 'redcarpet'
|
27
|
+
s.add_development_dependency 'guard'
|
28
|
+
s.add_development_dependency 'guard-rspec'
|
29
|
+
s.add_development_dependency 'guard-spork'
|
30
|
+
s.add_development_dependency 'guard-yard'
|
31
|
+
s.add_development_dependency 'coolline'
|
32
|
+
end
|