You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@ant.apache.org by bo...@apache.org on 2026/02/06 17:49:39 UTC
(ant) 01/02: add that deletes symbolic links or Windows junctions
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 <bo...@apache.org>
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">