This is an automated email from the ASF dual-hosted git repository. bodewig pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/ant.git
commit 3c005b701a7561f70282624661807fcc03671809 Author: Stefan Bodewig <[email protected]> AuthorDate: Fri Feb 6 18:30:42 2026 +0100 add <delete link="..."> that deletes symbolic links or Windows junctions --- WHATSNEW | 11 +- manual/Tasks/delete.html | 13 ++- manual/Tasks/mklink.html | 2 +- src/main/org/apache/tools/ant/taskdefs/Delete.java | 33 +++++- .../tools/ant/taskdefs/optional/unix/Symlink.java | 3 + .../ant/taskdefs/optional/windows/Mklink.java | 2 +- .../apache/tools/ant/util/NtfsJunctionUtils.java | 112 +++++++++++++++++++++ .../antunit/core/dirscanner-symlinks-test.xml | 14 +-- .../antunit/taskdefs/delete-and-symlinks-test.xml | 2 +- 9 files changed, 175 insertions(+), 17 deletions(-) diff --git a/WHATSNEW b/WHATSNEW index 771a0f77e..b2d6b4633 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -60,6 +60,14 @@ Other changes: MailLogger property and a new <mail> task attribute. Bugzilla Report 69416 + * added a Windows specific <mklink> task that can be used to create + hard links, symbolic links and NTFS directory junctions. + + * added <delete link="..."> that can be used delete symbolic links or + NTFS directory junctions. For symbolic links this duplicates what + <symlink action="delete" ...> does - it has been introduced to + handle symlinks and junctions via a single API. + Changes from Ant 1.10.14 TO Ant 1.10.15 ======================================= @@ -541,9 +549,6 @@ Other changes: * a new property ant.tmpdir provides improved control over the location Ant uses to create temporary files - * added a Windows specific <mklink> task that can be used to create - hard links, symbolic links and NTFS directory junctions. - Changes from Ant 1.10.6 TO Ant 1.10.7 ===================================== diff --git a/manual/Tasks/delete.html b/manual/Tasks/delete.html index a019de97c..475414284 100644 --- a/manual/Tasks/delete.html +++ b/manual/Tasks/delete.html @@ -26,7 +26,7 @@ <h2 id="delete">Delete</h2> <h3>Description</h3> -<p>Deletes a single file, a specified directory and all its files and subdirectories, or a set of +<p>Deletes a single file, a symbolic link or NTFS junction, a specified directory and all its files and subdirectories, or a set of files specified by one or more <a href="../Types/resources.html#collection">resource collection</a>s. The literal implication of <code><fileset></code> is that directories are not included; however the removal of empty directories can be triggered when using nested filesets @@ -57,7 +57,7 @@ <h3>Parameters</h3> <td>file</td> <td>The file to delete, specified as either the simple filename (if the file exists in the current base directory), a relative-path filename, or a full-path filename.</td> - <td rowspan="2">At least one of the two, unless nested resource collections are specified</td> + <td rowspan="3">At least one of the three, unless nested resource collections are specified</td> </tr> <tr> <td>dir</td> @@ -69,6 +69,15 @@ <h3>Parameters</h3> truly <em>intend</em> to recursively remove the entire contents of the current base directory (and the base directory itself, if different from the current working directory).</td> </tr> + <tr> + <td>link</td> + <td>The symbolic link or Windows directory junction to delete, + specified as either the simple filename (if the link exists in + the current base directory), a relative-path filename, or a + full-path filename.<br/> + <em>since Ant 1.10.16</em>. + </td> + </tr> <tr> <td>verbose</td> <td>Whether to show the name of each deleted file.</td> diff --git a/manual/Tasks/mklink.html b/manual/Tasks/mklink.html index 33a6b371c..f63bdaac7 100644 --- a/manual/Tasks/mklink.html +++ b/manual/Tasks/mklink.html @@ -25,7 +25,7 @@ <body> <h2 id="symlink">Mklink</h2> -<p><em>Since Apache Ant 1.10.9</em>.</p> +<p><em>Since Apache Ant 1.10.16</em>.</p> <h3>Description</h3> <p>Creates hardlinks, directory junctions and symbolic links on the windows platform.</p> diff --git a/src/main/org/apache/tools/ant/taskdefs/Delete.java b/src/main/org/apache/tools/ant/taskdefs/Delete.java index 1917c8391..665c36255 100644 --- a/src/main/org/apache/tools/ant/taskdefs/Delete.java +++ b/src/main/org/apache/tools/ant/taskdefs/Delete.java @@ -63,6 +63,7 @@ import org.apache.tools.ant.types.selectors.SelectSelector; import org.apache.tools.ant.types.selectors.SizeSelector; import org.apache.tools.ant.types.selectors.modifiedselector.ModifiedSelector; import org.apache.tools.ant.util.FileUtils; +import org.apache.tools.ant.util.NtfsJunctionUtils; /** * Deletes a file or directory, or set of files defined by a fileset. @@ -82,6 +83,7 @@ public class Delete extends MatchingTask { private static final ResourceComparator REVERSE_FILESYSTEM = new Reverse(new FileSystem()); private static final ResourceSelector EXISTS = new Exists(); private static FileUtils FILE_UTILS = FileUtils.getFileUtils(); + private static final NtfsJunctionUtils JUNCTION_UTILS = NtfsJunctionUtils.getNtfsJunctionUtils(); private static class ReverseDirs implements ResourceCollection { @@ -114,6 +116,7 @@ public class Delete extends MatchingTask { // CheckStyle:VisibilityModifier OFF - bc protected File file = null; + protected File link = null; protected File dir = null; protected Vector<FileSet> filesets = new Vector<>(); protected boolean usedMatchingTask = false; @@ -138,6 +141,16 @@ public class Delete extends MatchingTask { this.file = file; } + /** + * Set the name of a single symbolic link or junction to be removed. + * + * @param file the link to be deleted + * @since Ant 1.10.16 + */ + public void setLink(File link) { + this.link = link; + } + /** * Set the directory from which files are to be deleted * @@ -588,9 +601,9 @@ public class Delete extends MatchingTask { quiet ? Project.MSG_VERBOSE : verbosity); } - if (file == null && dir == null && filesets.isEmpty() && rcs == null) { + if (file == null && link == null && dir == null && filesets.isEmpty() && rcs == null) { throw new BuildException( - "At least one of the file or dir attributes, or a nested resource collection, must be set."); + "At least one of the file, link or dir attributes, or a nested resource collection, must be set."); } if (quiet && failonerror) { @@ -625,6 +638,22 @@ public class Delete extends MatchingTask { } } + // delete the single link + if (link != null) { + if (link.exists()) { + if (Files.isSymbolicLink(link.toPath()) || JUNCTION_UTILS.isDirectoryJunctionSafe(link)) { + log("Deleting: " + link.getAbsolutePath()); + + if (!delete(link)) { + handle("Unable to delete link " + link.getAbsolutePath()); + } + } + } else { + log("Could not find link " + link.getAbsolutePath() + + " to delete.", quiet ? Project.MSG_VERBOSE : verbosity); + } + } + // delete the directory if (dir != null && !usedMatchingTask) { if (dir.exists() && dir.isDirectory()) { diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/unix/Symlink.java b/src/main/org/apache/tools/ant/taskdefs/optional/unix/Symlink.java index 54d6811ba..7cc0c022b 100644 --- a/src/main/org/apache/tools/ant/taskdefs/optional/unix/Symlink.java +++ b/src/main/org/apache/tools/ant/taskdefs/optional/unix/Symlink.java @@ -71,6 +71,9 @@ import org.apache.tools.ant.types.FileSet; * that have been previously recorded for each directory. Finally, it can be * used to remove a symlink without deleting the associated resource.</p> * + * <p>Since Ant 1.10.16 <code><delete link=...></code> can be + * used as an alterantive to the "delete" action.</p> + * * <p>Usage examples:</p> * * <p>Make a link named "foo" to a resource named diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/windows/Mklink.java b/src/main/org/apache/tools/ant/taskdefs/optional/windows/Mklink.java index 53899ed73..2a3a225ef 100644 --- a/src/main/org/apache/tools/ant/taskdefs/optional/windows/Mklink.java +++ b/src/main/org/apache/tools/ant/taskdefs/optional/windows/Mklink.java @@ -33,7 +33,7 @@ import org.apache.tools.ant.types.EnumeratedAttribute; /** * Runs <a href="https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/mklink">mklink</a> on Win32 systems. * - * @since Ant 1.10.9 + * @since Ant 1.10.16 */ public class Mklink extends Task { private static final String FILE_SYMLINK = "file-symlink"; diff --git a/src/main/org/apache/tools/ant/util/NtfsJunctionUtils.java b/src/main/org/apache/tools/ant/util/NtfsJunctionUtils.java new file mode 100644 index 000000000..e86295879 --- /dev/null +++ b/src/main/org/apache/tools/ant/util/NtfsJunctionUtils.java @@ -0,0 +1,112 @@ +/* + * 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 + * + * https://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.tools.ant.util; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; + +import org.apache.tools.ant.taskdefs.condition.Os; + +/** + * Contains methods related to Windows NTFS junctions. + * + * @since Ant 1.10.16 + */ +public class NtfsJunctionUtils { + + private static final boolean ON_WINDOWS = Os.isFamily("windows"); + + /** + * Shared instance. + */ + private static final NtfsJunctionUtils PRIMARY_INSTANCE = new NtfsJunctionUtils(); + + /** + * Method to retrieve The NtfsJunctionUtils, which is shared by + * all users of this method. + * @return an instance of NtfsJunctionUtils. + */ + public static NtfsJunctionUtils getNtfsJunctionUtils() { + return PRIMARY_INSTANCE; + } + + /** + * Empty constructor. + */ + protected NtfsJunctionUtils() { + } + + /** + * Checks whether a given file is a directory junction. + * + * @return true if the file is a directory junction. + * @throws IOException on error. + */ + public boolean isDirectoryJunction(final File file) throws IOException { + return isDirectoryJunction(file.toPath()); + } + + /** + * Checks whether a given file is a directory junction. + * + * @return false if the given file is not a directory junction or + * an exception occured while trying to check the file - most + * likely because the file didn't exists. + */ + public boolean isDirectoryJunctionSafe(final File file) { + return isDirectoryJunctionSafe(file.toPath()); + } + + /** + * Checks whether a given path is a directory junction. + * + * @return false if the given path is not a directory junction or + * an exception occured while trying to check the path - most + * likely because the path didn't exists. + */ + public boolean isDirectoryJunctionSafe(final Path path) { + try { + return isDirectoryJunction(path); + } catch (FileNotFoundException ex) { + // ignore + } catch (IOException ex) { + System.err.println("Caught IOException " + ex.getMessage() + " while testing for junction."); + } + return false; + } + + /** + * Checks whether a given path is a directory junction. + * + * @return true if the path is a directory junction. + * @throws IOException on error. + */ + public boolean isDirectoryJunction(final Path path) throws IOException { + if (!ON_WINDOWS) { + return false; + } + BasicFileAttributes attrs = + Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + return attrs.isDirectory() && attrs.isOther(); + } +} diff --git a/src/tests/antunit/core/dirscanner-symlinks-test.xml b/src/tests/antunit/core/dirscanner-symlinks-test.xml index 934d4e7e4..8e365cd6d 100644 --- a/src/tests/antunit/core/dirscanner-symlinks-test.xml +++ b/src/tests/antunit/core/dirscanner-symlinks-test.xml @@ -81,7 +81,7 @@ <copy todir="${output}"> <fileset dir="${base}" followsymlinks="true" maxLevelsOfSymlinks="1"/> </copy> - <symlink action="delete" link="${base}/A"/> + <delete link="${base}/A"/> <au:assertFileExists file="${output}/A/B/file.txt"/> <au:assertFileDoesntExist file="${output}/A/base/A/B/file.txt"/> </target> @@ -92,7 +92,7 @@ <copy todir="${output}"> <fileset dir="${base}" followsymlinks="true" maxLevelsOfSymlinks="2"/> </copy> - <symlink action="delete" link="${base}/A"/> + <delete link="${base}/A"/> <au:assertFileExists file="${output}/A/B/file.txt"/> <au:assertFileExists file="${output}/A/base/A/B/file.txt"/> </target> @@ -105,7 +105,7 @@ <include name="A/B/*"/> </fileset> </copy> - <symlink action="delete" link="${base}/A"/> + <delete link="${base}/A"/> <au:assertFileExists file="${output}/A/B/file.txt"/> </target> @@ -118,7 +118,7 @@ <include name="A/base/A/B/*"/> </fileset> </copy> - <symlink action="delete" link="${base}/A"/> + <delete link="${base}/A"/> <au:assertFileExists file="${output}/A/base/A/B/file.txt"/> </target> @@ -128,7 +128,7 @@ <copy todir="${output}"> <fileset dir="${base}" followsymlinks="false"/> </copy> - <symlink action="delete" link="${base}/A"/> + <delete link="${base}/A"/> <au:assertFileDoesntExist file="${output}/A/B/file.txt"/> </target> @@ -138,7 +138,7 @@ <copy todir="${output}"> <fileset dir="${base}" followsymlinks="true"/> </copy> - <symlink action="delete" link="${base}"/> + <delete link="${base}"/> <assertDirIsEmpty/> </target> @@ -148,7 +148,7 @@ <copy todir="${output}"> <fileset dir="${base}" followsymlinks="false"/> </copy> - <symlink action="delete" link="${base}"/> + <delete link="${base}"/> <au:assertFileDoesntExist file="${output}"/> </target> diff --git a/src/tests/antunit/taskdefs/delete-and-symlinks-test.xml b/src/tests/antunit/taskdefs/delete-and-symlinks-test.xml index ffc4a8acf..fbe94ac74 100644 --- a/src/tests/antunit/taskdefs/delete-and-symlinks-test.xml +++ b/src/tests/antunit/taskdefs/delete-and-symlinks-test.xml @@ -28,7 +28,7 @@ <target name="tearDown" depends="removelink, antunit-base.tearDown"/> <target name="removelink" if="link"> - <symlink action="delete" link="${link}"/> + <delete link="${link}"/> </target> <target name="setUp" if="unix">
