require 'open3' require 'yaml' require 'digest/md5' module Rubu class State @@md5 = {} @@state_file = ".rubu_state.yml" def State.save if @@md5.any? IO.write( @@state_file, YAML.dump( @@md5 ) ) end end def State.load if File.exist?( @@state_file ) @@md5 = YAML.load_file( @@state_file ) end end def State.md5_gen( file ) Digest::MD5.hexdigest( File.read( file ) ) end def State.md5_check_and_update?( file ) if @@md5[ file ] md5 = State.md5_gen( file ) if md5 != @@md5[ file ] @@md5[ file ] = md5 return true end return false else @@md5[ file ] = State.md5_gen( file ) return true end end end # Build activity. class Action @@host = [] # Status after execution. attr_reader :status # Storage for error message. attr_reader :errmsg # Command output. attr_reader :output # Sub actions. attr_reader :subs def initialize @status = :success @errmsg = nil @output = nil @subs = [] end # Register Action to host (upper level). def use host.subs.push( self ) if host self end # Take flow into use. def pick( flow ) Flow[ flow ].use end # Push host. def host_in @@host.push self end # Pop host. def host_out @@host.pop end # Current host. def host @@host[-1] end # Report (and store) error. def error( msg ) @errmsg = msg STDERR.puts "Rubu Error: #{@errmsg}" end # Display command output. def display( msg ) @output = msg STDOUT.puts @output end end # Shell based command. class ShellCommand < Action def initialize( cmd ) super() @cmd = cmd end def run begin stdout, stderr, status = Open3.capture3( @cmd ) if Order[ :verbose ] STDOUT.puts @cmd end if status.exitstatus == 0 @status = :success else @status = :error error( stderr ) end rescue error( "Invalid command: \"#{@cmd}\"..." ) @status = :error end self end end # Ruby based command. class RubyCommand < Action def initialize( desc = nil, &cmd ) super() @desc = desc @cmd = cmd end def run begin ret = instance_eval( &@cmd ) @status = :success if @desc && Order[ :verbose ] STDOUT.puts @desc end rescue => f @status = :error error( f.backtrace.join("\n") ) end self end end # Flow execution methods. module FlowRun def serial_run @subs.each do |sub| sub.run if sub.status == :error @status = :error @errmsg = sub.errmsg break end end self end def parallel_run if Order[ :serial ] serial_run else if Order[ :parmax ] == 0 || @subs.length < Order[ :parmax ] ths = [] @subs.each do |item| ths.push( Thread.new do item.run end ) end ths.each do |th| th.join end else cnt = @subs.length done = 0 while done < cnt incr = Order[ :parmax ] if done >= cnt incr = done - cnt end ths = [] incr.times do |i| item = @subs[ done+i ] ths.push( Thread.new do item.run end ) end ths.each do |th| th.join end done += incr end end @subs.each do |item| if item.status == :error @status = :error @errmsg = item.errmsg break end end self end end end # Source or Target file for build commands. Rubu handles only # Marks, and hence bare file names are not usable. class Mark # Convert a list of file names to Marks. def Mark.list( files ) unless files.kind_of? Array files = [ files ] end files.map{ |file| Mark.path( file ) } end # Convert a list of file names matched with pattern to Marks. def Mark.glob( pattern ) Mark.list( Dir.glob( pattern ) ) end # Convert file path to Mark. def Mark.path( file_path ) path = File.absolute_path( file_path ) dir = File.dirname( path ).split( '/' ) rdir = dir - Dir.pwd.split( '/' ) ext = File.extname( path ) base = File.basename( path, ext ) Mark.new( rdir, base, ext ) end # Absolute directory. attr_reader :dir # Relative directory. attr_reader :rdir # Base name. attr_reader :base # File extension. attr_reader :ext # Skip file. attr_accessor :skip def initialize( rdir, base, ext ) if rdir.kind_of? Array @dir = File.absolute_path( rdir.join( '/' ) ).split( '/' ) @rdir = rdir else @dir = File.absolute_path( rdir ).split( '/' ) @rdir = rdir.split( '/' ) end @ext = ext @base = base @opt = {} @skip = false end # Get options. def []( key ) @opt[ key ] end # Set options. def []=( key, val ) @opt[ key ] = val end # Set options. def set_opt( key, val ) @opt[ key ] = val self end # Return absolute path. def path( ext = nil ) ext ||= @ext "#{@dir.join('/')}/#{@base}#{ext}" end # Return relative path. def rpath( ext = nil ) ext ||= @ext "#{@rdir.join('/')}/#{@base}#{ext}" end # Return peer of Mark. def peer( rdir, ext, base = nil ) base ||= @base Mark.new( rdir, base, ext ) end # Does Mark exist? def exist? File.exist?( path ) end # Mark creation time. def time File.stat( path ).mtime end end # Configuration space for Rubu. class Order @@order = {} def Order.[]=( key, val ) @@order[ key ] = val end def Order.[]( key ) @@order[ key ] end # Order defaults: # Force serial flow. Order[ :serial ] = false # Maximun parallel runs (0 for no limit). Order[ :parmax ] = 0 # Verbose execution. Order[ :verbose ] = false end # Option space for program. class Var @@var = {} def Var.[]=( key, val ) @@var[ key ] = val end def Var.[]( key ) @@var[ key ] end end # Information space for program. class Info @@info = {} def Info.[]=( key, val ) @@info[ key ] = val end def Info.[]( key ) @@info[ key ] end end # Build Action. Build Action takes one or more sources, and turns # them into one or more targets. class Build < Action include FlowRun # Create Action and register. def self.use( sources = [], targets = [] ) self.new( sources, targets ).use end # Combine list of sources and targets to source/target pairs. def self.zip( sources, targets ) sources.zip( targets ).map do |pair| self.new( *pair ) end end # Combine list of sources and targets to source/target pairs # and register. def self.usezip( sources, targets ) sources.zip( targets ).map do |pair| self.new( *pair ).use end end attr_reader :sources attr_reader :targets def initialize( sources = [], targets = [] ) super() unless sources.kind_of? Array @sources = [ sources ] else @sources = sources end unless targets.kind_of? Array @targets = [ targets ] else @targets = targets end setup end # Defined by users. def setup end # Run Build Action and capture status. def run if update? build else @status = :success end self end # Default update. def update? true end # Check for date (timestamp) based update needs. def date_update? # Check if targets are missing. @targets.each do |target| unless target.exist? return true end end # Check if source(s) are newer than target(s). newest_source = Time.new( 0 ) @sources.each do |source| if source.time > newest_source && !source.skip newest_source = source.time end end oldest_target = Time.now @targets.each do |target| if target.time < oldest_target oldest_target = target.time end end return newest_source > oldest_target end # Check for mark (checksum) based update needs. def mark_update? unless date_update? return false end # Check if targets are missing. unless target.exist? return true end old_verbose = Order[ :verbose ] Order[ :verbose ] = false build Order[ :verbose ] = old_verbose unless target.exist? error "file generation failure" exit false end unless State.md5_check_and_update?( target.rpath ) target.skip = true return false end true end # Main (first) source file. def source @sources[0] end # Main (first) target file. def target @targets[0] end # Define and run Shell command. def shrun( cmd ) ShellCommand.new( cmd ).run end # Define and register Shell command. def shdef( cmd ) sh = ShellCommand.new( cmd ) sh.use end # Define and run Ruby command. def rbrun( desc = nil, &cmd ) RubyCommand.new( desc, &cmd ).run end # Define and register Ruby command. def rbdef( desc = nil, &cmd ) rb = RubyCommand.new( desc, &cmd ) rb.use end # Execute commands (in block) in parallel. def fork( &blk ) host_in instance_eval &blk host_out parallel_run end # Execute commands (in block) in series. def walk( &blk ) host_in instance_eval &blk host_out serial_run end end # Uncondition build. class AlwaysBuild < Build def update? true end end # Date based build. class DateBuild < Build def update? date_update? end end # Mark based build. class MarkBuild < Build def update? mark_update? end end # Action Flow with name. Action Flow is a collection of build # steps. class Flow < Action include FlowRun # Flow hash. @@flows = {} # Replacement for new method. def self.form( name = nil, &blk ) self.new( name, &blk ) end # Reference Flow by name. def self.[]( name ) @@flows[ name ] end attr_reader :name def initialize( name = nil, &blk ) super() @name = name host_in instance_eval &blk host_out if @name @@flows[ @name ] = self end end # Default run style for Flow. def run serial_run end def Flow.load_setup( setup ) if File.exist? setup conf = YAML.load_file( setup ) conf.each do |k,v| scope = nil case k when :var; scope = Var when :order; scope = Order when :info; scope = Info end v.each do |k2,v2| scope[ k2 ] = v2 end end end end # Apply configuration options if any. def Flow.setup( spec ) load_setup( "#{ENV['HOME']}/.rubu.yml" ) load_setup( ENV['RUBU_CONF'] ) if ENV['RUBU_CONF'] load_setup( ".rubu.yml" ) State.load # Apply options from Como. if spec[ :como ] como = spec[ :como ] if Opt[ como ].given Opt[ como ].value.each do |conf| name, value = conf.split( '=' ) value = case value when 'true'; true when 'false'; false else value end Var[ name.to_sym ] = value end end end end # Run selected flow(s). def Flow.run( flows ) flows.each do |name| begin ret = Flow[ name ].run if ret.status == :error STDERR.puts "Rubu FAILURE..." exit false end rescue STDERR.puts "Broken flow: \"#{name}\"..." exit false end end State.save exit true end end # Serial Flow. class Walk < Flow; end # Parallel Flow. class Fork < Flow def run parallel_run end end end # Array class extension to support common Mark operations. class Array def use self.each do |item| item.use end end def set_opt( key, val ) self.each do |item| item.set_opt( key, val ) end self end def peer( rdir, ext, base = nil ) self.map do |item| item.peer( rdir, ext, base ) end end def path( joiner = ' ' ) self.map{ |item| item.path }.join( joiner ) end def rpath( joiner = ' ' ) self.map{ |item| item.rpath }.join( joiner ) end end