diff --git a/.github/workflows/maven-verify.yml b/.github/workflows/maven-verify.yml index ce4b500..56062b5 100644 --- a/.github/workflows/maven-verify.yml +++ b/.github/workflows/maven-verify.yml @@ -26,4 +26,6 @@ jobs: name: Verify uses: apache/maven-gh-actions-shared/.github/workflows/maven-verify.yml@v4 with: - maven4-enabled: true + ff-maven: "4.0.0-beta-3" # Maven version for fail-fast-build + maven-matrix: '[ "4.0.0-beta-3" ]' + jdk-matrix: '[ "17", "21" ]' diff --git a/pom.xml b/pom.xml index 73c3c72..a7ef0bb 100644 --- a/pom.xml +++ b/pom.xml @@ -28,7 +28,7 @@ under the License. maven-clean-plugin - 3.4.1-SNAPSHOT + 4.0.0-SNAPSHOT maven-plugin Apache Maven Clean Plugin @@ -61,46 +61,44 @@ under the License. - 3.6.3 + 4.0.0-beta-3 + 17 + 3.7.0 + 4.0.0-alpha-3-SNAPSHOT + 4.0.0-SNAPSHOT 2024-06-16T10:25:11Z org.apache.maven - maven-plugin-api + maven-api-core ${mavenVersion} provided org.apache.maven - maven-core + maven-api-di ${mavenVersion} provided - org.apache.maven.resolver - maven-resolver-api - 1.1.1 + org.apache.maven + maven-api-meta + ${mavenVersion} provided + org.codehaus.plexus plexus-utils - - - org.apache.maven.plugin-tools - maven-plugin-annotations - provided - - org.apache.maven.plugin-testing maven-plugin-testing-harness - 4.0.0-alpha-2 + ${version.maven-plugin-testing} test @@ -109,18 +107,42 @@ under the License. test - org.codehaus.plexus - plexus-testing - 1.3.0 + org.apache.maven + maven-core + ${mavenVersion} test - org.codehaus.plexus - plexus-xml + com.google.inject + guice + 6.0.0 + test + + + org.slf4j + slf4j-simple + 2.0.13 test + + + + org.apache.maven.plugins + maven-plugin-plugin + ${version.maven-plugin-tools} + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + + run-its diff --git a/src/it/settings.xml b/src/it/settings.xml index c8f77f0..5617e4e 100644 --- a/src/it/settings.xml +++ b/src/it/settings.xml @@ -32,9 +32,11 @@ under the License. @localRepositoryUrl@ true + ignore true + ignore @@ -44,9 +46,11 @@ under the License. @localRepositoryUrl@ true + ignore true + ignore 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 4f77c00..44ceb48 100644 --- a/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java +++ b/src/main/java/org/apache/maven/plugins/clean/CleanMojo.java @@ -18,14 +18,16 @@ */ package org.apache.maven.plugins.clean; -import java.io.File; import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; -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; -import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.api.Session; +import org.apache.maven.api.di.Inject; +import org.apache.maven.api.plugin.Log; +import org.apache.maven.api.plugin.MojoException; +import org.apache.maven.api.plugin.annotations.Mojo; +import org.apache.maven.api.plugin.annotations.Parameter; /** * Goal which cleans the build. @@ -43,8 +45,8 @@ * @see org.apache.maven.plugins.clean.Fileset * @since 2.0 */ -@Mojo(name = "clean", threadSafe = true) -public class CleanMojo extends AbstractMojo { +@Mojo(name = "clean") +public class CleanMojo implements org.apache.maven.api.plugin.Mojo { public static final String FAST_MODE_BACKGROUND = "background"; @@ -52,23 +54,26 @@ public class CleanMojo extends AbstractMojo { public static final String FAST_MODE_DEFER = "defer"; + @Inject + private Log logger; + /** * This is where build results go. */ @Parameter(defaultValue = "${project.build.directory}", readonly = true, required = true) - private File directory; + private Path directory; /** * This is where compiled classes go. */ @Parameter(defaultValue = "${project.build.outputDirectory}", readonly = true, required = true) - private File outputDirectory; + private Path outputDirectory; /** * This is where compiled test classes go. */ @Parameter(defaultValue = "${project.build.testOutputDirectory}", readonly = true, required = true) - private File testOutputDirectory; + private Path testOutputDirectory; /** * This is where the site plugin generates its pages. @@ -76,7 +81,7 @@ public class CleanMojo extends AbstractMojo { * @since 2.1.1 */ @Parameter(defaultValue = "${project.build.outputDirectory}", readonly = true, required = true) - private File reportDirectory; + private Path reportDirectory; /** * Sets whether the plugin runs in verbose mode. As of plugin version 2.3, the default value is derived from Maven's @@ -186,11 +191,11 @@ public class CleanMojo extends AbstractMojo { * 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. * - * @see #fast * @since 3.2 + * @see #fast */ @Parameter(property = "maven.clean.fastDir") - private File fastDir; + private Path fastDir; /** * Mode to use when using fast clean. Values are: background to start deletion immediately and @@ -199,35 +204,35 @@ public class CleanMojo extends AbstractMojo { * 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). * - * @see #fast * @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; + @Inject + private Session 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. * - * @throws MojoExecutionException When a directory failed to get deleted. - * @see org.apache.maven.plugin.Mojo#execute() + * @throws MojoException When a directory failed to get deleted. + * @see org.apache.maven.api.plugin.Mojo#execute() */ - public void execute() throws MojoExecutionException { + public void execute() { if (skip) { getLog().info("Clean is skipped."); return; } String multiModuleProjectDirectory = - session != null ? session.getSystemProperties().getProperty("maven.multiModuleProjectDirectory") : null; - File fastDir; + session != null ? session.getSystemProperties().get("maven.multiModuleProjectDirectory") : null; + Path fastDir; if (fast && this.fastDir != null) { fastDir = this.fastDir; } else if (fast && multiModuleProjectDirectory != null) { - fastDir = new File(multiModuleProjectDirectory, "target/.clean"); + fastDir = Paths.get(multiModuleProjectDirectory, "target/.clean"); } else { fastDir = null; if (fast) { @@ -247,7 +252,7 @@ public void execute() throws MojoExecutionException { Cleaner cleaner = new Cleaner(session, getLog(), isVerbose(), fastDir, fastMode); try { - for (File directoryItem : getDirectories()) { + for (Path directoryItem : getDirectories()) { if (directoryItem != null) { cleaner.delete(directoryItem, null, followSymLinks, failOnError, retryOnError); } @@ -256,7 +261,7 @@ public void execute() throws MojoExecutionException { if (filesets != null) { for (Fileset fileset : filesets) { if (fileset.getDirectory() == null) { - throw new MojoExecutionException("Missing base directory for " + fileset); + throw new MojoException("Missing base directory for " + fileset); } final String[] includes = fileset.getIncludes(); final String[] excludes = fileset.getExcludes(); @@ -273,8 +278,9 @@ public void execute() throws MojoExecutionException { fileset.getDirectory(), selector, fileset.isFollowSymlinks(), failOnError, retryOnError); } } + } catch (IOException e) { - throw new MojoExecutionException("Failed to clean project: " + e.getMessage(), e); + throw new MojoException("Failed to clean project: " + e.getMessage(), e); } } @@ -292,13 +298,17 @@ private boolean isVerbose() { * * @return The directories to clean or an empty array if none, never null. */ - private File[] getDirectories() { - File[] directories; + private Path[] getDirectories() { + Path[] directories; if (excludeDefaultDirectories) { - directories = new File[0]; + directories = new Path[0]; } else { - directories = new File[] {directory, outputDirectory, testOutputDirectory, reportDirectory}; + directories = new Path[] {directory, outputDirectory, testOutputDirectory, reportDirectory}; } return directories; } + + private Log getLog() { + return logger; + } } 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 f5c25d2..533a224 100644 --- a/src/main/java/org/apache/maven/plugins/clean/Cleaner.java +++ b/src/main/java/org/apache/maven/plugins/clean/Cleaner.java @@ -20,9 +20,6 @@ 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.LinkOption; import java.nio.file.Path; @@ -30,12 +27,17 @@ import java.nio.file.attribute.BasicFileAttributes; 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 java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import org.apache.maven.api.Event; +import org.apache.maven.api.EventType; +import org.apache.maven.api.Listener; +import org.apache.maven.api.Session; +import org.apache.maven.api.SessionData; +import org.apache.maven.api.plugin.Log; import org.codehaus.plexus.util.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; @@ -49,70 +51,93 @@ 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"; + private static final SessionData.Key LAST_DIRECTORY_TO_DELETE = + SessionData.key(Path.class, 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 Session session; - private final File fastDir; + private final Logger logDebug; - private final String fastMode; + private final Logger logInfo; + + private final Logger logVerbose; - private final boolean verbose; + private final Logger logWarn; - private Log log; + private final Path fastDir; + + private final String fastMode; /** * Creates a new cleaner. * * @param session The Maven session to be used. - * @param log The logger to use. + * @param log The logger to use, may be null to disable logging. * @param verbose Whether to perform verbose logging. * @param fastDir The explicit configured directory or to be deleted in fast mode. * @param fastMode The fast deletion mode. */ - Cleaner(MavenSession session, final Log log, boolean verbose, File fastDir, String fastMode) { + Cleaner(Session session, Log log, boolean verbose, Path fastDir, String fastMode) { + logDebug = (log == null || !log.isDebugEnabled()) ? null : logger(log::debug, log::debug); + + logInfo = (log == null || !log.isInfoEnabled()) ? null : logger(log::info, log::info); + + logWarn = (log == null || !log.isWarnEnabled()) ? null : logger(log::warn, log::warn); + + logVerbose = verbose ? logInfo : logDebug; + this.session = session; - // This can't be null as the Cleaner gets it from the CleanMojo which gets it from AbstractMojo class, where it - // is never null. - this.log = log; this.fastDir = fastDir; this.fastMode = fastMode; - this.verbose = verbose; + } + + private Logger logger(Consumer l1, BiConsumer l2) { + return new Logger() { + @Override + public void log(CharSequence message) { + l1.accept(message); + } + + @Override + public void log(CharSequence message, Throwable t) { + l2.accept(message, t); + } + }; } /** * Deletes the specified directories and its contents. * - * @param basedir The directory to delete, must not be null. Non-existing directories will be silently - * ignored. - * @param selector The selector used to determine what contents to delete, may be null to delete - * everything. + * @param basedir The directory to delete, must not be null. Non-existing directories will be silently + * ignored. + * @param selector The selector used to determine what contents to delete, may be null to delete + * everything. * @param followSymlinks Whether to follow symlinks. - * @param failOnError Whether to abort with an exception in case a selected file/directory could not be deleted. - * @param retryOnError Whether to undertake additional delete attempts in case the first attempt failed. + * @param failOnError Whether to abort with an exception in case a selected file/directory could not be deleted. + * @param retryOnError Whether to undertake additional delete attempts in case the first attempt failed. * @throws IOException If a file/directory could not be deleted and failOnError is true. */ public void delete( - File basedir, Selector selector, boolean followSymlinks, boolean failOnError, boolean retryOnError) + Path basedir, Selector selector, boolean followSymlinks, boolean failOnError, boolean retryOnError) throws IOException { - if (!basedir.isDirectory()) { - if (!basedir.exists()) { - if (log.isDebugEnabled()) { - log.debug("Skipping non-existing directory " + basedir); + if (!Files.isDirectory(basedir)) { + if (!Files.exists(basedir)) { + if (logDebug != null) { + logDebug.log("Skipping non-existing directory " + basedir); } return; } throw new IOException("Invalid base directory " + basedir); } - if (log.isInfoEnabled()) { - log.info("Deleting " + basedir + (selector != null ? " (" + selector + ")" : "")); + if (logInfo != null) { + logInfo.log("Deleting " + basedir + (selector != null ? " (" + selector + ")" : "")); } - File file = followSymlinks ? basedir : basedir.getCanonicalFile(); + Path file = followSymlinks ? basedir : getCanonicalPath(basedir); if (selector == null && !followSymlinks && fastDir != null && session != null) { // If anything wrong happens, we'll just use the usual deletion mechanism @@ -124,9 +149,8 @@ public void delete( delete(file, "", selector, followSymlinks, failOnError, retryOnError); } - private boolean fastDelete(File baseDirFile) { - Path baseDir = baseDirFile.toPath(); - Path fastDir = this.fastDir.toPath(); + private boolean fastDelete(Path baseDir) { + Path fastDir = this.fastDir; // Handle the case where we use ${maven.multiModuleProjectDirectory}/target/.clean for example if (fastDir.toAbsolutePath().startsWith(baseDir.toAbsolutePath())) { try { @@ -135,7 +159,7 @@ private boolean fastDelete(File baseDirFile) { try { Files.move(baseDir, tmpDir, StandardCopyOption.REPLACE_EXISTING); if (session != null) { - session.getRepositorySession().getData().set(LAST_DIRECTORY_TO_DELETE, baseDir.toFile()); + session.getData().set(LAST_DIRECTORY_TO_DELETE, baseDir); } baseDir = tmpDir; } catch (IOException e) { @@ -143,8 +167,8 @@ private boolean fastDelete(File baseDirFile) { throw e; } } catch (IOException e) { - if (log.isDebugEnabled()) { - log.debug("Unable to fast delete directory: ", e); + if (logDebug != null) { + logDebug.log("Unable to fast delete directory", e); } return false; } @@ -155,10 +179,10 @@ private boolean fastDelete(File baseDirFile) { Files.createDirectories(fastDir); } } catch (IOException e) { - if (log.isDebugEnabled()) { - log.debug( + if (logDebug != null) { + logDebug.log( "Unable to fast delete directory as the path " + fastDir - + " does not point to a directory or cannot be created: ", + + " does not point to a directory or cannot be created", e); } return false; @@ -172,11 +196,11 @@ private boolean fastDelete(File baseDirFile) { // 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); + BackgroundCleaner.delete(this, tmpDir, fastMode); return true; } catch (IOException e) { - if (log.isDebugEnabled()) { - log.debug("Unable to fast delete directory: ", e); + if (logDebug != null) { + logDebug.log("Unable to fast delete directory", e); } return false; } @@ -185,20 +209,20 @@ private boolean fastDelete(File baseDirFile) { /** * Deletes the specified file or directory. * - * @param file The file/directory to delete, must not be null. If followSymlinks is - * false, it is assumed that the parent file is canonical. - * @param pathname The relative pathname of the file, using {@link File#separatorChar}, must not be - * null. - * @param selector The selector used to determine what contents to delete, may be null to delete - * everything. + * @param file The file/directory to delete, must not be null. If followSymlinks is + * false, it is assumed that the parent file is canonical. + * @param pathname The relative pathname of the file, using {@link File#separatorChar}, must not be + * null. + * @param selector The selector used to determine what contents to delete, may be null to delete + * everything. * @param followSymlinks Whether to follow symlinks. - * @param failOnError Whether to abort with an exception in case a selected file/directory could not be deleted. - * @param retryOnError Whether to undertake additional delete attempts in case the first attempt failed. + * @param failOnError Whether to abort with an exception in case a selected file/directory could not be deleted. + * @param retryOnError Whether to undertake additional delete attempts in case the first attempt failed. * @return The result of the cleaning, never null. * @throws IOException If a file/directory could not be deleted and failOnError is true. */ private Result delete( - File file, + Path file, String pathname, Selector selector, boolean followSymlinks, @@ -207,46 +231,43 @@ private Result delete( throws IOException { Result result = new Result(); - boolean isDirectory = file.isDirectory(); + boolean isDirectory = Files.isDirectory(file); if (isDirectory) { if (selector == null || selector.couldHoldSelected(pathname)) { - if (followSymlinks || !isSymbolicLink(file.toPath())) { - File canonical = followSymlinks ? file : file.getCanonicalFile(); - String[] filenames = canonical.list(); - if (filenames != null) { - String prefix = pathname.length() > 0 ? pathname + File.separatorChar : ""; - for (int i = filenames.length - 1; i >= 0; i--) { - String filename = filenames[i]; - File child = new File(canonical, filename); + final boolean isSymlink = isSymbolicLink(file); + Path canonical = followSymlinks ? file : getCanonicalPath(file); + if (followSymlinks || !isSymlink) { + String prefix = !pathname.isEmpty() ? pathname + File.separatorChar : ""; + try (Stream children = Files.list(canonical)) { + for (Path child : children.toList()) { result.update(delete( - child, prefix + filename, selector, followSymlinks, failOnError, retryOnError)); + child, + prefix + child.getFileName(), + selector, + followSymlinks, + failOnError, + retryOnError)); } } - } else if (log.isDebugEnabled()) { - log.debug("Not recursing into symlink " + file); + } else if (logDebug != null) { + logDebug.log("Not recursing into symlink " + file); } - } else if (log.isDebugEnabled()) { - log.debug("Not recursing into directory without included files " + file); + } else if (logDebug != null) { + logDebug.log("Not recursing into directory without included files " + file); } } if (!result.excluded && (selector == null || selector.isSelected(pathname))) { - String logmessage; - if (isDirectory) { - logmessage = "Deleting directory " + file; - } else if (file.exists()) { - logmessage = "Deleting file " + file; - } else { - logmessage = "Deleting dangling symlink " + file; - } - - if (verbose && log.isInfoEnabled()) { - log.info(logmessage); - } else if (log.isDebugEnabled()) { - log.debug(logmessage); + if (logVerbose != null) { + if (isDirectory) { + logVerbose.log("Deleting directory " + file); + } else if (Files.exists(file)) { + logVerbose.log("Deleting file " + file); + } else { + logVerbose.log("Deleting dangling symlink " + file); + } } - result.failures += delete(file, failOnError, retryOnError); } else { result.excluded = true; @@ -255,6 +276,14 @@ private Result delete( return result; } + private static Path getCanonicalPath(Path path) { + try { + return path.toRealPath(); + } catch (IOException e) { + return getCanonicalPath(path.getParent()).resolve(path.getFileName()); + } + } + private boolean isSymbolicLink(Path path) throws IOException { BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); return attrs.isSymbolicLink() @@ -262,19 +291,13 @@ private boolean isSymbolicLink(Path path) throws IOException { || (attrs.isDirectory() && attrs.isOther()); } - /** - * Deletes the specified file, directory. If the path denotes a symlink, only the link is removed, its target is - * left untouched. - * - * @param file The file/directory to delete, must not be null. - * @param failOnError Whether to abort with an exception in case the file/directory could not be deleted. - * @param retryOnError Whether to undertake additional delete attempts in case the first attempt failed. - * @return 0 if the file was deleted, 1 otherwise. - * @throws IOException If a file/directory could not be deleted and failOnError is true. - */ - private int delete(File file, boolean failOnError, boolean retryOnError) throws IOException { - if (!file.delete()) { - boolean deleted = false; + private int delete(Path file, boolean failOnError, boolean retryOnError) throws IOException { + try { + Files.deleteIfExists(file); + return 0; + } catch (IOException e) { + IOException exception = new IOException("Failed to delete " + file); + exception.addSuppressed(e); if (retryOnError) { if (ON_WINDOWS) { @@ -283,24 +306,27 @@ private int delete(File file, boolean failOnError, boolean retryOnError) throws } final int[] delays = {50, 250, 750}; - for (int i = 0; !deleted && i < delays.length; i++) { + for (int delay : delays) { try { - Thread.sleep(delays[i]); - } catch (InterruptedException e) { - // ignore + Thread.sleep(delay); + } catch (InterruptedException e2) { + exception.addSuppressed(e2); + } + try { + Files.deleteIfExists(file); + return 0; + } catch (IOException e2) { + exception.addSuppressed(e2); } - deleted = file.delete() || !file.exists(); } - } else { - deleted = !file.exists(); } - if (!deleted) { + if (Files.exists(file)) { if (failOnError) { - throw new IOException("Failed to delete " + file); + throw new IOException("Failed to delete " + file, exception); } else { - if (log.isWarnEnabled()) { - log.warn("Failed to delete " + file); + if (logWarn != null) { + logWarn.log("Failed to delete " + file, exception); } return 1; } @@ -322,25 +348,30 @@ public void update(Result result) { } } + private interface Logger { + + void log(CharSequence message); + + void log(CharSequence message, Throwable t); + } + private static class BackgroundCleaner extends Thread { - private static final int NEW = 0; - private static final int RUNNING = 1; - private static final int STOPPED = 2; private static BackgroundCleaner instance; - private final Deque filesToDelete = new ArrayDeque<>(); + + private final Deque filesToDelete = new ArrayDeque<>(); + private final Cleaner cleaner; + private final String fastMode; - private int status = NEW; - private BackgroundCleaner(Cleaner cleaner, File dir, String fastMode) { - super("mvn-background-cleaner"); - this.cleaner = cleaner; - this.fastMode = fastMode; - init(cleaner.fastDir, dir); - } + private static final int NEW = 0; + private static final int RUNNING = 1; + private static final int STOPPED = 2; - public static void delete(Cleaner cleaner, File dir, String fastMode) { + private int status = NEW; + + public static void delete(Cleaner cleaner, Path dir, String fastMode) { synchronized (BackgroundCleaner.class) { if (instance == null || !instance.doDelete(dir)) { instance = new BackgroundCleaner(cleaner, dir, fastMode); @@ -356,9 +387,16 @@ static void sessionEnd() { } } + private BackgroundCleaner(Cleaner cleaner, Path 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(); + Path basedir = pollNext(); if (basedir == null) { break; } @@ -370,24 +408,25 @@ public void run() { } } - 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); + synchronized void init(Path fastDir, Path dir) { + if (Files.isDirectory(fastDir)) { + try { + try (Stream children = Files.list(fastDir)) { + children.forEach(this::doDelete); } + } catch (IOException e) { + throw new RuntimeException(e); } } doDelete(dir); } - synchronized File pollNext() { - File basedir = filesToDelete.poll(); + synchronized Path pollNext() { + Path 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); + SessionData data = cleaner.session.getData(); + Path lastDir = (Path) data.get(LAST_DIRECTORY_TO_DELETE); if (lastDir != null) { data.set(LAST_DIRECTORY_TO_DELETE, null); return lastDir; @@ -399,7 +438,7 @@ synchronized File pollNext() { return basedir; } - synchronized boolean doDelete(File dir) { + synchronized boolean doDelete(Path dir) { if (status == STOPPED) { return false; } @@ -421,15 +460,10 @@ synchronized boolean doDelete(File dir) { * 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 (CleanerListener.class) { + if (cleaner.session.getListeners().stream().noneMatch(l -> l instanceof CleanerListener)) { + cleaner.session.registerListener(new CleanerListener()); + } } } @@ -440,8 +474,8 @@ synchronized void doSessionEnd() { } if (!FAST_MODE_DEFER.equals(fastMode)) { try { - if (cleaner.log.isInfoEnabled()) { - cleaner.log.info("Waiting for background file deletion"); + if (cleaner.logInfo != null) { + cleaner.logInfo.log("Waiting for background file deletion"); } while (status != STOPPED) { wait(); @@ -454,22 +488,12 @@ synchronized void doSessionEnd() { } } - static class SpyInvocationHandler implements InvocationHandler { - private final ExecutionListener delegate; - - SpyInvocationHandler(ExecutionListener delegate) { - this.delegate = delegate; - } - + static class CleanerListener implements Listener { @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - if ("sessionEnded".equals(method.getName())) { + public void onEvent(Event event) { + if (event.getType() == EventType.SESSION_ENDED) { BackgroundCleaner.sessionEnd(); } - if (delegate != null) { - return method.invoke(delegate, args); - } - return null; } } } diff --git a/src/main/java/org/apache/maven/plugins/clean/Fileset.java b/src/main/java/org/apache/maven/plugins/clean/Fileset.java index ecd88b0..7508411 100644 --- a/src/main/java/org/apache/maven/plugins/clean/Fileset.java +++ b/src/main/java/org/apache/maven/plugins/clean/Fileset.java @@ -18,7 +18,7 @@ */ package org.apache.maven.plugins.clean; -import java.io.File; +import java.nio.file.Path; import java.util.Arrays; /** @@ -32,7 +32,7 @@ */ public class Fileset { - private File directory; + private Path directory; private String[] includes; @@ -45,7 +45,7 @@ public class Fileset { /** * @return {@link #directory} */ - public File getDirectory() { + public Path getDirectory() { return directory; } diff --git a/src/site/apt/examples/delete_additional_files.apt.vm b/src/site/apt/examples/delete_additional_files.apt.vm index 0fa768c..84ed079 100644 --- a/src/site/apt/examples/delete_additional_files.apt.vm +++ b/src/site/apt/examples/delete_additional_files.apt.vm @@ -68,7 +68,7 @@ Delete Additional Files Not Exposed to Maven is equivalent to: +-------- - ${project.basedir}/some/relative/path + ${basedir}/some/relative/path +-------- You could also define file set rules in a parent POM. In this case, the clean plugin adds the subproject diff --git a/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java b/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java index 22287d7..5a5d0d4 100644 --- a/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java +++ b/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java @@ -29,17 +29,17 @@ import java.nio.file.Paths; import java.util.Collections; -import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugin.logging.SystemStreamLog; -import org.apache.maven.plugin.testing.junit5.InjectMojo; -import org.apache.maven.plugin.testing.junit5.MojoTest; +import org.apache.maven.api.plugin.MojoException; +import org.apache.maven.api.plugin.testing.Basedir; +import org.apache.maven.api.plugin.testing.InjectMojo; +import org.apache.maven.api.plugin.testing.MojoTest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledOnOs; import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.condition.OS; -import static org.apache.maven.plugin.testing.junit5.MojoExtension.setVariableValueToObject; -import static org.codehaus.plexus.testing.PlexusExtension.getBasedir; +import static org.apache.maven.api.plugin.testing.MojoExtension.getBasedir; +import static org.apache.maven.api.plugin.testing.MojoExtension.setVariableValueToObject; import static org.codehaus.plexus.util.IOUtil.copy; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -49,30 +49,25 @@ /** * Test the clean mojo. - * - * @author Vincent Siveton */ @MojoTest -class CleanMojoTest { +public class CleanMojoTest { + private static final String LOCAL_REPO = "target/local-repo/"; + /** * Tests the simple removal of directories * * @throws Exception in case of an error. */ @Test - @InjectMojo(goal = "clean", pom = "classpath:/unit/basic-clean-test/plugin-pom.xml") - void testBasicClean(CleanMojo mojo) throws Exception { + @Basedir("${basedir}/target/test-classes/unit/basic-clean-test") + @InjectMojo(goal = "clean") + public void testBasicClean(CleanMojo mojo) throws Exception { mojo.execute(); - assertFalse( - checkExists(getBasedir() + "/target/test-classes/unit/" + "basic-clean-test/buildDirectory"), - "Directory exists"); - assertFalse( - checkExists(getBasedir() + "/target/test-classes/unit/basic-clean-test/" + "buildOutputDirectory"), - "Directory exists"); - assertFalse( - checkExists(getBasedir() + "/target/test-classes/unit/basic-clean-test/" + "buildTestDirectory"), - "Directory exists"); + assertFalse(checkExists(getBasedir() + "/buildDirectory"), "Directory exists"); + assertFalse(checkExists(getBasedir() + "/buildOutputDirectory"), "Directory exists"); + assertFalse(checkExists(getBasedir() + "/buildTestDirectory"), "Directory exists"); } /** @@ -81,13 +76,14 @@ void testBasicClean(CleanMojo mojo) throws Exception { * @throws Exception in case of an error. */ @Test - @InjectMojo(goal = "clean", pom = "classpath:/unit/nested-clean-test/plugin-pom.xml") - void testCleanNestedStructure(CleanMojo mojo) throws Exception { + @Basedir("${basedir}/target/test-classes/unit/nested-clean-test") + @InjectMojo(goal = "clean") + public void testCleanNestedStructure(CleanMojo mojo) throws Exception { mojo.execute(); - assertFalse(checkExists(getBasedir() + "/target/test-classes/unit/nested-clean-test/target")); - assertFalse(checkExists(getBasedir() + "/target/test-classes/unit/nested-clean-test/target/classes")); - assertFalse(checkExists(getBasedir() + "/target/test-classes/unit/nested-clean-test/target/test-classes")); + assertFalse(checkExists(getBasedir() + "/target")); + assertFalse(checkExists(getBasedir() + "/target/classes")); + assertFalse(checkExists(getBasedir() + "/target/test-classes")); } /** @@ -97,17 +93,15 @@ void testCleanNestedStructure(CleanMojo mojo) throws Exception { * @throws Exception in case of an error. */ @Test - @InjectMojo(goal = "clean", pom = "classpath:/unit/empty-clean-test/plugin-pom.xml") - void testCleanEmptyDirectories(CleanMojo mojo) throws Exception { + @Basedir("${basedir}/target/test-classes/unit/empty-clean-test") + @InjectMojo(goal = "clean") + public void testCleanEmptyDirectories(CleanMojo mojo) throws Exception { mojo.execute(); - assertTrue(checkExists(getBasedir() + "/target/test-classes/unit/empty-clean-test/testDirectoryStructure")); - assertTrue(checkExists( - getBasedir() + "/target/test-classes/unit/empty-clean-test/" + "testDirectoryStructure/file.txt")); - assertTrue(checkExists(getBasedir() + "/target/test-classes/unit/empty-clean-test/" - + "testDirectoryStructure/outputDirectory")); - assertTrue(checkExists(getBasedir() + "/target/test-classes/unit/empty-clean-test/" - + "testDirectoryStructure/outputDirectory/file.txt")); + assertTrue(checkExists(getBasedir() + "/testDirectoryStructure")); + assertTrue(checkExists(getBasedir() + "/testDirectoryStructure/file.txt")); + assertTrue(checkExists(getBasedir() + "/testDirectoryStructure/outputDirectory")); + assertTrue(checkExists(getBasedir() + "/testDirectoryStructure/outputDirectory/file.txt")); } /** @@ -116,25 +110,24 @@ void testCleanEmptyDirectories(CleanMojo mojo) throws Exception { * @throws Exception in case of an error. */ @Test - @InjectMojo(goal = "clean", pom = "classpath:/unit/fileset-clean-test/plugin-pom.xml") - void testFilesetsClean(CleanMojo mojo) throws Exception { + @Basedir("${basedir}/target/test-classes/unit/fileset-clean-test") + @InjectMojo(goal = "clean") + public void testFilesetsClean(CleanMojo mojo) throws Exception { mojo.execute(); // fileset 1 - assertTrue(checkExists(getBasedir() + "/target/test-classes/unit/fileset-clean-test/target")); - assertTrue(checkExists(getBasedir() + "/target/test-classes/unit/fileset-clean-test/target/classes")); - assertFalse(checkExists(getBasedir() + "/target/test-classes/unit/fileset-clean-test/target/test-classes")); - assertTrue(checkExists(getBasedir() + "/target/test-classes/unit/fileset-clean-test/target/subdir")); - assertFalse(checkExists(getBasedir() + "/target/test-classes/unit/fileset-clean-test/target/classes/file.txt")); - assertTrue(checkEmpty(getBasedir() + "/target/test-classes/unit/fileset-clean-test/target/classes")); - assertFalse(checkEmpty(getBasedir() + "/target/test-classes/unit/fileset-clean-test/target/subdir")); - assertTrue(checkExists(getBasedir() + "/target/test-classes/unit/fileset-clean-test/target/subdir/file.txt")); + assertTrue(checkExists(getBasedir() + "/target")); + assertTrue(checkExists(getBasedir() + "/target/classes")); + assertFalse(checkExists(getBasedir() + "/target/test-classes")); + assertTrue(checkExists(getBasedir() + "/target/subdir")); + assertFalse(checkExists(getBasedir() + "/target/classes/file.txt")); + assertTrue(checkEmpty(getBasedir() + "/target/classes")); + assertFalse(checkEmpty(getBasedir() + "/target/subdir")); + assertTrue(checkExists(getBasedir() + "/target/subdir/file.txt")); // fileset 2 - assertTrue( - checkExists(getBasedir() + "/target/test-classes/unit/fileset-clean-test/" + "buildOutputDirectory")); - assertFalse(checkExists( - getBasedir() + "/target/test-classes/unit/fileset-clean-test/" + "buildOutputDirectory/file.txt")); + assertTrue(checkExists(getBasedir() + "/" + "buildOutputDirectory")); + assertFalse(checkExists(getBasedir() + "/" + "buildOutputDirectory/file.txt")); } /** @@ -143,9 +136,10 @@ void testFilesetsClean(CleanMojo mojo) throws Exception { * @throws Exception in case of an error. */ @Test - @InjectMojo(goal = "clean", pom = "classpath:/unit/invalid-directory-test/plugin-pom.xml") - void testCleanInvalidDirectory(CleanMojo mojo) throws Exception { - assertThrows(MojoExecutionException.class, mojo::execute); + @Basedir("${basedir}/target/test-classes/unit/invalid-directory-test") + @InjectMojo(goal = "clean") + public void testCleanInvalidDirectory(CleanMojo mojo) throws Exception { + assertThrows(MojoException.class, mojo::execute, "Should fail to delete a file treated as a directory"); } /** @@ -154,11 +148,12 @@ void testCleanInvalidDirectory(CleanMojo mojo) throws Exception { * @throws Exception in case of an error. */ @Test - @InjectMojo(goal = "clean", pom = "classpath:/unit/missing-directory-test/plugin-pom.xml") - void testMissingDirectory(CleanMojo mojo) throws Exception { + @Basedir("${basedir}/target/test-classes/unit/missing-directory-test") + @InjectMojo(goal = "clean") + public void testMissingDirectory(CleanMojo mojo) throws Exception { mojo.execute(); - assertFalse(checkExists(getBasedir() + "/target/test-classes/unit/missing-directory-test/does-not-exist")); + assertFalse(checkExists(getBasedir() + "/does-not-exist")); } /** @@ -171,14 +166,15 @@ void testMissingDirectory(CleanMojo mojo) throws Exception { */ @Test @EnabledOnOs(OS.WINDOWS) - @InjectMojo(goal = "clean", pom = "classpath:/unit/locked-file-test/plugin-pom.xml") - void testCleanLockedFile(CleanMojo mojo) throws Exception { - File f = new File(getBasedir(), "target/test-classes/unit/locked-file-test/buildDirectory/file.txt"); + @Basedir("${basedir}/target/test-classes/unit/locked-file-test") + @InjectMojo(goal = "clean") + public void testCleanLockedFile(CleanMojo mojo) throws Exception { + File f = new File(getBasedir(), "buildDirectory/file.txt"); try (FileChannel channel = new RandomAccessFile(f, "rw").getChannel(); FileLock ignored = channel.lock()) { mojo.execute(); fail("Should fail to delete a file that is locked"); - } catch (MojoExecutionException expected) { + } catch (MojoException expected) { assertTrue(true); } } @@ -193,29 +189,29 @@ void testCleanLockedFile(CleanMojo mojo) throws Exception { */ @Test @EnabledOnOs(OS.WINDOWS) - @InjectMojo(goal = "clean", pom = "classpath:/unit/locked-file-test/plugin-pom.xml") - void testCleanLockedFileWithNoError(CleanMojo mojo) throws Exception { + @Basedir("${basedir}/target/test-classes/unit/locked-file-test") + @InjectMojo(goal = "clean") + public void testCleanLockedFileWithNoError(CleanMojo mojo) throws Exception { setVariableValueToObject(mojo, "failOnError", Boolean.FALSE); assertNotNull(mojo); - File f = new File(getBasedir(), "target/test-classes/unit/locked-file-test/buildDirectory/file.txt"); + File f = new File(getBasedir(), "buildDirectory/file.txt"); try (FileChannel channel = new RandomAccessFile(f, "rw").getChannel(); FileLock ignored = channel.lock()) { mojo.execute(); assertTrue(true); - } catch (MojoExecutionException expected) { + } catch (MojoException expected) { fail("Should display a warning when deleting a file that is locked"); } } /** * Test the followLink option with windows junctions - * * @throws Exception */ @Test @EnabledOnOs(OS.WINDOWS) - void testFollowLinksWithWindowsJunction() throws Exception { + public void testFollowLinksWithWindowsJunction() throws Exception { testSymlink((link, target) -> { Process process = new ProcessBuilder() .directory(link.getParent().toFile()) @@ -233,12 +229,11 @@ void testFollowLinksWithWindowsJunction() throws Exception { /** * Test the followLink option with sym link - * * @throws Exception */ @Test @DisabledOnOs(OS.WINDOWS) - void testFollowLinksWithSymLinkOnPosix() throws Exception { + public void testFollowLinksWithSymLinkOnPosix() throws Exception { testSymlink((link, target) -> { try { Files.createSymbolicLink(link, target); @@ -248,9 +243,13 @@ void testFollowLinksWithSymLinkOnPosix() throws Exception { }); } + @FunctionalInterface + interface LinkCreator { + void createLink(Path link, Path target) throws Exception; + } + private void testSymlink(LinkCreator linkCreator) throws Exception { - // We use the SystemStreamLog() as the AbstractMojo class, because from there the Log is always provided - Cleaner cleaner = new Cleaner(null, new SystemStreamLog(), false, null, null); + Cleaner cleaner = new Cleaner(null, null, false, null, null); Path testDir = Paths.get("target/test-classes/unit/test-dir").toAbsolutePath(); Path dirWithLnk = testDir.resolve("dir"); Path orgDir = testDir.resolve("org-dir"); @@ -263,7 +262,7 @@ private void testSymlink(LinkCreator linkCreator) throws Exception { Files.write(file, Collections.singleton("Hello world")); linkCreator.createLink(jctDir, orgDir); // delete - cleaner.delete(dirWithLnk.toFile(), null, false, true, false); + cleaner.delete(dirWithLnk, null, false, true, false); // verify assertTrue(Files.exists(file)); assertFalse(Files.exists(jctDir)); @@ -276,7 +275,7 @@ private void testSymlink(LinkCreator linkCreator) throws Exception { Files.write(file, Collections.singleton("Hello world")); linkCreator.createLink(jctDir, orgDir); // delete - cleaner.delete(dirWithLnk.toFile(), null, true, true, false); + cleaner.delete(dirWithLnk, null, true, true, false); // verify assertFalse(Files.exists(file)); assertFalse(Files.exists(jctDir)); @@ -284,6 +283,32 @@ private void testSymlink(LinkCreator linkCreator) throws Exception { assertFalse(Files.exists(dirWithLnk)); } + // @Provides + // @Singleton + // private Project createProject() { + // ProjectStub project = new ProjectStub(); + // project.setGroupId("myGroupId"); + // return project; + // } + + // @Provides + // @Singleton + // @SuppressWarnings("unused") + // private InternalSession createSession() { + // InternalSession session = SessionStub.getMockSession(LOCAL_REPO); + // Properties props = new Properties(); + // props.put("basedir", MojoExtension.getBasedir()); + // doReturn(props).when(session).getSystemProperties(); + // return session; + // } + + // @Provides + // @Singleton + // @SuppressWarnings("unused") + // private MojoExecution createMojoExecution() { + // return new MojoExecutionStub("default-clean", "clean"); + // } + /** * @param dir a dir or a file * @return true if a file/dir exists, false otherwise @@ -300,9 +325,4 @@ private boolean checkEmpty(String dir) { File[] files = new File(dir).listFiles(); return files == null || files.length == 0; } - - @FunctionalInterface - interface LinkCreator { - void createLink(Path link, Path target) throws Exception; - } } diff --git a/src/test/resources/unit/basic-clean-test/plugin-pom.xml b/src/test/resources/unit/basic-clean-test/plugin-pom.xml deleted file mode 100644 index 38a6000..0000000 --- a/src/test/resources/unit/basic-clean-test/plugin-pom.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - maven-clean-plugin - - ${basedir}/target/test-classes/unit/basic-clean-test/buildDirectory - ${basedir}/target/test-classes/unit/basic-clean-test/buildOutputDirectory - ${basedir}/target/test-classes/unit/basic-clean-test/buildTestDirectory - true - true - - - - - diff --git a/src/test/resources/unit/nested-clean-test/plugin-pom.xml b/src/test/resources/unit/basic-clean-test/pom.xml similarity index 76% rename from src/test/resources/unit/nested-clean-test/plugin-pom.xml rename to src/test/resources/unit/basic-clean-test/pom.xml index 790d14d..ba901f0 100644 --- a/src/test/resources/unit/nested-clean-test/plugin-pom.xml +++ b/src/test/resources/unit/basic-clean-test/pom.xml @@ -23,9 +23,9 @@ maven-clean-plugin - ${basedir}/target/test-classes/unit/nested-clean-test/target - ${basedir}/target/test-classes/unit/nested-clean-test/target/classes - ${basedir}/target/test-classes/unit/nested-clean-test/target/test-classes + ${project.basedir}/buildDirectory + ${project.basedir}/buildOutputDirectory + ${project.basedir}/buildTestDirectory true true diff --git a/src/test/resources/unit/empty-clean-test/plugin-pom.xml b/src/test/resources/unit/empty-clean-test/pom.xml similarity index 100% rename from src/test/resources/unit/empty-clean-test/plugin-pom.xml rename to src/test/resources/unit/empty-clean-test/pom.xml diff --git a/src/test/resources/unit/fileset-clean-test/plugin-pom.xml b/src/test/resources/unit/fileset-clean-test/pom.xml similarity index 88% rename from src/test/resources/unit/fileset-clean-test/plugin-pom.xml rename to src/test/resources/unit/fileset-clean-test/pom.xml index 5cd3ab3..2457c4e 100644 --- a/src/test/resources/unit/fileset-clean-test/plugin-pom.xml +++ b/src/test/resources/unit/fileset-clean-test/pom.xml @@ -23,9 +23,10 @@ maven-clean-plugin + true - ${basedir}/target/test-classes/unit/fileset-clean-test/target + ${project.basedir}/target **/file.txt **/test-classes/** @@ -35,7 +36,7 @@ - ${basedir}/target/test-classes/unit/fileset-clean-test/buildOutputDirectory + ${project.basedir}/buildOutputDirectory ** diff --git a/src/test/resources/unit/locked-file-test/plugin-pom.xml b/src/test/resources/unit/invalid-directory-test/pom.xml similarity index 91% rename from src/test/resources/unit/locked-file-test/plugin-pom.xml rename to src/test/resources/unit/invalid-directory-test/pom.xml index 4101091..5d35e78 100644 --- a/src/test/resources/unit/locked-file-test/plugin-pom.xml +++ b/src/test/resources/unit/invalid-directory-test/pom.xml @@ -23,7 +23,7 @@ maven-clean-plugin - ${basedir}/target/test-classes/unit/locked-file-test/buildDirectory + ${project.basedir}/this-is-a-file true true diff --git a/src/test/resources/unit/missing-directory-test/plugin-pom.xml b/src/test/resources/unit/locked-file-test/pom.xml similarity index 91% rename from src/test/resources/unit/missing-directory-test/plugin-pom.xml rename to src/test/resources/unit/locked-file-test/pom.xml index 15b3923..1f2f429 100644 --- a/src/test/resources/unit/missing-directory-test/plugin-pom.xml +++ b/src/test/resources/unit/locked-file-test/pom.xml @@ -23,7 +23,7 @@ maven-clean-plugin - ${basedir}/target/test-classes/unit/missing-clean-test/does-not-exist + ${project.basedir}/buildDirectory true true diff --git a/src/test/resources/unit/invalid-directory-test/plugin-pom.xml b/src/test/resources/unit/missing-directory-test/pom.xml similarity index 91% rename from src/test/resources/unit/invalid-directory-test/plugin-pom.xml rename to src/test/resources/unit/missing-directory-test/pom.xml index b562122..ec695d1 100644 --- a/src/test/resources/unit/invalid-directory-test/plugin-pom.xml +++ b/src/test/resources/unit/missing-directory-test/pom.xml @@ -23,7 +23,7 @@ maven-clean-plugin - ${basedir}/target/test-classes/unit/invalid-directory-test/this-is-a-file + ${project.basedir}/does-not-exist true true diff --git a/src/test/resources/unit/nested-clean-test/pom.xml b/src/test/resources/unit/nested-clean-test/pom.xml new file mode 100644 index 0000000..5f9e755 --- /dev/null +++ b/src/test/resources/unit/nested-clean-test/pom.xml @@ -0,0 +1,35 @@ + + + + + + + maven-clean-plugin + + ${project.basedir}/target + ${project.basedir}/target/classes + ${project.basedir}/target/test-classes + true + true + + + + +