diff --git a/src/it/projects/MSHADE-406/app/pom.xml b/src/it/projects/MSHADE-406/app/pom.xml new file mode 100644 index 000000000..c4aafb13d --- /dev/null +++ b/src/it/projects/MSHADE-406/app/pom.xml @@ -0,0 +1,78 @@ + + + + + + 4.0.0 + + + org.apache.maven.its.shade.mshade406 + mshade-406-parent + 1.0 + + + app + + + + org.apache.maven.its.shade.mshade406 + dependency + ${project.version} + + + + + + + maven-compiler-plugin + @version.maven-compiler-plugin@ + + 8 + + + + maven-shade-plugin + @project.version@ + + + package + + shade + + + true + + + reproducer.mr + reproducer.shaded.mr + + + + + reproducer.app.Main + + + + + + + + + diff --git a/src/it/projects/MSHADE-406/app/src/main/java/reproducer/app/Main.java b/src/it/projects/MSHADE-406/app/src/main/java/reproducer/app/Main.java new file mode 100644 index 000000000..790667b07 --- /dev/null +++ b/src/it/projects/MSHADE-406/app/src/main/java/reproducer/app/Main.java @@ -0,0 +1,37 @@ +/* + * 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 reproducer.app; + +import java.io.InputStream; +import java.util.Properties; + +import reproducer.mr.Versioned; + +public final class Main { + public static void main(String[] args) throws Exception { + try (InputStream input = Versioned.class.getResourceAsStream("config.properties")) { + if (input == null) { + throw new IllegalStateException("Missing relocated resource"); + } + Properties properties = new Properties(); + properties.load(input); + System.out.println(Versioned.value() + ":" + properties.getProperty("value")); + } + } +} diff --git a/src/it/projects/MSHADE-406/dependency/pom.xml b/src/it/projects/MSHADE-406/dependency/pom.xml new file mode 100644 index 000000000..bcfccce2b --- /dev/null +++ b/src/it/projects/MSHADE-406/dependency/pom.xml @@ -0,0 +1,83 @@ + + + + + + 4.0.0 + + + org.apache.maven.its.shade.mshade406 + mshade-406-parent + 1.0 + + + dependency + + + + + src/main/resources + + + src/main/resources11 + META-INF/versions/11 + + + + + maven-compiler-plugin + @version.maven-compiler-plugin@ + + + default-compile + + 8 + + + + compile-java11 + compile + + compile + + + 11 + + ${project.basedir}/src/main/java11 + + ${project.build.outputDirectory}/META-INF/versions/11 + + + + + + maven-jar-plugin + @version.maven-jar-plugin@ + + + + true + + + + + + + diff --git a/src/it/projects/MSHADE-406/dependency/src/main/java/reproducer/mr/Unused.java b/src/it/projects/MSHADE-406/dependency/src/main/java/reproducer/mr/Unused.java new file mode 100644 index 000000000..cee73a43b --- /dev/null +++ b/src/it/projects/MSHADE-406/dependency/src/main/java/reproducer/mr/Unused.java @@ -0,0 +1,21 @@ +/* + * 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 reproducer.mr; + +public final class Unused {} diff --git a/src/it/projects/MSHADE-406/dependency/src/main/java/reproducer/mr/Versioned.java b/src/it/projects/MSHADE-406/dependency/src/main/java/reproducer/mr/Versioned.java new file mode 100644 index 000000000..0641ed97a --- /dev/null +++ b/src/it/projects/MSHADE-406/dependency/src/main/java/reproducer/mr/Versioned.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 reproducer.mr; + +public final class Versioned { + public static String value() { + return "base"; + } +} diff --git a/src/it/projects/MSHADE-406/dependency/src/main/java11/module-info.java b/src/it/projects/MSHADE-406/dependency/src/main/java11/module-info.java new file mode 100644 index 000000000..58568179f --- /dev/null +++ b/src/it/projects/MSHADE-406/dependency/src/main/java11/module-info.java @@ -0,0 +1,21 @@ +/* + * 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 reproducer.mr { + exports reproducer.mr; +} diff --git a/src/it/projects/MSHADE-406/dependency/src/main/java11/reproducer/mr/Unused.java b/src/it/projects/MSHADE-406/dependency/src/main/java11/reproducer/mr/Unused.java new file mode 100644 index 000000000..cee73a43b --- /dev/null +++ b/src/it/projects/MSHADE-406/dependency/src/main/java11/reproducer/mr/Unused.java @@ -0,0 +1,21 @@ +/* + * 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 reproducer.mr; + +public final class Unused {} diff --git a/src/it/projects/MSHADE-406/dependency/src/main/java11/reproducer/mr/Versioned.java b/src/it/projects/MSHADE-406/dependency/src/main/java11/reproducer/mr/Versioned.java new file mode 100644 index 000000000..3e9b7c087 --- /dev/null +++ b/src/it/projects/MSHADE-406/dependency/src/main/java11/reproducer/mr/Versioned.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 reproducer.mr; + +public final class Versioned { + public static String value() { + return "java11"; + } +} diff --git a/src/it/projects/MSHADE-406/dependency/src/main/resources/reproducer/mr/config.properties b/src/it/projects/MSHADE-406/dependency/src/main/resources/reproducer/mr/config.properties new file mode 100644 index 000000000..59549b0cb --- /dev/null +++ b/src/it/projects/MSHADE-406/dependency/src/main/resources/reproducer/mr/config.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. + +value=root-resource diff --git a/src/it/projects/MSHADE-406/dependency/src/main/resources11/reproducer/mr/config.properties b/src/it/projects/MSHADE-406/dependency/src/main/resources11/reproducer/mr/config.properties new file mode 100644 index 000000000..7f01d67ba --- /dev/null +++ b/src/it/projects/MSHADE-406/dependency/src/main/resources11/reproducer/mr/config.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. + +value=java11-resource diff --git a/src/it/projects/MSHADE-406/invoker.properties b/src/it/projects/MSHADE-406/invoker.properties new file mode 100644 index 000000000..4b6d43be5 --- /dev/null +++ b/src/it/projects/MSHADE-406/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.java.version = 17+ diff --git a/src/it/projects/MSHADE-406/pom.xml b/src/it/projects/MSHADE-406/pom.xml new file mode 100644 index 000000000..acf745e44 --- /dev/null +++ b/src/it/projects/MSHADE-406/pom.xml @@ -0,0 +1,38 @@ + + + + + + 4.0.0 + + org.apache.maven.its.shade.mshade406 + mshade-406-parent + 1.0 + pom + + + dependency + app + + + + UTF-8 + + diff --git a/src/it/projects/MSHADE-406/verify.groovy b/src/it/projects/MSHADE-406/verify.groovy new file mode 100644 index 000000000..8f774263d --- /dev/null +++ b/src/it/projects/MSHADE-406/verify.groovy @@ -0,0 +1,56 @@ +/* + * 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. + */ + +def shadedJar = new File( basedir, "app/target/app-1.0.jar" ) +assert shadedJar.isFile() + +def jar = new java.util.jar.JarFile( shadedJar ) +try +{ + assert jar.manifest.mainAttributes.getValue( "Multi-Release" ) == "true" + assert jar.getJarEntry( "reproducer/shaded/mr/Versioned.class" ) != null + assert jar.getJarEntry( + "META-INF/versions/11/reproducer/shaded/mr/Versioned.class" ) != null + assert jar.getJarEntry( "reproducer/shaded/mr/config.properties" ) != null + assert jar.getJarEntry( + "META-INF/versions/11/reproducer/shaded/mr/config.properties" ) != null + + assert jar.getJarEntry( "reproducer/mr/Versioned.class" ) == null + assert jar.getJarEntry( "META-INF/versions/11/reproducer/mr/Versioned.class" ) == null + assert jar.getJarEntry( "reproducer/mr/config.properties" ) == null + assert jar.getJarEntry( + "META-INF/versions/11/reproducer/mr/config.properties" ) == null + + assert jar.getJarEntry( "reproducer/shaded/mr/Unused.class" ) == null + assert jar.getJarEntry( + "META-INF/versions/11/reproducer/shaded/mr/Unused.class" ) == null + assert jar.getJarEntry( "META-INF/versions/11/module-info.class" ) == null +} +finally +{ + jar.close() +} + +def java = new File( System.getProperty( "java.home" ), "bin/java" ) +def process = new ProcessBuilder( java.absolutePath, "-jar", shadedJar.absolutePath ) + .redirectErrorStream( true ) + .start() +def output = process.inputStream.getText( "UTF-8" ) +assert process.waitFor() == 0 : output +assert output.trim() == "java11:java11-resource" diff --git a/src/it/projects/MSHADE-460/invoker.properties b/src/it/projects/MSHADE-460/invoker.properties new file mode 100644 index 000000000..4b6d43be5 --- /dev/null +++ b/src/it/projects/MSHADE-460/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.java.version = 17+ diff --git a/src/it/projects/MSHADE-460/pom.xml b/src/it/projects/MSHADE-460/pom.xml new file mode 100644 index 000000000..890196487 --- /dev/null +++ b/src/it/projects/MSHADE-460/pom.xml @@ -0,0 +1,75 @@ + + + + + + 4.0.0 + + org.apache.maven.its.shade.mshade460 + mshade-460 + 1.0 + + + 17 + UTF-8 + + + + + jakarta.json.bind + jakarta.json.bind-api + 3.0.0 + + + org.eclipse + yasson + 3.0.3 + + + + + + + maven-compiler-plugin + @version.maven-compiler-plugin@ + + + maven-shade-plugin + @project.version@ + + + package + + shade + + + + + org.apache.maven.its.shade.mshade460.Main + + + + + + + + + + diff --git a/src/it/projects/MSHADE-460/src/main/java/org/apache/maven/its/shade/mshade460/Main.java b/src/it/projects/MSHADE-460/src/main/java/org/apache/maven/its/shade/mshade460/Main.java new file mode 100644 index 000000000..7f74a4402 --- /dev/null +++ b/src/it/projects/MSHADE-460/src/main/java/org/apache/maven/its/shade/mshade460/Main.java @@ -0,0 +1,33 @@ +/* + * 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.its.shade.mshade460; + +import java.time.Instant; + +import jakarta.json.bind.JsonbBuilder; + +public final class Main { + public record TimeInfo(Instant datetime) {} + + public static void main(String[] args) { + TimeInfo value = JsonbBuilder.create() + .fromJson("{\"datetime\":\"2023-10-29T12:00:00Z\"}", TimeInfo.class); + System.out.println(value); + } +} diff --git a/src/it/projects/MSHADE-460/verify.groovy b/src/it/projects/MSHADE-460/verify.groovy new file mode 100644 index 000000000..22ca7b4ad --- /dev/null +++ b/src/it/projects/MSHADE-460/verify.groovy @@ -0,0 +1,41 @@ +/* + * 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. + */ + +def shadedJar = new File( basedir, "target/mshade-460-1.0.jar" ) +assert shadedJar.isFile() + +def jar = new java.util.jar.JarFile( shadedJar ) +try +{ + assert jar.manifest.mainAttributes.getValue( "Multi-Release" ) == "true" + assert jar.getJarEntry( + "META-INF/versions/16/org/eclipse/yasson/internal/ClassMultiReleaseExtension.class" ) != null +} +finally +{ + jar.close() +} + +def java = new File( System.getProperty( "java.home" ), "bin/java" ) +def process = new ProcessBuilder( java.absolutePath, "-jar", shadedJar.absolutePath ) + .redirectErrorStream( true ) + .start() +def output = process.inputStream.getText( "UTF-8" ) +assert process.waitFor() == 0 : output +assert output.trim() == "TimeInfo[datetime=2023-10-29T12:00:00Z]" diff --git a/src/main/java/org/apache/maven/plugins/shade/DefaultShader.java b/src/main/java/org/apache/maven/plugins/shade/DefaultShader.java index d4f99674d..53c65413a 100644 --- a/src/main/java/org/apache/maven/plugins/shade/DefaultShader.java +++ b/src/main/java/org/apache/maven/plugins/shade/DefaultShader.java @@ -52,6 +52,7 @@ import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.CRC32; @@ -82,6 +83,12 @@ public class DefaultShader implements Shader { private static final int BUFFER_SIZE = 32 * 1024; + private static final Pattern MULTI_RELEASE_ENTRY_PATH = + Pattern.compile("^(META-INF/versions/(?:9|[1-9][0-9]+)/)(.+)$"); + + private static final Pattern VERSIONED_MODULE_INFO = + Pattern.compile("^META-INF/versions/(?:9|[1-9][0-9]+)/module-info[.]class$"); + private final Logger logger; public DefaultShader() { @@ -143,6 +150,9 @@ public void shade(ShadeRequest shadeRequest) throws IOException, MojoExecutionEx it.remove(); } } + if (manifestTransformer == null && containsMultiReleaseJar(shadeRequest.getJars())) { + manifestTransformer = new ManifestResourceTransformer(); + } final DefaultPackageMapper packageMapper = new DefaultPackageMapper(shadeRequest.getRelocators()); @@ -393,7 +403,8 @@ private boolean isExcludedEntry(final String name) { return true; } - if ("module-info.class".equals(name)) { + if ("module-info.class".equals(name) + || VERSIONED_MODULE_INFO.matcher(name).matches()) { logger.warn("Discovered module-info.class. " + "Shading will break its strong encapsulation."); return true; } @@ -415,7 +426,7 @@ private void shadeJarEntry( int method) throws Exception { try (InputStream in = inputProvider.call()) { - String mappedName = packageMapper.map(name, true, false); + String mappedName = ArchiveEntry.parse(name).map(packageMapper); int idx = mappedName.lastIndexOf('/'); if (idx != -1) { @@ -460,19 +471,20 @@ private void goThroughAllJarEntriesForManifestTransformer( throws IOException { if (manifestTransformer != null) { for (File jar : shadeRequest.getJars()) { - try (JarFile jarFile = newJarFile(jar)) { - for (Enumeration en = jarFile.entries(); en.hasMoreElements(); ) { - JarEntry entry = en.nextElement(); - String resource = entry.getName(); - if (manifestTransformer.canTransformResource(resource)) { - resources.add(resource); - try (InputStream inputStream = jarFile.getInputStream(entry)) { - manifestTransformer.processResource( - resource, inputStream, shadeRequest.getRelocators(), getTime(entry)); - } - break; + if (jar.isDirectory()) { + File manifestFile = new File(jar, JarFile.MANIFEST_NAME); + if (manifestFile.isFile()) { + resources.add(JarFile.MANIFEST_NAME); + try (InputStream inputStream = Files.newInputStream(manifestFile.toPath())) { + manifestTransformer.processResource( + JarFile.MANIFEST_NAME, + inputStream, + shadeRequest.getRelocators(), + manifestFile.lastModified()); } } + } else { + processManifest(jar, shadeRequest, resources, manifestTransformer); } } if (manifestTransformer.hasTransformedResource()) { @@ -481,6 +493,49 @@ private void goThroughAllJarEntriesForManifestTransformer( } } + private void processManifest( + File jar, ShadeRequest shadeRequest, Set resources, ManifestResourceTransformer manifestTransformer) + throws IOException { + try (JarFile jarFile = newJarFile(jar)) { + for (Enumeration en = jarFile.entries(); en.hasMoreElements(); ) { + JarEntry entry = en.nextElement(); + String resource = entry.getName(); + if (manifestTransformer.canTransformResource(resource)) { + resources.add(resource); + try (InputStream inputStream = jarFile.getInputStream(entry)) { + manifestTransformer.processResource( + resource, inputStream, shadeRequest.getRelocators(), getTime(entry)); + } + break; + } + } + } + } + + private boolean containsMultiReleaseJar(Collection jars) throws IOException { + for (File jar : jars) { + Manifest manifest; + if (jar.isDirectory()) { + File manifestFile = new File(jar, JarFile.MANIFEST_NAME); + if (!manifestFile.isFile()) { + continue; + } + try (InputStream inputStream = Files.newInputStream(manifestFile.toPath())) { + manifest = new Manifest(inputStream); + } + } else { + try (JarFile jarFile = newJarFile(jar)) { + manifest = jarFile.getManifest(); + } + } + if (manifest != null + && Boolean.parseBoolean(manifest.getMainAttributes().getValue("Multi-Release"))) { + return true; + } + } + return false; + } + private void showOverlappingWarning() { logger.warn("maven-shade-plugin has detected that some files are"); logger.warn("present in two or more JARs. When this happens, only one"); @@ -619,7 +674,8 @@ private void addRemappedClass( // that use the constant pool to determine the dependencies of a class. ClassWriter cw = new ClassWriter(0); - final String pkg = name.substring(0, name.lastIndexOf('/') + 1); + ArchiveEntry classEntry = ArchiveEntry.parse(name); + final String pkg = classEntry.logicalName.substring(0, classEntry.logicalName.lastIndexOf('/') + 1); final ShadeClassRemapper cv = new ShadeClassRemapper(cw, pkg, packageMapper); try { @@ -638,12 +694,10 @@ private void addRemappedClass( renamedClass = originalClass; } - // Need to take the .class off for remapping evaluation - String mappedName = packageMapper.map(name.substring(0, name.indexOf('.')), true, false); + String mappedName = classEntry.mapClass(packageMapper); try { - // Now we put it back on so the class file is written out with the right extension. - JarEntry entry = new JarEntry(mappedName + ".class"); + JarEntry entry = new JarEntry(mappedName); entry.setTime(time); jos.putNextEntry(entry); @@ -653,6 +707,33 @@ private void addRemappedClass( } } + private static final class ArchiveEntry { + private final String versionPrefix; + + private final String logicalName; + + private ArchiveEntry(String versionPrefix, String logicalName) { + this.versionPrefix = versionPrefix; + this.logicalName = logicalName; + } + + private static ArchiveEntry parse(String name) { + Matcher matcher = MULTI_RELEASE_ENTRY_PATH.matcher(name); + return matcher.matches() + ? new ArchiveEntry(matcher.group(1), matcher.group(2)) + : new ArchiveEntry("", name); + } + + private String map(PackageMapper packageMapper) { + return versionPrefix + packageMapper.map(logicalName, true, false); + } + + private String mapClass(PackageMapper packageMapper) { + String name = logicalName.substring(0, logicalName.length() - ".class".length()); + return versionPrefix + packageMapper.map(name, true, false) + ".class"; + } + } + private boolean isFiltered(List filters, String name) { for (Filter filter : filters) { if (filter.isFiltered(name)) { diff --git a/src/main/java/org/apache/maven/plugins/shade/filter/MinijarFilter.java b/src/main/java/org/apache/maven/plugins/shade/filter/MinijarFilter.java index c23e5c20f..db839732d 100644 --- a/src/main/java/org/apache/maven/plugins/shade/filter/MinijarFilter.java +++ b/src/main/java/org/apache/maven/plugins/shade/filter/MinijarFilter.java @@ -327,11 +327,15 @@ public boolean canFilter(File jar) { @Override public boolean isFiltered(String classFile) { - String className = classFile.replace('/', '.').replaceFirst("\\.class$", ""); - Clazz clazz = new Clazz(className); + Clazz.ParsedFileName parsedFileName = Clazz.parseClassFileName(classFile); + if (parsedFileName == null) { + classesKept += 1; + return false; + } + Clazz clazz = new Clazz(parsedFileName.className); if (removable != null && removable.contains(clazz)) { - log.debug("Removing " + className); + log.debug("Removing " + parsedFileName.className); classesRemoved += 1; return true; } diff --git a/src/main/java/org/apache/maven/plugins/shade/resource/ManifestResourceTransformer.java b/src/main/java/org/apache/maven/plugins/shade/resource/ManifestResourceTransformer.java index b9d63b37d..8cc23995f 100644 --- a/src/main/java/org/apache/maven/plugins/shade/resource/ManifestResourceTransformer.java +++ b/src/main/java/org/apache/maven/plugins/shade/resource/ManifestResourceTransformer.java @@ -30,6 +30,8 @@ import java.util.jar.Manifest; import org.apache.maven.plugins.shade.relocation.Relocator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * A resource processor that allows the arbitrary addition of attributes to @@ -40,6 +42,10 @@ * @since 1.2 */ public class ManifestResourceTransformer extends AbstractCompatibilityTransformer { + private static final Logger LOGGER = LoggerFactory.getLogger(ManifestResourceTransformer.class); + + private static final Attributes.Name MULTI_RELEASE = new Attributes.Name("Multi-Release"); + private final List defaultAttributes = Arrays.asList("Export-Package", "Import-Package", "Provide-Capability", "Require-Capability"); @@ -55,6 +61,8 @@ public class ManifestResourceTransformer extends AbstractCompatibilityTransforme private Manifest manifest; + private boolean multiRelease; + private long time = Long.MIN_VALUE; private String shade; @@ -79,11 +87,15 @@ public boolean canTransformResource(String resource) { @Override public void processResource(String resource, InputStream is, List relocators, long time) throws IOException { + Manifest incomingManifest = new Manifest(is); + multiRelease |= + Boolean.parseBoolean(incomingManifest.getMainAttributes().getValue(MULTI_RELEASE)); + // We just want to take the first manifest we come across as that's our project's manifest. This is the behavior // now which is situational at best. Right now there is no context passed in with the processing so we cannot // tell what artifact is being processed. if (!manifestDiscovered) { - manifest = new Manifest(is); + manifest = incomingManifest; if (relocators != null && !relocators.isEmpty()) { final Attributes attributes = manifest.getMainAttributes(); @@ -129,6 +141,10 @@ public void modifyOutputStream(JarOutputStream jos) throws IOException { Attributes attributes = manifest.getMainAttributes(); + if (multiRelease && attributes.getValue(MULTI_RELEASE) == null) { + attributes.put(MULTI_RELEASE, Boolean.TRUE.toString()); + } + if (mainClass != null) { attributes.put(Attributes.Name.MAIN_CLASS, mainClass); } @@ -143,6 +159,11 @@ public void modifyOutputStream(JarOutputStream jos) throws IOException { } } + if (multiRelease && !Boolean.parseBoolean(attributes.getValue(MULTI_RELEASE))) { + LOGGER.warn("Shaded inputs contain a multi-release JAR, but the shaded JAR is explicitly configured " + + "with Multi-Release: false. Versioned classes will not be active."); + } + JarEntry jarEntry = new JarEntry(JarFile.MANIFEST_NAME); jarEntry.setTime(time); jos.putNextEntry(jarEntry); diff --git a/src/test/java/org/apache/maven/plugins/shade/DefaultShaderTest.java b/src/test/java/org/apache/maven/plugins/shade/DefaultShaderTest.java index ed14ddf20..2fdd283da 100644 --- a/src/test/java/org/apache/maven/plugins/shade/DefaultShaderTest.java +++ b/src/test/java/org/apache/maven/plugins/shade/DefaultShaderTest.java @@ -45,6 +45,7 @@ import java.util.jar.JarFile; import java.util.jar.JarInputStream; import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; import java.util.stream.Collectors; import java.util.zip.CRC32; import java.util.zip.ZipEntry; @@ -152,6 +153,32 @@ public void testNoopWhenNotRelocated() throws IOException, MojoExecutionExceptio assertEquals(3 /* 1 | 2 */, result); } + @Test + public void testMultiReleaseManifestIsPropagatedWithoutTransformer() throws Exception { + TemporaryFolder temporaryFolder = new TemporaryFolder(); + try { + temporaryFolder.create(); + File projectJar = createJar(temporaryFolder.newFile("project.jar"), false); + File dependencyJar = createJar(temporaryFolder.newFile("dependency.jar"), true); + File shadedJar = temporaryFolder.newFile("shaded.jar"); + + ShadeRequest shadeRequest = new ShadeRequest(); + shadeRequest.setJars(new LinkedHashSet<>(Arrays.asList(projectJar, dependencyJar))); + shadeRequest.setRelocators(Collections.emptyList()); + shadeRequest.setResourceTransformers(Collections.emptyList()); + shadeRequest.setFilters(Collections.emptyList()); + shadeRequest.setUberJar(shadedJar); + + newShader().shade(shadeRequest); + + try (JarFile jarFile = new JarFile(shadedJar)) { + assertEquals("true", jarFile.getManifest().getMainAttributes().getValue("Multi-Release")); + } + } finally { + temporaryFolder.delete(); + } + } + @Test public void testOverlappingResourcesAreLogged() throws IOException, MojoExecutionException { DefaultShader shader = newShader(); @@ -566,6 +593,19 @@ private void writeEntryWithoutCompression(String entryName, byte[] entryBytes, J jos.closeEntry(); } + private File createJar(File file, boolean multiRelease) throws IOException { + Manifest manifest = new Manifest(); + manifest.getMainAttributes().putValue("Manifest-Version", "1.0"); + if (multiRelease) { + manifest.getMainAttributes().putValue("Multi-Release", "true"); + } + try (JarOutputStream jos = new JarOutputStream(Files.newOutputStream(file.toPath()), manifest)) { + jos.putNextEntry(new JarEntry((multiRelease ? "dependency" : "project") + ".txt")); + jos.closeEntry(); + } + return file; + } + private void shaderWithPattern(String shadedPattern, File jar, String[] excludes) throws Exception { Set set = new LinkedHashSet<>(); set.add(new File("src/test/jars/test-project-1.0-SNAPSHOT.jar")); diff --git a/src/test/java/org/apache/maven/plugins/shade/resource/ManifestResourceTransformerTest.java b/src/test/java/org/apache/maven/plugins/shade/resource/ManifestResourceTransformerTest.java index 76a1ae752..0c86c1875 100644 --- a/src/test/java/org/apache/maven/plugins/shade/resource/ManifestResourceTransformerTest.java +++ b/src/test/java/org/apache/maven/plugins/shade/resource/ManifestResourceTransformerTest.java @@ -168,14 +168,69 @@ public void rewriteAdditionalAttributes() throws Exception { } } + @Test + public void preserveMultiReleaseFromSubsequentManifest() throws Exception { + processManifest(createTestManifest(), Collections.emptyList()); + + Manifest dependencyManifest = new Manifest(); + dependencyManifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + dependencyManifest.getMainAttributes().putValue("Multi-Release", "true"); + processManifest(dependencyManifest, Collections.emptyList()); + + try (JarInputStream jis = + new JarInputStream(new ByteArrayInputStream(writeOutput().toByteArray()))) { + assertEquals("true", jis.getManifest().getMainAttributes().getValue("Multi-Release")); + } + } + + @Test + public void preserveExplicitMultiReleaseFalse() throws Exception { + Manifest projectManifest = createTestManifest(); + projectManifest.getMainAttributes().putValue("Multi-Release", "false"); + processManifest(projectManifest, Collections.emptyList()); + + Manifest dependencyManifest = new Manifest(); + dependencyManifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + dependencyManifest.getMainAttributes().putValue("Multi-Release", "true"); + processManifest(dependencyManifest, Collections.emptyList()); + + try (JarInputStream jis = + new JarInputStream(new ByteArrayInputStream(writeOutput().toByteArray()))) { + assertEquals("false", jis.getManifest().getMainAttributes().getValue("Multi-Release")); + } + } + + @Test + public void preserveConfiguredMultiReleaseFalse() throws Exception { + processManifest(createTestManifest(), Collections.emptyList()); + + Manifest dependencyManifest = new Manifest(); + dependencyManifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + dependencyManifest.getMainAttributes().putValue("Multi-Release", "true"); + processManifest(dependencyManifest, Collections.emptyList()); + transformer.setManifestEntries(Collections.singletonMap("Multi-Release", "false")); + + try (JarInputStream jis = + new JarInputStream(new ByteArrayInputStream(writeOutput().toByteArray()))) { + assertEquals("false", jis.getManifest().getMainAttributes().getValue("Multi-Release")); + } + } + private ByteArrayOutputStream transform(final Manifest manifest, List relocators) throws IOException { + processManifest(manifest, relocators); + return writeOutput(); + } + + private void processManifest(final Manifest manifest, List relocators) throws IOException { final ByteArrayOutputStream mboas = new ByteArrayOutputStream(); try (OutputStream mos = mboas) { manifest.write(mos); } transformer.processResource( JarFile.MANIFEST_NAME, new ByteArrayInputStream(mboas.toByteArray()), relocators, 0); + } + private ByteArrayOutputStream writeOutput() throws IOException { final ByteArrayOutputStream out = new ByteArrayOutputStream(); try (JarOutputStream jarOutputStream = new JarOutputStream(out)) { transformer.modifyOutputStream(jarOutputStream);