diff --git a/asciidoc/release-notes.adoc b/asciidoc/release-notes.adoc index d6c1a6ab..25f94d8f 100644 --- a/asciidoc/release-notes.adoc +++ b/asciidoc/release-notes.adoc @@ -56,6 +56,9 @@ Note, if you have resolved dependencies with version of Ivy prior to 2.6.0, you - IMPROVEMENT: use Apache Commons Compress for pack200 handling to avoid issues on Java 14 and later. If pack200 is needed, make sure to add Apache Commons Compress to your classpath. (IVY-1652) - FIX: improved Maven dependencyManagement matching for dependencies with a non-default type or classifier (IVY-1654) (Thanks to Mark Kittisopikul) - FIX: the `ivy:retrieve` task failed when the retrieve pattern contained some text in parentheses before the first token, for instance: `/jobs/lib (JDK 17)/[artifact].[ext]` (IVY-1660) +- FIX: when the `ivy:deliver` task is configured to replace dynamic revisions, it now replaces these revisions to the resolved revision before any conflict resolution was done, which was the original behavior before Ivy 2.3.0. +This way, the delivered ivy.xml can be used to have reproducible dependency resolution, especially when multiple configurations are used. +It also fixes issues where the dynamic revisions were replaced by versions from other configurations. (IVY-1485, IVY-1661) == Committers and Contributors diff --git a/asciidoc/use/deliver.adoc b/asciidoc/use/deliver.adoc index 411c271d..f944e9f9 100644 --- a/asciidoc/use/deliver.adoc +++ b/asciidoc/use/deliver.adoc @@ -27,7 +27,7 @@ This task generates a resolved descriptor of the current module, based upon the Moreover, all included configurations' files are included in the Ivy file, and variables are replaced by their values. -Finally, in the resolved Ivy file, dynamic revisions are replaced by the static ones that have been found during the resolve step, so the Ivy file can be used later safely to obtain the same dependencies (providing that a revision uniquely identifies a module, which should be the case for proper Ivy use). +Finally, in the resolved Ivy file, dynamic revisions are replaced by the static ones that have been found during the resolve step, so the Ivy file can be used later safely to obtain the same dependencies (providing that a revision uniquely identifies a module, which should be the case for proper Ivy use). Note that this replacement is done before any conflict resolution happens. This can cause differences between the selected versions in the delivered ivy.xml file and the ones that are actually retrieved. This should not be a problem, because if the delivered ivy.xml is used to resolve the dependencies again, the conflict resolution will select the correct versions, even if not all configurations are resolved. This way, we ensure a reproducible dependency resolution as much as possible. (*__since 1.3__*) The replacement of dynamic revisions by static ones can be turned off, so that dynamic revisions are kept in the Ivy file. This is an exception to the recommended standard that published module descriptors be fully resolved, so use it with caution. diff --git a/src/java/org/apache/ivy/core/resolve/ResolveEngine.java b/src/java/org/apache/ivy/core/resolve/ResolveEngine.java index d746bb07..75785434 100644 --- a/src/java/org/apache/ivy/core/resolve/ResolveEngine.java +++ b/src/java/org/apache/ivy/core/resolve/ResolveEngine.java @@ -22,18 +22,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.net.URL; import java.text.ParseException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.Set; +import java.util.*; import org.apache.ivy.Ivy; import org.apache.ivy.core.IvyContext; @@ -278,65 +267,51 @@ public class ResolveEngine { } IvyNode root = dependencies[0].getRoot(); - - Map topLevelDeps = new HashMap<>(); for (IvyNode dependency : dependencies) { - if (!dependency.hasProblem()) { - DependencyDescriptor dd = dependency.getDependencyDescriptor(root); - if (dd != null) { - ModuleId orgMod = dependency.getModuleId(); - topLevelDeps.put(orgMod, dependency); - } + if (dependency.hasProblem()) { + // skip dependencies with problems + continue; } - } - for (IvyNode dependency : dependencies) { - if (!dependency.hasProblem() && !dependency.isCompletelyEvicted()) { - DependencyDescriptor dd = dependency.getDependencyDescriptor(root); - if (dd == null) { - ModuleId mid = dependency.getModuleId(); - IvyNode tlDep = topLevelDeps.get(mid); - if (tlDep != null) { - dd = tlDep.getDependencyDescriptor(root); - } - } - if (dd != null) { - ModuleRevisionId depResolvedId = dependency.getResolvedId(); - ModuleDescriptor depDescriptor = dependency.getDescriptor(); - ModuleRevisionId depRevisionId = dd.getDependencyRevisionId(); - ModuleRevisionId forcedRevisionId = forcedRevisions.get(dependency - .getModuleId()); - - if (dependency.getModuleRevision() != null - && dependency.getModuleRevision().isForce() - && !depResolvedId.equals(depRevisionId) - && !settings.getVersionMatcher().isDynamic(depRevisionId)) { - // if we were forced to this revision and we - // are not a dynamic revision, reset to the - // asked revision - depResolvedId = depRevisionId; - depDescriptor = null; - } - - if (depResolvedId == null) { - throw new NullPointerException("getResolvedId() is null for " - + dependency.toString()); - } - if (depRevisionId == null) { - throw new NullPointerException("getDependencyRevisionId() " - + "is null for " + dd.toString()); - } - String rev = depResolvedId.getRevision(); - String forcedRev = forcedRevisionId == null ? rev : forcedRevisionId - .getRevision(); - - // The evicted modules have no description, so we can't put the status - String status = depDescriptor == null ? "?" : depDescriptor.getStatus(); - Message.debug("storing dependency " + depResolvedId + " in props"); - props.put(depRevisionId.encodeToString(), rev + " " + status + " " - + forcedRev + " " + depResolvedId.getBranch()); - } + DependencyDescriptor dd = dependency.getDependencyDescriptor(root); + if (dd == null) { + // we are only interested in direct dependencies + continue; } + + ModuleRevisionId depResolvedId = dependency.getResolvedId(); + ModuleDescriptor depDescriptor = dependency.getDescriptor(); + ModuleRevisionId depRevisionId = dd.getDependencyRevisionId(); + ModuleRevisionId forcedRevisionId = forcedRevisions.get(dependency.getModuleId()); + + if (dependency.getModuleRevision() != null + && dependency.getModuleRevision().isForce() + && !depResolvedId.equals(depRevisionId) + && !settings.getVersionMatcher().isDynamic(depRevisionId)) { + // if we were forced to this revision and we + // are not a dynamic revision, reset to the + // asked revision + depResolvedId = depRevisionId; + depDescriptor = null; + } + + if (depResolvedId == null) { + throw new NullPointerException("getResolvedId() is null for " + + dependency.toString()); + } + if (depRevisionId == null) { + throw new NullPointerException("getDependencyRevisionId() " + + "is null for " + dd.toString()); + } + String rev = depResolvedId.getRevision(); + String forcedRev = forcedRevisionId == null ? rev : forcedRevisionId + .getRevision(); + + // The evicted modules have no description, so we can't put the status + String status = depDescriptor == null ? "?" : depDescriptor.getStatus(); + Message.debug("storing dependency " + depResolvedId + " in props"); + props.put(depRevisionId.encodeToString(), rev + " " + status + " " + + forcedRev + " " + depResolvedId.getBranch()); } } FileOutputStream out = new FileOutputStream(ivyPropertiesInCache); diff --git a/test/java/org/apache/ivy/ant/IvyDeliverTest.java b/test/java/org/apache/ivy/ant/IvyDeliverTest.java index acb403ab..0c29614e 100644 --- a/test/java/org/apache/ivy/ant/IvyDeliverTest.java +++ b/test/java/org/apache/ivy/ant/IvyDeliverTest.java @@ -22,9 +22,7 @@ import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.InputStreamReader; -import java.util.Arrays; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; import org.apache.ivy.TestHelper; @@ -396,6 +394,133 @@ public class IvyDeliverTest { dds[0].getDependencyRevisionId()); } + /** + * Test case for IVY-1300. + * + * @throws Exception if something goes wrong + * @see IVY-1300 + */ + @Test + public void testIVY1300() throws Exception { + project.setProperty("ivy.settings.file", "test/repositories/IVY-1300/ivysettings.xml"); + project.setProperty("ivy.dep.file", "test/repositories/IVY-1300/assembly-ivy.xml"); + IvyResolve res = new IvyResolve(); + res.setValidate(false); + res.setProject(project); + res.execute(); + + deliver.setPubrevision("1"); + deliver.setDeliverpattern("build/test/deliver/ivy-[revision].xml"); + deliver.setStatus("release"); + deliver.setValidate(false); + deliver.execute(); + + // verify that the dynamic revisions have been replaced by the resolved ones (before conflict resolution) + File deliveredIvyFile = new File("build/test/deliver/ivy-1.xml"); + assertTrue(deliveredIvyFile.exists()); + ModuleDescriptor md = XmlModuleDescriptorParser.getInstance().parseDescriptor( + new IvySettings(), deliveredIvyFile.toURI().toURL(), false); + DependencyDescriptor[] dds = md.getDependencies(); + assertEquals(2, dds.length); + assertEquals(ModuleRevisionId.newInstance("myorg", "modA", "releasebranch", "1"), + dds[0].getDependencyRevisionId()); + assertEquals(ModuleRevisionId.newInstance("myorg", "modB", "releasebranch", "1"), + dds[1].getDependencyRevisionId()); + } + + /** + * Test case for IVY-1485. + * + * @see IVY-1485 + */ + @Test + public void testDeliverWithTransitiveDepsInDifferentConfs() throws Exception { + project.setProperty("ivy.settings.file", "test/repositories/IVY-1485/ivysettings.xml"); + project.setProperty("ivy.dep.file", "test/repositories/IVY-1485/ivy-simple.xml"); + IvyResolve res = new IvyResolve(); + res.setValidate(false); + res.setProject(project); + res.execute(); + + deliver.setPubrevision("1"); + deliver.setDeliverpattern("build/test/deliver/ivy-[revision].xml"); + deliver.setStatus("release"); + deliver.setValidate(false); + deliver.execute(); + + // now check that the transitive dependency didn't override the direct ones + File deliveredIvyFile = new File("build/test/deliver/ivy-1.xml"); + assertTrue(deliveredIvyFile.exists()); + ModuleDescriptor md = XmlModuleDescriptorParser.getInstance().parseDescriptor( + new IvySettings(), deliveredIvyFile.toURL(), false); + DependencyDescriptor[] dds = md.getDependencies(); + assertEquals(2, dds.length); + assertEquals(ModuleRevisionId.newInstance("simple", "modA", "5"), dds[0].getDependencyRevisionId()); + assertEquals(ModuleRevisionId.newInstance("simple", "modB", "1"), dds[1].getDependencyRevisionId()); + } + + /** + * Test case for IVY-1485. + * + * @see IVY-1485 + */ + @Test + public void testDeliverWithTransitiveDepsInOverlappingConfsTransitiveNewer() throws Exception { + project.setProperty("ivy.settings.file", "test/repositories/IVY-1485/ivysettings.xml"); + project.setProperty("ivy.dep.file", "test/repositories/IVY-1485/ivy-overlap-transitive-newer.xml"); + IvyResolve res = new IvyResolve(); + res.setValidate(false); + res.setProject(project); + res.execute(); + + deliver.setPubrevision("1"); + deliver.setDeliverpattern("build/test/deliver/ivy-[revision].xml"); + deliver.setStatus("release"); + deliver.setValidate(false); + deliver.execute(); + + // now check that the transitive dependency didn't override the direct ones + File deliveredIvyFile = new File("build/test/deliver/ivy-1.xml"); + assertTrue(deliveredIvyFile.exists()); + ModuleDescriptor md = XmlModuleDescriptorParser.getInstance().parseDescriptor( + new IvySettings(), deliveredIvyFile.toURL(), false); + DependencyDescriptor[] dds = md.getDependencies(); + assertEquals(2, dds.length); + assertEquals(ModuleRevisionId.newInstance("overlap-newer", "modA", "1"), dds[0].getDependencyRevisionId()); + assertEquals(ModuleRevisionId.newInstance("overlap-newer", "modB", "1"), dds[1].getDependencyRevisionId()); + } + + /** + * Test case for IVY-1485. + * + * @see IVY-1485 + */ + @Test + public void testDeliverWithTransitiveDepsInOverlappingConfsTransitiveOlder() throws Exception { + project.setProperty("ivy.settings.file", "test/repositories/IVY-1485/ivysettings.xml"); + project.setProperty("ivy.dep.file", "test/repositories/IVY-1485/ivy-overlap-transitive-older.xml"); + IvyResolve res = new IvyResolve(); + res.setValidate(false); + res.setProject(project); + res.execute(); + + deliver.setPubrevision("1"); + deliver.setDeliverpattern("build/test/deliver/ivy-[revision].xml"); + deliver.setStatus("release"); + deliver.setValidate(false); + deliver.execute(); + + // now check that the transitive dependency didn't override the direct ones + File deliveredIvyFile = new File("build/test/deliver/ivy-1.xml"); + assertTrue(deliveredIvyFile.exists()); + ModuleDescriptor md = XmlModuleDescriptorParser.getInstance().parseDescriptor( + new IvySettings(), deliveredIvyFile.toURL(), false); + DependencyDescriptor[] dds = md.getDependencies(); + assertEquals(2, dds.length); + assertEquals(ModuleRevisionId.newInstance("overlap-older", "modA", "5"), dds[0].getDependencyRevisionId()); + assertEquals(ModuleRevisionId.newInstance("overlap-older", "modB", "1"), dds[1].getDependencyRevisionId()); + } + @Test public void testWithDynEvicted() throws Exception { project.setProperty("ivy.dep.file", "test/java/org/apache/ivy/ant/ivy-dyn-evicted.xml"); @@ -418,27 +543,8 @@ public class IvyDeliverTest { md.getModuleRevisionId()); DependencyDescriptor[] dds = md.getDependencies(); assertEquals(2, dds.length); - assertEquals(ModuleRevisionId.newInstance("org1", "mod1.2", "2.2"), + assertEquals(ModuleRevisionId.newInstance("org1", "mod1.2", "1.1"), dds[0].getDependencyRevisionId()); - - IvyRetrieve ret = new IvyRetrieve(); - ret.setProject(project); - ret.setPattern("build/test/retrieve/[artifact]-[revision].[ext]"); - ret.execute(); - - File list = new File("build/test/retrieve"); - String[] files = list.list(); - HashSet actualFileSet = new HashSet<>(Arrays.asList(files)); - HashSet expectedFileSet = new HashSet<>(); - for (DependencyDescriptor dd : dds) { - String name = dd.getDependencyId().getName(); - String rev = dd.getDependencyRevisionId().getRevision(); - String ext = "jar"; - String artifact = name + "-" + rev + "." + ext; - expectedFileSet.add(artifact); - } - assertEquals("Delivered Ivy descriptor inconsistent with retrieved artifacts", - expectedFileSet, actualFileSet); } /** @@ -470,28 +576,8 @@ public class IvyDeliverTest { md.getModuleRevisionId()); DependencyDescriptor[] dds = md.getDependencies(); assertEquals(2, dds.length); - assertEquals(ModuleRevisionId.newInstance("org1", "mod1.2", "2.2"), + assertEquals(ModuleRevisionId.newInstance("org1", "mod1.2", "1.1"), dds[1].getDependencyRevisionId()); - - IvyRetrieve ret = new IvyRetrieve(); - ret.setProject(project); - ret.setPattern("build/test/retrieve/[artifact]-[revision].[ext]"); - ret.execute(); - - File list = new File("build/test/retrieve"); - String[] files = list.list(); - HashSet actualFileSet = new HashSet<>(Arrays.asList(files)); - HashSet expectedFileSet = new HashSet<>(); - for (DependencyDescriptor dd : dds) { - String name = dd.getDependencyId().getName(); - String rev = dd.getDependencyRevisionId().getRevision(); - String ext = "jar"; - String artifact = name + "-" + rev + "." + ext; - expectedFileSet.add(artifact); - } - assertEquals("Delivered Ivy descriptor inconsistent with retrieved artifacts", - expectedFileSet, actualFileSet); - list.delete(); } @Test diff --git a/test/java/org/apache/ivy/core/resolve/ResolveTest.java b/test/java/org/apache/ivy/core/resolve/ResolveTest.java index 08f9900b..53631de8 100644 --- a/test/java/org/apache/ivy/core/resolve/ResolveTest.java +++ b/test/java/org/apache/ivy/core/resolve/ResolveTest.java @@ -6261,71 +6261,6 @@ public class ResolveTest { ivy.deliver(pubrev, deliveryPattern, dopts); } - /** - * Test case for IVY-1300. - * - * @throws Exception if something goes wrong - * @see IVY-1300 - */ - @Test - public void testIVY1300() throws Exception { - ivy = Ivy.newInstance(); - ivy.configure(new File("test/repositories/IVY-1300/ivysettings.xml")); - - ResolveOptions opts = new ResolveOptions(); - opts.setConfs(new String[] {"*"}); - opts.setResolveId("resolveid"); - opts.setTransitive(true); - - ResolveReport report = ivy.resolve(new File("test/repositories/IVY-1300/assembly-ivy.xml"), - opts); - assertFalse(report.hasError()); - - ModuleRevisionId modAExpectedRevId = ModuleRevisionId.newInstance("myorg", "modA", "trunk", - "5"); - ModuleRevisionId modBExpectedRevId = ModuleRevisionId.newInstance("myorg", "modB", - "releasebranch", "1"); - - // check that the resolve report has the expected results, namely that - // trunk/5 is considered later than branch/1 purely because 5>1. Of - // course it is more likely that we would want to consider this a - // 'bad comparison', but this Unit Test is not about that. It is about - // inconsistency of results between the resolve report and the - // delivered descriptor. In fact the delivered descriptor is out of - // step, because retrieve and the report both agree that trunk/5 is - // selected. Deliver begs to differ. - - Set reportMrids = report.getConfigurationReport("default") - .getModuleRevisionIds(); - assertEquals( - new HashSet<>(Arrays.asList(modAExpectedRevId, modBExpectedRevId)), - reportMrids); - - DeliverOptions dopts = new DeliverOptions(); - dopts.setGenerateRevConstraint(true); - dopts.setConfs(new String[] {"*"}); - dopts.setStatus("release"); - dopts.setPubdate(new Date()); - dopts.setResolveId("resolveid"); - String pubrev = "1"; - String deliveryPattern = "build/test/deliver/assembly-[revision].xml"; - - ivy.deliver(pubrev, deliveryPattern, dopts); - - // now check that the resolve report has the same info as the delivered descriptor - - File deliveredIvyFile = new File("build/test/deliver/assembly-1.xml"); - assertTrue(deliveredIvyFile.exists()); - ModuleDescriptor md = XmlModuleDescriptorParser.getInstance().parseDescriptor( - ivy.getSettings(), deliveredIvyFile.toURI().toURL(), false); - DependencyDescriptor[] dds = md.getDependencies(); - assertEquals(2, dds.length); - assertEquals(ModuleRevisionId.newInstance("myorg", "modB", "releasebranch", "1"), - dds[1].getDependencyRevisionId()); - assertEquals(ModuleRevisionId.newInstance("myorg", "modA", "trunk", "5"), - dds[0].getDependencyRevisionId()); - } - @Test public void testUseCacheOnly() throws Exception { ResolveOptions option = getResolveOptions(new String[] {"*"}).setValidate(false); diff --git a/test/repositories/IVY-1485/ivy-overlap-transitive-newer.xml b/test/repositories/IVY-1485/ivy-overlap-transitive-newer.xml new file mode 100644 index 00000000..28904a35 --- /dev/null +++ b/test/repositories/IVY-1485/ivy-overlap-transitive-newer.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + diff --git a/test/repositories/IVY-1485/ivy-overlap-transitive-older.xml b/test/repositories/IVY-1485/ivy-overlap-transitive-older.xml new file mode 100644 index 00000000..9ce21075 --- /dev/null +++ b/test/repositories/IVY-1485/ivy-overlap-transitive-older.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + diff --git a/test/repositories/IVY-1485/ivy-simple.xml b/test/repositories/IVY-1485/ivy-simple.xml new file mode 100644 index 00000000..ea462993 --- /dev/null +++ b/test/repositories/IVY-1485/ivy-simple.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/test/repositories/IVY-1485/ivysettings.xml b/test/repositories/IVY-1485/ivysettings.xml new file mode 100644 index 00000000..869eabb3 --- /dev/null +++ b/test/repositories/IVY-1485/ivysettings.xml @@ -0,0 +1,27 @@ + + + + + + + + + + diff --git a/test/repositories/IVY-1485/overlap-newer/modA/1/ivy.xml b/test/repositories/IVY-1485/overlap-newer/modA/1/ivy.xml new file mode 100644 index 00000000..d24d993f --- /dev/null +++ b/test/repositories/IVY-1485/overlap-newer/modA/1/ivy.xml @@ -0,0 +1,26 @@ + + + + + + + + + diff --git a/test/repositories/IVY-1485/overlap-newer/modA/5/ivy.xml b/test/repositories/IVY-1485/overlap-newer/modA/5/ivy.xml new file mode 100644 index 00000000..d3e0794a --- /dev/null +++ b/test/repositories/IVY-1485/overlap-newer/modA/5/ivy.xml @@ -0,0 +1,26 @@ + + + + + + + + + diff --git a/test/repositories/IVY-1485/overlap-newer/modB/1/ivy.xml b/test/repositories/IVY-1485/overlap-newer/modB/1/ivy.xml new file mode 100644 index 00000000..66de5d7c --- /dev/null +++ b/test/repositories/IVY-1485/overlap-newer/modB/1/ivy.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + diff --git a/test/repositories/IVY-1485/overlap-older/modA/1/ivy.xml b/test/repositories/IVY-1485/overlap-older/modA/1/ivy.xml new file mode 100644 index 00000000..1372d9da --- /dev/null +++ b/test/repositories/IVY-1485/overlap-older/modA/1/ivy.xml @@ -0,0 +1,26 @@ + + + + + + + + + diff --git a/test/repositories/IVY-1485/overlap-older/modA/5/ivy.xml b/test/repositories/IVY-1485/overlap-older/modA/5/ivy.xml new file mode 100644 index 00000000..ddb9be0a --- /dev/null +++ b/test/repositories/IVY-1485/overlap-older/modA/5/ivy.xml @@ -0,0 +1,26 @@ + + + + + + + + + diff --git a/test/repositories/IVY-1485/overlap-older/modB/1/ivy.xml b/test/repositories/IVY-1485/overlap-older/modB/1/ivy.xml new file mode 100644 index 00000000..b18f9cec --- /dev/null +++ b/test/repositories/IVY-1485/overlap-older/modB/1/ivy.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + diff --git a/test/repositories/IVY-1485/simple/modA/1/ivy.xml b/test/repositories/IVY-1485/simple/modA/1/ivy.xml new file mode 100644 index 00000000..701473d2 --- /dev/null +++ b/test/repositories/IVY-1485/simple/modA/1/ivy.xml @@ -0,0 +1,26 @@ + + + + + + + + + diff --git a/test/repositories/IVY-1485/simple/modA/5/ivy.xml b/test/repositories/IVY-1485/simple/modA/5/ivy.xml new file mode 100644 index 00000000..262680c2 --- /dev/null +++ b/test/repositories/IVY-1485/simple/modA/5/ivy.xml @@ -0,0 +1,26 @@ + + + + + + + + + diff --git a/test/repositories/IVY-1485/simple/modB/1/ivy.xml b/test/repositories/IVY-1485/simple/modB/1/ivy.xml new file mode 100644 index 00000000..23a57679 --- /dev/null +++ b/test/repositories/IVY-1485/simple/modB/1/ivy.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + +