lib/berkshelf/lockfile.rb in berkshelf-3.0.0.beta9 vs lib/berkshelf/lockfile.rb in berkshelf-3.0.0.rc1

- old
+ new

@@ -96,12 +96,10 @@ checked = {} berksfile.dependencies.each do |dependency| Berkshelf.log.debug "Checking #{dependency}" - checked[dependency.name] = true - locked = find(dependency) if locked.nil? Berkshelf.log.debug " Not in lockfile - cannot be trusted!" return false end @@ -150,21 +148,40 @@ # the graph item to check transitive dependencies for # @param [Hash] checked # the list of already checked dependencies # # @return [Boolean] - def satisfies_transitive?(graph_item, checked) - graph_item.dependencies.all? do |name, constraint| - return true if checked[name] + def satisfies_transitive?(graph_item, checked, level = 0) + indent = ' '*(level + 2) - checked[name] = true + Berkshelf.log.debug "#{indent}Checking transitive dependencies for #{graph_item}" + if checked[graph_item.name] + Berkshelf.log.debug "#{indent} Already checked - skipping" + return true + end + + graph_item.dependencies.each do |name, constraint| + Berkshelf.log.debug "#{indent} Checking #{name} (#{constraint})" + graphed = graph.find(name) - return false if graphed.nil? + if graphed.nil? + Berkshelf.log.debug "#{indent} Not graphed - cannot be satisifed" + return false + end - Semverse::Constraint.new(constraint).satisfies?(graphed.version) && - satisfies_transitive?(graphed, checked) + unless Semverse::Constraint.new(constraint).satisfies?(graphed.version) + Berkshelf.log.debug "#{indent} Version constraint is not satisfied" + return false + end + + unless satisfies_transitive?(graphed, checked, level + 2) + Berkshelf.log.debug "#{indent} Transitive are not satisifed" + return false + end + + checked[name] = true end end # Resolve this Berksfile and apply the locks found in the generated # +Berksfile.lock+ to the target Chef environment @@ -286,20 +303,28 @@ # # This method first removes the dependency from the list of top-level # dependencies. Then it uses a recursive algorithm to safely remove any # other dependencies from the graph that are no longer needed. # - # @raise [CookbookNotFound] - # if the provided dependency does not exist - # # @param [String] dependency # the name of the cookbook to remove - def unlock(dependency) + def unlock(dependency, force = false) @dependencies.delete(Dependency.name(dependency)) - graph.remove(dependency) + + if force + graph.remove(dependency, ignore: graph.locks.keys) + else + graph.remove(dependency) + end end + # Completely remove all dependencies from the lockfile and underlying graph. + def unlock_all + @dependencies = {} + @graph = Graph.new(self) + end + # Iterate over each top-level dependency defined in the lockfile and # check if that dependency is still defined in the Berksfile. # # If the dependency is no longer present in the Berksfile, it is "safely" # removed using {Lockfile#unlock} and {Lockfile#remove}. This prevents @@ -315,62 +340,76 @@ # if the constraint exists, but is no longer satisifed by the existing # locked version # # @return [Array<Dependency>] def reduce! - # Store a list of cookbooks to ungraph - to_ungraph = {} - to_ignore = {} + Berkshelf.log.info "Reducing lockfile" + Berkshelf.log.debug "Current lockfile:" + Berkshelf.log.debug "" + to_lock.each_line do |line| + Berkshelf.log.debug " #{line.chomp}" + end + Berkshelf.log.debug "" + + # Unlock any locked dependencies that are no longer in the Berksfile + Berkshelf.log.debug "Unlocking dependencies no longer in the Berksfile" + dependencies.each do |dependency| - unless berksfile.has_dependency?(dependency.name) - unlock(dependency) + Berkshelf.log.debug " Checking #{dependency}" - # Keep a record. We know longer trust these dependencies, but simply - # unlocking them does not guarantee their removal from the graph. - # Instead, we keep a record of the dependency to unlock it later (in - # case it is actually removable because it's parent requirer is also - # being removed in this reduction). It's a form of science. Don't - # question it too much. - to_ungraph[dependency.name] = true - to_ignore[dependency.name] = true + if berksfile.has_dependency?(dependency.name) + Berkshelf.log.debug " Skipping unlock for #{dependency.name} (exists in the Berksfile)" + else + Berkshelf.log.debug " Unlocking #{dependency.name}" + unlock(dependency, true) end end # Remove any transitive dependencies + Berkshelf.log.debug "Removing transitive dependencies" + berksfile.dependencies.each do |dependency| + Berkshelf.log.debug " Checking #{dependency}" + graphed = graph.find(dependency) - next if graphed.nil? + if graphed.nil? + Berkshelf.log.debug " Skipping (not graphed)" + next + end + unless dependency.version_constraint.satisfies?(graphed.version) + Berkshelf.log.debug " Constraints are not satisfied!" raise OutdatedDependency.new(graphed, dependency) end if cookbook = dependency.cached_cookbook + Berkshelf.log.debug " Cached cookbook exists" + Berkshelf.log.debug " Checking dependencies on the cached cookbook" + graphed.dependencies.each do |name, constraint| + Berkshelf.log.debug " Checking #{name} (#{constraint})" + # Unless the cookbook still depends on this key, we want to queue it # for unlocking. This is the magic that prevents transitive # dependency leaking. unless cookbook.dependencies.has_key?(name) - to_ungraph[name] = true - - # We also want to ignore the top-level dependency. We can no - # longer trust the graph that we have been given for that - # dependency and therefore need to reduce it. - to_ignore[dependency.name] = true + Berkshelf.log.debug " Not found!" + unlock(name, true) end end end end - # Now remove all the unlockable items - ignore = to_ungraph.merge(to_ignore).keys - - to_ungraph.each do |name, _| - graph.remove(name, ignore: ignore) + Berkshelf.log.debug "New lockfile:" + Berkshelf.log.debug "" + to_lock.each_line do |line| + Berkshelf.log.debug " #{line.chomp}" end + Berkshelf.log.debug "" end # Write the contents of the current statue of the lockfile to disk. This # method uses an atomic file write. A temporary file is created, written, @@ -564,10 +603,14 @@ def locks @graph.sort.inject({}) do |hash, (name, item)| dependency = @lockfile.find(name) || @berksfile && @berksfile.find(name) || Dependency.new(@berksfile, name) + + # We need to make a copy of the dependency, or else we could be + # modifying an existing object that other processes depend on! + dependency = dependency.dup dependency.locked_version = item.version hash[item.name] = dependency hash end @@ -745,9 +788,14 @@ # the name to use # @param [#to_s] constraint # the version constraint to use def add_dependency(name, constraint) @dependencies[name.to_s] = constraint.to_s + end + + # @private + def to_s + "#{name} (#{version})" end end end end end