From 119f3c9f0fe1d6bc68d4daa044978afe33127c37 Mon Sep 17 00:00:00 2001 From: Josh Holtrop Date: Sun, 23 Jun 2013 23:21:02 -0400 Subject: [PATCH] Do not rebuild targets when they are up to date Store dependencies and file checksums in a cache file --- lib/rscons.rb | 1 + lib/rscons/builders/cc.rb | 19 +++++---- lib/rscons/builders/program.rb | 22 +++++----- lib/rscons/cache.rb | 74 ++++++++++++++++++++++++++++++++++ spec/build_tests_spec.rb | 11 +++++ 5 files changed, 110 insertions(+), 17 deletions(-) create mode 100644 lib/rscons/cache.rb diff --git a/lib/rscons.rb b/lib/rscons.rb index 2047c38..934c6b4 100644 --- a/lib/rscons.rb +++ b/lib/rscons.rb @@ -1,6 +1,7 @@ require "rscons/version" require "rscons/builder" require "rscons/environment" +require "rscons/cache" # default builders require "rscons/builders/cc" diff --git a/lib/rscons/builders/cc.rb b/lib/rscons/builders/cc.rb index aba0805..d7af459 100644 --- a/lib/rscons/builders/cc.rb +++ b/lib/rscons/builders/cc.rb @@ -17,14 +17,17 @@ module Rscons def run(env, target, source) raise "String expected, not #{source.inspect}" unless source.is_a?(String) o_file = "#{env.stem(source)}#{env['OBJSUFFIX']}" - command = [ - env['CC'], - *env['CPPFLAGS'], - *env['CFLAGS'], - '-o', o_file, - source - ] - env.execute("CC #{o_file}", command) + unless Cache.open.up_to_date?(target, [source]) + command = [ + env['CC'], + *env['CPPFLAGS'], + *env['CFLAGS'], + '-o', o_file, + source + ] + env.execute("CC #{o_file}", command) + Cache.open.register_build(target, [source]) + end o_file end end diff --git a/lib/rscons/builders/program.rb b/lib/rscons/builders/program.rb index 0ecbc10..b457036 100644 --- a/lib/rscons/builders/program.rb +++ b/lib/rscons/builders/program.rb @@ -11,6 +11,7 @@ module Rscons def run(env, target, sources) sources = [sources] if sources.is_a?(String) + # convert sources to object file names sources = sources.map do |source| if source =~ /#{env['OBJSUFFIX']}$/ or source =~ /#{env['LIBSUFFIX']}$/ source @@ -24,15 +25,18 @@ module Rscons end end end - command = [ - env['LD'] || env['CC'], - '-o', target, - *env['LDFLAGS'], - *sources, - *env['LIBPATHS'].map {|lp| "-L#{lp}"}, - *env['LIBS'].map {|lib| "-l#{lib}"} - ] - env.execute("LINK #{target}", command) + unless Cache.open.up_to_date?(target, sources) + command = [ + env['LD'] || env['CC'], + '-o', target, + *env['LDFLAGS'], + *sources, + *env['LIBPATHS'].map {|lp| "-L#{lp}"}, + *env['LIBS'].map {|lib| "-l#{lib}"} + ] + env.execute("LINK #{target}", command) + Cache.open.register_build(target, sources) + end target end end diff --git a/lib/rscons/cache.rb b/lib/rscons/cache.rb new file mode 100644 index 0000000..ea86386 --- /dev/null +++ b/lib/rscons/cache.rb @@ -0,0 +1,74 @@ +require 'yaml' +require 'fileutils' +require 'digest/md5' +require 'set' + +module Rscons + class Cache + CACHE_FILE = '.rscons' + + private_class_method :new + def initialize + @cache = YAML.load(File.read(CACHE_FILE)) rescue {} + @lookup_checksums = {} + @stored_checksums = {} + end + + def self.open + @@cache ||= new + end + + def clear + FileUtils.rm_f(CACHE_FILE) + initialize + end + + def write + File.open(CACHE_FILE, 'w') do |fh| + fh.puts(YAML.dump(@cache)) + end + end + + def up_to_date?(file, deps = nil) + return false unless File.exists?(file) + stored_md5 = @cache[:checksums][file] rescue nil + return false unless stored_md5 + return false unless lookup_checksum(file) == stored_md5 + cached_deps = @cache[:deps][file] || [] rescue [] + return false if deps and Set.new(cached_deps) != Set.new(deps) + cached_deps.map {|d| up_to_date?(d)}.all? + end + + def register_build(target, deps) + @cache[:deps] ||= {} + @cache[:deps][target] = deps + store_checksum(target) + deps.each do |dep| + store_checksum(dep) + end + # it is unfortunate to write the file here since many file writes may + # occur before the final version, but I don't know where else to do so + write + end + + private + + def lookup_checksum(file) + @lookup_checksums[file] ||= calculate_checksum(file) + end + + def store_checksum(file) + cs = calculate_checksum(file) + if @stored_checksums[file] and @stored_checksums[file] != cs + $stderr.puts "Warning: file #{file.inspect} changed after previously used by builder" + end + @cache[:checksums] ||= {} + @cache[:checksums][file] = cs + @lookup_checksums[file] = cs + end + + def calculate_checksum(file) + Digest::MD5.hexdigest(File.read(file)) + end + end +end diff --git a/spec/build_tests_spec.rb b/spec/build_tests_spec.rb index dba6cc0..1cea453 100644 --- a/spec/build_tests_spec.rb +++ b/spec/build_tests_spec.rb @@ -17,6 +17,7 @@ describe Rscons do before do $stdout.stub(:puts) { nil } + Rscons::Cache.open.clear end ########################################################################### @@ -31,4 +32,14 @@ describe Rscons do `./simple`.should =~ /This is a simple C program/ end end + + it 'does not rebuild the program if no sources changed' do + $stdout.should_receive(:puts).once.with('gcc -c -o simple.o simple.c') + $stdout.should_receive(:puts).once.with('gcc -o simple simple.o') + setup_testdir(['simple.c']) do + env = Rscons::Environment.new + env.Program('simple', 'simple.c') + env.Program('simple', 'simple.c') + end + end end