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());
+}