blob: 3aec671a52dd16e6d687788762f5a6f941d8ea08 [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with this
# work for additional information regarding copyright ownership. The ASF
# licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
module Buildr #:nodoc:
# The underlying compiler used by CompileTask.
# To add a new compiler, extend Compiler::Base and add your compiler using:
# Buildr::Compiler.add MyCompiler
module Compiler
class << self
# Returns true if the specified compiler exists.
def has?(name)
compilers.any? { |compiler| compiler.to_sym == name.to_sym }
end
# Select a compiler by its name.
def select(name)
compilers.detect { |compiler| compiler.to_sym == name.to_sym }
end
# Adds a compiler to the list of supported compiler.
#
# For example:
# Buildr::Compiler << Buildr::Javac
def add(compiler)
@compilers ||= []
@compilers |= [compiler]
end
alias :<< :add
# Returns a list of available compilers.
def compilers
@compilers ||= []
end
private
# Only used by our specs.
def compilers=(compilers)
@compilers = compilers
end
end
# Base class for all compilers, with common functionality. Extend and over-ride as you see fit
# (see Javac as an example).
class Base #:nodoc:
class << self
# The compiler's identifier (e.g. :javac). Inferred from the class name.
def to_sym
@symbol ||= name.split('::').last.downcase.to_sym
end
# The compiled language (e.g. :java).
attr_reader :language
# Source directories to use if none were specified (e.g. 'java'). Defaults to #language.
attr_reader :sources
# Extension for source files (e.g. 'java'). Defaults to language.
attr_reader :source_ext
# The target path (e.g. 'classes')
attr_reader :target
# Extension for target files (e.g. 'class').
attr_reader :target_ext
# The default packaging type (e.g. :jar).
attr_reader :packaging
# Returns true if this compiler applies to any source code found in the listed source
# directories. For example, Javac returns true if any of the source directories contains
# a .java file. The default implementation looks to see if there are any files in the
# specified path with the extension #source_ext.
def applies_to?(project, task)
paths = task.sources + [sources].flatten.map { |src| Array(project.path_to(:source, task.usage, src.to_sym)) }
paths.flatten!
paths.each { |path|
Find.find(path) {|found|
if (!File.directory?(found)) && found.match(/.*\.#{Array(source_ext).join('|')}/)
return true
end
} if File.exist? path.to_s
}
false
end
# Implementations can use this method to specify various compiler attributes.
# For example:
# specify :language=>:java, :target=>'classes', :target_ext=>'class', :packaging=>:jar
def specify(attrs)
attrs[:sources] ||= attrs[:language].to_s
attrs[:source_ext] ||= attrs[:language].to_s
attrs.each { |name, value| instance_variable_set("@#{name}", value) }
end
# Returns additional dependencies required by this language. For example, since the
# test framework picks on these, you can use the JUnit framework with Scala.
# Defaults to obtaining a list of artifact specifications from the REQUIRES constant.
def dependencies
[]
end
end
# Construct a new compiler with the specified options. Note that options may
# change before the compiler is run.
def initialize(project, options)
@project = project
@options = options
end
# Options for this compiler.
attr_reader :options
# Determines if the compiler needs to run by checking if the target files exist,
# and if any source files or dependencies are newer than corresponding target files.
def needed?(sources, target, dependencies)
map = compile_map(sources, target)
return false if map.empty?
return true unless File.exist?(target.to_s)
source_files_not_yet_compiled = map.select { |source, target| !File.exist?(target) }.to_a
trace "Compile needed because source file #{source_files_not_yet_compiled[0][0]} has no corresponding #{source_files_not_yet_compiled[0][1]}" unless source_files_not_yet_compiled.empty?
return true if map.any? { |source, target| !File.exist?(target) || File.stat(source).mtime > File.stat(target).mtime }
oldest = map.map { |source, target| File.stat(target).mtime }.min
return dependencies.any? { |path| file(path).timestamp > oldest }
end
# Compile all files lists in sources (files and directories) into target using the
# specified dependencies.
def compile(sources, target, dependencies)
raise 'Not implemented'
end
# Returns additional dependencies required by this language. For example, since the
# test framework picks on these, you can use the JUnit framework with Scala.
def dependencies
self.class.dependencies
end
protected
# Use this to complain about CompileTask options not supported by this compiler.
#
# For example:
# def compile(files, task)
# check_options task, OPTIONS
# . . .
# end
def check_options(options, *supported)
unsupported = options.to_hash.keys - supported.flatten
raise ArgumentError, "No such option: #{unsupported.join(' ')}" unless unsupported.empty?
end
# Expands a list of source directories/files into a list of files that have the #source_ext extension.
def files_from_sources(sources)
ext_glob = Array(self.class.source_ext).join(',')
sources.flatten.map { |source| File.directory?(source) ? FileList["#{source}/**/*.{#{ext_glob}}"] : source }.
flatten.reject { |file| File.directory?(file) }.map { |file| File.expand_path(file) }.uniq
end
# The compile map is a hash that associates source files with target files based
# on a list of source directories and target directory. The compile task uses this
# to determine if there are source files to compile, and which source files to compile.
# The default method maps all files in the source directories with #source_ext into
# paths in the target directory with #target_ext (e.g. 'source/foo.java'=>'target/foo.class').
def compile_map(sources, target)
target_ext = self.class.target_ext
ext_glob = Array(self.class.source_ext).join(',')
sources.flatten.map{|f| File.expand_path(f)}.inject({}) do |map, source|
if File.directory?(source)
FileList["#{source}/**/*.{#{ext_glob}}"].reject { |file| File.directory?(file) }.
each { |file| map[file] = File.join(target, Util.relative_path(file, source).ext(target_ext)) }
else
# try to extract package name from .java or .scala files
if %w(.java .scala .groovy).include? File.extname(source)
package = findFirst(source, /^\s*package\s+([^\s;]+)\s*;?\s*/)
map[source] = package ? File.join(target, package[1].gsub('.', '/'), File.basename(source).ext(target_ext)) : target
elsif
map[source] = target
end
end
map
end
end
private
def findFirst(file, pattern)
match = nil
File.open(file, "r") do |infile|
while (line = infile.gets)
match = line.match(pattern)
break if match
end
end
match
end
end
end
# Compile task.
#
# Attempts to determine which compiler to use based on the project layout, for example,
# uses the Javac compiler if it finds any .java files in src/main/java. You can also
# select the compiler explicitly:
# compile.using(:scalac)
#
# Accepts multiple source directories that are invoked as prerequisites before compilation.
# You can pass a task as a source directory:
# compile.from(apt)
#
# Likewise, dependencies are invoked before compiling. All dependencies are evaluated as
# #artifacts, so you can pass artifact specifications and even projects:
# compile.with('module1.jar', 'log4j:log4j:jar:1.0', project('foo'))
#
# Creates a file task for the target directory, so executing that task as a dependency will
# execute the compile task first.
#
# Compiler options are inherited form a parent task, e.g. the foo:bar:compile task inherits
# its options from the foo:compile task. Even if foo is an empty project that does not compile
# any classes itself, you can use it to set compile options for all its sub-projects.
#
# Normally, the project will take care of setting the source and target directory, and you
# only need to set options and dependencies. See Project#compile.
class CompileTask < Rake::Task
def initialize(*args) #:nodoc:
super
parent_task = Project.parent_task(name)
inherit = lambda { |hash, key| parent_task.options[key] } if parent_task.respond_to?(:options)
@options = OpenObject.new &inherit
@sources = FileList[]
@dependencies = FileList[]
enhance do |task|
unless sources.empty?
raise 'No compiler selected and can\'t determine which compiler to use' unless compiler
raise 'No target directory specified' unless target
mkpath target.to_s
info "Compiling #{task.name.gsub(/:[^:]*$/, '')} into #{target.to_s}"
@compiler.compile(sources.map(&:to_s), target.to_s, dependencies.map(&:to_s))
# By touching the target we let other tasks know we did something,
# and also prevent recompiling again for dependencies.
touch target.to_s
end
end
end
# Source directories.
attr_accessor :sources
# :call-seq:
# from(*sources) => self
#
# Adds source directories and files to compile, and returns self.
#
# For example:
# compile.from('src/java').into('classes').with('module1.jar')
def from(*sources)
@sources |= sources.flatten
guess_compiler if @compiler.nil? && sources.flatten.any? { |source| File.exist?(source.to_s) }
self
end
# Compilation dependencies.
attr_accessor :dependencies
# :call-seq:
# with(*artifacts) => self
#
# Adds files and artifacts as dependencies, and returns self.
#
# Calls #artifacts on the arguments, so you can pass artifact specifications,
# tasks, projects, etc. Use this rather than setting the dependencies array directly.
#
# For example:
# compile.with('module1.jar', 'log4j:log4j:jar:1.0', project('foo'))
def with(*specs)
@dependencies |= Buildr.artifacts(specs.flatten).uniq
self
end
# The target directory for the compiled code.
attr_reader :target
# :call-seq:
# into(path) => self
#
# Sets the target directory and returns self. This will also set the compile task
# as a prerequisite to a file task on the target directory.
#
# For example:
# compile(src_dir).into(target_dir).with(artifacts)
# Both compile.invoke and file(target_dir).invoke will compile the source files.
def into(path)
@target = file(path.to_s).enhance([self]) unless @target.to_s == path.to_s
self
end
# Returns the compiler options.
attr_reader :options
# :call-seq:
# using(options) => self
#
# Sets the compiler options from a hash and returns self. Can also be used to
# select the compiler.
#
# For example:
# compile.using(:warnings=>true, :source=>'1.5')
# compile.using(:scala)
def using(*args)
args.pop.each { |key, value| options.send "#{key}=", value } if Hash === args.last
self.compiler = args.pop until args.empty?
self
end
# Returns the compiler if known. The compiler is either automatically selected
# based on existing source directories (e.g. src/main/java), or by requesting
# a specific compiler (see #using).
def compiler
guess_compiler unless @compiler
@compiler && @compiler.class.to_sym
end
# Returns the compiled language, if known. See also #compiler.
def language
compiler && @compiler.class.language
end
# Returns the default packaging type for this compiler, if known.
def packaging
compiler && @compiler.class.packaging
end
def timestamp #:nodoc:
# If we compiled successfully, then the target directory reflects that.
# If we didn't, see needed?
target ? target.timestamp : Rake::EARLY
end
# The project this task belongs to.
attr_reader :project
# The usage, one of :main or :test.
attr_reader :usage
protected
# Selects which compiler to use.
def compiler=(name) #:nodoc:
cls = Compiler.select(name) or raise ArgumentError, "No #{name} compiler available. Did you install it?"
return self if cls === @compiler
@compiler = cls.new(project, options)
from Array(cls.sources).map { |path| project.path_to(:source, usage, path) }.
select { |path| File.exist?(path) } if sources.empty?
into project.path_to(:target, usage, cls.target) unless target
with Array(@compiler.dependencies)
self
end
# Associates this task with project and particular usage (:main, :test).
def associate_with(project, usage) #:nodoc:
@project, @usage = project, usage
guess_compiler
end
# Try to guess if we have a compiler to match source files.
def guess_compiler #:nodoc:
candidate = Compiler.compilers.detect { |cls| cls.applies_to?(project, self) }
self.compiler = candidate if candidate
end
private
def needed? #:nodoc:
return false if sources.empty?
# Fail during invoke.
return true unless @compiler && target
return @compiler.needed?(sources.map(&:to_s), target.to_s, dependencies.map(&:to_s))
end
def invoke_prerequisites(args, chain) #:nodoc:
@sources = Array(@sources).map(&:to_s).uniq
@dependencies = FileList[@dependencies.uniq]
@prerequisites |= @dependencies + @sources
super
end
end
# The resources task is executed by the compile task to copy resource files over
# to the target directory. You can enhance this task in the normal way, but mostly
# you will use the task's filter.
#
# For example:
# resources.filter.using 'Copyright'=>'Acme Inc, 2007'
class ResourcesTask < Rake::Task
# Returns the filter used to copy resources over. See Buildr::Filter.
attr_reader :filter
def initialize(*args) #:nodoc:
super
@filter = Buildr::Filter.new
@filter.using Buildr.settings.profile['filter'] if Hash === Buildr.settings.profile['filter']
enhance do
target.invoke if target
end
end
# :call-seq:
# include(*files) => self
#
# Includes the specified files in the filter and returns self.
def include(*files)
filter.include *files
self
end
# :call-seq:
# exclude(*files) => self
#
# Excludes the specified files in the filter and returns self.
def exclude(*files)
filter.exclude *files
self
end
# :call-seq:
# from(*sources) => self
#
# Adds additional directories from which to copy resources.
#
# For example:
# resources.from _('src/etc')
def from(*sources)
filter.from *sources
self
end
# Returns the list of source directories (each being a file task).
def sources
filter.sources
end
# :call-seq:
# target => task
#
# Returns the filter's target directory as a file task.
def target
filter.into @project.path_to(:target, @usage, :resources) unless filter.target || sources.empty?
filter.target
end
def prerequisites #:nodoc:
super + filter.sources.flatten
end
protected
# Associates this task with project and particular usage (:main, :test).
def associate_with(project, usage) #:nodoc:
@project, @usage = project, usage
end
end
# Methods added to Project for compiling, handling of resources and generating source documentation.
module Compile
include Extension
first_time do
desc 'Compile all projects'
Project.local_task('compile') { |name| "Compiling #{name}" }
end
before_define(:compile) do |project|
resources = ResourcesTask.define_task('resources')
resources.send :associate_with, project, :main
project.path_to(:source, :main, :resources).tap { |dir| resources.from dir if File.exist?(dir) }
compile = CompileTask.define_task('compile'=>resources)
compile.send :associate_with, project, :main
project.recursive_task('compile')
end
after_define(:compile) do |project|
if project.compile.target
# This comes last because the target path is set inside the project definition.
project.build project.compile.target
project.clean do
rm_rf project.compile.target.to_s, :verbose=>false
end
end
end
# :call-seq:
# compile(*sources) => CompileTask
# compile(*sources) { |task| .. } => CompileTask
#
# The compile task does what its name suggests. This method returns the project's
# CompileTask. It also accepts a list of source directories and files to compile
# (equivalent to calling CompileTask#from on the task), and a block for any
# post-compilation work.
#
# The compile task attempts to guess which compiler to use. For example, if it finds
# any Java files in the src/main/java directory, it will use the Java compiler and
# create class files in the target/classes directory.
#
# You can also configure it yourself by telling it which compiler to use, pointing
# it as source directories and chooing a different target directory.
#
# For example:
# # Include Log4J and the api sub-project artifacts.
# compile.with 'log4j:log4j:jar:1.2', project('api')
# # Include Apt-generated source files.
# compile.from apt
# # For JavaC, force target compatibility.
# compile.options.source = '1.6'
# # Run the OpenJPA bytecode enhancer after compilation.
# compile { open_jpa_enhance }
# # Pick a given compiler.
# compile.using(:scalac).from('src/scala')
#
# For more information, see CompileTask.
def compile(*sources, &block)
task('compile').from(sources).enhance &block
end
# :call-seq:
# resources(*prereqs) => ResourcesTask
# resources(*prereqs) { |task| .. } => ResourcesTask
#
# The resources task is executed by the compile task to copy resources files
# from the resource directory into the target directory. By default the resources
# task copies files from the src/main/resources into the target/resources directory.
#
# This method returns the project's resources task. It also accepts a list of
# prerequisites and a block, used to enhance the resources task.
#
# Resources files are copied and filtered (see Buildr::Filter for more information).
# The default filter uses the profile properties for the current environment.
#
# For example:
# resources.from _('src/etc')
# resources.filter.using 'Copyright'=>'Acme Inc, 2007'
#
# Or in your profiles.yaml file:
# common:
# Copyright: Acme Inc, 2007
def resources(*prereqs, &block)
task('resources').enhance prereqs, &block
end
end
class Options
# Returns the debug option (environment variable DEBUG).
def debug
(ENV['DEBUG'] || ENV['debug']) !~ /(no|off|false)/
end
# Sets the debug option (environment variable DEBUG).
#
# You can turn this option off directly, or by setting the environment variable
# DEBUG to +no+. For example:
# buildr build DEBUG=no
#
# The release tasks runs a build with <tt>DEBUG=no</tt>.
def debug=(flag)
ENV['debug'] = nil
ENV['DEBUG'] = flag.to_s
end
end
end
class Buildr::Project
include Buildr::Compile
end