rakelib/rbspec.rb
author Jan Vrany <jan.vrany@fit.cvut.cz>
Fri, 21 Sep 2018 23:14:41 +0100
changeset 255 6d6880749905
parent 140 f5b73114d0f1
child 268 da6c28844912
permissions -rw-r--r--
Add `tmp` to `.hgignore`

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_name


    def initialize(name = nil, parent = nil)
      @name = name
      @parent = parent
      @properties = {}
    end

    def name(name = NO_VALUE)
      @name = name unless name == NO_VALUE
      @name
    end


    def group(*properties, &builder)
      g = Group.new('<group>', self)
      g._build(properties, &builder)
      g
    end

    def _build(properties, &builder)
      if properties and properties.size > 0
        properties.last.each {|name, value| self._set_property(name, caller(4).first, value)}
      end
      unless builder.nil?
        case builder.arity
          when -1
            self.instance_exec &builder
          when 0
            self.instance_exec &builder
          when 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_name

    def self.named?(name)
      PROJECTS.include?(name) ? PROJECTS[name] : nil
    end

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

    def self.current
      defined? @@Current ? @@Current : (@@Current = nil)
      @@Current
    end

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

    property :app_name
    property :app_version

    def initialize(name)
      super(name)
      @imports = []
      @packages = []
      @application = nil
      @tasks = []
      @last_repository_name = nil
    end

    def clone
      p = super
      p.repositories = @repositories.clone
      p.packages = @packages.clone
      p
    end

    def packages_and_application
      app = self.application
      app ? self.packages + [app] : self.packages
    end

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

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

        #merge packages
        begin
          importee_last_repository_name = @importee.last_repository_name
          @importee.last_repository_name = @last_repository_name
          p.packages.each do |pkg|
            @importee.package?(pkg.name)? @importee.package(pkg.name).merge(pkg) : @importee.packages.push(pkg.clone)
            pkg = @importee.package(pkg.name)
            pkg.set_property(:repository, @last_repository_name) if !pkg.repository and @last_repository_name
          end
          app = p.application
          if app
            if @importee.application
              (@importee.application.parent == @importee) ? @importee.application.merge(app) : (@importee.application = app.merge(@importee.application))
            else
              @importee.application = app
            end
          end
        ensure
          @importee.last_repository_name = importee_last_repository_name
        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 {|import| import.apply}
    end

    def repository(name)
      raise Exception.new('Defining repository within package is no longer supported') if block_given?
      @last_repository_name = name
    end

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

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

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

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

    def accept_visitor(visitor)
      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!
      unless @builder.nil?
        case @builder.arity
          when -1
            @builder.call
          when 0
            @builder.call
          when 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)
      visitor.visit_tasks(self)
    end

  end # class Tasks


  class Package < ConfigurationObject


    property :repository, :class => Symbol
    property :branch, :class => String
    property :revision, :class => String
    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, :revision => pkg.revision, :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, :revision => pkg.revision, :package => pkg, :separator => pkg._separator
    end), :class => Proc

    property :stc_flags, :default => '+optinline +optinline2 -optContext', :class => String

    def _separator
      (Repository::find(self.repository)).separator
    end

    def name_components
      @name_components = self.name.sub(':', '/').split('/') unless @name_components
      @name_components
    end

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

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

    def project_definition_file_name
      "#{self.directory.gsub(File::SEPARATOR, '_')}.st"
    end

    def dll_name_without_suffix
      # Sigh, a special hack for stx:librun which is NOT
      # libstx_librun.so/fll but just librun.so / librun.dll      
      # and for STC which  produces no .so / .dll
      case self.name
        when 'stx:stc'
          nil
        when 'stx:librun'
          'librun'
        else
          "lib#{self.directory.gsub(File::SEPARATOR, '_')}"
      end
    end

    def dll_name
      case
        when self.name == 'stx:stc'
          nil
        when win32?
          "#{dll_name_without_suffix}.dll"
        when unix?
          "#{dll_name_without_suffix}.so"
        else
          error_unsupported_platform
      end
    end

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

    def test_package?
      ((self.name.end_with? 'tests') or (self.name.end_with? 'test'))
    end

    def parent_package
      return nil unless 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
      nil
    end

    def application?
      false
    end

    def accept_visitor(visitor)
      visitor.visit_package(self)
    end


    def prereqs(mandatory: false, referenced: false, root: nil)
      raise Exception.new(%q{Nor mandatory nor referenced prereqs asked. Must specify either `mandatory: true` or `referenced: true` or both}) unless (mandatory or referenced)

      # Special hack for stx:stc and stx:librun which are actually 
      # not a smalltalk packages. 
      return [] if @name == 'stx:stc'
      # stx:librun depends on STC
      return ['stx:stc'] if @name == 'stx:librun'

      root = BUILD_DIR if (not root && defined? BUILD_DIR)
      raise Exception.new('BUILD_DIR not defined and no package root passed to Package::prereqs()') unless root

      source_file = root / directory / project_definition_file_name
      unless File.exist? source_file
        warn "Cannot fine project definition file for #{@name}. Container for nested packages?"
        return ['stx:libbasic']
      end

      source = ''
      File.open(source_file, 'r').each {|line| source << line}

      source.encode!('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '')
      # All packaged depends on STC - one cannot compile them without it!
      prereqs = ['stx:stc']
      prereqs += source.scan(/^mandatoryPreRequisites(.*?)!$/m).flatten.to_s.scan(/#'([^']+)'/).flatten if mandatory
      prereqs += source.scan(/^referencedPreRequisites(.*?)!$/m).flatten.to_s.scan(/#'([^']+)'/).flatten if referenced
      prereqs.uniq
    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!!!
        return 'stx' if self.name == 'stx:projects/smalltalk'
      end
      nm == 'application' ? c[c.size - 2] : nm
    end

    def application?
      true
    end

    def accept_visitor(visitor)
      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 {|name, value| each.set_property(name, value, caller(2).first)}
      end
    end

    def method_missing(selector, *args, &block)
      e = parent.send(selector, *args, &block)
      @nested_entities.push(e)
      e
    end

    class Properties < ConfigurationObject
    end # class Group::Properties

  end # class Group

  class Repository < ConfigurationObject
    property :type, :class => Symbol

    # defines a canonical repossitory - this means, the one and only
    # repository that contains "official" code.
    property :canonical, :class => String

    # defines a so called upstream repository - this means the repository
    # where the code from staging repository goes once verified. If no
    # upstream repository is defined, canonical repository is taken
    # instead
    property :upstream, :class => String

    # defines staging repository
    property :staging, :class => String

    property :separator, :default => '.'


    # Keeps reference to all defined repositories
    @@DEFINED = {}

    def self.find(name)
      return @@DEFINED[name] || nil
    end

    def initialize(name, parent)
      super(name, parent)
      if Repository::find(name)
        raise Exception.new("Trying to redefine repository #{name}")
      end
      @@DEFINED[name] = self
    end

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

  end # class Repository

  class Visitor

    def visit(object)
      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)
      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 {@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 %q{  # nested package}
        package = pkg.parent_package
        package.nil? ? (puts %q{  # OOPS, parent package not found!}) : (puts %Q{  # parent package: #{p.name}})
      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

# Defines a new repository
def repository(name, *properties, &builder)
  r = Rake::Stx::Configuration::Repository.new(name, nil)
  r._build(properties, &builder)
  r
end

def projects
  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
    Rake::Stx::Configuration::Project.current
  else
    p = Rake::Stx::Configuration::Project.named(name)
    p._build(properties, &builder)
    p
  end
end

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