diff --git a/Readme.md b/Readme.md index 6266b7c..cab35ba 100644 --- a/Readme.md +++ b/Readme.md @@ -166,8 +166,8 @@ xalan/xerces libraries used by those gems are popular ones in the Java world. # Troubleshooting -Since maven is used under the hood it is possible to get more insight -what maven is doing. Show the regular maven output: +Jar dependency resolution uses the bundled Mima resolver. To get more +insight into jar-dependencies progress and warnings, enable verbose output: ```shell JARS_VERBOSE=true bundle install @@ -175,19 +175,14 @@ JARS_VERBOSE=true gem install some_gem ``` -Or, with maven debug enabled +For Ruby-side debug diagnostics, including backtraces for suppressed setup +errors and jar loading decisions, enable debug output: ```shell JARS_DEBUG=true bundle install JARS_DEBUG=true gem install some_gem ``` -The maven command line which gets printed needs maven-3.9.x and the -ruby DSL extension for maven: -[polyglot-maven -configuration](https://github.com/takari/polyglot-maven#configuration) where `${maven.multiModuleProjectDirectory}` is -your current directory. - # Configuration @@ -195,10 +190,10 @@ your current directory. - + - + diff --git a/lib/jar_dependencies.rb b/lib/jar_dependencies.rb index 212b6e4..7d070b5 100644 --- a/lib/jar_dependencies.rb +++ b/lib/jar_dependencies.rb @@ -22,34 +22,34 @@ # module Jars - unless defined? Jars::SKIP_LOCK - MAVEN_SETTINGS = 'JARS_MAVEN_SETTINGS' - LOCAL_MAVEN_REPO = 'JARS_LOCAL_MAVEN_REPO' - # lock file to use - LOCK = 'JARS_LOCK' - # where the locally stored jars are search for or stored - HOME = 'JARS_HOME' - # skip the gem post install hook - SKIP = 'JARS_SKIP' - # skip Jars.lock mainly to run lock_jars - SKIP_LOCK = 'JARS_SKIP_LOCK' - # do not require any jars if set to false - REQUIRE = 'JARS_REQUIRE' - # @private - NO_REQUIRE = 'JARS_NO_REQUIRE' - # no more warnings on conflict. this still requires jars but will - # not warn. it is needed to load jars from (default) gems which - # do contribute to any dependency manager (maven, gradle, jbundler) - QUIET = 'JARS_QUIET' - # show maven output - VERBOSE = 'JARS_VERBOSE' - # maven debug - DEBUG = 'JARS_DEBUG' - # vendor jars inside gem when installing gem - VENDOR = 'JARS_VENDOR' - # string used when the version is unknown - UNKNOWN = 'unknown' - end + # rubocop:disable Style/RedundantFreeze + MAVEN_SETTINGS = 'JARS_MAVEN_SETTINGS'.freeze + LOCAL_MAVEN_REPO = 'JARS_LOCAL_MAVEN_REPO'.freeze + # lock file to use + LOCK = 'JARS_LOCK'.freeze + # where the locally stored jars are search for or stored + HOME = 'JARS_HOME'.freeze + # skip the gem post install hook + SKIP = 'JARS_SKIP'.freeze + # skip Jars.lock mainly to run lock_jars + SKIP_LOCK = 'JARS_SKIP_LOCK'.freeze + # do not require any jars if set to false + REQUIRE = 'JARS_REQUIRE'.freeze + # @private + NO_REQUIRE = 'JARS_NO_REQUIRE'.freeze + # no more warnings on conflict. this still requires jars but will + # not warn. it is needed to load jars from (default) gems which + # do contribute to any dependency manager (maven, gradle, jbundler) + QUIET = 'JARS_QUIET'.freeze + # show resolver output + VERBOSE = 'JARS_VERBOSE'.freeze + # show jar-dependencies debug output + DEBUG = 'JARS_DEBUG'.freeze + # vendor jars inside gem when installing gem + VENDOR = 'JARS_VENDOR'.freeze + # string used when the version is unknown + UNKNOWN = 'unknown'.freeze + # rubocop:enable Style/RedundantFreeze autoload :Classpath, 'jars/classpath' autoload :MavenSettings, 'jars/maven_settings' @@ -58,13 +58,22 @@ module Jars @jars_lock = false @jars = {} + class JarLoadError < LoadError; end + class << self - def lock_down(debug: false, verbose: false, **kwargs) + def lock_down(debug: nil, verbose: nil, **kwargs) + previous_debug = ENV[DEBUG] + previous_verbose = ENV[VERBOSE] + previous_skip_lock = ENV[SKIP_LOCK] + ENV[DEBUG] = debug.to_s unless debug.nil? + ENV[VERBOSE] = verbose.to_s unless verbose.nil? ENV[SKIP_LOCK] = 'true' require 'jars/lock_down' # do this lazy to keep things clean Jars::LockDown.new(debug, verbose).lock_down(kwargs.delete(:vendor_dir), **kwargs) ensure - ENV[SKIP_LOCK] = nil + ENV[DEBUG] = previous_debug unless debug.nil? + ENV[VERBOSE] = previous_verbose unless verbose.nil? + ENV[SKIP_LOCK] = previous_skip_lock end if defined? JRUBY_VERSION @@ -119,7 +128,8 @@ def jarfile end def verbose? - to_boolean(VERBOSE) + verbose = to_boolean(VERBOSE) + verbose.nil? ? debug? : (verbose || !!debug?) end def debug? @@ -266,7 +276,7 @@ def require_jar(group_id, artifact_id, *classifier_version) end def warn(msg = nil) - return if (verbose? == nil && quiet?) || (verbose? == false && !debug?) + return if quiet? && !debug? Kernel.warn(msg || yield) end @@ -324,8 +334,8 @@ def detect_local_repository(settings) local_repo = nil if local_repo.empty? || !File.exist?(local_repo) local_repo rescue => e - Jars.debug(e) Jars.warn "error reading or parsing local settings from: #{settings}" + Jars.debug(e) nil end @@ -352,25 +362,26 @@ def do_require(*args) require jar end rescue LoadError => e - raise "\n\n\tyou might need to reinstall the gem which depends on the " \ - 'missing jar or in case there is Jars.lock then resolve the jars with ' \ - "`lock_jars` command\n\n#{e.message} (LoadError)" + Jars.warn "failed to load jar: #{jar} (#{e.message})" + Jars.debug(e) + raise JarLoadError, "failed to load jar: #{jar}; run `lock_jars` or reinstall the gem" end end end def require_jar(*args, &block) - return nil unless Jars.require? + return unless Jars.require? result = Jars.require_jar(*args, &block) if result.is_a? String args << (yield || Jars::UNKNOWN) if args.size == 2 && block Jars.warn do - "--- jar coordinate #{args[0..-2].join(':')} already loaded with version #{result} - omit version #{args[-1]}" + "jar conflict: #{args[0..-2].join(':')} already loaded with version #{result}; " \ + "skipping requested version #{args[-1]}" end - Jars.debug { " try to load from #{caller.join("\n\t")}" } + Jars.debug("\n\t#{caller.join("\n\t")}") if Jars.debug? return false end - Jars.debug { " register #{args.inspect} - #{result == true}" } + Jars.debug { "jar registration: #{args.inspect}; loaded=#{result == true}" } result end diff --git a/lib/jars/gemspec_artifacts.rb b/lib/jars/gemspec_artifacts.rb index 3cf5664..4e84bc0 100644 --- a/lib/jars/gemspec_artifacts.rb +++ b/lib/jars/gemspec_artifacts.rb @@ -30,7 +30,7 @@ def convert(arg, low = nil, high = nil) elsif arg.include?('<=') val = arg.sub(/<=\s*/, '') [low, "#{snapshot_version(val)}]"] - # treat '!' the same way as '>' since maven can not describe such range + # treat '!' the same way as '>' since maven cannot describe such range elsif /[!>]/.match?(arg) val = arg.sub(/[!>]\s*/, '') ["(#{snapshot_version(val)}", high] diff --git a/lib/jars/installer.rb b/lib/jars/installer.rb index 6dd6c78..d51d279 100644 --- a/lib/jars/installer.rb +++ b/lib/jars/installer.rb @@ -178,10 +178,11 @@ def jars? # first look if there are any requirements in the spec # and then if gem depends on jar-dependencies for runtime. # only then install the jars declared in the requirements - result = (spec = self.spec) && !spec.requirements.empty? && + spec = self.spec + result = spec && !spec.requirements.empty? && spec.dependencies.detect { |d| d.name == 'jar-dependencies' && d.type == :runtime } if result && spec.platform.to_s != 'java' - Jars.warn "\njar dependencies found on non-java platform gem - do not install jars\n" + Jars.warn "jar-dependencies found on non-java platform gem; skipping jar installation" false else result @@ -191,21 +192,18 @@ def jars? private def do_install(vendor_dir, write_require_file) - if !spec.require_paths.include?(vendor_dir) && vendor_dir - raise "vendor dir #{vendor_dir} not in require_paths of gemspec #{spec.require_paths}" + require_paths = spec.require_paths + if vendor_dir && !require_paths.include?(vendor_dir) + raise "vendor dir #{vendor_dir} not in require_paths of gemspec #{require_paths}" end target_dir = File.join(@mvn.basedir, vendor_dir || spec.require_path) jars_file = File.join(target_dir, "#{spec.name}_jars.rb") - # write out new jars_file it write_require_file is true or - # check timestamps: - # do not generate file if specfile is older then the generated file - if !write_require_file && - File.exist?(jars_file) && - File.mtime(@mvn.specfile) < File.mtime(jars_file) - # leave jars_file as is - jars_file = nil + # write out new jars_file if write_require_file is true or check timestamps: + # do not generate file if specfile is older than the generated file + if !write_require_file && File.exist?(jars_file) && File.mtime(@mvn.specfile) < File.mtime(jars_file) + jars_file = nil # leave jars_file as is end deps = install_dependencies self.class.write_require_jars(deps, jars_file) diff --git a/lib/jars/lock_down.rb b/lib/jars/lock_down.rb index d04317d..9711d57 100644 --- a/lib/jars/lock_down.rb +++ b/lib/jars/lock_down.rb @@ -59,14 +59,11 @@ def attach_jar_coordinates_from_bundler_dependencies(artifacts, done) end end rescue LoadError => e - if Jars.verbose? - warn e.message - warn 'no bundler found - ignore Gemfile if exists' - end + Jars.warn "bundler unavailable (#{e.message}); skipping dependency discovery" if Jars.verbose? rescue Bundler::GemfileNotFound - # do nothing then as we have bundler but no Gemfile + # bundler is available, but there is no Gemfile to inspect rescue Bundler::GemNotFound - warn "can not setup bundler with #{Bundler.default_lockfile}" + Jars.warn "cannot set up bundler with #{Bundler.default_lockfile}" raise ensure $LOAD_PATH.replace(load_path) diff --git a/lib/jars/maven_exec.rb b/lib/jars/maven_exec.rb index db82755..5758568 100644 --- a/lib/jars/maven_exec.rb +++ b/lib/jars/maven_exec.rb @@ -13,7 +13,7 @@ def find_spec(allow_no_file) when 1 specs.first else - raise 'more then one gemspec found. please specify a specfile' unless allow_no_file + raise 'more than one gemspec found; please specify a specfile' unless allow_no_file end end private :find_spec @@ -21,12 +21,10 @@ def find_spec(allow_no_file) attr_reader :basedir, :spec, :specfile def initialize(spec = nil) - @options = {} setup(spec) rescue StandardError, LoadError => e - # If spec load fails, skip looking for jar-dependencies - warn "jar-dependencies: #{e}" - warn e.backtrace.join("\n") if Jars.verbose? + Jars.warn "unable to load gemspec (#{e.message}); skipping jar dependency discovery" + Jars.debug(e) end def setup(spec = nil, allow_no_file: false) @@ -45,8 +43,7 @@ def setup(spec = nil, allow_no_file: false) @specfile = spec.loaded_from else # this happens with bundle and local gems - # there the spec_file is "not installed" but inside - # the gem_dir directory + # there spec_file is "not installed" but is inside the gem_dir directory Dir.chdir(spec.gem_dir) do setup(nil, allow_no_file: true) end @@ -54,16 +51,11 @@ def setup(spec = nil, allow_no_file: false) when nil # ignore else - Jars.debug('spec must be either String or Gem::Specification. ' \ - 'File an issue on github if you need it.') + Jars.debug "unsupported spec argument #{spec.class}; expected String or Gem::Specification" end @spec = spec end - def ruby_maven_install_options=(options) - @options = options - end - def resolve_dependencies_list(file) require 'jars/mima' diff --git a/specs/jars_spec.rb b/specs/jars_spec.rb index fac0b8a..3514ded 100644 --- a/specs/jars_spec.rb +++ b/specs/jars_spec.rb @@ -57,6 +57,85 @@ end end + it 'treats debug as verbose' do + ENV['JARS_DEBUG'] = 'true' + ENV['JARS_VERBOSE'] = nil + Jars.reset + + _(Jars.debug?).must_equal true + _(Jars.verbose?).must_equal true + end + + it 'does not treat explicit verbose false as quiet' do + ENV['JARS_VERBOSE'] = 'false' + ENV['JARS_QUIET'] = nil + Jars.reset + + $stderr = StringIO.new + Jars.warn('visible warning') + + _($stderr.string).must_equal "visible warning\n" + ensure + $stderr = STDERR + end + + it 'lets debug override quiet warnings' do + ENV['JARS_QUIET'] = 'true' + ENV['JARS_DEBUG'] = 'true' + Jars.reset + + $stderr = StringIO.new + Jars.warn('debug warning') + + _($stderr.string).must_equal "debug warning\n" + ensure + $stderr = STDERR + end + + it 'logs readable require_jar debug output' do + ENV['JARS_HOME'] = File.join('specs', 'repo') + ENV['JARS_DEBUG'] = 'true' + Jars.reset + + begin + $stderr = StringIO.new + _(require_jar('org.slf4j', 'slf4j-simple', '1.6.6')).must_equal true + _(require_jar('org.slf4j', 'slf4j-simple', '1.6.4')).must_equal false + + conflict = 'jar conflict: org.slf4j:slf4j-simple already loaded with version 1.6.6; ' \ + 'skipping requested version 1.6.4' + _($stderr.string).must_include 'jar registration: ["org.slf4j", "slf4j-simple", "1.6.6"]; loaded=true' + _($stderr.string).must_include conflict + _($stderr.string).must_match(%r{\n\t.*/?specs/jars_spec\.rb}) + ensure + $stderr = STDERR + ENV['JARS_HOME'] = nil + end + end + + it 'applies lock_down debug and verbose options while preserving environment' do + require 'jars/lock_down' + + ENV['JARS_DEBUG'] = nil + ENV['JARS_VERBOSE'] = nil + ENV['JARS_SKIP_LOCK'] = 'false' + Jars.reset + + fake = Object.new + def fake.lock_down(_vendor_dir = nil, **_kwargs) + [Jars.debug?, Jars.verbose?, ENV['JARS_SKIP_LOCK']] + end + + result = Jars::LockDown.stub(:new, fake) do + Jars.lock_down(debug: true, verbose: false) + end + + _(result).must_equal [true, true, 'true'] + _(ENV['JARS_DEBUG']).must_be_nil + _(ENV['JARS_VERBOSE']).must_be_nil + _(ENV['JARS_SKIP_LOCK']).must_equal 'false' + end + it 'extract maven settings' do settings = Jars.maven_settings # likely nil on CI @@ -108,11 +187,18 @@ ENV['JARS_LOCAL_MAVEN_REPO'] = nil end - it 'raises RuntimeError on requires of unknown group-id' do - _ { require_jar('org.something', 'slf4j-simple', '1.6.6') }.must_raise RuntimeError + it 'raises a concise error on requires of unknown group-id' do + $stderr = StringIO.new + error = _ { require_jar('org.something', 'slf4j-simple', '1.6.6') }.must_raise Jars::JarLoadError + + _(error).must_be_kind_of LoadError + _(error.message).must_equal 'failed to load jar: org/something/slf4j-simple/1.6.6/slf4j-simple-1.6.6.jar; ' \ + 'run `lock_jars` or reinstall the gem' + _($stderr.string).must_include 'failed to load jar: org/something/slf4j-simple/1.6.6/slf4j-simple-1.6.6.jar' + ensure + $stderr = STDERR end - # rubocop:disable Layout/LineLength it 'does not require jar but sets version to unknown' do ENV['JARS_HOME'] = File.join('specs', 'repo') Jars.reset @@ -123,7 +209,9 @@ $stderr = StringIO.new _(require_jar('org.slf4j', 'slf4j-simple') { '1.6.6' }).must_equal false - _($stderr.string).must_equal "--- jar coordinate org.slf4j:slf4j-simple already loaded with version unknown - omit version 1.6.6\n" + expected = 'jar conflict: org.slf4j:slf4j-simple already loaded with version unknown; ' \ + "skipping requested version 1.6.6\n" + _($stderr.string).must_equal expected ensure $stderr = STDERR ENV['JARS_HOME'] = nil @@ -142,15 +230,21 @@ $stderr = StringIO.new _(require_jar('org.slf4j', 'slf4j-simple', '1.6.4')).must_equal false - _($stderr.string).must_equal "--- jar coordinate org.slf4j:slf4j-simple already loaded with version 1.6.6 - omit version 1.6.4\n" + expected = 'jar conflict: org.slf4j:slf4j-simple already loaded with version 1.6.6; ' \ + "skipping requested version 1.6.4\n" + _($stderr.string).must_equal expected $stderr = StringIO.new _(require_jar('org.slf4j', 'slf4j-simple') { '1.6.4' }).must_equal false - _($stderr.string).must_equal "--- jar coordinate org.slf4j:slf4j-simple already loaded with version 1.6.6 - omit version 1.6.4\n" + expected = 'jar conflict: org.slf4j:slf4j-simple already loaded with version 1.6.6; ' \ + "skipping requested version 1.6.4\n" + _($stderr.string).must_equal expected $stderr = StringIO.new _(require_jar('org.slf4j', 'slf4j-simple') { nil }).must_equal false - _($stderr.string).must_equal "--- jar coordinate org.slf4j:slf4j-simple already loaded with version 1.6.6 - omit version unknown\n" + expected = 'jar conflict: org.slf4j:slf4j-simple already loaded with version 1.6.6; ' \ + "skipping requested version unknown\n" + _($stderr.string).must_equal expected ensure $stderr = STDERR ENV['JARS_HOME'] = nil @@ -165,7 +259,7 @@ begin $stderr = StringIO.new - _ { require_jar('org.slf4j', 'slf4j-simple', '1.6.6') }.must_raise RuntimeError + _ { require_jar('org.slf4j', 'slf4j-simple', '1.6.6') }.must_raise Jars::JarLoadError $stderr.flush @@ -174,14 +268,14 @@ _(require_jar('org.slf4j', 'slf4j-simple', '1.6.6')).must_equal false - _($stderr.string).must_equal "--- jar coordinate org.slf4j:slf4j-simple already loaded with version 1.6.4 - omit version 1.6.6\n" + expected = 'jar conflict: org.slf4j:slf4j-simple already loaded with version 1.6.4; ' \ + "skipping requested version 1.6.6\n" + _($stderr.string).must_equal expected ensure $stderr = STDERR ENV['JARS_HOME'] = nil end end - # rubocop:enable Layout/LineLength - it 'freezes jar loading unless jar is not loaded yet' do size = $CLASSPATH.length @@ -240,16 +334,6 @@ $stderr = STDERR end - it 'no warnings on reload' do - $stderr = StringIO.new - - load File.expand_path('lib/jar_dependencies.rb') - - _($stderr.string).must_equal '' - ensure - $stderr = STDERR - end - it 'requires jars from various default places' do pwd = File.expand_path(__dir__) $LOAD_PATH << File.join(pwd, 'path') @@ -262,10 +346,10 @@ require_jar 'more', 'sample', '3' end - _($stderr.string).wont_match(/omit version 1/) - _($stderr.string).must_match(/omit version 2/) - _($stderr.string).must_match(/omit version 3/) - _($stderr.string).wont_match(/omit version 4/) + _($stderr.string).wont_match(/skipping requested version 1/) + _($stderr.string).must_match(/skipping requested version 2/) + _($stderr.string).must_match(/skipping requested version 3/) + _($stderr.string).wont_match(/skipping requested version 4/) ensure $stderr = STDERR
ENVjava system propertydefaultdescription
JARS_DEBUGjars.debugfalseif set to true it will produce lots of debug out (maven -X switch)JARS_DEBUGjars.debugfalseif set to true it will produce Ruby-side debug diagnostics and implies verbose output
JARS_VERBOSEjars.verbosefalseif set to true it will produce some extra outputJARS_VERBOSEjars.verbosefalseif set to true it will produce some extra resolver output
JARS_HOMEjars.home$HOME/.m2/repositoryfilesystem location where to store the jar files and some metadata