lib/ratch/batch.rb in ratch-0.2.3 vs lib/ratch/batch.rb in ratch-0.3.0

- old
+ new

@@ -1,10 +1,10 @@ -# TITLE: +# = TITLE: # # BatchFile # -# COPYING: +# = COPYING: # # Copyright (c) 2007 Psi T Corp. # # This file is part of the ProUtils' Ratch program. # @@ -24,78 +24,465 @@ #require 'shellwords' require 'yaml' require 'rbconfig' # replace with facets/rbsystem in future ? #require 'facets/hash/merge' # for reverse_merge -require 'ratch/options' +require 'ratch/batch/options' -require 'ratch/consoleutils' -require 'ratch/configutils' -require 'ratch/emailutils' -require 'ratch/fileutils' -require 'ratch/argvutils' +require 'ratch/batch/consoleutils' +require 'ratch/batch/configutils' +require 'ratch/batch/emailutils' +require 'ratch/batch/fileutils' +require 'ratch/batch/argvutils' -require 'ratch/taskable' -require 'ratch/buildable' -require 'ratch/batchable' +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 - include Buildable - include Taskable - include Batchable + # Shell runner. - # Quick start, equivalent to calling new.run(file). + 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) -#puts script -#p $0 -#puts -# eval(script, binding, $0) #instance_eval(script) -# #@main.call if @main -# task_manager.call_main -# #run(:main) if task_manager.main -# end + # 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 main runspace. +# Load BatchScript into to toplevel. # # TODO: Should this be in all Object space (ie. no class << self)? + class << self include Ratch::BatchScript end -$batch_binding = binding - END { - task_manager.call_main + #task = File.expand_path($0).sub(batch_directory + '/', '') + call_main #(task) } + +#$batch_binding = binding