Visual Studio generators for GN

BUG=305761

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

Cr-Original-Commit-Position: refs/heads/master@{#372354}
Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src
Cr-Mirrored-Commit: 050f20691a64eda7dc6df981bd576856fa19be4c
diff --git a/tools/gn/BUILD.gn b/tools/gn/BUILD.gn
index a655a43..e643f97 100644
--- a/tools/gn/BUILD.gn
+++ b/tools/gn/BUILD.gn
@@ -189,6 +189,12 @@
     "variables.h",
     "visibility.cc",
     "visibility.h",
+    "visual_studio_utils.cc",
+    "visual_studio_utils.h",
+    "visual_studio_writer.cc",
+    "visual_studio_writer.h",
+    "xml_element_writer.cc",
+    "xml_element_writer.h",
   ]
 
   deps = [
@@ -299,6 +305,9 @@
     "unique_vector_unittest.cc",
     "value_unittest.cc",
     "visibility_unittest.cc",
+    "visual_studio_utils_unittest.cc",
+    "visual_studio_writer_unittest.cc",
+    "xml_element_writer_unittest.cc",
   ]
 
   data = [
diff --git a/tools/gn/command_gen.cc b/tools/gn/command_gen.cc
index a8221a6..f85a299 100644
--- a/tools/gn/command_gen.cc
+++ b/tools/gn/command_gen.cc
@@ -18,12 +18,15 @@
 #include "tools/gn/standard_out.h"
 #include "tools/gn/switches.h"
 #include "tools/gn/target.h"
+#include "tools/gn/visual_studio_writer.h"
 
 namespace commands {
 
 namespace {
 
 const char kSwitchCheck[] = "check";
+const char kSwitchIde[] = "ide";
+const char kSwitchIdeValueVs[] = "vs";
 
 // Called on worker thread to write the ninja file.
 void BackgroundDoWrite(const Target* target) {
@@ -144,6 +147,29 @@
   return false;
 }
 
+bool RunIdeWriter(const std::string& ide,
+                  const BuildSettings* build_settings,
+                  Builder* builder,
+                  Err* err) {
+  if (ide == kSwitchIdeValueVs) {
+    base::TimeTicks begin_vs_gen = base::TimeTicks::Now();
+    bool res =
+        VisualStudioWriter::RunAndWriteFiles(build_settings, builder, err);
+    if (res &&
+        !base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kQuiet)) {
+      OutputString(
+          "Generating Visual Studio projects took " +
+          base::Int64ToString(
+              (base::TimeTicks::Now() - begin_vs_gen).InMilliseconds()) +
+          "ms\n");
+    }
+    return res;
+  }
+
+  *err = Err(Location(), "Unknown IDE: " + ide);
+  return false;
+}
+
 }  // namespace
 
 const char kGen[] = "gen";
@@ -152,7 +178,7 @@
 const char kGen_Help[] =
     "gn gen: Generate ninja files.\n"
     "\n"
-    "  gn gen <out_dir>\n"
+    "  gn gen [--ide=<ide_name>] <out_dir>\n"
     "\n"
     "  Generates ninja files from the current tree and puts them in the given\n"
     "  output directory.\n"
@@ -162,6 +188,10 @@
     "  Or it can be a directory relative to the current directory such as:\n"
     "      out/foo\n"
     "\n"
+    "  --ide=<ide_name>\n"
+    "    Also generate files for an IDE. Currently supported values:\n"
+    "      'vs' - Visual Studio project/solution files.\n"
+    "\n"
     "  See \"gn help switches\" for the common command-line switches.\n";
 
 int RunGen(const std::vector<std::string>& args) {
@@ -179,7 +209,9 @@
   if (!setup->DoSetup(args[0], true))
     return 1;
 
-  if (base::CommandLine::ForCurrentProcess()->HasSwitch(kSwitchCheck))
+  const base::CommandLine* command_line =
+      base::CommandLine::ForCurrentProcess();
+  if (command_line->HasSwitch(kSwitchCheck))
     setup->set_check_public_headers(true);
 
   // Cause the load to also generate the ninja files for each target. We wrap
@@ -210,9 +242,16 @@
   if (!CheckForInvalidGeneratedInputs(setup))
     return 1;
 
+  if (command_line->HasSwitch(kSwitchIde) &&
+      !RunIdeWriter(command_line->GetSwitchValueASCII(kSwitchIde),
+                    &setup->build_settings(), setup->builder(), &err)) {
+    err.PrintToStdout();
+    return 1;
+  }
+
   base::TimeDelta elapsed_time = timer.Elapsed();
 
-  if (!base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kQuiet)) {
+  if (!command_line->HasSwitch(switches::kQuiet)) {
     OutputString("Done. ", DECORATION_GREEN);
 
     std::string stats = "Wrote " +
diff --git a/tools/gn/gn.gyp b/tools/gn/gn.gyp
index ade8d3a..24cff71 100644
--- a/tools/gn/gn.gyp
+++ b/tools/gn/gn.gyp
@@ -189,6 +189,12 @@
         'variables.h',
         'visibility.cc',
         'visibility.h',
+        'visual_studio_utils.cc',
+        'visual_studio_utils.h',
+        'visual_studio_writer.cc',
+        'visual_studio_writer.h',
+        'xml_element_writer.cc',
+        'xml_element_writer.h',
       ],
     },
     {
@@ -259,6 +265,9 @@
         'unique_vector_unittest.cc',
         'value_unittest.cc',
         'visibility_unittest.cc',
+        'visual_studio_utils_unittest.cc',
+        'visual_studio_writer_unittest.cc',
+        'xml_element_writer_unittest.cc',
       ],
       'dependencies': [
         'gn_lib',
diff --git a/tools/gn/visual_studio_utils.cc b/tools/gn/visual_studio_utils.cc
new file mode 100644
index 0000000..8944722
--- /dev/null
+++ b/tools/gn/visual_studio_utils.cc
@@ -0,0 +1,117 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "tools/gn/visual_studio_utils.h"
+
+#include "base/md5.h"
+#include "base/strings/string_util.h"
+
+CompilerOptions::CompilerOptions() = default;
+
+CompilerOptions::~CompilerOptions() = default;
+
+std::string MakeGuid(const std::string& entry_path, const std::string& seed) {
+  std::string str = base::ToUpperASCII(base::MD5String(seed + entry_path));
+  return '{' + str.substr(0, 8) + '-' + str.substr(8, 4) + '-' +
+         str.substr(12, 4) + '-' + str.substr(16, 4) + '-' +
+         str.substr(20, 12) + '}';
+}
+
+#define SetOption(condition, member, value) \
+  if (condition) {                          \
+    options->member = value;                \
+    return;                                 \
+  }
+
+#define AppendOption(condition, member, value, separator) \
+  if (condition) {                                        \
+    options->member += value + separator;                 \
+    return;                                               \
+  }
+
+void ParseCompilerOption(const std::string& cflag, CompilerOptions* options) {
+  if (cflag.size() > 2 && cflag[0] == '/') {
+    switch (cflag[1]) {
+      case 'F':
+        AppendOption(cflag.size() > 3 && cflag[2] == 'I', forced_include_files,
+                     cflag.substr(3), ';')
+        break;
+
+      case 'G':
+        if (cflag[2] == 'S') {
+          SetOption(cflag.size() == 3, buffer_security_check, "true")
+          SetOption(cflag.size() == 4 && cflag[3] == '-',
+                    buffer_security_check, "false")
+        }
+        break;
+
+      case 'M':
+        switch (cflag[2]) {
+          case 'D':
+            SetOption(cflag.size() == 3, runtime_library, "MultiThreadedDLL")
+            SetOption(cflag.size() == 4 && cflag[3] == 'd', runtime_library,
+                      "MultiThreadedDebugDLL")
+            break;
+
+          case 'T':
+            SetOption(cflag.size() == 3, runtime_library, "MultiThreaded")
+            SetOption(cflag.size() == 4 && cflag[3] == 'd', runtime_library,
+                      "MultiThreadedDebug")
+            break;
+        }
+        break;
+
+      case 'O':
+        switch (cflag[2]) {
+          case '1':
+            SetOption(cflag.size() == 3, optimization, "MinSpace")
+            break;
+
+          case '2':
+            SetOption(cflag.size() == 3, optimization, "MaxSpeed")
+            break;
+
+          case 'd':
+            SetOption(cflag.size() == 3, optimization, "Disabled")
+            break;
+
+          case 'x':
+            SetOption(cflag.size() == 3, optimization, "Full")
+              break;
+        }
+        break;
+
+      case 'T':
+        // Skip flags that cause treating all source files as C and C++ files.
+        if (cflag.size() == 3 && (cflag[2] == 'C' || cflag[2] == 'P'))
+          return;
+        break;
+
+      case 'W':
+        switch (cflag[2]) {
+          case '0':
+          case '1':
+          case '2':
+          case '3':
+          case '4':
+            SetOption(cflag.size() == 3, warning_level,
+                      std::string("Level") + cflag[2])
+            break;
+
+          case 'X':
+            SetOption(cflag.size() == 3, treat_warning_as_error, "true")
+            break;
+        }
+        break;
+
+      case 'w':
+        AppendOption(cflag.size() > 3 && cflag[2] == 'd',
+                     disable_specific_warnings, cflag.substr(3), ';')
+        break;
+    }
+  }
+
+  // Put everything else into additional_options.
+  options->additional_options += cflag + ' ';
+}
diff --git a/tools/gn/visual_studio_utils.h b/tools/gn/visual_studio_utils.h
new file mode 100644
index 0000000..b91b218
--- /dev/null
+++ b/tools/gn/visual_studio_utils.h
@@ -0,0 +1,37 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef TOOLS_GN_VISUAL_STUDIO_UTILS_H_
+#define TOOLS_GN_VISUAL_STUDIO_UTILS_H_
+
+#include <string>
+
+// Some compiler options which will be written to project file. We don't need to
+// specify all options because generated project file is going to be used only
+// for compilation of single file. For real build ninja files are used.
+struct CompilerOptions {
+  CompilerOptions();
+  ~CompilerOptions();
+
+  std::string additional_options;
+  std::string buffer_security_check;
+  std::string forced_include_files;
+  std::string disable_specific_warnings;
+  std::string optimization;
+  std::string runtime_library;
+  std::string treat_warning_as_error;
+  std::string warning_level;
+};
+
+// Generates something which looks like a GUID, but depends only on the name and
+// seed. This means the same name / seed will always generate the same GUID, so
+// that projects and solutions which refer to each other can explicitly
+// determine the GUID to refer to explicitly. It also means that the GUID will
+// not change when the project for a target is rebuilt.
+std::string MakeGuid(const std::string& entry_path, const std::string& seed);
+
+// Parses |cflag| value and stores it in |options|.
+void ParseCompilerOption(const std::string& cflag, CompilerOptions* options);
+
+#endif  // TOOLS_GN_VISUAL_STUDIO_UTILS_H_
diff --git a/tools/gn/visual_studio_utils_unittest.cc b/tools/gn/visual_studio_utils_unittest.cc
new file mode 100644
index 0000000..c4e2530
--- /dev/null
+++ b/tools/gn/visual_studio_utils_unittest.cc
@@ -0,0 +1,94 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "tools/gn/visual_studio_utils.h"
+
+#include "base/location.h"
+#include "base/strings/string_util.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+TEST(VisualStudioUtils, MakeGuid) {
+  std::string pattern = "{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}";
+  std::string guid = MakeGuid(__FILE__, "foo");
+  ASSERT_EQ(pattern.size(), guid.size());
+  for (size_t i = 0; i < pattern.size(); ++i) {
+    if (pattern[i] == 'x')
+      ASSERT_TRUE(base::IsAsciiAlpha(guid[i]) || base::IsAsciiDigit(guid[i]));
+    else
+      ASSERT_EQ(pattern[i], guid[i]);
+  }
+
+  // Calling function again should produce the same GUID.
+  ASSERT_EQ(guid, MakeGuid(__FILE__, "foo"));
+
+  // GUIDs should be different if path or seed is different.
+  ASSERT_NE(guid, MakeGuid(std::string(__FILE__) + ".txt", "foo"));
+  ASSERT_NE(guid, MakeGuid(__FILE__, "bar"));
+}
+
+TEST(VisualStudioUtils, ParseCompilerOption) {
+  CompilerOptions options;
+  ParseCompilerOption("/FIinclude.h", &options);
+  ParseCompilerOption("/FIC:/path/file.h", &options);
+  ASSERT_EQ("include.h;C:/path/file.h;", options.forced_include_files);
+
+  CHECK(options.buffer_security_check.empty());
+  ParseCompilerOption("/GS", &options);
+  ASSERT_EQ("true", options.buffer_security_check);
+  ParseCompilerOption("/GS-", &options);
+  ASSERT_EQ("false", options.buffer_security_check);
+
+  CHECK(options.runtime_library.empty());
+  ParseCompilerOption("/MD", &options);
+  ASSERT_EQ("MultiThreadedDLL", options.runtime_library);
+  ParseCompilerOption("/MDd", &options);
+  ASSERT_EQ("MultiThreadedDebugDLL", options.runtime_library);
+  ParseCompilerOption("/MT", &options);
+  ASSERT_EQ("MultiThreaded", options.runtime_library);
+  ParseCompilerOption("/MTd", &options);
+  ASSERT_EQ("MultiThreadedDebug", options.runtime_library);
+
+  CHECK(options.optimization.empty());
+  ParseCompilerOption("/O1", &options);
+  ASSERT_EQ("MinSpace", options.optimization);
+  ParseCompilerOption("/O2", &options);
+  ASSERT_EQ("MaxSpeed", options.optimization);
+  ParseCompilerOption("/Od", &options);
+  ASSERT_EQ("Disabled", options.optimization);
+  ParseCompilerOption("/Ox", &options);
+  ASSERT_EQ("Full", options.optimization);
+
+  CHECK(options.additional_options.empty());
+  ParseCompilerOption("/TC", &options);
+  ASSERT_TRUE(options.additional_options.empty());
+  ParseCompilerOption("/TP", &options);
+  ASSERT_TRUE(options.additional_options.empty());
+
+  CHECK(options.warning_level.empty());
+  ParseCompilerOption("/W0", &options);
+  ASSERT_EQ("Level0", options.warning_level);
+  ParseCompilerOption("/W1", &options);
+  ASSERT_EQ("Level1", options.warning_level);
+  ParseCompilerOption("/W2", &options);
+  ASSERT_EQ("Level2", options.warning_level);
+  ParseCompilerOption("/W3", &options);
+  ASSERT_EQ("Level3", options.warning_level);
+  ParseCompilerOption("/W4", &options);
+  ASSERT_EQ("Level4", options.warning_level);
+
+  CHECK(options.treat_warning_as_error.empty());
+  ParseCompilerOption("/WX", &options);
+  ASSERT_EQ("true", options.treat_warning_as_error);
+
+  CHECK(options.disable_specific_warnings.empty());
+  ParseCompilerOption("/wd1234", &options);
+  ParseCompilerOption("/wd56", &options);
+  ASSERT_EQ("1234;56;", options.disable_specific_warnings);
+
+  CHECK(options.additional_options.empty());
+  ParseCompilerOption("/MP", &options);
+  ParseCompilerOption("/bigobj", &options);
+  ParseCompilerOption("/Zc:sizedDealloc", &options);
+  ASSERT_EQ("/MP /bigobj /Zc:sizedDealloc ", options.additional_options);
+}
diff --git a/tools/gn/visual_studio_writer.cc b/tools/gn/visual_studio_writer.cc
new file mode 100644
index 0000000..968e046
--- /dev/null
+++ b/tools/gn/visual_studio_writer.cc
@@ -0,0 +1,754 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "tools/gn/visual_studio_writer.h"
+
+#include <algorithm>
+#include <map>
+#include <set>
+#include <string>
+
+#include "base/files/file_util.h"
+#include "base/logging.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/strings/string_util.h"
+#include "base/strings/utf_string_conversions.h"
+#include "tools/gn/builder.h"
+#include "tools/gn/config.h"
+#include "tools/gn/config_values_extractors.h"
+#include "tools/gn/filesystem_utils.h"
+#include "tools/gn/parse_tree.h"
+#include "tools/gn/path_output.h"
+#include "tools/gn/source_file_type.h"
+#include "tools/gn/standard_out.h"
+#include "tools/gn/target.h"
+#include "tools/gn/variables.h"
+#include "tools/gn/visual_studio_utils.h"
+#include "tools/gn/xml_element_writer.h"
+
+#if defined(OS_WIN)
+#include "base/win/registry.h"
+#endif
+
+namespace {
+
+struct SemicolonSeparatedWriter {
+  void operator()(const std::string& value, std::ostream& out) const {
+    out << value + ';';
+  }
+};
+
+struct IncludeDirWriter {
+  explicit IncludeDirWriter(PathOutput& path_output)
+      : path_output_(path_output) {}
+  ~IncludeDirWriter() = default;
+
+  void operator()(const SourceDir& dir, std::ostream& out) const {
+    path_output_.WriteDir(out, dir, PathOutput::DIR_NO_LAST_SLASH);
+    out << ";";
+  }
+
+  PathOutput& path_output_;
+};
+
+struct SourceFileWriter {
+  SourceFileWriter(PathOutput& path_output, const SourceFile& source_file)
+      : path_output_(path_output), source_file_(source_file) {}
+  ~SourceFileWriter() = default;
+
+  void operator()(std::ostream& out) const {
+    path_output_.WriteFile(out, source_file_);
+  }
+
+  PathOutput& path_output_;
+  const SourceFile& source_file_;
+};
+
+const char kToolsetVersion[] = "v140";                     // Visual Studio 2015
+const char kVisualStudioVersion[] = "14.0";                // Visual Studio 2015
+const char kWindowsKitsVersion[] = "10";                   // Windows 10 SDK
+const char kWindowsKitsIncludeVersion[] = "10.0.10240.0";  // Windows 10 SDK
+
+const char kGuidTypeProject[] = "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}";
+const char kGuidTypeFolder[] = "{2150E333-8FDC-42A3-9474-1A3956D46DE8}";
+const char kGuidSeedProject[] = "project";
+const char kGuidSeedFolder[] = "folder";
+const char kGuidSeedFilter[] = "filter";
+
+std::string GetWindowsKitsIncludeDirs() {
+  std::string kits_path;
+
+#if defined(OS_WIN)
+  const base::char16* const subkeys[] = {
+      L"SOFTWARE\\Microsoft\\Windows Kits\\Installed Roots",
+      L"SOFTWARE\\Wow6432Node\\Microsoft\\Windows Kits\\Installed Roots"};
+
+  base::string16 value_name =
+      base::ASCIIToUTF16("KitsRoot") + base::ASCIIToUTF16(kWindowsKitsVersion);
+
+  for (const base::char16* subkey : subkeys) {
+    base::win::RegKey key(HKEY_LOCAL_MACHINE, subkey, KEY_READ);
+    base::string16 value;
+    if (key.ReadValue(value_name.c_str(), &value) == ERROR_SUCCESS) {
+      kits_path = base::UTF16ToUTF8(value);
+      break;
+    }
+  }
+#endif  // OS_WIN
+
+  if (kits_path.empty()) {
+    kits_path = std::string("C:\\Program Files (x86)\\Windows Kits\\") +
+                kWindowsKitsVersion + "\\";
+  }
+
+  return kits_path + "Include\\" + kWindowsKitsIncludeVersion + "\\shared;" +
+         kits_path + "Include\\" + kWindowsKitsIncludeVersion + "\\um;" +
+         kits_path + "Include\\" + kWindowsKitsIncludeVersion + "\\winrt;";
+}
+
+std::string GetConfigurationType(const Target* target, Err* err) {
+  switch (target->output_type()) {
+    case Target::EXECUTABLE:
+      return "Application";
+    case Target::SHARED_LIBRARY:
+    case Target::LOADABLE_MODULE:
+      return "DynamicLibrary";
+    case Target::STATIC_LIBRARY:
+    case Target::SOURCE_SET:
+      return "StaticLibrary";
+
+    default:
+      *err = Err(Location(),
+                 "Visual Studio doesn't support '" + target->label().name() +
+                     "' target output type: " +
+                     Target::GetStringForOutputType(target->output_type()));
+      return std::string();
+  }
+}
+
+void ParseCompilerOptions(const std::vector<std::string>& cflags,
+                          CompilerOptions* options) {
+  for (const std::string& flag : cflags)
+    ParseCompilerOption(flag, options);
+}
+
+void ParseCompilerOptions(const Target* target, CompilerOptions* options) {
+  for (ConfigValuesIterator iter(target); !iter.done(); iter.Next()) {
+    ParseCompilerOptions(iter.cur().cflags(), options);
+    ParseCompilerOptions(iter.cur().cflags_c(), options);
+    ParseCompilerOptions(iter.cur().cflags_cc(), options);
+  }
+}
+
+// Returns a string piece pointing into the input string identifying the parent
+// directory path, excluding the last slash. Note that the input pointer must
+// outlive the output.
+base::StringPiece FindParentDir(const std::string* path) {
+  DCHECK(path && !path->empty());
+  for (int i = static_cast<int>(path->size()) - 2; i >= 0; --i) {
+    if (IsSlash((*path)[i]))
+      return base::StringPiece(path->data(), i);
+  }
+  return base::StringPiece();
+}
+
+bool HasSameContent(std::stringstream& data_1, const base::FilePath& data_2) {
+  // Compare file sizes first. Quick and will save us some time if they are
+  // different sizes.
+  int64_t data_1_len = data_1.tellp();
+
+  int64_t data_2_len;
+  if (!base::GetFileSize(data_2, &data_2_len) || data_1_len != data_2_len)
+    return false;
+
+  std::string data_2_data;
+  data_2_data.resize(data_2_len);
+  if (!base::ReadFileToString(data_2, &data_2_data))
+    return false;
+
+  std::string data_1_data;
+  data_1_data = data_1.str();
+
+  return data_1_data == data_2_data;
+}
+
+}  // namespace
+
+VisualStudioWriter::SolutionEntry::SolutionEntry(const std::string& _name,
+                                                 const std::string& _path,
+                                                 const std::string& _guid)
+    : name(_name), path(_path), guid(_guid), parent_folder(nullptr) {}
+
+VisualStudioWriter::SolutionEntry::~SolutionEntry() = default;
+
+VisualStudioWriter::VisualStudioWriter(const BuildSettings* build_settings)
+    : build_settings_(build_settings) {
+  const Value* value = build_settings->build_args().GetArgOverride("is_debug");
+  is_debug_config_ = value == nullptr || value->boolean_value();
+  config_platform_ = "Win32";
+  value = build_settings->build_args().GetArgOverride(variables::kTargetCpu);
+  if (value != nullptr && value->string_value() == "x64")
+    config_platform_ = "x64";
+
+  windows_kits_include_dirs_ = GetWindowsKitsIncludeDirs();
+}
+
+VisualStudioWriter::~VisualStudioWriter() {
+  STLDeleteContainerPointers(projects_.begin(), projects_.end());
+  STLDeleteContainerPointers(folders_.begin(), folders_.end());
+}
+
+// static
+bool VisualStudioWriter::RunAndWriteFiles(const BuildSettings* build_settings,
+                                          Builder* builder,
+                                          Err* err) {
+  std::vector<const Target*> targets = builder->GetAllResolvedTargets();
+
+  VisualStudioWriter writer(build_settings);
+  writer.projects_.reserve(targets.size());
+  writer.folders_.reserve(targets.size());
+
+  std::set<std::string> processed_targets;
+  for (const Target* target : targets) {
+    // Skip targets which are duplicated in vector.
+    std::string target_path =
+        target->label().dir().value() + target->label().name();
+    if (processed_targets.find(target_path) != processed_targets.end())
+      continue;
+
+    // Skip actions and groups.
+    if (target->output_type() == Target::GROUP ||
+        target->output_type() == Target::COPY_FILES ||
+        target->output_type() == Target::ACTION ||
+        target->output_type() == Target::ACTION_FOREACH) {
+      continue;
+    }
+
+    if (!writer.WriteProjectFiles(target, err))
+      return false;
+
+    processed_targets.insert(target_path);
+  }
+
+  if (writer.projects_.empty()) {
+    *err = Err(Location(), "No Visual Studio projects generated.");
+    return false;
+  }
+
+  writer.ResolveSolutionFolders();
+  return writer.WriteSolutionFile(err);
+}
+
+bool VisualStudioWriter::WriteProjectFiles(const Target* target, Err* err) {
+  SourceFile target_file = GetTargetOutputDir(target).ResolveRelativeFile(
+      Value(nullptr, target->label().name() + ".vcxproj"), err);
+  if (target_file.is_null())
+    return false;
+
+  base::FilePath vcxproj_path = build_settings_->GetFullPath(target_file);
+  std::string vcxproj_path_str = FilePathToUTF8(vcxproj_path);
+
+  projects_.push_back(
+      new SolutionEntry(target->label().name(), vcxproj_path_str,
+                        MakeGuid(vcxproj_path_str, kGuidSeedProject)));
+  projects_.back()->label_dir_path =
+      FilePathToUTF8(build_settings_->GetFullPath(target->label().dir()));
+
+  std::stringstream vcxproj_string_out;
+  if (!WriteProjectFileContents(vcxproj_string_out, *projects_.back(), target,
+                                err)) {
+    projects_.pop_back();
+    return false;
+  }
+
+  // Only write the content to the file if it's different. That is
+  // both a performance optimization and more importantly, prevents
+  // Visual Studio from reloading the projects.
+  if (!HasSameContent(vcxproj_string_out, vcxproj_path)) {
+    std::string content = vcxproj_string_out.str();
+    int size = static_cast<int>(content.size());
+    if (base::WriteFile(vcxproj_path, content.c_str(), size) != size) {
+      *err = Err(Location(), "Couldn't open " + target->label().name() +
+                                 ".vcxproj for writing");
+      return false;
+    }
+  }
+
+  base::FilePath filters_path = UTF8ToFilePath(vcxproj_path_str + ".filters");
+
+  std::stringstream filters_string_out;
+  WriteFiltersFileContents(filters_string_out, target);
+
+  if (!HasSameContent(filters_string_out, filters_path)) {
+    std::string content = filters_string_out.str();
+    int size = static_cast<int>(content.size());
+    if (base::WriteFile(filters_path, content.c_str(), size) != size) {
+      *err = Err(Location(), "Couldn't open " + target->label().name() +
+                                 ".vcxproj.filters for writing");
+      return false;
+    }
+  }
+
+  return true;
+}
+
+bool VisualStudioWriter::WriteProjectFileContents(
+    std::ostream& out,
+    const SolutionEntry& solution_project,
+    const Target* target,
+    Err* err) {
+  PathOutput path_output(GetTargetOutputDir(target),
+                         build_settings_->root_path_utf8(),
+                         EscapingMode::ESCAPE_NONE);
+
+  out << "<?xml version=\"1.0\" encoding=\"utf-8\"?>" << std::endl;
+  XmlElementWriter project(
+      out, "Project",
+      XmlAttributes("DefaultTargets", "Build")
+          .add("ToolsVersion", kVisualStudioVersion)
+          .add("xmlns", "http://schemas.microsoft.com/developer/msbuild/2003"));
+
+  {
+    scoped_ptr<XmlElementWriter> configurations = project.SubElement(
+        "ItemGroup", XmlAttributes("Label", "ProjectConfigurations"));
+    std::string config_name = is_debug_config_ ? "Debug" : "Release";
+    scoped_ptr<XmlElementWriter> project_config = configurations->SubElement(
+        "ProjectConfiguration",
+        XmlAttributes("Include", config_name + '|' + config_platform_));
+    project_config->SubElement("Configuration")->Text(config_name);
+    project_config->SubElement("Platform")->Text(config_platform_);
+  }
+
+  {
+    scoped_ptr<XmlElementWriter> globals =
+        project.SubElement("PropertyGroup", XmlAttributes("Label", "Globals"));
+    globals->SubElement("ProjectGuid")->Text(solution_project.guid);
+    globals->SubElement("Keyword")->Text("Win32Proj");
+    globals->SubElement("RootNamespace")->Text(target->label().name());
+    globals->SubElement("IgnoreWarnCompileDuplicatedFilename")->Text("true");
+    globals->SubElement("PreferredToolArchitecture")->Text("x64");
+  }
+
+  project.SubElement(
+      "Import", XmlAttributes("Project",
+                              "$(VCTargetsPath)\\Microsoft.Cpp.Default.props"));
+
+  {
+    scoped_ptr<XmlElementWriter> configuration = project.SubElement(
+        "PropertyGroup", XmlAttributes("Label", "Configuration"));
+    configuration->SubElement("CharacterSet")->Text("Unicode");
+    std::string configuration_type = GetConfigurationType(target, err);
+    if (configuration_type.empty())
+      return false;
+    configuration->SubElement("ConfigurationType")->Text(configuration_type);
+  }
+
+  {
+    scoped_ptr<XmlElementWriter> locals =
+        project.SubElement("PropertyGroup", XmlAttributes("Label", "Locals"));
+    locals->SubElement("PlatformToolset")->Text(kToolsetVersion);
+  }
+
+  project.SubElement(
+      "Import",
+      XmlAttributes("Project", "$(VCTargetsPath)\\Microsoft.Cpp.props"));
+  project.SubElement(
+      "Import",
+      XmlAttributes("Project",
+                    "$(VCTargetsPath)\\BuildCustomizations\\masm.props"));
+  project.SubElement("ImportGroup",
+                     XmlAttributes("Label", "ExtensionSettings"));
+
+  {
+    scoped_ptr<XmlElementWriter> property_sheets = project.SubElement(
+        "ImportGroup", XmlAttributes("Label", "PropertySheets"));
+    property_sheets->SubElement(
+        "Import",
+        XmlAttributes(
+            "Condition",
+            "exists('$(UserRootDir)\\Microsoft.Cpp.$(Platform).user.props')")
+            .add("Label", "LocalAppDataPlatform")
+            .add("Project",
+                 "$(UserRootDir)\\Microsoft.Cpp.$(Platform).user.props"));
+  }
+
+  project.SubElement("PropertyGroup", XmlAttributes("Label", "UserMacros"));
+
+  {
+    scoped_ptr<XmlElementWriter> properties =
+        project.SubElement("PropertyGroup");
+    {
+      scoped_ptr<XmlElementWriter> out_dir = properties->SubElement("OutDir");
+      path_output.WriteDir(out_dir->StartContent(false),
+                           build_settings_->build_dir(),
+                           PathOutput::DIR_INCLUDE_LAST_SLASH);
+    }
+    properties->SubElement("TargetName")->Text("$(ProjectName)");
+    properties->SubElement("TargetPath")
+        ->Text("$(OutDir)\\$(ProjectName)$(TargetExt)");
+  }
+
+  {
+    scoped_ptr<XmlElementWriter> item_definitions =
+        project.SubElement("ItemDefinitionGroup");
+    {
+      scoped_ptr<XmlElementWriter> cl_compile =
+          item_definitions->SubElement("ClCompile");
+      {
+        scoped_ptr<XmlElementWriter> include_dirs =
+            cl_compile->SubElement("AdditionalIncludeDirectories");
+        RecursiveTargetConfigToStream<SourceDir>(
+            target, &ConfigValues::include_dirs, IncludeDirWriter(path_output),
+            include_dirs->StartContent(false));
+        include_dirs->Text(windows_kits_include_dirs_ +
+                           "$(VSInstallDir)\\VC\\atlmfc\\include;" +
+                           "%(AdditionalIncludeDirectories)");
+      }
+      CompilerOptions options;
+      ParseCompilerOptions(target, &options);
+      if (!options.additional_options.empty()) {
+        cl_compile->SubElement("AdditionalOptions")
+            ->Text(options.additional_options + "%(AdditionalOptions)");
+      }
+      if (!options.buffer_security_check.empty()) {
+        cl_compile->SubElement("BufferSecurityCheck")
+            ->Text(options.buffer_security_check);
+      }
+      cl_compile->SubElement("CompileAsWinRT")->Text("false");
+      cl_compile->SubElement("DebugInformationFormat")->Text("ProgramDatabase");
+      if (!options.disable_specific_warnings.empty()) {
+        cl_compile->SubElement("DisableSpecificWarnings")
+            ->Text(options.disable_specific_warnings +
+                   "%(DisableSpecificWarnings)");
+      }
+      cl_compile->SubElement("ExceptionHandling")->Text("false");
+      if (!options.forced_include_files.empty()) {
+        cl_compile->SubElement("ForcedIncludeFiles")
+            ->Text(options.forced_include_files);
+      }
+      cl_compile->SubElement("MinimalRebuild")->Text("false");
+      if (!options.optimization.empty())
+        cl_compile->SubElement("Optimization")->Text(options.optimization);
+      if (target->config_values().has_precompiled_headers()) {
+        cl_compile->SubElement("PrecompiledHeader")->Text("Use");
+        cl_compile->SubElement("PrecompiledHeaderFile")
+            ->Text(target->config_values().precompiled_header());
+      } else {
+        cl_compile->SubElement("PrecompiledHeader")->Text("NotUsing");
+      }
+      {
+        scoped_ptr<XmlElementWriter> preprocessor_definitions =
+            cl_compile->SubElement("PreprocessorDefinitions");
+        RecursiveTargetConfigToStream<std::string>(
+            target, &ConfigValues::defines, SemicolonSeparatedWriter(),
+            preprocessor_definitions->StartContent(false));
+        preprocessor_definitions->Text("%(PreprocessorDefinitions)");
+      }
+      if (!options.runtime_library.empty())
+        cl_compile->SubElement("RuntimeLibrary")->Text(options.runtime_library);
+      if (!options.treat_warning_as_error.empty()) {
+        cl_compile->SubElement("TreatWarningAsError")
+            ->Text(options.treat_warning_as_error);
+      }
+      if (!options.warning_level.empty())
+        cl_compile->SubElement("WarningLevel")->Text(options.warning_level);
+    }
+
+    // We don't include resource compilation and link options as ninja files
+    // are used to generate real build.
+  }
+
+  {
+    scoped_ptr<XmlElementWriter> group = project.SubElement("ItemGroup");
+    if (!target->config_values().precompiled_source().is_null()) {
+      group
+          ->SubElement(
+              "ClCompile", "Include",
+              SourceFileWriter(path_output,
+                               target->config_values().precompiled_source()))
+          ->SubElement("PrecompiledHeader")
+          ->Text("Create");
+    }
+
+    for (const SourceFile& file : target->sources()) {
+      SourceFileType type = GetSourceFileType(file);
+      if (type == SOURCE_H || type == SOURCE_CPP || type == SOURCE_C) {
+        group->SubElement(type == SOURCE_H ? "ClInclude" : "ClCompile",
+                          "Include", SourceFileWriter(path_output, file));
+      }
+    }
+  }
+
+  project.SubElement(
+      "Import",
+      XmlAttributes("Project", "$(VCTargetsPath)\\Microsoft.Cpp.targets"));
+  project.SubElement(
+      "Import",
+      XmlAttributes("Project",
+                    "$(VCTargetsPath)\\BuildCustomizations\\masm.targets"));
+  project.SubElement("ImportGroup", XmlAttributes("Label", "ExtensionTargets"));
+
+  {
+    scoped_ptr<XmlElementWriter> build =
+        project.SubElement("Target", XmlAttributes("Name", "Build"));
+    build->SubElement(
+        "Exec",
+        XmlAttributes("Command", "call ninja.exe -C $(OutDir) $(ProjectName)"));
+  }
+
+  {
+    scoped_ptr<XmlElementWriter> clean =
+        project.SubElement("Target", XmlAttributes("Name", "Clean"));
+    clean->SubElement(
+        "Exec",
+        XmlAttributes("Command",
+                      "call ninja.exe -C $(OutDir) -tclean $(ProjectName)"));
+  }
+
+  return true;
+}
+
+void VisualStudioWriter::WriteFiltersFileContents(std::ostream& out,
+                                                  const Target* target) {
+  out << "<?xml version=\"1.0\" encoding=\"utf-8\"?>" << std::endl;
+  XmlElementWriter project(
+      out, "Project",
+      XmlAttributes("ToolsVersion", "4.0")
+          .add("xmlns", "http://schemas.microsoft.com/developer/msbuild/2003"));
+
+  std::ostringstream files_out;
+
+  {
+    scoped_ptr<XmlElementWriter> filters_group =
+        project.SubElement("ItemGroup");
+    XmlElementWriter files_group(files_out, "ItemGroup", XmlAttributes(), 2);
+
+    // File paths are relative to vcxproj files which are generated to out dirs.
+    // Filters tree structure need to reflect source directories and be relative
+    // to target file. We need two path outputs then.
+    PathOutput file_path_output(GetTargetOutputDir(target),
+                                build_settings_->root_path_utf8(),
+                                EscapingMode::ESCAPE_NONE);
+    PathOutput filter_path_output(target->label().dir(),
+                                  build_settings_->root_path_utf8(),
+                                  EscapingMode::ESCAPE_NONE);
+
+    std::set<std::string> processed_filters;
+
+    for (const SourceFile& file : target->sources()) {
+      SourceFileType type = GetSourceFileType(file);
+      if (type == SOURCE_H || type == SOURCE_CPP || type == SOURCE_C) {
+        scoped_ptr<XmlElementWriter> cl_item = files_group.SubElement(
+            type == SOURCE_H ? "ClInclude" : "ClCompile", "Include",
+            SourceFileWriter(file_path_output, file));
+
+        std::ostringstream target_relative_out;
+        filter_path_output.WriteFile(target_relative_out, file);
+        std::string target_relative_path = target_relative_out.str();
+        ConvertPathToSystem(&target_relative_path);
+        base::StringPiece filter_path = FindParentDir(&target_relative_path);
+
+        if (!filter_path.empty()) {
+          std::string filter_path_str = filter_path.as_string();
+          while (processed_filters.find(filter_path_str) ==
+                 processed_filters.end()) {
+            auto it = processed_filters.insert(filter_path_str).first;
+            filters_group
+                ->SubElement("Filter",
+                             XmlAttributes("Include", filter_path_str))
+                ->SubElement("UniqueIdentifier")
+                ->Text(MakeGuid(filter_path_str, kGuidSeedFilter));
+            filter_path_str = FindParentDir(&(*it)).as_string();
+            if (filter_path_str.empty())
+              break;
+          }
+          cl_item->SubElement("Filter")->Text(filter_path);
+        }
+      }
+    }
+  }
+
+  project.Text(files_out.str());
+}
+
+bool VisualStudioWriter::WriteSolutionFile(Err* err) {
+  SourceFile sln_file = build_settings_->build_dir().ResolveRelativeFile(
+      Value(nullptr, "all.sln"), err);
+  if (sln_file.is_null())
+    return false;
+
+  base::FilePath sln_path = build_settings_->GetFullPath(sln_file);
+
+  std::stringstream string_out;
+  WriteSolutionFileContents(string_out, sln_path.DirName());
+
+  // Only write the content to the file if it's different. That is
+  // both a performance optimization and more importantly, prevents
+  // Visual Studio from reloading the projects.
+  if (HasSameContent(string_out, sln_path))
+    return true;
+
+  std::string content = string_out.str();
+  int size = static_cast<int>(content.size());
+  if (base::WriteFile(sln_path, content.c_str(), size) != size) {
+    *err = Err(Location(), "Couldn't open all.sln for writing");
+    return false;
+  }
+
+  return true;
+}
+
+void VisualStudioWriter::WriteSolutionFileContents(
+    std::ostream& out,
+    const base::FilePath& solution_dir_path) {
+  out << "Microsoft Visual Studio Solution File, Format Version 12.00"
+      << std::endl;
+  out << "# Visual Studio 2015" << std::endl;
+
+  SourceDir solution_dir(FilePathToUTF8(solution_dir_path));
+  for (const SolutionEntry* folder : folders_) {
+    out << "Project(\"" << kGuidTypeFolder << "\") = \"(" << folder->name
+        << ")\", \"" << RebasePath(folder->path, solution_dir, "/") << "\", \""
+        << folder->guid << "\"" << std::endl;
+    out << "EndProject" << std::endl;
+  }
+
+  for (const SolutionEntry* project : projects_) {
+    out << "Project(\"" << kGuidTypeProject << "\") = \"" << project->name
+        << "\", \"" << RebasePath(project->path, solution_dir, "/") << "\", \""
+        << project->guid << "\"" << std::endl;
+    out << "EndProject" << std::endl;
+  }
+
+  out << "Global" << std::endl;
+
+  out << "\tGlobalSection(SolutionConfigurationPlatforms) = preSolution"
+      << std::endl;
+  const std::string config_mode =
+      std::string(is_debug_config_ ? "Debug" : "Release") + '|' +
+      config_platform_;
+  out << "\t\t" << config_mode << " = " << config_mode << std::endl;
+  out << "\tEndGlobalSection" << std::endl;
+
+  out << "\tGlobalSection(ProjectConfigurationPlatforms) = postSolution"
+      << std::endl;
+  for (const SolutionEntry* project : projects_) {
+    out << "\t\t" << project->guid << '.' << config_mode
+        << ".ActiveCfg = " << config_mode << std::endl;
+    out << "\t\t" << project->guid << '.' << config_mode
+        << ".Build.0 = " << config_mode << std::endl;
+  }
+  out << "\tEndGlobalSection" << std::endl;
+
+  out << "\tGlobalSection(SolutionProperties) = preSolution" << std::endl;
+  out << "\t\tHideSolutionNode = FALSE" << std::endl;
+  out << "\tEndGlobalSection" << std::endl;
+
+  out << "\tGlobalSection(NestedProjects) = preSolution" << std::endl;
+  for (const SolutionEntry* folder : folders_) {
+    if (folder->parent_folder) {
+      out << "\t\t" << folder->guid << " = " << folder->parent_folder->guid
+          << std::endl;
+    }
+  }
+  for (const SolutionEntry* project : projects_) {
+    out << "\t\t" << project->guid << " = " << project->parent_folder->guid
+        << std::endl;
+  }
+  out << "\tEndGlobalSection" << std::endl;
+
+  out << "EndGlobal" << std::endl;
+}
+
+void VisualStudioWriter::ResolveSolutionFolders() {
+  root_folder_path_.clear();
+
+  // Get all project directories. Create solution folder for each directory.
+  std::map<base::StringPiece, SolutionEntry*> processed_paths;
+  for (SolutionEntry* project : projects_) {
+    base::StringPiece folder_path = project->label_dir_path;
+    if (IsSlash(folder_path[folder_path.size() - 1]))
+      folder_path = folder_path.substr(0, folder_path.size() - 1);
+    auto it = processed_paths.find(folder_path);
+    if (it != processed_paths.end()) {
+      project->parent_folder = it->second;
+    } else {
+      std::string folder_path_str = folder_path.as_string();
+      SolutionEntry* folder = new SolutionEntry(
+          FindLastDirComponent(SourceDir(folder_path)).as_string(),
+          folder_path_str, MakeGuid(folder_path_str, kGuidSeedFolder));
+      folders_.push_back(folder);
+      project->parent_folder = folder;
+      processed_paths[folder_path] = folder;
+
+      if (root_folder_path_.empty()) {
+        root_folder_path_ = folder_path_str;
+      } else {
+        size_t common_prefix_len = 0;
+        size_t max_common_length =
+            std::min(root_folder_path_.size(), folder_path.size());
+        size_t i;
+        for (i = common_prefix_len; i < max_common_length; ++i) {
+          if (IsSlash(root_folder_path_[i]) && IsSlash(folder_path[i]))
+            common_prefix_len = i + 1;
+          else if (root_folder_path_[i] != folder_path[i])
+            break;
+        }
+        if (i == max_common_length)
+          common_prefix_len = max_common_length;
+        if (common_prefix_len < root_folder_path_.size()) {
+          if (IsSlash(root_folder_path_[common_prefix_len - 1]))
+            --common_prefix_len;
+          root_folder_path_ = root_folder_path_.substr(0, common_prefix_len);
+        }
+      }
+    }
+  }
+
+  // Create also all parent folders up to |root_folder_path_|.
+  SolutionEntries additional_folders;
+  for (SolutionEntry* folder : folders_) {
+    if (folder->path == root_folder_path_)
+      continue;
+
+    base::StringPiece parent_path;
+    while ((parent_path = FindParentDir(&folder->path)) != root_folder_path_) {
+      auto it = processed_paths.find(parent_path);
+      if (it != processed_paths.end()) {
+        folder = it->second;
+      } else {
+        folder = new SolutionEntry(
+            FindLastDirComponent(SourceDir(parent_path)).as_string(),
+            parent_path.as_string(),
+            MakeGuid(parent_path.as_string(), kGuidSeedFolder));
+        additional_folders.push_back(folder);
+        processed_paths[parent_path] = folder;
+      }
+    }
+  }
+  folders_.insert(folders_.end(), additional_folders.begin(),
+                  additional_folders.end());
+
+  // Sort folders by path.
+  std::sort(folders_.begin(), folders_.end(),
+            [](const SolutionEntry* a, const SolutionEntry* b) {
+              return a->path < b->path;
+            });
+
+  // Match subfolders with their parents. Since |folders_| are sorted by path we
+  // know that parent folder always precedes its children in vector.
+  SolutionEntries parents;
+  for (SolutionEntry* folder : folders_) {
+    while (!parents.empty()) {
+      if (base::StartsWith(folder->path, parents.back()->path,
+                           base::CompareCase::SENSITIVE)) {
+        folder->parent_folder = parents.back();
+        break;
+      } else {
+        parents.pop_back();
+      }
+    }
+    parents.push_back(folder);
+  }
+}
diff --git a/tools/gn/visual_studio_writer.h b/tools/gn/visual_studio_writer.h
new file mode 100644
index 0000000..5f54b3f
--- /dev/null
+++ b/tools/gn/visual_studio_writer.h
@@ -0,0 +1,98 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef TOOLS_GN_VISUAL_STUDIO_WRITER_H_
+#define TOOLS_GN_VISUAL_STUDIO_WRITER_H_
+
+#include <iosfwd>
+#include <string>
+#include <vector>
+
+#include "base/gtest_prod_util.h"
+#include "base/macros.h"
+
+namespace base {
+class FilePath;
+}
+
+class Builder;
+class BuildSettings;
+class Err;
+class Target;
+
+class VisualStudioWriter {
+ public:
+  // On failure will populate |err| and will return false.
+  static bool RunAndWriteFiles(const BuildSettings* build_settings,
+                               Builder* builder,
+                               Err* err);
+
+ private:
+  FRIEND_TEST_ALL_PREFIXES(VisualStudioWriterTest, ResolveSolutionFolders);
+  FRIEND_TEST_ALL_PREFIXES(VisualStudioWriterTest,
+                           ResolveSolutionFolders_AbsPath);
+
+  // Solution project or folder.
+  struct SolutionEntry {
+    SolutionEntry(const std::string& name,
+                  const std::string& path,
+                  const std::string& guid);
+
+    ~SolutionEntry();
+
+    // Entry name. For projects must be unique in the solution.
+    std::string name;
+    // Absolute project file or folder directory path.
+    std::string path;
+    // Absolute label dir path (only for projects).
+    std::string label_dir_path;
+    // GUID-like string.
+    std::string guid;
+    // Pointer to parent folder. nullptr if entry has no parent.
+    SolutionEntry* parent_folder;
+  };
+
+  using SolutionEntries = std::vector<SolutionEntry*>;
+
+  explicit VisualStudioWriter(const BuildSettings* build_settings);
+  ~VisualStudioWriter();
+
+  bool WriteProjectFiles(const Target* target, Err* err);
+  bool WriteProjectFileContents(std::ostream& out,
+                                const SolutionEntry& solution_project,
+                                const Target* target,
+                                Err* err);
+  void WriteFiltersFileContents(std::ostream& out, const Target* target);
+  bool WriteSolutionFile(Err* err);
+  void WriteSolutionFileContents(std::ostream& out,
+                                 const base::FilePath& solution_dir_path);
+
+  // Resolves all solution folders (parent folders for projects) into |folders_|
+  // and updates |root_folder_dir_|. Also sets |parent_folder| for |projects_|.
+  void ResolveSolutionFolders();
+
+  const BuildSettings* build_settings_;
+
+  // Indicates if project files are generated for Debug mode configuration.
+  bool is_debug_config_;
+
+  // Platform for projects configuration (Win32, x64).
+  std::string config_platform_;
+
+  // All projects contained by solution.
+  SolutionEntries projects_;
+
+  // Absolute root solution folder path.
+  std::string root_folder_path_;
+
+  // Folders for all solution projects.
+  SolutionEntries folders_;
+
+  // Semicolon-separated Windows SDK include directories.
+  std::string windows_kits_include_dirs_;
+
+  DISALLOW_COPY_AND_ASSIGN(VisualStudioWriter);
+};
+
+#endif  // TOOLS_GN_VISUAL_STUDIO_WRITER_H_
diff --git a/tools/gn/visual_studio_writer_unittest.cc b/tools/gn/visual_studio_writer_unittest.cc
new file mode 100644
index 0000000..e7fe6de
--- /dev/null
+++ b/tools/gn/visual_studio_writer_unittest.cc
@@ -0,0 +1,152 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "tools/gn/visual_studio_writer.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "tools/gn/test_with_scope.h"
+#include "tools/gn/visual_studio_utils.h"
+
+namespace {
+
+class VisualStudioWriterTest : public testing::Test {
+ protected:
+  TestWithScope setup_;
+};
+
+std::string MakeTestPath(const std::string& path) {
+#if defined(OS_WIN)
+  return "C:" + path;
+#else
+  return path;
+#endif
+}
+
+}  // namespace
+
+TEST_F(VisualStudioWriterTest, ResolveSolutionFolders) {
+  VisualStudioWriter writer(setup_.build_settings());
+
+  std::string path =
+      MakeTestPath("/foo/chromium/src/out/Debug/obj/base/base.vcxproj");
+  writer.projects_.push_back(new VisualStudioWriter::SolutionEntry(
+      "base", path, MakeGuid(path, "project")));
+  writer.projects_.back()->label_dir_path =
+      MakeTestPath("/foo/chromium/src/base");
+
+  path = MakeTestPath("/foo/chromium/src/out/Debug/obj/tools/gn/gn.vcxproj");
+  writer.projects_.push_back(new VisualStudioWriter::SolutionEntry(
+      "gn", path, MakeGuid(path, "project")));
+  writer.projects_.back()->label_dir_path =
+      MakeTestPath("/foo/chromium/src/tools/gn");
+
+  path = MakeTestPath("/foo/chromium/src/out/Debug/obj/chrome/chrome.vcxproj");
+  writer.projects_.push_back(new VisualStudioWriter::SolutionEntry(
+      "chrome", path, MakeGuid(path, "project")));
+  writer.projects_.back()->label_dir_path =
+      MakeTestPath("/foo/chromium/src/chrome");
+
+  path = MakeTestPath("/foo/chromium/src/out/Debug/obj/base/bar.vcxproj");
+  writer.projects_.push_back(new VisualStudioWriter::SolutionEntry(
+      "bar", path, MakeGuid(path, "project")));
+  writer.projects_.back()->label_dir_path =
+      MakeTestPath("/foo/chromium/src/base");
+
+  writer.ResolveSolutionFolders();
+
+  ASSERT_EQ(MakeTestPath("/foo/chromium/src"), writer.root_folder_path_);
+
+  ASSERT_EQ(4u, writer.folders_.size());
+
+  ASSERT_EQ("base", writer.folders_[0]->name);
+  ASSERT_EQ(MakeTestPath("/foo/chromium/src/base"), writer.folders_[0]->path);
+  ASSERT_EQ(nullptr, writer.folders_[0]->parent_folder);
+
+  ASSERT_EQ("chrome", writer.folders_[1]->name);
+  ASSERT_EQ(MakeTestPath("/foo/chromium/src/chrome"), writer.folders_[1]->path);
+  ASSERT_EQ(nullptr, writer.folders_[1]->parent_folder);
+
+  ASSERT_EQ("tools", writer.folders_[2]->name);
+  ASSERT_EQ(MakeTestPath("/foo/chromium/src/tools"), writer.folders_[2]->path);
+  ASSERT_EQ(nullptr, writer.folders_[2]->parent_folder);
+
+  ASSERT_EQ("gn", writer.folders_[3]->name);
+  ASSERT_EQ(MakeTestPath("/foo/chromium/src/tools/gn"),
+            writer.folders_[3]->path);
+  ASSERT_EQ(writer.folders_[2], writer.folders_[3]->parent_folder);
+
+  ASSERT_EQ(writer.folders_[0], writer.projects_[0]->parent_folder);
+  ASSERT_EQ(writer.folders_[3], writer.projects_[1]->parent_folder);
+  ASSERT_EQ(writer.folders_[1], writer.projects_[2]->parent_folder);
+  ASSERT_EQ(writer.folders_[0], writer.projects_[3]->parent_folder);
+}
+
+TEST_F(VisualStudioWriterTest, ResolveSolutionFolders_AbsPath) {
+  VisualStudioWriter writer(setup_.build_settings());
+
+  std::string path =
+      MakeTestPath("/foo/chromium/src/out/Debug/obj/base/base.vcxproj");
+  writer.projects_.push_back(new VisualStudioWriter::SolutionEntry(
+      "base", path, MakeGuid(path, "project")));
+  writer.projects_.back()->label_dir_path =
+      MakeTestPath("/foo/chromium/src/base");
+
+  path = MakeTestPath("/foo/chromium/src/out/Debug/obj/tools/gn/gn.vcxproj");
+  writer.projects_.push_back(new VisualStudioWriter::SolutionEntry(
+      "gn", path, MakeGuid(path, "project")));
+  writer.projects_.back()->label_dir_path =
+      MakeTestPath("/foo/chromium/src/tools/gn");
+
+  path = MakeTestPath(
+      "/foo/chromium/src/out/Debug/obj/ABS_PATH/C/foo/bar/bar.vcxproj");
+  writer.projects_.push_back(new VisualStudioWriter::SolutionEntry(
+      "bar", path, MakeGuid(path, "project")));
+  writer.projects_.back()->label_dir_path = MakeTestPath("/foo/bar");
+
+  path = MakeTestPath(
+      "/foo/chromium/src/out/Debug/obj/ABS_PATH/C/foo/bar/baz/baz.vcxproj");
+  writer.projects_.push_back(new VisualStudioWriter::SolutionEntry(
+      "baz", path, MakeGuid(path, "project")));
+  writer.projects_.back()->label_dir_path = MakeTestPath("/foo/bar/baz");
+
+  writer.ResolveSolutionFolders();
+
+  ASSERT_EQ(MakeTestPath("/foo"), writer.root_folder_path_);
+
+  ASSERT_EQ(7u, writer.folders_.size());
+
+  ASSERT_EQ("bar", writer.folders_[0]->name);
+  ASSERT_EQ(MakeTestPath("/foo/bar"), writer.folders_[0]->path);
+  ASSERT_EQ(nullptr, writer.folders_[0]->parent_folder);
+
+  ASSERT_EQ("baz", writer.folders_[1]->name);
+  ASSERT_EQ(MakeTestPath("/foo/bar/baz"), writer.folders_[1]->path);
+  ASSERT_EQ(writer.folders_[0], writer.folders_[1]->parent_folder);
+
+  ASSERT_EQ("chromium", writer.folders_[2]->name);
+  ASSERT_EQ(MakeTestPath("/foo/chromium"), writer.folders_[2]->path);
+  ASSERT_EQ(nullptr, writer.folders_[2]->parent_folder);
+
+  ASSERT_EQ("src", writer.folders_[3]->name);
+  ASSERT_EQ(MakeTestPath("/foo/chromium/src"), writer.folders_[3]->path);
+  ASSERT_EQ(writer.folders_[2], writer.folders_[3]->parent_folder);
+
+  ASSERT_EQ("base", writer.folders_[4]->name);
+  ASSERT_EQ(MakeTestPath("/foo/chromium/src/base"), writer.folders_[4]->path);
+  ASSERT_EQ(writer.folders_[3], writer.folders_[4]->parent_folder);
+
+  ASSERT_EQ("tools", writer.folders_[5]->name);
+  ASSERT_EQ(MakeTestPath("/foo/chromium/src/tools"), writer.folders_[5]->path);
+  ASSERT_EQ(writer.folders_[3], writer.folders_[5]->parent_folder);
+
+  ASSERT_EQ("gn", writer.folders_[6]->name);
+  ASSERT_EQ(MakeTestPath("/foo/chromium/src/tools/gn"),
+            writer.folders_[6]->path);
+  ASSERT_EQ(writer.folders_[5], writer.folders_[6]->parent_folder);
+
+  ASSERT_EQ(writer.folders_[4], writer.projects_[0]->parent_folder);
+  ASSERT_EQ(writer.folders_[6], writer.projects_[1]->parent_folder);
+  ASSERT_EQ(writer.folders_[0], writer.projects_[2]->parent_folder);
+  ASSERT_EQ(writer.folders_[1], writer.projects_[3]->parent_folder);
+}
diff --git a/tools/gn/xml_element_writer.cc b/tools/gn/xml_element_writer.cc
new file mode 100644
index 0000000..51e0488
--- /dev/null
+++ b/tools/gn/xml_element_writer.cc
@@ -0,0 +1,79 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "tools/gn/xml_element_writer.h"
+
+XmlAttributes::XmlAttributes() {}
+
+XmlAttributes::XmlAttributes(const base::StringPiece& attr_key,
+                             const base::StringPiece& attr_value) {
+  add(attr_key, attr_value);
+}
+
+XmlAttributes& XmlAttributes::add(const base::StringPiece& attr_key,
+                                  const base::StringPiece& attr_value) {
+  push_back(std::make_pair(attr_key, attr_value));
+  return *this;
+}
+
+XmlElementWriter::XmlElementWriter(std::ostream& out,
+                                   const std::string& tag,
+                                   const XmlAttributes& attributes)
+    : XmlElementWriter(out, tag, attributes, 0) {}
+
+XmlElementWriter::XmlElementWriter(std::ostream& out,
+                                   const std::string& tag,
+                                   const XmlAttributes& attributes,
+                                   int indent)
+    : out_(out),
+      tag_(tag),
+      indent_(indent),
+      opening_tag_finished_(false),
+      one_line_(true) {
+  out << std::string(indent, ' ') << '<' << tag;
+  for (auto attribute : attributes)
+    out << ' ' << attribute.first << "=\"" << attribute.second << '"';
+}
+
+XmlElementWriter::~XmlElementWriter() {
+  if (!opening_tag_finished_) {
+    out_ << "/>" << std::endl;
+  } else {
+    if (!one_line_)
+      out_ << std::string(indent_, ' ');
+    out_ << "</" << tag_ << '>' << std::endl;
+  }
+}
+
+void XmlElementWriter::Text(const base::StringPiece& content) {
+  StartContent(false);
+  out_ << content;
+}
+
+scoped_ptr<XmlElementWriter> XmlElementWriter::SubElement(
+    const std::string& tag) {
+  return SubElement(tag, XmlAttributes());
+}
+
+scoped_ptr<XmlElementWriter> XmlElementWriter::SubElement(
+    const std::string& tag,
+    const XmlAttributes& attributes) {
+  StartContent(true);
+  return make_scoped_ptr(
+      new XmlElementWriter(out_, tag, attributes, indent_ + 2));
+}
+
+std::ostream& XmlElementWriter::StartContent(bool start_new_line) {
+  if (!opening_tag_finished_) {
+    out_ << '>';
+    opening_tag_finished_ = true;
+
+    if (start_new_line && one_line_) {
+      out_ << std::endl;
+      one_line_ = false;
+    }
+  }
+
+  return out_;
+}
diff --git a/tools/gn/xml_element_writer.h b/tools/gn/xml_element_writer.h
new file mode 100644
index 0000000..186bd35
--- /dev/null
+++ b/tools/gn/xml_element_writer.h
@@ -0,0 +1,121 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef TOOLS_GN_XML_ELEMENT_WRITER_H_
+#define TOOLS_GN_XML_ELEMENT_WRITER_H_
+
+#include <iosfwd>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "base/macros.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/strings/string_piece.h"
+
+// Vector of XML attribute key-value pairs.
+class XmlAttributes
+    : public std::vector<std::pair<base::StringPiece, base::StringPiece>> {
+ public:
+  XmlAttributes();
+  XmlAttributes(const base::StringPiece& attr_key,
+                const base::StringPiece& attr_value);
+
+  XmlAttributes& add(const base::StringPiece& attr_key,
+                     const base::StringPiece& attr_value);
+};
+
+// Helper class for writing XML elements. New XML element is started in
+// XmlElementWriter constructor and ended in its destructor. XmlElementWriter
+// handles XML file formatting in order to produce human-readable document.
+class XmlElementWriter {
+ public:
+  // Starts new XML element. This constructor adds no indentation and is
+  // designed for XML root element.
+  XmlElementWriter(std::ostream& out,
+                   const std::string& tag,
+                   const XmlAttributes& attributes);
+  // Starts new XML element with specified indentation.
+  XmlElementWriter(std::ostream& out,
+                   const std::string& tag,
+                   const XmlAttributes& attributes,
+                   int indent);
+  // Starts new XML element with specified indentation. Specialized constructor
+  // that allows writting XML element with single attribute without copying
+  // attribute value.
+  template <class Writer>
+  XmlElementWriter(std::ostream& out,
+                   const std::string& tag,
+                   const std::string& attribute_name,
+                   const Writer& attribute_value_writer,
+                   int indent);
+  // Ends XML element. All sub-elements should be ended at this point.
+  ~XmlElementWriter();
+
+  // Writes arbitrary XML element text.
+  void Text(const base::StringPiece& content);
+
+  // Starts new XML sub-element. Caller must ensure that parent element outlives
+  // its children.
+  scoped_ptr<XmlElementWriter> SubElement(const std::string& tag);
+  scoped_ptr<XmlElementWriter> SubElement(const std::string& tag,
+                                          const XmlAttributes& attributes);
+  template <class Writer>
+  scoped_ptr<XmlElementWriter> SubElement(const std::string& tag,
+                                          const std::string& attribute_name,
+                                          const Writer& attribute_value_writer);
+
+  // Finishes opening tag if it isn't finished yet and optionally starts new
+  // document line. Returns the stream where XML element content can be written.
+  // This is an alternative to Text() and SubElement() methods.
+  std::ostream& StartContent(bool start_new_line);
+
+ private:
+  // Output stream. XmlElementWriter objects for XML element and its
+  // sub-elements share the same output stream.
+  std::ostream& out_;
+
+  // XML element tag name.
+  std::string tag_;
+
+  // XML element indentation in the document.
+  int indent_;
+
+  // Flag indicating if opening tag is finished with '>' character already.
+  bool opening_tag_finished_;
+
+  // Flag indicating if XML element should be written in one document line.
+  bool one_line_;
+
+  DISALLOW_COPY_AND_ASSIGN(XmlElementWriter);
+};
+
+template <class Writer>
+XmlElementWriter::XmlElementWriter(std::ostream& out,
+                                   const std::string& tag,
+                                   const std::string& attribute_name,
+                                   const Writer& attribute_value_writer,
+                                   int indent)
+    : out_(out),
+      tag_(tag),
+      indent_(indent),
+      opening_tag_finished_(false),
+      one_line_(true) {
+  out << std::string(indent, ' ') << '<' << tag;
+  out << ' ' << attribute_name << "=\"";
+  attribute_value_writer(out);
+  out << '\"';
+}
+
+template <class Writer>
+scoped_ptr<XmlElementWriter> XmlElementWriter::SubElement(
+    const std::string& tag,
+    const std::string& attribute_name,
+    const Writer& attribute_value_writer) {
+  StartContent(true);
+  return make_scoped_ptr(new XmlElementWriter(
+      out_, tag, attribute_name, attribute_value_writer, indent_ + 2));
+}
+
+#endif  // TOOLS_GN_XML_ELEMENT_WRITER_H_
diff --git a/tools/gn/xml_element_writer_unittest.cc b/tools/gn/xml_element_writer_unittest.cc
new file mode 100644
index 0000000..6702061
--- /dev/null
+++ b/tools/gn/xml_element_writer_unittest.cc
@@ -0,0 +1,86 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "tools/gn/xml_element_writer.h"
+
+#include <sstream>
+
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+class MockValueWriter {
+ public:
+  explicit MockValueWriter(const std::string& value) : value_(value) {}
+  void operator()(std::ostream& out) const { out << value_; }
+
+ private:
+  std::string value_;
+};
+
+}  // namespace
+
+TEST(XmlElementWriter, EmptyElement) {
+  std::ostringstream out;
+  { XmlElementWriter writer(out, "foo", XmlAttributes()); }
+  EXPECT_EQ("<foo/>\n", out.str());
+
+  std::ostringstream out_attr;
+  {
+    XmlElementWriter writer(out_attr, "foo",
+                            XmlAttributes("bar", "abc").add("baz", "123"));
+  }
+  EXPECT_EQ("<foo bar=\"abc\" baz=\"123\"/>\n", out_attr.str());
+
+  std::ostringstream out_indent;
+  {
+    XmlElementWriter writer(out_indent, "foo", XmlAttributes("bar", "baz"), 2);
+  }
+  EXPECT_EQ("  <foo bar=\"baz\"/>\n", out_indent.str());
+
+  std::ostringstream out_writer;
+  {
+    XmlElementWriter writer(out_writer, "foo", "bar", MockValueWriter("baz"),
+                            2);
+  }
+  EXPECT_EQ("  <foo bar=\"baz\"/>\n", out_writer.str());
+}
+
+TEST(XmlElementWriter, ElementWithText) {
+  std::ostringstream out;
+  {
+    XmlElementWriter writer(out, "foo", XmlAttributes("bar", "baz"));
+    writer.Text("Hello world!");
+  }
+  EXPECT_EQ("<foo bar=\"baz\">Hello world!</foo>\n", out.str());
+}
+
+TEST(XmlElementWriter, SubElements) {
+  std::ostringstream out;
+  {
+    XmlElementWriter writer(out, "root", XmlAttributes("aaa", "000"));
+    writer.SubElement("foo", XmlAttributes());
+    writer.SubElement("bar", XmlAttributes("bbb", "111"))->Text("hello");
+    writer.SubElement("baz", "ccc", MockValueWriter("222"))
+        ->SubElement("grandchild");
+  }
+  std::string expected =
+      "<root aaa=\"000\">\n"
+      "  <foo/>\n"
+      "  <bar bbb=\"111\">hello</bar>\n"
+      "  <baz ccc=\"222\">\n"
+      "    <grandchild/>\n"
+      "  </baz>\n"
+      "</root>\n";
+  EXPECT_EQ(expected, out.str());
+}
+
+TEST(XmlElementWriter, StartContent) {
+  std::ostringstream out;
+  {
+    XmlElementWriter writer(out, "foo", XmlAttributes("bar", "baz"));
+    writer.StartContent(false) << "Hello world!";
+  }
+  EXPECT_EQ("<foo bar=\"baz\">Hello world!</foo>\n", out.str());
+}