blob: a70901846f52319bb1b1e93791083236c5f85709 [file] [log] [blame]
package org.apache.maven.plugins.shade;
/*
* 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.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.shade.filter.Filter;
import org.apache.maven.plugins.shade.relocation.Relocator;
import org.apache.maven.plugins.shade.relocation.SimpleRelocator;
import org.apache.maven.plugins.shade.resource.AppendingTransformer;
import org.apache.maven.plugins.shade.resource.ComponentsXmlResourceTransformer;
import org.apache.maven.plugins.shade.resource.ResourceTransformer;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.Os;
import org.junit.Assert;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.ArgumentCaptor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Opcodes;
import org.slf4j.Logger;
import static java.util.Objects.requireNonNull;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.hasItems;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* @author Jason van Zyl
* @author Mauro Talevi
*/
public class DefaultShaderTest
{
private static final String[] EXCLUDES = new String[] { "org/codehaus/plexus/util/xml/Xpp3Dom",
"org/codehaus/plexus/util/xml/pull.*" };
@Test
public void testNoopWhenNotRelocated() throws IOException, MojoExecutionException {
final File plexusJar = new File("src/test/jars/plexus-utils-1.4.1.jar" );
final File shadedOutput = new File( "target/foo-custom_testNoopWhenNotRelocated.jar" );
final Set<File> jars = new LinkedHashSet<>();
jars.add( new File( "src/test/jars/test-project-1.0-SNAPSHOT.jar" ) );
jars.add( plexusJar );
final Relocator relocator = new SimpleRelocator(
"org/codehaus/plexus/util/cli", "relocated/plexus/util/cli",
Collections.<String>emptyList(), Collections.<String>emptyList() );
final ShadeRequest shadeRequest = new ShadeRequest();
shadeRequest.setJars( jars );
shadeRequest.setRelocators( Collections.singletonList( relocator ) );
shadeRequest.setResourceTransformers( Collections.<ResourceTransformer>emptyList() );
shadeRequest.setFilters( Collections.<Filter>emptyList() );
shadeRequest.setUberJar( shadedOutput );
final DefaultShader shader = newShader();
shader.shade( shadeRequest );
try ( final JarFile originalJar = new JarFile( plexusJar );
final JarFile shadedJar = new JarFile( shadedOutput ) )
{
// ASM processes all class files. In doing so, it modifies them, even when not relocating anything.
// Before MSHADE-391, the processed files were written to the uber JAR, which did no harm, but made it
// difficult to find out by simple file comparison, if a file was actually relocated or not. Now, Shade
// makes sure to always write the original file if the class neither was relocated itself nor references
// other, relocated classes. So we are checking for regressions here.
assertTrue( areEqual( originalJar, shadedJar,
"org/codehaus/plexus/util/Expand.class" ) );
// Relocated files should always be different, because they contain different package names in their byte
// code. We should verify this anyway, in order to avoid an existing class file from simply being moved to
// another location without actually having been relocated internally.
assertFalse( areEqual(
originalJar, shadedJar,
"org/codehaus/plexus/util/cli/Arg.class", "relocated/plexus/util/cli/Arg.class" ) );
}
int result = 0;
for ( final String msg : debugMessages.getAllValues() )
{
if ( "Rewrote class bytecode: org/codehaus/plexus/util/cli/Arg.class".equals(msg) )
{
result |= 1;
}
else if ( "Keeping original class bytecode: org/codehaus/plexus/util/Expand.class".equals(msg) )
{
result |= 2;
}
}
assertEquals( 3 /* 1 | 2 */ , result);
}
@Test
public void testOverlappingResourcesAreLogged() throws IOException, MojoExecutionException {
final DefaultShader shader = newShader();
// we will shade two jars and expect to see META-INF/MANIFEST.MF overlaps, this will always be true
// but this can lead to a broken deployment if intended for OSGi or so, so even this should be logged
final Set<File> set = new LinkedHashSet<>();
set.add( new File( "src/test/jars/test-project-1.0-SNAPSHOT.jar" ) );
set.add( new File( "src/test/jars/plexus-utils-1.4.1.jar" ) );
final ShadeRequest shadeRequest = new ShadeRequest();
shadeRequest.setJars( set );
shadeRequest.setRelocators( Collections.<Relocator>emptyList() );
shadeRequest.setResourceTransformers( Collections.<ResourceTransformer>emptyList() );
shadeRequest.setFilters( Collections.<Filter>emptyList() );
shadeRequest.setUberJar( new File( "target/foo-custom_testOverlappingResourcesAreLogged.jar" ) );
shader.shade( shadeRequest );
assertThat(warnMessages.getAllValues(),
hasItem(containsString("plexus-utils-1.4.1.jar, test-project-1.0-SNAPSHOT.jar define 1 overlapping resource:")));
assertThat(warnMessages.getAllValues(),
hasItem(containsString("- META-INF/MANIFEST.MF")));
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
assertThat(debugMessages.getAllValues(),
hasItem(containsString("We have a duplicate META-INF/MANIFEST.MF in src\\test\\jars\\plexus-utils-1.4.1.jar")));
}
else {
assertThat(debugMessages.getAllValues(),
hasItem(containsString("We have a duplicate META-INF/MANIFEST.MF in src/test/jars/plexus-utils-1.4.1.jar")));
}
}
@Test
public void testOverlappingResourcesAreLoggedExceptATransformerHandlesIt() throws Exception {
TemporaryFolder temporaryFolder = new TemporaryFolder();
try {
Set<File> set = new LinkedHashSet<>();
temporaryFolder.create();
File j1 = temporaryFolder.newFile("j1.jar");
try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(j1))) {
jos.putNextEntry(new JarEntry("foo.txt"));
jos.write("c1".getBytes(StandardCharsets.UTF_8));
jos.closeEntry();
}
File j2 = temporaryFolder.newFile("j2.jar");
try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(j2))) {
jos.putNextEntry(new JarEntry("foo.txt"));
jos.write("c2".getBytes(StandardCharsets.UTF_8));
jos.closeEntry();
}
set.add(j1);
set.add(j2);
AppendingTransformer transformer = new AppendingTransformer();
Field resource = AppendingTransformer.class.getDeclaredField("resource");
resource.setAccessible(true);
resource.set(transformer, "foo.txt");
ShadeRequest shadeRequest = new ShadeRequest();
shadeRequest.setJars(set);
shadeRequest.setRelocators(Collections.<Relocator>emptyList());
shadeRequest.setResourceTransformers(Collections.<ResourceTransformer>singletonList(transformer));
shadeRequest.setFilters(Collections.<Filter>emptyList());
shadeRequest.setUberJar(new File("target/foo-custom_testOverlappingResourcesAreLogged.jar"));
DefaultShader shaderWithTransformer = newShader();
shaderWithTransformer.shade(shadeRequest);
assertThat(warnMessages.getAllValues().size(), is(0) );
DefaultShader shaderWithoutTransformer = newShader();
shadeRequest.setResourceTransformers(Collections.<ResourceTransformer>emptyList());
shaderWithoutTransformer.shade(shadeRequest);
assertThat(warnMessages.getAllValues(),
hasItems(containsString("j1.jar, j2.jar define 1 overlapping resource:")));
assertThat(warnMessages.getAllValues(),
hasItems(containsString("- foo.txt")));
}
finally {
temporaryFolder.delete();
}
}
@Test
public void testShaderWithDefaultShadedPattern()
throws Exception
{
shaderWithPattern( null, new File( "target/foo-default.jar" ), EXCLUDES );
}
@Test
public void testShaderWithStaticInitializedClass()
throws Exception
{
Shader s = newShader();
Set<File> set = new LinkedHashSet<>();
set.add( new File( "src/test/jars/test-artifact-1.0-SNAPSHOT.jar" ) );
List<Relocator> relocators = new ArrayList<>();
relocators.add( new SimpleRelocator( "org.apache.maven.plugins.shade", null, null, null ) );
List<ResourceTransformer> resourceTransformers = new ArrayList<>();
List<Filter> filters = new ArrayList<>();
File file = new File( "target/testShaderWithStaticInitializedClass.jar" );
ShadeRequest shadeRequest = new ShadeRequest();
shadeRequest.setJars( set );
shadeRequest.setUberJar( file );
shadeRequest.setFilters( filters );
shadeRequest.setRelocators( relocators );
shadeRequest.setResourceTransformers( resourceTransformers );
s.shade( shadeRequest );
try ( URLClassLoader cl = new URLClassLoader( new URL[] { file.toURI().toURL() } ) ) {
Class<?> c = cl.loadClass( "hidden.org.apache.maven.plugins.shade.Lib" );
Object o = c.newInstance();
assertEquals( "foo.bar/baz", c.getDeclaredField( "CONSTANT" ).get( o ) );
}
}
@Test
public void testShaderWithCustomShadedPattern()
throws Exception
{
shaderWithPattern( "org/shaded/plexus/util", new File( "target/foo-custom.jar" ), EXCLUDES );
}
@Test
public void testShaderWithoutExcludesShouldRemoveReferencesOfOriginalPattern()
throws Exception
{
// FIXME: shaded jar should not include references to org/codehaus/* (empty dirs) or org.codehaus.* META-INF
// files.
shaderWithPattern( "org/shaded/plexus/util", new File( "target/foo-custom-without-excludes.jar" ),
new String[] {} );
}
@Test
public void testShaderWithRelocatedClassname()
throws Exception
{
DefaultShader s = newShader();
Set<File> set = new LinkedHashSet<>();
set.add( new File( "src/test/jars/test-project-1.0-SNAPSHOT.jar" ) );
set.add( new File( "src/test/jars/plexus-utils-1.4.1.jar" ) );
List<Relocator> relocators = new ArrayList<>();
relocators.add( new SimpleRelocator( "org/codehaus/plexus/util/", "_plexus/util/__", null,
Collections.<String>emptyList() ) );
List<ResourceTransformer> resourceTransformers = new ArrayList<>();
resourceTransformers.add( new ComponentsXmlResourceTransformer() );
List<Filter> filters = new ArrayList<>();
File file = new File( "target/foo-relocate-class.jar" );
ShadeRequest shadeRequest = new ShadeRequest();
shadeRequest.setJars( set );
shadeRequest.setUberJar( file );
shadeRequest.setFilters( filters );
shadeRequest.setRelocators( relocators );
shadeRequest.setResourceTransformers( resourceTransformers );
s.shade( shadeRequest );
try ( URLClassLoader cl = new URLClassLoader( new URL[] { file.toURI().toURL() } ) ) {
Class<?> c = cl.loadClass( "_plexus.util.__StringUtils" );
// first, ensure it works:
Object o = c.newInstance();
assertEquals( "", c.getMethod( "clean", String.class ).invoke( o, (String) null ) );
// now, check that its source file was rewritten:
final String[] source = { null };
final ClassReader classReader = new ClassReader( cl.getResourceAsStream( "_plexus/util/__StringUtils.class" ) );
classReader.accept( new ClassVisitor( Opcodes.ASM4 )
{
@Override
public void visitSource( String arg0, String arg1 )
{
super.visitSource( arg0, arg1 );
source[0] = arg0;
}
}, ClassReader.SKIP_CODE );
assertEquals( "__StringUtils.java", source[0] );
}
}
@Test
public void testShaderWithNestedJar() throws Exception
{
TemporaryFolder temporaryFolder = new TemporaryFolder();
final String innerJarFileName = "inner.jar";
temporaryFolder.create();
File innerJar = temporaryFolder.newFile( innerJarFileName );
try ( JarOutputStream jos = new JarOutputStream( new FileOutputStream( innerJar ) ) )
{
jos.putNextEntry( new JarEntry( "foo.txt" ) );
jos.write( "c1".getBytes( StandardCharsets.UTF_8 ) );
jos.closeEntry();
}
File outerJar = temporaryFolder.newFile( "outer.jar" );
try ( JarOutputStream jos = new JarOutputStream( new FileOutputStream( outerJar ) ) )
{
FileInputStream innerStream = new FileInputStream( innerJar );
byte[] bytes = IOUtil.toByteArray( innerStream, 32 * 1024 );
innerStream.close();
writeEntryWithoutCompression( innerJarFileName, bytes, jos );
}
ShadeRequest shadeRequest = new ShadeRequest();
shadeRequest.setJars( new LinkedHashSet<>( Collections.singleton( outerJar ) ) );
shadeRequest.setFilters( new ArrayList<Filter>() );
shadeRequest.setRelocators( new ArrayList<Relocator>() );
shadeRequest.setResourceTransformers( new ArrayList<ResourceTransformer>() );
File shadedFile = temporaryFolder.newFile( "shaded.jar" );
shadeRequest.setUberJar( shadedFile );
DefaultShader shader = newShader();
shader.shade( shadeRequest );
JarFile shadedJarFile = new JarFile( shadedFile );
JarEntry entry = shadedJarFile.getJarEntry( innerJarFileName );
//After shading, entry compression method should not be changed.
Assert.assertEquals( entry.getMethod(), ZipEntry.STORED );
temporaryFolder.delete();
}
private void writeEntryWithoutCompression( String entryName, byte[] entryBytes, JarOutputStream jos ) throws IOException
{
final JarEntry entry = new JarEntry( entryName );
final int size = entryBytes.length;
final CRC32 crc = new CRC32();
crc.update( entryBytes, 0, size );
entry.setSize( size );
entry.setCompressedSize( size );
entry.setMethod( ZipEntry.STORED );
entry.setCrc( crc.getValue() );
jos.putNextEntry( entry );
jos.write( entryBytes );
jos.closeEntry();
}
private void shaderWithPattern( String shadedPattern, File jar, String[] excludes )
throws Exception
{
DefaultShader s = newShader();
Set<File> set = new LinkedHashSet<>();
set.add( new File( "src/test/jars/test-project-1.0-SNAPSHOT.jar" ) );
set.add( new File( "src/test/jars/plexus-utils-1.4.1.jar" ) );
List<Relocator> relocators = new ArrayList<>();
relocators.add( new SimpleRelocator( "org/codehaus/plexus/util", shadedPattern, null, Arrays.asList( excludes ) ) );
List<ResourceTransformer> resourceTransformers = new ArrayList<>();
resourceTransformers.add( new ComponentsXmlResourceTransformer() );
List<Filter> filters = new ArrayList<>();
ShadeRequest shadeRequest = new ShadeRequest();
shadeRequest.setJars( set );
shadeRequest.setUberJar( jar );
shadeRequest.setFilters( filters );
shadeRequest.setRelocators( relocators );
shadeRequest.setResourceTransformers( resourceTransformers );
s.shade( shadeRequest );
}
private DefaultShader newShader()
{
return new DefaultShader(mockLogger());
}
private ArgumentCaptor<String> debugMessages;
private ArgumentCaptor<String> warnMessages;
private Logger mockLogger()
{
debugMessages = ArgumentCaptor.forClass(String.class);
warnMessages = ArgumentCaptor.forClass(String.class);
Logger logger = mock(Logger.class);
when(logger.isDebugEnabled()).thenReturn(true);
when(logger.isWarnEnabled()).thenReturn(true);
doNothing().when(logger).debug(debugMessages.capture());
doNothing().when(logger).warn(warnMessages.capture());
return logger;
}
private boolean areEqual( final JarFile jar1, final JarFile jar2, final String entry ) throws IOException
{
return areEqual( jar1, jar2, entry, entry );
}
private boolean areEqual( final JarFile jar1, final JarFile jar2, final String entry1, String entry2 )
throws IOException
{
try ( final InputStream s1 = jar1.getInputStream(
requireNonNull(jar1.getJarEntry(entry1), entry1 + " in " + jar1.getName() ) );
final InputStream s2 = jar2.getInputStream(
requireNonNull(jar2.getJarEntry(entry2), entry2 + " in " + jar2.getName() ) ))
{
return Arrays.equals( IOUtil.toByteArray( s1 ), IOUtil.toByteArray( s2 ) );
}
}
}