blob: 1ad6125b1e8c833ad65470c29af0065aa548a53c [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:
module Eclipse #:nodoc:
include Extension
class Eclipse
attr_reader :options
attr_writer :name
def initialize(project)
@project = project
@options = Options.new(project)
end
def name
return @name if @name
return @project.id.split('-').last if @options.short_names
@project.id
end
# :call-seq:
# classpath_variables :VAR => '/path/to/location'
# Sets classpath variables to be used for library path substitution
# on the project.
#
def classpath_variables(*values)
fail "eclipse.classpath_variables expects a single hash argument" if values.size > 1
if values.size == 1
fail "eclipse.classpath_variables expects a Hash argument" unless values[0].is_a? Hash
# convert keys to strings
values = values[0].inject({}) { |h, (k,v)| h[k.to_s] = @project.path_to(v); h }
@variables = values.merge(@variables || {})
end
@variables || (@project.parent ? @project.parent.eclipse.classpath_variables : default_classpath_variables)
end
def default_classpath_variables
vars = {}
vars[:SCALA_HOME] = ENV['SCALA_HOME'] if ENV['SCALA_HOME']
vars[:JAVA_HOME] = ENV['JAVA_HOME'] if ENV['JAVA_HOME']
vars
end
# :call-seq:
# natures=(natures)
# Sets the Eclipse project natures on the project.
#
def natures=(var)
@natures = arrayfy(var)
end
# :call-seq:
# natures() => [n1, n2]
# Returns the Eclipse project natures on the project.
# They may be derived from the parent project if no specific natures have been set
# on the project.
#
# An Eclipse project nature is used internally by Eclipse to determine the aspects of a project.
def natures(*values)
if values.size > 0
@natures ||= []
@natures += values.flatten
else
@natures || (@project.parent ? @project.parent.eclipse.natures : [])
end
end
# :call-seq:
# classpath_containers=(cc)
# Sets the Eclipse project classpath containers on the project.
#
def classpath_containers=(var)
@classpath_containers = arrayfy(var)
end
# :call-seq:
# classpath_containers() => [con1, con2]
# Returns the Eclipse project classpath containers on the project.
# They may be derived from the parent project if no specific classpath containers have been set
# on the project.
#
# A classpath container is an Eclipse pre-determined ensemble of dependencies made available to
# the project classpath.
def classpath_containers(*values)
if values.size > 0
@classpath_containers ||= []
@classpath_containers += values.flatten
else
@classpath_containers || (@project.parent ? @project.parent.eclipse.classpath_containers : [])
end
end
# :call-seq:
# exclude_libs() => [lib1, lib2]
# Returns the an array of libraries to be excluded from the generated Eclipse classpath
def exclude_libs(*values)
if values.size > 0
@exclude_libs ||= []
@exclude_libs += values.flatten
else
@exclude_libs || (@project.parent ? @project.parent.eclipse.exclude_libs : [])
end
end
# :call-seq:
# exclude_libs=(lib1, lib2)
# Sets libraries to be excluded from the generated Eclipse classpath
#
def exclude_libs=(libs)
@exclude_libs = arrayfy(libs)
end
# :call-seq:
# builders=(builders)
# Sets the Eclipse project builders on the project.
#
def builders=(var)
@builders = arrayfy(var)
end
# :call-seq:
# builders() => [b1, b2]
# Returns the Eclipse project builders on the project.
# They may be derived from the parent project if no specific builders have been set
# on the project.
#
# A builder is an Eclipse background job that parses the source code to produce built artifacts.
def builders(*values)
if values.size > 0
@builders ||= []
@builders += values.flatten
else
@builders || (@project.parent ? @project.parent.eclipse.builders : [])
end
end
private
def arrayfy(obj)
obj.is_a?(Array) ? obj : [obj]
end
end
class Options
attr_writer :m2_repo_var, :short_names
def initialize(project)
@project = project
end
# The classpath variable used to point at the local maven2 repository.
# Example:
# eclipse.options.m2_repo_var = 'M2_REPO'
def m2_repo_var(*values)
fail "m2_repo_var can only accept one value: #{values}" if values.size > 1
if values.size > 0
@m2_repo_var = values[0]
else
@m2_repo_var || (@project.parent ? @project.parent.eclipse.options.m2_repo_var : 'M2_REPO')
end
end
def short_names
@short_names || (@project.parent ? @project.parent.eclipse.options.short_names : false)
end
end
def eclipse
@eclipse ||= Eclipse.new(self)
@eclipse
end
first_time do
# Global task "eclipse" generates artifacts for all projects.
desc 'Generate Eclipse artifacts for all projects'
Project.local_task('eclipse'=>'artifacts') { |name| "Generating Eclipse project for #{name}" }
end
before_define do |project|
project.recursive_task('eclipse')
end
after_define(:eclipse => :package) do |project|
# Need to enhance because using project.projects during load phase of the
# buildfile has harmful side-effects on project definition order
project.enhance do
eclipse = project.task('eclipse')
# We don't create the .project and .classpath files if the project contains projects.
if project.projects.empty?
eclipse.enhance [ file(project.path_to('.classpath')), file(project.path_to('.project')) ]
# The only thing we need to look for is a change in the Buildfile.
file(project.path_to('.classpath')=>Buildr.application.buildfile) do |task|
if (project.eclipse.natures.reject { |x| x.is_a?(Symbol) }.size > 0)
info "Writing #{task.name}"
m2repo = Buildr::Repositories.instance.local
File.open(task.name, 'w') do |file|
classpathentry = ClasspathEntryWriter.new project, file
classpathentry.write do
# Note: Use the test classpath since Eclipse compiles both "main" and "test" classes using the same classpath
cp = project.test.compile.dependencies.map(&:to_s) - [ project.compile.target.to_s, project.resources.target.to_s ]
cp = cp.uniq
# Convert classpath elements into applicable Project objects
cp.collect! { |path| Buildr.projects.detect { |prj| prj.packages.detect { |pkg| pkg.to_s == path } } || path }
# Remove excluded libs
cp -= project.eclipse.exclude_libs.map(&:to_s)
# project_libs: artifacts created by other projects
project_libs, others = cp.partition { |path| path.is_a?(Project) }
# Separate artifacts under known classpath variable paths
# including artifacts located in local Maven2 repository
vars = []
project.eclipse.classpath_variables.merge(project.eclipse.options.m2_repo_var => m2repo).each do |name, path|
matching, others = others.partition { |f| File.expand_path(f.to_s).index(path) == 0 }
matching.each do |m|
vars << [m, name, path]
end
end
# Generated: Any non-file classpath elements in the project are assumed to be generated
libs, generated = others.partition { |path| File.file?(path.to_s) }
classpathentry.src project.compile.sources + generated
classpathentry.src project.resources
if project.test.compile.target
classpathentry.src project.test.compile
classpathentry.src project.test.resources
end
project.eclipse.classpath_containers.each { |container|
classpathentry.con container
}
# Classpath elements from other projects
classpathentry.src_projects project_libs
classpathentry.output project.compile.target if project.compile.target
classpathentry.lib libs
classpathentry.var vars
end
end
end
end
# The only thing we need to look for is a change in the Buildfile.
file(project.path_to('.project')=>Buildr.application.buildfile) do |task|
info "Writing #{task.name}"
File.open(task.name, 'w') do |file|
xml = Builder::XmlMarkup.new(:target=>file, :indent=>2)
xml.projectDescription do
xml.name project.eclipse.name
xml.projects
unless project.eclipse.builders.empty?
xml.buildSpec do
project.eclipse.builders.each { |builder|
xml.buildCommand do
xml.name builder
end
}
end
end
unless project.eclipse.natures.empty?
xml.natures do
project.eclipse.natures.each { |nature|
xml.nature nature unless nature.is_a? Symbol
}
end
end
end
end
end
end
end
end
# Writes 'classpathentry' tags in an xml file.
# It converts tasks to paths.
# It converts absolute paths to relative paths.
# It ignores duplicate directories.
class ClasspathEntryWriter #:nodoc:
def initialize project, target
@project = project
@xml = Builder::XmlMarkup.new(:target=>target, :indent=>2)
@excludes = [ '**/.svn/', '**/CVS/' ].join('|')
@paths_written = []
end
def write &block
@xml.classpath &block
end
def con path
@xml.classpathentry :kind=>'con', :path=>path
end
def lib libs
libs.map(&:to_s).sort.uniq.each do |path|
@xml.classpathentry :kind=>'lib', :path=>relative(path)
end
end
# Write a classpathentry of kind 'src'.
# Accept an array of absolute paths or a task.
def src arg
if [:sources, :target].all? { |message| arg.respond_to?(message) }
src_from_task arg
else
src_from_absolute_paths arg
end
end
# Write a classpathentry of kind 'src' for dependent projects.
# Accept an array of projects.
def src_projects project_libs
project_libs.map { |project| project.eclipse.name }.sort.uniq.each do |eclipse_name|
@xml.classpathentry :kind=>'src', :combineaccessrules=>'false', :path=>"/#{eclipse_name}"
end
end
def output target
@xml.classpathentry :kind=>'output', :path=>relative(target)
end
# Write a classpathentry of kind 'var' (variable) for a library in a local repo.
# * +libs+ is an array of library paths.
# * +var_name+ is a variable name as defined in Eclipse (e.g., 'M2_REPO').
# * +var_value+ is the value of this variable (e.g., '/home/me/.m2').
# E.g., <tt>var([lib1, lib2], 'M2_REPO', '/home/me/.m2/repo')</tt>
def var(libs)
libs.each do |lib_path, var_name, var_value|
lib_artifact = file(lib_path)
attribs = { :kind => 'var', :path => lib_path }
if lib_artifact.respond_to? :sources_artifact
attribs[:sourcepath] = lib_artifact.sources_artifact
end
if lib_artifact.respond_to? :javadoc_artifact
attribs[:javadocpath] = lib_artifact.javadoc_artifact
end
# make all paths relative
attribs.each_key do |k|
attribs[k] = attribs[k].to_s.sub(var_value, var_name.to_s) if k.to_s =~ /path/
end
@xml.classpathentry attribs
end
end
private
# Find a path relative to the project's root directory if possible. If the
# two paths do not share the same root the absolute path is returned. This
# can happen on Windows, for instance, when the two paths are not on the
# same drive.
def relative path
path or raise "Invalid path '#{path.inspect}'"
msg = [:to_path, :to_str, :to_s].find { |msg| path.respond_to? msg }
path = path.__send__(msg)
begin
relative = Util.relative_path(File.expand_path(path), @project.path_to)
if relative['..']
# paths don't share same root
Util.normalize_path(path)
else
relative
end
rescue ArgumentError
Util.normalize_path(path)
end
end
def src_from_task task
src_from_absolute_paths task.sources, task.target
end
def src_from_absolute_paths absolute_paths, output=nil
relative_paths = absolute_paths.map { |src| relative(src) }
relative_paths.sort.uniq.each do |path|
unless @paths_written.include?(path)
attributes = { :kind=>'src', :path=>path, :excluding=>@excludes }
attributes[:output] = relative(output) if output
@xml.classpathentry attributes
@paths_written << path
end
end
end
end
module Plugin
include Extension
NATURE = 'org.eclipse.pde.PluginNature'
CONTAINER = 'org.eclipse.pde.core.requiredPlugins'
BUILDERS = ['org.eclipse.pde.ManifestBuilder', 'org.eclipse.pde.SchemaBuilder']
after_define do |project|
eclipse = project.eclipse
# smart defaults
if eclipse.natures.empty? && (
(File.exists? project.path_to("plugin.xml")) ||
(File.exists? project.path_to("OSGI-INF")) ||
(File.exists?(project.path_to("META-INF/MANIFEST.MF")) && File.read(project.path_to("META-INF/MANIFEST.MF")).match(/^Bundle-SymbolicName:/)))
eclipse.natures = [NATURE, Buildr::Eclipse::Java::NATURE]
eclipse.classpath_containers = [CONTAINER, Buildr::Eclipse::Java::CONTAINER] if eclipse.classpath_containers.empty?
eclipse.builders = BUILDERS + [Buildr::Eclipse::Java::BUILDER] if eclipse.builders.empty?
end
# :plugin nature explicitly set
if eclipse.natures.include? :plugin
unless eclipse.natures.include? NATURE
# plugin nature must be before java nature
eclipse.natures += [Buildr::Eclipse::Java::NATURE] unless eclipse.natures.include? Buildr::Eclipse::Java::NATURE
index = eclipse.natures.index(Buildr::Eclipse::Java::NATURE) || -1
eclipse.natures = eclipse.natures.insert(index, NATURE)
end
unless eclipse.classpath_containers.include? CONTAINER
# plugin container must be before java container
index = eclipse.classpath_containers.index(Buildr::Eclipse::Java::CONTAINER) || -1
eclipse.classpath_containers = eclipse.classpath_containers.insert(index, CONTAINER)
end
unless (eclipse.builders.include?(BUILDERS[0]) && eclipse.builders.include?(BUILDERS[1]))
# plugin builder must be before java builder
index = eclipse.classpath_containers.index(Buildr::Eclipse::Java::BUILDER) || -1
eclipse.builders = eclipse.builders.insert(index, BUILDERS[1]) unless eclipse.builders.include? BUILDERS[1]
index = eclipse.classpath_containers.index(BUILDERS[1]) || -1
eclipse.builders = eclipse.builders.insert(index, BUILDERS[0]) unless eclipse.builders.include? BUILDERS[0]
end
end
end
end
module Scala
include Extension
NATURE = 'org.scala-ide.sdt.core.scalanature'
CONTAINER = 'org.scala-ide.sdt.launching.SCALA_CONTAINER'
BUILDER = 'org.scala-ide.sdt.core.scalabuilder'
after_define :eclipse => :eclipse_scala
after_define :eclipse_scala do |project|
eclipse = project.eclipse
# smart defaults
if eclipse.natures.empty? && (project.compile.language == :scala || project.test.compile.language == :scala)
eclipse.natures = [NATURE, Buildr::Eclipse::Java::NATURE]
eclipse.classpath_containers = [CONTAINER, Buildr::Eclipse::Java::CONTAINER] if eclipse.classpath_containers.empty?
eclipse.builders = BUILDER if eclipse.builders.empty?
eclipse.exclude_libs += Buildr::Scala::Scalac.dependencies
end
# :scala nature explicitly set
if eclipse.natures.include? :scala
unless eclipse.natures.include? NATURE
# scala nature must be before java nature
eclipse.natures += [Buildr::Eclipse::Java::NATURE] unless eclipse.natures.include? Buildr::Eclipse::Java::NATURE
index = eclipse.natures.index(Buildr::Eclipse::Java::NATURE) || -1
eclipse.natures = eclipse.natures.insert(index, NATURE)
end
unless eclipse.classpath_containers.include? CONTAINER
# scala container must be before java container
index = eclipse.classpath_containers.index(Buildr::Eclipse::Java::CONTAINER) || -1
eclipse.classpath_containers = eclipse.classpath_containers.insert(index, CONTAINER)
end
unless eclipse.builders.include? BUILDER
# scala builder overrides java builder
eclipse.builders -= [Buildr::Eclipse::Java::BUILDER]
eclipse.builders += [BUILDER]
end
eclipse.exclude_libs += Buildr::Scala::Scalac.dependencies
end
end
end
module Java
include Extension
NATURE = 'org.eclipse.jdt.core.javanature'
CONTAINER = 'org.eclipse.jdt.launching.JRE_CONTAINER'
BUILDER = 'org.eclipse.jdt.core.javabuilder'
after_define do |project|
eclipse = project.eclipse
# smart defaults
if project.compile.language == :java || project.test.compile.language == :java
eclipse.natures = NATURE if eclipse.natures.empty?
eclipse.classpath_containers = CONTAINER if eclipse.classpath_containers.empty?
eclipse.builders = BUILDER if eclipse.builders.empty?
end
# :java nature explicitly set
if eclipse.natures.include? :java
eclipse.natures += [NATURE] unless eclipse.natures.include? NATURE
eclipse.classpath_containers += [CONTAINER] unless eclipse.classpath_containers.include? CONTAINER
eclipse.builders += [BUILDER] unless eclipse.builders.include? BUILDER
end
end
end
end
end # module Buildr
class Buildr::Project
include Buildr::Eclipse
include Buildr::Eclipse::Plugin
include Buildr::Eclipse::Scala
include Buildr::Eclipse::Java
end