Maven support with JRuby #1683

Closed
wants to merge 11 commits into
from
View
14 README.md
@@ -27,3 +27,17 @@ See [UPGRADING](https://github.com/carlhuda/bundler/blob/master/UPGRADING.md).
### Other questions
Feel free to chat with the Bundler core team (and many other users) on IRC in the [#bundler](irc://irc.freenode.net/bundler) channel on Freenode, or via email on the [Bundler mailing list](http://groups.google.com/group/ruby-bundler).
+
+
+### Maven Integration (JRuby only)
+This version of bundler allows maven dependencies to be included alongside your other Ruby dependencies opening up a whole new world of possibilities!
+There are two ways to add your maven dependencies:
+<ol>
+<li> mvn "repo URL (or 'default' for the default repo URL)" do
+ gem "mvn:<group_id>:<artifact_id>", "version number"
+ end</li>
+<li> gem "mvn:<group_id>:<artifact_id>", "version number", :mvn=>"repo URL (or 'default' for the default repo URL)" </li>
+</ol>
+This integration will download the right jar using maven into your *maven* repo location and simply write the necessary ruby files to require the jars from the
+right location in the maven repository. This allows for maven repos and gem repos to live side by side without duplication of binaries and ensure that dependencies
+are resolved properly as maven does that automatically.
View
4 lib/bundler.rb
@@ -76,6 +76,10 @@ class InvalidSpecSet < StandardError; end
class << self
attr_writer :ui, :bundle_path
+ def java?
+ RUBY_PLATFORM == "java"
+ end
+
def configure
@configured ||= begin
configure_gem_home_and_path
View
51 lib/bundler/dsl.rb
@@ -1,5 +1,4 @@
require 'bundler/dependency'
-
module Bundler
class Dsl
def self.evaluate(gemfile, lockfile, unlock)
@@ -45,7 +44,31 @@ def gemspec(opts = nil)
raise InvalidOption, "There are multiple gemspecs at #{path}. Please use the :name option to specify which one."
end
end
-
+
+ #START MAVEN STUFF
+
+ #Influenced from https://github.com/jkutner/bundler/blob/master/lib/bundler/dsl.rb
+ def mvn(repo, options={}, source_options={}, &blk)
+ unless Bundler.java?
+ raise InvalidOption, "mvn can only be executed in JRuby"
+ end
+
+ if (options['name'].nil? || options['version'].nil?) and !block_given?
+ raise InvalidOption, 'Must specify a dependency name+version, or block of dependencies.'
+ end
+ puts "MVN REPO=#{repo} OPTS = #{options.inspect} SOPTS = #{source_options.inspect}"
+ remotes = Array === repo ? repo : [repo]
+ local_source = source Source::Maven.new(_normalize_hash(options).merge('remotes' => remotes)), source_options, &blk
+
+ #This is when you specify the mvn information on the gem line
+ #i.e. gem ... :mvn=> ...
+ if options['name'] && options['version']
+ local_source.add_dependency(options['name'], options['version'])
+ end
+ local_source
+ end
+ #END MAVEN STUFF
+
def gem(name, *args)
if name.is_a?(Symbol)
raise GemfileError, %{You need to specify gem names as Strings. Use 'gem "#{name.to_s}"' instead.}
@@ -56,8 +79,15 @@ def gem(name, *args)
_deprecated_options(options)
_normalize_options(name, version, options)
-
- dep = Dependency.new(name, version, options)
+ #Do a custom dependency creation for Maven dependencies because
+ #maven source needs the mvn: prefixed name while everything else
+ #will use the maven_name for dependency resolution, gem require etc.
+ if options['source'] && options['source'].is_a?(Bundler::Source::Maven)
+ mvn_source = options['source']
+ dep = Dependency.new(mvn_source.maven_name(name), version, options)
+ else
+ dep = Dependency.new(name, version, options)
+ end
# if there's already a dependency with this name we try to prefer one
if current = @dependencies.find { |d| d.name == dep.name }
@@ -85,7 +115,13 @@ def gem(name, *args)
end
end
end
-
+
+ #@source is populated if gem is called from within the mvn block
+ #i.e. mvn do gem ... end
+ #need to add the dependency to the maven source
+ if !@source.nil? && @source.is_a?(Bundler::Source::Maven)
+ @source.add_dependency(name,version[0])
+ end
@dependencies << dep
end
@@ -185,7 +221,7 @@ def _normalize_hash(opts)
def _normalize_options(name, version, opts)
_normalize_hash(opts)
- invalid_keys = opts.keys - %w(group groups git github path name branch ref tag require submodules platform platforms type)
+ invalid_keys = opts.keys - %w(group groups git github path name branch ref tag require submodules platform platforms type mvn)
if invalid_keys.any?
plural = invalid_keys.size > 1
message = "You passed #{invalid_keys.map{|k| ':'+k }.join(", ")} "
@@ -215,8 +251,7 @@ def _normalize_options(name, version, opts)
github = "#{github}/#{github}" unless github.include?("/")
opts["git"] = "git://github.com/#{github}.git"
end
-
- ["git", "path"].each do |type|
+ ["git", "path","mvn"].each do |type|
if param = opts[type]
if version.first && version.first =~ /^\s*=?\s*(\d[^\s]*)\s*$/
options = opts.merge("name" => name, "version" => $1)
View
5 lib/bundler/lockfile_parser.rb
@@ -27,12 +27,13 @@ def initialize(lockfile)
TYPES = {
"GIT" => Bundler::Source::Git,
"GEM" => Bundler::Source::Rubygems,
- "PATH" => Bundler::Source::Path
+ "PATH" => Bundler::Source::Path,
+ "MAVEN" => Bundler::Source::Maven
}
def parse_source(line)
case line
- when "GIT", "GEM", "PATH"
+ when "GIT", "GEM", "PATH", "MAVEN"
@current_source = nil
@opts, @type = {}, line
when " specs:"
View
313 lib/bundler/maven_gemify2.rb
@@ -0,0 +1,313 @@
+require 'uri'
+require 'tempfile'
+require 'fileutils'
+require 'rubygems'
+require 'rubygems/builder'
+require 'rubygems/installer'
+require 'set'
+
+# Amit Nithianandan
+# ANithian-at-gmail.com 2/01/2012
+# A modified maven_gemify that relies on the underlying Maven dependency
+# plugin to generate the proper classpath. Instead of downloading and packaging
+# the jar inside the gem, make a ruby file require the jar file that already exists
+# in the existing maven repo. This is handy in cases where java and ruby projects are simultaneously
+# deployed across an organization and servers have mounted both a common maven repo AND a common gem
+# mount.
+module Gem
+
+ class Maven3NotFound < StandardError; end
+
+ #A simple sub-class of the Specification that stores the "original" bundler
+ #name of the gem. This name is not compatible with most file systems and is a pain
+ #to require (require 'mvn:something:something' is ugly compared to require 'something_something')
+ #Since this maven_gemify gets called multiple times, it's necessary to make sure that the original
+ #name is preserved. This could rather store the maven group/artifact id so as to not keep parsing the
+ #original name over and over again.
+ class MavenSpec < Gem::Specification
+ attr_reader :orig_name
+ def orig_name=(orig_name)
+ @orig_name=orig_name
+ end
+ end
+
+ module Maven
+
+ class Gemify2
+ attr_reader :repositories
+
+ #repositories should be an array of urls
+ def initialize(*repositories)
+ maven # ensure maven initialized
+ @repositories = Set.new
+ if repositories.length > 0
+ @repositories.merge([repositories].flatten)
+ end
+
+ end
+
+ def add_repository(repository_url)
+ @repositories << repository_url
+ end
+
+ @@verbose = false
+ def self.verbose?
+ @@verbose || $DEBUG
+ end
+ def verbose?
+ self.class.verbose?
+ end
+ def self.verbose=(v)
+ @@verbose = v
+ end
+
+ private
+ def self.maven_config
+ @maven_config ||= Gem.configuration["maven"] || {}
+ end
+ def maven_config; self.class.maven_config; end
+
+ def self.java_imports
+ %w(
+ org.codehaus.plexus.classworlds.ClassWorld
+ org.codehaus.plexus.DefaultContainerConfiguration
+ org.codehaus.plexus.DefaultPlexusContainer
+ org.apache.maven.Maven
+ org.apache.maven.repository.RepositorySystem
+ org.apache.maven.execution.DefaultMavenExecutionRequest
+ org.apache.maven.artifact.repository.MavenArtifactRepository
+ org.apache.maven.artifact.repository.layout.DefaultRepositoryLayout
+ org.apache.maven.artifact.repository.ArtifactRepositoryPolicy
+ javax.xml.stream.XMLStreamWriter
+ javax.xml.stream.XMLOutputFactory
+ javax.xml.stream.XMLStreamException
+ ).each {|i| java_import i }
+ end
+
+ def self.create_maven
+ require 'java' # done lazily, so we're not loading it all the time
+ bin = nil
+ if ENV['M2_HOME'] # use M2_HOME if set
+ bin = File.join(ENV['M2_HOME'], "bin")
+ else
+ ENV['PATH'].split(File::PATH_SEPARATOR).detect do |path|
+ mvn = File.join(path, "mvn")
+ if File.exists?(mvn)
+ if File.symlink?(mvn)
+ link = File.readlink(mvn)
+ if link =~ /^\// # is absolute path
+ bin = File.dirname(File.expand_path(link))
+ else # is relative path so join with dir of the maven command
+ bin = File.dirname(File.expand_path(File.join(File.dirname(mvn), link)))
+ end
+ else # is no link so just expand it
+ bin = File.expand_path(path)
+ end
+ else
+ nil
+ end
+ end
+ end
+ bin = "/usr/share/maven2/bin" if bin.nil? # OK let's try debian default
+ if File.exists?(bin)
+ @mvn = File.join(bin, "mvn")
+ if Dir.glob(File.join(bin, "..", "lib", "maven-core-3.*jar")).size == 0
+ begin
+ gem 'ruby-maven', ">=0"
+ bin = File.dirname(Gem.bin_path('ruby-maven', "rmvn"))
+ @mvn = File.join(bin, "rmvn")
+ rescue LoadError
+ bin = nil
+ end
+ end
+ else
+ bin = nil
+ end
+ raise Gem::Maven3NotFound.new("can not find maven3 installation. install ruby-maven with\n\n\tjruby -S gem install ruby-maven\n\n") if bin.nil?
+
+ warn "Using Maven install at #{bin}" if verbose?
+
+ boot = File.join(bin, "..", "boot")
+ lib = File.join(bin, "..", "lib")
+ ext = File.join(bin, "..", "ext")
+ (Dir.glob(lib + "/*jar") + Dir.glob(boot + "/*jar")).each {|path| require path }
+
+ java.lang.System.setProperty("classworlds.conf", File.join(bin, "m2.conf"))
+ java.lang.System.setProperty("maven.home", File.join(bin, ".."))
+ java_imports
+
+ class_world = ClassWorld.new("plexus.core", java.lang.Thread.currentThread().getContextClassLoader());
+ config = DefaultContainerConfiguration.new
+ config.set_class_world class_world
+ config.set_name "ruby-tools"
+ container = DefaultPlexusContainer.new(config);
+ @@execution_request_populator = container.lookup(org.apache.maven.execution.MavenExecutionRequestPopulator.java_class)
+
+ @@settings_builder = container.lookup(org.apache.maven.settings.building.SettingsBuilder.java_class )
+ container.lookup(Maven.java_class)
+ end
+
+ def self.maven
+ @maven ||= create_maven
+ end
+ def maven; self.class.maven; end
+
+ def self.temp_dir
+ @temp_dir ||=
+ begin
+ d=Dir.mktmpdir
+ at_exit {FileUtils.rm_rf(d.dup)}
+ d
+ end
+ end
+
+ def temp_dir
+ self.class.temp_dir
+ end
+
+ def execute(goals, pomFile,props = {})
+ request = DefaultMavenExecutionRequest.new
+ request.set_show_errors(true)
+
+ props.each do |k,v|
+ request.user_properties.put(k.to_s, v.to_s)
+ end
+ request.set_goals(goals)
+ request.set_logging_level 0
+ request.setPom(java.io.File.new(pomFile))
+ if verbose?
+ active_profiles = request.getActiveProfiles.collect{ |p| p.to_s }
+ puts "active profiles:\n\t[#{active_profiles.join(', ')}]"
+ puts "maven goals:"
+ request.goals.each { |g| puts "\t#{g}" }
+ puts "system properties:"
+ request.getUserProperties.map.each { |k,v| puts "\t#{k} => #{v}" }
+ puts
+ end
+ out = java.lang.System.out
+ string_io = java.io.ByteArrayOutputStream.new
+ java.lang.System.setOut(java.io.PrintStream.new(string_io))
+ result = maven.execute request
+ java.lang.System.out = out
+ has_exceptions = false
+ result.exceptions.each do |e|
+ has_exceptions = true
+ e.print_stack_trace
+ string_io.write(e.get_message.to_java_string.get_bytes)
+ end
+ raise string_io.to_s if has_exceptions
+ string_io.to_s
+ end
+
+ def writeElement(xmlWriter,element_name, text)
+ xmlWriter.writeStartElement(element_name.to_java)
+ xmlWriter.writeCharacters(text.to_java)
+ xmlWriter.writeEndElement
+ end
+
+ public
+ def maven_name(gemname)
+ self.class.mname(gemname)
+ end
+ #gemname==mvn:group_id:artifact_id
+ def self.mname(gemname)
+ gemname.gsub("mvn:","").gsub(".","_").gsub(":","_")
+ end
+
+ def get_versions(gemname)
+ []
+ end
+
+ def generate_spec(gemname, version)
+ mname = maven_name(gemname)
+ MavenSpec.new do |s|
+ s.name = mname
+ s.orig_name = gemname
+ s.date = '2010-04-28'
+ s.summary = "Hola!"
+ s.description = "A simple hello world gem"
+ s.authors = ["Nick Quaranto"]
+ s.email = 'nick@quaran.to'
+ s.homepage = 'http://rubygems.org/gems/hola'
+ s.version = version
+ s.files = "lib/#{mname}.rb"
+ end
+ end
+
+ def generate_gem(gemname, version)
+ mname = maven_name(gemname)
+ spec_file=generate_spec(gemname,version)
+ # spec_file.name=mname #So that the gem's name is correct
+ gemname=gemname.gsub("mvn:","")
+ maven_parts = gemname.split(":")
+ group_id = maven_parts[0]
+ artifact_id = maven_parts[1]
+
+ FileUtils.mkdir_p(File.join(temp_dir,"lib"))
+ #Generate a dummy POM file that we'll use to run maven against
+ #to resolve deps and generate a classpath
+ pomfile=File.join(temp_dir,"pom.xml")
+ puts "pomfile=#{pomfile}"
+ out = java.io.BufferedOutputStream.new(java.io.FileOutputStream.new(pomfile.to_java))
+ outputFactory = XMLOutputFactory.newFactory()
+ xmlStreamWriter = outputFactory.createXMLStreamWriter(out)
+ xmlStreamWriter.writeStartDocument
+ xmlStreamWriter.writeStartElement("project".to_java)
+
+ writeElement(xmlStreamWriter,"groupId","org.hokiesuns.mavengemify")
+ writeElement(xmlStreamWriter,"artifactId","mavengemify")
+ writeElement(xmlStreamWriter,"modelVersion","4.0.0")
+ writeElement(xmlStreamWriter,"version","1.0-SNAPSHOT")
+
+ #Repositories
+ if @repositories.length > 0
+ xmlStreamWriter.writeStartElement("repositories".to_java)
+ @repositories.each_with_index {|repo,i|
+ xmlStreamWriter.writeStartElement("repository".to_java)
+ writeElement(xmlStreamWriter,"id","repository_#{i}")
+ writeElement(xmlStreamWriter,"url",repo)
+ xmlStreamWriter.writeEndElement #repository
+ }
+ xmlStreamWriter.writeEndElement #repositories
+ end
+ xmlStreamWriter.writeStartElement("dependencies".to_java)
+
+ xmlStreamWriter.writeStartElement("dependency".to_java)
+ writeElement(xmlStreamWriter,"groupId",group_id)
+ writeElement(xmlStreamWriter,"artifactId",artifact_id)
+ writeElement(xmlStreamWriter,"version",version.to_s)
+
+ xmlStreamWriter.writeEndElement #dependency
+
+ xmlStreamWriter.writeEndElement #dependencies
+
+ xmlStreamWriter.writeEndElement #project
+
+ xmlStreamWriter.writeEndDocument
+ xmlStreamWriter.close
+ out.close
+
+ execute(["dependency:resolve","dependency:build-classpath"],pomfile,{"mdep.outputFile" => "cp.txt","mdep.fileSeparator"=>"/"})
+
+ ruby_file = File.new(File.join(temp_dir,"lib/#{mname}.rb"),"w")
+ cp_file = File.new(File.join(temp_dir,"cp.txt"),"r")
+ cp_line = cp_file.gets
+ cp_file.close
+ cp_entries = cp_line.split(";")
+ cp_entries.each{ |entry|
+ ruby_file.puts "require \"#{entry}\""
+ }
+ ruby_file.close
+ old_pwd = Dir.pwd
+ Dir.chdir(temp_dir)
+ gembuilder = Gem::Builder.new(spec_file)
+ gemfile=gembuilder.build
+
+ geminstaller = Gem::Installer.new(gemfile)
+ geminstaller.install
+ Dir.chdir(old_pwd)
+ end
+
+ end
+ end
+end
View
67 lib/bundler/source.rb
@@ -5,9 +5,76 @@
require "rubygems/format"
require "digest/sha1"
require "open3"
+require 'bundler/maven_gemify2' if Bundler.java?
+require 'set'
module Bundler
module Source
+
+ class Maven
+ def initialize(options = {})
+ @maven_gemify = Gem::Maven::Gemify2.new
+ @dependencies = []
+ repos = options['remotes']
+ if repos
+ repos.each {|repo|
+ @maven_gemify.add_repository(repo) unless repo == "default"
+ }
+ end
+ end
+
+ def add_repository(repo_url)
+ @maven_gemify.add_repository(repo_url)
+ end
+
+ def add_dependency(gemname, version)
+ @dependencies << [gemname,version]
+ end
+
+ def install(spec)
+ #Use maven_gemify to generate the gem here
+ Bundler.ui.info "Installing #{spec.orig_name} (#{spec.version}) "
+ @maven_gemify.generate_gem(spec.orig_name,spec.version)
+ end
+
+ def specs
+ @specs ||= download_specs
+ end
+
+ def remote!
+
+ end
+
+ def to_lock
+ out = "MAVEN\n"
+ out << @maven_gemify.repositories.map {|r| " remotes: #{r}\n" }.join
+ out << " specs:\n"
+ end
+
+ def maven_name(gemname)
+ @maven_gemify.maven_name(gemname)
+ end
+
+ def self.from_lock(options)
+ if options['remotes']
+ options['remotes']=Array(options['remotes'])
+ end
+
+ new(options)
+ end
+ private
+
+ def download_specs
+ return_specs = Index.new
+ @dependencies.each {|dep|
+ spec = @maven_gemify.generate_spec(dep[0],dep[1])
+ spec.source = self
+ spec.loaded_from = "#{Bundler.rubygems.gem_dir}/specifications/#{@maven_gemify.maven_name(spec.name)}-#{spec.version.to_s}.gemspec"
+ return_specs << spec
+ }
+ return_specs
+ end
+ end
# TODO: Refactor this class
class Rubygems
FORCE_MODERN_INDEX_LIMIT = 100 # threshold for switching back to the modern index instead of fetching every spec
View
16 spec/bundler/dsl_spec.rb
@@ -18,5 +18,21 @@
github_uri = "git://github.com/rails/rails.git"
subject.dependencies.first.source.uri.should == github_uri
end
+
+ it "should work with maven as an option" do
+ subject.gem("mvn:commons-lang:commons-lang","2.6.1",:mvn=>"default")
+ source = subject.dependencies.first.source
+ puts "SOURCE=#{source}"
+ puts "SPECS = #{source.specs.inspect}"
+ end
+
+ it "should work with maven as a block" do
+ subject.mvn("default") do
+ subject.gem("mvn:commons-lang:commons-lang","2.6.1")
+ end
+ source = subject.dependencies.first.source
+ puts "SOURCE=#{source}"
+ puts "SPECS = #{source.specs.inspect}"
+ end
end
end
View
15 spec/install/mvn_spec.rb
@@ -0,0 +1,15 @@
+require "spec_helper"
+
+describe "bundle install with maven" do
+ it "fetches gems" do
+ build_lib "foo"
+
+ install_gemfile <<-G
+ mvn "default"
+ gem 'mvn:commons-lang:commons-lang','2.3'
+ G
+
+ should_be_installed("foo 1.0")
+ end
+
+end