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