# = TITLE: # # BatchFile # # = 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 . #require 'shellwords' require 'yaml' require 'rbconfig' # replace with facets/rbsystem in future ? #require 'facets/hash/merge' # for reverse_merge require 'ratch/batch/options' require 'ratch/batch/consoleutils' require 'ratch/batch/configutils' require 'ratch/batch/emailutils' require 'ratch/batch/fileutils' require 'ratch/batch/argvutils' require 'ratch/batch/task' require 'ratch/batch/build' module Ratch # If no batch file is found. class NoBatchError < Exception end # This is a type of functor, that allows for calling batch files # that are in subdirectories using "dir.file" notation. Eg. # # svn.log # # could run the svn/log ratch file. class BatchDirectory private *instance_methods.select{ |m| m !~ /^__/ } def initialize(manager, directory) @manager = manager @directory = directory end def method_missing(sym, *args) path = File.join(@directory, sym.to_s) @manager.open_batch(path, *args) end end # BatchScript module defines the DSL available to a ratch script. module BatchScript #< Module include GeneralOptions include ConsoleUtils include ArgvUtils include FileUtils include ConfigUtils include EmailUtils # Shell runner. def sh(cmd) if noharm? puts cmd true else puts "--> system call: #{cmd}" if trace? system(cmd) end end # Abort running. #def abort(msg=nil) # puts msg if msg # exit 0 #end def root_directory @root_directory ||= Dir.pwd end def call_directory @call_directory ||= File.expand_path(File.dirname($0)) end # TODO Better name? Better definition? (Won't handle task subdirs!). def batch_directory @batch_directory ||= ( dir = call_directory.sub(root_directory + '/', '').split('/').first File.join(root_directory, dir) ) end # Run batch file and cache result. # # Usually this can be taken care of by method_missing. # But, in some cases, built in method names block batch # calls, so you have to use #batch to invoke those. def batch(batchfile, arguments=nil) batch_cache[batchfile] ||= launch(batchfile, arguments) end # Lauch a batch file. Like #batch but not-cached. # Run a batch file. # TODO: How to handle arguments? def launch(batchfile, arguments=nil) # # TODO probably should raise error instead # abort "missing batch file -- #{batchfile}" unless File.file?(batchfile) #BatchFile.new(batchfile).call # Old way with batch execution context object. script = File.read($0 = batchfile) #eval(script, $batch_binding, $0) eval(script, TOPLEVEL_BINDING, $0) batch_file = File.expand_path($0).sub(batch_directory + '/', '') call_task(batch_file) end # Is a path a local batch directory? def batch_directory?(path) b = File.dirname($0) + "/#{path}" b if FileTest.directory?(b) end # Is a file a local batch file? def batch?(path) b = File.dirname($0) + "/#{path}" #.chomp!('!') b if FileTest.file?(b) && FileTest.executable?(b) end # Is a batch run complete or in the process of being completed? # Has the batch file been executed before? def done?(batchfile) batchfile == $0 || batch_cache.key?(batchfile) end # Batch cache, which prevents batch runs from re-executing. def batch_cache @batch_cache ||= {} end # If method is missing try to run an external task # or binary by that name. If it is a binary, arguments # translate into commandline parameters. For example: # # tar 'foo/', :x=>true, :v=>true, :z=>true, :f=>'foo.tar.gz' # # or # # tar '-xvzf', "foo.tar.gz", "foo/" # # becomes # # tar -x -v -z -f foo.tar.gz foo/ # # If it is a task, it will be cached. Tasks only ever run once. # To run them more than once you can manually execute them with #run. # Likewise you can manually run and cache by calling #batch. # This is good to know, b/c in some cases built in method names # block task calls, so you have to #batch to invoke them. def method_missing(sym,*args) puts "method_missing: #{sym}" if debug? #begin open_batch(sym,*args) #rescue NoBatchError # super #end end private # def open_batch(name, *args) name = name.to_s force = name.chomp!('!') # is this a batch directory? if dir = batch_directory?(name) return BatchDirectory.new(self, dir) end params = args.to_params # is this a batch file? if bat = batch?(name) if force cmd = "./#{bat} #{params}" puts "--> non-cached execution: #{cmd}" if trace? return launch(bat, args) else if done?(bat) return nil unless bin?(name) # return cache? else cmd = "./#{bat} #{params}" puts "--> cached execution: #{cmd}" if trace? return batch(bat, args) end end end # is this a bin file? if bin = bin?(name) cmd = "#{File.basename(bin)} #{params}" return sh(cmd) end raise NoBatchError, "no extecutable file found -- #{name}" end public # Define main task. def main(name, &block) name, deps, block = *parse_task_dependencies(name, &block) define_main(name, *deps, &block) end # Define a task. def task(name, &block) name, deps, block = *parse_task_dependencies(name, &block) define_task(name, *deps, &block) end # Run a task. def run(name, arguments=nil) call_task(name) end private def tasks ; @tasks ||= {} ; end # TODO If @main is nil try task by same name a file (?) def main_task @main end # def parse_task_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 def define_main(name=nil, *depend, &block) @main = define_task(name, *depend, &block) #@main = Task.new(name, *depend, &block) #tasks[@main.name] = @main end def define_task(name, *depend, &block) task = Task.new(name, *depend, &block) tasks[task.name] = task end # Call main task. def call_main #(task=nil) #@main ||= task #return unless @main || task call_task(main_task.name) end # Call task. def call_task(name) task_plan(name).each{ |name| tasks[name].call } end # Prepare plan, checking for circular dependencies. def task_plan(name, list=[]) if list.include?(name) raise "Circular dependency #{name}." end if task = tasks[name] task.needs.each do |need| need = need.to_s next if list.include?(need) #@tasks[need].task_plan(need, list) task_plan(need, list) end list << task.name else # TODO THIS TIES TASKS INTO BATCH, BETTER WAY? if name != main_task && fname = batch?(name) task = Task.new(name) do batch(fname) end tasks[name] = task list << task.name else abort "no task -- #{name}" end end return list end public # Define a build target. def file(name, &block) name, deps, block = *parse_build_dependencies(name, &block) define_file(name, *deps, &block) end # Build target(s). def build(file) call_build(file) end private def builds; @builds ||= [] ; end # 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 # Define a file build task. def define_file(name, *depend, &block) build = Build.new(name, *depend, &block) builds << build end # Call build. def call_build(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 = *build_plan(build, path) todo.each{|bld, pth| bld.call(pth) } #@builds[name].call } build.call(path) end else raise "build not found -- #{path}" end end def find(path) builds.find{ |b| b.match?(path) } end # Prepare build plan, checking for circular dependencies. def build_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) build_plan(nbuild, npath, list, todo) todo << [nbuild, npath] else list << npath end end return list, todo end # OLD WAY # def method_missing(sym,*args) # name = sym.to_s # # bat = batch?(name) # is this a batch file? # done = bat && done?(bat) # cache = bat && !done && name[1,-1] != '!' # bin = bin?(name) if (!bat || done) # none = bat && done && !bin # #bat = name if bin # # return super unless bat || bin # # return if none # nothing to do # # params = args.to_params # # if bin # cmd = "#{File.basename(bin)} #{params}" # res = sh(cmd) # elsif bat # cmd = "./#{bat} #{params}" # puts "--> #{cache ? '' : 'not-'}cached execution: #{cmd}" if trace? # res = batch(bat, args) # if cache # #@batch_catch[bat] ||= (system(cmd); true) # #batch_cache[bat] ||= res # batch_manager.cache ||= res # end # end # # return res # Quick start, equivalent to calling new.run(file). #def self.start(file) # new(file).call #end # New Batch File #def initialize(file) # abort "missing batch file -- #{file}" unless File.file?(file) # @file = file #end # TODO What todo about arguments? #def call(arguments=nil) # script = File.read($0 = @file) # eval(script, binding, $0) #instance_eval(script) # #@main.call if @main # call_main # #run(:main) if task_manager.main # end end end # Load BatchScript into to toplevel. # # TODO: Should this be in all Object space (ie. no class << self)? class << self include Ratch::BatchScript end END { #task = File.expand_path($0).sub(batch_directory + '/', '') call_main #(task) } #$batch_binding = binding