berkshelf 3.0.0.beta7 → 3.0.0.beta8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +4 -1
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +0 -1
- data/Guardfile +0 -8
- data/README.md +33 -13
- data/berkshelf.gemspec +3 -3
- data/features/commands/install.feature +16 -88
- data/features/commands/search.feature +15 -0
- data/features/commands/shelf/show.feature +2 -2
- data/features/commands/shelf/uninstall.feature +1 -1
- data/features/commands/show.feature +3 -3
- data/features/commands/update.feature +29 -1
- data/features/commands/upload.feature +172 -7
- data/features/commands/vendor.feature +32 -0
- data/features/json_formatter.feature +26 -24
- data/features/lifecycle.feature +285 -0
- data/features/lockfile.feature +9 -7
- data/features/step_definitions/chef_server_steps.rb +1 -0
- data/features/step_definitions/cli_steps.rb +2 -2
- data/features/step_definitions/filesystem_steps.rb +2 -4
- data/gem_graph.png +0 -0
- data/generator_files/chefignore +0 -2
- data/lib/berkshelf.rb +39 -14
- data/lib/berkshelf/berksfile.rb +161 -113
- data/lib/berkshelf/cached_cookbook.rb +2 -2
- data/lib/berkshelf/cli.rb +15 -3
- data/lib/berkshelf/commands/shelf.rb +3 -7
- data/lib/berkshelf/community_rest.rb +9 -9
- data/lib/berkshelf/config.rb +3 -3
- data/lib/berkshelf/cookbook_generator.rb +0 -8
- data/lib/berkshelf/cookbook_store.rb +1 -2
- data/lib/berkshelf/dependency.rb +25 -138
- data/lib/berkshelf/downloader.rb +41 -7
- data/lib/berkshelf/errors.rb +113 -214
- data/lib/berkshelf/formatters/base.rb +42 -0
- data/lib/berkshelf/formatters/human.rb +145 -0
- data/lib/berkshelf/formatters/json.rb +149 -133
- data/lib/berkshelf/formatters/null.rb +8 -18
- data/lib/berkshelf/init_generator.rb +1 -1
- data/lib/berkshelf/installer.rb +115 -104
- data/lib/berkshelf/location.rb +22 -121
- data/lib/berkshelf/locations/base.rb +75 -0
- data/lib/berkshelf/locations/git.rb +196 -0
- data/lib/berkshelf/locations/github.rb +8 -0
- data/lib/berkshelf/locations/path.rb +78 -0
- data/lib/berkshelf/lockfile.rb +452 -290
- data/lib/berkshelf/logger.rb +9 -3
- data/lib/berkshelf/mixin/logging.rb +4 -9
- data/lib/berkshelf/resolver.rb +12 -12
- data/lib/berkshelf/source.rb +13 -1
- data/lib/berkshelf/version.rb +1 -1
- data/spec/fixtures/cookbooks/example_cookbook-0.5.0/metadata.rb +3 -7
- data/spec/fixtures/cookbooks/example_cookbook/metadata.rb +3 -6
- data/spec/spec_helper.rb +5 -6
- data/spec/support/matchers/file_system_matchers.rb +4 -0
- data/spec/support/shared_examples/formatter.rb +11 -0
- data/spec/unit/berkshelf/berksfile_spec.rb +25 -28
- data/spec/unit/berkshelf/cli_spec.rb +19 -11
- data/spec/unit/berkshelf/dependency_spec.rb +4 -164
- data/spec/unit/berkshelf/formatters/base_spec.rb +35 -0
- data/spec/unit/berkshelf/formatters/human_spec.rb +7 -0
- data/spec/unit/berkshelf/formatters/json_spec.rb +7 -0
- data/spec/unit/berkshelf/formatters/null_spec.rb +7 -11
- data/spec/unit/berkshelf/location_spec.rb +16 -144
- data/spec/unit/berkshelf/locations/base_spec.rb +80 -0
- data/spec/unit/berkshelf/locations/git_spec.rb +249 -0
- data/spec/unit/berkshelf/locations/path_spec.rb +107 -0
- data/spec/unit/berkshelf/lockfile_parser_spec.rb +3 -3
- data/spec/unit/berkshelf/lockfile_spec.rb +55 -11
- data/spec/unit/berkshelf/logger_spec.rb +2 -2
- data/spec/unit/berkshelf/mixin/logging_spec.rb +5 -9
- data/spec/unit/berkshelf/source_spec.rb +32 -13
- data/spec/unit/berkshelf_spec.rb +6 -9
- metadata +33 -33
- data/.ruby-version +0 -1
- data/berkshelf-complete.sh +0 -75
- data/lib/berkshelf/formatters.rb +0 -110
- data/lib/berkshelf/formatters/human_readable.rb +0 -142
- data/lib/berkshelf/git.rb +0 -204
- data/lib/berkshelf/locations/git_location.rb +0 -135
- data/lib/berkshelf/locations/github_location.rb +0 -55
- data/lib/berkshelf/locations/mercurial_location.rb +0 -114
- data/lib/berkshelf/locations/path_location.rb +0 -88
- data/lib/berkshelf/mercurial.rb +0 -146
- data/lib/berkshelf/mixin.rb +0 -7
- data/spec/support/mercurial.rb +0 -123
- data/spec/unit/berkshelf/formatters_spec.rb +0 -114
- data/spec/unit/berkshelf/git_spec.rb +0 -312
- data/spec/unit/berkshelf/locations/git_location_spec.rb +0 -126
- data/spec/unit/berkshelf/locations/mercurial_location_spec.rb +0 -131
- data/spec/unit/berkshelf/locations/path_location_spec.rb +0 -25
- data/spec/unit/berkshelf/mercurial_spec.rb +0 -172
@@ -0,0 +1,75 @@
|
|
1
|
+
module Berkshelf
|
2
|
+
class BaseLocation
|
3
|
+
attr_reader :dependency
|
4
|
+
attr_reader :options
|
5
|
+
|
6
|
+
def initialize(dependency, options = {})
|
7
|
+
@dependency = dependency
|
8
|
+
@options = options
|
9
|
+
end
|
10
|
+
|
11
|
+
# Determine if this revision is installed.
|
12
|
+
#
|
13
|
+
# @return [Boolean]
|
14
|
+
def installed?
|
15
|
+
raise AbstractFunction,
|
16
|
+
"#installed? must be implemented on #{self.class.name}!"
|
17
|
+
end
|
18
|
+
|
19
|
+
# Install the given cookbook. Subclasses that implement this method should
|
20
|
+
# perform all the installation and validation steps required.
|
21
|
+
#
|
22
|
+
# @return [void]
|
23
|
+
def install
|
24
|
+
raise AbstractFunction,
|
25
|
+
"#install must be implemented on #{self.class.name}!"
|
26
|
+
end
|
27
|
+
|
28
|
+
# The cached cookbook for this location.
|
29
|
+
#
|
30
|
+
# @return [CachedCookbook]
|
31
|
+
def cached_cookbook
|
32
|
+
raise AbstractFunction,
|
33
|
+
"#cached_cookbook must be implemented on #{self.class.name}!"
|
34
|
+
end
|
35
|
+
|
36
|
+
# The lockfile representation of this location.
|
37
|
+
#
|
38
|
+
# @return [string]
|
39
|
+
def to_lock
|
40
|
+
raise AbstractFunction,
|
41
|
+
"#to_lock must be implemented on #{self.class.name}!"
|
42
|
+
end
|
43
|
+
|
44
|
+
# Ensure the given {CachedCookbook} is valid
|
45
|
+
#
|
46
|
+
# @param [String] path
|
47
|
+
# the path to the possible cookbook
|
48
|
+
#
|
49
|
+
# @raise [NotACookbook]
|
50
|
+
# if the cookbook at the path does not have a metadata
|
51
|
+
# @raise [CookbookValidationFailure]
|
52
|
+
# if given CachedCookbook does not satisfy the constraint of the location
|
53
|
+
# @raise [MismatcheCookboookName]
|
54
|
+
# if the cookbook does not have a name or if the name is different
|
55
|
+
#
|
56
|
+
# @return [true]
|
57
|
+
def validate_cached!(path)
|
58
|
+
unless File.cookbook?(path)
|
59
|
+
raise NotACookbook.new(path)
|
60
|
+
end
|
61
|
+
|
62
|
+
cookbook = CachedCookbook.from_path(path)
|
63
|
+
|
64
|
+
unless @dependency.version_constraint.satisfies?(cookbook.version)
|
65
|
+
raise CookbookValidationFailure.new(dependency, cookbook)
|
66
|
+
end
|
67
|
+
|
68
|
+
unless @dependency.name == cookbook.cookbook_name
|
69
|
+
raise MismatchedCookbookName.new(dependency, cookbook)
|
70
|
+
end
|
71
|
+
|
72
|
+
true
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
require 'buff/shell_out'
|
2
|
+
|
3
|
+
module Berkshelf
|
4
|
+
class GitLocation < BaseLocation
|
5
|
+
class GitError < BerkshelfError; status_code(400); end
|
6
|
+
|
7
|
+
class GitNotInstalled < GitError
|
8
|
+
def initialize
|
9
|
+
super 'You need to install Git before you can download ' \
|
10
|
+
'cookbooks from git repositories. For more information, please ' \
|
11
|
+
'see the Git docs: http://git-scm.org.'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class GitCommandError < GitError
|
16
|
+
def initialize(command, path, stderr = nil)
|
17
|
+
out = "Git error: command `git #{command}` failed. If this error "
|
18
|
+
out << "persists, try removing the cache directory at '#{path}'."
|
19
|
+
|
20
|
+
if stderr
|
21
|
+
out << "Output from the command:\n\n"
|
22
|
+
out << stderr
|
23
|
+
end
|
24
|
+
|
25
|
+
super(out)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_reader :uri
|
30
|
+
attr_reader :branch
|
31
|
+
attr_reader :tag
|
32
|
+
attr_reader :ref
|
33
|
+
attr_reader :revision
|
34
|
+
attr_reader :rel
|
35
|
+
|
36
|
+
def initialize(dependency, options = {})
|
37
|
+
super
|
38
|
+
|
39
|
+
@uri = options[:git]
|
40
|
+
@branch = options[:branch]
|
41
|
+
@tag = options[:tag]
|
42
|
+
@ref = options[:ref]
|
43
|
+
@revision = options[:revision]
|
44
|
+
@rel = options[:rel]
|
45
|
+
|
46
|
+
# The revision to parse
|
47
|
+
@rev_parse = options[:ref] || options[:branch] || options[:tag] || 'master'
|
48
|
+
end
|
49
|
+
|
50
|
+
# @see BaseLoation#installed?
|
51
|
+
def installed?
|
52
|
+
revision && install_path.exist?
|
53
|
+
end
|
54
|
+
|
55
|
+
# Install this git cookbook into the cookbook store. This method leverages
|
56
|
+
# a cached git copy and a scratch directory to prevent bad cookbooks from
|
57
|
+
# making their way into the cookbook store.
|
58
|
+
#
|
59
|
+
# @see BaseLOcation#install
|
60
|
+
def install
|
61
|
+
scratch_path = Pathname.new(Dir.mktmpdir)
|
62
|
+
|
63
|
+
if cached?
|
64
|
+
Dir.chdir(cache_path) do
|
65
|
+
git %|fetch --force --tags #{uri} "refs/heads/*:refs/heads/*"|
|
66
|
+
end
|
67
|
+
else
|
68
|
+
git %|clone #{uri} "#{cache_path}" --bare --no-hardlinks|
|
69
|
+
end
|
70
|
+
|
71
|
+
Dir.chdir(cache_path) do
|
72
|
+
@revision ||= git %|rev-parse #{@rev_parse}|
|
73
|
+
end
|
74
|
+
|
75
|
+
# Clone into a scratch directory for validations
|
76
|
+
git %|clone --no-checkout "#{cache_path}" "#{scratch_path}"|
|
77
|
+
|
78
|
+
# Make sure the scratch directory is up-to-date and account for rel paths
|
79
|
+
Dir.chdir(scratch_path) do
|
80
|
+
git %|fetch --force --tags "#{cache_path}"|
|
81
|
+
git %|reset --hard #{@revision}|
|
82
|
+
|
83
|
+
if rel
|
84
|
+
git %|filter-branch --subdirectory-filter "#{rel}" --force|
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Validate the scratched path is a valid cookbook
|
89
|
+
validate_cached!(scratch_path)
|
90
|
+
|
91
|
+
# If we got this far, we should copy
|
92
|
+
FileUtils.rm_rf(install_path) if install_path.exist?
|
93
|
+
FileUtils.cp_r(scratch_path, install_path)
|
94
|
+
install_path.chmod(0777 & ~File.umask)
|
95
|
+
ensure
|
96
|
+
# Ensure the scratch directory is cleaned up
|
97
|
+
FileUtils.rm_rf(scratch_path)
|
98
|
+
end
|
99
|
+
|
100
|
+
# @see BaseLocation#cached_cookbook
|
101
|
+
def cached_cookbook
|
102
|
+
if installed?
|
103
|
+
@cached_cookbook ||= CachedCookbook.from_path(install_path)
|
104
|
+
else
|
105
|
+
nil
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def ==(other)
|
110
|
+
other.is_a?(GitLocation) &&
|
111
|
+
other.uri == uri &&
|
112
|
+
other.branch == branch &&
|
113
|
+
other.tag == tag &&
|
114
|
+
other.shortref == shortref &&
|
115
|
+
other.rel == rel
|
116
|
+
end
|
117
|
+
|
118
|
+
def to_s
|
119
|
+
info = tag || branch || shortref || @rev_parse
|
120
|
+
|
121
|
+
if rel
|
122
|
+
"#{uri} (at #{info}/#{rel})"
|
123
|
+
else
|
124
|
+
"#{uri} (at #{info})"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def to_lock
|
129
|
+
out = " git: #{uri}\n"
|
130
|
+
out << " revision: #{revision}\n"
|
131
|
+
out << " ref: #{shortref}\n" if shortref
|
132
|
+
out << " branch: #{branch}\n" if branch
|
133
|
+
out << " tag: #{tag}\n" if tag
|
134
|
+
out << " rel: #{rel}\n" if rel
|
135
|
+
out
|
136
|
+
end
|
137
|
+
|
138
|
+
protected
|
139
|
+
|
140
|
+
# The short ref (if one was given).
|
141
|
+
#
|
142
|
+
# @return [String, nil]
|
143
|
+
def shortref
|
144
|
+
ref && ref[0...7]
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
# Perform a git command.
|
150
|
+
#
|
151
|
+
# @param [String] command
|
152
|
+
# the command to run
|
153
|
+
# @param [Boolean] error
|
154
|
+
# whether to raise error if the command fails
|
155
|
+
#
|
156
|
+
# @raise [String]
|
157
|
+
# the +$stdout+ from the command
|
158
|
+
def git(command, error = true)
|
159
|
+
unless Berkshelf.which('git') || Berkshelf.which('git.exe')
|
160
|
+
raise GitNotInstalled.new
|
161
|
+
end
|
162
|
+
|
163
|
+
response = Buff::ShellOut.shell_out(%|git #{command}|)
|
164
|
+
|
165
|
+
if error && !response.success?
|
166
|
+
raise GitCommandError.new(command, cache_path, stderr = response.stderr)
|
167
|
+
end
|
168
|
+
|
169
|
+
response.stdout.strip
|
170
|
+
end
|
171
|
+
|
172
|
+
# Determine if this git repo has already been downloaded.
|
173
|
+
#
|
174
|
+
# @return [Boolean]
|
175
|
+
def cached?
|
176
|
+
cache_path.exist?
|
177
|
+
end
|
178
|
+
|
179
|
+
# The path where this cookbook would live in the store, if it were
|
180
|
+
# installed.
|
181
|
+
#
|
182
|
+
# @return [Pathname, nil]
|
183
|
+
def install_path
|
184
|
+
Berkshelf.cookbook_store.storage_path
|
185
|
+
.join("#{dependency.name}-#{revision}")
|
186
|
+
end
|
187
|
+
|
188
|
+
# The path where this git repository is cached.
|
189
|
+
#
|
190
|
+
# @return [Pathname]
|
191
|
+
def cache_path
|
192
|
+
Pathname.new(Berkshelf.berkshelf_path)
|
193
|
+
.join('.cache', 'git', Digest::SHA1.hexdigest(uri))
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Berkshelf
|
2
|
+
class PathLocation < BaseLocation
|
3
|
+
# Technically path locations are always installed, but this method
|
4
|
+
# intentionally returns +false+ to force validation of the cookbook at the
|
5
|
+
# path.
|
6
|
+
#
|
7
|
+
# @see BaseLocation#installed?
|
8
|
+
def installed?
|
9
|
+
false
|
10
|
+
end
|
11
|
+
|
12
|
+
# The installation for a path location is actually just a noop
|
13
|
+
#
|
14
|
+
# @see BaseLocation#install
|
15
|
+
def install
|
16
|
+
validate_cached!(expanded_path)
|
17
|
+
end
|
18
|
+
|
19
|
+
# @see BaseLocation#cached_cookbook
|
20
|
+
def cached_cookbook
|
21
|
+
@cached_cookbook ||= CachedCookbook.from_path(expanded_path)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns true if the location is a metadata location. By default, no
|
25
|
+
# locations are the metadata location.
|
26
|
+
#
|
27
|
+
# @return [Boolean]
|
28
|
+
def metadata?
|
29
|
+
!!options[:metadata]
|
30
|
+
end
|
31
|
+
|
32
|
+
# Return this PathLocation's path relative to the associated Berksfile. It
|
33
|
+
# is actually the path reative to the associated Berksfile's parent
|
34
|
+
# directory.
|
35
|
+
#
|
36
|
+
# @return [String]
|
37
|
+
# the relative path relative to the target
|
38
|
+
def relative_path
|
39
|
+
my_path = Pathname.new(options[:path]).expand_path
|
40
|
+
target_path = Pathname.new(dependency.berksfile.filepath).expand_path
|
41
|
+
target_path = target_path.dirname if target_path.file?
|
42
|
+
|
43
|
+
new_path = my_path.relative_path_from(target_path).to_s
|
44
|
+
|
45
|
+
return new_path if new_path.index('.') == 0
|
46
|
+
"./#{new_path}"
|
47
|
+
end
|
48
|
+
|
49
|
+
# The fully expanded path of this cookbook on disk, relative to the
|
50
|
+
# Berksfile.
|
51
|
+
#
|
52
|
+
# @return [String]
|
53
|
+
def expanded_path
|
54
|
+
parent = File.expand_path(File.dirname(dependency.berksfile.filepath))
|
55
|
+
File.expand_path(relative_path, parent)
|
56
|
+
end
|
57
|
+
|
58
|
+
def ==(other)
|
59
|
+
other.is_a?(PathLocation) &&
|
60
|
+
other.metadata? == metadata? &&
|
61
|
+
other.relative_path == relative_path
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_lock
|
65
|
+
out = " path: #{relative_path}\n"
|
66
|
+
out << " metadata: true\n" if metadata?
|
67
|
+
out
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_s
|
71
|
+
"source at #{relative_path}"
|
72
|
+
end
|
73
|
+
|
74
|
+
def inspect
|
75
|
+
"#<Berkshelf::PathLocation metadata: #{metadata?}, path: #{relative_path}>"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/berkshelf/lockfile.rb
CHANGED
@@ -21,10 +21,10 @@ module Berkshelf
|
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
|
-
DEFAULT_FILENAME = 'Berksfile.lock'
|
24
|
+
DEFAULT_FILENAME = 'Berksfile.lock'.freeze
|
25
25
|
|
26
|
-
DEPENDENCIES = 'DEPENDENCIES'
|
27
|
-
GRAPH = 'GRAPH'
|
26
|
+
DEPENDENCIES = 'DEPENDENCIES'.freeze
|
27
|
+
GRAPH = 'GRAPH'.freeze
|
28
28
|
|
29
29
|
include Berkshelf::Mixin::Logging
|
30
30
|
|
@@ -79,7 +79,9 @@ module Berkshelf
|
|
79
79
|
#
|
80
80
|
# 1. All dependencies defined in the Berksfile are present in this
|
81
81
|
# lockfile
|
82
|
-
# 2. Each dependency's
|
82
|
+
# 2. Each dependency's transitive dependencies are contained and locked
|
83
|
+
# in the lockfile
|
84
|
+
# 3. Each dependency's constraint in the Berksfile is still satisifed by
|
83
85
|
# the currently locked version
|
84
86
|
#
|
85
87
|
# This method does _not_ account for leaky dependencies (i.e. dependencies
|
@@ -89,14 +91,78 @@ module Berkshelf
|
|
89
91
|
# @return [Boolean]
|
90
92
|
# true if this lockfile is trusted, false otherwise
|
91
93
|
def trusted?
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
94
|
+
Berkshelf.log.info 'Checking if lockfile is trusted'
|
95
|
+
|
96
|
+
checked = {}
|
97
|
+
|
98
|
+
berksfile.dependencies.each do |dependency|
|
99
|
+
Berkshelf.log.debug "Checking #{dependency}"
|
100
|
+
|
101
|
+
checked[dependency.name] = true
|
102
|
+
|
103
|
+
locked = find(dependency)
|
104
|
+
if locked.nil?
|
105
|
+
Berkshelf.log.debug " Not in lockfile - cannot be trusted!"
|
106
|
+
return false
|
107
|
+
end
|
108
|
+
|
109
|
+
graphed = graph.find(dependency)
|
110
|
+
if graphed.nil?
|
111
|
+
Berkshelf.log.debug " Not in graph - cannot be trusted!"
|
112
|
+
return false
|
113
|
+
end
|
114
|
+
|
115
|
+
if cookbook = dependency.cached_cookbook
|
116
|
+
Berkshelf.log.debug " Detected there is a cached cookbook"
|
117
|
+
|
118
|
+
unless (cookbook.dependencies.keys - graphed.dependencies.keys).empty?
|
119
|
+
Berkshelf.log.debug " Cached cookbook has different dependencies - cannot be trusted!"
|
120
|
+
return false
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
unless dependency.location == locked.location
|
125
|
+
Berkshelf.log.debug " Different location - cannot be trusted!"
|
126
|
+
Berkshelf.log.debug " Dependency location: #{dependency.location.inspect}"
|
127
|
+
Berkshelf.log.debug " Locked location: #{locked.location.inspect}"
|
128
|
+
return false
|
129
|
+
end
|
130
|
+
|
131
|
+
unless dependency.version_constraint.satisfies?(graphed.version)
|
132
|
+
Berkshelf.log.debug " Version constraint is not satisified - cannot be trusted!"
|
133
|
+
return false
|
134
|
+
end
|
135
|
+
|
136
|
+
unless satisfies_transitive?(graphed, checked)
|
137
|
+
Berkshelf.log.debug " Transitive dependencies not satisfies - cannot be trusted!"
|
138
|
+
return false
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
true
|
143
|
+
end
|
144
|
+
|
145
|
+
# Recursive helper method for checking if transitive dependencies (i.e.
|
146
|
+
# those dependencies defined in the metadata) are satisfied. This method is
|
147
|
+
# used in calculating the trustworthiness of a lockfile.
|
148
|
+
#
|
149
|
+
# @param [GraphItem] graph_item
|
150
|
+
# the graph item to check transitive dependencies for
|
151
|
+
# @param [Hash] checked
|
152
|
+
# the list of already checked dependencies
|
153
|
+
#
|
154
|
+
# @return [Boolean]
|
155
|
+
def satisfies_transitive?(graph_item, checked)
|
156
|
+
graph_item.dependencies.all? do |name, constraint|
|
157
|
+
return true if checked[name]
|
158
|
+
|
159
|
+
checked[name] = true
|
160
|
+
|
161
|
+
graphed = graph.find(name)
|
162
|
+
return false if graphed.nil?
|
163
|
+
|
164
|
+
Solve::Constraint.new(constraint).satisfies?(graphed.version) &&
|
165
|
+
satisfies_transitive?(graphed, checked)
|
100
166
|
end
|
101
167
|
end
|
102
168
|
|
@@ -179,7 +245,7 @@ module Berkshelf
|
|
179
245
|
# @raise [DependencyNotFound]
|
180
246
|
# if this lockfile does not have the given dependency
|
181
247
|
# @raise [CookbookNotFound]
|
182
|
-
# if this lockfile has the dependency, but the cookbook is not
|
248
|
+
# if this lockfile has the dependency, but the cookbook is not installed
|
183
249
|
#
|
184
250
|
# @param [String, Dependency] dependency
|
185
251
|
# the dependency or name of the dependency to find
|
@@ -193,9 +259,10 @@ module Berkshelf
|
|
193
259
|
raise DependencyNotFound.new(Dependency.name(dependency))
|
194
260
|
end
|
195
261
|
|
196
|
-
unless locked.
|
197
|
-
|
198
|
-
|
262
|
+
unless locked.installed?
|
263
|
+
name = locked.name
|
264
|
+
version = locked.locked_version || locked.version_constraint
|
265
|
+
raise CookbookNotFound.new(name, version, 'in the cookbook store')
|
199
266
|
end
|
200
267
|
|
201
268
|
locked.cached_cookbook
|
@@ -221,20 +288,90 @@ module Berkshelf
|
|
221
288
|
# dependencies. Then it uses a recursive algorithm to safely remove any
|
222
289
|
# other dependencies from the graph that are no longer needed.
|
223
290
|
#
|
224
|
-
# @raise [
|
291
|
+
# @raise [CookbookNotFound]
|
225
292
|
# if the provided dependency does not exist
|
226
293
|
#
|
227
294
|
# @param [String] dependency
|
228
295
|
# the name of the cookbook to remove
|
229
296
|
def unlock(dependency)
|
230
|
-
unless dependency?(dependency)
|
231
|
-
raise Berkshelf::CookbookNotFound, "'#{dependency}' does not exist in this lockfile!"
|
232
|
-
end
|
233
|
-
|
234
297
|
@dependencies.delete(Dependency.name(dependency))
|
235
298
|
graph.remove(dependency)
|
236
299
|
end
|
237
300
|
|
301
|
+
# Iterate over each top-level dependency defined in the lockfile and
|
302
|
+
# check if that dependency is still defined in the Berksfile.
|
303
|
+
#
|
304
|
+
# If the dependency is no longer present in the Berksfile, it is "safely"
|
305
|
+
# removed using {Lockfile#unlock} and {Lockfile#remove}. This prevents
|
306
|
+
# the lockfile from "leaking" dependencies when they have been removed
|
307
|
+
# from the Berksfile, but still remained locked in the lockfile.
|
308
|
+
#
|
309
|
+
# If the dependency exists, a constraint comparison is conducted to verify
|
310
|
+
# that the locked dependency still satisifes the original constraint. This
|
311
|
+
# handles the edge case where a user has updated or removed a constraint
|
312
|
+
# on a dependency that already existed in the lockfile.
|
313
|
+
#
|
314
|
+
# @raise [OutdatedDependency]
|
315
|
+
# if the constraint exists, but is no longer satisifed by the existing
|
316
|
+
# locked version
|
317
|
+
#
|
318
|
+
# @return [Array<Dependency>]
|
319
|
+
def reduce!
|
320
|
+
# Store a list of cookbooks to ungraph
|
321
|
+
to_ungraph = {}
|
322
|
+
to_ignore = {}
|
323
|
+
|
324
|
+
# Unlock any locked dependencies that are no longer in the Berksfile
|
325
|
+
dependencies.each do |dependency|
|
326
|
+
unless berksfile.has_dependency?(dependency.name)
|
327
|
+
unlock(dependency)
|
328
|
+
|
329
|
+
# Keep a record. We know longer trust these dependencies, but simply
|
330
|
+
# unlocking them does not guarantee their removal from the graph.
|
331
|
+
# Instead, we keep a record of the dependency to unlock it later (in
|
332
|
+
# case it is actually removable because it's parent requirer is also
|
333
|
+
# being removed in this reduction). It's a form of science. Don't
|
334
|
+
# question it too much.
|
335
|
+
to_ungraph[dependency.name] = true
|
336
|
+
to_ignore[dependency.name] = true
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
# Remove any transitive dependencies
|
341
|
+
berksfile.dependencies.each do |dependency|
|
342
|
+
graphed = graph.find(dependency)
|
343
|
+
next if graphed.nil?
|
344
|
+
|
345
|
+
unless dependency.version_constraint.satisfies?(graphed.version)
|
346
|
+
raise OutdatedDependency.new(graphed, dependency)
|
347
|
+
end
|
348
|
+
|
349
|
+
if cookbook = dependency.cached_cookbook
|
350
|
+
graphed.dependencies.each do |name, constraint|
|
351
|
+
# Unless the cookbook still depends on this key, we want to queue it
|
352
|
+
# for unlocking. This is the magic that prevents transitive
|
353
|
+
# dependency leaking.
|
354
|
+
unless cookbook.dependencies.has_key?(name)
|
355
|
+
to_ungraph[name] = true
|
356
|
+
|
357
|
+
# We also want to ignore the top-level dependency. We can no
|
358
|
+
# longer trust the graph that we have been given for that
|
359
|
+
# dependency and therefore need to reduce it.
|
360
|
+
to_ignore[dependency.name] = true
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
# Now remove all the unlockable items
|
367
|
+
ignore = to_ungraph.merge(to_ignore).keys
|
368
|
+
|
369
|
+
to_ungraph.each do |name, _|
|
370
|
+
graph.remove(name, ignore: ignore)
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
|
238
375
|
# Write the contents of the current statue of the lockfile to disk. This
|
239
376
|
# method uses an atomic file write. A temporary file is created, written,
|
240
377
|
# and then copied over the existing one. This ensures any partial updates
|
@@ -248,14 +385,7 @@ module Berkshelf
|
|
248
385
|
|
249
386
|
tempfile = Tempfile.new(['Berksfile', '.lock'])
|
250
387
|
|
251
|
-
tempfile.write(
|
252
|
-
tempfile.write("\n")
|
253
|
-
dependencies.sort.each do |dependency|
|
254
|
-
tempfile.write(dependency.to_lock)
|
255
|
-
end
|
256
|
-
|
257
|
-
tempfile.write("\n")
|
258
|
-
tempfile.write(graph.to_lock)
|
388
|
+
tempfile.write(to_lock)
|
259
389
|
|
260
390
|
tempfile.rewind
|
261
391
|
tempfile.close
|
@@ -268,6 +398,17 @@ module Berkshelf
|
|
268
398
|
tempfile.unlink if tempfile
|
269
399
|
end
|
270
400
|
|
401
|
+
# @private
|
402
|
+
def to_lock
|
403
|
+
out = "#{DEPENDENCIES}\n"
|
404
|
+
dependencies.sort.each do |dependency|
|
405
|
+
out << dependency.to_lock
|
406
|
+
end
|
407
|
+
out << "\n"
|
408
|
+
out << graph.to_lock
|
409
|
+
out
|
410
|
+
end
|
411
|
+
|
271
412
|
# @private
|
272
413
|
def to_s
|
273
414
|
"#<Berkshelf::Lockfile #{Pathname.new(filepath).basename}>"
|
@@ -280,313 +421,334 @@ module Berkshelf
|
|
280
421
|
|
281
422
|
private
|
282
423
|
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
424
|
+
# The class responsible for parsing the lockfile and turning it into a
|
425
|
+
# useful data structure.
|
426
|
+
class LockfileParser
|
427
|
+
NAME_VERSION = '(?! )(.*?)(?: \(([^-]*)(?:-(.*))?\))?'.freeze
|
428
|
+
DEPENDENCY_PATTERN = /^ {2}#{NAME_VERSION}$/.freeze
|
429
|
+
DEPENDENCIES_PATTERN = /^ {4}#{NAME_VERSION}$/.freeze
|
430
|
+
OPTION_PATTERN = /^ {4}(.+)\: (.+)/.freeze
|
290
431
|
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
# Parse the lockfile contents, adding the correct things to the lockfile.
|
300
|
-
#
|
301
|
-
# @return [true]
|
302
|
-
def run
|
303
|
-
@parsed_dependencies = {}
|
432
|
+
# Create a new lockfile parser.
|
433
|
+
#
|
434
|
+
# @param [Lockfile]
|
435
|
+
def initialize(lockfile)
|
436
|
+
@lockfile = lockfile
|
437
|
+
@berksfile = lockfile.berksfile
|
438
|
+
end
|
304
439
|
|
305
|
-
contents
|
440
|
+
# Parse the lockfile contents, adding the correct things to the lockfile.
|
441
|
+
#
|
442
|
+
# @return [true]
|
443
|
+
def run
|
444
|
+
@parsed_dependencies = {}
|
306
445
|
|
307
|
-
|
308
|
-
Berkshelf.formatter.warn "Your lockfile at '#{@lockfile.filepath}' " \
|
309
|
-
"is empty. I am going to parse it anyway, but there is a chance " \
|
310
|
-
"that a larger problem is at play. If you manually edited your " \
|
311
|
-
"lockfile, you may have corrupted it."
|
312
|
-
end
|
446
|
+
contents = File.read(@lockfile.filepath)
|
313
447
|
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
graph = "#{Lockfile::GRAPH}\n"
|
320
|
-
|
321
|
-
begin
|
322
|
-
hash = JSON.parse(contents)
|
323
|
-
rescue JSON::ParserError
|
324
|
-
Berkshelf.formatter.warn "Could not convert lockfile! This is a " \
|
325
|
-
"problem. You see, previous versions of the lockfile were " \
|
326
|
-
"actually a lie. It lied to you about your version locks, and we " \
|
327
|
-
"are really sorry about that.\n\n" \
|
328
|
-
"Here's the good news - we fixed it!\n\n" \
|
329
|
-
"Here's the bad news - you probably should not trust your old " \
|
330
|
-
"lockfile. You should manually delete your old lockfile and " \
|
331
|
-
"re-run the installer."
|
448
|
+
if contents.strip.empty?
|
449
|
+
Berkshelf.formatter.warn "Your lockfile at '#{@lockfile.filepath}' " \
|
450
|
+
"is empty. I am going to parse it anyway, but there is a chance " \
|
451
|
+
"that a larger problem is at play. If you manually edited your " \
|
452
|
+
"lockfile, you may have corrupted it."
|
332
453
|
end
|
333
454
|
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
455
|
+
if contents.strip[0] == '{'
|
456
|
+
Berkshelf.formatter.warn "It looks like you are using an older " \
|
457
|
+
"version of the lockfile. Attempting to convert..."
|
458
|
+
|
459
|
+
dependencies = "#{Lockfile::DEPENDENCIES}\n"
|
460
|
+
graph = "#{Lockfile::GRAPH}\n"
|
461
|
+
|
462
|
+
begin
|
463
|
+
hash = JSON.parse(contents)
|
464
|
+
rescue JSON::ParserError
|
465
|
+
Berkshelf.formatter.warn "Could not convert lockfile! This is a " \
|
466
|
+
"problem. You see, previous versions of the lockfile were " \
|
467
|
+
"actually a lie. It lied to you about your version locks, and we " \
|
468
|
+
"are really sorry about that.\n\n" \
|
469
|
+
"Here's the good news - we fixed it!\n\n" \
|
470
|
+
"Here's the bad news - you probably should not trust your old " \
|
471
|
+
"lockfile. You should manually delete your old lockfile and " \
|
472
|
+
"re-run the installer."
|
473
|
+
end
|
474
|
+
|
475
|
+
hash['dependencies'] && hash['dependencies'].sort .each do |name, info|
|
476
|
+
dependencies << " #{name} (>= 0.0.0)\n"
|
477
|
+
info.each do |key, value|
|
478
|
+
unless key == 'locked_version'
|
479
|
+
dependencies << " #{key}: #{value}\n"
|
480
|
+
end
|
339
481
|
end
|
482
|
+
|
483
|
+
graph << " #{name} (#{info['locked_version']})\n"
|
340
484
|
end
|
341
485
|
|
342
|
-
|
486
|
+
contents = "#{dependencies}\n#{graph}"
|
343
487
|
end
|
344
488
|
|
345
|
-
contents
|
346
|
-
|
489
|
+
contents.split(/(?:\r?\n)+/).each do |line|
|
490
|
+
if line == Lockfile::DEPENDENCIES
|
491
|
+
@state = :dependency
|
492
|
+
elsif line == Lockfile::GRAPH
|
493
|
+
@state = :graph
|
494
|
+
else
|
495
|
+
send("parse_#{@state}", line)
|
496
|
+
end
|
497
|
+
end
|
347
498
|
|
348
|
-
|
349
|
-
|
350
|
-
@
|
351
|
-
elsif line == Lockfile::GRAPH
|
352
|
-
@state = :graph
|
353
|
-
else
|
354
|
-
send("parse_#{@state}", line)
|
499
|
+
@parsed_dependencies.each do |name, options|
|
500
|
+
dependency = Dependency.new(@berksfile, name, options)
|
501
|
+
@lockfile.add(dependency)
|
355
502
|
end
|
356
|
-
end
|
357
503
|
|
358
|
-
|
359
|
-
dependency = Dependency.new(@berksfile, name, options)
|
360
|
-
@lockfile.add(dependency)
|
504
|
+
true
|
361
505
|
end
|
362
506
|
|
363
|
-
|
364
|
-
|
507
|
+
private
|
508
|
+
|
509
|
+
# Parse a dependency line.
|
510
|
+
#
|
511
|
+
# @param [String] line
|
512
|
+
def parse_dependency(line)
|
513
|
+
if line =~ DEPENDENCY_PATTERN
|
514
|
+
name, version = $1, $2
|
515
|
+
|
516
|
+
@parsed_dependencies[name] ||= {}
|
517
|
+
@parsed_dependencies[name][:constraint] = version if version
|
518
|
+
@current_dependency = @parsed_dependencies[name]
|
519
|
+
elsif line =~ OPTION_PATTERN
|
520
|
+
key, value = $1, $2
|
521
|
+
@current_dependency[key.to_sym] = value
|
522
|
+
end
|
523
|
+
end
|
365
524
|
|
366
|
-
|
525
|
+
# Parse a graph line.
|
526
|
+
#
|
527
|
+
# @param [String] line
|
528
|
+
def parse_graph(line)
|
529
|
+
if line =~ DEPENDENCY_PATTERN
|
530
|
+
name, version = $1, $2
|
531
|
+
|
532
|
+
@lockfile.graph.find(name) || @lockfile.graph.add(name, version)
|
533
|
+
@current_lock = name
|
534
|
+
elsif line =~ DEPENDENCIES_PATTERN
|
535
|
+
name, constraint = $1, $2
|
536
|
+
@lockfile.graph.find(@current_lock).add_dependency(name, constraint)
|
537
|
+
end
|
538
|
+
end
|
539
|
+
end
|
367
540
|
|
368
|
-
#
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
@
|
541
|
+
# The class representing an internal graph.
|
542
|
+
class Graph
|
543
|
+
# Create a new Lockfile graph.
|
544
|
+
#
|
545
|
+
# Some clarifying terminology:
|
546
|
+
#
|
547
|
+
# yum-epel (0.2.0) <- lock
|
548
|
+
# yum (~> 3.0) <- dependency
|
549
|
+
#
|
550
|
+
# @return [Graph]
|
551
|
+
def initialize(lockfile)
|
552
|
+
@lockfile = lockfile
|
553
|
+
@berksfile = lockfile.berksfile
|
554
|
+
@graph = {}
|
381
555
|
end
|
382
|
-
end
|
383
556
|
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
@
|
393
|
-
|
394
|
-
|
395
|
-
|
557
|
+
# The list of locks for this graph. Dependencies are retrieved from the
|
558
|
+
# lockfile, then the Berksfile, and finally a new dependency object is
|
559
|
+
# created if none of those exist.
|
560
|
+
#
|
561
|
+
# @return [Hash<String, Dependency>]
|
562
|
+
# a key-value hash where the key is the name of the cookbook and the
|
563
|
+
# value is the locked dependency
|
564
|
+
def locks
|
565
|
+
@graph.sort.inject({}) do |hash, (name, item)|
|
566
|
+
dependency = @lockfile.find(name) ||
|
567
|
+
@berksfile && @berksfile.find(name) ||
|
568
|
+
Dependency.new(@berksfile, name)
|
569
|
+
dependency.locked_version = item.version
|
570
|
+
|
571
|
+
hash[item.name] = dependency
|
572
|
+
hash
|
573
|
+
end
|
396
574
|
end
|
397
|
-
end
|
398
|
-
end
|
399
575
|
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
def initialize(lockfile)
|
411
|
-
@lockfile = lockfile
|
412
|
-
@berksfile = lockfile.berksfile
|
413
|
-
@graph = {}
|
414
|
-
end
|
576
|
+
# Find a given dependency in the graph.
|
577
|
+
#
|
578
|
+
# @param [Dependency, String]
|
579
|
+
# the name/dependency to find
|
580
|
+
#
|
581
|
+
# @return [GraphItem, nil]
|
582
|
+
# the item for the name
|
583
|
+
def find(dependency)
|
584
|
+
@graph[Dependency.name(dependency)]
|
585
|
+
end
|
415
586
|
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
@graph.sort.inject({}) do |hash, (name, item)|
|
425
|
-
dependency = @lockfile.find(name) ||
|
426
|
-
@berksfile && @berksfile.find(name) ||
|
427
|
-
Dependency.new(@berksfile, name)
|
428
|
-
dependency.locked_version = item.version
|
429
|
-
|
430
|
-
hash[item.name] = dependency
|
431
|
-
hash
|
587
|
+
# Find if the given lock exists?
|
588
|
+
#
|
589
|
+
# @param [Dependency, String]
|
590
|
+
# the name/dependency to find
|
591
|
+
#
|
592
|
+
# @return [true, false]
|
593
|
+
def lock?(dependency)
|
594
|
+
!find(dependency).nil?
|
432
595
|
end
|
433
|
-
|
596
|
+
alias_method :has_lock?, :lock?
|
434
597
|
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
598
|
+
# Determine if this graph contains the given dependency. This method is
|
599
|
+
# used by the lockfile when adding or removing dependencies to see if a
|
600
|
+
# dependency can be safely removed.
|
601
|
+
#
|
602
|
+
# @param [Dependency, String] dependency
|
603
|
+
# the name/dependency to find
|
604
|
+
#
|
605
|
+
# @option options [String, Array<String>] :ignore
|
606
|
+
# the list of dependencies to ignore
|
607
|
+
def dependency?(dependency, options = {})
|
608
|
+
name = Dependency.name(dependency)
|
609
|
+
ignore = Hash[*Array(options[:ignore]).map { |i| [i, true] }.flatten]
|
445
610
|
|
446
|
-
|
447
|
-
|
448
|
-
# @param [Dependency, String]
|
449
|
-
# the name/dependency to find
|
450
|
-
#
|
451
|
-
# @return [true, false]
|
452
|
-
def lock?(dependency)
|
453
|
-
!find(dependency).nil?
|
454
|
-
end
|
455
|
-
alias_method :has_lock?, :lock?
|
611
|
+
@graph.values.each do |item|
|
612
|
+
next if ignore[item.name]
|
456
613
|
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
def dependency?(dependency)
|
464
|
-
@graph.values.any? do |item|
|
465
|
-
item.dependencies.key?(Dependency.name(dependency))
|
614
|
+
if item.dependencies.key?(name)
|
615
|
+
return true
|
616
|
+
end
|
617
|
+
end
|
618
|
+
|
619
|
+
false
|
466
620
|
end
|
467
|
-
|
468
|
-
alias_method :has_dependency?, :dependency?
|
621
|
+
alias_method :has_dependency?, :dependency?
|
469
622
|
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
623
|
+
# Add each a new {GraphItem} to the graph.
|
624
|
+
#
|
625
|
+
# @param [#to_s] name
|
626
|
+
# the name of the cookbook
|
627
|
+
# @param [#to_s] version
|
628
|
+
# the version of the lock
|
629
|
+
#
|
630
|
+
# @return [GraphItem]
|
631
|
+
def add(name, version)
|
632
|
+
@graph[name.to_s] = GraphItem.new(name, version)
|
633
|
+
end
|
481
634
|
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
635
|
+
# Recursively remove any dependencies from the graph unless they exist as
|
636
|
+
# top-level dependencies or nested dependencies.
|
637
|
+
#
|
638
|
+
# @param [Dependency, String] dependency
|
639
|
+
# the name/dependency to remove
|
640
|
+
#
|
641
|
+
# @option options [String, Array<String>] :ignore
|
642
|
+
# the list of dependencies to ignore
|
643
|
+
def remove(dependency, options = {})
|
644
|
+
name = Dependency.name(dependency)
|
489
645
|
|
490
|
-
|
646
|
+
if @lockfile.dependency?(name)
|
647
|
+
return
|
648
|
+
end
|
491
649
|
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
nested_dependencies = locked && locked.dependencies.keys || []
|
650
|
+
if dependency?(name, options)
|
651
|
+
return
|
652
|
+
end
|
496
653
|
|
497
|
-
|
498
|
-
|
654
|
+
# Grab the nested dependencies for this particular entry so we can
|
655
|
+
# recurse and try to remove them from the graph.
|
656
|
+
locked = @graph[name]
|
657
|
+
nested_dependencies = locked && locked.dependencies.keys || []
|
499
658
|
|
500
|
-
|
501
|
-
|
502
|
-
end
|
659
|
+
# Now delete the entry
|
660
|
+
@graph.delete(name)
|
503
661
|
|
504
|
-
|
505
|
-
|
506
|
-
#
|
507
|
-
# @param [Array<CachedCookbook>]
|
508
|
-
# the list of cookbooks to populate the graph with
|
509
|
-
def update(cookbooks)
|
510
|
-
@graph = {}
|
511
|
-
|
512
|
-
cookbooks.each do |cookbook|
|
513
|
-
@graph[cookbook.cookbook_name.to_s] = GraphItem.new(
|
514
|
-
cookbook.name,
|
515
|
-
cookbook.version,
|
516
|
-
cookbook.dependencies,
|
517
|
-
)
|
662
|
+
# Recursively try to delete the remaining dependencies for this item
|
663
|
+
nested_dependencies.each(&method(:remove))
|
518
664
|
end
|
519
|
-
end
|
520
665
|
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
out = "#{Lockfile::GRAPH}\n"
|
536
|
-
@graph.sort.each do |name, item|
|
537
|
-
out << " #{name} (#{item.version})\n"
|
538
|
-
|
539
|
-
unless item.dependencies.empty?
|
540
|
-
item.dependencies.sort.each do |name, constraint|
|
541
|
-
out << " #{name} (#{constraint})\n"
|
542
|
-
end
|
666
|
+
# Update the graph with the given cookbooks. This method destroys the
|
667
|
+
# existing dependency graph with this new result!
|
668
|
+
#
|
669
|
+
# @param [Array<CachedCookbook>]
|
670
|
+
# the list of cookbooks to populate the graph with
|
671
|
+
def update(cookbooks)
|
672
|
+
@graph = {}
|
673
|
+
|
674
|
+
cookbooks.each do |cookbook|
|
675
|
+
@graph[cookbook.cookbook_name.to_s] = GraphItem.new(
|
676
|
+
cookbook.name,
|
677
|
+
cookbook.version,
|
678
|
+
cookbook.dependencies,
|
679
|
+
)
|
543
680
|
end
|
544
681
|
end
|
545
682
|
|
546
|
-
|
547
|
-
end
|
548
|
-
|
549
|
-
private
|
550
|
-
|
551
|
-
# A single item inside the graph.
|
552
|
-
class GraphItem
|
553
|
-
# The name of the cookbook that corresponds to this graph item.
|
683
|
+
# Write the contents of the graph to the lockfile format.
|
554
684
|
#
|
555
|
-
#
|
556
|
-
#
|
557
|
-
|
558
|
-
|
559
|
-
#
|
685
|
+
# The resulting format looks like:
|
686
|
+
#
|
687
|
+
# GRAPH
|
688
|
+
# apache2 (1.8.14)
|
689
|
+
# yum-epel (0.2.0)
|
690
|
+
# yum (~> 3.0)
|
691
|
+
#
|
692
|
+
# @example lockfile.graph.to_lock #=> "GRAPH\n apache2 (1.18.14)\n..."
|
560
693
|
#
|
561
694
|
# @return [String]
|
562
|
-
# the locked version of the graph item (as a string)
|
563
|
-
attr_reader :version
|
564
|
-
|
565
|
-
# The list of dependencies and their constraints.
|
566
695
|
#
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
end
|
696
|
+
def to_lock
|
697
|
+
out = "#{Lockfile::GRAPH}\n"
|
698
|
+
@graph.sort.each do |name, item|
|
699
|
+
out << " #{name} (#{item.version})\n"
|
700
|
+
|
701
|
+
unless item.dependencies.empty?
|
702
|
+
item.dependencies.sort.each do |name, constraint|
|
703
|
+
out << " #{name} (#{constraint})\n"
|
704
|
+
end
|
705
|
+
end
|
706
|
+
end
|
579
707
|
|
580
|
-
|
581
|
-
#
|
582
|
-
# @param [#to_s] name
|
583
|
-
# the name to use
|
584
|
-
# @param [#to_s] constraint
|
585
|
-
# the version constraint to use
|
586
|
-
def add_dependency(name, constraint)
|
587
|
-
@dependencies[name.to_s] = constraint.to_s
|
708
|
+
out
|
588
709
|
end
|
710
|
+
|
711
|
+
private
|
712
|
+
|
713
|
+
# A single item inside the graph.
|
714
|
+
class GraphItem
|
715
|
+
# The name of the cookbook that corresponds to this graph item.
|
716
|
+
#
|
717
|
+
# @return [String]
|
718
|
+
# the name of the cookbook
|
719
|
+
attr_reader :name
|
720
|
+
|
721
|
+
# The locked version for this graph item.
|
722
|
+
#
|
723
|
+
# @return [String]
|
724
|
+
# the locked version of the graph item (as a string)
|
725
|
+
attr_reader :version
|
726
|
+
|
727
|
+
# The list of dependencies and their constraints.
|
728
|
+
#
|
729
|
+
# @return [Hash<String, String>]
|
730
|
+
# the list of dependencies for this graph item, where the key
|
731
|
+
# corresponds to the name of the dependency and the value is the
|
732
|
+
# version constraint.
|
733
|
+
attr_reader :dependencies
|
734
|
+
|
735
|
+
# Create a new graph item.
|
736
|
+
def initialize(name, version, dependencies = {})
|
737
|
+
@name = name.to_s
|
738
|
+
@version = version.to_s
|
739
|
+
@dependencies = dependencies
|
740
|
+
end
|
741
|
+
|
742
|
+
# Add a new dependency to the list.
|
743
|
+
#
|
744
|
+
# @param [#to_s] name
|
745
|
+
# the name to use
|
746
|
+
# @param [#to_s] constraint
|
747
|
+
# the version constraint to use
|
748
|
+
def add_dependency(name, constraint)
|
749
|
+
@dependencies[name.to_s] = constraint.to_s
|
750
|
+
end
|
751
|
+
end
|
589
752
|
end
|
590
|
-
end
|
591
753
|
end
|
592
754
|
end
|