groupId
is equivalent to groupId:*:*:*
, groupId:artifactId
is
* equivalent to groupId:artifactId:*:*
and groupId:artifactId:classifier
is equivalent to
* groupId:artifactId:*:classifier
. For example:
- *
+ *
* * <artifactSet> * <includes> @@ -164,7 +164,7 @@ public class ShadeMojo /** * Packages to be relocated. For example: - * + * ** <relocations> * <relocation> @@ -179,7 +179,7 @@ public class ShadeMojo * </relocation> * </relocations> *- * + * * Note: Support for includes exists only since version 1.4. */ @SuppressWarnings( "MismatchedReadAndWriteOfArray" ) @@ -200,7 +200,7 @@ public class ShadeMojo * to use an include to collect a set of files from the archive then use excludes to further reduce the set. By * default, all files are included and no files are excluded. If multiple filters apply to an artifact, the * intersection of the matched files will be included in the final JAR. For example: - * + * ** <filters> * <filter> @@ -336,13 +336,41 @@ public class ShadeMojo /** * When true, dependencies will be stripped down on the class level to only the transitive hull required for the - * artifact. Note: Usage of this feature requires Java 1.5 or higher. + * artifact. See also {@link #entryPoints}, if you wish to further optimize JAR minimization. + *+ * Note: This feature requires Java 1.8 or higher due to its use of + * jdependency. Its accuracy therefore also depends on + * jdependency's limitations. * * @since 1.4 */ @Parameter private boolean minimizeJar; + /** + * Use this option in order to fine-tune {@link #minimizeJar}: By default, all of the target module's classes are + * kept and used as entry points for JAR minimization. By explicitly limiting the set of entry points, you can + * further minimize the set of classes kept in the shaded JAR. This affects both classes in the module itself and + * dependency classes. If {@link #minimizeJar} is inactive, this option has no effect either. + *
+ * Note: This feature requires Java 1.8 or higher due to its use of + * jdependency. Its accuracy therefore also depends on + * jdependency's limitations. + *
+ * Configuration example: + *
{@code + *+ * + * @since 3.3.1 + */ + @Parameter + private Settrue + *+ * + * }org.acme.Application + *org.acme.OtherEntryPoint + *entryPoints; + /** * The path to the output file for the shaded artifact. When this parameter is set, the created archive will neither * replace the project's main artifact nor will it be attached. Hence, this parameter causes the parameters @@ -391,7 +419,7 @@ public class ShadeMojo */ @Parameter( defaultValue = "false" ) private boolean skip; - + /** * @throws MojoExecutionException in case of an error. */ @@ -555,7 +583,7 @@ public void execute() replaceFile( finalFile, testSourcesJar ); testSourcesJar = finalFile; } - + renamed = true; } @@ -965,11 +993,16 @@ private List getFilters() if ( minimizeJar ) { - getLog().info( "Minimizing jar " + project.getArtifact() ); + if ( entryPoints == null ) + { + entryPoints = new HashSet<>(); + } + getLog().info( "Minimizing jar " + project.getArtifact() + + ( entryPoints.isEmpty() ? "" : ", entry points = " + entryPoints ) ); try { - filters.add( new MinijarFilter( project, getLog(), simpleFilters ) ); + filters.add( new MinijarFilter( project, getLog(), simpleFilters, entryPoints ) ); } catch ( IOException e ) { @@ -1149,7 +1182,7 @@ private void rewriteDependencyReducedPomIfWeHaveReduction( List depe } File f = dependencyReducedPomLocation; - // MSHADE-225 + // MSHADE-225 // Works for now, maybe there's a better algorithm where no for-loop is required if ( loopCounter == 0 ) { 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 a7090184..d2512fba 100644 --- a/src/test/java/org/apache/maven/plugins/shade/DefaultShaderTest.java +++ b/src/test/java/org/apache/maven/plugins/shade/DefaultShaderTest.java @@ -19,11 +19,31 @@ * under the License. */ +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.ClassRule; +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 java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.lang.reflect.Field; import java.net.URL; import java.net.URLClassLoader; @@ -31,33 +51,20 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Enumeration; 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.JarInputStream; 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.Arrays.asList; +import static java.util.Collections.singleton; +import static org.codehaus.plexus.util.FileUtils.forceMkdir; import static java.util.Objects.requireNonNull; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.hasItem; @@ -80,6 +87,9 @@ public class DefaultShaderTest private static final String[] EXCLUDES = new String[] { "org/codehaus/plexus/util/xml/Xpp3Dom", "org/codehaus/plexus/util/xml/pull.*" }; + @ClassRule + public static final TemporaryFolder tmp = new TemporaryFolder(); + @Test public void testNoopWhenNotRelocated() throws IOException, MojoExecutionException { final File plexusJar = new File("src/test/jars/plexus-utils-1.4.1.jar" ); @@ -110,7 +120,7 @@ public void testNoopWhenNotRelocated() throws IOException, MojoExecutionExceptio // 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. + // other, relocated classes. So we are checking for regressions here. assertTrue( areEqual( originalJar, shadedJar, "org/codehaus/plexus/util/Expand.class" ) ); @@ -280,6 +290,60 @@ public void testShaderWithoutExcludesShouldRemoveReferencesOfOriginalPattern() new String[] {} ); } + @Test + public void testHandleDirectory() + throws Exception + { + final File dir = tmp.getRoot(); + // explode src/test/jars/test-artifact-1.0-SNAPSHOT.jar in this temp dir + try ( final JarInputStream in = new JarInputStream( + new FileInputStream( "src/test/jars/test-artifact-1.0-SNAPSHOT.jar" ) ) ) + { + JarEntry nextJarEntry; + while ( (nextJarEntry = in.getNextJarEntry()) != null ) + { + if ( nextJarEntry.isDirectory() ) + { + continue; + } + final File out = new File( dir, nextJarEntry.getName() ); + forceMkdir( out.getParentFile() ); + try ( final OutputStream outputStream = new FileOutputStream( out ) ) + { + IOUtil.copy( in, outputStream, (int) Math.max( nextJarEntry.getSize(), 512 ) ); + } + } + } + + // do shade + final File shade = new File( "target/testHandleDirectory.jar" ); + shaderWithPattern( "org/shaded/plexus/util", shade, new String[0], singleton( dir ) ); + + // ensure directory was shaded properly + try ( final JarFile jar = new JarFile( shade ) ) + { + final List entries = new ArrayList<>(); + final Enumeration jarEntryEnumeration = jar.entries(); + while ( jarEntryEnumeration.hasMoreElements() ) + { + final JarEntry jarEntry = jarEntryEnumeration.nextElement(); + if ( jarEntry.isDirectory() ) + { + continue; + } + entries.add( jarEntry.getName() ); + } + Collections.sort( entries ); + assertEquals( + asList( + "META-INF/maven/org.apache.maven.plugins.shade/test-artifact/pom.properties", + "META-INF/maven/org.apache.maven.plugins.shade/test-artifact/pom.xml", + "org/apache/maven/plugins/shade/Lib.class" + ), + entries ); + } + } + @Test public void testShaderWithRelocatedClassname() throws Exception @@ -397,16 +461,18 @@ private void writeEntryWithoutCompression( String entryName, byte[] entryBytes, jos.closeEntry(); } - private void shaderWithPattern( String shadedPattern, File jar, String[] excludes ) - throws Exception + private void shaderWithPattern( String shadedPattern, File jar, String[] excludes ) throws Exception { - DefaultShader s = newShader(); - Set 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" ) ); + shaderWithPattern( shadedPattern, jar, excludes, set ); + } + + private void shaderWithPattern( String shadedPattern, File jar, String[] excludes, Set set ) + throws Exception + { + DefaultShader s = newShader(); List relocators = new ArrayList<>(); diff --git a/src/test/java/org/apache/maven/plugins/shade/filter/MinijarFilterTest.java b/src/test/java/org/apache/maven/plugins/shade/filter/MinijarFilterTest.java index bfbaee24..948838be 100644 --- a/src/test/java/org/apache/maven/plugins/shade/filter/MinijarFilterTest.java +++ b/src/test/java/org/apache/maven/plugins/shade/filter/MinijarFilterTest.java @@ -20,6 +20,7 @@ */ import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; import static org.junit.Assume.assumeFalse; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -28,14 +29,17 @@ import java.io.File; import java.io.IOException; +import java.util.Arrays; import java.util.Set; import java.util.TreeSet; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.DefaultArtifact; +import org.apache.maven.artifact.DependencyResolutionRequiredException; import org.apache.maven.plugin.logging.Log; import org.apache.maven.project.MavenProject; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.mockito.ArgumentCaptor; @@ -43,16 +47,20 @@ public class MinijarFilterTest { + @Rule + public TemporaryFolder tempFolder = TemporaryFolder.builder().assureDeletion().build(); + private File emptyFile; + private Log log; + private ArgumentCaptor logCaptor; @Before public void init() throws IOException { - TemporaryFolder tempFolder = new TemporaryFolder(); - tempFolder.create(); this.emptyFile = tempFolder.newFile(); - + this.log = mock(Log.class); + logCaptor = ArgumentCaptor.forClass(CharSequence.class); } /** @@ -64,12 +72,8 @@ public void testWithMockProject() { assumeFalse( "Expected to run under JDK8+", System.getProperty("java.version").startsWith("1.7") ); - ArgumentCaptor logCaptor = ArgumentCaptor.forClass( CharSequence.class ); - MavenProject mavenProject = mockProject( emptyFile ); - Log log = mock( Log.class ); - MinijarFilter mf = new MinijarFilter( mavenProject, log ); mf.finished(); @@ -84,14 +88,10 @@ public void testWithMockProject() public void testWithPomProject() throws IOException { - ArgumentCaptor logCaptor = ArgumentCaptor.forClass( CharSequence.class ); - // project with pom packaging and no artifact. MavenProject mavenProject = mockProject( null ); mavenProject.setPackaging( "pom" ); - Log log = mock( Log.class ); - MinijarFilter mf = new MinijarFilter( mavenProject, log ); mf.finished(); @@ -105,7 +105,7 @@ public void testWithPomProject() } - private MavenProject mockProject( File file ) + private MavenProject mockProject( File file, String... classPathElements ) { MavenProject mavenProject = mock( MavenProject.class ); @@ -129,17 +129,18 @@ private MavenProject mockProject( File file ) when( mavenProject.getArtifact().getFile() ).thenReturn( file ); - return mavenProject; + try { + when(mavenProject.getRuntimeClasspathElements()).thenReturn(Arrays.asList(classPathElements)); + } catch (DependencyResolutionRequiredException e) { + fail("Encountered unexpected exception: " + e.getClass().getSimpleName() + ": " + e.getMessage()); + } + return mavenProject; } @Test public void finsishedShouldProduceMessageForClassesTotalNonZero() { - ArgumentCaptor logCaptor = ArgumentCaptor.forClass( CharSequence.class ); - - Log log = mock( Log.class ); - MinijarFilter m = new MinijarFilter( 1, 50, log ); m.finished(); @@ -153,10 +154,6 @@ public void finsishedShouldProduceMessageForClassesTotalNonZero() @Test public void finishedShouldProduceMessageForClassesTotalZero() { - ArgumentCaptor logCaptor = ArgumentCaptor.forClass( CharSequence.class ); - - Log log = mock( Log.class ); - MinijarFilter m = new MinijarFilter( 0, 0, log ); m.finished(); @@ -166,4 +163,5 @@ public void finishedShouldProduceMessageForClassesTotalZero() assertEquals( "Minimized 0 -> 0", logCaptor.getValue() ); } + }