Add an output_dir override to GN.

This adds support for the ability for targets to override their output
directory. The tool defines a default output directory in terms of the normal
patterns. The target can optionally supply an override, and the tool uses the
new {{output_dir}} pattern when specifying tool output to express the result of
the default-or-override.

Review URL: https://codereview.chromium.org/1887533003

Cr-Original-Commit-Position: refs/heads/master@{#387366}
Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src
Cr-Mirrored-Commit: f2eba7f76b6effb82ddce70d2bb227f4e25b1f69
diff --git a/tools/gn/binary_target_generator.cc b/tools/gn/binary_target_generator.cc
index 7c81232..7828013 100644
--- a/tools/gn/binary_target_generator.cc
+++ b/tools/gn/binary_target_generator.cc
@@ -7,8 +7,10 @@
 #include "tools/gn/config_values_generator.h"
 #include "tools/gn/deps_iterator.h"
 #include "tools/gn/err.h"
+#include "tools/gn/filesystem_utils.h"
 #include "tools/gn/functions.h"
 #include "tools/gn/scope.h"
+#include "tools/gn/settings.h"
 #include "tools/gn/value_extractors.h"
 #include "tools/gn/variables.h"
 
@@ -34,6 +36,9 @@
   if (!FillOutputPrefixOverride())
     return;
 
+  if (!FillOutputDir())
+    return;
+
   if (!FillOutputExtension())
     return;
 
@@ -98,6 +103,29 @@
   return true;
 }
 
+bool BinaryTargetGenerator::FillOutputDir() {
+  const Value* value = scope_->GetValue(variables::kOutputDir, true);
+  if (!value)
+    return true;
+  if (!value->VerifyTypeIs(Value::STRING, err_))
+    return false;
+
+  if (value->string_value().empty())
+    return true;  // Treat empty string as the default and do nothing.
+
+  const BuildSettings* build_settings = scope_->settings()->build_settings();
+  SourceDir dir = scope_->GetSourceDir().ResolveRelativeDir(
+      *value, err_, build_settings->root_path_utf8());
+  if (err_->has_error())
+    return false;
+
+  if (!EnsureStringIsInOutputDir(build_settings->build_dir(),
+                                 dir.value(), value->origin(), err_))
+    return false;
+  target_->set_output_dir(dir);
+  return true;
+}
+
 bool BinaryTargetGenerator::FillOutputExtension() {
   const Value* value = scope_->GetValue(variables::kOutputExtension, true);
   if (!value)
diff --git a/tools/gn/binary_target_generator.h b/tools/gn/binary_target_generator.h
index 40ed432..0788a20 100644
--- a/tools/gn/binary_target_generator.h
+++ b/tools/gn/binary_target_generator.h
@@ -27,6 +27,7 @@
   bool FillCompleteStaticLib();
   bool FillOutputName();
   bool FillOutputPrefixOverride();
+  bool FillOutputDir();
   bool FillOutputExtension();
   bool FillAllowCircularIncludesFrom();
 
diff --git a/tools/gn/docs/reference.md b/tools/gn/docs/reference.md
index 22cbee0..38e8ce7 100644
--- a/tools/gn/docs/reference.md
+++ b/tools/gn/docs/reference.md
@@ -2730,6 +2730,20 @@
 
         The command to run.
 
+    default_output_dir  [string with substitutions]
+        Valid for: linker tools
+
+        Default directory name for the output file relative to the
+        root_build_dir. It can contain other substitution patterns.
+        This will be the default value for the {{output_dir}} expansion
+        (discussed below) but will be overridden by the "output_dir"
+        variable in a target, if one is specified.
+
+        GN doesn't do anything with this string other than pass it
+        along, potentially with target-specific overrides. It is the
+        tool's job to use the expansion so that the files will be in
+        the right place.
+
     default_output_extension  [string]
         Valid for: linker tools
 
@@ -2746,7 +2760,7 @@
 
         Example: default_output_extension = ".exe"
 
-    depfile  [string]
+    depfile  [string with substitutions]
         Valid for: compiler tools (optional)
 
         If the tool can write ".d" files, this specifies the name of
@@ -2808,13 +2822,11 @@
           ]
 
         Example for a linker tool that produces a .dll and a .lib. The
-        use of {{output_extension}} rather than hardcoding ".dll"
-        allows the extension of the library to be overridden on a
-        target-by-target basis, but in this example, it always
-        produces a ".lib" import library:
+        use of {{target_output_name}}, {{output_extension}} and
+        {{output_dir}} allows the target to override these values.
           outputs = [
-            "{{root_out_dir}}/{{target_output_name}}{{output_extension}}",
-            "{{root_out_dir}}/{{target_output_name}}.lib",
+            "{{output_dir}}/{{target_output_name}}{{output_extension}}",
+            "{{output_dir}}/{{target_output_name}}.lib",
           ]
 
     link_output  [string with substitutions]
@@ -2827,7 +2839,7 @@
         should match entries in the "outputs". If unspecified, the
         first item in the "outputs" array will be used for all. See
         "Separate linking and dependencies for shared libraries"
-        below for more.  If link_output is set but runtime_link_output
+        below for more. If link_output is set but runtime_link_output
         is not set, runtime_link_output defaults to link_output.
 
         On Windows, where the tools produce a .dll shared library and
@@ -2937,7 +2949,7 @@
     {{target_out_dir}}
         The directory of the generated file and output directories,
         respectively, for the current target. There is no trailing
-        slash.
+        slash. See also {{output_dir}} for linker tools.
         Example: "out/base/test"
 
     {{target_output_name}}
@@ -3020,6 +3032,21 @@
 
         Example: "-lfoo -lbar"
 
+    {{output_dir}}
+        The value of the "output_dir" variable in the target, or the
+        the value of the "default_output_dir" value in the tool if the
+        target does not override the output directory. This will be
+        relative to the root_build_dir and will not end in a slash.
+        Will be "." for output to the root_build_dir.
+
+        This is subtly different than {{target_out_dir}} which is
+        defined by GN based on the target's path and not overridable.
+        {{output_dir}} is for the final output, {{target_out_dir}} is
+        generally for object files and other outputs.
+
+        Usually {{output_dir}} would be defined in terms of either
+        {{target_out_dir}} or {{root_out_dir}}
+
     {{output_extension}}
         The value of the "output_extension" variable in the target,
         or the value of the "default_output_extension" value in the
@@ -3075,13 +3102,13 @@
     tool("solink") {
       command = "..."
       outputs = [
-        "{{root_out_dir}}/{{target_output_name}}{{output_extension}}",
-        "{{root_out_dir}}/{{target_output_name}}{{output_extension}}.TOC",
+        "{{output_dir}}/{{target_output_name}}{{output_extension}}",
+        "{{output_dir}}/{{target_output_name}}{{output_extension}}.TOC",
       ]
       link_output =
-        "{{root_out_dir}}/{{target_output_name}}{{output_extension}}"
+        "{{output_dir}}/{{target_output_name}}{{output_extension}}"
       depend_output =
-        "{{root_out_dir}}/{{target_output_name}}{{output_extension}}.TOC"
+        "{{output_dir}}/{{target_output_name}}{{output_extension}}.TOC"
       restat = true
     }
 
@@ -4660,6 +4687,36 @@
 
 
 ```
+## **output_dir**: [directory] Directory to put output file in.
+
+```
+  For library and executable targets, overrides the directory for the
+  final output. This must be in the root_build_dir or a child thereof.
+
+  This should generally be in the root_out_dir or a subdirectory thereof
+  (the root_out_dir will be the same as the root_build_dir for the
+  default toolchain, and will be a subdirectory for other toolchains).
+  Not putting the output in a subdirectory of root_out_dir can result
+  in collisions between different toolchains, so you will need to take
+  steps to ensure that your target is only present in one toolchain.
+
+  Normally the toolchain specifies the output directory for libraries
+  and executables (see "gn help tool"). You will have to consult that
+  for the default location. The default location will be used if
+  output_dir is undefined or empty.
+
+```
+
+### **Example**
+
+```
+  shared_library("doom_melon") {
+    output_dir = "$root_out_dir/plugin_libs"
+    ...
+  }
+
+
+```
 ## **output_extension**: Value to use for the output's file extension.
 
 ```
@@ -5724,6 +5781,10 @@
 ### **Placeholders**
 
 ```
+  This section discusses only placeholders for actions. There are other
+  placeholders used in the definition of tools. See "gn help tool" for
+  those.
+
   {{source}}
       The name of the source file including directory (*). This will
       generally be used for specifying inputs to a script in the
diff --git a/tools/gn/example/build/toolchain/BUILD.gn b/tools/gn/example/build/toolchain/BUILD.gn
index 77e33fe..d9457d7 100644
--- a/tools/gn/example/build/toolchain/BUILD.gn
+++ b/tools/gn/example/build/toolchain/BUILD.gn
@@ -37,9 +37,10 @@
 
   tool("solink") {
     soname = "{{target_output_name}}{{output_extension}}"  # e.g. "libfoo.so".
+    sofile = "{{output_dir}}/$soname"
     rspfile = soname + ".rsp"
 
-    command = "g++ -shared {{ldflags}} -o $soname -Wl,-soname=$soname @$rspfile"
+    command = "g++ -shared {{ldflags}} -o $sofile -Wl,-soname=$soname @$rspfile"
     rspfile_content = "-Wl,--whole-archive {{inputs}} {{solibs}} -Wl,--no-whole-archive {{libs}}"
 
     description = "SOLINK $soname"
@@ -49,11 +50,15 @@
     # specifies).
     default_output_extension = ".so"
 
+    # Use this for {{output_dir}} expansions unless a target manually overrides
+    # it (in which case {{output_dir}} will be what the target specifies).
+    default_output_dir = "{{root_out_dir}}"
+
     outputs = [
-      soname,
+      sofile,
     ]
-    link_output = soname
-    depend_output = soname
+    link_output = sofile
+    depend_output = sofile
     output_prefix = "lib"
   }
 
@@ -62,6 +67,7 @@
     rspfile = "$outfile.rsp"
     command = "g++ {{ldflags}} -o $outfile -Wl,--start-group @$rspfile {{solibs}} -Wl,--end-group {{libs}}"
     description = "LINK $outfile"
+    default_output_dir = "{{root_out_dir}}"
     rspfile_content = "{{inputs}}"
     outputs = [
       outfile,
diff --git a/tools/gn/function_toolchain.cc b/tools/gn/function_toolchain.cc
index f3344ff..bdf5385 100644
--- a/tools/gn/function_toolchain.cc
+++ b/tools/gn/function_toolchain.cc
@@ -418,6 +418,20 @@
     "\n"
     "        The command to run.\n"
     "\n"
+    "    default_output_dir  [string with substitutions]\n"
+    "        Valid for: linker tools\n"
+    "\n"
+    "        Default directory name for the output file relative to the\n"
+    "        root_build_dir. It can contain other substitution patterns.\n"
+    "        This will be the default value for the {{output_dir}} expansion\n"
+    "        (discussed below) but will be overridden by the \"output_dir\"\n"
+    "        variable in a target, if one is specified.\n"
+    "\n"
+    "        GN doesn't do anything with this string other than pass it\n"
+    "        along, potentially with target-specific overrides. It is the\n"
+    "        tool's job to use the expansion so that the files will be in\n"
+    "        the right place.\n"
+    "\n"
     "    default_output_extension  [string]\n"
     "        Valid for: linker tools\n"
     "\n"
@@ -434,7 +448,7 @@
     "\n"
     "        Example: default_output_extension = \".exe\"\n"
     "\n"
-    "    depfile  [string]\n"
+    "    depfile  [string with substitutions]\n"
     "        Valid for: compiler tools (optional)\n"
     "\n"
     "        If the tool can write \".d\" files, this specifies the name of\n"
@@ -496,14 +510,12 @@
     "          ]\n"
     "\n"
     "        Example for a linker tool that produces a .dll and a .lib. The\n"
-    "        use of {{output_extension}} rather than hardcoding \".dll\"\n"
-    "        allows the extension of the library to be overridden on a\n"
-    "        target-by-target basis, but in this example, it always\n"
-    "        produces a \".lib\" import library:\n"
+    "        use of {{target_output_name}}, {{output_extension}} and\n"
+    "        {{output_dir}} allows the target to override these values.\n"
     "          outputs = [\n"
-    "            \"{{root_out_dir}}/{{target_output_name}}"
+    "            \"{{output_dir}}/{{target_output_name}}"
                      "{{output_extension}}\",\n"
-    "            \"{{root_out_dir}}/{{target_output_name}}.lib\",\n"
+    "            \"{{output_dir}}/{{target_output_name}}.lib\",\n"
     "          ]\n"
     "\n"
     "    link_output  [string with substitutions]\n"
@@ -516,7 +528,7 @@
     "        should match entries in the \"outputs\". If unspecified, the\n"
     "        first item in the \"outputs\" array will be used for all. See\n"
     "        \"Separate linking and dependencies for shared libraries\"\n"
-    "        below for more.  If link_output is set but runtime_link_output\n"
+    "        below for more. If link_output is set but runtime_link_output\n"
     "        is not set, runtime_link_output defaults to link_output.\n"
     "\n"
     "        On Windows, where the tools produce a .dll shared library and\n"
@@ -624,7 +636,7 @@
     "    {{target_out_dir}}\n"
     "        The directory of the generated file and output directories,\n"
     "        respectively, for the current target. There is no trailing\n"
-    "        slash.\n"
+    "        slash. See also {{output_dir}} for linker tools.\n"
     "        Example: \"out/base/test\"\n"
     "\n"
     "    {{target_output_name}}\n"
@@ -707,6 +719,21 @@
     "\n"
     "        Example: \"-lfoo -lbar\"\n"
     "\n"
+    "    {{output_dir}}\n"
+    "        The value of the \"output_dir\" variable in the target, or the\n"
+    "        the value of the \"default_output_dir\" value in the tool if the\n"
+    "        target does not override the output directory. This will be\n"
+    "        relative to the root_build_dir and will not end in a slash.\n"
+    "        Will be \".\" for output to the root_build_dir.\n"
+    "\n"
+    "        This is subtly different than {{target_out_dir}} which is\n"
+    "        defined by GN based on the target's path and not overridable.\n"
+    "        {{output_dir}} is for the final output, {{target_out_dir}} is\n"
+    "        generally for object files and other outputs.\n"
+    "\n"
+    "        Usually {{output_dir}} would be defined in terms of either\n"
+    "        {{target_out_dir}} or {{root_out_dir}}\n"
+    "\n"
     "    {{output_extension}}\n"
     "        The value of the \"output_extension\" variable in the target,\n"
     "        or the value of the \"default_output_extension\" value in the\n"
@@ -759,14 +786,14 @@
     "    tool(\"solink\") {\n"
     "      command = \"...\"\n"
     "      outputs = [\n"
-    "        \"{{root_out_dir}}/{{target_output_name}}{{output_extension}}\",\n"
-    "        \"{{root_out_dir}}/{{target_output_name}}"
+    "        \"{{output_dir}}/{{target_output_name}}{{output_extension}}\",\n"
+    "        \"{{output_dir}}/{{target_output_name}}"
                  "{{output_extension}}.TOC\",\n"
     "      ]\n"
     "      link_output =\n"
-    "        \"{{root_out_dir}}/{{target_output_name}}{{output_extension}}\"\n"
+    "        \"{{output_dir}}/{{target_output_name}}{{output_extension}}\"\n"
     "      depend_output =\n"
-    "        \"{{root_out_dir}}/{{target_output_name}}"
+    "        \"{{output_dir}}/{{target_output_name}}"
                  "{{output_extension}}.TOC\"\n"
     "      restat = true\n"
     "    }\n"
@@ -866,6 +893,8 @@
                    tool.get(), &Tool::set_runtime_link_output, err) ||
       !ReadString(&block_scope, "output_prefix", tool.get(),
                   &Tool::set_output_prefix, err) ||
+      !ReadPattern(&block_scope, "default_output_dir", subst_validator,
+                   tool.get(), &Tool::set_default_output_dir, err) ||
       !ReadPrecompiledHeaderType(&block_scope, tool.get(), err) ||
       !ReadBool(&block_scope, "restat", tool.get(), &Tool::set_restat, err) ||
       !ReadPattern(&block_scope, "rspfile", subst_validator, tool.get(),
diff --git a/tools/gn/ninja_binary_target_writer.cc b/tools/gn/ninja_binary_target_writer.cc
index 7077b17..a62f872 100644
--- a/tools/gn/ninja_binary_target_writer.cc
+++ b/tools/gn/ninja_binary_target_writer.cc
@@ -780,7 +780,7 @@
     WriteLinkerFlags(optional_def_file);
     WriteLibs();
   }
-  WriteOutputExtension();
+  WriteOutputSubstitutions();
   WriteSolibs(solibs);
 }
 
@@ -847,11 +847,15 @@
   out_ << std::endl;
 }
 
-void NinjaBinaryTargetWriter::WriteOutputExtension() {
+void NinjaBinaryTargetWriter::WriteOutputSubstitutions() {
   out_ << "  output_extension = "
        << SubstitutionWriter::GetLinkerSubstitution(
               target_, tool_, SUBSTITUTION_OUTPUT_EXTENSION);
   out_ << std::endl;
+  out_ << "  output_dir = "
+       << SubstitutionWriter::GetLinkerSubstitution(
+              target_, tool_, SUBSTITUTION_OUTPUT_DIR);
+  out_ << std::endl;
 }
 
 void NinjaBinaryTargetWriter::WriteSolibs(
diff --git a/tools/gn/ninja_binary_target_writer.h b/tools/gn/ninja_binary_target_writer.h
index db2b15d..1876486 100644
--- a/tools/gn/ninja_binary_target_writer.h
+++ b/tools/gn/ninja_binary_target_writer.h
@@ -99,7 +99,7 @@
                         const std::vector<SourceFile>& other_files);
   void WriteLinkerFlags(const SourceFile* optional_def_file);
   void WriteLibs();
-  void WriteOutputExtension();
+  void WriteOutputSubstitutions();
   void WriteSolibs(const std::vector<OutputFile>& solibs);
 
   // Writes the stamp line for a source set. These are not linked.
diff --git a/tools/gn/ninja_binary_target_writer_unittest.cc b/tools/gn/ninja_binary_target_writer_unittest.cc
index 1e02446..d620f94 100644
--- a/tools/gn/ninja_binary_target_writer_unittest.cc
+++ b/tools/gn/ninja_binary_target_writer_unittest.cc
@@ -82,7 +82,8 @@
             "|| obj/foo/bar.stamp\n"
         "  ldflags =\n"
         "  libs =\n"
-        "  output_extension = .so\n";
+        "  output_extension = .so\n"
+        "  output_dir = \n";
     std::string out_str = out.str();
     EXPECT_EQ(expected, out_str);
   }
@@ -110,7 +111,8 @@
         // There are no sources so there are no params to alink. (In practice
         // this will probably fail in the archive tool.)
         "build obj/foo/libstlib.a: alink || obj/foo/bar.stamp\n"
-        "  output_extension = \n";
+        "  output_extension = \n"
+        "  output_dir = \n";
     std::string out_str = out.str();
     EXPECT_EQ(expected, out_str);
   }
@@ -136,15 +138,16 @@
         "build obj/foo/libstlib.a: alink obj/foo/bar.input1.o "
             "obj/foo/bar.input2.o ../../foo/input3.o ../../foo/input4.obj "
             "|| obj/foo/bar.stamp\n"
-        "  output_extension = \n";
+        "  output_extension = \n"
+        "  output_dir = \n";
     std::string out_str = out.str();
     EXPECT_EQ(expected, out_str);
   }
 }
 
-// This tests that output extension overrides apply, and input dependencies
-// are applied.
-TEST(NinjaBinaryTargetWriter, ProductExtensionAndInputDeps) {
+// This tests that output extension and output dir overrides apply, and input
+// dependencies are applied.
+TEST(NinjaBinaryTargetWriter, OutputExtensionAndInputDeps) {
   TestWithScope setup;
   Err err;
 
@@ -157,10 +160,11 @@
   action.SetToolchain(setup.toolchain());
   ASSERT_TRUE(action.OnResolved(&err));
 
-  // A shared library w/ the product_extension set to a custom value.
+  // A shared library w/ the output_extension set to a custom value.
   Target target(setup.settings(), Label(SourceDir("//foo/"), "shlib"));
   target.set_output_type(Target::SHARED_LIBRARY);
   target.set_output_extension(std::string("so.6"));
+  target.set_output_dir(SourceDir("//out/Debug/foo/"));
   target.sources().push_back(SourceFile("//foo/input1.cc"));
   target.sources().push_back(SourceFile("//foo/input2.cc"));
   target.public_deps().push_back(LabelTargetPair(&action));
@@ -192,7 +196,8 @@
           "obj/foo/libshlib.input2.o || obj/foo/action.stamp\n"
       "  ldflags =\n"
       "  libs =\n"
-      "  output_extension = .so.6\n";
+      "  output_extension = .so.6\n"
+      "  output_dir = foo\n";
 
   std::string out_str = out.str();
   EXPECT_EQ(expected, out_str);
@@ -229,19 +234,20 @@
       "build ./libshlib.so: solink | ../../foo/lib1.a\n"
       "  ldflags = -L../../foo/bar\n"
       "  libs = ../../foo/lib1.a -lfoo\n"
-      "  output_extension = .so\n";
+      "  output_extension = .so\n"
+      "  output_dir = \n";
 
   std::string out_str = out.str();
   EXPECT_EQ(expected, out_str);
 }
 
-TEST(NinjaBinaryTargetWriter, EmptyProductExtension) {
+TEST(NinjaBinaryTargetWriter, EmptyOutputExtension) {
   TestWithScope setup;
   Err err;
 
   setup.build_settings()->SetBuildDir(SourceDir("//out/Debug/"));
 
-  // This test is the same as ProductExtension, except that we call
+  // This test is the same as OutputExtensionAndInputDeps, except that we call
   // set_output_extension("") and ensure that we get an empty one and override
   // the output prefix so that the name matches the target exactly.
   Target target(setup.settings(), Label(SourceDir("//foo/"), "shlib"));
@@ -274,7 +280,8 @@
           "obj/foo/shlib.input2.o\n"
       "  ldflags =\n"
       "  libs =\n"
-      "  output_extension = \n";
+      "  output_extension = \n"
+      "  output_dir = \n";
 
   std::string out_str = out.str();
   EXPECT_EQ(expected, out_str);
@@ -357,7 +364,8 @@
           "obj/foo/inter.stamp\n"
       "  ldflags =\n"
       "  libs =\n"
-      "  output_extension = \n";
+      "  output_extension = \n"
+      "  output_dir = \n";
   EXPECT_EQ(final_expected, final_out.str());
 }
 
@@ -392,7 +400,8 @@
       "build ./libbar.so: solink obj/foo/libbar.sources.o | ../../foo/bar.def\n"
       "  ldflags = /DEF:../../foo/bar.def\n"
       "  libs =\n"
-      "  output_extension = .so\n";
+      "  output_extension = .so\n"
+      "  output_dir = \n";
   EXPECT_EQ(expected, out.str());
 }
 
@@ -427,7 +436,8 @@
       "build ./libbar.so: solink_module obj/foo/libbar.sources.o\n"
       "  ldflags =\n"
       "  libs =\n"
-      "  output_extension = .so\n";
+      "  output_extension = .so\n"
+      "  output_dir = \n";
   EXPECT_EQ(loadable_expected, out.str());
 
   // Final target.
@@ -458,7 +468,8 @@
       "build ./exe: link obj/foo/exe.final.o || ./libbar.so\n"
       "  ldflags =\n"
       "  libs =\n"
-      "  output_extension = \n";
+      "  output_extension = \n"
+      "  output_dir = \n";
   EXPECT_EQ(final_expected, final_out.str());
 }
 
diff --git a/tools/gn/substitution_type.cc b/tools/gn/substitution_type.cc
index e1ea14d..75d8b7a 100644
--- a/tools/gn/substitution_type.cc
+++ b/tools/gn/substitution_type.cc
@@ -43,6 +43,7 @@
   "{{inputs_newline}}",  // SUBSTITUTION_LINKER_INPUTS_NEWLINE
   "{{ldflags}}",  // SUBSTITUTION_LDFLAGS
   "{{libs}}",  // SUBSTITUTION_LIBS
+  "{{output_dir}}",  // SUBSTITUTION_OUTPUT_DIR
   "{{output_extension}}",  // SUBSTITUTION_OUTPUT_EXTENSION
   "{{solibs}}",  // SUBSTITUTION_SOLIBS
 
@@ -91,6 +92,7 @@
     "in_newline",        // SUBSTITUTION_LINKER_INPUTS_NEWLINE
     "ldflags",           // SUBSTITUTION_LDFLAGS
     "libs",              // SUBSTITUTION_LIBS
+    "output_dir",        // SUBSTITUTION_OUTPUT_DIR
     "output_extension",  // SUBSTITUTION_OUTPUT_EXTENSION
     "solibs",            // SUBSTITUTION_SOLIBS
 
@@ -193,6 +195,7 @@
          type == SUBSTITUTION_LINKER_INPUTS_NEWLINE ||
          type == SUBSTITUTION_LDFLAGS ||
          type == SUBSTITUTION_LIBS ||
+         type == SUBSTITUTION_OUTPUT_DIR ||
          type == SUBSTITUTION_OUTPUT_EXTENSION ||
          type == SUBSTITUTION_SOLIBS;
 }
@@ -200,6 +203,7 @@
 bool IsValidLinkerOutputsSubstitution(SubstitutionType type) {
   // All valid compiler outputs plus the output extension.
   return IsValidCompilerOutputsSubstitution(type) ||
+         type == SUBSTITUTION_OUTPUT_DIR ||
          type == SUBSTITUTION_OUTPUT_EXTENSION;
 }
 
diff --git a/tools/gn/substitution_type.h b/tools/gn/substitution_type.h
index 3f7ec97..2e63373 100644
--- a/tools/gn/substitution_type.h
+++ b/tools/gn/substitution_type.h
@@ -56,6 +56,7 @@
   SUBSTITUTION_LINKER_INPUTS_NEWLINE,  // {{inputs_newline}}
   SUBSTITUTION_LDFLAGS,  // {{ldflags}}
   SUBSTITUTION_LIBS,  // {{libs}}
+  SUBSTITUTION_OUTPUT_DIR,  // {{output_dir}}
   SUBSTITUTION_OUTPUT_EXTENSION,  // {{output_extension}}
   SUBSTITUTION_SOLIBS,  // {{solibs}}
 
diff --git a/tools/gn/substitution_writer.cc b/tools/gn/substitution_writer.cc
index 23d6914..a82e7ee 100644
--- a/tools/gn/substitution_writer.cc
+++ b/tools/gn/substitution_writer.cc
@@ -56,6 +56,10 @@
     "\n"
     "Placeholders\n"
     "\n"
+    "  This section discusses only placeholders for actions. There are other\n"
+    "  placeholders used in the definition of tools. See \"gn help tool\" for\n"
+    "  those.\n"
+    "\n"
     "  {{source}}\n"
     "      The name of the source file including directory (*). This will\n"
     "      generally be used for specifying inputs to a script in the\n"
@@ -546,6 +550,23 @@
 
   // Fall-through to the linker-specific ones.
   switch (type) {
+    case SUBSTITUTION_OUTPUT_DIR:
+      // Use the target's value if there is one (it will have no expansion
+      // patterns since it can directly use GN variables to compute whatever
+      // path it wants), or the tool's default (which will contain further
+      // expansions).
+      if (target->output_dir().is_null()) {
+        return ApplyPatternToLinkerAsOutputFile(
+            target, tool, tool->default_output_dir()).value();
+      } else {
+        SetDirOrDotWithNoSlash(RebasePath(
+                target->output_dir().value(),
+                target->settings()->build_settings()->build_dir()),
+            &result);
+        return result;
+      }
+      break;
+
     case SUBSTITUTION_OUTPUT_EXTENSION:
       // Use the extension provided on the target if specified, otherwise
       // fall back on the default. Note that the target's output extension
diff --git a/tools/gn/substitution_writer_unittest.cc b/tools/gn/substitution_writer_unittest.cc
index 77374f1..d252c79 100644
--- a/tools/gn/substitution_writer_unittest.cc
+++ b/tools/gn/substitution_writer_unittest.cc
@@ -278,4 +278,46 @@
   EXPECT_EQ("",
             SubstitutionWriter::GetLinkerSubstitution(
                 &target, tool, SUBSTITUTION_OUTPUT_EXTENSION));
+
+  // Output directory is tested in a separate test below.
+}
+
+TEST(SubstitutionWriter, OutputDir) {
+  TestWithScope setup;
+  Err err;
+
+  // This tool has an output directory pattern and uses that for the
+  // output name.
+  Tool tool;
+  SubstitutionPattern out_dir_pattern;
+  ASSERT_TRUE(out_dir_pattern.Parse("{{root_out_dir}}/{{target_output_name}}",
+                                    nullptr, &err));
+  tool.set_default_output_dir(out_dir_pattern);
+  tool.SetComplete();
+
+  // Default target with no output dir overrides.
+  Target target(setup.settings(), Label(SourceDir("//foo/"), "baz"));
+  target.set_output_type(Target::EXECUTABLE);
+  target.SetToolchain(setup.toolchain());
+  ASSERT_TRUE(target.OnResolved(&err));
+
+  // The output should expand the default from the patterns in the tool.
+  SubstitutionPattern output_name;
+  ASSERT_TRUE(output_name.Parse("{{output_dir}}/{{target_output_name}}.exe",
+                                nullptr, &err));
+  EXPECT_EQ("./baz/baz.exe",
+            SubstitutionWriter::ApplyPatternToLinkerAsOutputFile(
+                &target, &tool, output_name).value());
+
+  // Override the output name to the root build dir.
+  target.set_output_dir(SourceDir("//out/Debug/"));
+  EXPECT_EQ("./baz.exe",
+            SubstitutionWriter::ApplyPatternToLinkerAsOutputFile(
+                &target, &tool, output_name).value());
+
+  // Override the output name to a new subdirectory.
+  target.set_output_dir(SourceDir("//out/Debug/foo/bar"));
+  EXPECT_EQ("foo/bar/baz.exe",
+            SubstitutionWriter::ApplyPatternToLinkerAsOutputFile(
+                &target, &tool, output_name).value());
 }
diff --git a/tools/gn/target.h b/tools/gn/target.h
index fdd6f1a..ac159e1 100644
--- a/tools/gn/target.h
+++ b/tools/gn/target.h
@@ -100,6 +100,12 @@
     output_prefix_override_ = prefix_override;
   }
 
+  // Desired output directory for the final output. This will be used for
+  // the {{output_dir}} substitution in the tool if it is specified. If
+  // is_null, the tool default will be used.
+  const SourceDir& output_dir() const { return output_dir_; }
+  void set_output_dir(const SourceDir& dir) { output_dir_ = dir; }
+
   // The output extension is really a tri-state: unset (output_extension_set
   // is false and the string is empty, meaning the default extension should be
   // used), the output extension is set but empty (output should have no
@@ -332,6 +338,7 @@
   OutputType output_type_;
   std::string output_name_;
   bool output_prefix_override_;
+  SourceDir output_dir_;
   std::string output_extension_;
   bool output_extension_set_;
 
diff --git a/tools/gn/tool.h b/tools/gn/tool.h
index 9360ce6..f6727ed 100644
--- a/tools/gn/tool.h
+++ b/tools/gn/tool.h
@@ -52,6 +52,14 @@
     default_output_extension_ = ext;
   }
 
+  const SubstitutionPattern& default_output_dir() const {
+    return default_output_dir_;
+  }
+  void set_default_output_dir(const SubstitutionPattern& dir) {
+    DCHECK(!complete_);
+    default_output_dir_ = dir;
+  }
+
   // Dependency file (if supported).
   const SubstitutionPattern& depfile() const {
     return depfile_;
@@ -186,6 +194,7 @@
  private:
   SubstitutionPattern command_;
   std::string default_output_extension_;
+  SubstitutionPattern default_output_dir_;
   SubstitutionPattern depfile_;
   DepsFormat depsformat_;
   PrecompiledHeaderType precompiled_header_type_;
diff --git a/tools/gn/variables.cc b/tools/gn/variables.cc
index 4c5fae1..46cd57e 100644
--- a/tools/gn/variables.cc
+++ b/tools/gn/variables.cc
@@ -1119,6 +1119,34 @@
     "    }\n"
     "  }\n";
 
+const char kOutputDir[] = "output_dir";
+const char kOutputDir_HelpShort[] =
+    "output_dir: [directory] Directory to put output file in.";
+const char kOutputDir_Help[] =
+    "output_dir: [directory] Directory to put output file in.\n"
+    "\n"
+    "  For library and executable targets, overrides the directory for the\n"
+    "  final output. This must be in the root_build_dir or a child thereof.\n"
+    "\n"
+    "  This should generally be in the root_out_dir or a subdirectory thereof\n"
+    "  (the root_out_dir will be the same as the root_build_dir for the\n"
+    "  default toolchain, and will be a subdirectory for other toolchains).\n"
+    "  Not putting the output in a subdirectory of root_out_dir can result\n"
+    "  in collisions between different toolchains, so you will need to take\n"
+    "  steps to ensure that your target is only present in one toolchain.\n"
+    "\n"
+    "  Normally the toolchain specifies the output directory for libraries\n"
+    "  and executables (see \"gn help tool\"). You will have to consult that\n"
+    "  for the default location. The default location will be used if\n"
+    "  output_dir is undefined or empty.\n"
+    "\n"
+    "Example\n"
+    "\n"
+    "  shared_library(\"doom_melon\") {\n"
+    "    output_dir = \"$root_out_dir/plugin_libs\"\n"
+    "    ...\n"
+    "  }\n";
+
 const char kOutputName[] = "output_name";
 const char kOutputName_HelpShort[] =
     "output_name: [string] Name for the output file other than the default.";
@@ -1636,6 +1664,7 @@
     INSERT_VARIABLE(Ldflags)
     INSERT_VARIABLE(Libs)
     INSERT_VARIABLE(LibDirs)
+    INSERT_VARIABLE(OutputDir)
     INSERT_VARIABLE(OutputExtension)
     INSERT_VARIABLE(OutputName)
     INSERT_VARIABLE(OutputPrefixOverride)
diff --git a/tools/gn/variables.h b/tools/gn/variables.h
index 08fea1a..2dafdee 100644
--- a/tools/gn/variables.h
+++ b/tools/gn/variables.h
@@ -183,6 +183,10 @@
 extern const char kLibs_HelpShort[];
 extern const char kLibs_Help[];
 
+extern const char kOutputDir[];
+extern const char kOutputDir_HelpShort[];
+extern const char kOutputDir_Help[];
+
 extern const char kOutputExtension[];
 extern const char kOutputExtension_HelpShort[];
 extern const char kOutputExtension_Help[];