# TITLE: # # Buildable # # COPYING: # # Copyright (c) 2007 Psi T Corp. # # This file is part of the ProUtils' Ratch program. # # Ratch is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Ratch is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Ratch. If not, see . module Ratch # = Buildable mixin # module Buildable # Reference task manager. def build_manager @build_manager ||= BuildManager.new(self) end # Define a build target. def file(name, &block) name, deps, block = *parse_build_dependencies(name, &block) build_manager.define_file(name, *deps, &block) end # Build a file. def build(file) build_manager.call(file) end private # def parse_build_dependencies(name_deps, &block) if Hash===name_deps name = name_deps.keys[0] deps = name_deps.values[0] else name = name_deps deps = [] end [name, deps, block] end end # = BuildManager class # class BuildManager attr :builds def initialize(runspace) @runspace = runspace @builds = [] end def force? ; @runspace.force? ; end # Define a file build task. def define_file(name, *depend, &block) build = Build.new(name, *depend, &block) builds << build end # Call build. def call(path) # TODO How to handle more than one matching means of building? #warn "More than one build definition matches #{path} using #{means.first}" if means.size > 1 if build = find(path) if build.needed_for?(path) || force? list, todo = *plan(build, path) todo.each{|bld, pth| bld.call(pth) } #@builds[name].call } build.call(path) end else raise end end def find(path) builds.find{ |b| b.match?(path) } end # Prepare plan, checking for circular dependencies. def plan(build, path, list=[], todo=[]) #if list.include?(build) # raise "Circular build dependency #{build.name}." #end build.needed_paths.each do |npath| next if list.include?(npath) if nbuild = find(npath) plan(nbuild, npath, list, todo) todo << [nbuild, npath] else list << npath end end return list, todo end end # = Build class # class Build attr :match attr :needs attr :action alias_method :name, :match # Create a new build definition. def initialize(match, *needs, &action) @match = match @needs = needs @action = action end # Call this build process for the given path. def call(path) if File.exist?(path) mtime = File.mtime(path) dated = needs.find do |file| !File.exist?(file) || File.mtime(file) > mtime end else dated = true end action.call(path) if dated end # Does a file match this build definition? def match?(path) case match when String File.fnmatch(match, path) when Regexp match =~ path else false # ??? end end # Is this build needed to update/create path? def needed_for?(path) return true unless File.exist?(path) mtimes = needed_paths.collect{|f| File.mtime(f)} mtimes.max > File.mtime(path) end # Glob expanded needs. def needed_paths #exact = needs.select{|n| File.fnmatch?(n,n)} + exact = needs.select{|n| n !~ /[\[*?]/ } globs = needs.collect{|n| Dir.glob(n)}.flatten (exact + globs).uniq end end end