|
|
@@ -36,9 +36,11 @@ 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.Path; |
|
|
|
import java.nio.file.Paths; |
|
|
|
import java.util.ArrayList; |
|
|
|
import java.util.HashMap; |
|
|
|
import java.nio.file.Files; |
|
|
|
import java.util.HashSet; |
|
|
|
import java.util.List; |
|
|
|
import java.util.Map; |
|
|
@@ -51,11 +53,8 @@ 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.Execute; |
|
|
|
import org.apache.tools.ant.taskdefs.LogOutputStream; |
|
|
|
import org.apache.tools.ant.types.FileSet; |
|
|
|
import org.apache.tools.ant.util.FileUtils; |
|
|
|
import org.apache.tools.ant.util.SymbolicLinkUtils; |
|
|
|
|
|
|
|
/** |
|
|
|
* Creates, Deletes, Records and Restores Symlinks. |
|
|
@@ -100,24 +99,10 @@ import org.apache.tools.ant.util.SymbolicLinkUtils; |
|
|
|
* <symlink action="delete" link="${dir.top}/foo"/> |
|
|
|
* </pre> |
|
|
|
* |
|
|
|
* <p><strong>LIMITATIONS:</strong> Because Java has no direct support for |
|
|
|
* handling symlinks this task divines them by comparing canonical and |
|
|
|
* absolute paths. On non-unix systems this may cause false positives. |
|
|
|
* Furthermore, any operating system on which the command |
|
|
|
* <code>ln -s link resource</code> is not a valid command on the command line |
|
|
|
* will not be able to use action="delete", action="single" |
|
|
|
* or action="recreate", but action="record" should still |
|
|
|
* work. Finally, the lack of support for symlinks in Java means that all links |
|
|
|
* are recorded as links to the <strong>canonical</strong> resource name. |
|
|
|
* Therefore the link: <code>link --> subdir/dir/../foo.bar</code> will be |
|
|
|
* recorded as <code>link=subdir/foo.bar</code> and restored as |
|
|
|
* <code>link --> subdir/foo.bar</code>.</p> |
|
|
|
* |
|
|
|
* <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</code>. |
|
|
|
*/ |
|
|
|
public class Symlink extends DispatchTask { |
|
|
|
private static final FileUtils FILE_UTILS = FileUtils.getFileUtils(); |
|
|
|
private static final SymbolicLinkUtils SYMLINK_UTILS = |
|
|
|
SymbolicLinkUtils.getSymbolicLinkUtils(); |
|
|
|
|
|
|
|
private String resource; |
|
|
|
private String link; |
|
|
@@ -187,12 +172,18 @@ public class Symlink extends DispatchTask { |
|
|
|
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); |
|
|
|
SYMLINK_UTILS.deleteSymbolicLink(FILE_UTILS |
|
|
|
.resolveFile(new File("."), link), |
|
|
|
this); |
|
|
|
deleteSymLink(linkPath); |
|
|
|
} catch (IOException ioe) { |
|
|
|
handleError(ioe.toString()); |
|
|
|
handleError(ioe.getMessage()); |
|
|
|
} finally { |
|
|
|
setDefaults(); |
|
|
|
} |
|
|
@@ -207,26 +198,31 @@ public class Symlink extends DispatchTask { |
|
|
|
try { |
|
|
|
if (fileSets.isEmpty()) { |
|
|
|
handleError( |
|
|
|
"File set identifying link file(s) required for action recreate"); |
|
|
|
"File set identifying link file(s) required for action recreate"); |
|
|
|
return; |
|
|
|
} |
|
|
|
Properties links = loadLinks(fileSets); |
|
|
|
|
|
|
|
for (String lnk : links.stringPropertyNames()) { |
|
|
|
String res = links.getProperty(lnk); |
|
|
|
// handle the case where lnk points to a directory (bug 25181) |
|
|
|
final Properties links = loadLinks(fileSets); |
|
|
|
for (final String lnk : links.stringPropertyNames()) { |
|
|
|
final String res = links.getProperty(lnk); |
|
|
|
try { |
|
|
|
File test = new File(lnk); |
|
|
|
if (!SYMLINK_UTILS.isSymbolicLink(lnk)) { |
|
|
|
doLink(res, lnk); |
|
|
|
} else if (!test.getCanonicalPath().equals( |
|
|
|
new File(res).getCanonicalPath())) { |
|
|
|
SYMLINK_UTILS.deleteSymbolicLink(test, this); |
|
|
|
doLink(res, lnk); |
|
|
|
} // else lnk exists, do nothing |
|
|
|
} catch (IOException ioe) { |
|
|
|
handleError("IO exception while creating link"); |
|
|
|
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 |
|
|
|
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(); |
|
|
@@ -361,56 +357,45 @@ public class Symlink extends DispatchTask { |
|
|
|
|
|
|
|
/** |
|
|
|
* Delete a symlink (without deleting the associated resource). |
|
|
|
* <p> |
|
|
|
* <p>This is a convenience method that simply invokes {@link #deleteSymlink(File)} |
|
|
|
* |
|
|
|
* <p>This is a convenience method that simply invokes |
|
|
|
* <code>deleteSymlink(java.io.File)</code>. |
|
|
|
* |
|
|
|
* @param path A string containing the path of the symlink to delete. |
|
|
|
* @param path A string containing the path of the symlink to delete. |
|
|
|
* @throws IOException If the deletion attempt fails |
|
|
|
* |
|
|
|
* @throws IOException If calls to <code>File.rename</code> |
|
|
|
* or <code>File.delete</code> fail. |
|
|
|
* @deprecated use |
|
|
|
* org.apache.tools.ant.util.SymbolicLinkUtils#deleteSymbolicLink |
|
|
|
* instead |
|
|
|
* @deprecated use {@link Files#delete(Path)} instead |
|
|
|
*/ |
|
|
|
@Deprecated |
|
|
|
public static void deleteSymlink(String path) |
|
|
|
throws IOException { |
|
|
|
SYMLINK_UTILS.deleteSymbolicLink(new File(path), null); |
|
|
|
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 unix symlink without removing |
|
|
|
* <p> |
|
|
|
* <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.</p> |
|
|
|
* |
|
|
|
* <p>This method works by |
|
|
|
* getting the canonical path of the link, using the canonical path to |
|
|
|
* rename the resource (breaking the link) and then deleting the link. |
|
|
|
* The resource is then returned to its original name inside a finally |
|
|
|
* block to ensure that the resource is unharmed even in the event of |
|
|
|
* an exception.</p> |
|
|
|
* on a real file, the real file will not be harmed and instead this method |
|
|
|
* returns silently.</p> |
|
|
|
* <p> |
|
|
|
* |
|
|
|
* <p>Since Ant 1.8.0 this method will try to delete the File object if |
|
|
|
* it reports it wouldn't exist (as symlinks pointing nowhere usually do). |
|
|
|
* Prior version would throw a FileNotFoundException in that case.</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. |
|
|
|
* @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 |
|
|
|
* |
|
|
|
* @throws IOException If calls to <code>File.rename</code>, |
|
|
|
* <code>File.delete</code> or |
|
|
|
* <code>File.getCanonicalPath</code> |
|
|
|
* fail. |
|
|
|
* @deprecated use |
|
|
|
* org.apache.tools.ant.util.SymbolicLinkUtils#deleteSymbolicLink |
|
|
|
* instead |
|
|
|
* @deprecated use {@link Files#delete(Path)} instead |
|
|
|
*/ |
|
|
|
@Deprecated |
|
|
|
public static void deleteSymlink(File linkfil) |
|
|
|
throws IOException { |
|
|
|
SYMLINK_UTILS.deleteSymbolicLink(linkfil, null); |
|
|
|
public static void deleteSymlink(final File linkfil) |
|
|
|
throws IOException { |
|
|
|
if (!Files.isSymbolicLink(linkfil.toPath())) { |
|
|
|
return; |
|
|
|
} |
|
|
|
deleteSymLink(linkfil.toPath()); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
@@ -448,36 +433,59 @@ public class Symlink extends DispatchTask { |
|
|
|
/** |
|
|
|
* Conduct the actual construction of a link. |
|
|
|
* |
|
|
|
* <p>The link is constructed by calling <code>Execute.runCommand</code>.</p> |
|
|
|
* |
|
|
|
* @param res The path of the resource we are linking to. |
|
|
|
* @param lnk The name of the link we wish to make. |
|
|
|
* @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 { |
|
|
|
File linkfil = new File(lnk); |
|
|
|
String options = "-s"; |
|
|
|
if (overwrite) { |
|
|
|
options += "f"; |
|
|
|
if (linkfil.exists()) { |
|
|
|
try { |
|
|
|
SYMLINK_UTILS.deleteSymbolicLink(linkfil, this); |
|
|
|
} catch (FileNotFoundException fnfe) { |
|
|
|
log("Symlink disappeared before it was deleted: " + lnk); |
|
|
|
} catch (IOException ioe) { |
|
|
|
log("Unable to overwrite preexisting link or file: " + lnk, |
|
|
|
ioe, Project.MSG_INFO); |
|
|
|
final Path link = Paths.get(lnk); |
|
|
|
final Path target = Paths.get(res); |
|
|
|
final boolean alreadyExists = Files.exists(link); |
|
|
|
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 { |
|
|
|
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 *only* if the path is a symlink, else we fail with error |
|
|
|
if (!Files.isSymbolicLink(link)) { |
|
|
|
final String errMessage = "Cannot overwrite " + lnk + " since the path already exists and isn't a symlink"; |
|
|
|
if (failonerror) { |
|
|
|
throw new BuildException(errMessage); |
|
|
|
} |
|
|
|
// log and return |
|
|
|
log(errMessage, Project.MSG_INFO); |
|
|
|
return; |
|
|
|
} |
|
|
|
// it's a symlink, so we delete the *link* first |
|
|
|
try { |
|
|
|
Execute.runCommand(this, "ln", options, res, lnk); |
|
|
|
} catch (BuildException failedToExecute) { |
|
|
|
deleteSymLink(link); |
|
|
|
} catch (IOException e) { |
|
|
|
// our deletion attempt failed, just log it and try to create the symlink |
|
|
|
// anyway, since we have been asked to overwrite |
|
|
|
log("Failed to delete existing symlink at " + lnk, e, Project.MSG_DEBUG); |
|
|
|
} |
|
|
|
try { |
|
|
|
Files.createSymbolicLink(link, target); |
|
|
|
} catch (IOException e) { |
|
|
|
if (failonerror) { |
|
|
|
throw failedToExecute; |
|
|
|
throw new BuildException("Failed to create symlink " + lnk + " to target " + res, e); |
|
|
|
} |
|
|
|
//log at the info level, and keep going. |
|
|
|
log(failedToExecute.getMessage(), failedToExecute, Project.MSG_INFO); |
|
|
|
log("Unable to create symlink " + lnk + " to target " + res, e, Project.MSG_INFO); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
@@ -488,30 +496,33 @@ public class Symlink extends DispatchTask { |
|
|
|
* "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 HashSet of <code>File</code> objects containing the |
|
|
|
* links (with canonical parent directories). |
|
|
|
* @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) { |
|
|
|
Set<File> result = new HashSet<>(); |
|
|
|
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 { |
|
|
|
File f = new File(dir, path); |
|
|
|
File pf = f.getParentFile(); |
|
|
|
String name = f.getName(); |
|
|
|
if (SYMLINK_UTILS.isSymbolicLink(pf, name)) { |
|
|
|
result.add(new File(pf.getCanonicalFile(), name)); |
|
|
|
.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"); |
|
|
|
} |
|
|
|
} catch (IOException e) { |
|
|
|
handleError("IOException: " + path + " omitted"); |
|
|
|
} |
|
|
|
}); |
|
|
|
}); |
|
|
|
} |
|
|
|
return result; |
|
|
|
} |
|
|
@@ -568,4 +579,20 @@ public class Symlink extends DispatchTask { |
|
|
|
} |
|
|
|
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 |
|
|
|
// probably a bug, 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); |
|
|
|
} |
|
|
|
} |
|
|
|
} |