/*
 *  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.
 *
 */

/*
 * Since the initial version of this file was developed on the clock on
 * an NSF grant I should say the following boilerplate:
 *
 * This material is based upon work supported by the National Science
 * Foundation under Grant No. EIA-0196404. Any opinions, findings, and
 * conclusions or recommendations expressed in this material are those
 * of the author and do not necessarily reflect the views of the
 * National Science Foundation.
 */

package org.apache.tools.ant.taskdefs.optional.unix;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Stream;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.dispatch.DispatchTask;
import org.apache.tools.ant.dispatch.DispatchUtils;
import org.apache.tools.ant.taskdefs.LogOutputStream;
import org.apache.tools.ant.types.FileSet;

Creates, Deletes, Records and Restores Symlinks.

This task performs several related operations. In the most trivial and default usage, it creates a link specified in the link attribute to a resource specified in the resource attribute. The second usage of this task is to traverse a directory structure specified by a fileset, and write a properties file in each included directory describing the links found in that directory. The third usage is to traverse a directory structure specified by a fileset, looking for properties files (also specified as included in the fileset) and recreate the links that have been previously recorded for each directory. Finally, it can be used to remove a symlink without deleting the associated resource.

Usage examples:

Make a link named "foo" to a resource named "bar.foo" in subdir:

<symlink link="${dir.top}/foo" resource="${dir.top}/subdir/bar.foo"/>

Record all links in subdir and its descendants in files named "dir.links":

<symlink action="record" linkfilename="dir.links">
   <fileset dir="${dir.top}" includes="subdir/**" />
</symlink>

Recreate the links recorded in the previous example:

<symlink action="recreate">
   <fileset dir="${dir.top}" includes="subdir/**/dir.links" />
</symlink>

Delete a link named "foo" to a resource named "bar.foo" in subdir:

<symlink action="delete" link="${dir.top}/foo"/>

Note: Starting Ant version 1.10.2, this task relies on the symbolic link support introduced in Java 7 through the Files APIs.

/** * Creates, Deletes, Records and Restores Symlinks. * * <p>This task performs several related operations. In the most trivial * and default usage, it creates a link specified in the link attribute to * a resource specified in the resource attribute. The second usage of this * task is to traverse a directory structure specified by a fileset, * and write a properties file in each included directory describing the * links found in that directory. The third usage is to traverse a * directory structure specified by a fileset, looking for properties files * (also specified as included in the fileset) and recreate the links * that have been previously recorded for each directory. Finally, it can be * used to remove a symlink without deleting the associated resource.</p> * * <p>Usage examples:</p> * * <p>Make a link named &quot;foo&quot; to a resource named * &quot;bar.foo&quot; in subdir:</p> * <pre> * &lt;symlink link=&quot;${dir.top}/foo&quot; resource=&quot;${dir.top}/subdir/bar.foo&quot;/&gt; * </pre> * * <p>Record all links in subdir and its descendants in files named * &quot;dir.links&quot;:</p> * <pre> * &lt;symlink action=&quot;record&quot; linkfilename=&quot;dir.links&quot;&gt; * &lt;fileset dir=&quot;${dir.top}&quot; includes=&quot;subdir&#47;**&quot; /&gt; * &lt;/symlink&gt; * </pre> * * <p>Recreate the links recorded in the previous example:</p> * <pre> * &lt;symlink action=&quot;recreate&quot;&gt; * &lt;fileset dir=&quot;${dir.top}&quot; includes=&quot;subdir&#47;**&#47;dir.links&quot; /&gt; * &lt;/symlink&gt; * </pre> * * <p>Delete a link named &quot;foo&quot; to a resource named * &quot;bar.foo&quot; in subdir:</p> * <pre> * &lt;symlink action=&quot;delete&quot; link=&quot;${dir.top}/foo&quot;/&gt; * </pre> * * <p><strong>Note:</strong> Starting Ant version 1.10.2, this task relies on the symbolic link support * introduced in Java 7 through the {@link Files} APIs. */
public class Symlink extends DispatchTask { private String resource; private String link; private List<FileSet> fileSets = new ArrayList<>(); private String linkFileName; private boolean overwrite; private boolean failonerror; private boolean executing = false;
Initialize the task.
Throws:
  • BuildException – on error.
/** * Initialize the task. * @throws BuildException on error. */
@Override public void init() throws BuildException { super.init(); setDefaults(); }
The standard method for executing any task.
Throws:
  • BuildException – on error.
/** * The standard method for executing any task. * @throws BuildException on error. */
@Override public synchronized void execute() throws BuildException { if (executing) { throw new BuildException( "Infinite recursion detected in Symlink.execute()"); } try { executing = true; DispatchUtils.execute(this); } finally { executing = false; } }
Create a symlink.
Throws:
  • BuildException – on error.
Since:Ant 1.7
/** * Create a symlink. * @throws BuildException on error. * @since Ant 1.7 */
public void single() throws BuildException { try { if (resource == null) { handleError("Must define the resource to symlink to!"); return; } if (link == null) { handleError("Must define the link name for symlink!"); return; } doLink(resource, link); } finally { setDefaults(); } }
Delete a symlink.
Throws:
  • BuildException – on error.
Since:Ant 1.7
/** * Delete a symlink. * @throws BuildException on error. * @since Ant 1.7 */
public void delete() throws BuildException { try { if (link == null) { handleError("Must define the link name for symlink!"); return; } final Path linkPath = Paths.get(link); if (!Files.isSymbolicLink(linkPath)) { log("Skipping deletion of " + linkPath + " since it's not a symlink", Project.MSG_VERBOSE); // just ignore and silently return (this is consistent // with the current, 1.9.x versions, of Ant) return; } log("Removing symlink: " + link); deleteSymLink(linkPath); } catch (IOException ioe) { handleError(ioe.getMessage()); } finally { setDefaults(); } }
Restore symlinks.
Throws:
  • BuildException – on error.
Since:Ant 1.7
/** * Restore symlinks. * @throws BuildException on error. * @since Ant 1.7 */
public void recreate() throws BuildException { try { if (fileSets.isEmpty()) { handleError( "File set identifying link file(s) required for action recreate"); return; } final Properties links = loadLinks(fileSets); for (final String lnk : links.stringPropertyNames()) { final String res = links.getProperty(lnk); try { if (Files.isSymbolicLink(Paths.get(lnk)) && new File(lnk).getCanonicalPath().equals(new File(res).getCanonicalPath())) { // it's already a symlink and the symlink target is the same // as the target noted in the properties file. So there's no // need to recreate it log("not recreating " + lnk + " as it points to the correct target already", Project.MSG_DEBUG); continue; } } catch (IOException e) { final String errMessage = "Failed to check if path " + lnk + " is a symbolic link, linking to " + res; if (failonerror) { throw new BuildException(errMessage, e); } // log and continue log(errMessage, Project.MSG_INFO); continue; } // create the link this.doLink(res, lnk); } } finally { setDefaults(); } }
Record symlinks.
Throws:
  • BuildException – on error.
Since:Ant 1.7
/** * Record symlinks. * @throws BuildException on error. * @since Ant 1.7 */
public void record() throws BuildException { try { if (fileSets.isEmpty()) { handleError("Fileset identifying links to record required"); return; } if (linkFileName == null) { handleError("Name of file to record links in required"); return; } // create a map to group them by parent directory: Map<File, List<File>> byDir = new HashMap<>(); // get an Iterator of file objects representing links (canonical): findLinks(fileSets).forEach(lnk -> byDir .computeIfAbsent(lnk.getParentFile(), k -> new ArrayList<>()) .add(lnk)); // write a Properties file in each directory: byDir.forEach((dir, linksInDir) -> { Properties linksToStore = new Properties(); // fill up a Properties object with link and resource names: for (File lnk : linksInDir) { try { linksToStore.put(lnk.getName(), lnk.getCanonicalPath()); } catch (IOException ioe) { handleError("Couldn't get canonical name of parent link"); } } writePropertyFile(linksToStore, dir); }); } finally { setDefaults(); } }
Return all variables to their default state for the next invocation.
Since:Ant 1.7
/** * Return all variables to their default state for the next invocation. * @since Ant 1.7 */
private void setDefaults() { resource = null; link = null; linkFileName = null; failonerror = true; // default behavior is to fail on an error overwrite = false; // default behavior is to not overwrite setAction("single"); // default behavior is make a single link fileSets.clear(); }
Set overwrite mode. If set to false (default) the task will not overwrite existing links, and may stop the build if a link already exists depending on the setting of failonerror.
Params:
  • owrite – If true overwrite existing links.
/** * Set overwrite mode. If set to false (default) * the task will not overwrite existing links, and may stop the build * if a link already exists depending on the setting of failonerror. * * @param owrite If true overwrite existing links. */
public void setOverwrite(boolean owrite) { this.overwrite = owrite; }
Set failonerror mode. If set to true (default) the entire build fails upon error; otherwise the error is logged and the build will continue.
Params:
  • foe – If true throw BuildException on error, else log it.
/** * Set failonerror mode. If set to true (default) the entire build fails * upon error; otherwise the error is logged and the build will continue. * * @param foe If true throw BuildException on error, else log it. */
public void setFailOnError(boolean foe) { this.failonerror = foe; }
Set the action to be performed. May be "single", "delete", "recreate" or "record".
Params:
  • action – The action to perform.
/** * Set the action to be performed. May be &quot;single&quot;, * &quot;delete&quot;, &quot;recreate&quot; or &quot;record&quot;. * * @param action The action to perform. */
@Override public void setAction(String action) { super.setAction(action); }
Set the name of the link. Used when action = "single".
Params:
  • lnk – The name for the link.
/** * Set the name of the link. Used when action = &quot;single&quot;. * * @param lnk The name for the link. */
public void setLink(String lnk) { this.link = lnk; }
Set the name of the resource to which a link should be created. Used when action = "single".
Params:
  • src – The resource to be linked.
/** * Set the name of the resource to which a link should be created. * Used when action = &quot;single&quot;. * * @param src The resource to be linked. */
public void setResource(String src) { this.resource = src; }
Set the name of the file to which links will be written. Used when action = "record".
Params:
  • lf – The name of the file to write links to.
/** * Set the name of the file to which links will be written. * Used when action = &quot;record&quot;. * * @param lf The name of the file to write links to. */
public void setLinkfilename(String lf) { this.linkFileName = lf; }
Add a fileset to this task.
Params:
  • set – The fileset to add.
/** * Add a fileset to this task. * * @param set The fileset to add. */
public void addFileset(FileSet set) { fileSets.add(set); }
Delete a symlink (without deleting the associated resource).

This is a convenience method that simply invokes deleteSymlink(File)

Params:
  • path – A string containing the path of the symlink to delete.
Throws:
Deprecated:use Files.delete(Path) instead
/** * Delete a symlink (without deleting the associated resource). * * <p>This is a convenience method that simply invokes {@link #deleteSymlink(File)}</p> * * @param path A string containing the path of the symlink to delete. * @throws IOException If the deletion attempt fails * * @deprecated use {@link Files#delete(Path)} instead */
@Deprecated public static void deleteSymlink(final String path) throws IOException { deleteSymlink(Paths.get(path).toFile()); }
Delete a symlink (without deleting the associated resource).

This is a utility method that removes a symlink without removing the resource that the symlink points to. If it is accidentally invoked on a real file, the real file will not be harmed and instead this method returns silently.

Since Ant 1.10.2 this method relies on the Files.isSymbolicLink(Path) and Files.delete(Path) to check and delete the symlink

Params:
  • linkfil – A File object of the symlink to delete. Cannot be null.
Throws:
  • IOException – If the attempt to delete runs into exception
Deprecated:use Files.delete(Path) instead
/** * Delete a symlink (without deleting the associated resource). * * <p>This is a utility method that removes a symlink without removing * the resource that the symlink points to. If it is accidentally invoked * on a real file, the real file will not be harmed and instead this method * returns silently.</p> * * <p>Since Ant 1.10.2 this method relies on the {@link Files#isSymbolicLink(Path)} * and {@link Files#delete(Path)} to check and delete the symlink * </p> * * @param linkfil A <code>File</code> object of the symlink to delete. Cannot be null. * @throws IOException If the attempt to delete runs into exception * * @deprecated use {@link Files#delete(Path)} instead */
@Deprecated public static void deleteSymlink(final File linkfil) throws IOException { if (!Files.isSymbolicLink(linkfil.toPath())) { return; } deleteSymLink(linkfil.toPath()); }
Write a properties file. This method uses Properties.store and thus may throw exceptions that occur while writing the file.
Params:
  • properties – The properties object to be written.
  • dir – The directory for which we are writing the links.
Throws:
/** * Write a properties file. This method uses <code>Properties.store</code> * and thus may throw exceptions that occur while writing the file. * * @param properties The properties object to be written. * @param dir The directory for which we are writing the links. * @throws BuildException if the property file could not be written */
private void writePropertyFile(Properties properties, File dir) throws BuildException { try (BufferedOutputStream bos = new BufferedOutputStream( Files.newOutputStream(new File(dir, linkFileName).toPath()))) { properties.store(bos, "Symlinks from " + dir); } catch (IOException ioe) { throw new BuildException(ioe, getLocation()); } }
Handle errors based on the setting of failonerror.
Params:
  • msg – The message to log, or include in the BuildException.
Throws:
/** * Handle errors based on the setting of failonerror. * * @param msg The message to log, or include in the * <code>BuildException</code>. * @throws BuildException with the message if failonerror=true */
private void handleError(String msg) { if (failonerror) { throw new BuildException(msg); } log(msg); }
Conduct the actual construction of a link.
Params:
  • res – The path of the resource we are linking to.
  • lnk – The name of the link we wish to make.
Throws:
/** * Conduct the actual construction of a link. * * @param res The path of the resource we are linking to. * @param lnk The name of the link we wish to make. * @throws BuildException when things go wrong */
private void doLink(String res, String lnk) throws BuildException { final Path link = Paths.get(lnk); final Path target = Paths.get(res); final boolean alreadyExists = Files.exists(link, LinkOption.NOFOLLOW_LINKS); if (!alreadyExists) { // if the path (at which the link is expected to be created) isn't already present // then we just go ahead and attempt to symlink try { log("creating symlink " + link + " -> " + target, Project.MSG_DEBUG); Files.createSymbolicLink(link, target); } catch (IOException e) { if (failonerror) { throw new BuildException("Failed to create symlink " + lnk + " to target " + res, e); } log("Unable to create symlink " + lnk + " to target " + res, e, Project.MSG_INFO); } return; } // file already exists, see if we are allowed to overwrite if (!overwrite) { log("Skipping symlink creation, since file at " + lnk + " already exists and overwrite is set to false", Project.MSG_INFO); return; } // we have been asked to overwrite, so we now do the necessary steps // initiate a deletion of the existing file final boolean existingFileDeleted = link.toFile().delete(); if (!existingFileDeleted) { handleError("Deletion of file at " + lnk + " failed, while trying to overwrite it with a symlink"); return; } try { log("creating symlink " + link + " -> " + target + " after removing original", Project.MSG_DEBUG); Files.createSymbolicLink(link, target); } catch (IOException e) { if (failonerror) { throw new BuildException("Failed to create symlink " + lnk + " to target " + res, e); } log("Unable to create symlink " + lnk + " to target " + res, e, Project.MSG_INFO); } }
Find all the links in all supplied filesets.

This method is invoked when the action attribute is "record". This means that filesets are interpreted as the directories in which links may be found.

Params:
  • fileSets – The filesets specified by the user.
Returns:A Set of File objects containing the links (with canonical parent directories).
/** * Find all the links in all supplied filesets. * * <p>This method is invoked when the action attribute is * &quot;record&quot;. This means that filesets are interpreted * as the directories in which links may be found.</p> * * @param fileSets The filesets specified by the user. * @return A Set of <code>File</code> objects containing the * links (with canonical parent directories). */
private Set<File> findLinks(List<FileSet> fileSets) { final Set<File> result = new HashSet<>(); for (FileSet fs : fileSets) { DirectoryScanner ds = fs.getDirectoryScanner(getProject()); File dir = fs.getDir(getProject()); Stream.of(ds.getIncludedFiles(), ds.getIncludedDirectories()) .flatMap(Stream::of).forEach(path -> { try { final File f = new File(dir, path); final File pf = f.getParentFile(); final String name = f.getName(); // we use the canonical path of the parent dir in which the (potential) // link resides final File parentDirCanonicalizedFile = new File(pf.getCanonicalPath(), name); if (Files.isSymbolicLink(parentDirCanonicalizedFile.toPath())) { result.add(parentDirCanonicalizedFile); } } catch (IOException e) { handleError("IOException: " + path + " omitted"); } }); } return result; }
Load links from properties files included in one or more FileSets.

This method is only invoked when the action attribute is set to "recreate". The filesets passed in are assumed to specify the names of the property files with the link information and the subdirectories in which to look for them.

Params:
  • fileSets – The FileSets for this task.
Returns: The links to be made.
/** * Load links from properties files included in one or more FileSets. * * <p>This method is only invoked when the action attribute is set to * &quot;recreate&quot;. The filesets passed in are assumed to specify the * names of the property files with the link information and the * subdirectories in which to look for them.</p> * * @param fileSets The <code>FileSet</code>s for this task. * @return The links to be made. */
private Properties loadLinks(List<FileSet> fileSets) { Properties finalList = new Properties(); // loop through the supplied file sets: for (FileSet fs : fileSets) { DirectoryScanner ds = new DirectoryScanner(); fs.setupDirectoryScanner(ds, getProject()); ds.setFollowSymlinks(false); ds.scan(); File dir = fs.getDir(getProject()); // load included files as properties files: for (String name : ds.getIncludedFiles()) { File inc = new File(dir, name); File pf = inc.getParentFile(); Properties lnks = new Properties(); try (InputStream is = new BufferedInputStream( Files.newInputStream(inc.toPath()))) { lnks.load(is); pf = pf.getCanonicalFile(); } catch (FileNotFoundException fnfe) { handleError("Unable to find " + name + "; skipping it."); continue; } catch (IOException ioe) { handleError("Unable to open " + name + " or its parent dir; skipping it."); continue; } try { lnks.store(new PrintStream( new LogOutputStream(this, Project.MSG_INFO)), "listing properties"); } catch (IOException ex) { log("failed to log unshortened properties"); lnks.list(new PrintStream( new LogOutputStream(this, Project.MSG_INFO))); } // Write the contents to our master list of links // This method assumes that all links are defined in // terms of absolute paths, or paths relative to the // working directory: for (String key : lnks.stringPropertyNames()) { finalList.put(new File(pf, key).getAbsolutePath(), lnks.getProperty(key)); } } } return finalList; } private static void deleteSymLink(final Path path) throws IOException { // Implementation note: We intentionally use java.io.File#delete() instead of // java.nio.file.Files#delete(Path) since it turns out that the latter doesn't // update/clear the "canonical file paths cache" maintained by the JRE FileSystemProvider. // Not clearing/updating that cache results in this deleted (and later recreated) symlink // to point to a wrong/outdated target for a few seconds (30 seconds is the time the JRE // maintains the cache entries for). All this is implementation detail of the JRE and // is a JRE bug https://mail.openjdk.java.net/pipermail/core-libs-dev/2017-December/050540.html, // but given that it affects our tests (SymlinkTest#testRecreate consistently fails // on MacOS/Unix) as well as the Symlink task, it makes sense to use this API instead of // the Files#delete(Path) API final boolean deleted = path.toFile().delete(); if (!deleted) { throw new IOException("Could not delete symlink at " + path); } } }