diff --git a/pom.xml b/pom.xml
index cebba16..4e52070 100644
--- a/pom.xml
+++ b/pom.xml
@@ -115,7 +115,7 @@ under the License.
org.apache.maven
maven-core
${mavenVersion}
- test
+ provided
junit
diff --git a/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java b/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java
index 0561ad7..4b5ba86 100644
--- a/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java
+++ b/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java
@@ -19,6 +19,7 @@
* under the License.
*/
+import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Mojo;
@@ -48,6 +49,12 @@ public class CleanMojo
extends AbstractMojo
{
+ public static final String FAST_MODE_BACKGROUND = "background";
+
+ public static final String FAST_MODE_AT_END = "at-end";
+
+ public static final String FAST_MODE_DEFER = "defer";
+
/**
* This is where build results go.
*/
@@ -161,6 +168,49 @@ public class CleanMojo
@Parameter( property = "maven.clean.excludeDefaultDirectories", defaultValue = "false" )
private boolean excludeDefaultDirectories;
+ /**
+ * Enables fast clean if possible. If set to true
, when the plugin is executed, a directory to
+ * be deleted will be atomically moved inside the maven.clean.fastDir
directory and a thread will
+ * be launched to delete the needed files in the background. When the build is completed, maven will wait
+ * until all the files have been deleted. If any problem occurs during the atomic move of the directories,
+ * the plugin will default to the traditional deletion mechanism.
+ *
+ * @since 3.2
+ */
+ @Parameter( property = "maven.clean.fast", defaultValue = "false" )
+ private boolean fast;
+
+ /**
+ * When fast clean is specified, the fastDir
property will be used as the location where directories
+ * to be deleted will be moved prior to background deletion. If not specified, the
+ * ${maven.multiModuleProjectDirectory}/target/.clean
directory will be used. If the
+ * ${build.directory}
has been modified, you'll have to adjust this property explicitly.
+ * In order for fast clean to work correctly, this directory and the various directories that will be deleted
+ * should usually reside on the same volume. The exact conditions are system dependant though, but if an atomic
+ * move is not supported, the standard deletion mechanism will be used.
+ *
+ * @since 3.2
+ * @see #fast
+ */
+ @Parameter( property = "maven.clean.fastDir" )
+ private File fastDir;
+
+ /**
+ * Mode to use when using fast clean. Values are: background
to start deletion immediately and
+ * waiting for all files to be deleted when the session ends, at-end
to indicate that the actual
+ * deletion should be performed synchronously when the session ends, or defer
to specify that
+ * the actual file deletion should be started in the background when the session ends (this should only be used
+ * when maven is embedded in a long running process).
+ *
+ * @since 3.2
+ * @see #fast
+ */
+ @Parameter( property = "maven.clean.fastMode", defaultValue = FAST_MODE_BACKGROUND )
+ private String fastMode;
+
+ @Parameter( defaultValue = "${session}", readonly = true )
+ private MavenSession session;
+
/**
* Deletes file-sets in the following project build directory order: (source) directory, output directory, test
* directory, report directory, and then the additional file-sets.
@@ -177,7 +227,36 @@ public void execute()
return;
}
- Cleaner cleaner = new Cleaner( getLog(), isVerbose() );
+ String multiModuleProjectDirectory = session != null
+ ? session.getSystemProperties().getProperty( "maven.multiModuleProjectDirectory" ) : null;
+ File fastDir;
+ if ( fast && this.fastDir != null )
+ {
+ fastDir = this.fastDir;
+ }
+ else if ( fast && multiModuleProjectDirectory != null )
+ {
+ fastDir = new File( multiModuleProjectDirectory, "target/.clean" );
+ }
+ else
+ {
+ fastDir = null;
+ if ( fast )
+ {
+ getLog().warn( "Fast clean requires maven 3.3.1 or newer, "
+ + "or an explicit directory to be specified with the 'fastDir' configuration of "
+ + "this plugin, or the 'maven.clean.fastDir' user property to be set." );
+ }
+ }
+ if ( fast && !FAST_MODE_BACKGROUND.equals( fastMode )
+ && !FAST_MODE_AT_END.equals( fastMode )
+ && !FAST_MODE_DEFER.equals( fastMode ) )
+ {
+ throw new IllegalArgumentException( "Illegal value '" + fastMode + "' for fastMode. Allowed values are '"
+ + FAST_MODE_BACKGROUND + "', '" + FAST_MODE_AT_END + "' and '" + FAST_MODE_DEFER + "'." );
+ }
+
+ Cleaner cleaner = new Cleaner( session, getLog(), isVerbose(), fastDir, fastMode );
try
{
diff --git a/src/main/java/org/apache/maven/plugins/clean/Cleaner.java b/src/main/java/org/apache/maven/plugins/clean/Cleaner.java
index 33dbd49..4e6e9d2 100644
--- a/src/main/java/org/apache/maven/plugins/clean/Cleaner.java
+++ b/src/main/java/org/apache/maven/plugins/clean/Cleaner.java
@@ -21,10 +21,23 @@
import java.io.File;
import java.io.IOException;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import org.apache.maven.execution.ExecutionListener;
+import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.shared.utils.Os;
+import org.eclipse.aether.SessionData;
+
+import static org.apache.maven.plugins.clean.CleanMojo.FAST_MODE_BACKGROUND;
+import static org.apache.maven.plugins.clean.CleanMojo.FAST_MODE_DEFER;
/**
* Cleans directories.
@@ -36,6 +49,13 @@ class Cleaner
private static final boolean ON_WINDOWS = Os.isFamily( Os.FAMILY_WINDOWS );
+ private static final String LAST_DIRECTORY_TO_DELETE = Cleaner.class.getName() + ".lastDirectoryToDelete";
+
+ /**
+ * The maven session. This is typically non-null in a real run, but it can be during unit tests.
+ */
+ private final MavenSession session;
+
private final Logger logDebug;
private final Logger logInfo;
@@ -44,13 +64,17 @@ class Cleaner
private final Logger logWarn;
+ private final File fastDir;
+
+ private final String fastMode;
+
/**
* Creates a new cleaner.
- *
* @param log The logger to use, may be null
to disable logging.
* @param verbose Whether to perform verbose logging.
+ * @param fastMode The fast deletion mode
*/
- Cleaner( final Log log, boolean verbose )
+ Cleaner( MavenSession session, final Log log, boolean verbose, File fastDir, String fastMode )
{
logDebug = ( log == null || !log.isDebugEnabled() ) ? null : log::debug;
@@ -59,6 +83,10 @@ class Cleaner
logWarn = ( log == null || !log.isWarnEnabled() ) ? null : log::warn;
logVerbose = verbose ? logInfo : logDebug;
+
+ this.session = session;
+ this.fastDir = fastDir;
+ this.fastMode = fastMode;
}
/**
@@ -97,9 +125,96 @@ public void delete( File basedir, Selector selector, boolean followSymlinks, boo
File file = followSymlinks ? basedir : basedir.getCanonicalFile();
+ if ( selector == null && !followSymlinks && fastDir != null && session != null )
+ {
+ // If anything wrong happens, we'll just use the usual deletion mechanism
+ if ( fastDelete( file ) )
+ {
+ return;
+ }
+ }
+
delete( file, "", selector, followSymlinks, failOnError, retryOnError );
}
+ private boolean fastDelete( File baseDirFile )
+ {
+ Path baseDir = baseDirFile.toPath();
+ Path fastDir = this.fastDir.toPath();
+ // Handle the case where we use ${maven.multiModuleProjectDirectory}/target/.clean for example
+ if ( fastDir.toAbsolutePath().startsWith( baseDir.toAbsolutePath() ) )
+ {
+ try
+ {
+ String prefix = baseDir.getFileName().toString() + ".";
+ Path tmpDir = Files.createTempDirectory( baseDir.getParent(), prefix );
+ try
+ {
+ Files.move( baseDir, tmpDir, StandardCopyOption.REPLACE_EXISTING );
+ if ( session != null )
+ {
+ session.getRepositorySession().getData().set( LAST_DIRECTORY_TO_DELETE, baseDir.toFile() );
+ }
+ baseDir = tmpDir;
+ }
+ catch ( IOException e )
+ {
+ Files.delete( tmpDir );
+ throw e;
+ }
+ }
+ catch ( IOException e )
+ {
+ if ( logDebug != null )
+ {
+ // TODO: this Logger interface cannot log exceptions and needs refactoring
+ logDebug.log( "Unable to fast delete directory: " + e );
+ }
+ return false;
+ }
+ }
+ // Create fastDir and the needed parents if needed
+ try
+ {
+ if ( !Files.isDirectory( fastDir ) )
+ {
+ Files.createDirectories( fastDir );
+ }
+ }
+ catch ( IOException e )
+ {
+ if ( logDebug != null )
+ {
+ // TODO: this Logger interface cannot log exceptions and needs refactoring
+ logDebug.log( "Unable to fast delete directory as the path "
+ + fastDir + " does not point to a directory or cannot be created: " + e );
+ }
+ return false;
+ }
+
+ try
+ {
+ Path tmpDir = Files.createTempDirectory( fastDir, "" );
+ Path dstDir = tmpDir.resolve( baseDir.getFileName() );
+ // Note that by specifying the ATOMIC_MOVE, we expect an exception to be thrown
+ // if the path leads to a directory on another mountpoint. If this is the case
+ // or any other exception occurs, an exception will be thrown in which case
+ // the method will return false and the usual deletion will be performed.
+ Files.move( baseDir, dstDir, StandardCopyOption.ATOMIC_MOVE );
+ BackgroundCleaner.delete( this, tmpDir.toFile(), fastMode );
+ return true;
+ }
+ catch ( IOException e )
+ {
+ if ( logDebug != null )
+ {
+ // TODO: this Logger interface cannot log exceptions and needs refactoring
+ logDebug.log( "Unable to fast delete directory: " + e );
+ }
+ return false;
+ }
+ }
+
/**
* Deletes the specified file or directory.
*
@@ -268,4 +383,200 @@ private interface Logger
}
+ private static class BackgroundCleaner extends Thread
+ {
+
+ private static BackgroundCleaner instance;
+
+ private final Deque filesToDelete = new ArrayDeque<>();
+
+ private final Cleaner cleaner;
+
+ private final String fastMode;
+
+ private static final int NEW = 0;
+ private static final int RUNNING = 1;
+ private static final int STOPPED = 2;
+
+ private int status = NEW;
+
+ public static void delete( Cleaner cleaner, File dir, String fastMode )
+ {
+ synchronized ( BackgroundCleaner.class )
+ {
+ if ( instance == null || !instance.doDelete( dir ) )
+ {
+ instance = new BackgroundCleaner( cleaner, dir, fastMode );
+ }
+ }
+ }
+
+ static void sessionEnd()
+ {
+ synchronized ( BackgroundCleaner.class )
+ {
+ if ( instance != null )
+ {
+ instance.doSessionEnd();
+ }
+ }
+ }
+
+ private BackgroundCleaner( Cleaner cleaner, File dir, String fastMode )
+ {
+ super( "mvn-background-cleaner" );
+ this.cleaner = cleaner;
+ this.fastMode = fastMode;
+ init( cleaner.fastDir, dir );
+ }
+
+ public void run()
+ {
+ while ( true )
+ {
+ File basedir = pollNext();
+ if ( basedir == null )
+ {
+ break;
+ }
+ try
+ {
+ cleaner.delete( basedir, "", null, false, false, true );
+ }
+ catch ( IOException e )
+ {
+ // do not display errors
+ }
+ }
+ }
+
+ synchronized void init( File fastDir, File dir )
+ {
+ if ( fastDir.isDirectory() )
+ {
+ File[] children = fastDir.listFiles();
+ if ( children != null && children.length > 0 )
+ {
+ for ( File child : children )
+ {
+ doDelete( child );
+ }
+ }
+ }
+ doDelete( dir );
+ }
+
+ synchronized File pollNext()
+ {
+ File basedir = filesToDelete.poll();
+ if ( basedir == null )
+ {
+ if ( cleaner.session != null )
+ {
+ SessionData data = cleaner.session.getRepositorySession().getData();
+ File lastDir = ( File ) data.get( LAST_DIRECTORY_TO_DELETE );
+ if ( lastDir != null )
+ {
+ data.set( LAST_DIRECTORY_TO_DELETE, null );
+ return lastDir;
+ }
+ }
+ status = STOPPED;
+ notifyAll();
+ }
+ return basedir;
+ }
+
+ synchronized boolean doDelete( File dir )
+ {
+ if ( status == STOPPED )
+ {
+ return false;
+ }
+ filesToDelete.add( dir );
+ if ( status == NEW && FAST_MODE_BACKGROUND.equals( fastMode ) )
+ {
+ status = RUNNING;
+ notifyAll();
+ start();
+ }
+ wrapExecutionListener();
+ return true;
+ }
+
+ /**
+ * If this has not been done already, we wrap the ExecutionListener inside a proxy
+ * which simply delegates call to the previous listener. When the session ends, it will
+ * also call {@link BackgroundCleaner#sessionEnd()}.
+ * There's no clean API to do that properly as this is a very unusual use case for a plugin
+ * to outlive its main execution.
+ */
+ private void wrapExecutionListener()
+ {
+ ExecutionListener executionListener = cleaner.session.getRequest().getExecutionListener();
+ if ( executionListener == null
+ || !Proxy.isProxyClass( executionListener.getClass() )
+ || !( Proxy.getInvocationHandler( executionListener ) instanceof SpyInvocationHandler ) )
+ {
+ ExecutionListener listener = ( ExecutionListener ) Proxy.newProxyInstance(
+ ExecutionListener.class.getClassLoader(),
+ new Class[] { ExecutionListener.class },
+ new SpyInvocationHandler( executionListener ) );
+ cleaner.session.getRequest().setExecutionListener( listener );
+ }
+ }
+
+ synchronized void doSessionEnd()
+ {
+ if ( status != STOPPED )
+ {
+ if ( status == NEW )
+ {
+ start();
+ }
+ if ( !FAST_MODE_DEFER.equals( fastMode ) )
+ {
+ try
+ {
+ cleaner.logInfo.log( "Waiting for background file deletion" );
+ while ( status != STOPPED )
+ {
+ wait();
+ }
+ }
+ catch ( InterruptedException e )
+ {
+ // ignore
+ }
+ }
+ }
+ }
+
+ }
+
+ static class SpyInvocationHandler implements InvocationHandler
+ {
+ private final ExecutionListener delegate;
+
+ SpyInvocationHandler( ExecutionListener delegate )
+ {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable
+ {
+ if ( "sessionEnded".equals( method.getName() ) )
+ {
+ BackgroundCleaner.sessionEnd();
+ }
+ if ( delegate != null )
+ {
+ return method.invoke( delegate, args );
+ }
+ return null;
+ }
+
+ }
+
}