class Rscons::Cache

The Cache class keeps track of file checksums, build target commands and dependencies in a JSON file which persists from one invocation to the next. Example cache:

{
  "version" => "1.2.3",
  "targets" => {
    "program" => {
      "checksum" => "A1B2C3D4",
      "command" => "13543518FE",
      "deps" => [
        {
          "fname" => "program.o",
          "checksum" => "87654321",
        },
      ],
      "user_deps" => [
        {
          "fname" => "lscript.ld",
          "checksum" => "77551133",
        },
      ],
    },
    "program.o" => {
      "checksum" => "87654321",
      "command" => "98765ABCD",
      "deps" => [
        {
          "fname" => "program.c",
          "checksum" => "456789ABC",
        },
        {
          "fname" => "program.h",
          "checksum" => "7979764643",
        },
      ],
      "user_deps" => [],
    }
  },
  "directories" => {
    "build" => true,
    "build/one" => true,
    "build/two" => true,
  },
}

Constants

CACHE_FILE

Name of the file to store cache information in

PHONY_PREFIX

Prefix for phony cache entries.

Public Class Methods

instance() click to toggle source

Access the singleton instance.

# File lib/rscons/cache.rb, line 62
def instance
  @instance ||= Cache.new
end
new() click to toggle source

Create a Cache object and load in the previous contents from the cache file.

# File lib/rscons/cache.rb, line 69
def initialize
  initialize!
end

Public Instance Methods

clear() click to toggle source

Remove the cache file.

@return [void]

# File lib/rscons/cache.rb, line 76
def clear
  FileUtils.rm_f(CACHE_FILE)
  initialize!
end
clear_checksum_cache!() click to toggle source

Clear the cached file checksums.

@return [void]

# File lib/rscons/cache.rb, line 84
def clear_checksum_cache!
  @lookup_checksums = {}
end
directories() click to toggle source

Return a list of directories which were created as a part of the build.

@return [Array<String>]

List of directories which were created as a part of the build.
# File lib/rscons/cache.rb, line 281
def directories
  @cache["directories"].keys
end
lookup_checksum(file) click to toggle source

Return a file's checksum, or the previously calculated checksum for the same file.

@param file [String] The file name.

@return [String] The file's checksum.

# File lib/rscons/cache.rb, line 291
def lookup_checksum(file)
  @lookup_checksums[file] || calculate_checksum(file)
end
mkdir_p(path) click to toggle source

Make any needed directories and record the ones that are created for removal upon a “clean” operation.

@param path [String] Directory to create.

@return [void]

# File lib/rscons/cache.rb, line 265
def mkdir_p(path)
  parts = path.split(/[\\\/]/)
  parts.each_index do |i|
    next if parts[i] == ""
    subpath = File.join(*parts[0, i + 1])
    unless File.exists?(subpath)
      FileUtils.mkdir_p(subpath)
      @cache["directories"][subpath] = true
    end
  end
end
register_build(targets, command, deps, env) click to toggle source

Store cache information about target(s) built by a builder.

@param targets [Symbol, String, Array<String>]

The name of the target(s) built.

@param command [String, Array, Hash]

The command used to build the target. The command parameter can
actually be a String, Array, or Hash and could contain information
other than just the actual command used to build the target. For the
purposes of the Cache, any difference in the command argument will
trigger a rebuild.

@param deps [Array<String>] List of dependencies for the target. @param env [Environment] The {Rscons::Environment}.

@return [void]

# File lib/rscons/cache.rb, line 230
def register_build(targets, command, deps, env)
  Array(targets).each do |target|
    target_checksum = Rscons.phony_target?(target) ? "" : calculate_checksum(target)
    @cache["targets"][get_cache_key(target)] = {
      "command" => Digest::MD5.hexdigest(command.inspect),
      "checksum" => target_checksum,
      "deps" => deps.map do |dep|
        {
          "fname" => dep,
          "checksum" => lookup_checksum(dep),
        }
      end,
      "user_deps" => (env.get_user_deps(target) || []).map do |dep|
        {
          "fname" => dep,
          "checksum" => lookup_checksum(dep),
        }
      end,
    }
  end
end
targets() click to toggle source

Return a list of targets that have been built.

@return [Array<String>] List of targets that have been built.

# File lib/rscons/cache.rb, line 255
def targets
  @cache["targets"].keys
end
up_to_date?(targets, command, deps, env, options = {}) click to toggle source

Check if target(s) are up to date.

@param targets [Symbol, String, Array<String>]

The name(s) of the target file(s).

@param command [String, Array, Hash]

The command used to build the target. The command parameter can
actually be a String, Array, or Hash and could contain information
other than just the actual command used to build the target. For the
purposes of the Cache, any difference in the command argument will
trigger a rebuild.

@param deps [Array<String>] List of the target's dependency files. @param env [Environment] The Rscons::Environment. @param options [Hash] Optional options. @option options [Boolean] :debug

If turned on, this causes the Cache to print messages explaining why
a build target is out of date. This could aid a builder author in
debugging the operation of their builder.

@option options [Boolean] :strict_deps

Only consider a target up to date if its list of dependencies is
exactly equal (including order) to the cached list of dependencies

@return [Boolean]

True value if the targets are all up to date, meaning that,
for each target:
- the target exists on disk
- the cache has information for the target
- the target's checksum matches its checksum when it was last built
- the command used to build the target is the same as last time
- all dependencies listed are also listed in the cache, or, if
  :strict_deps was given in options, the list of dependencies is
  exactly equal to those cached
- each cached dependency file's current checksum matches the checksum
  stored in the cache file
# File lib/rscons/cache.rb, line 131
def up_to_date?(targets, command, deps, env, options = {})
  Array(targets).each do |target|
    cache_key = get_cache_key(target)

    unless Rscons.phony_target?(target)
      # target file must exist on disk
      unless File.exists?(target)
        if options[:debug]
          puts "Target #{target} needs rebuilding because it does not exist on disk"
        end
        return false
      end
    end

    # target must be registered in the cache
    unless @cache["targets"].has_key?(cache_key)
      if options[:debug]
        puts "Target #{target} needs rebuilding because there is no cached build information for it"
      end
      return false
    end

    unless Rscons.phony_target?(target)
      # target must have the same checksum as when it was built last
      unless @cache["targets"][cache_key]["checksum"] == lookup_checksum(target)
        if options[:debug]
          puts "Target #{target} needs rebuilding because it has been changed on disk since being built last"
        end
        return false
      end
    end

    # command used to build target must be identical
    unless @cache["targets"][cache_key]["command"] == Digest::MD5.hexdigest(command.inspect)
      if options[:debug]
        puts "Target #{target} needs rebuilding because the command used to build it has changed"
      end
      return false
    end

    cached_deps = @cache["targets"][cache_key]["deps"] || []
    cached_deps_fnames = cached_deps.map { |dc| dc["fname"] }
    if options[:strict_deps]
      # depedencies passed in must exactly equal those in the cache
      unless deps == cached_deps_fnames
        if options[:debug]
          puts "Target #{target} needs rebuilding because the :strict_deps option is given and the set of dependencies does not match the previous set of dependencies"
        end
        return false
      end
    else
      # all dependencies passed in must exist in cache (but cache may have more)
      unless (Set.new(deps) - Set.new(cached_deps_fnames)).empty?
        if options[:debug]
          puts "Target #{target} needs rebuilding because there are new dependencies"
        end
        return false
      end
    end

    # set of user dependencies must match
    user_deps = env.get_user_deps(target) || []
    cached_user_deps = @cache["targets"][cache_key]["user_deps"] || []
    cached_user_deps_fnames = cached_user_deps.map { |dc| dc["fname"] }
    unless user_deps == cached_user_deps_fnames
      if options[:debug]
        puts "Target #{target} needs rebuilding because the set of user-specified dependency files has changed"
      end
      return false
    end

    # all cached dependencies must have their checksums match
    (cached_deps + cached_user_deps).each do |dep_cache|
      unless dep_cache["checksum"] == lookup_checksum(dep_cache["fname"])
        if options[:debug]
          puts "Target #{target} needs rebuilding because dependency file #{dep_cache["fname"]} has changed"
        end
        return false
      end
    end
  end

  true
end
write() click to toggle source

Write the cache to disk.

@return [void]

# File lib/rscons/cache.rb, line 91
def write
  @cache["version"] = VERSION
  File.open(CACHE_FILE, "w") do |fh|
    fh.puts(JSON.dump(@cache))
  end
end

Private Instance Methods

calculate_checksum(file) click to toggle source

Calculate and return a file's checksum.

@param file [String] The file name.

@return [String] The file's checksum.

# File lib/rscons/cache.rb, line 330
def calculate_checksum(file)
  @lookup_checksums[file] = Digest::MD5.hexdigest(File.read(file, mode: "rb")) rescue ""
end
get_cache_key(target_name) click to toggle source

Return a String key based on the target name to use in the on-disk cache.

@param target_name [Symbol, String]

Target name.

@return [String]

Key name.
# File lib/rscons/cache.rb, line 304
def get_cache_key(target_name)
  if Rscons.phony_target?(target_name)
    PHONY_PREFIX + target_name.to_s
  else
    target_name
  end
end
initialize!() click to toggle source

Create a Cache object and load in the previous contents from the cache file.

# File lib/rscons/cache.rb, line 314
def initialize!
  @cache = JSON.load(File.read(CACHE_FILE)) rescue {}
  unless @cache.is_a?(Hash)
    $stderr.puts "Warning: #{CACHE_FILE} was corrupt. Contents:\n#{@cache.inspect}"
    @cache = {}
  end
  @cache["targets"] ||= {}
  @cache["directories"] ||= {}
  @lookup_checksums = {}
end