Create file/api.symlink.

Add a new method for creating symlinks inside recipes.

BUG=chromium:783517

Change-Id: I43bf6263fe2a6f1a270216d610032803e138af01
Reviewed-on: https://chromium-review.googlesource.com/776077
Commit-Queue: Don Garrett <dgarrett@chromium.org>
Reviewed-by: Robbie Iannucci <iannucci@chromium.org>
Reviewed-by: Nodir Turakulov <nodir@chromium.org>
diff --git a/README.recipes.md b/README.recipes.md
index d292e448..d1dfee4 100644
--- a/README.recipes.md
+++ b/README.recipes.md
@@ -58,6 +58,7 @@
   * [file:examples/glob](#recipes-file_examples_glob)
   * [file:examples/listdir](#recipes-file_examples_listdir)
   * [file:examples/raw_copy](#recipes-file_examples_raw_copy)
+  * [file:examples/symlink](#recipes-file_examples_symlink)
   * [generator_script:examples/full](#recipes-generator_script_examples_full)
   * [json:examples/full](#recipes-json_examples_full)
   * [json:tests/add_json_log](#recipes-json_tests_add_json_log)
@@ -476,6 +477,19 @@
 
 Raises file.Error.
 
+&mdash; **def [symlink](/recipe_modules/file/api.py#338)(self, name, source, link):**
+
+Creates a symlink from link to source on the local filesystem.
+
+Behaves identically to os.symlink.
+
+Args:
+  * name (str) - The name of the step.
+  * source (Path|Placeholder) - The path to link too.
+  * link (Path|Placeholder) - The link to create.
+
+Raises file.Error
+
 &mdash; **def [write\_raw](/recipe_modules/file/api.py#131)(self, name, dest, data):**
 
 Write the given `data` to `dest`.
@@ -1466,6 +1480,11 @@
 [DEPS](/recipe_modules/file/examples/raw_copy.py#5): [file](#recipe_modules-file), [json](#recipe_modules-json), [path](#recipe_modules-path)
 
 &mdash; **def [RunSteps](/recipe_modules/file/examples/raw_copy.py#12)(api):**
+### *recipes* / [file:examples/symlink](/recipe_modules/file/examples/symlink.py)
+
+[DEPS](/recipe_modules/file/examples/symlink.py#5): [file](#recipe_modules-file), [json](#recipe_modules-json), [path](#recipe_modules-path)
+
+&mdash; **def [RunSteps](/recipe_modules/file/examples/symlink.py#12)(api):**
 ### *recipes* / [generator\_script:examples/full](/recipe_modules/generator_script/examples/full.py)
 
 [DEPS](/recipe_modules/generator_script/examples/full.py#7): [generator\_script](#recipe_modules-generator_script), [json](#recipe_modules-json), [path](#recipe_modules-path), [properties](#recipe_modules-properties), [step](#recipe_modules-step)
diff --git a/recipe_modules/file/api.py b/recipe_modules/file/api.py
index f61796a..b2e11a5 100644
--- a/recipe_modules/file/api.py
+++ b/recipe_modules/file/api.py
@@ -334,3 +334,20 @@
       assert p.startswith(src), (src, p)
       return fnmatch.fnmatch(p[len(src)+1:].split(os.path.sep)[0], pattern)
     self.m.path.mock_remove_paths(str(source), filt)
+
+  def symlink(self, name, source, link):
+    """Creates a symlink from link to source on the local filesystem.
+
+    Behaves identically to os.symlink.
+
+    Args:
+      * name (str) - The name of the step.
+      * source (Path|Placeholder) - The path to link to.
+      * link (Path|Placeholder) - The link to create.
+
+    Raises file.Error
+    """
+    self._assert_absolute_path_or_placeholder(source)
+    self._assert_absolute_path_or_placeholder(link)
+    self._run(name, ['symlink', source, link])
+    self.m.path.mock_copy_paths(source, link)
diff --git a/recipe_modules/file/examples/symlink.expected/basic.json b/recipe_modules/file/examples/symlink.expected/basic.json
new file mode 100644
index 0000000..b714074
--- /dev/null
+++ b/recipe_modules/file/examples/symlink.expected/basic.json
@@ -0,0 +1,49 @@
+[
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "Here is some text data",
+      "[START_DIR]/some file"
+    ],
+    "infra_step": true,
+    "name": "write a file"
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "symlink",
+      "[START_DIR]/some file",
+      "[START_DIR]/new path"
+    ],
+    "infra_step": true,
+    "name": "symlink it"
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[START_DIR]/new path",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "read it"
+  },
+  {
+    "name": "$result",
+    "recipe_result": null,
+    "status_code": 0
+  }
+]
\ No newline at end of file
diff --git a/recipe_modules/file/examples/symlink.py b/recipe_modules/file/examples/symlink.py
new file mode 100644
index 0000000..e311b9c
--- /dev/null
+++ b/recipe_modules/file/examples/symlink.py
@@ -0,0 +1,25 @@
+# Copyright 2017 The LUCI Authors. All rights reserved.
+# Use of this source code is governed under the Apache License, Version 2.0
+# that can be found in the LICENSE file.
+
+DEPS = [
+  'file',
+  'path',
+  'json',
+]
+
+
+def RunSteps(api):
+  dest = api.path['start_dir'].join('some file')
+  data = 'Here is some text data'
+
+  api.file.write_text('write a file', dest, data)
+  api.file.symlink('symlink it', dest, api.path['start_dir'].join('new path'))
+  read_data = api.file.read_text(
+    'read it', api.path['start_dir'].join('new path'), test_data=data)
+
+  assert read_data == data, (read_data, data)
+
+
+def GenTests(api):
+  yield api.test('basic')
diff --git a/recipe_modules/file/resources/fileutil.py b/recipe_modules/file/resources/fileutil.py
index 461ad96..d35cb52 100755
--- a/recipe_modules/file/resources/fileutil.py
+++ b/recipe_modules/file/resources/fileutil.py
@@ -258,6 +258,14 @@
       func=lambda opts: print('\n'.join(str(os.stat(f).st_size)
                                             for f in opts.file)))
 
+  # Subcommand: filesizes
+  subparser = subparsers.add_parser('symlink',
+      help='Creates a symlink. Behaves like os.symlink.')
+  subparser.add_argument('source', help='The thing to link to.')
+  subparser.add_argument('link', help='The link to create.')
+  subparser.set_defaults(
+      func=lambda opts: os.symlink(opts.source, opts.link))
+
   # Parse arguments.
   opts = parser.parse_args(args)