diff --git a/pom.xml b/pom.xml
index 9016cc4a..2f9a0630 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
@@ -132,6 +152,12 @@
${mavenVersion}provided
+
+ org.apache.maven
+ maven-support
+ ${mavenVersion}
+ compile
+ org.apache.mavenmaven-api-xml
@@ -144,11 +170,6 @@
${mavenVersion}provided
-
- org.apache.maven.shared
- file-management
- ${mavenFileManagementVersion}
- org.apache.maven.sharedmaven-archiver
@@ -213,7 +234,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/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/multi-module/pom.xml b/src/it/multi-module/pom.xml
new file mode 100644
index 00000000..d4a781e9
--- /dev/null
+++ b/src/it/multi-module/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-3
+
+
+
+
+ 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/multi-module/src/foo.bar.more/main/java/module-info.java b/src/it/multi-module/src/foo.bar.more/main/java/module-info.java
new file mode 100644
index 00000000..778a3a4a
--- /dev/null
+++ b/src/it/multi-module/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/multi-module/src/foo.bar.more/main/java/more/MainFile.java b/src/it/multi-module/src/foo.bar.more/main/java/more/MainFile.java
new file mode 100644
index 00000000..d64f30a7
--- /dev/null
+++ b/src/it/multi-module/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/multi-module/src/foo.bar/main/java/foo/MainFile.java b/src/it/multi-module/src/foo.bar/main/java/foo/MainFile.java
new file mode 100644
index 00000000..502f2780
--- /dev/null
+++ b/src/it/multi-module/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/multi-module/src/foo.bar/main/java/module-info.java b/src/it/multi-module/src/foo.bar/main/java/module-info.java
new file mode 100644
index 00000000..38f61c0e
--- /dev/null
+++ b/src/it/multi-module/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/multi-module/verify.groovy b/src/it/multi-module/verify.groovy
new file mode 100644
index 00000000..df8756de
--- /dev/null
+++ b/src/it/multi-module/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/multirelease-with-modules/pom.xml b/src/it/multirelease-with-modules/pom.xml
new file mode 100644
index 00000000..5f02bbbe
--- /dev/null
+++ b/src/it/multirelease-with-modules/pom.xml
@@ -0,0 +1,102 @@
+
+
+
+ 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-3
+
+
+
+
+ 17
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-antrun-plugin
+ 3.1.0
+
+
+ move
+
+ run
+
+ prepare-package
+
+
+
+
+
+
+
+
+
+
+ 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/multirelease-with-modules/src/foo.bar.more/main/java/module-info.java b/src/it/multirelease-with-modules/src/foo.bar.more/main/java/module-info.java
new file mode 100644
index 00000000..778a3a4a
--- /dev/null
+++ b/src/it/multirelease-with-modules/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/multirelease-with-modules/src/foo.bar.more/main/java/more/MainFile.java b/src/it/multirelease-with-modules/src/foo.bar.more/main/java/more/MainFile.java
new file mode 100644
index 00000000..d64f30a7
--- /dev/null
+++ b/src/it/multirelease-with-modules/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/multirelease-with-modules/src/foo.bar.more/main/java/more/OtherFile.java b/src/it/multirelease-with-modules/src/foo.bar.more/main/java/more/OtherFile.java
new file mode 100644
index 00000000..54e29b3c
--- /dev/null
+++ b/src/it/multirelease-with-modules/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/multirelease-with-modules/src/foo.bar.more/main/java_16/more/OtherFile.java b/src/it/multirelease-with-modules/src/foo.bar.more/main/java_16/more/OtherFile.java
new file mode 100644
index 00000000..4b21485e
--- /dev/null
+++ b/src/it/multirelease-with-modules/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/multirelease-with-modules/src/foo.bar/main/java/foo/MainFile.java b/src/it/multirelease-with-modules/src/foo.bar/main/java/foo/MainFile.java
new file mode 100644
index 00000000..502f2780
--- /dev/null
+++ b/src/it/multirelease-with-modules/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/multirelease-with-modules/src/foo.bar/main/java/foo/OtherFile.java b/src/it/multirelease-with-modules/src/foo.bar/main/java/foo/OtherFile.java
new file mode 100644
index 00000000..472210e1
--- /dev/null
+++ b/src/it/multirelease-with-modules/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/multirelease-with-modules/src/foo.bar/main/java/foo/YetAnotherFile.java b/src/it/multirelease-with-modules/src/foo.bar/main/java/foo/YetAnotherFile.java
new file mode 100644
index 00000000..ab5f9009
--- /dev/null
+++ b/src/it/multirelease-with-modules/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/multirelease-with-modules/src/foo.bar/main/java/module-info.java b/src/it/multirelease-with-modules/src/foo.bar/main/java/module-info.java
new file mode 100644
index 00000000..38f61c0e
--- /dev/null
+++ b/src/it/multirelease-with-modules/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/multirelease-with-modules/src/foo.bar/main/java_16/foo/OtherFile.java b/src/it/multirelease-with-modules/src/foo.bar/main/java_16/foo/OtherFile.java
new file mode 100644
index 00000000..cbfa0b98
--- /dev/null
+++ b/src/it/multirelease-with-modules/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/multirelease-with-modules/verify.groovy b/src/it/multirelease-with-modules/verify.groovy
new file mode 100644
index 00000000..d62d91f2
--- /dev/null
+++ b/src/it/multirelease-with-modules/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/main/java/org/apache/maven/plugins/jar/AbstractJarMojo.java b/src/main/java/org/apache/maven/plugins/jar/AbstractJarMojo.java
index 058ac058..ed823459 100644
--- a/src/main/java/org/apache/maven/plugins/jar/AbstractJarMojo.java
+++ b/src/main/java/org/apache/maven/plugins/jar/AbstractJarMojo.java
@@ -18,16 +18,19 @@
*/
package org.apache.maven.plugins.jar;
-import java.io.File;
+import javax.lang.model.SourceVersion;
+
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.time.Instant;
import java.util.Arrays;
+import java.util.List;
import java.util.Map;
-import java.util.Objects;
-import java.util.jar.Attributes;
-import java.util.stream.Stream;
+import java.util.jar.Manifest;
+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;
@@ -38,24 +41,22 @@
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 shall 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)
+ private String toolId;
/**
* List of files to include. Specified as fileset patterns which are relative to the input directory whose contents
@@ -72,23 +73,20 @@ public abstract class AbstractJarMojo implements org.apache.maven.api.plugin.Moj
private String[] excludes;
/**
- * Directory containing the generated JAR.
+ * Directory containing the generated JAR files.
*/
@Parameter(defaultValue = "${project.build.directory}", required = true)
private 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)
private String finalName;
- /**
- * The JAR archiver.
- */
- @Inject
- private Map archivers;
-
/**
* The Maven project.
*/
@@ -112,9 +110,9 @@ public abstract class AbstractJarMojo implements org.apache.maven.api.plugin.Moj
private 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.
+ * 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.
@@ -124,20 +122,23 @@ public abstract class AbstractJarMojo implements org.apache.maven.api.plugin.Moj
* to {@code maven.jar.forceCreation}.
*/
@Parameter(property = "maven.jar.forceCreation", defaultValue = "false")
- private boolean forceCreation;
+ protected boolean forceCreation;
/**
* Skip creating empty archives.
*/
@Parameter(defaultValue = "false")
- private boolean skipIfEmpty;
+ protected boolean skipIfEmpty;
/**
* 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.
*
* @since 3.2.0
*/
@@ -145,14 +146,25 @@ public abstract class AbstractJarMojo implements org.apache.maven.api.plugin.Moj
private 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 will be detected as a multi-release JAR file,
+ * adding the {@code Multi-Release: true} attribute to the main section of the JAR {@code MANIFEST.MF} entry.
+ * In addition, the class files in {@code META-INF/versions} will be 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")
- private boolean detectMultiReleaseJar;
+ protected boolean detectMultiReleaseJar;
+
+ /**
+ * Specifies whether to attach the JAR files to the project.
+ *
+ * @since 4.0.0-beta-2
+ */
+ @Parameter(property = "maven.jar.attach", defaultValue = "true")
+ protected boolean attach;
/**
* The MOJO logger.
@@ -166,22 +178,26 @@ public abstract class AbstractJarMojo implements org.apache.maven.api.plugin.Moj
protected AbstractJarMojo() {}
/**
- * Specifies whether to attach the jar to the project
- *
- * @since 4.0.0-beta-2
+ * {@return the specific output directory to serve as the root for the archive}
*/
- @Parameter(property = "maven.jar.attach", defaultValue = "true")
- protected boolean attach;
+ protected abstract Path getClassesDirectory();
/**
- * {@return the specific output directory to serve as the root for the archive}
+ * {@return the directory containing the generated JAR files}
*/
- protected abstract Path getClassesDirectory();
+ protected Path getOutputDirectory() {
+ return outputDirectory;
+ }
/**
- * Return the {@linkplain #project Maven project}.
- *
- * @return the Maven project
+ * {@return the Maven session in which the project is built}
+ */
+ protected final Session getSession() {
+ return session;
+ }
+
+ /**
+ * {@return the Maven project}
*/
protected final Project getProject() {
return project;
@@ -190,12 +206,19 @@ protected final Project getProject() {
/**
* {@return the MOJO logger}
*/
- protected final Log getLog() {
+ protected Log getLog() {
return log;
}
/**
- * {@return the classifier of the JAR file to produce}
+ * {@return the name of the generated JAR file}
+ */
+ protected String getFinalName() {
+ return finalName;
+ }
+
+ /**
+ * {@return the classifier of the JAR file 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,160 +230,197 @@ 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 didn't had compile scope.
+ */
+ protected abstract PathScope getDependencyScope();
+
+ /**
+ * {@return the JAR tool to use for archiving the code}
+ *
+ * @throws MojoException if no JAR tool was found
*
- * @param basedir the output directory
- * @param resultFinalName the name of the ear 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);
+ * @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 whether the specified Java version is supported.
*
- * @return the path to the created archive file
- * @throws MojoException in case of an error
+ * @param release name of an {@link SourceVersion} enumeration constant
+ * @return whether the current environment support that version
*/
- public Path createArchive() throws MojoException {
- Path jarFile = getJarFile(outputDirectory, finalName, 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");
+ private static boolean isSupported(String release) {
+ try {
+ return SourceVersion.latestSupported().compareTo(SourceVersion.valueOf(release)) >= 0;
+ } catch (IllegalArgumentException e) {
+ return false;
}
+ }
- // 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";
+ /**
+ * Returns the output time stamp or, as a fallback, the {@code SOURCE_DATE_EPOCH} environment variable.
+ * If the time stamp is expressed in seconds, it is converted to ISO 8601 format. Otherwise it is returned as-is.
+ *
+ * @return the time stamp in presumed ISO 8601 format, or {@code null} if none
+ *
+ * @since 4.0.0-beta-2
+ */
+ protected String getOutputTimestamp() {
+ String time = nullIfAbsent(outputTimestamp);
+ if (time == null) {
+ time = nullIfAbsent(System.getenv("SOURCE_DATE_EPOCH"));
+ if (time == null) {
+ return null;
+ }
+ }
+ if (!isSupported("RELEASE_19")) {
+ log.warn("Reproducible build requires Java 19 or later.");
+ return null;
+ }
+ for (int i = time.length(); --i >= 0; ) {
+ char c = time.charAt(i);
+ if ((c < '0' || c > '9') && (i != 0 || c != '-')) {
+ return time;
+ }
+ }
+ return Instant.ofEpochSecond(Long.parseLong(time)).toString();
+ }
- MavenArchiver archiver = new MavenArchiver();
- archiver.setCreatedBy("Maven JAR Plugin", "org.apache.maven.plugins", "maven-jar-plugin");
- archiver.setArchiver((JarArchiver) archivers.get(archiverName));
- archiver.setOutputFile(jarFile.toFile());
+ /**
+ * {@return the patterns of files to include, or an empty list if no include pattern was specified}
+ */
+ protected List getIncludes() {
+ return asList(includes);
+ }
- // configure for Reproducible Builds based on outputTimestamp value
- archiver.configureReproducibleBuild(outputTimestamp);
+ /**
+ * {@return the patterns of files to exclude, or an empty list if no exclude pattern was specified}
+ */
+ protected List getExcludes() {
+ return asList(excludes);
+ }
- archive.setForced(forceCreation);
+ /**
+ * 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);
+ }
- try {
- Path contentDirectory = getClassesDirectory();
- if (!Files.exists(contentDirectory)) {
- if (!forceCreation) {
- getLog().warn("JAR will be empty - no content was marked for inclusion!");
- }
- } else {
- archiver.getArchiver().addDirectory(contentDirectory.toFile(), getIncludes(), getExcludes());
+ /**
+ * 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 means that the JAR is not modular.
+ * It only means that the project was not compiled with module hierarchy,
+ * i.e. {@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) {
+ getLog().warn("No JAR created because no content was marked for inclusion.");
}
-
- archiver.createArchive(session, project, archive);
-
- return jarFile;
- } catch (Exception e) {
- // TODO: improve error handling
- throw new MojoException("Error assembling JAR", e);
+ if (skipIfEmpty) {
+ getLog().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.
+ Manifest manifest = new MavenArchiver().getManifest(session, project, archive);
+ var executor = new ToolExecutor(this, manifest, archive);
+ var files = new FileCollector(this, executor, classesDirectory);
+ 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();
- ProducedArtifact artifact;
- String classifier = getClassifier();
- if (attach) {
- 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()) {
+ ProducedArtifact artifact;
+ if (moduleName == null && classifier == null) {
+ // Note: the two maps on which we are iterating should contain only one entry in this case.
+ 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,
+ path.getKey());
}
- 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 {
+ getLog().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} in case where the classifier is not {@code null} and contains something else 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..fb72aea8
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/jar/Archive.java
@@ -0,0 +1,579 @@
+/*
+ * 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.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} may contain many directories for different target Java releases.
+ * Many instances of {@code Archive} may 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. May be an existing file,
+ * in which case the file creation may 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. The manifest file
+ * should be included by the {@code --manifest} option instead of as an ordinary file.
+ */
+ @Nullable
+ private Path manifest;
+
+ /**
+ * The Maven generated {@code pom.xml} and {@code pom.properties} files, or {@code null} if none.
+ * This first item shall 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 shall 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.
+ * May be absolute paths or paths relative to {@link #directory}.
+ */
+ @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) {
+ file = directory.resolve(file);
+ 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.
+ * This method may store a relative path instead of the absolute path.
+ *
+ * @param item a file or directory to archive
+ * @param attributes the file's basic attributes
+ * @param isDirectory whether the file is a directory
+ * @throws IllegalArgumentException if the given path cannot be made relative to the base 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.
+ }
+ if (files.isEmpty()) {
+ /*
+ * In our tests, it seems that the first file after the "-C" option needs to be relative
+ * to the directory given to "-C" and all other files need to be absolute. This behavior
+ * does not seem to be documented, but we couldn't get the "jar" tool to work otherwise
+ * (except by repeating "-C" before each file).
+ */
+ item = directory.relativize(item);
+ }
+ files.add(item);
+ }
+
+ /**
+ * Adds to the given list the arguments to provide to the "jar" tool for this version.
+ * Elements added to the list shall be instances of {@link String} or {@link Path}.
+ *
+ * @param addTo the list where to add the arguments as {@link String} or {@link Path} instances
+ * @param version the target Java release, or {@code null} for the base version of the JAR file
+ */
+ private void arguments(List