def compile_dependency_tree(opts = {})
      old_opts = @opts
      @opts = @opts.merge(opts)
      @queue = []
      @processed_files = {}
      @tree ||= DependencyLeaf.new

      queue_file_for_processing(@user_file, @tree)

      while job = pull_file_from_queue
        process_lilypond_file(job[:path], job[:leaf], opts)
      end

      unless @opts[:ignore_missing]
        squash_old_versions
        remove_unfulfilled_dependencies(tree)
      end

      @tree
    ensure
      @opts = old_opts
    end
def compile_dependency_tree(opts = {})
      old_opts = @opts
      @opts = @opts.merge(opts)
      @queue = []
      @processed_files = {}
      @tree ||= DependencyLeaf.new

      queue_file_for_processing(@user_file, @tree)

      while job = pull_file_from_queue
        process_lilypond_file(job[:path], job[:leaf], opts)
      end

      unless @opts[:ignore_missing]
        squash_old_versions
        remove_unfulfilled_dependencies(tree)
      end

      @tree
    ensure
      @opts = old_opts
    end
Each "leaf" on the dependency tree is a hash of the following structure:
 {
   dependencies: {
     "<package_name>" => {
       clause: "<package>@<version_specifier>",
       versions: {
         "<version>" => {...}
         ...
       }
     }
   }
 }

 Since files to be processed are added to a queue, this method loops through
 the queue until it's empty.