diff --git a/pom.xml b/pom.xml
index c84c2485..a00bd420 100644
--- a/pom.xml
+++ b/pom.xml
@@ -34,6 +34,26 @@
Apache Maven JAR PluginBuilds a Java Archive (JAR) file from the compiled project classes and resources.
+
+
+ evenisse
+ Emmanuel Venisse
+ evenisse@apache.org
+
+ Java Developer
+
+
+
+ desruisseaux
+ Martin Desruisseaux
+ desruisseaux@apache.org
+ Geomatys
+
+ Java Developer
+
+ +1
+
+ Jerome Lacoste
@@ -133,9 +153,9 @@
provided
- org.apache.maven.shared
- file-management
- ${mavenFileManagementVersion}
+ org.apache.maven
+ maven-support
+ ${mavenVersion}org.apache.maven.shared
@@ -205,7 +225,7 @@
- src/it/mjar-71-01/src/main/resources/META-INF/MANIFEST.MF
+ src/it/mjar-71-01/src/main/my-custom-dir/some-manifest.mfsrc/it/mjar-71-02/src/main/resources/META-INF/MANIFEST.MF
diff --git a/src/it/MJAR-260-invalid-automatic-module-name/verify.bsh b/src/it/MJAR-260-invalid-automatic-module-name/verify.bsh
index 5b9d457f..7b39fb52 100644
--- a/src/it/MJAR-260-invalid-automatic-module-name/verify.bsh
+++ b/src/it/MJAR-260-invalid-automatic-module-name/verify.bsh
@@ -45,8 +45,7 @@ try
String[] snippets = new String[] {
"[INFO] BUILD FAILURE",
"[ERROR] Failed to execute goal org.apache.maven.plugins:maven-jar-plugin",
- "Caused by: org.apache.maven.api.plugin.MojoException: Error assembling JAR",
- "Caused by: org.codehaus.plexus.archiver.jar.ManifestException: Invalid automatic module name: 'in-valid.name.with.new.keyword'"
+ "Caused by: org.apache.maven.api.plugin.MojoException: Invalid automatic module name: \"in-valid.name.with.new.keyword\"."
};
System.out.println("\nVerifying log snippets...");
diff --git a/src/it/MJAR-275-reproducible-module-info/invoker.properties b/src/it/MJAR-275-reproducible-module-info/invoker.properties
index 71eea457..452fbdb2 100644
--- a/src/it/MJAR-275-reproducible-module-info/invoker.properties
+++ b/src/it/MJAR-275-reproducible-module-info/invoker.properties
@@ -5,9 +5,9 @@
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
-#
+#
# http://www.apache.org/licenses/LICENSE-2.0
-#
+#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@@ -15,7 +15,5 @@
# specific language governing permissions and limitations
# under the License.
-# NOTE: Requires Java 10+ to compile the module declaration for Java 9+,
-# this is due that compiling the module declaration generates a
-# module descriptor with the JDK version on it, making it unreproducible.
-invoker.java.version = 10+
+# The --date option needed for reproducible build is available only since Java 19.
+invoker.java.version = 19+
diff --git a/src/it/MJAR-292-detect-mjar/pom.xml b/src/it/MJAR-292-detect-mjar/pom.xml
index 9c0afd52..d473e74c 100644
--- a/src/it/MJAR-292-detect-mjar/pom.xml
+++ b/src/it/MJAR-292-detect-mjar/pom.xml
@@ -86,8 +86,8 @@
false
diff --git a/src/it/MJAR-292-disable-detect-mjar/pom.xml b/src/it/MJAR-292-disable-detect-mjar/pom.xml
index 1cc61ea9..c868bdce 100644
--- a/src/it/MJAR-292-disable-detect-mjar/pom.xml
+++ b/src/it/MJAR-292-disable-detect-mjar/pom.xml
@@ -84,6 +84,7 @@
myproject.HelloWorld
+ false
diff --git a/src/it/MJAR-30-include/verify.groovy b/src/it/MJAR-30-include/verify.groovy
index 28d3e5f3..1b0b838c 100644
--- a/src/it/MJAR-30-include/verify.groovy
+++ b/src/it/MJAR-30-include/verify.groovy
@@ -67,7 +67,7 @@ try
{
String artifactName = artifactNames[i];
if ( !contents.contains( artifactName ) )
- {
+ {
System.err.println( "Artifact[" + artifactName + "] not found in jar archive" );
return false;
}
diff --git a/src/it/MJAR-70-recreation/verify.bsh b/src/it/MJAR-70-recreation/verify.bsh
index 29b80ca2..25da0f97 100644
--- a/src/it/MJAR-70-recreation/verify.bsh
+++ b/src/it/MJAR-70-recreation/verify.bsh
@@ -58,7 +58,7 @@ if ( buildLog.exists() ) {
int jarPluginExecutions = 0;
String[] lines = buildLogContent.split( "\n" );
for ( String line : lines ) {
- if ( line.contains( "Building jar:" ) && line.contains( "MJAR-70-recreation-1.0-SNAPSHOT.jar" ) ) {
+ if ( line.contains( "Building JAR:" ) && line.contains( "MJAR-70-recreation-1.0-SNAPSHOT.jar" ) ) {
jarPluginExecutions++;
System.out.println( "Found JAR creation: " + line );
}
diff --git a/src/it/directory-entries-mr/pom.xml b/src/it/directory-entries-mr/pom.xml
new file mode 100644
index 00000000..de13d653
--- /dev/null
+++ b/src/it/directory-entries-mr/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+ 4.0.0
+ org.apache.maven.plugins
+ directory-entries-mr
+ 1.0-SNAPSHOT
+ jar
+ directory-entries-mr-it
+ Verifies that a non-modular multi-release JAR preserves the intermediate directory
+ entries (com/, com/acme/, com/acme/sub/) in the base area and in the versioned area
+ (META-INF/versions/17/, .../com/, .../com/acme/). Consumers relying on JAR directory
+ traversal (e.g. Spring @ComponentScan) depend on those entries being present.
+
+ This is the same test as `directory-entries` but with multi-release.
+
+
+
+
+
+ maven-compiler-plugin
+ 3.10.1
+
+ 11
+
+
+
+ java17
+
+ compile
+
+
+ 17
+
+ ${project.basedir}/src/main/java17
+
+ true
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ @project.version@
+
+
+
+
diff --git a/src/it/directory-entries-mr/src/main/java/com/acme/App.java b/src/it/directory-entries-mr/src/main/java/com/acme/App.java
new file mode 100644
index 00000000..79f451f2
--- /dev/null
+++ b/src/it/directory-entries-mr/src/main/java/com/acme/App.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package com.acme;
+
+public class App {
+ public String greet() {
+ return "Hello (base) from " + new com.acme.sub.Helper().name();
+ }
+}
diff --git a/src/it/directory-entries-mr/src/main/java/com/acme/sub/Helper.java b/src/it/directory-entries-mr/src/main/java/com/acme/sub/Helper.java
new file mode 100644
index 00000000..d1ea9fd4
--- /dev/null
+++ b/src/it/directory-entries-mr/src/main/java/com/acme/sub/Helper.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package com.acme.sub;
+
+public class Helper {
+ public String name() {
+ return "helper";
+ }
+}
diff --git a/src/it/directory-entries-mr/src/main/java17/com/acme/App.java b/src/it/directory-entries-mr/src/main/java17/com/acme/App.java
new file mode 100644
index 00000000..7df19a34
--- /dev/null
+++ b/src/it/directory-entries-mr/src/main/java17/com/acme/App.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package com.acme;
+
+/**
+ * JDK-17 specific override of {@link App}. Placed under {@code src/main/java17} and compiled with
+ * {@code multiReleaseOutput=true}, so it lands in {@code META-INF/versions/17/com/acme/App.class}.
+ */
+public class App {
+ public String greet() {
+ return "Hello (java17) from " + new com.acme.sub.Helper().name();
+ }
+}
diff --git a/src/it/directory-entries-mr/verify.groovy b/src/it/directory-entries-mr/verify.groovy
new file mode 100644
index 00000000..e1aec4e7
--- /dev/null
+++ b/src/it/directory-entries-mr/verify.groovy
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.util.jar.Attributes
+import java.util.jar.JarFile
+
+File artifact = new File(basedir, "target/directory-entries-mr-1.0-SNAPSHOT.jar")
+assert artifact.isFile() : "JAR artifact is missing: " + artifact
+
+JarFile jar = new JarFile(artifact)
+try {
+ // Sanity: this must really be a multi-release JAR.
+ assert jar.manifest.mainAttributes.getValue(Attributes.Name.MULTI_RELEASE) == "true" :
+ "Expected Multi-Release: true in the manifest"
+
+ // Regression guard for maven-jar-plugin #508, extended to non-modular multi-release JARs.
+ // if the JAR is built by enumerating individual files, the intermediate directory entries
+ // can not be specified (if they were, they would be traversed), which breaks consumers
+ // relying on JAR directory traversal (e.g. Spring Boot @ComponentScan). Assert that the
+ // directory entries are present in both the base area and the "META-INF/versions/17" area.
+ def requiredDirectoryEntries = [
+ // base area
+ "com/",
+ "com/acme/",
+ "com/acme/sub/",
+ // versioned area
+ "META-INF/versions/17/",
+ "META-INF/versions/17/com/",
+ "META-INF/versions/17/com/acme/"
+ ]
+ for (String name : requiredDirectoryEntries) {
+ def entry = jar.getEntry(name)
+ assert entry != null : "Missing directory entry: " + name
+ assert entry.isDirectory() : "Entry is not a directory: " + name
+ }
+
+ // The class files must of course still be present.
+ def requiredFileEntries = [
+ "com/acme/App.class",
+ "com/acme/sub/Helper.class",
+ "META-INF/versions/17/com/acme/App.class"
+ ]
+ for (String name : requiredFileEntries) {
+ def entry = jar.getEntry(name)
+ assert entry != null : "Missing class entry: " + name
+ assert !entry.isDirectory() : "Class entry unexpectedly a directory: " + name
+ }
+} finally {
+ jar.close()
+}
+
+return true
diff --git a/src/it/directory-entries/invoker.properties b/src/it/directory-entries/invoker.properties
new file mode 100644
index 00000000..a644cb7e
--- /dev/null
+++ b/src/it/directory-entries/invoker.properties
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+invoker.goals = clean package
diff --git a/src/it/directory-entries/pom.xml b/src/it/directory-entries/pom.xml
new file mode 100644
index 00000000..a114f0e6
--- /dev/null
+++ b/src/it/directory-entries/pom.xml
@@ -0,0 +1,45 @@
+
+
+
+ 4.0.0
+ org.apache.maven.plugins
+ directory-entries
+ 1.0-SNAPSHOT
+ jar
+ directory-entries-it
+ Verifies that an unfiltered JAR preserves the intermediate directory entries
+ ("com/", "com/acme/", "com/acme/sub/"), which consumers relying on JAR directory traversal
+ (e.g. Spring @ComponentScan) depend on.
+
+ This is the same test as `directory-entries-mr` but without multi-release.
+
+ 11
+ 2020-02-02T00:00:00Z
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ @project.version@
+
+
+
+
diff --git a/src/it/directory-entries/src/main/java/com/acme/App.java b/src/it/directory-entries/src/main/java/com/acme/App.java
new file mode 100644
index 00000000..903772e6
--- /dev/null
+++ b/src/it/directory-entries/src/main/java/com/acme/App.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package com.acme;
+
+public class App {
+ public String greet() {
+ return "Hello from " + new com.acme.sub.Helper().name();
+ }
+}
diff --git a/src/it/directory-entries/src/main/java/com/acme/sub/Helper.java b/src/it/directory-entries/src/main/java/com/acme/sub/Helper.java
new file mode 100644
index 00000000..2523651e
--- /dev/null
+++ b/src/it/directory-entries/src/main/java/com/acme/sub/Helper.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package com.acme.sub;
+
+public class Helper {
+ public String name() {
+ return "Helper";
+ }
+}
diff --git a/src/it/directory-entries/verify.groovy b/src/it/directory-entries/verify.groovy
new file mode 100644
index 00000000..ccab2aa7
--- /dev/null
+++ b/src/it/directory-entries/verify.groovy
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.util.jar.JarFile
+
+File artifact = new File(basedir, "target/directory-entries-1.0-SNAPSHOT.jar")
+assert artifact.isFile() : "JAR artifact is missing: " + artifact
+
+JarFile jar = new JarFile(artifact)
+try {
+ // Regression guard for maven-jar-plugin #508 (Sergey Chernov, dev list):
+ // if the JAR is built by enumerating individual files, the intermediate directory entries
+ // can not be specified (if they were, they would be traversed), which breaks consumers
+ // relying on JAR directory traversal (e.g. Spring Boot @ComponentScan).
+ // Assert that the directory entries are present.
+ def requiredDirectoryEntries = [
+ "com/",
+ "com/acme/",
+ "com/acme/sub/"
+ ]
+ for (String name : requiredDirectoryEntries) {
+ def entry = jar.getEntry(name)
+ assert entry != null : "Missing directory entry: " + name
+ assert entry.isDirectory() : "Entry is not a directory: " + name
+ }
+
+ // The class files must of course still be present.
+ def requiredFileEntries = [
+ "com/acme/App.class",
+ "com/acme/sub/Helper.class"
+ ]
+ for (String name : requiredFileEntries) {
+ def entry = jar.getEntry(name)
+ assert entry != null : "Missing class entry: " + name
+ assert !entry.isDirectory() : "Class entry unexpectedly a directory: " + name
+ }
+} finally {
+ jar.close()
+}
+
+return true
diff --git a/src/it/jar-without-sources/invoker.properties b/src/it/jar-without-sources/invoker.properties
new file mode 100644
index 00000000..a644cb7e
--- /dev/null
+++ b/src/it/jar-without-sources/invoker.properties
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+invoker.goals = clean package
diff --git a/src/it/jar-without-sources/pom.xml b/src/it/jar-without-sources/pom.xml
new file mode 100644
index 00000000..3b990ad5
--- /dev/null
+++ b/src/it/jar-without-sources/pom.xml
@@ -0,0 +1,44 @@
+
+
+
+ 4.0.0
+ org.apache.maven.plugins
+ jar-without-sources
+ 1.0-SNAPSHOT
+ jar
+ jar-without-sources-it
+ This project has neither `src/main` nor `src/test`, so nothing is compiled
+ and the `target` directory is never created by an earlier lifecycle phase.
+ Versions 3.x of the JAR plugin still produced an empty, manifest-only JAR in that case.
+ This IT verifies that versions 4.x of the JAR plugin has the same behavior for compatibility purposes.
+
+ NOTE: it does not mean that it is a good idea to produce an empty JAR in such case.
+ It would also be a reasonable choice if a future version decide to fail the build instead.
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ @project.version@
+
+
+
+
diff --git a/src/it/jar-without-sources/verify.groovy b/src/it/jar-without-sources/verify.groovy
new file mode 100644
index 00000000..eff05af6
--- /dev/null
+++ b/src/it/jar-without-sources/verify.groovy
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.util.jar.JarFile
+
+File artifact = new File(basedir, "target/jar-without-sources-1.0-SNAPSHOT.jar")
+if (!artifact.isFile()) {
+ System.err.println("Expected JAR is missing: " + artifact)
+ return false
+}
+
+JarFile jar = new JarFile(artifact)
+try {
+ // The manifest must be present.
+ if (jar.getEntry("META-INF/MANIFEST.MF") == null) {
+ System.err.println("JAR does not contain META-INF/MANIFEST.MF")
+ return false
+ }
+ // As there is no source directory, the JAR must not contain any compiled class.
+ for (entry in jar.entries()) {
+ if (entry.getName().endsWith(".class")) {
+ System.err.println("JAR unexpectedly contains a class entry: " + entry.getName())
+ return false
+ }
+ }
+} finally {
+ jar.close()
+}
+
+return true
diff --git a/src/it/manifest-created-by/pom.xml b/src/it/manifest-created-by/pom.xml
new file mode 100644
index 00000000..7eb23287
--- /dev/null
+++ b/src/it/manifest-created-by/pom.xml
@@ -0,0 +1,52 @@
+
+
+
+ 4.0.0
+ org.apache.maven.plugins
+ manifest-created-by
+ 1.0-SNAPSHOT
+ jar
+ manifest-created-by-it
+ Verifies that when strictly reproducible builds is requested,
+ the vendor-specific "Created-By" attribute value (e.g. "21.0.10 (Amazon.com Inc.)")
+ is replaced by a hard-coded value.
+
+ 11
+
+ 2020-02-02T00:00:00Z
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ @project.version@
+
+
+ false
+
+ false
+
+
+
+
+
+
+
diff --git a/src/it/manifest-created-by/src/main/java/myproject/HelloWorld.java b/src/it/manifest-created-by/src/main/java/myproject/HelloWorld.java
new file mode 100644
index 00000000..0e4760f5
--- /dev/null
+++ b/src/it/manifest-created-by/src/main/java/myproject/HelloWorld.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package myproject;
+
+public class HelloWorld {
+ public String greeting() {
+ return "Hello World";
+ }
+}
diff --git a/src/it/manifest-created-by/verify.groovy b/src/it/manifest-created-by/verify.groovy
new file mode 100644
index 00000000..f13f6b48
--- /dev/null
+++ b/src/it/manifest-created-by/verify.groovy
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.util.jar.*;
+
+/*
+ * The project sets addDefaultEntries=false, so Maven archiver writes no Created-By. Without the fix the
+ * jar tool fills that gap with its own value, java.version + " (" + java.vendor + ")" (for example
+ * "21.0.10 (Amazon.com Inc.)"). That is JDK- and vendor-specific, so the same sources produce different
+ * bytes on Temurin vs Corretto -- a reproducibility regression, and it ignores addDefaultEntries=false.
+ * The plugin must instead write a stable, JDK-independent Created-By.
+ */
+File artifact = new File(basedir, "target/manifest-created-by-1.0-SNAPSHOT.jar")
+assert artifact.isFile() : "artifact is missing: " + artifact
+
+JarFile jar = new JarFile(artifact)
+try {
+ Attributes attributes = jar.getManifest().getMainAttributes()
+ String createdBy = attributes.getValue("Created-By")
+
+ assert createdBy != null : "Created-By is missing."
+ assert createdBy.startsWith("Maven JAR Plugin") : "Unexpected Created-By: " + createdBy
+
+ // addMavenDescriptor=false must still be honored: no Maven metadata in the JAR.
+ assert jar.getEntry("META-INF/maven/") == null : "addMavenDescriptor=false was not honored"
+} finally {
+ jar.close()
+}
diff --git a/src/it/mjar-71-01/pom.xml b/src/it/mjar-71-01/pom.xml
index cb6b3195..d5b6de07 100644
--- a/src/it/mjar-71-01/pom.xml
+++ b/src/it/mjar-71-01/pom.xml
@@ -25,7 +25,7 @@ under the License.
1.0jarit-mjar-71
- Test that the default manifest is added by default if found under target/classes. Can also be overriden.
+ Test that the specified manifest is used.http://maven.apache.org
@@ -36,7 +36,7 @@ under the License.
@project.version@
- src/main/resources/META-INF/MANIFEST.MF
+ src/main/my-custom-dir/some-manifest.mf
diff --git a/src/it/mjar-71-01/src/main/resources/META-INF/MANIFEST.MF b/src/it/mjar-71-01/src/main/my-custom-dir/some-manifest.mf
similarity index 100%
rename from src/it/mjar-71-01/src/main/resources/META-INF/MANIFEST.MF
rename to src/it/mjar-71-01/src/main/my-custom-dir/some-manifest.mf
diff --git a/src/it/mjar-71-01/verify.groovy b/src/it/mjar-71-01/verify.groovy
index 97f78e88..60faf65e 100644
--- a/src/it/mjar-71-01/verify.groovy
+++ b/src/it/mjar-71-01/verify.groovy
@@ -51,14 +51,14 @@ try
// Only compare files
if ( entry.getName().equals( "META-INF/MANIFEST.MF" ) )
{
- String manifest = IOUtils.toString( jar.getInputStream ( entry ) );
- int index = manifest.indexOf( "Archiver-Version: foobar-1.23456" );
- if ( index <= 0 )
- {
- System.err.println( "MANIFEST doesn't contain: 'Archiver-Version: foobar-1.23456'" );
- return false;
- }
- return true;
+ String manifest = IOUtils.toString( jar.getInputStream ( entry ) );
+ int index = manifest.indexOf( "Archiver-Version: foobar-1.23456" );
+ if ( index <= 0 )
+ {
+ System.err.println( "MANIFEST doesn't contain: 'Archiver-Version: foobar-1.23456'" );
+ return false;
+ }
+ return true;
}
}
}
diff --git a/src/it/mjar-71-02/pom.xml b/src/it/mjar-71-02/pom.xml
index fa3adac7..3fd9b506 100644
--- a/src/it/mjar-71-02/pom.xml
+++ b/src/it/mjar-71-02/pom.xml
@@ -25,7 +25,7 @@ under the License.
1.0jarit-mjar-71-02
- Test that the default manifest is not added when found under target/classes but support is disabled.
+ Test that the manifest found under target/classes is automatically used.http://maven.apache.org
diff --git a/src/it/mjar-71-02/verify.groovy b/src/it/mjar-71-02/verify.groovy
index 33e40531..ffddfcbf 100644
--- a/src/it/mjar-71-02/verify.groovy
+++ b/src/it/mjar-71-02/verify.groovy
@@ -51,14 +51,14 @@ try
// Only compare files
if ( entry.getName().equals ( "META-INF/MANIFEST.MF" ) )
{
- String manifest = IOUtils.toString( jar.getInputStream ( entry ) );
- int index = manifest.indexOf( "Archiver-Version: foobar-1.23456" );
- if ( index > 0 )
- {
- System.err.println( "MANIFEST contains: 'Archiver-Version: foobar-1.23456', but shouldn't" );
- return false;
- }
- return true;
+ String manifest = IOUtils.toString( jar.getInputStream ( entry ) );
+ int index = manifest.indexOf( "Archiver-Version: foobar-1.23456" );
+ if ( index <= 0 )
+ {
+ System.err.println( "MANIFEST doesn't contain: 'Archiver-Version: foobar-1.23456'" );
+ return false;
+ }
+ return true;
}
}
}
diff --git a/src/it/multimodule-multirelease-with-excludes/pom.xml b/src/it/multimodule-multirelease-with-excludes/pom.xml
new file mode 100644
index 00000000..d0351313
--- /dev/null
+++ b/src/it/multimodule-multirelease-with-excludes/pom.xml
@@ -0,0 +1,85 @@
+
+
+
+ 4.1.0
+
+ org.apache.maven.plugins
+ multirelease-modules-filtered
+ 1.0-SNAPSHOT
+ jar
+ Multirelease with modules and filters
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 4.0.0-beta-5
+
+
+
+
+ 17
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ @project.version@
+
+
+ **/ExcludedByFilter.class
+
+
+
+ true
+ true
+ foo.bar/foo.MainFile
+
+
+
+
+
+
+
+
+ foo.bar
+ src/foo.bar/main/java
+ 15
+
+
+ foo.bar
+ src/foo.bar/main/java_16
+ 16
+
+
+ foo.bar.more
+ src/foo.bar.more/main/java
+ 15
+
+
+ foo.bar.more
+ src/foo.bar.more/main/java_16
+ 16
+
+
+
+
diff --git a/src/it/multimodule-multirelease-with-excludes/src/foo.bar.more/main/java/module-info.java b/src/it/multimodule-multirelease-with-excludes/src/foo.bar.more/main/java/module-info.java
new file mode 100644
index 00000000..778a3a4a
--- /dev/null
+++ b/src/it/multimodule-multirelease-with-excludes/src/foo.bar.more/main/java/module-info.java
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module foo.bar.more {}
diff --git a/src/it/multimodule-multirelease-with-excludes/src/foo.bar.more/main/java/more/MainFile.java b/src/it/multimodule-multirelease-with-excludes/src/foo.bar.more/main/java/more/MainFile.java
new file mode 100644
index 00000000..03a35bf9
--- /dev/null
+++ b/src/it/multimodule-multirelease-with-excludes/src/foo.bar.more/main/java/more/MainFile.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package more;
+
+public class MainFile {
+ public static void main(String[] args) {
+ System.out.println("MainFile of more");
+ }
+}
diff --git a/src/it/multimodule-multirelease-with-excludes/src/foo.bar.more/main/java/more/OtherFile.java b/src/it/multimodule-multirelease-with-excludes/src/foo.bar.more/main/java/more/OtherFile.java
new file mode 100644
index 00000000..b3f782d8
--- /dev/null
+++ b/src/it/multimodule-multirelease-with-excludes/src/foo.bar.more/main/java/more/OtherFile.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package more;
+
+public class OtherFile {
+ public static void main(String[] args) {
+ System.out.println("OtherFile of more");
+ }
+}
diff --git a/src/it/multimodule-multirelease-with-excludes/src/foo.bar.more/main/java_16/more/OtherFile.java b/src/it/multimodule-multirelease-with-excludes/src/foo.bar.more/main/java_16/more/OtherFile.java
new file mode 100644
index 00000000..7b80d07b
--- /dev/null
+++ b/src/it/multimodule-multirelease-with-excludes/src/foo.bar.more/main/java_16/more/OtherFile.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package more;
+
+public class OtherFile {
+ public static void main(String[] args) {
+ System.out.println("OtherFile of more on Java 16");
+ MainFile.main(args); // Verify that we have access to the base version.
+ }
+}
diff --git a/src/it/multimodule-multirelease-with-excludes/src/foo.bar/main/java/foo/ExcludedByFilter.java b/src/it/multimodule-multirelease-with-excludes/src/foo.bar/main/java/foo/ExcludedByFilter.java
new file mode 100644
index 00000000..15ef8f42
--- /dev/null
+++ b/src/it/multimodule-multirelease-with-excludes/src/foo.bar/main/java/foo/ExcludedByFilter.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package foo;
+
+public class ExcludedByFilter {
+ public static void main(String[] args) {
+ System.out.println("ExcludedByFilter");
+ }
+}
diff --git a/src/it/multimodule-multirelease-with-excludes/src/foo.bar/main/java/foo/MainFile.java b/src/it/multimodule-multirelease-with-excludes/src/foo.bar/main/java/foo/MainFile.java
new file mode 100644
index 00000000..9c18f957
--- /dev/null
+++ b/src/it/multimodule-multirelease-with-excludes/src/foo.bar/main/java/foo/MainFile.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package foo;
+
+public class MainFile {
+ public static void main(String[] args) {
+ System.out.println("MainFile");
+ }
+}
diff --git a/src/it/multimodule-multirelease-with-excludes/src/foo.bar/main/java/foo/OtherFile.java b/src/it/multimodule-multirelease-with-excludes/src/foo.bar/main/java/foo/OtherFile.java
new file mode 100644
index 00000000..b6ea5abf
--- /dev/null
+++ b/src/it/multimodule-multirelease-with-excludes/src/foo.bar/main/java/foo/OtherFile.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package foo;
+
+public class OtherFile {
+ public static void main(String[] args) {
+ System.out.println("OtherFile");
+ }
+}
diff --git a/src/it/multimodule-multirelease-with-excludes/src/foo.bar/main/java/module-info.java b/src/it/multimodule-multirelease-with-excludes/src/foo.bar/main/java/module-info.java
new file mode 100644
index 00000000..38f61c0e
--- /dev/null
+++ b/src/it/multimodule-multirelease-with-excludes/src/foo.bar/main/java/module-info.java
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module foo.bar {}
diff --git a/src/it/multimodule-multirelease-with-excludes/src/foo.bar/main/java_16/foo/OtherFile.java b/src/it/multimodule-multirelease-with-excludes/src/foo.bar/main/java_16/foo/OtherFile.java
new file mode 100644
index 00000000..a859c9df
--- /dev/null
+++ b/src/it/multimodule-multirelease-with-excludes/src/foo.bar/main/java_16/foo/OtherFile.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package foo;
+
+public class OtherFile {
+ public static void main(String[] args) {
+ System.out.println("OtherFile on Java 16");
+ MainFile.main(args); // Verify that we have access to the base version.
+ }
+
+ static void requireJava16() {
+ System.out.println("Method available only on Java 16+");
+ }
+}
diff --git a/src/it/multimodule-multirelease-with-excludes/verify.groovy b/src/it/multimodule-multirelease-with-excludes/verify.groovy
new file mode 100644
index 00000000..210693ab
--- /dev/null
+++ b/src/it/multimodule-multirelease-with-excludes/verify.groovy
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.util.jar.*;
+
+/*
+ * Multi-module (therefore two JAR files), multi-release with at least one file excluded.
+ * Asserts the exact set of file entries per JAR: every file in the JAR is listed below.
+ */
+File target = new File(basedir, "target");
+
+// Module foo.bar: base classes, the Java 16 override, the manifest and the Maven descriptor.
+Set content = new HashSet<>();
+content.add("module-info.class") // this is a modular JAR
+content.add("foo/") // directory entry
+content.add("foo/MainFile.class") // base class, also the declared Main-Class
+content.add("foo/OtherFile.class") // base class
+content.add("META-INF/")
+content.add("META-INF/versions/16/foo/")
+content.add("META-INF/versions/16/foo/OtherFile.class") // multi-release override for Java 16
+content.add("META-INF/MANIFEST.MF")
+content.add("META-INF/maven/")
+content.add("META-INF/maven/org.apache.maven.plugins/")
+content.add("META-INF/maven/org.apache.maven.plugins/multirelease-modules-filtered/")
+content.add("META-INF/maven/org.apache.maven.plugins/multirelease-modules-filtered/pom.xml")
+content.add("META-INF/maven/org.apache.maven.plugins/multirelease-modules-filtered/pom.properties")
+// Note: foo/ExcludedByFilter.class is deliberately absent -- removed by the filter.
+verify(new File(target, "foo.bar-1.0-SNAPSHOT.jar"), content, "foo.MainFile")
+
+content.clear()
+// Module foo.bar.more: the filter does not match anything here, so the full set is expected.
+content.add("module-info.class")
+content.add("more/")
+content.add("more/MainFile.class")
+content.add("more/OtherFile.class")
+content.add("META-INF/")
+content.add("META-INF/versions/16/more/")
+content.add("META-INF/versions/16/more/OtherFile.class")
+content.add("META-INF/MANIFEST.MF")
+content.add("META-INF/maven/")
+content.add("META-INF/maven/org.apache.maven.plugins/")
+content.add("META-INF/maven/org.apache.maven.plugins/multirelease-modules-filtered/")
+content.add("META-INF/maven/org.apache.maven.plugins/multirelease-modules-filtered/pom.xml")
+content.add("META-INF/maven/org.apache.maven.plugins/multirelease-modules-filtered/pom.properties")
+verify(new File(target, "foo.bar.more-1.0-SNAPSHOT.jar"), content, null)
+
+/**
+ * Asserts the JAR's file entries are exactly the given set,
+ * and that the JAR is flagged Multi-Release with the expected Main-Class.
+ */
+void verify(File artifact, Set content, String mainClass)
+{
+ JarFile jar = new JarFile(artifact)
+ Enumeration jarEntries = jar.entries()
+ while (jarEntries.hasMoreElements())
+ {
+ JarEntry entry = (JarEntry) jarEntries.nextElement()
+ String name = entry.getName()
+ assert content.remove(name) : "Missing entry: " + name
+ }
+ assert content.isEmpty() : "Unexpected entries: " + content
+
+ Attributes attributes = jar.getManifest().getMainAttributes()
+ assert Objects.equals("true", attributes.get(Attributes.Name.MULTI_RELEASE))
+ assert Objects.equals(mainClass, attributes.get(Attributes.Name.MAIN_CLASS))
+
+ jar.close();
+}
diff --git a/src/it/multimodule-multirelease/pom.xml b/src/it/multimodule-multirelease/pom.xml
new file mode 100644
index 00000000..7394f632
--- /dev/null
+++ b/src/it/multimodule-multirelease/pom.xml
@@ -0,0 +1,81 @@
+
+
+
+ 4.1.0
+
+ org.apache.maven.plugins
+ multirelease-with-modules
+ 1.0-SNAPSHOT
+ jar
+ Multirelease with modules
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 4.0.0-beta-5
+
+
+
+
+ 17
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ @project.version@
+
+
+
+ true
+ true
+ foo.bar/foo.MainFile
+
+
+
+
+
+
+
+ foo.bar
+ src/foo.bar/main/java
+ 15
+
+
+ foo.bar
+ src/foo.bar/main/java_16
+ 16
+
+
+ foo.bar.more
+ src/foo.bar.more/main/java
+ 15
+
+
+ foo.bar.more
+ src/foo.bar.more/main/java_16
+ 16
+
+
+
+
diff --git a/src/it/multimodule-multirelease/src/foo.bar.more/main/java/module-info.java b/src/it/multimodule-multirelease/src/foo.bar.more/main/java/module-info.java
new file mode 100644
index 00000000..778a3a4a
--- /dev/null
+++ b/src/it/multimodule-multirelease/src/foo.bar.more/main/java/module-info.java
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module foo.bar.more {}
diff --git a/src/it/multimodule-multirelease/src/foo.bar.more/main/java/more/MainFile.java b/src/it/multimodule-multirelease/src/foo.bar.more/main/java/more/MainFile.java
new file mode 100644
index 00000000..d64f30a7
--- /dev/null
+++ b/src/it/multimodule-multirelease/src/foo.bar.more/main/java/more/MainFile.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package more;
+
+/**
+ * Test {@code <Source>}.
+ * Another {@code <Source>}.
+ */
+public class MainFile {
+ public static void main(String[] args) {
+ System.out.println("MainFile of more");
+ }
+}
diff --git a/src/it/multimodule-multirelease/src/foo.bar.more/main/java/more/OtherFile.java b/src/it/multimodule-multirelease/src/foo.bar.more/main/java/more/OtherFile.java
new file mode 100644
index 00000000..54e29b3c
--- /dev/null
+++ b/src/it/multimodule-multirelease/src/foo.bar.more/main/java/more/OtherFile.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package more;
+
+/**
+ * Test {@code <Source>}.
+ * Another {@code <Source>}.
+ */
+public class OtherFile {
+ public static void main(String[] args) {
+ System.out.println("OtherFile of more");
+ }
+}
diff --git a/src/it/multimodule-multirelease/src/foo.bar.more/main/java_16/more/OtherFile.java b/src/it/multimodule-multirelease/src/foo.bar.more/main/java_16/more/OtherFile.java
new file mode 100644
index 00000000..4b21485e
--- /dev/null
+++ b/src/it/multimodule-multirelease/src/foo.bar.more/main/java_16/more/OtherFile.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package more;
+
+/**
+ * Test {@code <Source>}.
+ * Another {@code <Source>}.
+ */
+public class OtherFile {
+ public static void main(String[] args) {
+ System.out.println("OtherFile of more on Java 16");
+ MainFile.main(args); // Verify that we have access to the base version.
+ }
+}
diff --git a/src/it/multimodule-multirelease/src/foo.bar/main/java/foo/MainFile.java b/src/it/multimodule-multirelease/src/foo.bar/main/java/foo/MainFile.java
new file mode 100644
index 00000000..502f2780
--- /dev/null
+++ b/src/it/multimodule-multirelease/src/foo.bar/main/java/foo/MainFile.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package foo;
+
+/**
+ * Test {@code <Source>}.
+ * Another {@code <Source>}.
+ */
+public class MainFile {
+ public static void main(String[] args) {
+ System.out.println("MainFile");
+ }
+}
diff --git a/src/it/multimodule-multirelease/src/foo.bar/main/java/foo/OtherFile.java b/src/it/multimodule-multirelease/src/foo.bar/main/java/foo/OtherFile.java
new file mode 100644
index 00000000..472210e1
--- /dev/null
+++ b/src/it/multimodule-multirelease/src/foo.bar/main/java/foo/OtherFile.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package foo;
+
+/**
+ * Test {@code <Source>}.
+ * Another {@code <Source>}.
+ */
+public class OtherFile {
+ public static void main(String[] args) {
+ System.out.println("OtherFile");
+ }
+}
diff --git a/src/it/multimodule-multirelease/src/foo.bar/main/java/foo/YetAnotherFile.java b/src/it/multimodule-multirelease/src/foo.bar/main/java/foo/YetAnotherFile.java
new file mode 100644
index 00000000..ab5f9009
--- /dev/null
+++ b/src/it/multimodule-multirelease/src/foo.bar/main/java/foo/YetAnotherFile.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package foo;
+
+/**
+ * Test {@code <Source>}.
+ * Another {@code <Source>}.
+ */
+public class YetAnotherFile {
+ public static void main(String[] args) {
+ System.out.println("YetAnotherFile");
+ }
+}
diff --git a/src/it/multimodule-multirelease/src/foo.bar/main/java/module-info.java b/src/it/multimodule-multirelease/src/foo.bar/main/java/module-info.java
new file mode 100644
index 00000000..38f61c0e
--- /dev/null
+++ b/src/it/multimodule-multirelease/src/foo.bar/main/java/module-info.java
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module foo.bar {}
diff --git a/src/it/multimodule-multirelease/src/foo.bar/main/java_16/foo/OtherFile.java b/src/it/multimodule-multirelease/src/foo.bar/main/java_16/foo/OtherFile.java
new file mode 100644
index 00000000..cbfa0b98
--- /dev/null
+++ b/src/it/multimodule-multirelease/src/foo.bar/main/java_16/foo/OtherFile.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package foo;
+
+/**
+ * Test {@code <Source>}.
+ * Another {@code <Source>}.
+ */
+public class OtherFile {
+ public static void main(String[] args) {
+ System.out.println("OtherFile on Java 16");
+ MainFile.main(args); // Verify that we have access to the base version.
+ }
+
+ static void requireJava16() {
+ System.out.println("Method available only on Java 16+");
+ }
+}
diff --git a/src/it/multimodule-multirelease/verify.groovy b/src/it/multimodule-multirelease/verify.groovy
new file mode 100644
index 00000000..d62d91f2
--- /dev/null
+++ b/src/it/multimodule-multirelease/verify.groovy
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.io.*;
+import java.util.*;
+import java.util.jar.*;
+
+File target = new File(basedir, "target");
+
+Set content = new HashSet<>();
+content.add("module-info.class")
+content.add("foo/MainFile.class")
+content.add("foo/OtherFile.class")
+content.add("foo/YetAnotherFile.class")
+content.add("META-INF/versions/16/foo/OtherFile.class")
+content.add("META-INF/MANIFEST.MF")
+content.add("META-INF/maven/org.apache.maven.plugins/multirelease-with-modules/pom.xml")
+content.add("META-INF/maven/org.apache.maven.plugins/multirelease-with-modules/pom.properties")
+verify(new File(target, "foo.bar-1.0-SNAPSHOT.jar"), content, "foo.MainFile")
+
+content.clear()
+content.add("module-info.class")
+content.add("more/MainFile.class")
+content.add("more/OtherFile.class")
+content.add("META-INF/versions/16/more/OtherFile.class")
+content.add("META-INF/MANIFEST.MF")
+content.add("META-INF/maven/org.apache.maven.plugins/multirelease-with-modules/pom.xml")
+content.add("META-INF/maven/org.apache.maven.plugins/multirelease-with-modules/pom.properties")
+verify(new File(target, "foo.bar.more-1.0-SNAPSHOT.jar"), content, null)
+
+void verify(File artifact, Set content, String mainClass)
+{
+ JarFile jar = new JarFile(artifact)
+ Enumeration jarEntries = jar.entries()
+ while (jarEntries.hasMoreElements())
+ {
+ JarEntry entry = (JarEntry) jarEntries.nextElement()
+ if (!entry.isDirectory())
+ {
+ String name = entry.getName()
+ assert content.remove(name) : "Missing entry: " + name
+ }
+ }
+ assert content.isEmpty() : "Unexpected entries: " + content
+
+ Attributes attributes = jar.getManifest().getMainAttributes()
+ assert Objects.equals("true", attributes.get(Attributes.Name.MULTI_RELEASE))
+ assert Objects.equals(mainClass, attributes.get(Attributes.Name.MAIN_CLASS))
+
+ jar.close();
+}
diff --git a/src/it/multimodule/pom.xml b/src/it/multimodule/pom.xml
new file mode 100644
index 00000000..5e0794c0
--- /dev/null
+++ b/src/it/multimodule/pom.xml
@@ -0,0 +1,67 @@
+
+
+
+ 4.1.0
+
+ org.apache.maven.plugins
+ multi-module
+ 1.0-SNAPSHOT
+ jar
+ Multi-module
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 4.0.0-beta-5
+
+
+
+
+ 17
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ @project.version@
+
+
+
+ true
+ foo.bar/foo.MainFile
+
+
+
+
+
+
+
+ foo.bar
+ src/foo.bar/main/java
+
+
+ foo.bar.more
+ src/foo.bar.more/main/java
+
+
+
+
diff --git a/src/it/multimodule/src/foo.bar.more/main/java/module-info.java b/src/it/multimodule/src/foo.bar.more/main/java/module-info.java
new file mode 100644
index 00000000..778a3a4a
--- /dev/null
+++ b/src/it/multimodule/src/foo.bar.more/main/java/module-info.java
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module foo.bar.more {}
diff --git a/src/it/multimodule/src/foo.bar.more/main/java/more/MainFile.java b/src/it/multimodule/src/foo.bar.more/main/java/more/MainFile.java
new file mode 100644
index 00000000..d64f30a7
--- /dev/null
+++ b/src/it/multimodule/src/foo.bar.more/main/java/more/MainFile.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package more;
+
+/**
+ * Test {@code <Source>}.
+ * Another {@code <Source>}.
+ */
+public class MainFile {
+ public static void main(String[] args) {
+ System.out.println("MainFile of more");
+ }
+}
diff --git a/src/it/multimodule/src/foo.bar/main/java/foo/MainFile.java b/src/it/multimodule/src/foo.bar/main/java/foo/MainFile.java
new file mode 100644
index 00000000..502f2780
--- /dev/null
+++ b/src/it/multimodule/src/foo.bar/main/java/foo/MainFile.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package foo;
+
+/**
+ * Test {@code <Source>}.
+ * Another {@code <Source>}.
+ */
+public class MainFile {
+ public static void main(String[] args) {
+ System.out.println("MainFile");
+ }
+}
diff --git a/src/it/multimodule/src/foo.bar/main/java/module-info.java b/src/it/multimodule/src/foo.bar/main/java/module-info.java
new file mode 100644
index 00000000..38f61c0e
--- /dev/null
+++ b/src/it/multimodule/src/foo.bar/main/java/module-info.java
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module foo.bar {}
diff --git a/src/it/multimodule/verify.groovy b/src/it/multimodule/verify.groovy
new file mode 100644
index 00000000..df8756de
--- /dev/null
+++ b/src/it/multimodule/verify.groovy
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.io.*;
+import java.util.*;
+import java.util.jar.*;
+
+File target = new File(basedir, "target");
+
+Set content = new HashSet<>();
+content.add("module-info.class")
+content.add("foo/MainFile.class")
+content.add("META-INF/MANIFEST.MF")
+content.add("META-INF/maven/org.apache.maven.plugins/multi-module/pom.xml")
+content.add("META-INF/maven/org.apache.maven.plugins/multi-module/pom.properties")
+verify(new File(target, "foo.bar-1.0-SNAPSHOT.jar"), content, "foo.MainFile")
+
+content.clear()
+content.add("module-info.class")
+content.add("more/MainFile.class")
+content.add("META-INF/MANIFEST.MF")
+content.add("META-INF/maven/org.apache.maven.plugins/multi-module/pom.xml")
+content.add("META-INF/maven/org.apache.maven.plugins/multi-module/pom.properties")
+verify(new File(target, "foo.bar.more-1.0-SNAPSHOT.jar"), content, null)
+
+void verify(File artifact, Set content, String mainClass)
+{
+ JarFile jar = new JarFile(artifact)
+ Enumeration jarEntries = jar.entries()
+ while (jarEntries.hasMoreElements())
+ {
+ JarEntry entry = (JarEntry) jarEntries.nextElement()
+ if (!entry.isDirectory())
+ {
+ String name = entry.getName()
+ assert content.remove(name) : "Missing entry: " + name
+ }
+ }
+ assert content.isEmpty() : "Unexpected entries: " + content
+
+ Attributes attributes = jar.getManifest().getMainAttributes()
+ assert attributes.get(Attributes.Name.MULTI_RELEASE) == null
+ assert Objects.equals(mainClass, attributes.get(Attributes.Name.MAIN_CLASS))
+
+ jar.close();
+}
diff --git a/src/it/records-jar-validate/invoker.properties b/src/it/records-jar-validate/invoker.properties
new file mode 100644
index 00000000..a644cb7e
--- /dev/null
+++ b/src/it/records-jar-validate/invoker.properties
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+invoker.goals = clean package
diff --git a/src/it/records-jar-validate/pom.xml b/src/it/records-jar-validate/pom.xml
new file mode 100644
index 00000000..42e05dec
--- /dev/null
+++ b/src/it/records-jar-validate/pom.xml
@@ -0,0 +1,44 @@
+
+
+
+ 4.0.0
+ org.apache.maven.plugins
+ records-jar-validate
+ 1.0-SNAPSHOT
+ jar
+ records-jar-validate
+ Verifies that a modular JAR containing a record can be packaged. The post-creation
+ "jar --validate" pass of the JDK 17 and 18 jar tool crashes on record classes (JDK-8282446,
+ fixed in JDK 19), so this build must not fail on those JDKs.
+
+
+ 17
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ @project.version@
+
+
+
+
diff --git a/src/it/records-jar-validate/src/main/java/it/records/Point.java b/src/it/records-jar-validate/src/main/java/it/records/Point.java
new file mode 100644
index 00000000..e42b2382
--- /dev/null
+++ b/src/it/records-jar-validate/src/main/java/it/records/Point.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package it.records;
+
+/**
+ * A record, whose class file carries a {@code Record} attribute with record components.
+ * The JDK 17/18 {@code jar --validate} tool crashes on such attributes (JDK-8282446).
+ */
+public record Point(int x, int y) {
+
+ public int sum() {
+ return x + y;
+ }
+}
diff --git a/src/it/records-jar-validate/src/main/java/module-info.java b/src/it/records-jar-validate/src/main/java/module-info.java
new file mode 100644
index 00000000..962b6424
--- /dev/null
+++ b/src/it/records-jar-validate/src/main/java/module-info.java
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+module it.records {
+ exports it.records;
+}
diff --git a/src/it/records-jar-validate/verify.bsh b/src/it/records-jar-validate/verify.bsh
new file mode 100644
index 00000000..99e8d886
--- /dev/null
+++ b/src/it/records-jar-validate/verify.bsh
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.io.*;
+import java.util.jar.*;
+
+boolean result = true;
+JarFile jar = null;
+
+try
+{
+ File artifact = new File( basedir, "target/records-jar-validate-1.0-SNAPSHOT.jar" );
+ if ( !artifact.isFile() )
+ {
+ System.err.println( "Expected JAR is missing: " + artifact );
+ return false;
+ }
+
+ jar = new JarFile( artifact );
+ if ( jar.getEntry( "it/records/Point.class" ) == null )
+ {
+ System.err.println( "JAR does not contain the record class it/records/Point.class" );
+ return false;
+ }
+ if ( jar.getEntry( "module-info.class" ) == null )
+ {
+ System.err.println( "JAR does not contain module-info.class" );
+ return false;
+ }
+}
+catch ( Throwable e )
+{
+ e.printStackTrace();
+ result = false;
+}
+finally
+{
+ if ( jar != null )
+ {
+ jar.close();
+ }
+}
+
+return result;
diff --git a/src/main/java/org/apache/maven/plugins/jar/AbstractJarMojo.java b/src/main/java/org/apache/maven/plugins/jar/AbstractJarMojo.java
index 10e00afb..0330ed69 100644
--- a/src/main/java/org/apache/maven/plugins/jar/AbstractJarMojo.java
+++ b/src/main/java/org/apache/maven/plugins/jar/AbstractJarMojo.java
@@ -18,44 +18,46 @@
*/
package org.apache.maven.plugins.jar;
-import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.time.DateTimeException;
+import java.time.Instant;
import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
import java.util.Map;
-import java.util.Objects;
-import java.util.jar.Attributes;
-import java.util.stream.Stream;
+import java.util.spi.ToolProvider;
+import org.apache.maven.api.PathScope;
import org.apache.maven.api.ProducedArtifact;
import org.apache.maven.api.Project;
import org.apache.maven.api.Session;
+import org.apache.maven.api.Type;
import org.apache.maven.api.di.Inject;
import org.apache.maven.api.plugin.Log;
import org.apache.maven.api.plugin.MojoException;
import org.apache.maven.api.plugin.annotations.Parameter;
+import org.apache.maven.api.services.PathMatcherFactory;
import org.apache.maven.api.services.ProjectManager;
import org.apache.maven.shared.archiver.MavenArchiveConfiguration;
import org.apache.maven.shared.archiver.MavenArchiver;
-import org.apache.maven.shared.archiver.MavenArchiverException;
-import org.apache.maven.shared.model.fileset.FileSet;
-import org.apache.maven.shared.model.fileset.util.FileSetManager;
-import org.codehaus.plexus.archiver.Archiver;
-import org.codehaus.plexus.archiver.jar.JarArchiver;
/**
* Base class for creating a JAR file from project classes.
*
* @author Emmanuel Venisse
+ * @author Martin Desruisseaux
*/
public abstract class AbstractJarMojo implements org.apache.maven.api.plugin.Mojo {
-
- private static final String[] DEFAULT_EXCLUDES = new String[] {"**/package.html"};
-
- private static final String[] DEFAULT_INCLUDES = new String[] {"**/**"};
-
- private static final String MODULE_DESCRIPTOR_FILE_NAME = "module-info.class";
+ /**
+ * Identifier of the tool to use. This identifier must match the identifier of a tool
+ * registered as a {@link ToolProvider}. By default, the {@code "jar"} tool is used.
+ *
+ * @since 4.0.0-beta-2
+ */
+ @Parameter(defaultValue = "jar", required = true)
+ protected String toolId;
/**
* List of files to include. Specified as fileset patterns which are relative to the input directory whose contents
@@ -72,23 +74,20 @@ public abstract class AbstractJarMojo implements org.apache.maven.api.plugin.Moj
protected String[] excludes;
/**
- * Directory containing the generated JAR.
+ * Directory containing the generated JAR files.
*/
@Parameter(defaultValue = "${project.build.directory}", required = true)
protected Path outputDirectory;
/**
- * Name of the generated JAR.
+ * Name of the generated JAR file.
+ * The default value is {@code "${project.build.finalName}"},
+ * which itself defaults to {@code "${artifactId}-${version}"}.
+ * Ignored if the Maven sub-project to archive uses module hierarchy.
*/
@Parameter(defaultValue = "${project.build.finalName}", readonly = true)
protected String finalName;
- /**
- * The JAR archiver.
- */
- @Inject
- protected Map archivers;
-
/**
* The Maven project.
*/
@@ -108,13 +107,22 @@ public abstract class AbstractJarMojo implements org.apache.maven.api.plugin.Moj
@Parameter
protected MavenArchiveConfiguration archive = new MavenArchiveConfiguration();
+ /**
+ * The service to use for attaching the artifacts produced by this plugin.
+ */
@Inject
protected ProjectManager projectManager;
/**
- * Require the jar plugin to build a new JAR even if none of the contents appear to have changed.
- * By default, this plugin looks to see if the output JAR exists and inputs have not changed.
- * If these conditions are true, the plugin skips creation of the JAR file.
+ * The service to use for creating include and exclude filters.
+ */
+ @Inject
+ private PathMatcherFactory matcherFactory;
+
+ /**
+ * Require the jar plugin to build new JAR files even if none of the contents appear to have changed.
+ * By default, this plugin looks to see if the output JAR files exist and inputs have not changed.
+ * If these conditions are true, the plugin skips creation of the JAR files.
* This does not work when other plugins, like the maven-shade-plugin, are configured to post-process the JAR.
* This plugin can not detect the post-processing, and so leaves the post-processed JAR file in place.
* This can lead to failures when those plugins do not expect to find their own output as an input.
@@ -136,8 +144,13 @@ public abstract class AbstractJarMojo implements org.apache.maven.api.plugin.Moj
* Timestamp for reproducible output archive entries.
* This is either formatted as ISO 8601 extended offset date-time
* (e.g. in UTC such as '2011-12-03T10:15:30Z' or with an offset '2019-10-05T20:37:42+06:00'),
- * or as an integer representing seconds since the epoch
- * (like SOURCE_DATE_EPOCH).
+ * or as an integer representing seconds since the Java epoch (January 1st, 1970).
+ * If not configured or disabled,
+ * the SOURCE_DATE_EPOCH
+ * environment variable is used as a fallback value,
+ * to ease forcing Reproducible Build externally when the build has not enabled it natively in POM.
+ *
+ *
This property is supported only in Java 19 or later.
*
* @since 3.2.0
*/
@@ -145,15 +158,38 @@ public abstract class AbstractJarMojo implements org.apache.maven.api.plugin.Moj
protected String outputTimestamp;
/**
- * Whether to detect multi-release JAR files.
- * If the JAR contains the {@code META-INF/versions} directory it will be detected as a multi-release JAR file
- * ("MRJAR"), adding the {@code Multi-Release: true} attribute to the main section of the JAR {@code MANIFEST.MF}.
+ * Whether to detect multi-release JAR files.
+ * If the JAR contains the {@code META-INF/versions} directory, it is detected as a multi-release JAR file.
+ * In such case:
+ *
+ *
+ *
the {@code Multi-Release: true} attribute is added
+ * to the main section of the JAR {@code MANIFEST.MF} entry, and
+ *
the class files in {@code META-INF/versions} are checked for API compatibility
+ * with the class files in the base version.
+ *
+ *
+ * If this flag is {@code false}, then the {@code META-INF/versions} directories are included without processing.
*
* @since 3.4.0
*/
@Parameter(property = "maven.jar.detectMultiReleaseJar", defaultValue = "true")
protected boolean detectMultiReleaseJar;
+ /**
+ * Whether to validate the JAR files after their creation.
+ * If {@code true}, the {@code jar} tool is executed a second time with
+ * the {@code --validate} option for each created JAR file.
+ * This verification can slow down the build if these files are large.
+ * If {@code false} (the default), there is no second {@code jar} execution.
+ * If {@code auto}, the Maven JAR Plugin will decide itself whether
+ * to execute {@code jar --validate} based on heuristic rules.
+ *
+ * @since 4.0.0-beta-2
+ */
+ @Parameter(defaultValue = "false")
+ protected String validate;
+
/**
* The MOJO logger.
*/
@@ -166,7 +202,7 @@ public abstract class AbstractJarMojo implements org.apache.maven.api.plugin.Moj
protected AbstractJarMojo() {}
/**
- * Specifies whether to attach the jar to the project.
+ * Specifies whether to attach the JAR file(s) to the project.
*
* @since 4.0.0-beta-2
*/
@@ -179,23 +215,7 @@ protected AbstractJarMojo() {}
protected abstract Path getClassesDirectory();
/**
- * Return the {@linkplain #project Maven project}.
- *
- * @return the Maven project
- */
- protected final Project getProject() {
- return project;
- }
-
- /**
- * {@return the MOJO logger}
- */
- protected final Log getLog() {
- return log;
- }
-
- /**
- * {@return the classifier of the JAR file to produce}
+ * {@return the classifier of the JAR file(s) to produce}
* This is usually null or empty for the main artifact, or {@code "tests"} for the JAR file of test code.
*/
protected abstract String getClassifier();
@@ -207,167 +227,232 @@ protected final Log getLog() {
protected abstract String getType();
/**
- * Returns the JAR file to generate, based on an optional classifier.
+ * {@return the scope of dependencies}
+ * It should be {@link PathScope#MAIN_COMPILE} or {@link PathScope#TEST_COMPILE}.
+ * Note that we use compile scope rather than runtime scope because dependencies
+ * cannot appear in {@code requires} statement if they don't have compile scope.
+ */
+ protected abstract PathScope getDependencyScope();
+
+ /**
+ * {@return the JAR tool to use for archiving the code}
*
- * @param basedir the output directory
- * @param resultFinalName the name of the JAR file
- * @param classifier an optional classifier
- * @return the file to generate
- */
- protected Path getJarFile(Path basedir, String resultFinalName, String classifier) {
- Objects.requireNonNull(basedir, "basedir is not allowed to be null");
- Objects.requireNonNull(resultFinalName, "finalName is not allowed to be null");
- String fileName = resultFinalName + (hasClassifier(classifier) ? '-' + classifier : "") + ".jar";
- return basedir.resolve(fileName);
+ * @throws MojoException if no JAR tool was found
+ *
+ * @since 4.0.0-beta-2
+ */
+ protected ToolProvider getJarTool() throws MojoException {
+ return ToolProvider.findFirst(toolId).orElseThrow(() -> new MojoException("No such \"" + toolId + "\" tool."));
}
/**
- * Generates the JAR.
+ * Returns the output timestamp or, as a fallback, the {@code SOURCE_DATE_EPOCH} environment variable.
+ * If the timestamp is expressed in seconds, it is converted to ISO 8601 format. Otherwise it is returned as-is.
*
- * @return the path to the created archive file
- * @throws MojoException in case of an error
+ * @return the timestamp in presumed ISO 8601 format, or {@code null} if none
+ * @throws MojoException if the timestamp looks like a number of seconds but cannot be parsed as such
+ *
+ * @since 4.0.0-beta-2
*/
- public Path createArchive() throws MojoException {
- Path basedir = outputDirectory != null
- ? outputDirectory
- : Path.of(project.getBuild().getDirectory());
- String resultFinalName =
- finalName != null ? finalName : project.getBuild().getFinalName();
- Path jarFile = getJarFile(basedir, resultFinalName, getClassifier());
-
- FileSetManager fileSetManager = new FileSetManager();
- FileSet jarContentFileSet = new FileSet();
- jarContentFileSet.setDirectory(getClassesDirectory().toAbsolutePath().toString());
- jarContentFileSet.setIncludes(Arrays.asList(getIncludes()));
- jarContentFileSet.setExcludes(Arrays.asList(getExcludes()));
-
- String[] includedFiles = fileSetManager.getIncludedFiles(jarContentFileSet);
-
- if (detectMultiReleaseJar
- && Arrays.stream(includedFiles)
- .anyMatch(
- p -> p.startsWith("META-INF" + File.separatorChar + "versions" + File.separatorChar))) {
- getLog().debug("Adding 'Multi-Release: true' manifest entry.");
- archive.addManifestEntry(Attributes.Name.MULTI_RELEASE.toString(), "true");
+ protected String getOutputTimestamp() {
+ String time = nullIfAbsent(outputTimestamp);
+ if (time == null) {
+ time = nullIfAbsent(System.getenv("SOURCE_DATE_EPOCH"));
+ if (time == null) {
+ return null;
+ }
}
+ for (int i = time.length(); --i >= 0; ) {
+ char c = time.charAt(i);
+ if ((c < '0' || c > '9') && (i != 0 || c != '-')) {
+ return time;
+ }
+ }
+ try {
+ return Instant.ofEpochSecond(Long.parseLong(time)).toString();
+ } catch (NumberFormatException | DateTimeException e) {
+ throw new MojoException("Timestamp \"" + time + "\" is not a number of seconds.", e);
+ }
+ }
- // May give false positives if the files is named as module descriptor
- // but is not in the root of the archive or in the versioned area
- // (and hence not actually a module descriptor).
- // That is fine since the modular Jar archiver will gracefully
- // handle such case.
- // And also such case is unlikely to happen as file ending
- // with "module-info.class" is unlikely to be included in Jar file
- // unless it is a module descriptor.
- boolean containsModuleDescriptor =
- Arrays.stream(includedFiles).anyMatch(p -> p.endsWith(MODULE_DESCRIPTOR_FILE_NAME));
-
- String archiverName = containsModuleDescriptor ? "mjar" : "jar";
+ /**
+ * {@return the patterns of files to include, or an empty list if no include pattern was specified}
+ */
+ final List getIncludes() {
+ return asList(includes);
+ }
- MavenArchiver archiver = new MavenArchiver();
- archiver.setCreatedBy("Maven JAR Plugin", "org.apache.maven.plugins", "maven-jar-plugin");
- archiver.setBuildJdkSpecDefaultEntry(archive.getManifest().isAddBuildEnvironmentEntries());
- archiver.setArchiver((JarArchiver) archivers.get(archiverName));
- archiver.setOutputFile(jarFile.toFile());
+ /**
+ * {@return the patterns of files to exclude, or an empty list if no exclude pattern was specified}
+ */
+ final List getExcludes() {
+ return asList(excludes);
+ }
- // configure for Reproducible Builds based on outputTimestamp value
- archiver.configureReproducibleBuild(outputTimestamp);
+ /**
+ * Returns the given elements as a list if non-null.
+ *
+ * @param elements the elements, or {@code null}
+ * @return the elements as a list, or {@code null} if the given array was null
+ */
+ private static List asList(String[] elements) {
+ return (elements == null) ? List.of() : Arrays.asList(elements);
+ }
- archive.setForced(forceCreation);
+ /**
+ * Returns the output directory and ensures that the directory exists.
+ * The returned directory will be either {@link #outputDirectory} if non-null,
+ * or {@link org.apache.maven.api.model.Build#getDirectory()} otherwise.
+ *
+ *
The directory is usually {@code target} and should always exist since it is usually the output
+ * of the compiler plugin. If nevertheless the directory does not exist, then this method creates it.
+ * However, this method does not try to create the parent directory, which should be the Maven sub-project.
+ *
+ * @return the directory containing the generated JAR files.
+ * @throws IOException if the output directory did not exist and could not be created
+ */
+ protected Path getOutputDirectory() throws IOException {
+ Path dir = outputDirectory;
+ if (dir == null) {
+ dir = Path.of(project.getBuild().getDirectory());
+ }
+ if (Files.notExists(dir)) {
+ dir = Files.createDirectory(dir);
+ }
+ return dir;
+ }
- try {
- Path contentDirectory = getClassesDirectory();
- if (!Files.exists(contentDirectory)) {
- if (!forceCreation) {
- getLog().warn("JAR will be empty - no content was marked for inclusion!");
+ /**
+ * Returns the value of the {@link #validate} parameter, or {@code null} if {@code "auto"}.
+ */
+ final Boolean getValidate() {
+ String value = validate;
+ if (value != null) {
+ value = value.strip();
+ if (!value.isEmpty()) {
+ switch (value.toLowerCase(Locale.ENGLISH)) {
+ default:
+ throw new MojoException("The 'validate' parameter value cannot be \"" + value + "\".");
+ case "auto":
+ return null;
+ case "true":
+ return Boolean.TRUE;
+ case "false":
+ break;
}
- } else {
- archiver.getArchiver().addDirectory(contentDirectory.toFile(), getIncludes(), getExcludes());
}
+ }
+ return Boolean.FALSE;
+ }
- archiver.createArchive(session, project, archive);
-
- return jarFile;
- } catch (Exception e) {
- // TODO: improve error handling
- throw new MojoException("Error assembling JAR", e);
+ /**
+ * Generates the JAR files.
+ * Map keys are module names or {@code null} if the project does not use module hierarchy.
+ * Values are (type, path) pairs associated with each module where
+ * type is {@code "pom"}, {@code "jar"} or {@code "test-jar"} and path
+ * is the path to the POM or JAR file.
+ *
+ *
Note that a null key does not necessarily mean that the JAR is not modular.
+ * It only means that the project was not compiled with module hierarchy,
+ * that is {@code target/classes/} subdirectories having module names.
+ * A project can be compiled with package hierarchy and still be modular.
+ *
+ * @return the paths to the created archive files
+ * @throws IOException if an error occurred while walking the file tree
+ * @throws MojoException if an error occurred while writing a JAR file
+ */
+ public Map> createArchives() throws IOException, MojoException {
+ final Path classesDirectory = getClassesDirectory();
+ final boolean notExists = Files.notExists(classesDirectory);
+ if (notExists) {
+ if (forceCreation) {
+ log.warn("No JAR created because no content was marked for inclusion.");
+ }
+ if (skipIfEmpty) {
+ log.info(String.format("Skipping packaging of the %s.", getType()));
+ return Map.of();
+ }
+ }
+ archive.setForced(forceCreation);
+ // TODO: we want a null manifest if there is no configuration.
+ final var archiver = new MavenArchiver();
+ archiver.setCreatedBy("Maven JAR Plugin", "org.apache.maven.plugins", "maven-jar-plugin");
+ archiver.setBuildJdkSpecDefaultEntry(archive.getManifest().isAddBuildEnvironmentEntries());
+ var executor = new ToolExecutor(this, archiver.getManifest(session, project, archive), archive);
+ var files = new FileCollector(this, executor, classesDirectory, matcherFactory);
+ if (!notExists) {
+ Files.walkFileTree(classesDirectory, files);
}
+ files.prune(skipIfEmpty);
+ List moduleRoots = files.getModuleHierarchyRoots();
+ if (!moduleRoots.isEmpty()) {
+ executor.pomDerivation = new PomDerivation(this, moduleRoots);
+ }
+ return executor.writeAllJARs(files);
}
/**
- * Generates the JAR.
+ * Generates the JAR file, then attaches the artifact.
*
* @throws MojoException in case of an error
*/
@Override
+ @SuppressWarnings("UseSpecificCatch")
public void execute() throws MojoException {
- if (skipIfEmpty && isEmpty(getClassesDirectory())) {
- getLog().info(String.format("Skipping packaging of the %s.", getType()));
- } else {
- Path jarFile = createArchive();
-
- if (attach) {
- ProducedArtifact artifact;
- String classifier = getClassifier();
- if (hasClassifier(classifier)) {
- artifact = session.createProducedArtifact(
- project.getGroupId(),
- project.getArtifactId(),
- project.getVersion(),
- classifier,
- null,
- getType());
- } else {
- if (projectHasAlreadySetAnArtifact()) {
- throw new MojoException("You have to use a classifier "
- + "to attach supplemental artifacts to the project instead of replacing them.");
+ final Map> artifactFiles;
+ try {
+ artifactFiles = createArchives();
+ } catch (MojoException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new MojoException("Error while assembling the JAR file.", e);
+ }
+ if (artifactFiles.isEmpty()) {
+ // Message already logged by `createArchives()`.
+ return;
+ }
+ if (attach) {
+ final String classifier = nullIfAbsent(getClassifier());
+ for (Map.Entry> entry : artifactFiles.entrySet()) {
+ String moduleName = entry.getKey();
+ for (Map.Entry path : entry.getValue().entrySet()) {
+ String type = path.getKey();
+ ProducedArtifact artifact;
+ if (moduleName == null && classifier == null && Type.JAR.equals(type)) {
+ if (projectHasAlreadySetAnArtifact()) {
+ throw new MojoException("You have to use a classifier "
+ + "to attach supplemental artifacts to the project instead of replacing them.");
+ }
+ artifact = project.getMainArtifact().orElseThrow();
+ } else {
+ artifact = session.createProducedArtifact(
+ project.getGroupId(),
+ (moduleName != null) ? moduleName : project.getArtifactId(),
+ project.getVersion(),
+ classifier,
+ null,
+ type);
}
- artifact = project.getMainArtifact().get();
+ projectManager.attachArtifact(project, artifact, path.getValue());
}
- projectManager.attachArtifact(project, artifact, jarFile);
- } else {
- getLog().debug("Skipping attachment of the " + getType() + " artifact to the project.");
}
+ } else {
+ log.debug("Skipping attachment of the " + getType() + " artifact to the project.");
}
}
- private static boolean isEmpty(Path directory) {
- if (!Files.isDirectory(directory)) {
- return true;
- }
- try (Stream children = Files.list(directory)) {
- return children.findAny().isEmpty();
- } catch (IOException e) {
- throw new MavenArchiverException("Unable to access directory", e);
- }
- }
-
+ /**
+ * Verifies whether the main artifact is already set.
+ * This verification does not apply for module hierarchy, where more than one artifact is produced.
+ */
private boolean projectHasAlreadySetAnArtifact() {
- Path path = projectManager.getPath(project).orElse(null);
- return path != null && Files.isRegularFile(path);
+ return projectManager.getPath(project).filter(Files::isRegularFile).isPresent();
}
/**
- * Return {@code true} if the classifier is not {@code null} and contains something other than white spaces.
- *
- * @param classifier the classifier to verify
- * @return {@code true} if the classifier is set
+ * Returns the given value if non-null, non-empty and non-blank, or {@code null} otherwise.
*/
- private static boolean hasClassifier(String classifier) {
- return classifier != null && !classifier.isBlank();
- }
-
- private String[] getIncludes() {
- if (includes != null && includes.length > 0) {
- return includes;
- }
- return DEFAULT_INCLUDES;
- }
-
- private String[] getExcludes() {
- if (excludes != null && excludes.length > 0) {
- return excludes;
- }
- return DEFAULT_EXCLUDES;
+ static String nullIfAbsent(String value) {
+ return (value == null || value.isBlank()) ? null : value;
}
}
diff --git a/src/main/java/org/apache/maven/plugins/jar/Archive.java b/src/main/java/org/apache/maven/plugins/jar/Archive.java
new file mode 100644
index 00000000..726f059c
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/jar/Archive.java
@@ -0,0 +1,655 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.plugins.jar;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.TreeMap;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+
+import org.apache.maven.api.Type;
+import org.apache.maven.api.annotations.Nonnull;
+import org.apache.maven.api.annotations.Nullable;
+import org.apache.maven.api.plugin.Log;
+import org.apache.maven.api.plugin.MojoException;
+
+/**
+ * Files or root directories to archive for a single module.
+ * A single instance of {@code Archive} can contain many directories for different target Java releases.
+ * Many instances of {@code Archive} can exist when archiving a multi-modules project.
+ */
+final class Archive {
+ /**
+ * Path to the POM file generated for this archive, or {@code null} if none.
+ * This is non-null only if module source hierarchy is used, in which case the dependencies
+ * declared in this file are the intersection of the project dependencies and the content of
+ * the {@code module-info.class} file.
+ */
+ @Nullable
+ Path pomFile;
+
+ /**
+ * The JAR file to create. Can be an existing file,
+ * in which case the file creation can be skipped if the file is still up-to-date.
+ */
+ @Nonnull
+ final Path jarFile;
+
+ /**
+ * A helper class for checking whether an existing JAR file is still up-to-date.
+ * This is null if there is no existing JAR file, or if we determined that the file is outdated.
+ */
+ private TimestampCheck existingJAR;
+
+ /**
+ * Name of the module being archived when the project is using module hierarchy.
+ * This is {@code null} if the project is using package hierarchy, either because it is a classical
+ * class-path project or because it is a single module compiled without using the module hierarchy.
+ * When using module source hierarchy, {@code javac} guarantees that the module name in the output
+ * directory is the name of the parent directory of {@code module-info.class}.
+ */
+ @Nullable
+ final String moduleName;
+
+ /**
+ * Path to {@code META-INF/MANIFEST.MF}, or {@code null} if none.
+ * If non-null, this value will be given to the {@code --manifest} option.
+ * The use of this option is preferable to adding {@code MANIFEST.MF} as an ordinary file.
+ *
+ * @see #setManifest(Path, boolean)
+ * @see #mergeManifest(Path, Manifest)
+ */
+ @Nullable
+ private Path manifest;
+
+ /**
+ * The Maven generated {@code pom.xml} and {@code pom.properties} files, or {@code null} if none.
+ * This first item must be the base directory where the files are located.
+ */
+ @Nullable
+ List mavenFiles;
+
+ /**
+ * Fully-qualified name of the main class, or {@code null} if none.
+ * This is the value to provide to the {@code --main-class} option.
+ */
+ private String mainClass;
+
+ /**
+ * Files or root directories to store in the JAR file for each target Java release
+ * other than the base release. Keys are the target Java release with {@code null} for the base
+ * release.
+ */
+ @Nonnull
+ private final NavigableMap filesetForRelease;
+
+ /**
+ * Files or root directories to archive for a single target Java release of a single module.
+ * The {@link Archive} enclosing must contain at least one instance of {@code FileSet} for
+ * the base release, and an arbitrary amount of other instances for other target releases.
+ */
+ final class FileSet {
+ /**
+ * The root directory of all files or directories to archive.
+ * This is the value to pass to the {@code -C} tool option.
+ */
+ @Nonnull
+ final Path directory;
+
+ /**
+ * The files or directories to include in the JAR file.
+ * Can be absolute paths or paths relative to {@link #directory}.
+ * It usually contains only the files or directories directly in
+ * the root {@linkplain #directory}, not in sub-directories.
+ */
+ @Nonnull
+ final List files;
+
+ /**
+ * Creates an initially empty set of files or directories for a specific target Java release.
+ *
+ * @param directory the base directory of the files or directories to archive
+ */
+ private FileSet(Path directory) {
+ this.directory = directory;
+ this.files = new ArrayList<>();
+ }
+
+ /**
+ * Discards all files in this file set, normally because those files are not in any module.
+ * This method returns a common parent directory for all the files that were discarded.
+ * The caller should use that common directory for logging a warning message.
+ *
+ * @param base base directory found by previous invocations of this method, or {@code null} if none
+ * @return common directory of discarded files
+ */
+ private Path discardAllFiles(Path base) {
+ for (Path file : files) {
+ if (base == null) {
+ base = file.getParent();
+ } else {
+ while (!file.startsWith(base)) {
+ base = base.getParent();
+ if (base == null) {
+ break;
+ }
+ }
+ }
+ }
+ files.clear();
+ return base;
+ }
+
+ /**
+ * Adds the given path to the list of files or directories to archive.
+ * If the given path is a directory, then all children will be included.
+ * Children to exclude, if any, should be managed by {@link ExcludedFiles}.
+ *
+ * @param item a file or directory to archive
+ * @param attributes the file's basic attributes
+ * @param isDirectory whether the file is a directory
+ */
+ void add(Path item, BasicFileAttributes attributes, boolean isDirectory) {
+ TimestampCheck tc = existingJAR;
+ if (tc != null && tc.isUpdated(item, attributes, isDirectory)) {
+ existingJAR = null; // Signal that the existing file is outdated.
+ }
+ files.add(item);
+ }
+
+ /**
+ * Adds to the given list the arguments to provide to the "jar" tool for this version.
+ * The elements added to the list will be instances of {@link String} or {@link Path}.
+ *
+ *
Note about the {@code -C} option
+ * This method repeats the {@code -C} option before each file.
+ * Our tests suggest that the first file after the directory specified by the {@code -C} option must
+ * be relative to that directory and all files after the first one must be prefixed by the directory
+ * which was specified in the {@code -C} option. This behavior is not very intuitive and replying on
+ * it can be fragile. Furthermore, it seems that the relativized file needs to be the shortest one,
+ * otherwise the {@code jar} tool rejects files after the first one with "names do not match".
+ * Which file is first depends on the unspecified directory-iteration order.
+ * Repeating the {@code -C} option for each file seems safer.
+ *
+ * @param addTo the list to add the arguments as {@link String} or {@link Path} instances to
+ * @param version the target Java release, or {@code null} for the base version of the JAR file
+ * @throws IllegalArgumentException if a path cannot be made relative to the base directory
+ */
+ private void arguments(List