| # 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 |