semantic_puppet 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +10 -0
- data/Gemfile +20 -0
- data/Gemfile.lock +38 -0
- data/README.md +64 -0
- data/Rakefile +69 -0
- data/lib/semantic_puppet.rb +7 -0
- data/lib/semantic_puppet/dependency.rb +181 -0
- data/lib/semantic_puppet/dependency/graph.rb +60 -0
- data/lib/semantic_puppet/dependency/graph_node.rb +117 -0
- data/lib/semantic_puppet/dependency/module_release.rb +46 -0
- data/lib/semantic_puppet/dependency/source.rb +25 -0
- data/lib/semantic_puppet/dependency/unsatisfiable_graph.rb +31 -0
- data/lib/semantic_puppet/version.rb +185 -0
- data/lib/semantic_puppet/version_range.rb +422 -0
- data/semantic_puppet.gemspec +30 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/unit/semantic_puppet/dependency/graph_node_spec.rb +141 -0
- data/spec/unit/semantic_puppet/dependency/graph_spec.rb +162 -0
- data/spec/unit/semantic_puppet/dependency/module_release_spec.rb +143 -0
- data/spec/unit/semantic_puppet/dependency/source_spec.rb +5 -0
- data/spec/unit/semantic_puppet/dependency/unsatisfiable_graph_spec.rb +44 -0
- data/spec/unit/semantic_puppet/dependency_spec.rb +383 -0
- data/spec/unit/semantic_puppet/version_range_spec.rb +307 -0
- data/spec/unit/semantic_puppet/version_spec.rb +644 -0
- metadata +169 -0
data/.gitignore
ADDED
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
-m markdown
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# Change Log
|
2
|
+
All notable changes to this project will be documented in this file.
|
3
|
+
This project adheres to [Semantic Versioning](http://semver.org/).
|
4
|
+
|
5
|
+
## [Unreleased version][Unreleased date]
|
6
|
+
### Changed
|
7
|
+
|
8
|
+
## 0.1.0 - 2015-03-23
|
9
|
+
### Added
|
10
|
+
- initial release in concert with current Puppet Module Tool v4.0.0 behavior
|
data/Gemfile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
|
3
|
+
group :development do
|
4
|
+
gem 'rake'
|
5
|
+
gem 'rubygems-tasks'
|
6
|
+
end
|
7
|
+
|
8
|
+
group :test do
|
9
|
+
gem 'rspec'
|
10
|
+
end
|
11
|
+
|
12
|
+
group :metrics do
|
13
|
+
gem 'cane', :platform => [ :mri_19, :mri_20, :mri_21 ]
|
14
|
+
gem 'simplecov', :platform => [ :mri_19, :mri_20, :mri_21 ]
|
15
|
+
end
|
16
|
+
|
17
|
+
group :doc do
|
18
|
+
gem 'yard', '~> 0.8', :platform => [ :mri_19, :mri_20, :mri_21 ]
|
19
|
+
gem 'redcarpet', :platform => [ :mri_19, :mri_20, :mri_21 ]
|
20
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
GEM
|
2
|
+
remote: https://rubygems.org/
|
3
|
+
specs:
|
4
|
+
cane (2.6.1)
|
5
|
+
parallel
|
6
|
+
diff-lcs (1.2.5)
|
7
|
+
docile (1.1.1)
|
8
|
+
multi_json (1.8.2)
|
9
|
+
parallel (0.9.1)
|
10
|
+
rake (10.1.0)
|
11
|
+
redcarpet (3.0.0)
|
12
|
+
rspec (2.14.1)
|
13
|
+
rspec-core (~> 2.14.0)
|
14
|
+
rspec-expectations (~> 2.14.0)
|
15
|
+
rspec-mocks (~> 2.14.0)
|
16
|
+
rspec-core (2.14.7)
|
17
|
+
rspec-expectations (2.14.4)
|
18
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
19
|
+
rspec-mocks (2.14.4)
|
20
|
+
rubygems-tasks (0.2.4)
|
21
|
+
simplecov (0.8.2)
|
22
|
+
docile (~> 1.1.0)
|
23
|
+
multi_json
|
24
|
+
simplecov-html (~> 0.8.0)
|
25
|
+
simplecov-html (0.8.0)
|
26
|
+
yard (0.8.7.3)
|
27
|
+
|
28
|
+
PLATFORMS
|
29
|
+
ruby
|
30
|
+
|
31
|
+
DEPENDENCIES
|
32
|
+
cane
|
33
|
+
rake
|
34
|
+
redcarpet
|
35
|
+
rspec
|
36
|
+
rubygems-tasks
|
37
|
+
simplecov
|
38
|
+
yard (~> 0.8)
|
data/README.md
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
SemanticPuppet
|
2
|
+
==============
|
3
|
+
|
4
|
+
Library of useful tools for working with Semantic Versions and module
|
5
|
+
dependencies.
|
6
|
+
|
7
|
+
Description
|
8
|
+
-----------
|
9
|
+
|
10
|
+
Library of tools used by Puppet to parse, validate, and compare Semantic
|
11
|
+
Versions and Version Ranges and to query and resolve module dependencies.
|
12
|
+
|
13
|
+
For sparse, but accurate documentation, please see the docs directory.
|
14
|
+
|
15
|
+
Note that this is a 0 release version, and things can change. Expect that the
|
16
|
+
version and version range code to stay relatively stable, but the module
|
17
|
+
dependency code is expected to change.
|
18
|
+
|
19
|
+
This library is used by a number of Puppet Labs projects, including
|
20
|
+
[Puppet](https://github.com/puppetlabs/puppet) and
|
21
|
+
[r10k](https://github.com/puppetlabs/r10k).
|
22
|
+
|
23
|
+
Requirements
|
24
|
+
------------
|
25
|
+
|
26
|
+
Semantic_puppet will work on several ruby versions, including 1.9.3, 2.0.0, and
|
27
|
+
2.1.0. Ruby 1.8.7 is immediately deprecated as it is in
|
28
|
+
[r10k](https://github.com/puppetlabs/r10k).
|
29
|
+
|
30
|
+
No gem/library requirements.
|
31
|
+
|
32
|
+
Installation
|
33
|
+
------------
|
34
|
+
|
35
|
+
### Rubygems
|
36
|
+
|
37
|
+
For general use, you should install semantic_puppet from Ruby gems:
|
38
|
+
|
39
|
+
gem install semantic_puppet
|
40
|
+
|
41
|
+
### Github
|
42
|
+
|
43
|
+
If you have more specific needs or plan on modifying semantic_puppet you can
|
44
|
+
install it out of a git repository:
|
45
|
+
|
46
|
+
git clone git://github.com/puppetlabs/semantic_puppet
|
47
|
+
|
48
|
+
Usage
|
49
|
+
-----
|
50
|
+
|
51
|
+
SemanticPuppet is intended to be used as a library.
|
52
|
+
|
53
|
+
### Verison Range Operator Support
|
54
|
+
|
55
|
+
SemanticPuppet will support the same version range operators as those used when
|
56
|
+
publishing modules to [Puppet Forge](https://forge.puppetlabs.com) which is
|
57
|
+
documented at
|
58
|
+
[Publishing Modules on the Puppet Forge](https://docs.puppetlabs.com/puppet/latest/reference/modules_publishing.html#dependencies-in-metadatajson).
|
59
|
+
|
60
|
+
Contributors
|
61
|
+
------------
|
62
|
+
|
63
|
+
Pieter van de Bruggen wrote the library originally, with additions by Alex
|
64
|
+
Dreyer, Jesse Scott and Anderson Mills.
|
data/Rakefile
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
# RSpec tasks
|
2
|
+
begin
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
|
5
|
+
# Create the 'spec' task
|
6
|
+
RSpec::Core::RakeTask.new(:spec) do |task|
|
7
|
+
task.rspec_opts = '--color'
|
8
|
+
end
|
9
|
+
|
10
|
+
namespace :spec do
|
11
|
+
desc "Run the test suite and generate coverage metrics"
|
12
|
+
task :coverage => [ :simplecov, :spec ]
|
13
|
+
|
14
|
+
# Add test coverage to the 'spec' task.
|
15
|
+
task :simplecov do
|
16
|
+
ENV['COVERAGE'] = '1'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
task :default => :spec
|
21
|
+
rescue LoadError
|
22
|
+
warn "[Warning]: Could not load `rspec`."
|
23
|
+
end
|
24
|
+
|
25
|
+
# YARD tasks
|
26
|
+
begin
|
27
|
+
require 'yard'
|
28
|
+
require 'yard/rake/yardoc_task'
|
29
|
+
|
30
|
+
YARD::Rake::YardocTask.new(:doc) do |yardoc|
|
31
|
+
yardoc.files = [ 'lib/**/*.rb', '-', '**/*.md' ]
|
32
|
+
end
|
33
|
+
rescue LoadError
|
34
|
+
warn "[Warning]: Could not load `yard`."
|
35
|
+
end
|
36
|
+
|
37
|
+
# Cane tasks
|
38
|
+
begin
|
39
|
+
require 'cane/rake_task'
|
40
|
+
|
41
|
+
Cane::RakeTask.new(:cane) do |cane|
|
42
|
+
cane.add_threshold 'coverage/.last_run.json', :>=, 100
|
43
|
+
cane.abc_max = 15
|
44
|
+
end
|
45
|
+
|
46
|
+
Rake::Task['cane'].prerequisites << Rake::Task['spec:coverage']
|
47
|
+
Rake::Task[:default].clear_prerequisites
|
48
|
+
task :default => :cane
|
49
|
+
rescue LoadError
|
50
|
+
warn "[Warning]: Could not load `cane`."
|
51
|
+
end
|
52
|
+
|
53
|
+
# Gem tasks
|
54
|
+
begin
|
55
|
+
require 'rubygems/tasks'
|
56
|
+
|
57
|
+
task :gem => 'gem:build'
|
58
|
+
task :validate => [ 'cane', 'doc', 'gem:validate' ]
|
59
|
+
|
60
|
+
namespace :gem do
|
61
|
+
Gem::Tasks.new(
|
62
|
+
:tag => { :format => 'v%s' },
|
63
|
+
:sign => { :checksum => true, :pgp => true },
|
64
|
+
:build => { :tar => true }
|
65
|
+
)
|
66
|
+
end
|
67
|
+
rescue LoadError
|
68
|
+
warn "[Warning]: Could not load `rubygems/tasks`."
|
69
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
require 'semantic_puppet'
|
2
|
+
|
3
|
+
module SemanticPuppet
|
4
|
+
module Dependency
|
5
|
+
extend self
|
6
|
+
|
7
|
+
autoload :Graph, 'semantic_puppet/dependency/graph'
|
8
|
+
autoload :GraphNode, 'semantic_puppet/dependency/graph_node'
|
9
|
+
autoload :ModuleRelease, 'semantic_puppet/dependency/module_release'
|
10
|
+
autoload :Source, 'semantic_puppet/dependency/source'
|
11
|
+
|
12
|
+
autoload :UnsatisfiableGraph, 'semantic_puppet/dependency/unsatisfiable_graph'
|
13
|
+
|
14
|
+
# @!group Sources
|
15
|
+
|
16
|
+
# @return [Array<Source>] a frozen copy of the {Source} list
|
17
|
+
def sources
|
18
|
+
(@sources ||= []).dup.freeze
|
19
|
+
end
|
20
|
+
|
21
|
+
# Appends a new {Source} to the current list.
|
22
|
+
# @param source [Source] the {Source} to add
|
23
|
+
# @return [void]
|
24
|
+
def add_source(source)
|
25
|
+
sources
|
26
|
+
@sources << source
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
|
30
|
+
# Clears the current list of {Source}s.
|
31
|
+
# @return [void]
|
32
|
+
def clear_sources
|
33
|
+
sources
|
34
|
+
@sources.clear
|
35
|
+
nil
|
36
|
+
end
|
37
|
+
|
38
|
+
# @!endgroup
|
39
|
+
|
40
|
+
# Fetches a graph of modules and their dependencies from the currently
|
41
|
+
# configured list of {Source}s.
|
42
|
+
#
|
43
|
+
# @todo Return a specialized "Graph" object.
|
44
|
+
# @todo Allow for external constraints to be added to the graph.
|
45
|
+
# @see #sources
|
46
|
+
# @see #add_source
|
47
|
+
# @see #clear_sources
|
48
|
+
#
|
49
|
+
# @param modules [{ String => String }]
|
50
|
+
# @return [Graph] the root of a dependency graph
|
51
|
+
def query(modules)
|
52
|
+
constraints = Hash[modules.map { |k, v| [ k, VersionRange.parse(v) ] }]
|
53
|
+
|
54
|
+
graph = Graph.new(constraints)
|
55
|
+
fetch_dependencies(graph)
|
56
|
+
return graph
|
57
|
+
end
|
58
|
+
|
59
|
+
# Given a graph result from {#query}, this method will resolve the graph of
|
60
|
+
# dependencies, if possible, into a flat list of the best suited modules. If
|
61
|
+
# the dependency graph does not have a suitable resolution, this method will
|
62
|
+
# raise an exception to that effect.
|
63
|
+
#
|
64
|
+
# @param graph [Graph] the root of a dependency graph
|
65
|
+
# @return [Array<ModuleRelease>] the list of releases to act on
|
66
|
+
def resolve(graph)
|
67
|
+
catch :next do
|
68
|
+
return walk(graph, graph.dependencies.dup)
|
69
|
+
end
|
70
|
+
raise UnsatisfiableGraph.new(graph)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Fetches all available releases for the given module name.
|
74
|
+
#
|
75
|
+
# @param name [String] the module name to find releases for
|
76
|
+
# @return [Array<ModuleRelease>] the available releases
|
77
|
+
def fetch_releases(name)
|
78
|
+
releases = {}
|
79
|
+
|
80
|
+
sources.each do |source|
|
81
|
+
source.fetch(name).each do |dependency|
|
82
|
+
releases[dependency.version] ||= dependency
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
return releases.values
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
# Iterates over a changing set of dependencies in search of the best
|
92
|
+
# solution available. Fitness is specified as meeting all the constraints
|
93
|
+
# placed on it, being {ModuleRelease#satisfied? satisfied}, and having the
|
94
|
+
# greatest version number (with stability being preferred over prereleases).
|
95
|
+
#
|
96
|
+
# @todo Traversal order is not presently guaranteed.
|
97
|
+
#
|
98
|
+
# @param graph [Graph] the root of a dependency graph
|
99
|
+
# @param dependencies [{ String => Array<ModuleRelease> }] the dependencies
|
100
|
+
# @param considering [Array<GraphNode>] the set of releases being tested
|
101
|
+
# @return [Array<GraphNode>] the list of releases to use, if successful
|
102
|
+
def walk(graph, dependencies, *considering)
|
103
|
+
return considering if dependencies.empty?
|
104
|
+
|
105
|
+
# Selecting a dependency from the collection...
|
106
|
+
name = dependencies.keys.sort.first
|
107
|
+
deps = dependencies.delete(name)
|
108
|
+
|
109
|
+
# ... (and stepping over it if we've seen it before) ...
|
110
|
+
unless (deps & considering).empty?
|
111
|
+
return walk(graph, dependencies, *considering)
|
112
|
+
end
|
113
|
+
|
114
|
+
# ... we'll iterate through the list of possible versions in order.
|
115
|
+
preferred_releases(deps).reverse_each do |dep|
|
116
|
+
|
117
|
+
# We should skip any releases that violate any module's constraints.
|
118
|
+
unless [graph, *considering].all? { |x| x.satisfies_constraints?(dep) }
|
119
|
+
next
|
120
|
+
end
|
121
|
+
|
122
|
+
# We should skip over any releases that violate graph-level constraints.
|
123
|
+
potential_solution = considering.dup << dep
|
124
|
+
unless graph.satisfies_graph? potential_solution
|
125
|
+
next
|
126
|
+
end
|
127
|
+
|
128
|
+
catch :next do
|
129
|
+
# After adding any new dependencies and imposing our own constraints
|
130
|
+
# on existing dependencies, we'll mark ourselves as "under
|
131
|
+
# consideration" and recurse.
|
132
|
+
merged = dependencies.merge(dep.dependencies) { |_,a,b| a & b }
|
133
|
+
|
134
|
+
# If all subsequent dependencies resolved well, the recursive call
|
135
|
+
# will return a completed dependency list. If there were problems
|
136
|
+
# resolving our dependencies, we'll catch `:next`, which will cause
|
137
|
+
# us to move to the next possibility.
|
138
|
+
return walk(graph, merged, *potential_solution)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Once we've exhausted all of our possible versions, we know that our
|
143
|
+
# last choice was unusable, so we'll unwind the stack and make a new
|
144
|
+
# choice.
|
145
|
+
throw :next
|
146
|
+
end
|
147
|
+
|
148
|
+
# Given a {ModuleRelease}, this method will iterate through the current
|
149
|
+
# list of {Source}s to find the complete list of versions available for its
|
150
|
+
# dependencies.
|
151
|
+
#
|
152
|
+
# @param node [GraphNode] the node to fetch details for
|
153
|
+
# @return [void]
|
154
|
+
def fetch_dependencies(node, cache = {})
|
155
|
+
node.dependency_names.each do |name|
|
156
|
+
unless cache.key?(name)
|
157
|
+
cache[name] = fetch_releases(name)
|
158
|
+
cache[name].each { |dep| fetch_dependencies(dep, cache) }
|
159
|
+
end
|
160
|
+
|
161
|
+
node << cache[name]
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Given a list of potential releases, this method returns the most suitable
|
166
|
+
# releases for exploration. Only {ModuleRelease#satisfied? satisfied}
|
167
|
+
# releases are considered, and releases with stable versions are preferred.
|
168
|
+
#
|
169
|
+
# @param releases [Array<ModuleRelease>] a list of potential releases
|
170
|
+
# @return [Array<ModuleRelease>] releases open for consideration
|
171
|
+
def preferred_releases(releases)
|
172
|
+
satisfied = releases.select { |x| x.satisfied? }
|
173
|
+
|
174
|
+
if satisfied.any? { |x| x.version.stable? }
|
175
|
+
return satisfied.select { |x| x.version.stable? }
|
176
|
+
else
|
177
|
+
return satisfied
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'semantic_puppet/dependency'
|
2
|
+
|
3
|
+
module SemanticPuppet
|
4
|
+
module Dependency
|
5
|
+
class Graph
|
6
|
+
include GraphNode
|
7
|
+
|
8
|
+
attr_reader :modules
|
9
|
+
|
10
|
+
# Create a new instance of a dependency graph.
|
11
|
+
#
|
12
|
+
# @param modules [{String => VersionRange}] the required module
|
13
|
+
# set and their version constraints
|
14
|
+
def initialize(modules = {})
|
15
|
+
@modules = modules.keys
|
16
|
+
|
17
|
+
modules.each do |name, range|
|
18
|
+
add_constraint('initialize', name, range.to_s) do |node|
|
19
|
+
range === node.version
|
20
|
+
end
|
21
|
+
|
22
|
+
add_dependency(name)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Constrains graph solutions based on the given block. Graph constraints
|
27
|
+
# are used to describe fundamental truths about the tooling or module
|
28
|
+
# system (e.g.: module names contain a namespace component which is
|
29
|
+
# dropped during install, so module names must be unique excluding the
|
30
|
+
# namespace).
|
31
|
+
#
|
32
|
+
# @example Ensuring a single source for all modules
|
33
|
+
# @graph.add_constraint('installed', mod.name) do |nodes|
|
34
|
+
# nodes.count { |node| node.source } == 1
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# @see #considering_solution?
|
38
|
+
#
|
39
|
+
# @param source [String, Symbol] a name describing the source of the
|
40
|
+
# constraint
|
41
|
+
# @yieldparam nodes [Array<GraphNode>] the nodes to test the constraint
|
42
|
+
# against
|
43
|
+
# @yieldreturn [Boolean] whether the node passed the constraint
|
44
|
+
# @return [void]
|
45
|
+
def add_graph_constraint(source, &block)
|
46
|
+
constraints[:graph] << [ source, block ]
|
47
|
+
end
|
48
|
+
|
49
|
+
# Checks the proposed solution (or partial solution) against the graph's
|
50
|
+
# constraints.
|
51
|
+
#
|
52
|
+
# @see #add_graph_constraint
|
53
|
+
#
|
54
|
+
# @return [Boolean] true if none of the graph constraints are violated
|
55
|
+
def satisfies_graph?(solution)
|
56
|
+
constraints[:graph].all? { |_, check| check[solution] }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|