| /* |
| * 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. |
| * |
| * <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 "foo" to a resource named |
| * "bar.foo" in subdir:</p> |
| * <pre> |
| * <symlink link="${dir.top}/foo" resource="${dir.top}/subdir/bar.foo"/> |
| * </pre> |
| * |
| * <p>Record all links in subdir and its descendants in files named |
| * "dir.links":</p> |
| * <pre> |
| * <symlink action="record" linkfilename="dir.links"> |
| * <fileset dir="${dir.top}" includes="subdir/**" /> |
| * </symlink> |
| * </pre> |
| * |
| * <p>Recreate the links recorded in the previous example:</p> |
| * <pre> |
| * <symlink action="recreate"> |
| * <fileset dir="${dir.top}" includes="subdir/**/dir.links" /> |
| * </symlink> |
| * </pre> |
| * |
| * <p>Delete a link named "foo" to a resource named |
| * "bar.foo" in subdir:</p> |
| * <pre> |
| * <symlink action="delete" link="${dir.top}/foo"/> |
| * </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. |
| */ |
| @Override |
| public void init() throws BuildException { |
| super.init(); |
| setDefaults(); |
| } |
| |
| /** |
| * 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 |
| */ |
| 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 |
| */ |
| 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 |
| */ |
| 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 link : links.stringPropertyNames()) { |
| final String resource = links.getProperty(link); |
| try { |
| if (Files.isSymbolicLink(Paths.get(link)) && |
| new File(link).getCanonicalPath().equals(new File(resource).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 " + link + " as it points to the correct target already", |
| Project.MSG_DEBUG); |
| continue; |
| } |
| } catch (IOException e) { |
| final String errMessage = "Failed to check if path " + link + " is a symbolic link, linking to " + resource; |
| if (failonerror) { |
| throw new BuildException(errMessage, e); |
| } |
| // log and continue |
| log(errMessage, Project.MSG_INFO); |
| continue; |
| } |
| // create the link |
| this.doLink(resource, link); |
| } |
| } finally { |
| setDefaults(); |
| } |
| } |
| |
| /** |
| * 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(link -> byDir |
| .computeIfAbsent(link.getParentFile(), k -> new ArrayList<>()) |
| .add(link)); |
| |
| // 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 link : linksInDir) { |
| try { |
| linksToStore.put(link.getName(), link.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 |
| */ |
| 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. |
| * |
| * @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. |
| * |
| * @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". |
| * |
| * @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". |
| * |
| * @param link The name for the link. |
| */ |
| public void setLink(String link) { |
| this.link = link; |
| } |
| |
| /** |
| * Set the name of the resource to which a link should be created. |
| * Used when action = "single". |
| * |
| * @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". |
| * |
| * @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. |
| * |
| * @param set The fileset to add. |
| */ |
| public void addFileset(FileSet set) { |
| fileSets.add(set); |
| } |
| |
| /** |
| * 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). |
| * |
| * <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 <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. |
| * |
| * @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. |
| * |
| * @param resource The path of the resource we are linking to. |
| * @param link The name of the link we wish to make. |
| * @throws BuildException when things go wrong |
| */ |
| private void doLink(String resource, String link) throws BuildException { |
| final Path linkPath = Paths.get(link); |
| final Path target = Paths.get(resource); |
| final boolean alreadyExists = Files.exists(linkPath, 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 " + linkPath + " -> " + target, Project.MSG_DEBUG); |
| Files.createSymbolicLink(linkPath, target); |
| } catch (IOException e) { |
| if (failonerror) { |
| throw new BuildException("Failed to create symlink " + link + " to target " + resource, e); |
| } |
| log("Unable to create symlink " + link + " to target " + resource, e, Project.MSG_INFO); |
| } |
| return; |
| } |
| // file already exists, see if we are allowed to overwrite |
| if (!overwrite) { |
| log("Skipping symlink creation, since file at " + link + " 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 = linkPath.toFile().delete(); |
| if (!existingFileDeleted) { |
| handleError("Deletion of file at " + link + " failed, while trying to overwrite it with a symlink"); |
| return; |
| } |
| try { |
| log("creating symlink " + linkPath + " -> " + target + " after removing original", |
| Project.MSG_DEBUG); |
| Files.createSymbolicLink(linkPath, target); |
| } catch (IOException e) { |
| if (failonerror) { |
| throw new BuildException("Failed to create symlink " + link + " to target " + resource, e); |
| } |
| log("Unable to create symlink " + link + " to target " + resource, e, Project.MSG_INFO); |
| } |
| } |
| |
| /** |
| * Find all the links in all supplied filesets. |
| * |
| * <p>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.</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. |
| * |
| * <p>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.</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 links = new Properties(); |
| try (InputStream is = new BufferedInputStream( |
| Files.newInputStream(inc.toPath()))) { |
| links.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 { |
| links.store(new PrintStream( |
| new LogOutputStream(this, Project.MSG_INFO)), |
| "listing properties"); |
| } catch (IOException ex) { |
| log("failed to log unshortened properties"); |
| links.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 : links.stringPropertyNames()) { |
| finalList.put(new File(pf, key).getAbsolutePath(), |
| links.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); |
| } |
| } |
| } |