rakelib/rbspec.rb
author Jan Vrany <jan.vrany@fit.cvut.cz>
Sun, 22 May 2016 00:32:07 +0100
changeset 0 f46260ba26b1
child 27 8b1c02403638
permissions -rw-r--r--
Initial shot of "new" rake-based builder Based on SVN https://swing.fit.cvut.cz/svn/stx/goodies/builder/trunk/rake@592

require 'rake'
require 'tsort'
require "#{File.dirname(__FILE__)}/dsl"

module Rake
end

module Rake::DSL
end

module Rake::Stx  
end

module Rake::Stx::Configuration

  def self.read_specs_from_dir(dir)
    if File.exist? dir
      info "Reading project specs from #{dir}..."
      d = Dir.new(dir)
      d.each do | file | 
        if file[0] != ?. and file =~ /.*\.rbspec$/ 
          begin
            info "  #{file}"
            $:.push(dir)
            load dir / file            
          ensure
            $:.pop()
          end          
        end    
      end

      d.each do | file | 
        if File.directory? File.join(dir, file)
          read_specs_from_dir(File.join(dir, file)) if file[0] != ?.
        end
      end        
    end
  end

  def self.read_specs(project_dirs_spec)

    project_dirs = ['specs' , 
                    ENV['HOME'] / '.smalltalk' / 'builder'
                   ]


    if win32?
      project_dirs += project_dirs_spec.split(";")
    else
      project_dirs += project_dirs_spec.split(":")
    end
    project_dirs.each do | dir |
      read_specs_from_dir(dir)
    end
  end



  class ConfigurationException < Exception 
  end
             
  class ConfigurationObject < Rake::StX::DSL::Object

    NO_VALUE = Object.new()
    attr_accessor  :parent
   attr_reader :last_repository

    

    def initialize(name = nil, parent = nil)
      @name = name
      @parent = parent
      @properties = {}
    end
    
    def name(name = NO_VALUE) 
      if (name != NO_VALUE)
        @name = name
      end
      return @name
    end


    def group(*properties, &builder)
      g = Group.new("<group>", self)
      g._build(properties, &builder)
      return g
    end
    
    def _build(properties, &builder)
      if (properties and properties.size() > 0) 
        properties.last.each do | name, value |        
          self._set_property(name, caller(4).first, value )
        end
      end
      if (builder != nil)
        if (builder.arity == -1)
          self.instance_exec &builder
        elsif (builder.arity == 0)
          self.instance_exec &builder          
        elsif (builder.arity == 1) 
          self.instance_exec(self,&builder)
        else
          raise ConfigurationException.new("invalid arity of builder block (0 or 1 arguments expected, #{builder.arity} given")
        end
      end
    end          


    def accept_visitor(visitor)
      raise Exception.new("Subclass responsibility (#{self.class.name}#accept_visitor")
    end

  end # class ConfigurationObject

  class Project < ConfigurationObject

    include Rake::DSL

    PROJECTS = {}

    attr_accessor :repositories, :packages, :tasks, :application
    attr_accessor :last_repository

    def self.named?(name)
      if (PROJECTS.include?(name)) 
        p = PROJECTS[name]     
      else
        p = nil
      end
      return p
    end
    
    def self.named(name)
      if (PROJECTS.include?(name)) 
        p = PROJECTS[name]     
      else
        p = self.new(name)
        PROJECTS[name] = p
      end
      return p
    end

    def self.current()
      if (not defined? @@Current) 
        @@Current = nil
      end
      return @@Current
    end

    def self.current=(project)
      @@Current = project
    end

    property :app_name
    property :app_version
    
    def initialize(name)
      super(name)
      @imports = []
      @repositories = []
      @packages = []
      @application = nil
      @tasks = []
      @last_repository = nil
    end

    def clone()
      p = super()
      p.repositories = @repositories.clone()
      p.packages = @packages.clone()
      return p
    end

    def packages_and_application()
      app = self.application()
      if app 
        return self.packages() + [ app ]
      else
        return self.packages()
      end
    end

    class Import
      def initialize(importee, imported)
        @importee = importee
        @imported = imported#as symbol!        
        @last_repository = importee.last_repository
      end

      def apply()
        p = @importee.class.named(@imported)
        p.apply_imports()
        @importee.merge(p)

        #merge repositories
        p.repositories.each do | repo |		  
		  @importee.repository(repo.name).merge(repo)
        end

        #merge packages
        begin
          importee_last_repository = @importee.last_repository
          @importee.last_repository = @last_repository
          p.packages.each do | pkg |
            if @importee.package? pkg.name
              @importee.package(pkg.name).merge(pkg)
            else
              @importee.packages.push(pkg.clone())
            end
            pkg = @importee.package(pkg.name)
            if (not pkg.repository and @last_repository_name)
              pkg.set_property(:repository, @last_repository_name)
            end
          end
          app = p.application 
          if app
            if @importee.application
              if @importee.application.parent == @importee
                @importee.application.merge(app)
              else
                @importee.application = app.merge(@importee.application)
              end
            else
              @importee.application = app
            end
          end
        ensure
          @importee.last_repository = importee_last_repository 
        end
            
        p_tasks = p.tasks.collect { | t | t.clone }
        @importee.tasks = p_tasks + @importee.tasks
        info "Importing #{@imported}"
        info "     into #{@importee.name}"        
      end

    end # class Import

    def import(name)
      @imports.push(Import.new(self, name))
    end
    
    def apply_imports() 
      @imports.each do | import |
        import.apply()
      end
    end

    def repository?(name) 
      if ((i = @repositories.index { | r | r.name == name }) != nil) 
        return @repositories[i]
      else
        return nil
      end
    end

    
    def repository(name, *properties, &builder)
      r = repository? name
      if (r == nil)
        r = Repository.new(name, self)        
        @repositories.push(r)
      end
      r._build(properties, &builder)
      @last_repository = r
      return r
    end

    def package?(name)
      if ((i = @packages.index { | r | r.name == name }) != nil) 
        return @packages[i]        
      else
        return nil
      end
    end

    def package(name, *properties, &builder)
      if (not (p = package?(name)))
        p = Package.new(name, self)
        @packages.push(p)
      end      
      p._build(properties, &builder)
      if (not p.repository and @last_repository)
        p.repository = @last_repository.name
      end
      return p
    end

    def application(name = NO_VALUE, *properties, &builder)
      return @application if name == NO_VALUE         
      warn "Refefining application for project #{self.name}!" if @application
      if (not (p = package?(name)))
        p = Application.new(name, self)
        @application = p
      end      
      p._build(properties, &builder)
      if (not p.repository and @last_repository)
        p.repository = @last_repository.name
      end
      return p
    end

    def tasks(*properties, &builder)
      if (properties.empty? and not builder) 
        return @tasks
      else
        t = Tasks.new()
        t._build(properties, &builder)
        @tasks.push(t)
        return t
      end
    end

    def accept_visitor(visitor)
      return visitor.visit_project(self)
    end

  end # class Configuration

  class Tasks < ConfigurationObject
    include Rake::TaskManager    

    def _build(*properties, &builder)
      super(properties) {} 
      @builder = builder
    end
    
    def define!()
      if (@builder != nil)
        if (@builder.arity == -1)
          @builder.call()
        elsif (@builder.arity == 0)
          @builder.call()
        elsif (@builder.arity == 1) 
          @builder.call(self)
        else
          raise ConfigurationException.new("invalid arity of builder block (0 or 1 arguments expected, #{builder.arity} given")
        end
      end
    end

    def accept_visitor(visitor)
      return visitor.visit_tasks(self)
    end

  end # class Tasks



  class Package < ConfigurationObject 

    
    property :repository, :class => Symbol
    property :branch
    property :link, :values => [ true, false ], :default => true
    property :test, :values => [ true, false ], :default => false
    property :coverage, :values => [ true, false ], :default => false
    property :lint, :values => [ true, false ], :default => false
  
    property :checkout, :default => (Proc.new do | pkg |
                                     info "Checking out #{pkg.name}..."
                                     checkout pkg.repository, pkg.directory, 
                                     :branch => pkg.branch, :package => pkg, :separator => pkg._separator
                                   end), :class => Proc

    property :update, :default => (Proc.new do | pkg |
                                   info "Updating #{pkg.name}..."
                                   update pkg.repository, pkg.directory, 
                                   :branch => pkg.branch, :package => pkg, :separator => pkg._separator
                                 end), :class => Proc

    property :stc_flags, :default => '+optinline +optinline2 -optContext', :class => String    
    
    def _separator()
      return (self.parent.repository(self.repository)).separator
    end
    
    def name_components()
      if not @name_components then
        @name_components = self.name.sub(":", '/').split('/')
      end
      return @name_components
    end

    def top()
      # Answers relative path to top directory
      return File.join(self.name_components().collect { | e | '..' })
    end


    def directory()
      return File.join(*self.name_components)
    end

    def dll_name_without_suffix()
      return "lib#{self.directory.gsub(File::SEPARATOR, "_")}"
    end
    
    def dll_name()
      case 
      when win32?
        return  "#{dll_name_without_suffix()}.dll"
      when unix?
        return"#{dll_name_without_suffix()}.so"
      else
        error_unsupported_platform
      end       
    end

    def nested_package?
      project.packages.each do | each |
        if self.name != each.name and self.name.start_with? each.name + '/'
          return true
        end
      end
      return false
    end

    def parent_package
      return nil if not nested_package?
      last_slash = self.name.rindex(?/)
      return nil if last_slash == nil
      parent_package_name = self.name.slice(0, last_slash)
      project.packages.each do | each |
        return each if each.name == parent_package_name
      end
      return nil
    end

    def application? 
      return false
    end

    def accept_visitor(visitor)
      return visitor.visit_package(self)
    end
    
  end # class Package

  class Application < Package

    def executable
      c = self.name_components
      nm = c.last
      if win32?
        #JV@2011-07-22: HACK for Windows, since smalltalk.bat messes the argument list!!!
        if self.name == 'stx:projects/smalltalk'
          return 'stx'
        end        
      end
      
      if nm == 'application' 
        nm = c[c.size - 2]
      end
      return nm
    end

    def application? 
      return true
    end

    def accept_visitor(visitor)
      return visitor.visit_application(self)
    end


  end # class Application


  class Group < ConfigurationObject 

    def initialize(name, parent)
      super(name, parent)
      @properties_to_apply = Properties.new('<properties>', self);
      @nested_entities = []
    end

    def properties(*properties, &builder)      
      @properties_to_apply._build(properties, &builder)
    end

    def _build(*properties, &builder)
      @properties_to_apply._build(properties)
      super([], &builder)
      @nested_entities.each do | each |
        @properties_to_apply.properties.each do | name, value |
          each.set_property(name, value, caller(2).first)
        end
      end
    end

    def method_missing(selector, *args, &block)
      e = parent.send(selector,*args, &block)
      @nested_entities.push(e)
      return e
    end
    
    class Properties < ConfigurationObject
    end # class Group::Properties
    
  end # class Group
  
  class Repository < ConfigurationObject
    property :type, :class => Symbol
    property :url
    property :separator, :default => '.'

    def accept_visitor(visitor)
      return visitor.visit_repository(self)
    end

    
  end # class Repository

class Visitor

  def visit(object)
    return object.accept_visitor(self)
  end

  def visit_project(project)
    project.repositories.each { | repo | self.visit(repo) }
    project.packages.each { | repo | self.visit(repo) }
    project.tasks.each { | repo | self.visit(repo) }
    self.visit(project.application) if project.application
  end

  def visit_tasks(tasks)
    
  end

  def visit_repository(tasks)

  end

  def visit_package(tasks)

  end

  def visit_application(app)
    return self.visit_package(app)
  end

  
end

class Printer < Visitor 

  attr_accessor :stream

  def print_property(prop, overridden = false)        
    if overridden
      puts "#   overrides #{prop.value}"
      puts "#  defined at #{prop.source}"
    else
      puts "#{prop.name} #{prop.value}"
      puts "# (defined at #{prop.source})"
    end      
    print_property(prop.overridden, true) if prop.overridden
  end

  def print_properties(obj)
    obj.properties.each do | name, prop |
      print_property(prop)
    end
  end

  def initialize()
    @indent = 0
    @stream = $stdout
  end 

  def puts(*args)
    args.each do | arg |      
      @indent.times { | i | @stream.write( "  " )}
      @stream.puts(arg)
    end    
  end

  def indent(increment = 1) 
    @indent += increment
    yield
    @indent -= increment    
  end

  def visit_repository(repo)
    puts "repository :'#{repo.name}' do"
    indent do 
      print_properties(repo)
    end
    puts "end"
  end

  def visit_package(pkg)
    puts "#{pkg.application? ? 'application' : 'package'} \"#{pkg.name}\" do"
    if pkg.nested_package?      
      puts "  # nested package" 
      p = pkg.parent_package
      if p == nil 
        puts "  # OOPS, parent package not found!"
      else
        puts "  # parent package: #{p.name}"
      end
    end
    indent do
      print_properties(pkg)
    end
    puts 'end'
  end

  def visit_project(project)
    puts "project :'#{project.name}' do"
    indent do
      print_properties(project)
      super(project)
    end
    puts "end"
  end
  
end

  
end # module Rake::StX::Configuration

def projects()
  return Rake::Stx::Configuration::Project::PROJECTS
end

def project(name = Rake::Stx::Configuration::ConfigurationObject::NO_VALUE, *properties, &builder)
  if (name == Rake::Stx::Configuration::ConfigurationObject::NO_VALUE)
    return Rake::Stx::Configuration::Project.current()
  else
    p = Rake::Stx::Configuration::Project.named(name)
    p._build(properties, &builder)
    return p
  end
end

def project!(name) 
  if (p = Rake::Stx::Configuration::Project.named?(name))
    Rake::Stx::Configuration::Project.current = p
    return p
  else
    raise Rake::Stx::Configuration::ConfigurationException.new("No such project (#{name})")
  end
end