Implements repository_ctx.file

repository_ctx.file enable writing random file in the remote repository tree.

Issue #893: Step 4 of http://goo.gl/OZV3o0. See http://goo.gl/fD4ZsY.

--
MOS_MIGRATED_REVID=115338910
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/skylark/SkylarkRepositoryContext.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/skylark/SkylarkRepositoryContext.java
index 04a868b..c48a964 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/skylark/SkylarkRepositoryContext.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/skylark/SkylarkRepositoryContext.java
@@ -34,6 +34,8 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
 import java.util.List;
 import java.util.Map;
 
@@ -124,6 +126,7 @@
   )
   public void symlink(SkylarkPath from, SkylarkPath to) throws RepositoryFunctionException {
     try {
+      checkInOutputDirectory(to);
       to.path.createSymbolicLink(from.path);
     } catch (IOException e) {
       throw new RepositoryFunctionException(
@@ -133,6 +136,46 @@
     }
   }
 
+  private void checkInOutputDirectory(SkylarkPath path) throws RepositoryFunctionException {
+    if (!path.path.getPathString().startsWith(outputDirectory.getPathString())) {
+      throw new RepositoryFunctionException(
+          new IOException("Cannot write outside of the output directory for path " + path),
+          Transience.TRANSIENT);
+    }
+  }
+
+  @SkylarkCallable(name = "file", documented = false)
+  public void createFile(SkylarkPath path) throws RepositoryFunctionException {
+    createFile(path, "");
+  }
+
+  @SkylarkCallable(
+    name = "file",
+    doc = "Generate a file in the output directory with the provided content"
+  )
+  public void createFile(SkylarkPath path, String content) throws RepositoryFunctionException {
+    try {
+      checkInOutputDirectory(path);
+      makeDirectories(path.path);
+      try (OutputStream stream = path.path.getOutputStream()) {
+        stream.write(content.getBytes(StandardCharsets.UTF_8));
+      }
+    } catch (IOException e) {
+      throw new RepositoryFunctionException(e, Transience.TRANSIENT);
+    }
+  }
+
+  // Create parent directories for the given path
+  private void makeDirectories(Path path) throws IOException {
+    if (!path.isRootDirectory()) {
+      Path parent = path.getParentDirectory();
+      if (!parent.exists()) {
+        makeDirectories(path.getParentDirectory());
+        parent.createDirectory();
+      }
+    }
+  }
+
   @SkylarkCallable(
     name = "os",
     structField = true,
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/repository/skylark/SkylarkRepositoryContextTest.java b/src/test/java/com/google/devtools/build/lib/bazel/repository/skylark/SkylarkRepositoryContextTest.java
index e2db60a..22b185e 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/repository/skylark/SkylarkRepositoryContextTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/repository/skylark/SkylarkRepositoryContextTest.java
@@ -15,15 +15,18 @@
 package com.google.devtools.build.lib.bazel.repository.skylark;
 
 import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.io.CharStreams;
 import com.google.devtools.build.lib.events.Location;
 import com.google.devtools.build.lib.packages.Attribute;
 import com.google.devtools.build.lib.packages.Package;
 import com.google.devtools.build.lib.packages.Rule;
 import com.google.devtools.build.lib.packages.RuleClass;
 import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
+import com.google.devtools.build.lib.rules.repository.RepositoryFunction.RepositoryFunctionException;
 import com.google.devtools.build.lib.syntax.Argument.Passed;
 import com.google.devtools.build.lib.syntax.BuiltinFunction;
 import com.google.devtools.build.lib.syntax.FuncallExpression;
@@ -38,6 +41,9 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
 import java.util.Map;
 
 /**
@@ -115,4 +121,46 @@
     assertThat(context.which("true").toString()).isEqualTo("/bin/true");
     assertThat(context.which("false").toString()).isEqualTo("/path/sbin/false");
   }
+
+  @Test
+  public void testFile() throws Exception {
+    setUpContexForRule("test");
+    context.createFile(context.path("foobar"));
+    context.createFile(context.path("foo/bar"), "foobar");
+    context.createFile(context.path("bar/foo/bar"));
+
+    testOutputFile(outputDirectory.getChild("foobar"), "");
+    testOutputFile(outputDirectory.getRelative("foo/bar"), "foobar");
+    testOutputFile(outputDirectory.getRelative("bar/foo/bar"), "");
+
+    try {
+      context.createFile(context.path("/absolute"));
+      fail("Expected error on creating path outside of the output directory");
+    } catch (RepositoryFunctionException ex) {
+      assertThat(ex.getCause().getMessage())
+          .isEqualTo("Cannot write outside of the output directory for path /absolute");
+    }
+    try {
+      context.createFile(context.path("../somepath"));
+      fail("Expected error on creating path outside of the output directory");
+    } catch (RepositoryFunctionException ex) {
+      assertThat(ex.getCause().getMessage())
+          .isEqualTo("Cannot write outside of the output directory for path /somepath");
+    }
+    try {
+      context.createFile(context.path("foo/../../somepath"));
+      fail("Expected error on creating path outside of the output directory");
+    } catch (RepositoryFunctionException ex) {
+      assertThat(ex.getCause().getMessage())
+          .isEqualTo("Cannot write outside of the output directory for path /somepath");
+    }
+  }
+
+  private void testOutputFile(Path path, String content) throws IOException {
+    assertThat(path.exists()).isTrue();
+    assertThat(
+            CharStreams.toString(
+                new InputStreamReader(path.getInputStream(), StandardCharsets.UTF_8)))
+        .isEqualTo(content);
+  }
 }