class ChefCLI::PolicyfileCompiler

def self.evaluate(policyfile_string, policyfile_filename, ui: nil, chef_config: nil)

def self.evaluate(policyfile_string, policyfile_filename, ui: nil, chef_config: nil)
  compiler = new(ui: ui, chef_config: chef_config)
  compiler.evaluate_policyfile(policyfile_string, policyfile_filename)
  compiler
end

def all_cookbook_location_specs

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

def all_possible_dep_names

constraints are not considered so this could include extra cookbooks.
cookbooks that could be considered in the dependency solution. Version
Traverses the dependency graph in a simple manner to find the set of
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 artifacts_graph

def artifacts_graph
  remote_artifacts_graph.merge(local_artifacts_graph)
end

def best_source_for(cookbook_name)

def best_source_for(cookbook_name)
  preferred = default_source.find { |s| s.preferred_source_for?(cookbook_name) }
  if preferred.nil?
    default_source.find do |s|
      s.universe_graph.key?(cookbook_name)
    end
  else
    preferred
  end
end

def build

def build
  yield @dsl
  self
end

def cache_fixed_version_cookbooks

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 cache_path

def cache_path
  CookbookOmnifetch.storage_path
end

def check_for_default_attribute_conflicts!

def check_for_default_attribute_conflicts!
  checker = Policyfile::AttributeMergeChecker.new
  checker.with_attributes("user-specified", dsl.node_attributes.combined_default)
  included_policies.map do |policy_spec|
    lock = policy_spec.policyfile_lock
    checker.with_attributes(policy_spec.name, lock.default_attributes)
  end
  checker.check!
end

def check_for_override_attribute_conflicts!

def check_for_override_attribute_conflicts!
  checker = Policyfile::AttributeMergeChecker.new
  checker.with_attributes("user-specified", dsl.node_attributes.combined_override)
  included_policies.map do |policy_spec|
    lock = policy_spec.policyfile_lock
    checker.with_attributes(policy_spec.name, lock.override_attributes)
  end
  checker.check!
end

def combined_run_lists

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

def combined_run_lists_by_cb_name
  combined_run_lists.inject({}) do |by_name_accum, run_list_item|
    by_name_accum
  end
end

def cookbook_could_appear_in_solution?(cookbook_name)

def cookbook_could_appear_in_solution?(cookbook_name)
  all_possible_dep_names.include?(cookbook_name)
end

def cookbook_demands_from_current

def cookbook_demands_from_current
  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 cookbook_demands_from_policies

def cookbook_demands_from_policies
  included_policies.flat_map do |policy_spec|
    lock = policy_spec.policyfile_lock
    lock.solution_dependencies.to_lock["Policyfile"]
  end
end

def cookbook_location_spec_for(cookbook_name)

def cookbook_location_spec_for(cookbook_name)
  cookbook_location_specs[cookbook_name]
end

def cookbook_version_fixed?(cookbook_name)

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_for_demands

def cookbooks_for_demands
  (cookbooks_in_run_list + cookbook_location_specs.keys).uniq
end

def cookbooks_in_run_list

def cookbooks_in_run_list
  recipes = combined_run_lists.map(&:name)
  recipes.map { |r| r[/^([^:]+)/, 1] }
end

def create_spec_for_cookbook(cookbook_name, version)

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 default_attributes

def default_attributes
  check_for_default_attribute_conflicts!
  included_policies.map(&:policyfile_lock).inject(
    dsl.node_attributes.combined_default.to_hash
  ) do |acc, lock|
    Chef::Mixin::DeepMerge.merge(acc, lock.default_attributes)
  end
end

def default_source(source_type = nil, source_argument = nil, &block)

def default_source(source_type = nil, source_argument = nil, &block)
  if source_type.nil?
    prepend_array = if included_policies.length > 0
                      [included_policies_cookbook_source]
                    else
                      []
                    end
    prepend_array + dsl.default_source
  else
    dsl.default_source(source_type, source_argument, &block)
  end
end

def ensure_cache_dir_exists

def ensure_cache_dir_exists
  unless File.exist?(cache_path)
    FileUtils.mkdir_p(cache_path)
  end
end

def error!

def error!
  unless errors.empty?
    raise PolicyfileError, errors.join("\n")
  end
end

def evaluate_policyfile(policyfile_string, policyfile_filename)

def evaluate_policyfile(policyfile_string, policyfile_filename)
  storage_config.use_policyfile(policyfile_filename)
  @dsl.eval_policyfile(policyfile_string)
  self
end

def expanded_named_run_lists

def expanded_named_run_lists
  included_policies_named_runlists = included_policies.inject({}) do |acc, policy_spec|
    lock = policy_spec.policyfile_lock
    lock.named_run_lists.inject(acc) do |expanded, (name, run_list_items)|
      expanded[name] ||= Chef::RunList.new
      run_list_items.each do |run_list_item|
        expanded[name] << run_list_item
      end
      expanded
    end
    acc
  end
  named_run_lists.inject(included_policies_named_runlists) do |expanded, (name, run_list_items)|
    expanded[name] ||= Chef::RunList.new
    run_list_items.each do |run_list_item|
      expanded[name] << run_list_item
    end
    expanded
  end
end

def expanded_run_list

def expanded_run_list
  # doesn't support roles yet...
  concated_runlist = Chef::RunList.new
  included_policies.each do |policy_spec|
    lock = policy_spec.policyfile_lock
    lock.run_list.each do |run_list_item|
      concated_runlist << run_list_item
    end
  end
  run_list.each do |run_list_item|
    concated_runlist << run_list_item
  end
  concated_runlist
end

def fixed_version_cookbooks_specs

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

def graph

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 graph_demands

def graph_demands
  ## TODO: By merging cookbooks from the current policyfile and included policies,
  #        we lose the ability to know where a conflict came from
  (cookbook_demands_from_current + cookbook_demands_from_policies)
end

def graph_solution

def graph_solution
  return @solution if @solution
  cache_fixed_version_cookbooks
  @solution = Solve.it!(graph, graph_demands)
end

def handle_conflicting_cookbooks(conflicting_cookbooks)

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 handle_included_policies_preferred_cookbook_conflicts(included_policies_source)

def handle_included_policies_preferred_cookbook_conflicts(included_policies_source)
  # All cookbooks in the included policies are preferred.
  conflicting_source_messages = []
  dsl.default_source.reject(&:null?).each do |source_b|
    conflicting_preferences = included_policies_source.preferred_cookbooks & source_b.preferred_cookbooks
    next if conflicting_preferences.empty?
    next if conflicting_preferences.all? do |cookbook_name|
      version = included_policies_source.universe_graph[cookbook_name].keys.first
      if included_policies_source.source_options_for(cookbook_name, version) == source_b.source_options_for(cookbook_name, version)
        true
      else
        false
      end
    end
    conflicting_source_messages << "#{source_b.desc} sets a preferred for cookbook(s) #{conflicting_preferences.join(", ")}. This conflicts with an included policy."
  end
  unless conflicting_source_messages.empty?
    msg = "You may not override the cookbook sources for any cookbooks required by included policies.\n"
    msg << conflicting_source_messages.join("\n") << "\n"
    raise IncludePolicyCookbookSourceConflict.new(msg)
  end
end

def included_policies_cookbook_source

def included_policies_cookbook_source
  @included_policies_cookbook_source ||= begin
    source = Policyfile::IncludedPoliciesCookbookSource.new(included_policies)
    handle_included_policies_preferred_cookbook_conflicts(source)
    source
  end
end

def initialize(ui: nil, chef_config: nil)

def initialize(ui: nil, chef_config: nil)
  @storage_config = Policyfile::StorageConfig.new
  @dsl = Policyfile::DSL.new(storage_config, chef_config: chef_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 install

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? || !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}\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 local_artifacts_graph

merged over the upstream's artifacts graph.
version number. To accommodate this, the local_artifacts_graph should be
forked on github and modified the dependencies without changing the
may have a cookbook "apache2" at version "1.10.4", which the user has
compared to an unmodified copy upstream. For example, the community site
location. These cookbooks could have a different set of dependencies
Gives a dependency graph for cookbooks that are source from an alternate
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 lock

def lock
  @policyfile_lock ||= PolicyfileLock.build_from_compiler(self, storage_config)
end

def normalize_recipe(run_list_item)

def normalize_recipe(run_list_item)
  name = run_list_item.name
  name = "#{name}::default" unless name.include?("::")
  "recipe[#{name}]"
end

def normalized_named_run_lists

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 normalized_run_list

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 override_attributes

def override_attributes
  check_for_override_attribute_conflicts!
  included_policies.map(&:policyfile_lock).inject(
    dsl.node_attributes.combined_override.to_hash
  ) do |acc, lock|
    Chef::Mixin::DeepMerge.merge(acc, lock.override_attributes)
  end
end

def possible_dependencies_of(cookbook_name, source, dependency_set = Set.new)

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(&:first))
  end
  dep_cookbook_names.each do |dep_cookbook_name|
    possible_dependencies_of(dep_cookbook_name, source, dependency_set)
  end
  dependency_set
end

def preferred_source_for_cookbook(conflicting_cb_name)

def preferred_source_for_cookbook(conflicting_cb_name)
  default_source.find { |s| s.preferred_source_for?(conflicting_cb_name) }
end

def remote_artifacts_graph

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 solution_dependencies

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 version_constraint_for(cookbook_name)

def version_constraint_for(cookbook_name)
  if (cookbook_location_spec = cookbook_location_spec_for(cookbook_name)) && cookbook_location_spec.version_fixed?
    version = cookbook_location_spec.version
    "= #{version}"
  else
    DEFAULT_DEMAND_CONSTRAINT
  end
end