From d5f0eb3cd189da65ea1b948e11025239fd36dfa8 Mon Sep 17 00:00:00 2001 From: kares Date: Fri, 3 Jul 2026 19:43:13 +0200 Subject: [PATCH 01/11] [refactor] installer jar requirements check --- lib/jars/installer.rb | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/lib/jars/installer.rb b/lib/jars/installer.rb index d51d279..0817d1d 100644 --- a/lib/jars/installer.rb +++ b/lib/jars/installer.rb @@ -175,18 +175,15 @@ def ruby_maven_install_options=(options) end 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 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 "jar-dependencies found on non-java platform gem; skipping jar installation" - false - else - result - end + return if spec.nil? # rubocop:disable Style/ReturnNilInPredicateMethodDefinition + return false unless spec.requirements&.any? + return false unless spec.runtime_dependencies.find { |s| s.name == 'jar-dependencies' } + + return true if spec.platform.to_s == 'java' + + Jars.warn "jar-dependencies found on non-java platform gem; skipping jar installation" + nil # rubocop:disable Style/ReturnNilInPredicateMethodDefinition end private From d9ca3b10e45f9c8db102a319ea043f80500370af Mon Sep 17 00:00:00 2001 From: kares Date: Fri, 3 Jul 2026 19:51:00 +0200 Subject: [PATCH 02/11] [refactor] no String sub-class or new re-def --- lib/jars/gemspec_artifacts.rb | 51 +++++++++------------- lib/jars/mima.rb | 2 +- specs/gemspec_artifacts_spec.rb | 76 ++++++++++++++++----------------- specs/mima_spec.rb | 2 +- 4 files changed, 61 insertions(+), 70 deletions(-) diff --git a/lib/jars/gemspec_artifacts.rb b/lib/jars/gemspec_artifacts.rb index 4e84bc0..e3571b3 100644 --- a/lib/jars/gemspec_artifacts.rb +++ b/lib/jars/gemspec_artifacts.rb @@ -1,20 +1,14 @@ # frozen_string_literal: true module Jars - class MavenVersion < String + module MavenVersion class << self - def new(*args) - if args.empty? || (args.size == 1 && args[0].nil?) - nil - else - low, high = convert(args[0]) - low, high = convert(args[1], low, high) if /[=~><]/.match?(args[1]) - if low == high - low - else - super("#{low || '[0'},#{high || ')'}") - end - end + def resolve(*args) + return nil if args.empty? || (args.size == 1 && args[0].nil?) + + low, high = convert(args[0]) + low, high = convert(args[1], low, high) if /[=~><]/.match?(args[1]) + (low == high) ? low : "#{low || '[0'},#{high || ')'}" end private @@ -94,14 +88,19 @@ class Artifact ALLOWED_TYPES = %w[jar pom].freeze - def initialize(options, *args) - @type, @group_id, @artifact_id, @classifier, @version, @exclusions = *args + def initialize(type, group_id, artifact_id, classifier, version, exclusions, **options) + @type = type + @group_id = group_id + @artifact_id = artifact_id + @classifier = classifier + @version = version + @exclusions = exclusions options.each do |k, v| instance_variable_set(:"@#{k}", v) end end - def self.new(line) + def self.parse(line) line = line.strip index = line.index(/\s/) return nil if index.nil? @@ -151,7 +150,7 @@ def self.new(line) warn line return nil end - super(options, type, group_id, artifact_id, classifier, version, exclusions) + new(type, group_id, artifact_id, classifier, version, exclusions, **options) end def to_s @@ -169,18 +168,11 @@ def to_gacv args.join(':') end - def to_coord_no_classifier - args = [@group_id, @artifact_id] - args << @type - args << MavenVersion.new(@version) - args.join(':') - end - - def to_coord + def to_coord(classifier: true) args = [@group_id, @artifact_id] - args << @classifier if @classifier + args << @classifier if classifier && @classifier args << @type - args << MavenVersion.new(@version) + args << MavenVersion.resolve(@version) args.join(':') end @@ -197,9 +189,8 @@ def initialize(spec) @artifacts = [] spec.requirements.each do |req| req.split("\n").each do |line| - if (a = Artifact.new(line)) - @artifacts << a - end + artifact = Artifact.parse(line) + @artifacts << artifact if artifact end end @artifacts.freeze diff --git a/lib/jars/mima.rb b/lib/jars/mima.rb index aeb8c34..6245b77 100644 --- a/lib/jars/mima.rb +++ b/lib/jars/mima.rb @@ -251,7 +251,7 @@ def artifacts_to_dependencies(artifacts, all_dependencies: false) # @param artifact [Jars::GemspecArtifacts::Artifact] # @return [org.eclipse.aether.artifact.DefaultArtifact] def build_aether_artifact(artifact) - version = Jars::MavenVersion.new(artifact.version) || artifact.version + version = Jars::MavenVersion.resolve(artifact.version) || artifact.version if artifact.classifier org.eclipse.aether.artifact.DefaultArtifact.new( artifact.group_id, artifact.artifact_id, artifact.classifier, diff --git a/specs/gemspec_artifacts_spec.rb b/specs/gemspec_artifacts_spec.rb index f2ba2b3..2035e90 100644 --- a/specs/gemspec_artifacts_spec.rb +++ b/specs/gemspec_artifacts_spec.rb @@ -56,140 +56,140 @@ describe Jars::GemspecArtifacts::Artifact do it 'ignore unknow type' do - _(Jars::GemspecArtifacts::Artifact.new('bla')).must_be_nil - _(Jars::GemspecArtifacts::Artifact.new(' _(bla')).must_be_nil - _(Jars::GemspecArtifacts::Artifact.new('bla bla _(bla')).must_be_nil + _(Jars::GemspecArtifacts::Artifact.parse('bla')).must_be_nil + _(Jars::GemspecArtifacts::Artifact.parse(' _(bla')).must_be_nil + _(Jars::GemspecArtifacts::Artifact.parse('bla bla _(bla')).must_be_nil end %i[jar pom].each do |type| it "parse and to_s of simple GAV #{type}" do expected = "#{type} g:a, 1" - a = Jars::GemspecArtifacts::Artifact.new(expected) + a = Jars::GemspecArtifacts::Artifact.parse(expected) _(a.to_s).must_equal expected line = "#{type} 'g:a', '1'" - a = Jars::GemspecArtifacts::Artifact.new(line) + a = Jars::GemspecArtifacts::Artifact.parse(line) _(a.to_s).must_equal expected line = "#{type} g,a,1" - a = Jars::GemspecArtifacts::Artifact.new(line) + a = Jars::GemspecArtifacts::Artifact.parse(line) _(a.to_s).must_equal expected line = "#{type} 'g',\"a\", 1" - a = Jars::GemspecArtifacts::Artifact.new(line) + a = Jars::GemspecArtifacts::Artifact.parse(line) _(a.to_s).must_equal expected line = "#{type} g:a:1" - a = Jars::GemspecArtifacts::Artifact.new(line) + a = Jars::GemspecArtifacts::Artifact.parse(line) _(a.to_s).must_equal expected line = "#{type} 'g:a:1'" - a = Jars::GemspecArtifacts::Artifact.new(line) + a = Jars::GemspecArtifacts::Artifact.parse(line) _(a.to_s).must_equal expected end it "parse and to_s of simple GAV #{type} with range" do expected = "#{type} g:a, [1, 2)" - a = Jars::GemspecArtifacts::Artifact.new(expected) + a = Jars::GemspecArtifacts::Artifact.parse(expected) _(a.to_s).must_equal expected line = "#{type} g:a:[1, 2)" - a = Jars::GemspecArtifacts::Artifact.new(line) + a = Jars::GemspecArtifacts::Artifact.parse(line) _(a.to_s).must_equal expected line = "#{type} g:a,'[1, 2)'" - a = Jars::GemspecArtifacts::Artifact.new(line) + a = Jars::GemspecArtifacts::Artifact.parse(line) _(a.to_s).must_equal expected line = "#{type} 'g', \"a\", ' [1, 2) '" - a = Jars::GemspecArtifacts::Artifact.new(line) + a = Jars::GemspecArtifacts::Artifact.parse(line) _(a.to_s).must_equal expected end it "parse and to_s of simple GAV #{type} with one exclusion" do expected = "#{type} g:a, 1, [a:b]" - a = Jars::GemspecArtifacts::Artifact.new(expected) + a = Jars::GemspecArtifacts::Artifact.parse(expected) _(a.to_s).must_equal expected line = "#{type} 'g:a', '1', '[a:b]'" - a = Jars::GemspecArtifacts::Artifact.new(line) + a = Jars::GemspecArtifacts::Artifact.parse(line) _(a.to_s).must_equal expected line = "#{type} 'g:a', '1', ['a:b']" - a = Jars::GemspecArtifacts::Artifact.new(line) + a = Jars::GemspecArtifacts::Artifact.parse(line) _(a.to_s).must_equal expected line = "#{type} g,a,1,[a:b]" - a = Jars::GemspecArtifacts::Artifact.new(line) + a = Jars::GemspecArtifacts::Artifact.parse(line) _(a.to_s).must_equal expected end it "parse and to_s of simple GAV #{type} with exclusions" do expected = "#{type} g:a, 1, [a:b, c:d]" - a = Jars::GemspecArtifacts::Artifact.new(expected) + a = Jars::GemspecArtifacts::Artifact.parse(expected) _(a.to_s).must_equal expected line = "#{type} 'g:a', '1', ['a:b', 'c:d']'" - a = Jars::GemspecArtifacts::Artifact.new(line) + a = Jars::GemspecArtifacts::Artifact.parse(line) _(a.to_s).must_equal expected line = "#{type} g,a,1,[a:b,c:d]" - a = Jars::GemspecArtifacts::Artifact.new(line) + a = Jars::GemspecArtifacts::Artifact.parse(line) _(a.to_s).must_equal expected end it "parse and to_s of simple GAV #{type} with exclusions and range" do expected = "#{type} g:a, (1, 2], [a:b, c:d]" - a = Jars::GemspecArtifacts::Artifact.new(expected) + a = Jars::GemspecArtifacts::Artifact.parse(expected) _(a.to_s).must_equal expected line = "#{type} 'g:a', '(1,2]', ['a:b', 'c:d']'" - a = Jars::GemspecArtifacts::Artifact.new(line) + a = Jars::GemspecArtifacts::Artifact.parse(line) _(a.to_s).must_equal expected line = "#{type} g,a,(1,2],[a:b,c:d]" - a = Jars::GemspecArtifacts::Artifact.new(line) + a = Jars::GemspecArtifacts::Artifact.parse(line) _(a.to_s).must_equal expected line = "#{type} g,a,(1,2], :exclusions : [a:b,c:d]" - a = Jars::GemspecArtifacts::Artifact.new(line) + a = Jars::GemspecArtifacts::Artifact.parse(line) _(a.to_s).must_equal expected end it "parse and to_s of simple GACV #{type}" do expected = "#{type} g:a, c, 1" - a = Jars::GemspecArtifacts::Artifact.new(expected) + a = Jars::GemspecArtifacts::Artifact.parse(expected) _(a.to_s).must_equal expected line = "#{type} 'g:a', 'c', '1'" - a = Jars::GemspecArtifacts::Artifact.new(line) + a = Jars::GemspecArtifacts::Artifact.parse(line) _(a.to_s).must_equal expected line = "#{type} g,a,\"c\",1" - a = Jars::GemspecArtifacts::Artifact.new(line) + a = Jars::GemspecArtifacts::Artifact.parse(line) _(a.to_s).must_equal expected line = "#{type} g,a, 1 ,:classifier : c" - a = Jars::GemspecArtifacts::Artifact.new(line) + a = Jars::GemspecArtifacts::Artifact.parse(line) _(a.to_s).must_equal expected end it "parse and to_s of simple GACV #{type} with range" do expected = "#{type} g:a, c, [1, 2)" - a = Jars::GemspecArtifacts::Artifact.new(expected) + a = Jars::GemspecArtifacts::Artifact.parse(expected) _(a.to_s).must_equal expected _(a.scope).must_be_nil line = "#{type} g:a:c:[1, 2),:scope=>:runtime" - a = Jars::GemspecArtifacts::Artifact.new(line) + a = Jars::GemspecArtifacts::Artifact.parse(line) _(a.to_s).must_equal expected _(a.scope).must_equal 'runtime' line = "#{type} g:a:c,'[1, 2)'" - a = Jars::GemspecArtifacts::Artifact.new(line) + a = Jars::GemspecArtifacts::Artifact.parse(line) _(a.to_s).must_equal expected line = "#{type} 'g', \"a\", 'c', ' [1, 2) '" - a = Jars::GemspecArtifacts::Artifact.new(line) + a = Jars::GemspecArtifacts::Artifact.parse(line) _(a.to_s).must_equal expected line = "#{type} g,a,[1, 2),:classifier => c" - a = Jars::GemspecArtifacts::Artifact.new(line) + a = Jars::GemspecArtifacts::Artifact.parse(line) _(a.to_s).must_equal expected end it "parse and to_s of simple GACV #{type} with exclusions and range" do expected = "#{type} g:a, c, (1, 2], [a:b, c:d]" - a = Jars::GemspecArtifacts::Artifact.new(expected) + a = Jars::GemspecArtifacts::Artifact.parse(expected) _(a.to_s).must_equal expected _(a.scope).must_be_nil line = "#{type} 'g:a:c', '(1,2]', ['a:b', 'c:d']'" - a = Jars::GemspecArtifacts::Artifact.new(line) + a = Jars::GemspecArtifacts::Artifact.parse(line) _(a.to_s).must_equal expected _(a.scope).must_be_nil line = "#{type} g,a,c,(1,2],[a:b,c:d], :scope => :compile" - a = Jars::GemspecArtifacts::Artifact.new(line) + a = Jars::GemspecArtifacts::Artifact.parse(line) _(a.to_s).must_equal expected _(a.scope).must_equal 'compile' line = "#{type} g,a,(1,2],:classifier => c,:exclusions => [a:b,c:d]" - a = Jars::GemspecArtifacts::Artifact.new(line) + a = Jars::GemspecArtifacts::Artifact.parse(line) _(a.to_s).must_equal expected end end @@ -230,7 +230,7 @@ _(artifacts[5].exclusions).must_be_nil artifacts.each do |a| - _(a.to_s).must_equal Jars::GemspecArtifacts::Artifact.new(a.to_s).to_s + _(a.to_s).must_equal Jars::GemspecArtifacts::Artifact.parse(a.to_s).to_s end _(artifacts.size).must_equal 7 diff --git a/specs/mima_spec.rb b/specs/mima_spec.rb index 1c92fd6..e8809bd 100644 --- a/specs/mima_spec.rb +++ b/specs/mima_spec.rb @@ -28,7 +28,7 @@ ENV['JARS_DEBUG'] = 'true' $stderr = StringIO.new - artifact = Jars::GemspecArtifacts::Artifact.new('jar org.example:example, 1.0') + artifact = Jars::GemspecArtifacts::Artifact.parse('jar org.example:example, 1.0') begin Jars::Mima.resolve_artifacts([ artifact ]) rescue org.eclipse.aether.resolution.DependencyResolutionException From fb482330183db0ee42edc3a220929cf554636a2c Mon Sep 17 00:00:00 2001 From: kares Date: Fri, 3 Jul 2026 19:51:22 +0200 Subject: [PATCH 03/11] [test] cleanup extra comment noise --- specs/jar_installer_spec.rb | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/specs/jar_installer_spec.rb b/specs/jar_installer_spec.rb index d522142..5040afa 100644 --- a/specs/jar_installer_spec.rb +++ b/specs/jar_installer_spec.rb @@ -60,10 +60,8 @@ def do_install(vendor, write) it 'just skips install_jars and vendor_jars if there are no requirements' do jar = Jars::Installer.new jar.install_jars - # vendor method is a mocked method - _(jar.vendor).must_be_nil + _(jar.vendor).must_be_nil # vendor method is a mocked method jar.vendor_jars - # vendor method is a mocked method _(jar.vendor).must_be_nil end @@ -72,30 +70,24 @@ def do_install(vendor, write) spec.platform = 'ruby' jar = Jars::Installer.new(spec) jar.install_jars - # vendor method is a mocked method - _(jar.vendor).must_be_nil + _(jar.vendor).must_be_nil # vendor method is a mocked method jar.vendor_jars - # vendor method is a mocked method - _(jar.vendor).must_be_nil + _(jar.vendor).must_be_nil # vendor method is a mocked method end it 'does install_jars and vendor_jars' do ENV['JARS_VENDOR'] = nil jar = Jars::Installer.new(example_spec) jar.install_jars - # vendor method is a mocked method - assert_nil jar.vendor + assert_nil jar.vendor # vendor method is a mocked method ENV['JARS_VENDOR'] = 'false' jar.vendor_jars - # vendor method is a mocked method assert_nil jar.vendor ENV['JARS_VENDOR'] = 'true' jar.vendor_jars - # vendor method is a mocked method _(jar.vendor).must_equal 'lib' java.lang.System.set_property('jars.vendor', 'false') jar.vendor_jars - # vendor method is a mocked method assert_nil jar.vendor end From f0c58b2de150d34bffda519d947844abc4205c32 Mon Sep 17 00:00:00 2001 From: kares Date: Sat, 4 Jul 2026 19:24:01 +0200 Subject: [PATCH 04/11] only suppress conflict warnings for locked jarse (#46) --- lib/jar_dependencies.rb | 34 ++++++++++++++++++++++++---------- specs/jars_spec.rb | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/lib/jar_dependencies.rb b/lib/jar_dependencies.rb index f8cc5fa..b1ba9e2 100644 --- a/lib/jar_dependencies.rb +++ b/lib/jar_dependencies.rb @@ -55,8 +55,9 @@ module Jars autoload :MavenSettings, 'jars/maven_settings' autoload :Mima, 'jars/mima' - @jars_lock = false @jars = {} + @jars_lock = false + @jars_locked = nil class JarLoadError < LoadError; end @@ -119,10 +120,15 @@ def quiet? @quiet end + # @deprecated def no_more_warnings @quiet = true end + def locked_jar?(coordinate) + @jars_locked&.include?(coordinate) + end + def jarfile ENV['JARFILE'] || ENV_JAVA['jarfile'] || ENV['JBUNDLER_JARFILE'] || ENV_JAVA['jbundler.jarfile'] || 'Jarfile' end @@ -177,6 +183,7 @@ def reset end Jars::MavenSettings.reset @jars = {} + @jars_locked = nil end def maven_local_settings @@ -209,6 +216,7 @@ def home def require_jars_lock!(scope = :runtime) urls = jars_lock_from_class_loader if to_prop(LOCK).nil? + pre_lock_jars = @jars.keys.dup if urls && !urls.empty? @jars_lock = true @@ -223,15 +231,19 @@ def require_jars_lock!(scope = :runtime) done << url end end - no_more_warnings elsif (jars_lock = Jars.lock_path) Jars.debug { "--- load jars from #{jars_lock}" } @jars_lock = jars_lock classpath = Jars::Classpath.new(nil, jars_lock) classpath.require(scope) - no_more_warnings end + + return unless @jars_lock + + @jars_locked = (@jars.keys - pre_lock_jars).freeze + return if @jars_locked.empty? + Jars.debug do loaded = @jars.collect { |k, v| "#{k}:#{v}" } "--- loaded jars ---\n\t#{loaded.join("\n\t")}" @@ -306,9 +318,8 @@ def require_jar_with_block(group_id, artifact_id, *classifier_version) version = classifier_version[-1] classifier = classifier_version[-2] - coordinate = +"#{group_id}:#{artifact_id}" - coordinate << ":#{classifier}" if classifier - if @jars.key? coordinate + coordinate = "#{group_id}:#{artifact_id}#{":#{classifier}" if classifier}" + if @jars.key?(coordinate) if @jars[coordinate] == version false else @@ -373,11 +384,14 @@ def require_jar(*args, &block) return unless Jars.require? result = Jars.require_jar(*args, &block) - if result.is_a? String + if result.is_a?(String) args << (yield || Jars::UNKNOWN) if args.size == 2 && block - Jars.warn do - "jar conflict: #{args[0..-2].join(':')} already loaded with version #{result}; " \ - "skipping requested version #{args[-1]}" + coordinate = args[0..-2].join(':') + unless Jars.locked_jar?(coordinate) + Jars.warn do + "jar conflict: #{coordinate} already loaded with version #{result}; " \ + "skipping requested version #{args[-1]}" + end end Jars.debug("\n\t#{caller.join("\n\t")}") if Jars.debug? return false diff --git a/specs/jars_spec.rb b/specs/jars_spec.rb index 3514ded..d0fc595 100644 --- a/specs/jars_spec.rb +++ b/specs/jars_spec.rb @@ -364,4 +364,37 @@ def fake.lock_down(_vendor_dir = nil, **_kwargs) Jars.setup jars_lock: 'Jars_no_jline.lock' _(Jars.lock).must_equal 'Jars_no_jline.lock' end + + it 'only suppresses version conflict warnings for jars in the lockfile' do + lock_file = File.join(Dir.pwd, 'Jars_test_46.lock') + File.write(lock_file, "org.slf4j:slf4j-simple:1.6.6:compile:\n") + ENV['JARS_HOME'] = File.join('specs', 'repo') + ENV['JARS_LOCK'] = lock_file + Jars.instance_variable_set(:@jars_lock, false) + Jars.reset + + # trigger lock file loading + $stderr = StringIO.new + require_jar('org.slf4j', 'slf4j-simple', '1.6.6') + + _($stderr.string).must_equal '' + assert_nil Jars.quiet? + # verify only the lock jar is tracked + _(Jars.locked_jar?('org.slf4j:slf4j-simple')).must_equal true + _(Jars.locked_jar?('some:other')).must_equal false + + require 'jopenssl/version' + # verify conflict for a lock jar is suppressed + require_jar('org.slf4j', 'slf4j-simple', '1.7.4') + _($stderr.string).must_equal '' + require_jar('org.bouncycastle', 'bcpkix-jdk18on', JOpenSSL::BOUNCY_CASTLE_VERSION) + require_jar('org.bouncycastle', 'bcpkix-jdk18on', '1.42') + # warn still happens for non-lock jars + _($stderr.string).must_match 'jar conflict: org.bouncycastle:bcpkix-jdk18on already loaded' + ensure + $stderr = STDERR + FileUtils.rm_rf(lock_file) if lock_file + ENV['JARS_HOME'] = nil + ENV['JARS_LOCK'] = nil + end end From ca856da84c1e814ce378bdb62e5ca55f8fe4d9da Mon Sep 17 00:00:00 2001 From: kares Date: Mon, 6 Jul 2026 10:11:47 +0200 Subject: [PATCH 05/11] [refactor] Dependency.new to Dependency.parse --- lib/jars/installer.rb | 78 ++++++++++++++++++++-------------------- specs/dependency_spec.rb | 26 +++++++------- 2 files changed, 51 insertions(+), 53 deletions(-) diff --git a/lib/jars/installer.rb b/lib/jars/installer.rb index 0817d1d..cf48f0e 100644 --- a/lib/jars/installer.rb +++ b/lib/jars/installer.rb @@ -6,23 +6,36 @@ module Jars class Installer class Dependency - attr_reader :path, :file, :gav, :scope, :type, :coord + REG = /:jar:|:pom:|:test:|:compile:|:runtime:|:provided:|:system:/.freeze + EMPTY = "" - def self.new(line) - super if /:jar:|:pom:/.match?(line) - end + def self.parse(line) + return unless /:jar:|:pom:/.match?(line) + + line = line.dup + # remove ANSI escape sequences and module section (https://issues.apache.org/jira/browse/MDEP-974) + line.gsub!(/\e\[\d*m/, EMPTY) + line.gsub!(/ -- module.*/, EMPTY) + line.strip! - def setup_type(line) if line.index(':pom:') - @type = :pom + type = :pom elsif line.index(':jar:') - @type = :jar + type = :jar end - end - private :setup_type - def setup_scope(line) - @scope = + coord = line.sub(/:[^:]+:([A-Z]:\\)?[^:]+$/, EMPTY) + first, second = coord.split(/:#{type}:/) + group_id, artifact_id = first.split(':') + parts = group_id.split('.') + parts << artifact_id + parts << second.split(':')[-1] + file = line.slice(coord.length, line.length).sub(REG, EMPTY).strip + last = file.reverse.index(%r{\\|/}) + parts << line[-last..] + path = File.join(parts).strip + + scope = case line when /:provided:/ :provided @@ -31,34 +44,20 @@ def setup_scope(line) else :runtime end - end - private :setup_scope - REG = /:jar:|:pom:|:test:|:compile:|:runtime:|:provided:|:system:/.freeze - EMPTY = '' - def initialize(line) - # remove ANSI escape sequences and module section (https://issues.apache.org/jira/browse/MDEP-974) - line = line.gsub(/\e\[\d*m/, '') - line = line.gsub(/ -- module.*/, '') - - setup_type(line) - - line.strip! + new(type, scope, coord, file, path, !line.index(':system:').nil?) + end - @coord = line.sub(/:[^:]+:([A-Z]:\\)?[^:]+$/, EMPTY) - first, second = @coord.split(/:#{type}:/) - group_id, artifact_id = first.split(':') - parts = group_id.split('.') - parts << artifact_id - parts << second.split(':')[-1] - @file = line.slice(@coord.length, line.length).sub(REG, EMPTY).strip - last = @file.reverse.index(%r{\\|/}) - parts << line[-last..] - @path = File.join(parts).strip + attr_reader :path, :file, :gav, :scope, :type, :coord - setup_scope(line) + def initialize(type, scope, coord, file, path, system) + @type = type + @scope = scope + @coord = coord + @file = file + @path = path - @system = !line.index(':system:').nil? + @system = system @gav = @coord.sub(REG, ':') end @@ -74,7 +73,7 @@ def self.install_jars(write_require_file: false) def self.load_from_maven(file) result = [] File.read(file).each_line do |line| - dep = Dependency.new(line) + dep = Dependency.parse(line) result << dep if dep && dep.scope == :runtime end result @@ -103,12 +102,11 @@ def self.print_require_jar(file, dep, fallback: false) end COMMENT = '# this is a generated file, to avoid over-writing it just delete this comment' - def self.needs_to_write?(require_filename) - require_filename && (!File.exist?(require_filename) || File.read(require_filename).match(COMMENT)) - end def self.write_require_jars(deps, require_filename) - return unless needs_to_write?(require_filename) + return if !require_filename || ( + File.exist?(require_filename) && !File.read(require_filename).index(COMMENT) + ) FileUtils.mkdir_p(File.dirname(require_filename)) File.open(require_filename, 'w') do |f| diff --git a/specs/dependency_spec.rb b/specs/dependency_spec.rb index 9b949f2..32d901d 100644 --- a/specs/dependency_spec.rb +++ b/specs/dependency_spec.rb @@ -7,13 +7,13 @@ # rubocop:disable Layout/LineLength describe Jars::Installer::Dependency do it 'should parse dependency line only if it is jar or pom' do - assert_nil Jars::Installer::Dependency.new(+'something') - assert Jars::Installer::Dependency.new(+' org.apache.maven:maven-repository-metadata:jar:3.1.0:compile:/usr/local/repository/org/apache/maven/maven-repository-metadata/3.1.0/maven-repository-metadata-3.1.0.jar') - assert Jars::Installer::Dependency.new(+' org.apache.maven:maven-repository-metadata:pom:3.1.0:compile:/usr/local/repository/org/apache/maven/maven-repository-metadata/3.1.0/maven-repository-metadata-3.1.0.jar') + assert_nil Jars::Installer::Dependency.parse(+'something') + assert Jars::Installer::Dependency.parse(' org.apache.maven:maven-repository-metadata:jar:3.1.0:compile:/usr/local/repository/org/apache/maven/maven-repository-metadata/3.1.0/maven-repository-metadata-3.1.0.jar') + assert Jars::Installer::Dependency.parse(' org.apache.maven:maven-repository-metadata:pom:3.1.0:compile:/usr/local/repository/org/apache/maven/maven-repository-metadata/3.1.0/maven-repository-metadata-3.1.0.jar') end it 'should parse dependency line test scope' do - dep = Jars::Installer::Dependency.new(+' org.apache.maven:maven-repository-metadata:jar:3.1.0:test:/usr/local/repository/org/apache/maven/maven-repository-metadata/3.1.0/maven-repository-metadata-3.1.0.jar') + dep = Jars::Installer::Dependency.parse(' org.apache.maven:maven-repository-metadata:jar:3.1.0:test:/usr/local/repository/org/apache/maven/maven-repository-metadata/3.1.0/maven-repository-metadata-3.1.0.jar') _(dep.type).must_equal :jar _(dep.scope).must_equal :test _(dep.gav).must_equal 'org.apache.maven:maven-repository-metadata:3.1.0' @@ -23,7 +23,7 @@ end it 'should parse dependency line provided scope' do - dep = Jars::Installer::Dependency.new(+' org.apache.maven:maven-repository-metadata:jar:3.1.0:provided:/usr/local/repository/org/apache/maven/maven-repository-metadata/3.1.0/maven-repository-metadata-3.1.0.jar') + dep = Jars::Installer::Dependency.parse(' org.apache.maven:maven-repository-metadata:jar:3.1.0:provided:/usr/local/repository/org/apache/maven/maven-repository-metadata/3.1.0/maven-repository-metadata-3.1.0.jar') _(dep.type).must_equal :jar _(dep.scope).must_equal :provided _(dep.gav).must_equal 'org.apache.maven:maven-repository-metadata:3.1.0' @@ -33,7 +33,7 @@ end it 'should parse dependency line runtim scope' do - dep = Jars::Installer::Dependency.new(+' org.apache.maven:maven-repository-metadata:jar:3.1.0:compile:/usr/local/repository/org/apache/maven/maven-repository-metadata/3.1.0/maven-repository-metadata-3.1.0.jar') + dep = Jars::Installer::Dependency.parse(+' org.apache.maven:maven-repository-metadata:jar:3.1.0:compile:/usr/local/repository/org/apache/maven/maven-repository-metadata/3.1.0/maven-repository-metadata-3.1.0.jar') _(dep.type).must_equal :jar _(dep.scope).must_equal :runtime _(dep.gav).must_equal 'org.apache.maven:maven-repository-metadata:3.1.0' @@ -41,7 +41,7 @@ _(dep.path).must_equal 'org/apache/maven/maven-repository-metadata/3.1.0/maven-repository-metadata-3.1.0.jar' _(dep.file).must_equal '/usr/local/repository/org/apache/maven/maven-repository-metadata/3.1.0/maven-repository-metadata-3.1.0.jar' - dep = Jars::Installer::Dependency.new(+' org.apache.maven:maven-repository-metadata:jar:3.1.0:runtime:/usr/local/repository/org/apache/maven/maven-repository-metadata/3.1.0/maven-repository-metadata-3.1.0.jar') + dep = Jars::Installer::Dependency.parse(+' org.apache.maven:maven-repository-metadata:jar:3.1.0:runtime:/usr/local/repository/org/apache/maven/maven-repository-metadata/3.1.0/maven-repository-metadata-3.1.0.jar') _(dep.type).must_equal :jar _(dep.scope).must_equal :runtime _(dep.gav).must_equal 'org.apache.maven:maven-repository-metadata:3.1.0' @@ -51,7 +51,7 @@ end it 'should parse pom dependency' do - dep = Jars::Installer::Dependency.new(+' org.apache.maven:maven-repository-metadata:pom:3.1.0:compile:/usr/local/repository/org/apache/maven/maven-repository-metadata/3.1.0/maven-repository-metadata-3.1.0.pom') + dep = Jars::Installer::Dependency.parse(+' org.apache.maven:maven-repository-metadata:pom:3.1.0:compile:/usr/local/repository/org/apache/maven/maven-repository-metadata/3.1.0/maven-repository-metadata-3.1.0.pom') _(dep.type).must_equal :pom _(dep.scope).must_equal :runtime _(dep.gav).must_equal 'org.apache.maven:maven-repository-metadata:3.1.0' @@ -61,7 +61,7 @@ end it 'should parse dependency where artifact_id has dots' do - dep = Jars::Installer::Dependency.new(+' org.eclipse.sisu:org.eclipse.sisu.plexus:jar:0.0.0.M2a:compile:/usr/local/repository/org/eclipse/sisu/org.eclipse.sisu.plexus/0.0.0.M2a/org.eclipse.sisu.plexus-0.0.0.M2a.jar') + dep = Jars::Installer::Dependency.parse(+' org.eclipse.sisu:org.eclipse.sisu.plexus:jar:0.0.0.M2a:compile:/usr/local/repository/org/eclipse/sisu/org.eclipse.sisu.plexus/0.0.0.M2a/org.eclipse.sisu.plexus-0.0.0.M2a.jar') _(dep.type).must_equal :jar _(dep.scope).must_equal :runtime _(dep.gav).must_equal 'org.eclipse.sisu:org.eclipse.sisu.plexus:0.0.0.M2a' @@ -71,7 +71,7 @@ end it 'should parse dependency with classifier' do - dep = Jars::Installer::Dependency.new(+' org.sonatype.sisu:sisu-guice:jar:no_aop:3.1.0:compile:/usr/local/repository/org/sonatype/sisu/sisu-guice/3.1.0/sisu-guice-3.1.0-no_aop.jar') + dep = Jars::Installer::Dependency.parse(+' org.sonatype.sisu:sisu-guice:jar:no_aop:3.1.0:compile:/usr/local/repository/org/sonatype/sisu/sisu-guice/3.1.0/sisu-guice-3.1.0-no_aop.jar') _(dep.type).must_equal :jar _(dep.scope).must_equal :runtime _(dep.gav).must_equal 'org.sonatype.sisu:sisu-guice:no_aop:3.1.0' @@ -81,7 +81,7 @@ end it 'should parse dependency on windows' do - dep = Jars::Installer::Dependency.new(+' org.sonatype.sisu:sisu-guice:jar:no_aop:3.1.0:compile:C:\\Users\\Local\\repository\\org\\sonatype\\sisu\\sisu-guice\\3.1.0\\sisu-guice-3.1.0-no_aop.jar') + dep = Jars::Installer::Dependency.parse(+' org.sonatype.sisu:sisu-guice:jar:no_aop:3.1.0:compile:C:\\Users\\Local\\repository\\org\\sonatype\\sisu\\sisu-guice\\3.1.0\\sisu-guice-3.1.0-no_aop.jar') _(dep.type).must_equal :jar _(dep.scope).must_equal :runtime _(dep.gav).must_equal 'org.sonatype.sisu:sisu-guice:no_aop:3.1.0' @@ -93,7 +93,7 @@ # these next two combine every possible oddity to try to cover all combinations (classifier, windows path, ANSI and module) it 'should parse dependency with module section' do - dep = Jars::Installer::Dependency.new(+' org.eclipse.sisu:org.eclipse.sisu.plexus:jar:no_aop:0.0.0.M2a:compile:C:\\Users\\Local\\repository\\org\\sonatype\\sisu\\org.eclipse.sisu.plexus\\0.0.0.M2a\\org.eclipse.sisu.plexus-0.0.0.M2a-no_aop.jar -- module org.sonatype.sisu.sisu-guice') + dep = Jars::Installer::Dependency.parse(+' org.eclipse.sisu:org.eclipse.sisu.plexus:jar:no_aop:0.0.0.M2a:compile:C:\\Users\\Local\\repository\\org\\sonatype\\sisu\\org.eclipse.sisu.plexus\\0.0.0.M2a\\org.eclipse.sisu.plexus-0.0.0.M2a-no_aop.jar -- module org.sonatype.sisu.sisu-guice') _(dep.type).must_equal :jar _(dep.scope).must_equal :runtime _(dep.gav).must_equal 'org.eclipse.sisu:org.eclipse.sisu.plexus:no_aop:0.0.0.M2a' @@ -103,7 +103,7 @@ end it 'should parse dependency with ANSI-colored module section' do - dep = Jars::Installer::Dependency.new(+" org.eclipse.sisu:org.eclipse.sisu.plexus:jar:no_aop:0.0.0.M2a:compile:C:\\Users\\Local\\repository\\org\\sonatype\\sisu\\org.eclipse.sisu.plexus\\0.0.0.M2a\\org.eclipse.sisu.plexus-0.0.0.M2a-no_aop.jar\e[31m -- module org.sonatype.sisu.sisu-guice\e[m") + dep = Jars::Installer::Dependency.parse(+" org.eclipse.sisu:org.eclipse.sisu.plexus:jar:no_aop:0.0.0.M2a:compile:C:\\Users\\Local\\repository\\org\\sonatype\\sisu\\org.eclipse.sisu.plexus\\0.0.0.M2a\\org.eclipse.sisu.plexus-0.0.0.M2a-no_aop.jar\e[31m -- module org.sonatype.sisu.sisu-guice\e[m") _(dep.type).must_equal :jar _(dep.scope).must_equal :runtime _(dep.gav).must_equal 'org.eclipse.sisu:org.eclipse.sisu.plexus:no_aop:0.0.0.M2a' From ab46eb44f872b727dba50c564334c75d979422ca Mon Sep 17 00:00:00 2001 From: kares Date: Mon, 6 Jul 2026 19:18:54 +0200 Subject: [PATCH 06/11] proper minitest setup (including mock) --- Gemfile | 7 ++++++- jar-dependencies.gemspec | 4 +--- specs/jars_spec.rb | 7 ++++--- specs/setup.rb | 9 +-------- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/Gemfile b/Gemfile index 04be930..4685c72 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,7 @@ source 'https://gem.coop' gemspec -group :development do +group :development do gem 'rake', require: false gem 'ruby-debug', '~> 0.11', require: false @@ -13,3 +13,8 @@ group :development do gem 'rubocop-performance', require: false gem 'rubocop-rake', require: false end + +group :test do + gem 'minitest', '~> 5.27' + gem 'minitest-mock', '~> 5.27' +end diff --git a/jar-dependencies.gemspec b/jar-dependencies.gemspec index 9532d7b..7aaad94 100644 --- a/jar-dependencies.gemspec +++ b/jar-dependencies.gemspec @@ -20,7 +20,7 @@ Gem::Specification.new do |s| s.files = Dir['{lib}/**/*'] + %w[Mavenfile Rakefile Readme.md jar-dependencies.gemspec MIT-LICENSE] # explicitly require the jars - s.files += Jars::Mima::JARS.each_key.map {File.join(Jars::Mima::MIMA_DIR, _1)} + s.files += Jars::Mima::JARS.each_key.map { File.join(Jars::Mima::MIMA_DIR, _1) } s.description = <<~TEXT manage jar dependencies for gems and keep track which jar was already @@ -32,7 +32,5 @@ Gem::Specification.new do |s| s.required_ruby_version = '>= 2.6' - s.add_development_dependency 'minitest', '~> 5.10' - s.metadata['rubygems_mfa_required'] = 'true' end diff --git a/specs/jars_spec.rb b/specs/jars_spec.rb index d0fc595..9334c99 100644 --- a/specs/jars_spec.rb +++ b/specs/jars_spec.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true require File.expand_path('setup', File.dirname(__FILE__)) +require 'minitest/mock' require 'stringio' + describe Jars do before do - @env = ENV.to_h - # helpful when debugging + @env = ENV.to_h # helpful when debugging Jars.reset end @@ -122,7 +123,7 @@ Jars.reset fake = Object.new - def fake.lock_down(_vendor_dir = nil, **_kwargs) + def fake.lock_down(...) [Jars.debug?, Jars.verbose?, ENV['JARS_SKIP_LOCK']] end diff --git a/specs/setup.rb b/specs/setup.rb index 0a893aa..b4ec553 100644 --- a/specs/setup.rb +++ b/specs/setup.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true - -# single spec setup -$LOAD_PATH.unshift File.join(File.dirname(File.expand_path(__FILE__)), '../lib') +require 'bundler/setup' ENV['JARS_HOME'] = nil require 'jar_dependencies' @@ -9,9 +7,4 @@ p ENV['JARS_LOCAL_MAVEN_REPO'] = Jars.home Jars.reset -begin - require 'minitest' -rescue LoadError - # ignore -end require 'minitest/autorun' From 7300b89fda7d103d7a72898c7ab4aa77672f7321 Mon Sep 17 00:00:00 2001 From: kares Date: Mon, 6 Jul 2026 19:19:55 +0200 Subject: [PATCH 07/11] require same as bundler + enforce platform due mima the gem can not operate on non-jruby --- jar-dependencies.gemspec | 9 +++++++-- specs/setup.rb | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/jar-dependencies.gemspec b/jar-dependencies.gemspec index 7aaad94..73c2afc 100644 --- a/jar-dependencies.gemspec +++ b/jar-dependencies.gemspec @@ -1,12 +1,17 @@ # frozen_string_literal: true -require_relative 'lib/jars/version' -require_relative 'lib/jars/mima/version' +File.expand_path('lib', File.dirname(__FILE__)).tap do |lib| + $LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib) +end + +require 'jars/version' +require 'jars/mima/version' Gem::Specification.new do |s| s.name = 'jar-dependencies' s.version = Jars::VERSION + s.platform = 'java' s.author = 'christian meier' s.email = ['mkristian@web.de'] diff --git a/specs/setup.rb b/specs/setup.rb index b4ec553..917fbc1 100644 --- a/specs/setup.rb +++ b/specs/setup.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'bundler/setup' ENV['JARS_HOME'] = nil From d55483a7426b4df8255440209cd9988819a86220 Mon Sep 17 00:00:00 2001 From: kares Date: Tue, 7 Jul 2026 11:53:09 +0200 Subject: [PATCH 08/11] just print result as is with debug message --- lib/jar_dependencies.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jar_dependencies.rb b/lib/jar_dependencies.rb index b1ba9e2..5d5092e 100644 --- a/lib/jar_dependencies.rb +++ b/lib/jar_dependencies.rb @@ -396,6 +396,6 @@ def require_jar(*args, &block) Jars.debug("\n\t#{caller.join("\n\t")}") if Jars.debug? return false end - Jars.debug { "jar registration: #{args.inspect}; loaded=#{result == true}" } + Jars.debug { "jar registration: #{args.inspect}; loaded=#{result}" } result end From 55cfa93efb5e5e18d766af0180da9f27c969d1df Mon Sep 17 00:00:00 2001 From: kares Date: Tue, 7 Jul 2026 12:02:57 +0200 Subject: [PATCH 09/11] [refactor] explicit (private) method arguments --- lib/jar_dependencies.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/jar_dependencies.rb b/lib/jar_dependencies.rb index 5d5092e..b824815 100644 --- a/lib/jar_dependencies.rb +++ b/lib/jar_dependencies.rb @@ -357,8 +357,8 @@ def to_jar(group_id, artifact_id, version, classifier = nil) file end - def do_require(*args) - jar = to_jar(*args) + def do_require(group_id, artifact_id, version, classifier) + jar = to_jar(group_id, artifact_id, version, classifier) # use jar from PWD/vendor/jars if exists if File.exist?(vendor = File.join(Dir.pwd, 'vendor', 'jars', jar)) require vendor From 26d51347e216f5c932369bb99ef3df41a8326e90 Mon Sep 17 00:00:00 2001 From: kares Date: Tue, 7 Jul 2026 13:00:03 +0200 Subject: [PATCH 10/11] [feat] simple "dep resolve" progress monitor --- lib/jar_dependencies.rb | 7 +++++++ lib/jars/installer.rb | 3 ++- lib/jars/mima.rb | 9 +++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/jar_dependencies.rb b/lib/jar_dependencies.rb index b824815..61a7dbd 100644 --- a/lib/jar_dependencies.rb +++ b/lib/jar_dependencies.rb @@ -287,6 +287,13 @@ def require_jar(group_id, artifact_id, *classifier_version) end end + def info(msg = nil, newline: true) + return if quiet? + + msg = yield if msg.nil? && block_given? + Kernel.print("#{msg}#{"\n" if newline}") + end + def warn(msg = nil) return if quiet? && !debug? diff --git a/lib/jars/installer.rb b/lib/jars/installer.rb index cf48f0e..213f628 100644 --- a/lib/jars/installer.rb +++ b/lib/jars/installer.rb @@ -208,8 +208,9 @@ def do_install(vendor_dir, write_require_file) def install_dependencies deps = File.join(@mvn.basedir, 'deps.lst') - puts " jar dependencies for #{spec.spec_name} . . ." unless Jars.quiet? + Jars.info("jar dependencies for #{spec.spec_name} ", newline: false) @mvn.resolve_dependencies_list(deps) + Jars.info("") # newline after progress dots self.class.load_from_maven(deps) ensure diff --git a/lib/jars/mima.rb b/lib/jars/mima.rb index 6245b77..5cab010 100644 --- a/lib/jars/mima.rb +++ b/lib/jars/mima.rb @@ -132,18 +132,22 @@ def aether_repository_listener when :metadataInvalid Jars.debug { "[mima] invalid metadata #{metadata_label(event.getMetadata)}" } when :artifactResolving + progress_dot Jars.debug { "[mima] resolving artifact #{artifact_label(event.getArtifact)}" } when :artifactResolved Jars.debug { resolved_message('artifact', event) } when :metadataResolving + progress_dot Jars.debug { "[mima] resolving metadata #{metadata_label(event.getMetadata)}" } when :metadataResolved Jars.debug { resolved_message('metadata', event) } when :artifactDownloading + progress_dot Jars.debug { download_message('artifact', event) } when :artifactDownloaded Jars.debug { downloaded_message('artifact', event) } when :metadataDownloading + progress_dot Jars.debug { download_message('metadata', event) } when :metadataDownloaded Jars.debug { downloaded_message('metadata', event) } @@ -168,6 +172,11 @@ def aether_transfer_listener end end + # Emits a single progress dot (no newline) unless in debug mode + def progress_dot + Jars.info('.', newline: false) unless Jars.debug? + end + def resolved_message(kind, event) message = +"[mima] resolved #{kind} #{repository_item_label(event)}" message << " -> #{event.getFile.getAbsolutePath}" if event.getFile From fcfffc30440a50c584f0720037073f2c6da5b769 Mon Sep 17 00:00:00 2001 From: kares Date: Tue, 7 Jul 2026 14:47:25 +0200 Subject: [PATCH 11/11] [build] reformat and avoid rubocop inherits --- .rubocop.yml | 8 ++++++++ Rakefile | 8 ++++---- specs/mima_spec.rb | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index b2f2ed1..85ce27e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -3,9 +3,17 @@ plugins: - rubocop-performance - rubocop-minitest +inherit_mode: + merge: + - Exclude + AllCops: TargetRubyVersion: 2.6 NewCops: enable + Exclude: + - target/**/* + - vendor/**/* + - tmp/**/* Metrics: Enabled: false diff --git a/Rakefile b/Rakefile index bf0ce86..a524882 100644 --- a/Rakefile +++ b/Rakefile @@ -48,7 +48,7 @@ task :download_jars do puts " downloading #{filename}..." URI.open(info[:url]) do |remote| # rubocop:disable Security/Open - File.open(target, 'wb') { |f| f.write(remote.read) } + File.binwrite(target, remote.read) end verify_checksum(target, info[:sha1]) puts " saved: #{target}" @@ -60,7 +60,7 @@ def verify_checksum(path, expected_sha1) return if actual == expected_sha1 File.delete(path) - raise "SHA-1 mismatch for #{path}:\n" \ - " expected: #{expected_sha1}\n" \ - " actual: #{actual}" + raise "SHA-1 mismatch for #{path}:\n " \ + "expected: #{expected_sha1}\n " \ + "actual: #{actual}" end diff --git a/specs/mima_spec.rb b/specs/mima_spec.rb index e8809bd..f875e6d 100644 --- a/specs/mima_spec.rb +++ b/specs/mima_spec.rb @@ -30,7 +30,7 @@ artifact = Jars::GemspecArtifacts::Artifact.parse('jar org.example:example, 1.0') begin - Jars::Mima.resolve_artifacts([ artifact ]) + Jars::Mima.resolve_artifacts([artifact]) rescue org.eclipse.aether.resolution.DependencyResolutionException # expected end