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>&lt;fileset&gt;</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>&lt;delete link=...&gt;</code> can be
+ * used as an alterantive to the &quot;delete&quot; action.</p>
+ *
  * <p>Usage examples:</p>
  *
  * <p>Make a link named &quot;foo&quot; 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">

Reply via email to