Use symlinks when creating PythonLibrary environment.

Summary: Use symbolic links instead of copying. The links needed to be
relative in order to allow the keep cached data correct across repositories.
diff --git a/src/com/facebook/buck/python/PythonLibrary.java b/src/com/facebook/buck/python/PythonLibrary.java
index b3d08b1..6cc536b 100644
--- a/src/com/facebook/buck/python/PythonLibrary.java
+++ b/src/com/facebook/buck/python/PythonLibrary.java
@@ -30,9 +30,9 @@
 import com.facebook.buck.rules.RuleKey;
 import com.facebook.buck.rules.SrcsAttributeBuilder;
 import com.facebook.buck.step.Step;
-import com.facebook.buck.step.fs.CopyStep;
 import com.facebook.buck.step.fs.MakeCleanDirectoryStep;
 import com.facebook.buck.step.fs.MkdirStep;
+import com.facebook.buck.step.fs.SymlinkFileStep;
 import com.facebook.buck.util.BuckConstant;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
@@ -111,17 +111,19 @@
     commands.add(new MakeCleanDirectoryStep(pythonPathDirectory));
 
     ImmutableSortedSet.Builder<Path> directories = ImmutableSortedSet.naturalOrder();
-    ImmutableList.Builder<Step> copySteps = ImmutableList.builder();
+    ImmutableList.Builder<Step> symlinkSteps = ImmutableList.builder();
 
     for (String src : srcs) {
       Path srcPath = Paths.get(src);
-      Path relativeSrc = Paths.get(buildTarget.getBasePath()).relativize(srcPath);
-      Path target = pythonPathDirectory.resolve(relativeSrc);
+      Path targetPath = pythonPathDirectory.resolve(srcPath);
 
-      directories.add(target.getParent());
-      copySteps.add(new CopyStep(srcPath, target));
+      directories.add(targetPath.getParent());
+      symlinkSteps.add(
+          new SymlinkFileStep(srcPath.toString(),
+              targetPath.toString(),
+              /* useAbsolutePaths */ false));
 
-      Path pathToArtifact = Paths.get(getPathUnderGenDirectory()).resolve(relativeSrc);
+      Path pathToArtifact = Paths.get(getPathUnderGenDirectory()).resolve(srcPath);
       buildableContext.recordArtifact(pathToArtifact);
     }
 
@@ -129,7 +131,7 @@
       commands.add(new MkdirStep(path));
     }
 
-    commands.addAll(copySteps.build());
+    commands.addAll(symlinkSteps.build());
 
     return commands.build();
   }
diff --git a/src/com/facebook/buck/step/fs/MkdirAndSymlinkFileStep.java b/src/com/facebook/buck/step/fs/MkdirAndSymlinkFileStep.java
index 1157f61..2e7e92c 100644
--- a/src/com/facebook/buck/step/fs/MkdirAndSymlinkFileStep.java
+++ b/src/com/facebook/buck/step/fs/MkdirAndSymlinkFileStep.java
@@ -35,7 +35,7 @@
   public MkdirAndSymlinkFileStep(String source, String target) {
     super(ImmutableList.of(
         new MkdirStep(Paths.get(target).getParent().toString()),
-        new SymlinkFileStep(source, target)
+        new SymlinkFileStep(source, target, /* useAbsolutePaths */ true)
     ));
     this.source = Preconditions.checkNotNull(source);
     this.target = Preconditions.checkNotNull(target);
diff --git a/src/com/facebook/buck/step/fs/SymlinkFileStep.java b/src/com/facebook/buck/step/fs/SymlinkFileStep.java
index 4efae07..46b14c5 100644
--- a/src/com/facebook/buck/step/fs/SymlinkFileStep.java
+++ b/src/com/facebook/buck/step/fs/SymlinkFileStep.java
@@ -18,21 +18,45 @@
 
 import com.facebook.buck.step.ExecutionContext;
 import com.facebook.buck.step.Step;
-import com.google.common.base.Function;
+import com.facebook.buck.util.MorePaths;
 import com.google.common.base.Joiner;
 import com.google.common.base.Preconditions;
 
 import java.io.IOException;
 import java.nio.file.Path;
+import java.nio.file.Paths;
 
 public class SymlinkFileStep implements Step {
 
-  private final String source;
-  private final String target;
+  private final String existingFile;
+  private final String desiredLink;
+  private final boolean useAbsolutePaths;
 
-  public SymlinkFileStep(String source, String target) {
-    this.source = Preconditions.checkNotNull(source);
-    this.target = Preconditions.checkNotNull(target);
+  public SymlinkFileStep(String existingFile, String desiredLink, boolean useAbsolutePaths) {
+    this.existingFile = Preconditions.checkNotNull(existingFile);
+    this.desiredLink = Preconditions.checkNotNull(desiredLink);
+    this.useAbsolutePaths = useAbsolutePaths;
+  }
+
+  /**
+   * Get the path to the existing file that should be linked.
+   */
+  private Path getExistingFilePath(ExecutionContext context) {
+    // This could be either an absolute or relative path.
+    // TODO ideally all symbolic links should be relative, consider eliminating the absolute option.
+    return (useAbsolutePaths ? getAbsolutePath(existingFile, context) :
+        MorePaths.getRelativePath(Paths.get(existingFile), Paths.get(desiredLink).getParent()));
+  }
+
+  /**
+   * Get the path to the desired link that should be created.
+   */
+  private Path getDesiredLinkPath(ExecutionContext context) {
+    return getAbsolutePath(desiredLink, context);
+  }
+
+  private static Path getAbsolutePath(String path, ExecutionContext context) {
+    return context.getProjectFilesystem().getPathRelativizer().apply(path);
   }
 
   @Override
@@ -42,23 +66,23 @@
 
   @Override
   public String getDescription (ExecutionContext context) {
-    Function<String, Path> pathRelativizer = context.getProjectFilesystem().getPathRelativizer();
-    // Always symlink to an absolute path so the symlink is sure to be read correctly.
     return Joiner.on(" ").join(
         "ln",
         "-f",
         "-s",
-        pathRelativizer.apply(source),
-        pathRelativizer.apply(target));
+        getExistingFilePath(context),
+        getDesiredLinkPath(context));
   }
 
   @Override
   public int execute(ExecutionContext context) {
-    Function<String, Path> pathRelativizer = context.getProjectFilesystem().getPathRelativizer();
-    Path targetPath = pathRelativizer.apply(target);
-    Path sourcePath = pathRelativizer.apply(source);
+    Path existingFilePath = getExistingFilePath(context);
+    Path desiredLinkPath = getDesiredLinkPath(context);
     try {
-      context.getProjectFilesystem().createSymLink(sourcePath, targetPath, true);
+      context.getProjectFilesystem().createSymLink(
+          existingFilePath,
+          desiredLinkPath,
+          /* force */ true);
       return 0;
     } catch (IOException e) {
       e.printStackTrace(context.getStdErr());
diff --git a/src/com/facebook/buck/util/MorePaths.java b/src/com/facebook/buck/util/MorePaths.java
index fcaca0a..a147fdf 100644
--- a/src/com/facebook/buck/util/MorePaths.java
+++ b/src/com/facebook/buck/util/MorePaths.java
@@ -17,7 +17,6 @@
 package com.facebook.buck.util;
 
 import com.google.common.base.Preconditions;
-import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSortedSet;
 
 import java.io.File;
@@ -26,6 +25,8 @@
 import java.nio.file.Path;
 import java.nio.file.Paths;
 
+import javax.annotation.Nullable;
+
 public class MorePaths {
 
   /** Utility class: do not instantiate. */
@@ -65,6 +66,27 @@
   }
 
   /**
+   * Get the path of a file relative to a base directory.
+   *
+   * @param path must reference a file, not a directory.
+   * @param baseDir must reference a directory that is relative to a common directory with the path.
+   *     may be null if referencing the same directory as the path.
+   * @return the relative path of path from the directory baseDir.
+   */
+  public static Path getRelativePath(Path path, @Nullable Path baseDir) {
+    if (baseDir == null) {
+      // This allows callers to use this method with "file.parent()" for files from the project
+      // root dir.
+      baseDir = Paths.get("");
+    }
+    Preconditions.checkArgument(!path.isAbsolute(),
+        "Path must be relative: %s.", path);
+    Preconditions.checkArgument(!baseDir.isAbsolute(),
+        "Path must be relative: %s.", baseDir);
+    return baseDir.relativize(path);
+  }
+
+  /**
    * Creates a symlink at
    * {@code projectFilesystem.getRootPath().resolve(pathToDesiredLinkUnderProjectRoot)} that
    * points to {@code projectFilesystem.getRootPath().resolve(pathToExistingFileUnderProjectRoot)}
@@ -86,7 +108,7 @@
   /**
    * Creates a symlink at {@code pathToProjectRoot.resolve(pathToDesiredLinkUnderProjectRoot)} that
    * points to {@code pathToProjectRoot.resolve(pathToExistingFileUnderProjectRoot)} using a
-   * relative symlink.
+   * relative symlink. Both params must be relative to the project root.
    *
    * @param pathToDesiredLinkUnderProjectRoot must reference a file, not a directory.
    * @param pathToExistingFileUnderProjectRoot must reference a file, not a directory.
@@ -96,16 +118,9 @@
       Path pathToDesiredLinkUnderProjectRoot,
       Path pathToExistingFileUnderProjectRoot,
       Path pathToProjectRoot) throws IOException {
-    Preconditions.checkArgument(!pathToDesiredLinkUnderProjectRoot.isAbsolute(),
-        "Path must be relative to project root: %s.",
-        pathToDesiredLinkUnderProjectRoot);
-    Preconditions.checkArgument(!pathToExistingFileUnderProjectRoot.isAbsolute(),
-        "Path must be relative to project root: %s.",
-        pathToExistingFileUnderProjectRoot);
-
-    Path parent = pathToDesiredLinkUnderProjectRoot.getParent();
-    String base = Strings.repeat("../", parent == null ? 0 : parent.getNameCount());
-    Path target = Paths.get(base, pathToExistingFileUnderProjectRoot.toString());
+    Path target = getRelativePath(
+        pathToExistingFileUnderProjectRoot,
+        pathToDesiredLinkUnderProjectRoot.getParent());
     Files.createSymbolicLink(pathToProjectRoot.resolve(pathToDesiredLinkUnderProjectRoot), target);
     return target;
   }
diff --git a/test/com/facebook/buck/python/PythonLibraryTest.java b/test/com/facebook/buck/python/PythonLibraryTest.java
index 1bffd84..dd3802c 100644
--- a/test/com/facebook/buck/python/PythonLibraryTest.java
+++ b/test/com/facebook/buck/python/PythonLibraryTest.java
@@ -40,7 +40,9 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSortedSet;
 
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 
 import java.io.IOException;
 import java.io.File;
@@ -52,6 +54,8 @@
  * Unit test for {@link PythonLibrary}.
  */
 public class PythonLibraryTest {
+  @Rule
+  public final TemporaryFolder projectRootDir = new TemporaryFolder();
 
   @Test
   public void testGetters() {
@@ -82,7 +86,7 @@
     PythonLibrary rule = (PythonLibrary)ruleResolver.get(pyLibraryTarget).getBuildable();
     List<Step> steps = rule.getBuildSteps(buildContext, buildableContext);
 
-    final String projectRoot = "/";
+    final String projectRoot = projectRootDir.getRoot().getAbsolutePath();
     final String pylibpath = "__pylib_py_library";
 
     ProjectFilesystem projectFilesystem = new ProjectFilesystem(new File(projectRoot));
@@ -91,32 +95,35 @@
       .build();
 
     MoreAsserts.assertSteps(
-        "python_library() should ensure each file is copied and has its destination directory made",
+        "python_library() should ensure each file is linked and has its destination directory made",
         ImmutableList.of(
             String.format(
-              "mkdir -p %s%s/%s",
+              "mkdir -p %s/%s/%s",
               projectRoot,
               BuckConstant.GEN_DIR,
               pylibpath
               ),
             String.format(
-              "mkdir -p %s%s/%s/foo",
+              "mkdir -p %s/%s/%s/foo",
               projectRoot,
               BuckConstant.GEN_DIR,
               pylibpath
               ),
             String.format(
-              "cp baz.py %s/%s/baz.py",
+              "ln -f -s ../../../baz.py %s/%s/%s/baz.py",
+              projectRoot,
               BuckConstant.GEN_DIR,
               pylibpath
               ),
             String.format(
-              "cp foo/__init__.py %s/%s/foo/__init__.py",
+              "ln -f -s ../../../../foo/__init__.py %s/%s/%s/foo/__init__.py",
+              projectRoot,
               BuckConstant.GEN_DIR,
               pylibpath
               ),
             String.format(
-              "cp foo/bar.py %s/%s/foo/bar.py",
+              "ln -f -s ../../../../foo/bar.py %s/%s/%s/foo/bar.py",
+              projectRoot,
               BuckConstant.GEN_DIR,
               pylibpath
               )
diff --git a/test/com/facebook/buck/step/fs/SymlinkFileStepTest.java b/test/com/facebook/buck/step/fs/SymlinkFileStepTest.java
index ba56760..e567742 100644
--- a/test/com/facebook/buck/step/fs/SymlinkFileStepTest.java
+++ b/test/com/facebook/buck/step/fs/SymlinkFileStepTest.java
@@ -42,7 +42,16 @@
   public final TemporaryFolder tmpDir = new TemporaryFolder();
 
   @Test
-  public void testSymlinkFiles() throws IOException {
+  public void testAbsoluteSymlinkFiles() throws IOException {
+    internalTestSymlinkFiles(/* useAbsolutePaths */ true);
+  }
+
+  @Test
+  public void testRelativeSymlinkFiles() throws IOException {
+    internalTestSymlinkFiles(/* useAbsolutePaths */ false);
+  }
+
+  public void internalTestSymlinkFiles(boolean useAbsolutePaths) throws IOException {
     ExecutionContext context = TestExecutionContext.newBuilder()
         .setProjectFilesystem(new ProjectFilesystem(tmpDir.getRoot()))
         .build();
@@ -53,8 +62,10 @@
     File target = tmpDir.newFile();
     target.delete();
 
-    SymlinkFileStep step = new SymlinkFileStep(source.getName(),
-        target.getName());
+    SymlinkFileStep step = new SymlinkFileStep(
+        /* source */ source.getName(),
+        /* target */ target.getName(),
+        useAbsolutePaths);
     step.execute(context);
     // Run twice to ensure we can overwrite an existing symlink
     step.execute(context);
@@ -95,7 +106,8 @@
     tmpDir.newFile("dummy");
     SymlinkFileStep symlinkStep = new SymlinkFileStep(
         /* source */ "dummy",
-        /* target */ "my_symlink");
+        /* target */ "my_symlink",
+        /* useAbsolutePaths*/ true);
     int exitCode = symlinkStep.execute(executionContext);
     assertEquals(0, exitCode);
     assertTrue(java.nio.file.Files.isSymbolicLink(symlink));
diff --git a/test/com/facebook/buck/util/MorePathsTest.java b/test/com/facebook/buck/util/MorePathsTest.java
index 03513a7..452ecab 100644
--- a/test/com/facebook/buck/util/MorePathsTest.java
+++ b/test/com/facebook/buck/util/MorePathsTest.java
@@ -35,6 +35,29 @@
   public DebuggableTemporaryFolder tmp = new DebuggableTemporaryFolder();
 
   @Test
+  public void testGetRelativePath() {
+    // Path on base directory.
+    assertEquals(
+        Paths.get("file"),
+        MorePaths.getRelativePath(Paths.get("file"), Paths.get("")));
+
+    // Path on base directory (using null).
+    assertEquals(
+        Paths.get("file"),
+        MorePaths.getRelativePath(Paths.get("file"), null));
+
+    // Path internal to base directory.
+    assertEquals(
+        Paths.get("dir/file"),
+        MorePaths.getRelativePath(Paths.get("base/dir/file"), Paths.get("base")));
+
+    // Path external to base directory.
+    assertEquals(
+        Paths.get("../dir1/file"),
+        MorePaths.getRelativePath(Paths.get("dir1/file"), Paths.get("base")));
+  }
+
+  @Test
   public void testCreateRelativeSymlinkToFilesInRoot() throws IOException {
     ProjectFilesystem projectFilesystem = new ProjectFilesystem(tmp.getRoot());
     tmp.newFile("biz.txt");