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><mklink link="${dir.top}/foo" targetFile="${dir.top}/subdir/bar.foo"
linktype="hardlink"/></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>