berkshelf 3.0.0.beta7 → 3.0.0.beta8
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.
- 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
|