lib/chef-dk/policyfile_compiler.rb in chef-dk-0.11.2 vs lib/chef-dk/policyfile_compiler.rb in chef-dk-0.12.0

- old
+ new

@@ -1,419 +1,419 @@ -# -# Copyright:: Copyright (c) 2014 Chef Software Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require 'set' -require 'forwardable' - -require 'solve' -require 'chef/run_list' - -require 'chef-dk/policyfile/dsl' -require 'chef-dk/policyfile_lock' -require 'chef-dk/ui' -require 'chef-dk/policyfile/reports/install' -require 'chef-dk/exceptions' - -module ChefDK - - class PolicyfileCompiler - - extend Forwardable - - DEFAULT_DEMAND_CONSTRAINT = '>= 0.0.0'.freeze - - # Cookbooks from these sources lock that cookbook to exactly one version - SOURCE_TYPES_WITH_FIXED_VERSIONS = [:git, :path].freeze - - def self.evaluate(policyfile_string, policyfile_filename, ui: nil) - compiler = new(ui: ui) - compiler.evaluate_policyfile(policyfile_string, policyfile_filename) - compiler - end - - def_delegator :@dsl, :name - def_delegator :@dsl, :run_list - def_delegator :@dsl, :named_run_list - def_delegator :@dsl, :named_run_lists - def_delegator :@dsl, :errors - def_delegator :@dsl, :default_source - def_delegator :@dsl, :cookbook_location_specs - - attr_reader :dsl - attr_reader :storage_config - attr_reader :install_report - - def initialize(ui: nil) - @storage_config = Policyfile::StorageConfig.new - @dsl = Policyfile::DSL.new(storage_config) - @artifact_server_cookbook_location_specs = {} - - @merged_graph = nil - - @ui = ui || UI.null - @install_report = Policyfile::Reports::Install.new(ui: @ui, policyfile_compiler: self) - end - - def error! - unless errors.empty? - raise PolicyfileError, errors.join("\n") - end - end - - def cookbook_location_spec_for(cookbook_name) - cookbook_location_specs[cookbook_name] - end - - def expanded_run_list - # doesn't support roles yet... - Chef::RunList.new(*run_list) - end - - # copy of the expanded_run_list, properly formatted for use in a lockfile - def normalized_run_list - expanded_run_list.map { |i| normalize_recipe(i) } - end - - def expanded_named_run_lists - named_run_lists.inject({}) do |expanded, (name, run_list_items)| - expanded[name] = Chef::RunList.new(*run_list_items) - expanded - end - end - - def normalized_named_run_lists - expanded_named_run_lists.inject({}) do |normalized,(name, run_list)| - normalized[name] = run_list.map { |i| normalize_recipe(i) } - normalized - end - end - - def default_attributes - dsl.node_attributes.combined_default.to_hash - end - - def override_attributes - dsl.node_attributes.combined_override.to_hash - end - - def lock - @policyfile_lock ||= PolicyfileLock.build_from_compiler(self, storage_config) - end - - def install - ensure_cache_dir_exists - - cookbook_and_recipe_list = combined_run_lists.map(&:name).map do |recipe_spec| - cookbook, _separator, recipe = recipe_spec.partition("::") - recipe = "default" if recipe.empty? - [cookbook, recipe] - end - - missing_recipes_by_cb_spec = {} - - graph_solution.each do |cookbook_name, version| - spec = cookbook_location_spec_for(cookbook_name) - if spec.nil? or !spec.version_fixed? - spec = create_spec_for_cookbook(cookbook_name, version) - install_report.installing_cookbook(spec) - spec.ensure_cached - end - - required_recipes = cookbook_and_recipe_list.select { |cb_name, _recipe| cb_name == spec.name } - missing_recipes = required_recipes.select {|_cb_name, recipe| !spec.cookbook_has_recipe?(recipe) } - - unless missing_recipes.empty? - missing_recipes_by_cb_spec[spec] = missing_recipes - end - end - - unless missing_recipes_by_cb_spec.empty? - message = "The installed cookbooks do not contain all the recipes required by your run list(s):\n" - missing_recipes_by_cb_spec.each do |spec, missing_items| - message << "#{spec.to_s}\nis missing the following required recipes:\n" - missing_items.each { |_cb, recipe| message << "* #{recipe}\n" } - end - - message << "\n" - message << "You may have specified an incorrect recipe in your run list,\nor this recipe may not be available in that version of the cookbook\n" - - raise CookbookDoesNotContainRequiredRecipe, message - end - - - end - - def create_spec_for_cookbook(cookbook_name, version) - matching_source = best_source_for(cookbook_name) - source_options = matching_source.source_options_for(cookbook_name, version) - spec = Policyfile::CookbookLocationSpecification.new(cookbook_name, "= #{version}", source_options, storage_config) - @artifact_server_cookbook_location_specs[cookbook_name] = spec - end - - def all_cookbook_location_specs - # in the installation process, we create "artifact_server_cookbook_location_specs" - # for any cookbook that isn't sourced from a single-version source (e.g., - # path and git only support one version at a time), but we might have - # specs for them to track additional version constraint demands. Merging - # in this order ensures the artifact_server_cookbook_location_specs "win". - cookbook_location_specs.merge(@artifact_server_cookbook_location_specs) - end - - ## - # Compilation Methods - ## - - def graph_solution - return @solution if @solution - cache_fixed_version_cookbooks - @solution = Solve.it!(graph, graph_demands) - end - - def graph - @graph ||= Solve::Graph.new.tap do |g| - artifacts_graph.each do |name, dependencies_by_version| - dependencies_by_version.each do |version, dependencies| - artifact = g.artifact(name, version) - dependencies.each do |dep_name, constraint| - artifact.dependency(dep_name, constraint) - end - end - end - end - end - - def solution_dependencies - solution_deps = Policyfile::SolutionDependencies.new - - all_cookbook_location_specs.each do |name, spec| - solution_deps.add_policyfile_dep(name, spec.version_constraint) - end - - graph_solution.each do |name, version| - transitive_deps = artifacts_graph[name][version] - solution_deps.add_cookbook_dep(name, version, transitive_deps) - end - solution_deps - end - - def graph_demands - cookbooks_for_demands.map do |cookbook_name| - spec = cookbook_location_spec_for(cookbook_name) - if spec.nil? - [ cookbook_name, DEFAULT_DEMAND_CONSTRAINT ] - elsif spec.version_fixed? - [ cookbook_name, "= #{spec.version}" ] - else - [ cookbook_name, spec.version_constraint.to_s ] - end - end - end - - def artifacts_graph - remote_artifacts_graph.merge(local_artifacts_graph) - end - - # Gives a dependency graph for cookbooks that are source from an alternate - # location. These cookbooks could have a different set of dependencies - # compared to an unmodified copy upstream. For example, the community site - # may have a cookbook "apache2" at version "1.10.4", which the user has - # forked on github and modified the dependencies without changing the - # version number. To accomodate this, the local_artifacts_graph should be - # merged over the upstream's artifacts graph. - def local_artifacts_graph - cookbook_location_specs.inject({}) do |local_artifacts, (cookbook_name, cookbook_location_spec)| - if cookbook_location_spec.version_fixed? - local_artifacts[cookbook_name] = { cookbook_location_spec.version => cookbook_location_spec.dependencies } - end - local_artifacts - end - end - - def remote_artifacts_graph - @merged_graph ||= - begin - conflicting_cb_names = [] - merged = {} - default_source.each do |source| - merged.merge!(source.universe_graph) do |conflicting_cb_name, _old, _new| - if (preference = preferred_source_for_cookbook(conflicting_cb_name)) - preference.universe_graph[conflicting_cb_name] - elsif cookbook_could_appear_in_solution?(conflicting_cb_name) - conflicting_cb_names << conflicting_cb_name - {} # return empty set of versions - else - {} # return empty set of versions - end - end - end - handle_conflicting_cookbooks(conflicting_cb_names) - merged - end - end - - def version_constraint_for(cookbook_name) - if (cookbook_location_spec = cookbook_location_spec_for(cookbook_name)) and cookbook_location_spec.version_fixed? - version = cookbook_location_spec.version - "= #{version}" - else - DEFAULT_DEMAND_CONSTRAINT - end - end - - def cookbook_version_fixed?(cookbook_name) - if cookbook_location_spec = cookbook_location_spec_for(cookbook_name) - cookbook_location_spec.version_fixed? - else - false - end - end - - def cookbooks_in_run_list - recipes = combined_run_lists.map {|recipe| recipe.name } - recipes.map { |r| r[/^([^:]+)/, 1] } - end - - def combined_run_lists - expanded_named_run_lists.values.inject(expanded_run_list.to_a) do |accum_run_lists, run_list| - accum_run_lists |= run_list.to_a - end - end - - def combined_run_lists_by_cb_name - combined_run_lists.inject({}) do |by_name_accum, run_list_item| - by_name_accum - end - end - - - def build - yield @dsl - self - end - - def evaluate_policyfile(policyfile_string, policyfile_filename) - storage_config.use_policyfile(policyfile_filename) - @dsl.eval_policyfile(policyfile_string) - self - end - - def fixed_version_cookbooks_specs - @fixed_version_cookbooks_specs ||= cookbook_location_specs.select do |_cookbook_name, cookbook_location_spec| - cookbook_location_spec.version_fixed? - end - end - - private - - def normalize_recipe(run_list_item) - name = run_list_item.name - name = "#{name}::default" unless name.include?("::") - "recipe[#{name}]" - end - - def cookbooks_for_demands - (cookbooks_in_run_list + cookbook_location_specs.keys).uniq - end - - def cache_fixed_version_cookbooks - ensure_cache_dir_exists - - fixed_version_cookbooks_specs.each do |name, cookbook_location_spec| - install_report.installing_fixed_version_cookbook(cookbook_location_spec) - cookbook_location_spec.ensure_cached - end - end - - def ensure_cache_dir_exists - unless File.exist?(cache_path) - FileUtils.mkdir_p(cache_path) - end - end - - def cache_path - CookbookOmnifetch.storage_path - end - - def best_source_for(cookbook_name) - preferred = default_source.find { |s| s.preferred_source_for?(cookbook_name) } - if preferred.nil? - default_source.find { |s| - s.universe_graph.has_key?(cookbook_name) - } - else - preferred - end - end - - - def preferred_source_for_cookbook(conflicting_cb_name) - default_source.find { |s| s.preferred_source_for?(conflicting_cb_name) } - end - - def handle_conflicting_cookbooks(conflicting_cookbooks) - # ignore any cookbooks that have a source set. - cookbooks_wo_source = conflicting_cookbooks.select do |cookbook_name| - location_spec = cookbook_location_spec_for(cookbook_name) - location_spec.nil? || location_spec.source_options.empty? - end - - if cookbooks_wo_source.empty? - nil - else - raise CookbookSourceConflict.new(cookbooks_wo_source, default_source) - end - end - - def cookbook_could_appear_in_solution?(cookbook_name) - all_possible_dep_names.include?(cookbook_name) - end - - # Traverses the dependency graph in a simple manner to find the set of - # cookbooks that could be considered in the dependency solution. Version - # constraints are not considered so this could include extra cookbooks. - def all_possible_dep_names - @all_possible_dep_names ||= cookbooks_for_demands.inject(Set.new) do |deps_set, demand_cookbook| - - deps_set_for_source = default_source.inject(Set.new) do |deps_set_for_cb, source| - possible_deps = possible_dependencies_of(demand_cookbook, source) - deps_set_for_cb.merge(possible_deps) - end - - deps_set.merge(deps_set_for_source) - end - end - - def possible_dependencies_of(cookbook_name, source, dependency_set = Set.new) - return dependency_set if dependency_set.include?(cookbook_name) - return dependency_set unless source.universe_graph.key?(cookbook_name) - - dependency_set << cookbook_name - - deps_by_version = source.universe_graph[cookbook_name] - - dep_cookbook_names = deps_by_version.values.inject(Set.new) do |names, constraint_list| - names.merge(constraint_list.map { |c| c.first }) - end - - dep_cookbook_names.each do |dep_cookbook_name| - possible_dependencies_of(dep_cookbook_name, source, dependency_set) - end - - dependency_set - end - - end -end +# +# Copyright:: Copyright (c) 2014 Chef Software Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'set' +require 'forwardable' + +require 'solve' +require 'chef/run_list' + +require 'chef-dk/policyfile/dsl' +require 'chef-dk/policyfile_lock' +require 'chef-dk/ui' +require 'chef-dk/policyfile/reports/install' +require 'chef-dk/exceptions' + +module ChefDK + + class PolicyfileCompiler + + extend Forwardable + + DEFAULT_DEMAND_CONSTRAINT = '>= 0.0.0'.freeze + + # Cookbooks from these sources lock that cookbook to exactly one version + SOURCE_TYPES_WITH_FIXED_VERSIONS = [:git, :path].freeze + + def self.evaluate(policyfile_string, policyfile_filename, ui: nil) + compiler = new(ui: ui) + compiler.evaluate_policyfile(policyfile_string, policyfile_filename) + compiler + end + + def_delegator :@dsl, :name + def_delegator :@dsl, :run_list + def_delegator :@dsl, :named_run_list + def_delegator :@dsl, :named_run_lists + def_delegator :@dsl, :errors + def_delegator :@dsl, :default_source + def_delegator :@dsl, :cookbook_location_specs + + attr_reader :dsl + attr_reader :storage_config + attr_reader :install_report + + def initialize(ui: nil) + @storage_config = Policyfile::StorageConfig.new + @dsl = Policyfile::DSL.new(storage_config) + @artifact_server_cookbook_location_specs = {} + + @merged_graph = nil + + @ui = ui || UI.null + @install_report = Policyfile::Reports::Install.new(ui: @ui, policyfile_compiler: self) + end + + def error! + unless errors.empty? + raise PolicyfileError, errors.join("\n") + end + end + + def cookbook_location_spec_for(cookbook_name) + cookbook_location_specs[cookbook_name] + end + + def expanded_run_list + # doesn't support roles yet... + Chef::RunList.new(*run_list) + end + + # copy of the expanded_run_list, properly formatted for use in a lockfile + def normalized_run_list + expanded_run_list.map { |i| normalize_recipe(i) } + end + + def expanded_named_run_lists + named_run_lists.inject({}) do |expanded, (name, run_list_items)| + expanded[name] = Chef::RunList.new(*run_list_items) + expanded + end + end + + def normalized_named_run_lists + expanded_named_run_lists.inject({}) do |normalized,(name, run_list)| + normalized[name] = run_list.map { |i| normalize_recipe(i) } + normalized + end + end + + def default_attributes + dsl.node_attributes.combined_default.to_hash + end + + def override_attributes + dsl.node_attributes.combined_override.to_hash + end + + def lock + @policyfile_lock ||= PolicyfileLock.build_from_compiler(self, storage_config) + end + + def install + ensure_cache_dir_exists + + cookbook_and_recipe_list = combined_run_lists.map(&:name).map do |recipe_spec| + cookbook, _separator, recipe = recipe_spec.partition("::") + recipe = "default" if recipe.empty? + [cookbook, recipe] + end + + missing_recipes_by_cb_spec = {} + + graph_solution.each do |cookbook_name, version| + spec = cookbook_location_spec_for(cookbook_name) + if spec.nil? or !spec.version_fixed? + spec = create_spec_for_cookbook(cookbook_name, version) + install_report.installing_cookbook(spec) + spec.ensure_cached + end + + required_recipes = cookbook_and_recipe_list.select { |cb_name, _recipe| cb_name == spec.name } + missing_recipes = required_recipes.select {|_cb_name, recipe| !spec.cookbook_has_recipe?(recipe) } + + unless missing_recipes.empty? + missing_recipes_by_cb_spec[spec] = missing_recipes + end + end + + unless missing_recipes_by_cb_spec.empty? + message = "The installed cookbooks do not contain all the recipes required by your run list(s):\n" + missing_recipes_by_cb_spec.each do |spec, missing_items| + message << "#{spec.to_s}\nis missing the following required recipes:\n" + missing_items.each { |_cb, recipe| message << "* #{recipe}\n" } + end + + message << "\n" + message << "You may have specified an incorrect recipe in your run list,\nor this recipe may not be available in that version of the cookbook\n" + + raise CookbookDoesNotContainRequiredRecipe, message + end + + + end + + def create_spec_for_cookbook(cookbook_name, version) + matching_source = best_source_for(cookbook_name) + source_options = matching_source.source_options_for(cookbook_name, version) + spec = Policyfile::CookbookLocationSpecification.new(cookbook_name, "= #{version}", source_options, storage_config) + @artifact_server_cookbook_location_specs[cookbook_name] = spec + end + + def all_cookbook_location_specs + # in the installation process, we create "artifact_server_cookbook_location_specs" + # for any cookbook that isn't sourced from a single-version source (e.g., + # path and git only support one version at a time), but we might have + # specs for them to track additional version constraint demands. Merging + # in this order ensures the artifact_server_cookbook_location_specs "win". + cookbook_location_specs.merge(@artifact_server_cookbook_location_specs) + end + + ## + # Compilation Methods + ## + + def graph_solution + return @solution if @solution + cache_fixed_version_cookbooks + @solution = Solve.it!(graph, graph_demands) + end + + def graph + @graph ||= Solve::Graph.new.tap do |g| + artifacts_graph.each do |name, dependencies_by_version| + dependencies_by_version.each do |version, dependencies| + artifact = g.artifact(name, version) + dependencies.each do |dep_name, constraint| + artifact.dependency(dep_name, constraint) + end + end + end + end + end + + def solution_dependencies + solution_deps = Policyfile::SolutionDependencies.new + + all_cookbook_location_specs.each do |name, spec| + solution_deps.add_policyfile_dep(name, spec.version_constraint) + end + + graph_solution.each do |name, version| + transitive_deps = artifacts_graph[name][version] + solution_deps.add_cookbook_dep(name, version, transitive_deps) + end + solution_deps + end + + def graph_demands + cookbooks_for_demands.map do |cookbook_name| + spec = cookbook_location_spec_for(cookbook_name) + if spec.nil? + [ cookbook_name, DEFAULT_DEMAND_CONSTRAINT ] + elsif spec.version_fixed? + [ cookbook_name, "= #{spec.version}" ] + else + [ cookbook_name, spec.version_constraint.to_s ] + end + end + end + + def artifacts_graph + remote_artifacts_graph.merge(local_artifacts_graph) + end + + # Gives a dependency graph for cookbooks that are source from an alternate + # location. These cookbooks could have a different set of dependencies + # compared to an unmodified copy upstream. For example, the community site + # may have a cookbook "apache2" at version "1.10.4", which the user has + # forked on github and modified the dependencies without changing the + # version number. To accomodate this, the local_artifacts_graph should be + # merged over the upstream's artifacts graph. + def local_artifacts_graph + cookbook_location_specs.inject({}) do |local_artifacts, (cookbook_name, cookbook_location_spec)| + if cookbook_location_spec.version_fixed? + local_artifacts[cookbook_name] = { cookbook_location_spec.version => cookbook_location_spec.dependencies } + end + local_artifacts + end + end + + def remote_artifacts_graph + @merged_graph ||= + begin + conflicting_cb_names = [] + merged = {} + default_source.each do |source| + merged.merge!(source.universe_graph) do |conflicting_cb_name, _old, _new| + if (preference = preferred_source_for_cookbook(conflicting_cb_name)) + preference.universe_graph[conflicting_cb_name] + elsif cookbook_could_appear_in_solution?(conflicting_cb_name) + conflicting_cb_names << conflicting_cb_name + {} # return empty set of versions + else + {} # return empty set of versions + end + end + end + handle_conflicting_cookbooks(conflicting_cb_names) + merged + end + end + + def version_constraint_for(cookbook_name) + if (cookbook_location_spec = cookbook_location_spec_for(cookbook_name)) and cookbook_location_spec.version_fixed? + version = cookbook_location_spec.version + "= #{version}" + else + DEFAULT_DEMAND_CONSTRAINT + end + end + + def cookbook_version_fixed?(cookbook_name) + if cookbook_location_spec = cookbook_location_spec_for(cookbook_name) + cookbook_location_spec.version_fixed? + else + false + end + end + + def cookbooks_in_run_list + recipes = combined_run_lists.map {|recipe| recipe.name } + recipes.map { |r| r[/^([^:]+)/, 1] } + end + + def combined_run_lists + expanded_named_run_lists.values.inject(expanded_run_list.to_a) do |accum_run_lists, run_list| + accum_run_lists |= run_list.to_a + end + end + + def combined_run_lists_by_cb_name + combined_run_lists.inject({}) do |by_name_accum, run_list_item| + by_name_accum + end + end + + + def build + yield @dsl + self + end + + def evaluate_policyfile(policyfile_string, policyfile_filename) + storage_config.use_policyfile(policyfile_filename) + @dsl.eval_policyfile(policyfile_string) + self + end + + def fixed_version_cookbooks_specs + @fixed_version_cookbooks_specs ||= cookbook_location_specs.select do |_cookbook_name, cookbook_location_spec| + cookbook_location_spec.version_fixed? + end + end + + private + + def normalize_recipe(run_list_item) + name = run_list_item.name + name = "#{name}::default" unless name.include?("::") + "recipe[#{name}]" + end + + def cookbooks_for_demands + (cookbooks_in_run_list + cookbook_location_specs.keys).uniq + end + + def cache_fixed_version_cookbooks + ensure_cache_dir_exists + + fixed_version_cookbooks_specs.each do |name, cookbook_location_spec| + install_report.installing_fixed_version_cookbook(cookbook_location_spec) + cookbook_location_spec.ensure_cached + end + end + + def ensure_cache_dir_exists + unless File.exist?(cache_path) + FileUtils.mkdir_p(cache_path) + end + end + + def cache_path + CookbookOmnifetch.storage_path + end + + def best_source_for(cookbook_name) + preferred = default_source.find { |s| s.preferred_source_for?(cookbook_name) } + if preferred.nil? + default_source.find { |s| + s.universe_graph.has_key?(cookbook_name) + } + else + preferred + end + end + + + def preferred_source_for_cookbook(conflicting_cb_name) + default_source.find { |s| s.preferred_source_for?(conflicting_cb_name) } + end + + def handle_conflicting_cookbooks(conflicting_cookbooks) + # ignore any cookbooks that have a source set. + cookbooks_wo_source = conflicting_cookbooks.select do |cookbook_name| + location_spec = cookbook_location_spec_for(cookbook_name) + location_spec.nil? || location_spec.source_options.empty? + end + + if cookbooks_wo_source.empty? + nil + else + raise CookbookSourceConflict.new(cookbooks_wo_source, default_source) + end + end + + def cookbook_could_appear_in_solution?(cookbook_name) + all_possible_dep_names.include?(cookbook_name) + end + + # Traverses the dependency graph in a simple manner to find the set of + # cookbooks that could be considered in the dependency solution. Version + # constraints are not considered so this could include extra cookbooks. + def all_possible_dep_names + @all_possible_dep_names ||= cookbooks_for_demands.inject(Set.new) do |deps_set, demand_cookbook| + + deps_set_for_source = default_source.inject(Set.new) do |deps_set_for_cb, source| + possible_deps = possible_dependencies_of(demand_cookbook, source) + deps_set_for_cb.merge(possible_deps) + end + + deps_set.merge(deps_set_for_source) + end + end + + def possible_dependencies_of(cookbook_name, source, dependency_set = Set.new) + return dependency_set if dependency_set.include?(cookbook_name) + return dependency_set unless source.universe_graph.key?(cookbook_name) + + dependency_set << cookbook_name + + deps_by_version = source.universe_graph[cookbook_name] + + dep_cookbook_names = deps_by_version.values.inject(Set.new) do |names, constraint_list| + names.merge(constraint_list.map { |c| c.first }) + end + + dep_cookbook_names.each do |dep_cookbook_name| + possible_dependencies_of(dep_cookbook_name, source, dependency_set) + end + + dependency_set + end + + end +end