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


The following commit(s) were added to refs/heads/master by this push:
     new 0bdebe0ca add a new mklink task that can create file system links on 
windows
0bdebe0ca is described below

commit 0bdebe0ca3637822e79f027450e91038fb88f3f8
Author: Stefan Bodewig <[email protected]>
AuthorDate: Fri Feb 6 15:55:35 2026 +0100

    add a new mklink task that can create file system links on windows
---
 WHATSNEW                                           |   3 +
 manual/Tasks/mklink.html                           |  93 ++++++++++
 .../apache/tools/ant/taskdefs/defaults.properties  |   1 +
 .../ant/taskdefs/optional/windows/Mklink.java      | 201 +++++++++++++++++++++
 .../taskdefs/optional/windows/mklink-test.xml      |  64 +++++++
 5 files changed, 362 insertions(+)

diff --git a/WHATSNEW b/WHATSNEW
index 5d821e856..771a0f77e 100644
--- a/WHATSNEW
+++ b/WHATSNEW
@@ -541,6 +541,9 @@ 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/mklink.html b/manual/Tasks/mklink.html
new file mode 100644
index 000000000..33a6b371c
--- /dev/null
+++ b/manual/Tasks/mklink.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<!--
+   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.
+-->
+<html lang="en">
+
+<head>
+<link rel="stylesheet" type="text/css" href="../stylesheets/style.css">
+<title>Mklink Task</title>
+</head>
+
+<body>
+
+<h2 id="symlink">Mklink</h2>
+<p><em>Since Apache Ant 1.10.9</em>.</p>
+<h3>Description</h3>
+<p>Creates hardlinks, directory junctions and symbolic links on the
+  windows platform.</p>
+<p>The task is just a wrapper
+  around <a 
href="https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/mklink";>mklink</a>
+  and performs a few sanity checks but will not catch all restrictions
+  like hardlinks or junctions not crossing volume boundaries.</p>
+<p>In order to create symbolic links you need the "create symbolic
+  link" permission - this can be achieved by enabling developer mode
+  on the machine running Ant, running Ant with elevated permissions
+  (which sounds like a bad idea) or enable the permission explicitly
+  for oyur user.</p>
+<h3>Parameters</h3>
+<table class="attr">
+  <tr>
+    <th scope="col">Attribute</th>
+    <th scope="col">Description</th>
+    <th scope="col">Required</th>
+  </tr>
+  <tr>
+    <td>linkType</td>
+    <td>The type of link to create, may be <q>file-symlink</q>, 
<q>dir-symlink</q>, <q>hardlink</q>
+      or <q>junction</q>.</td>
+    <td>Yes unless <q>targetFile</q> is given in which case it
+    defaults to <q>file-symlink</q> or <q>dir-symlink</q> depending
+    on whether the target is a plain file or a directory.</td>
+  </tr>
+  <tr>
+    <td>link</td>
+    <td>The name of the link to be created, will be resolved relativ
+      to the project's basedir.</td>
+    <td>Yes</td>
+  </tr>
+  <tr>
+    <td>targetFile</td>
+    <td>The resource the link should point to, will be resolved relativ
+      to the project's basedir.</td>
+    <td>Exactly one of <q>targetFile</q> and <q>targetText</q> must
+      be given.</td>
+  </tr>
+  <tr>
+    <td>targetText</td>
+    <td>The resource the link should point to. This will be passed to
+      <q>mklink</q> verbatim.</td>
+    <td>Exactly one of <q>targetFile</q> and <q>targetText</q> must
+      be given.</td>
+  </tr>
+  <tr>
+    <td>overwrite</td>
+    <td>Overwrite existing files or not. If overwrite is set to <q>true</q>, 
then any existing file,
+      specified by the link attribute, will be overwritten irrespective of 
whether or not the
+      existing file is a link. otherwise and existing file is
+      considered an error and make the task fail.</td>
+    <td>No; defaults to <q>false</q></td>
+  </tr>
+</table>
+
+<h3>Examples</h3>
+
+<p>Make a hardlink named <samp>foo</samp> to a resource named 
<samp>bar.foo</samp>
+in <samp>subdir</samp>:</p>
+<pre>&lt;mklink link="${dir.top}/foo" targetFile="${dir.top}/subdir/bar.foo" 
linktype="hardlink"/&gt;</pre>
+
+</body>
+</html>
diff --git a/src/main/org/apache/tools/ant/taskdefs/defaults.properties 
b/src/main/org/apache/tools/ant/taskdefs/defaults.properties
index 791d8d9fe..01ed2ccaf 100644
--- a/src/main/org/apache/tools/ant/taskdefs/defaults.properties
+++ b/src/main/org/apache/tools/ant/taskdefs/defaults.properties
@@ -164,6 +164,7 @@ jjtree=org.apache.tools.ant.taskdefs.optional.javacc.JJTree
 junit=org.apache.tools.ant.taskdefs.optional.junit.JUnitTask
 junitreport=org.apache.tools.ant.taskdefs.optional.junit.XMLResultAggregator
 
junitlauncher=org.apache.tools.ant.taskdefs.optional.junitlauncher.confined.JUnitLauncherTask
+mklink=org.apache.tools.ant.taskdefs.optional.windows.Mklink
 native2ascii=org.apache.tools.ant.taskdefs.optional.Native2Ascii
 netrexxc=org.apache.tools.ant.taskdefs.optional.NetRexxC
 propertyfile=org.apache.tools.ant.taskdefs.optional.PropertyFile
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
new file mode 100644
index 000000000..53899ed73
--- /dev/null
+++ b/src/main/org/apache/tools/ant/taskdefs/optional/windows/Mklink.java
@@ -0,0 +1,201 @@
+/*
+ *  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.taskdefs.optional.windows;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.apache.tools.ant.BuildException;
+import org.apache.tools.ant.Project;
+import org.apache.tools.ant.Task;
+import org.apache.tools.ant.taskdefs.Execute;
+import org.apache.tools.ant.taskdefs.LogStreamHandler;
+import org.apache.tools.ant.taskdefs.condition.Os;
+import org.apache.tools.ant.types.Commandline;
+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
+ */
+public class Mklink extends Task {
+    private static final String FILE_SYMLINK = "file-symlink";
+    private static final String DIR_SYMLINK = "dir-symlink";
+    private static final String HARDLINK = "hardlink";
+    private static final String JUNCTION = "junction";
+
+    private File link;
+    private File targetFile;
+    private String targetText;
+    private LinkType linkType;
+    private boolean overwrite;
+
+    /**
+     * The link to create.
+     *
+     * @param link the path of the link to create
+     */
+    public void setLink(File link) {
+        this.link = link;
+    }
+
+    /**
+     * The link target specified as file.
+     *
+     * @param target the path of the link taget as resolved file
+     */
+    public void setTargetFile(File target) {
+        this.targetFile = target;
+    }
+
+    /**
+     * The link target specified as text.
+     *
+     * @param target the path of the link taget as string
+     */
+    public void setTargetText(String target) {
+        this.targetText = target;
+    }
+
+    /**
+     * The type of link to create.
+     *
+     * <p>If not specified explicitly and target is given as a file
+     * the link type will be guessed to be the proper type of symlink
+     * for the target type.</p>
+     *
+     * @param type one of "file-symlink", "dir-symlink", "hardlink" or 
"junction".
+     */
+    public void setLinkType(LinkType type) {
+        linkType = type;
+    }
+
+    /**
+     * Set overwrite mode. If set to false (default) the task will not
+     * overwrite existing links.
+     *
+     * @param owrite If true overwrite existing links.
+     */
+    public void setOverwrite(boolean owrite) {
+        this.overwrite = owrite;
+    }
+
+    @Override
+    public void execute() throws BuildException {
+        validate();
+        runMklink();
+    }
+
+    private void validate() throws BuildException {
+        if (!Os.isFamily(Os.FAMILY_WINDOWS)) {
+            throw new BuildException("this task only works on Windows");
+        }
+        if (link == null) {
+            throw new BuildException("this \"link\" attribute is required");
+        }
+        if (targetFile == null && targetText == null) {
+            throw new BuildException("either \"targetFile\" the \"targetText\" 
attribute is required");
+        }
+        if (targetFile != null && targetText != null) {
+            throw new BuildException("only one of \"targetFile\" and 
\"targetText\" can be specified");
+        }
+        if (linkType == null && targetFile == null) {
+            throw new BuildException("\"linkType\" is required when using 
\"targetText");
+        }
+    }
+
+    private void runMklink() throws BuildException {
+        Commandline cmd = createCommandLine();
+
+        if (link.exists()) {
+            if (!overwrite) {
+                log("Skipping link creation, since file at " + link + " 
already exists and overwrite is set to false", Project.MSG_INFO);
+                return;
+            }
+            boolean deleted = link.delete();
+            if (!deleted) {
+                throw new BuildException("Deletion of existing file at " + 
link + " failed");
+            }
+        }
+        Execute exe = new Execute(new LogStreamHandler(this, Project.MSG_INFO, 
Project.MSG_WARN),
+                                  null);
+        exe.setCommandline(cmd.getCommandline());
+        exe.setWorkingDirectory(getProject().getBaseDir());
+        log(cmd.describeCommand(), Project.MSG_VERBOSE);
+        try {
+            int returncode = exe.execute();
+            if (Execute.isFailure(returncode)) {
+                throw new BuildException("'mklink' failed with exit code " + 
returncode);
+            }
+        } catch (IOException e) {
+            throw new BuildException(e, getLocation());
+        }
+    }
+
+    private Commandline createCommandLine() throws BuildException {
+        StringBuilder sb = new StringBuilder();
+        sb.append("mklink ");
+        String linkValue = linkType != null ? linkType.getValue()
+            : targetFile.isDirectory() ? DIR_SYMLINK : FILE_SYMLINK;
+        if (DIR_SYMLINK.equals(linkValue)) {
+            sb.append("/d ");
+            if (targetFile != null && !targetFile.isDirectory()) {
+                throw new BuildException("target of a directory symlink must 
be a directory");
+            }
+        } else if (HARDLINK.equals(linkValue)) {
+            sb.append("/h ");
+            if (targetFile != null && !targetFile.isFile()) {
+                throw new BuildException("target of a hardlink must be a 
file");
+            }
+        } else if (JUNCTION.equals(linkValue)) {
+            sb.append("/j ");
+            if (targetFile != null && !targetFile.isDirectory()) {
+                throw new BuildException("target of a directory junction must 
be a directory");
+            }
+        } else if (targetFile != null && !targetFile.isFile()) {
+            throw new BuildException("target of a file symlink must be a 
file");
+        }
+        sb.append(Commandline.quoteArgument(link.getAbsolutePath()));
+        sb.append(" ");
+        if (targetFile != null) {
+            sb.append(Commandline.quoteArgument(targetFile.getAbsolutePath()));
+        } else {
+            sb.append(Commandline.quoteArgument(targetText));
+        }
+
+        Commandline cmd = new Commandline();
+        cmd.setExecutable("cmd.exe");
+        cmd.createArgument().setValue("/c");
+        cmd.createArgument().setValue(sb.toString());
+        return cmd;
+    }
+
+    public static class LinkType extends EnumeratedAttribute {
+        /**
+         * @see EnumeratedAttribute#getValues
+         * {@inheritDoc}.
+         */
+        @Override
+        public String[] getValues() {
+            return new String[] {FILE_SYMLINK, DIR_SYMLINK, HARDLINK, 
JUNCTION};
+        }
+    }
+
+}
diff --git a/src/tests/antunit/taskdefs/optional/windows/mklink-test.xml 
b/src/tests/antunit/taskdefs/optional/windows/mklink-test.xml
new file mode 100644
index 000000000..7f5d4b0df
--- /dev/null
+++ b/src/tests/antunit/taskdefs/optional/windows/mklink-test.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+   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.
+-->
+<project name="mklink-test"
+    default="antunit" xmlns:au="antlib:org.apache.ant.antunit">
+  <import file="../../../antunit-base.xml"/>
+
+  <target name="setUp">
+    <condition property="isWindows">
+      <os family="windows" />
+    </condition>
+    <mkdir dir="${input}/foo" />
+    <touch file="${input}/foo/test"/>
+    <mkdir dir="${output}" />
+  </target>
+
+  <target name="testCreateJunction" depends="setUp" if="isWindows">
+    <mklink linktype="junction" link="${output}/bar"
+            targetFile="${input}/foo"/>
+    <au:assertFileExists file="${output}/bar/test"/>
+  </target>
+
+  <target name="testCreateHardlink" depends="setUp" if="isWindows">
+    <mklink linktype="hardlink" link="${output}/bar"
+            targetFile="${input}/foo/test"/>
+    <au:assertFileExists file="${output}/bar"/>
+  </target>
+
+  <target name="testCreateHardlinkWithOverwrite" depends="setUp" 
if="isWindows">
+    <mklink linktype="hardlink" link="${output}/bar"
+            targetFile="${input}/foo/test"/>
+    <mklink linktype="hardlink" link="${output}/bar"
+            targetFile="${input}/foo/test"
+            overwrite="true"/>
+    <au:assertFileExists file="${output}/bar"/>
+  </target>
+
+  <target name="XtestCreateFileSymlink" depends="setUp" if="isWindows">
+    <mklink linktype="file-symlink" link="${output}/bar"
+            targetFile="${input}/foo/test"/>
+    <au:assertFileExists file="${output}/bar"/>
+  </target>
+
+  <target name="XtestCreateDirectorySymlink" depends="setUp" if="isWindows">
+    <mklink linktype="dir-symlink" link="${output}/bar"
+            targetFile="${input}/foo"/>
+    <au:assertFileExists file="${output}/bar/test"/>
+  </target>
+
+</project>

Reply via email to