class Inspec::Resolver
implementation of the fetcher being used.
Currently the fetching happens somewhat lazily depending on the
of the same profile being used at runtime.
will provide the isolation necessary to support multiple versions
The full dependency tree is then available for the loader, which
specified version constraint
- Checks that the specified dependency source satisfies the
- Checks the presence of cycles, and
- Fetches the dependency from the source
and:
to a single version. Rather, it traverses down the dependency tree
or Berkshelf, it does not attempt to resolve each named dependency
Inspec::Resolver is a simple dependency resolver. Unlike Bundler
def self.resolve(dependencies, cache, working_dir, backend)
def self.resolve(dependencies, cache, working_dir, backend) reqs = dependencies.map do |dep| req = Inspec::Requirement.from_metadata(dep, cache, cwd: working_dir, backend: backend) req || raise("Cannot initialize dependency: #{req}") end new.resolve(reqs) end
def detect_duplicates(deps, top_level, path_string)
def detect_duplicates(deps, top_level, path_string) seen_items_local = [] deps.each do |dep| if seen_items_local.include?(dep.name) problem_cookbook = if top_level "the inspec.yml for this profile." else "the dependency information for #{path_string.split(" ").last}" end raise Inspec::DuplicateDep, "The dependency #{dep.name} is listed twice in #{problem_cookbook}" else seen_items_local << dep.name end end end
def fallback_to_archive_on_fetch_failure(dep)
def fallback_to_archive_on_fetch_failure(dep) # This facility is intended to handle situations in which # the failing dependency *is* available in an archive that we have # available as a local dependency. We just need to find the archive and # alter the fetcher to refer to information in the archive. # Note that the vendor cache already should have the archive inflated # for this to work (see warm_cache_from_archives() from profile_vendor.rb) # Refs 4727 # This is where any existing archives should have been inflated - # that is, this is the vendor cache. Each archive would have a lockfile. cache_path = dep.cache.path worth_retrying = false Dir["#{cache_path}/*/inspec.lock"].each do |lockfile_path| lockfile = Inspec::Lockfile.from_file(lockfile_path) dep_set = Inspec::DependencySet.from_lockfile(lockfile, dep.opts) dep2 = dep_set.dep_list[dep.name] next unless dep2 if dep.opts.key?(:compliance) # This is ugly. The compliance fetcher works differently than the others, # and fails at the resolve stage, not the fetch stage. That means we can't # tweak the fetcher, we have to tweak the deps opts themselves. dep.opts[:sha256] = dep2.opts[:sha256] worth_retrying = true else # All other fetchers can be generalized, because they will survive their constructor. fetcher = dep.fetcher.fetcher # Not the CachedFetcher, but its fetcher made_a_change = fetcher.update_from_opts(dep2.opts) end worth_retrying ||= made_a_change end worth_retrying end
def resolve(deps, top_level = true, seen_items = {}, path_string = "") # rubocop:disable Metrics/AbcSize
Here deps is an Array of Inspec::Requirement
def resolve(deps, top_level = true, seen_items = {}, path_string = "") # rubocop:disable Metrics/AbcSize graph = {} if top_level Inspec::Log.debug("Starting traversal of dependencies #{deps.map(&:to_s)}") else Inspec::Log.debug("Traversing dependency tree of transitive dependency #{deps.map(&:name)}") end detect_duplicates(deps, top_level, path_string) deps.each do |dep| # Calling dep.resolved_source forces a fetch. Handle any airgap chicanery early. if Inspec::Config.cached[:airgap] begin dep.resolved_source rescue Inspec::FetcherFailure Inspec::Log.debug("Failed to fetch #{dep.name}, falling back to archives if possible") retry if fallback_to_archive_on_fetch_failure(dep) end end new_seen_items = seen_items.dup new_path_string = if path_string.empty? dep.name else path_string + " -> #{dep.name}" end raise Inspec::CyclicDependencyError, "Dependency #{dep} would cause a dependency cycle (#{new_path_string})" if new_seen_items.key?(dep.resolved_source) new_seen_items[dep.resolved_source] = true unless dep.source_satisfies_spec? raise Inspec::UnsatisfiedVersionSpecification, "The profile #{dep.name} from #{dep.resolved_source} has a version #{dep.source_version} which doesn't match #{dep.version_constraints}" end Inspec::Log.debug("Adding dependency #{dep.name} (#{dep.resolved_source})") graph[dep.name] = dep unless dep.dependencies.empty? resolve(dep.dependencies, false, new_seen_items.dup, new_path_string) end end Inspec::Log.debug("Dependency traversal complete.") if top_level graph end