require 'open3' require 'yaml' require 'digest/md5' require_relative './version.rb' module Rubu # Persistent state of Rubu. # # State maintains YAML based state file (if in use). The file # include MD5 checksums of file content. class State @@md5 = {} @@state_file = ".rubu_state.yml" # Save state. def State.save if @@md5.any? IO.write( @@state_file, YAML.dump( @@md5 ) ) end end # Load state. def State.load if File.exist?( @@state_file ) @@md5 = YAML.load_file( @@state_file ) end end # Generate MD5 checksum for file. # # @param file File. def State.md5_gen( file ) Digest::MD5.hexdigest( File.read( file ) ) end # Check for existing checksum and update it if needed. # # @param file File. 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 # Move is the action in Step. class Move @@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 Move to host (upper level). def use host.subs.push( self ) if host self end # Push host as current to host stack. def host_in @@host.push self end # Pop host from host stack. 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 # Report warning. def warn( msg ) STDERR.puts "Rubu Warning: #{msg}" end # Display command output. def display( msg ) @output = msg STDOUT.puts @output end end # Shell based command. class ShellCommand < Move def initialize( cmd ) super() @cmd = cmd end # Execution content. def run begin stdout, stderr, status = Open3.capture3( @cmd ) unless Order[ :noop ] if Order[ :verbose ] STDOUT.puts @cmd end unless Order[ :noop ] if status.exitstatus == 0 if Order[ :sh_warn ] && not( stderr.empty? ) warn( stderr ) end @status = :success else @status = :error error( stderr ) end end rescue error( "Invalid command: \"#{@cmd}\"..." ) @status = :error end self end end # Ruby based command. class RubyCommand < Move def initialize( desc = nil, &cmd ) super() @desc = desc @cmd = cmd end # Execution content. def run begin ret = instance_eval( &@cmd ) unless Order[ :noop ] @status = :success if @desc && Order[ :verbose ] STDOUT.puts @desc end rescue => f @status = :error error( f.backtrace.join("\n") ) end self end end # Trail/Step execution styles. module MoveStyles # Run @subs in sequence. def serial_run @subs.each do |sub| sub.run if sub.status == :error @status = :error @errmsg = sub.errmsg break end end self end # Run @subs in parallel (unless serial is forced). 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 Step commands. Rubu handles only # Marks, and hence bare file names are not usable. # # Mark includes both relative and absolute paths for the file. class Mark # Convert file path to Mark. # # @param file_path Relative or absolute path of file. 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 # Convert a list of file names to Marks. # # @param files List of files to convert 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 glob pattern to # Marks. # # @param pattern Glob pattern. def Mark.glob( pattern ) Mark.list( Dir.glob( pattern ) ) end # Absolute directory. attr_reader :dir # Relative directory. attr_reader :rdir # Base name. attr_reader :base # File extension. attr_reader :ext # Skip file in Step. attr_accessor :skip # Create Mark object. # # @param rdir Relative file path. # @param base Basename of file. # @param ext File extension (suffix). 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 with key. def []( key ) @opt[ key ] end # Set option. def []=( key, val ) @opt[ key ] = val end # Set option. def set_opt( key, val ) @opt[ key ] = val self end # Return absolute path. # # @param ext Use this extensions instead, if given. def path( ext = @ext ) "#{@dir.join('/')}/#{@base}#{ext}" end # Return relative path. # # @param ext Use this extensions instead, if given. def rpath( ext = @ext ) "#{@rdir.join('/')}/#{@base}#{ext}" end # Return peer of Mark. # # @param rdir Relative path of peer. # @param ext Extension of peer. # @param base Optional basename of peer (use original if not given). def peer( rdir, ext, base = nil ) base ||= @base Mark.new( rdir, base, ext ) end # Does Mark exist? def exist? File.exist?( path ) end # Mark update time. def time File.stat( path ).mtime end end # Configuration space for Rubu. # # Options: # * serial - Force parallel executions to serial (default: parallel). # * parmax - Limit the number of parallel executions (default: 0). # * verbose - Show command executions (default: false). # * noop - No operation (default: false). # * force - Force Step updates (default: false). # * sh_warn - Show shell warnings (default: true). class Order @@order = {} # Set Order entry value. def Order.[]=( key, val ) @@order[ key ] = val end # Get Order entry value. def Order.[]( key ) @@order[ key ] end # Order defaults: # Force serial trail. Order[ :serial ] = false # Maximun parallel runs (0 for no limit). Order[ :parmax ] = 0 # Verbose execution. Order[ :verbose ] = false # No operation. Order[ :noop ] = false # Force Step updates. Order[ :force ] = false # Show warnings (for shell commands). Order[ :sh_warn ] = true end # Option space for program. class Var @@var = {} # Set Var entry value. def Var.[]=( key, val ) @@var[ key ] = val end # Get Var entry value. def Var.[]( key ) @@var[ key ] end end # Information space for program. class Info @@info = {} # Set Info entry value. def Info.[]=( key, val ) @@info[ key ] = val end # Get Info entry value. def Info.[]( key ) @@info[ key ] end end # Step in Trail. Step takes one or more sources, and turns # them into one or more targets. class Step < Move include MoveStyles # Create Move and register. # # @param sources One or more sources. # @param targets One or more targets. def self.use( sources = [], targets = [] ) self.new( sources, targets ).use end # Combine list of sources and targets to source/target pairs. # # @param sources List of sources. # @param targets List of targets. 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. # # @param sources List of sources. # @param targets List of targets. def self.usezip( sources, targets ) sources.zip( targets ).map do |pair| self.new( *pair ).use end end attr_reader :sources attr_reader :targets # Create Step object. # # @param sources One or more sources. # @param targets One or more 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 # Setup variables for Step. Should be defined again in derived # classes. def setup end # Default to no action. Typically this method is redefined. def step end # Run Step and capture status. def run if update? step else @status = :success end self end # Default update. Should be defined again in derived classes. 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? target.skip = true return false end # Check if targets are missing. unless target.exist? return true end old_verbose = Order[ :verbose ] Order[ :verbose ] = false step 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/only (first) source file. def source @sources[0] end # Main/only (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 shuse( cmd ) sh = ShellCommand.new( cmd ) sh.use end # Define and run Ruby command. # # @param desc Optional description for :verbose mode. def rbrun( desc = nil, &cmd ) RubyCommand.new( desc, &cmd ).run end # Define and register Ruby command. # # @param desc Optional description for :verbose mode. def rbuse( desc = nil, &cmd ) rb = RubyCommand.new( desc, &cmd ) rb.use end # Execute commands (from block) in parallel. def fork( &blk ) host_in instance_eval &blk host_out parallel_run end # Execute commands (from block) in sequence. def walk( &blk ) host_in instance_eval &blk host_out serial_run end end # Unconditional Step. class StepAlways < Step # Update without conditions. def update? true end end # Date based Step. class StepAged < Step # Update if Step source is newer than target. def update? Order[ :force ] || date_update? end end # Mark (checksum) based Step. class StepMark < Step # Update if target generated from Step source is different # from old target in addition of being newer. def update? Order[ :force ] || mark_update? end end # Trail with name. Trail is a collection of Steps and/or Trails. class Trail < Move include MoveStyles # Trail hash. @@trails = {} # Replacement (alias) for new method. def self.form( name = nil, &blk ) self.new( name, &blk ) end # Reference Trail by name. def self.[]( name ) @@trails[ name ] end # Return list of trails. def self.list @@trails.keys end attr_reader :name # Create Trail object. Named Trails are registered and can be # referenced later. def initialize( name = nil, &blk ) super() @name = name host_in instance_eval &blk host_out if @name @@trails[ @name ] = self end end # Take trail into use. def pick( trail ) Trail[ trail ].use end # Default run style for Trail. def run serial_run end # Load a setup file. def Trail.load_setup( setup_file ) if File.exist? setup_file conf = YAML.load_file( setup_file ) 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 and command parameters, if any, # to Rubu. # # @param spec Hash of options for setup. def Trail.setup( spec ) Trail.load_setup( "#{ENV['HOME']}/.rubu.yml" ) Trail.load_setup( ENV['RUBU_CONF'] ) if ENV['RUBU_CONF'] Trail.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 trail(s). # # @param trails List of Trails to run. def Trail.run( trails = 'default' ) unless trails.kind_of? Array trails = [ trails ] end trails.each do |name| begin ret = Trail[ name ].run if ret.status == :error STDERR.puts "Rubu FAILURE..." exit false end rescue STDERR.puts "Broken trail: \"#{name}\"..." exit false end end State.save exit true end end # Serial Trail. class Walk < Trail; end # Parallel Trail. class Fork < Trail def run parallel_run end end end # Array class extension to support common Mark operations. class Array # Array version of Move#use. def use self.each do |item| item.use end end # Array version of Mark#set_opt. def set_opt( key, val ) self.each do |item| item.set_opt( key, val ) end self end # Array version of Mark#peer. def peer( rdir, ext, base = nil ) self.map do |item| item.peer( rdir, ext, base ) end end # Array version of Mark#path. def path( joiner = ' ' ) self.map{ |item| item.path }.join( joiner ) end # Array version of Mark#rpath. def rpath( joiner = ' ' ) self.map{ |item| item.rpath }.join( joiner ) end end