Add "create_bundle" target in order to support bundle with gn.

The "create_bundle" target recursively collect information from all the
"bundle_data" targets it depends and create a bundle from them. This is
configurable in many way to support all kinds of bundles on OS X or iOS.

Design: https://docs.google.com/document/d/1bKh57hg6TSBsEmeh0zWpOO5SVGA2INu-D3FGgsyICzk/view

BUG=297668

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

Cr-Original-Commit-Position: refs/heads/master@{#380454}
Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src
Cr-Mirrored-Commit: 1232aaeb49f5ec11e0e9fcc527bfa96de21904ca
diff --git a/tools/gn/BUILD.gn b/tools/gn/BUILD.gn
index b27ae70..37bce8d 100644
--- a/tools/gn/BUILD.gn
+++ b/tools/gn/BUILD.gn
@@ -24,8 +24,12 @@
     "builder.h",
     "builder_record.cc",
     "builder_record.h",
+    "bundle_data.cc",
+    "bundle_data.h",
     "bundle_data_target_generator.cc",
     "bundle_data_target_generator.h",
+    "bundle_file_rule.cc",
+    "bundle_file_rule.h",
     "c_include_iterator.cc",
     "c_include_iterator.h",
     "command_args.cc",
@@ -50,6 +54,8 @@
     "config_values_generator.h",
     "copy_target_generator.cc",
     "copy_target_generator.h",
+    "create_bundle_target_generator.cc",
+    "create_bundle_target_generator.h",
     "deps_iterator.cc",
     "deps_iterator.h",
     "eclipse_writer.cc",
@@ -116,6 +122,8 @@
     "ninja_bundle_data_target_writer.h",
     "ninja_copy_target_writer.cc",
     "ninja_copy_target_writer.h",
+    "ninja_create_bundle_target_writer.cc",
+    "ninja_create_bundle_target_writer.h",
     "ninja_group_target_writer.cc",
     "ninja_group_target_writer.h",
     "ninja_target_writer.cc",
@@ -278,6 +286,7 @@
     "ninja_action_target_writer_unittest.cc",
     "ninja_binary_target_writer_unittest.cc",
     "ninja_copy_target_writer_unittest.cc",
+    "ninja_create_bundle_target_writer_unittest.cc",
     "ninja_group_target_writer_unittest.cc",
     "ninja_target_writer_unittest.cc",
     "ninja_toolchain_writer_unittest.cc",
diff --git a/tools/gn/bundle_data.cc b/tools/gn/bundle_data.cc
new file mode 100644
index 0000000..e22612c
--- /dev/null
+++ b/tools/gn/bundle_data.cc
@@ -0,0 +1,100 @@
+// 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/bundle_data.h"
+
+#include "base/logging.h"
+#include "tools/gn/output_file.h"
+#include "tools/gn/settings.h"
+#include "tools/gn/target.h"
+
+namespace {
+
+// Return directory of |path| without the trailing directory separator.
+base::StringPiece FindDirNoTrailingSeparator(const base::StringPiece& path) {
+  base::StringPiece::size_type pos = path.find_last_of("/\\");
+  if (pos == base::StringPiece::npos)
+    return base::StringPiece();
+  return base::StringPiece(path.data(), pos);
+}
+
+}  // namespace
+
+bool IsSourceFileFromAssetCatalog(const SourceFile& source,
+                                  SourceFile* asset_catalog) {
+  // Check that the file matches the following pattern:
+  //    .*\.xcassets/[^/]*\.imageset/[^/]*
+  base::StringPiece dir;
+  dir = FindDirNoTrailingSeparator(source.value());
+  if (!dir.ends_with(".imageset"))
+    return false;
+  dir = FindDirNoTrailingSeparator(dir);
+  if (!dir.ends_with(".xcassets"))
+    return false;
+  if (asset_catalog) {
+    std::string asset_catalog_path = dir.as_string();
+    *asset_catalog = SourceFile(SourceFile::SWAP_IN, &asset_catalog_path);
+  }
+  return true;
+}
+
+BundleData::BundleData() {}
+
+BundleData::~BundleData() {}
+
+void BundleData::AddFileRuleFromTarget(const Target* target) {
+  DCHECK_EQ(target->output_type(), Target::BUNDLE_DATA);
+
+  std::vector<SourceFile> file_rule_sources;
+  for (const SourceFile& source_file : target->sources()) {
+    if (IsSourceFileFromAssetCatalog(source_file, nullptr)) {
+      asset_catalog_sources_.push_back(source_file);
+    } else {
+      file_rule_sources.push_back(source_file);
+    }
+  }
+
+  if (!file_rule_sources.empty()) {
+    DCHECK_EQ(target->action_values().outputs().list().size(), 1u);
+    file_rules_.push_back(BundleFileRule(
+        file_rule_sources, target->action_values().outputs().list()[0]));
+  }
+}
+
+void BundleData::GetSourceFiles(std::vector<SourceFile>* sources) const {
+  for (const BundleFileRule& file_rule : file_rules_) {
+    sources->insert(sources->end(), file_rule.sources().begin(),
+                    file_rule.sources().end());
+  }
+  sources->insert(sources->end(), asset_catalog_sources_.begin(),
+                  asset_catalog_sources_.end());
+}
+
+void BundleData::GetOutputFiles(const Settings* settings,
+                                std::vector<OutputFile>* outputs) const {
+  std::vector<SourceFile> outputs_as_sources;
+  GetOutputsAsSourceFiles(settings, &outputs_as_sources);
+  for (const SourceFile& source_file : outputs_as_sources)
+    outputs->push_back(OutputFile(settings->build_settings(), source_file));
+}
+
+void BundleData::GetOutputsAsSourceFiles(
+    const Settings* settings,
+    std::vector<SourceFile>* outputs_as_source) const {
+  for (const BundleFileRule& file_rule : file_rules_) {
+    for (const SourceFile& source : file_rule.sources()) {
+      outputs_as_source->push_back(
+          file_rule.ApplyPatternToSource(settings, *this, source));
+    }
+  }
+
+  if (!asset_catalog_sources_.empty())
+    outputs_as_source->push_back(GetCompiledAssetCatalogPath());
+}
+
+SourceFile BundleData::GetCompiledAssetCatalogPath() const {
+  DCHECK(!asset_catalog_sources_.empty());
+  std::string assets_car_path = resources_dir_ + "/Assets.car";
+  return SourceFile(SourceFile::SWAP_IN, &assets_car_path);
+}
diff --git a/tools/gn/bundle_data.h b/tools/gn/bundle_data.h
new file mode 100644
index 0000000..9d1df13
--- /dev/null
+++ b/tools/gn/bundle_data.h
@@ -0,0 +1,91 @@
+// 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_BUNDLE_DATA_H_
+#define TOOLS_GN_BUNDLE_DATA_H_
+
+#include <string>
+#include <vector>
+
+#include "tools/gn/bundle_file_rule.h"
+
+class OutputFile;
+class SourceFile;
+class Settings;
+class Target;
+
+// Returns true if |source| correspond to the path of a file in an asset
+// catalog. If defined |asset_catalog| is set to its path.
+//
+// An asset catalog is an OS X bundle with the ".xcassets" extension. It
+// contains one directory per assets each of them with the ".imageset"
+// extension.
+//
+// All asset catalogs are compiled by Xcode into single Assets.car file as
+// part of the creation of an application or framework bundle. BundleData
+// emulates this with the "compile_xcassets" tool.
+bool IsSourceFileFromAssetCatalog(const SourceFile& source,
+                                  SourceFile* asset_catalog);
+
+// BundleData holds the information required by "create_bundle" target.
+class BundleData {
+ public:
+  BundleData();
+  ~BundleData();
+
+  // Extracts the information required from a "bundle_data" target.
+  void AddFileRuleFromTarget(const Target* target);
+
+  // Returns the list of inputs.
+  void GetSourceFiles(std::vector<SourceFile>* sources) const;
+
+  // Returns the list of outputs.
+  void GetOutputFiles(const Settings* settings,
+                      std::vector<OutputFile>* outputs) const;
+
+  // Returns the list of outputs as SourceFile.
+  void GetOutputsAsSourceFiles(
+      const Settings* settings,
+      std::vector<SourceFile>* outputs_as_source) const;
+
+  // Returns the path to the compiled asset catalog. Only valid if
+  // asset_catalog_sources() is not empty.
+  SourceFile GetCompiledAssetCatalogPath() const;
+
+  // Returns the list of inputs for the compilation of the asset catalog.
+  std::vector<SourceFile>& asset_catalog_sources() {
+    return asset_catalog_sources_;
+  }
+  const std::vector<SourceFile>& asset_catalog_sources() const {
+    return asset_catalog_sources_;
+  }
+
+  std::vector<BundleFileRule>& file_rules() { return file_rules_; }
+  const std::vector<BundleFileRule>& file_rules() const { return file_rules_; }
+
+  std::string& root_dir() { return root_dir_; }
+  const std::string& root_dir() const { return root_dir_; }
+
+  std::string& resources_dir() { return resources_dir_; }
+  const std::string& resources_dir() const { return resources_dir_; }
+
+  std::string& executable_dir() { return executable_dir_; }
+  const std::string& executable_dir() const { return executable_dir_; }
+
+  std::string& plugins_dir() { return plugins_dir_; }
+  const std::string& plugins_dir() const { return plugins_dir_; }
+
+ private:
+  std::vector<SourceFile> asset_catalog_sources_;
+  std::vector<BundleFileRule> file_rules_;
+
+  // All those values are subdirectories relative to root_build_dir, and apart
+  // from root_dir, they are either equal to root_dir_ or subdirectories of it.
+  std::string root_dir_;
+  std::string resources_dir_;
+  std::string executable_dir_;
+  std::string plugins_dir_;
+};
+
+#endif  // TOOLS_GN_BUNDLE_DATA_H_
diff --git a/tools/gn/bundle_file_rule.cc b/tools/gn/bundle_file_rule.cc
new file mode 100644
index 0000000..0f42c8f
--- /dev/null
+++ b/tools/gn/bundle_file_rule.cc
@@ -0,0 +1,57 @@
+// 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/bundle_file_rule.h"
+
+#include "tools/gn/output_file.h"
+#include "tools/gn/settings.h"
+#include "tools/gn/substitution_pattern.h"
+#include "tools/gn/substitution_writer.h"
+#include "tools/gn/target.h"
+
+BundleFileRule::BundleFileRule(const std::vector<SourceFile> sources,
+                               const SubstitutionPattern& pattern)
+    : sources_(sources), pattern_(pattern) {}
+
+BundleFileRule::~BundleFileRule() {}
+
+SourceFile BundleFileRule::ApplyPatternToSource(
+    const Settings* settings,
+    const BundleData& bundle_data,
+    const SourceFile& source_file) const {
+  std::string output_path;
+  for (const auto& subrange : pattern_.ranges()) {
+    switch (subrange.type) {
+      case SUBSTITUTION_LITERAL:
+        output_path.append(subrange.literal);
+        break;
+      case SUBSTITUTION_BUNDLE_ROOT_DIR:
+        output_path.append(bundle_data.root_dir());
+        break;
+      case SUBSTITUTION_BUNDLE_RESOURCES_DIR:
+        output_path.append(bundle_data.resources_dir());
+        break;
+      case SUBSTITUTION_BUNDLE_EXECUTABLE_DIR:
+        output_path.append(bundle_data.executable_dir());
+        break;
+      case SUBSTITUTION_BUNDLE_PLUGINS_DIR:
+        output_path.append(bundle_data.plugins_dir());
+        break;
+      default:
+        output_path.append(SubstitutionWriter::GetSourceSubstitution(
+            settings, source_file, subrange.type,
+            SubstitutionWriter::OUTPUT_ABSOLUTE, SourceDir()));
+        break;
+    }
+  }
+  return SourceFile(SourceFile::SWAP_IN, &output_path);
+}
+
+OutputFile BundleFileRule::ApplyPatternToSourceAsOutputFile(
+    const Settings* settings,
+    const BundleData& bundle_data,
+    const SourceFile& source_file) const {
+  return OutputFile(settings->build_settings(),
+                    ApplyPatternToSource(settings, bundle_data, source_file));
+}
diff --git a/tools/gn/bundle_file_rule.h b/tools/gn/bundle_file_rule.h
new file mode 100644
index 0000000..3669cd2
--- /dev/null
+++ b/tools/gn/bundle_file_rule.h
@@ -0,0 +1,43 @@
+// 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_BUNDLE_FILE_RULE_H_
+#define TOOLS_GN_BUNDLE_FILE_RULE_H_
+
+#include <vector>
+
+#include "tools/gn/source_file.h"
+#include "tools/gn/substitution_pattern.h"
+
+class BundleData;
+class Settings;
+class SourceFile;
+class OutputFile;
+
+// BundleFileRule contains the information found in a "bundle_data" target.
+class BundleFileRule {
+ public:
+  BundleFileRule(const std::vector<SourceFile> sources,
+                 const SubstitutionPattern& pattern);
+  ~BundleFileRule();
+
+  // Applies the substitution pattern to a source file, returning the result
+  // as either a SourceFile or an OutputFile.
+  SourceFile ApplyPatternToSource(const Settings* settings,
+                                  const BundleData& bundle_data,
+                                  const SourceFile& source_file) const;
+  OutputFile ApplyPatternToSourceAsOutputFile(
+      const Settings* settings,
+      const BundleData& bundle_data,
+      const SourceFile& source_file) const;
+
+  // Returns the list of SourceFiles.
+  const std::vector<SourceFile>& sources() const { return sources_; }
+
+ private:
+  std::vector<SourceFile> sources_;
+  SubstitutionPattern pattern_;
+};
+
+#endif  // TOOLS_GN_BUNDLE_FILE_RULE_H_
diff --git a/tools/gn/command_desc.cc b/tools/gn/command_desc.cc
index 801055b..15efe77 100644
--- a/tools/gn/command_desc.cc
+++ b/tools/gn/command_desc.cc
@@ -327,6 +327,11 @@
     for (const auto& elem : target->action_values().outputs().list()) {
       OutputString("  " + elem.AsString() + "\n");
     }
+  } else if (target->output_type() == Target::CREATE_BUNDLE) {
+    std::vector<SourceFile> output_files;
+    target->bundle_data().GetOutputsAsSourceFiles(target->settings(),
+                                                  &output_files);
+    PrintFileList(output_files, "", true, false);
   } else {
     const SubstitutionList& outputs = target->action_values().outputs();
     if (!outputs.required_types().empty()) {
@@ -685,7 +690,8 @@
     target->output_type() != Target::COPY_FILES &&
     target->output_type() != Target::ACTION &&
     target->output_type() != Target::ACTION_FOREACH &&
-    target->output_type() != Target::BUNDLE_DATA;
+    target->output_type() != Target::BUNDLE_DATA &&
+    target->output_type() != Target::CREATE_BUNDLE;
 
   // Generally we only want to display toolchains on labels when the toolchain
   // is different than the default one for this target (which we always print
@@ -739,7 +745,8 @@
 
   if (target->output_type() == Target::ACTION ||
       target->output_type() == Target::ACTION_FOREACH ||
-      target->output_type() == Target::COPY_FILES) {
+      target->output_type() == Target::COPY_FILES ||
+      target->output_type() == Target::CREATE_BUNDLE) {
     PrintOutputs(target, true);
   }
 
diff --git a/tools/gn/create_bundle_target_generator.cc b/tools/gn/create_bundle_target_generator.cc
new file mode 100644
index 0000000..206a918
--- /dev/null
+++ b/tools/gn/create_bundle_target_generator.cc
@@ -0,0 +1,69 @@
+// 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/create_bundle_target_generator.h"
+
+#include "tools/gn/filesystem_utils.h"
+#include "tools/gn/parse_tree.h"
+#include "tools/gn/scope.h"
+#include "tools/gn/substitution_type.h"
+#include "tools/gn/target.h"
+#include "tools/gn/value.h"
+#include "tools/gn/variables.h"
+
+CreateBundleTargetGenerator::CreateBundleTargetGenerator(
+    Target* target,
+    Scope* scope,
+    const FunctionCallNode* function_call,
+    Err* err)
+    : TargetGenerator(target, scope, function_call, err) {}
+
+CreateBundleTargetGenerator::~CreateBundleTargetGenerator() {}
+
+void CreateBundleTargetGenerator::DoRun() {
+  target_->set_output_type(Target::CREATE_BUNDLE);
+
+  BundleData& bundle_data = target_->bundle_data();
+  if (!GetBundleDir(std::string(),
+                    variables::kBundleRootDir,
+                    &bundle_data.root_dir()))
+    return;
+  if (!GetBundleDir(bundle_data.root_dir(),
+                    variables::kBundleResourcesDir,
+                    &bundle_data.resources_dir()))
+    return;
+  if (!GetBundleDir(bundle_data.root_dir(),
+                    variables::kBundleExecutableDir,
+                    &bundle_data.executable_dir()))
+    return;
+  if (!GetBundleDir(bundle_data.root_dir(),
+                    variables::kBundlePlugInsDir,
+                    &bundle_data.plugins_dir()))
+    return;
+}
+
+bool CreateBundleTargetGenerator::GetBundleDir(
+    const std::string& bundle_root_dir,
+    const base::StringPiece& name,
+    std::string* bundle_dir) {
+  const Value* value = scope_->GetValue(name, true);
+  if (!value)
+    return true;
+  if (!value->VerifyTypeIs(Value::STRING, err_))
+    return false;
+  const std::string& str = value->string_value();
+  if (!EnsureStringIsInOutputDir(GetBuildSettings()->build_dir(), str,
+                                 value->origin(), err_))
+    return false;
+  if (str != bundle_root_dir &&
+      !IsStringInOutputDir(SourceDir(bundle_root_dir), str)) {
+    *err_ = Err(value->origin(), "Path is not in bundle root dir.",
+        "The given file should be in the bundle root directory or below.\n"
+        "Normally you would do \"$bundle_root_dir/foo\". I interpreted this\n"
+        "as \"" + str + "\".");
+    return false;
+  }
+  bundle_dir->assign(value->string_value());
+  return true;
+}
diff --git a/tools/gn/create_bundle_target_generator.h b/tools/gn/create_bundle_target_generator.h
new file mode 100644
index 0000000..db82b8a
--- /dev/null
+++ b/tools/gn/create_bundle_target_generator.h
@@ -0,0 +1,31 @@
+// 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_CREATE_BUNDLE_TARGET_GENERATOR_H_
+#define TOOLS_GN_CREATE_BUNDLE_TARGET_GENERATOR_H_
+
+#include "base/macros.h"
+#include "tools/gn/target_generator.h"
+
+// Populates a Target with the values from a create_bundle rule.
+class CreateBundleTargetGenerator : public TargetGenerator {
+ public:
+  CreateBundleTargetGenerator(Target* target,
+                              Scope* scope,
+                              const FunctionCallNode* function_call,
+                              Err* err);
+  ~CreateBundleTargetGenerator() override;
+
+ protected:
+  void DoRun() override;
+
+ private:
+  bool GetBundleDir(const std::string& bundle_root_dir,
+                    const base::StringPiece& name,
+                    std::string* bundle_dir);
+
+  DISALLOW_COPY_AND_ASSIGN(CreateBundleTargetGenerator);
+};
+
+#endif  // TOOLS_GN_CREATE_BUNDLE_TARGET_GENERATOR_H_
diff --git a/tools/gn/docs/reference.md b/tools/gn/docs/reference.md
index 40da46b..6edacfc 100644
--- a/tools/gn/docs/reference.md
+++ b/tools/gn/docs/reference.md
@@ -1056,6 +1056,65 @@
 
 
 ```
+## **bundle_data**: [iOS/OS X] Declare a target without output.
+
+```
+  This target type allows to declare data that is required at runtime.
+  It is used to inform "create_bundle" targets of the files to copy
+  into generated bundle, see "gn help create_bundle" for help.
+
+  The target must define a list of files as "sources" and a single
+  "outputs". If there are multiple files, source expansions must be
+  used to express the output. The output must reference a file inside
+  of {{bundle_root_dir}}.
+
+  This target can be used on all platforms though it is designed only to
+  generate iOS/OS X bundle. In cross-platform projects, it is advised to
+  put it behind iOS/Mac conditionals.
+
+  See "gn help create_bundle" for more information.
+
+```
+
+### **Variables**
+
+```
+  sources*, outputs*, deps, data_deps, public_deps, visibility
+  * = required
+
+```
+
+### **Examples**
+
+```
+  bundle_data("icudata") {
+    sources = [ "sources/data/in/icudtl.dat" ]
+    outputs = [ "{{bundle_resources_dir}}/{{source_file_part}}" ]
+  }
+
+  bundle_data("base_unittests_bundle_data]") {
+    sources = [ "test/data" ]
+    outputs = [
+      "{{bundle_resources_dir}}/{{source_root_relative_dir}}/" +
+          "{{source_file_part}}"
+    ]
+  }
+
+  bundle_data("material_typography_bundle_data") {
+    sources = [
+      "src/MaterialTypography.bundle/Roboto-Bold.ttf",
+      "src/MaterialTypography.bundle/Roboto-Italic.ttf",
+      "src/MaterialTypography.bundle/Roboto-Regular.ttf",
+      "src/MaterialTypography.bundle/Roboto-Thin.ttf",
+    ]
+    outputs = [
+      "{{bundle_resources_dir}}/MaterialTypography.bundle/"
+          "{{source_file_part}}"
+    ]
+  }
+
+
+```
 ## **config**: Defines a configuration object.
 
 ```
@@ -1154,6 +1213,103 @@
 
 
 ```
+## **create_bundle**: [iOS/OS X] Build an OS X / iOS bundle.
+
+```
+  This target generates an iOS/OS X bundle (which is a directory with a
+  well-know structure). This target does not define any sources, instead
+  they are computed from all "bundle_data" target this one depends on
+  transitively (the recursion stops at "create_bundle" targets).
+
+  The "bundle_*_dir" properties must be defined. They will be used for
+  the expansion of {{bundle_*_dir}} rules in "bundle_data" outputs.
+
+  This target can be used on all platforms though it is designed only to
+  generate iOS/OS X bundle. In cross-platform projects, it is advised to
+  put it behind iOS/Mac conditionals.
+
+```
+
+### **Variables**
+
+```
+  bundle_root_dir*, bundle_resources_dir*, bundle_executable_dir*,
+  bundle_plugins_dir*, deps, data_deps, public_deps, visibility
+  * = required
+
+```
+
+### **Example**
+
+```
+  # Defines a template to create an application. On most platform, this
+  # is just an alias for an "executable" target, but on iOS/OS X, it
+  # builds an application bundle.
+  template("app") {
+    if (!is_ios && !is_mac) {
+      executable(target_name) {
+        forward_variables_from(invoker, "*")
+      }
+    } else {
+      app_name = target_name
+      gen_path = target_gen_dir
+
+      action("${app_name}_generate_info_plist") {
+        script = [ "//build/ios/ios_gen_plist.py" ]
+        sources = [ "templates/Info.plist" ]
+        outputs = [ "$gen_path/Info.plist" ]
+        args = rebase_path(sources, root_build_dir) +
+               rebase_path(outputs, root_build_dir)
+      }
+
+      bundle_data("${app_name}_bundle_info_plist") {
+        deps = [ ":${app_name}_generate_info_plist" ]
+        sources = [ "$gen_path/Info.plist" ]
+        outputs = [ "{{bundle_root_dir}}/Info.plist" ]
+      }
+
+      executable("${app_name}_generate_executable") {
+        forward_variables_from(invoker, "*", [
+                                                "output_name",
+                                                "visibility",
+                                               ])
+        output_name =
+            rebase_path("$gen_path/$app_name", root_build_dir)
+      }
+
+      bundle_data("${app_name}_bundle_executable") {
+        deps = [ ":${app_name}_generate_executable" ]
+        sources = [ "$gen_path/$app_name" ]
+        outputs = [ "{{bundle_executable_dir}}/$app_name" ]
+      }
+
+      create_bundle("${app_name}.app") {
+        deps = [
+          ":${app_name}_bundle_executable",
+          ":${app_name}_bundle_info_plist",
+        ]
+        if (is_ios) {
+          bundle_root_dir = "${root_build_dir}/$target_name"
+          bundle_resources_dir = bundle_root_dir
+          bundle_executable_dir = bundle_root_dir
+          bundle_plugins_dir = bundle_root_dir + "/Plugins"
+        } else {
+          bundle_root_dir = "${root_build_dir}/target_name/Contents"
+          bundle_resources_dir = bundle_root_dir + "/Resources"
+          bundle_executable_dir = bundle_root_dir + "/MacOS"
+          bundle_plugins_dir = bundle_root_dir + "/Plugins"
+        }
+      }
+
+      group(target_name) {
+        forward_variables_from(invoker, ["visibility"])
+        deps = [ ":${app_name}.app" ]
+      }
+    }
+  }
+
+
+```
 ## **declare_args**: Declare build arguments.
 
 ```
@@ -2444,6 +2600,10 @@
       "stamp": Tool for creating stamp files
       "copy": Tool to copy files.
 
+    Platform specific tools:
+      "copy_bundle_data": [iOS, OS X] Tool to copy files in a bundle.
+      "compile_xcassets": [iOS, OS X] Tool to compile asset catalogs.
+
 ```
 
 ### **Tool variables**
@@ -2759,6 +2919,18 @@
   {{source}} which is the source of the copy. The stamp tool allows
   only the common tool substitutions.
 
+  The copy_bundle_data and compile_xcassets tools only allows the common
+  tool substitutions. Both tools are required to create iOS/OS X bundles
+  and need only be defined on those platforms.
+
+  The copy_bundle_data tool will be called with one source and needs to
+  copy (optionally optimizing the data representation) to its output. It
+  may be called with a directory as input and it needs to be recursively
+  copied.
+
+  The compile_xcassets tool will be called with one or more source (each
+  an asset catalog) that needs to be compiled to a single output.
+
 ```
 
 ### **Separate linking and dependencies for shared libraries**
@@ -3449,6 +3621,74 @@
 
 
 ```
+## **bundle_executable_dir**: Expansion of {{bundle_executable_dir}} in create_bundle.
+
+```
+  A string corresponding to a path in $root_build_dir.
+
+  This string is used by the "create_bundle" target to expand the
+  {{bundle_executable_dir}} of the "bundle_data" target it depends on.
+  This must correspond to a path under "bundle_root_dir".
+
+  See "gn help bundle_root_dir" for examples.
+
+
+```
+## **bundle_plugins_dir**: Expansion of {{bundle_plugins_dir}} in create_bundle.
+
+```
+  A string corresponding to a path in $root_build_dir.
+
+  This string is used by the "create_bundle" target to expand the
+  {{bundle_plugins_dir}} of the "bundle_data" target it depends on.
+  This must correspond to a path under "bundle_root_dir".
+
+  See "gn help bundle_root_dir" for examples.
+
+
+```
+## **bundle_resources_dir**: Expansion of {{bundle_resources_dir}} in create_bundle.
+
+```
+  A string corresponding to a path in $root_build_dir.
+
+  This string is used by the "create_bundle" target to expand the
+  {{bundle_resources_dir}} of the "bundle_data" target it depends on.
+  This must correspond to a path under "bundle_root_dir".
+
+  See "gn help bundle_root_dir" for examples.
+
+
+```
+## **bundle_root_dir**: Expansion of {{bundle_root_dir}} in create_bundle.
+
+```
+  A string corresponding to a path in root_build_dir.
+
+  This string is used by the "create_bundle" target to expand the
+  {{bundle_root_dir}} of the "bundle_data" target it depends on.
+  This must correspond to a path under root_build_dir.
+
+```
+
+### **Example**
+
+```
+  bundle_data("info_plist") {
+    sources = [ "Info.plist" ]
+    outputs = [ "{{bundle_root_dir}}/Info.plist" ]
+  }
+
+  create_bundle("doom_melon.app") {
+    deps = [ ":info_plist" ]
+    bundle_root_dir = root_build_dir + "/doom_melon.app/Contents"
+    bundle_resources_dir = bundle_root_dir + "/Resources"
+    bundle_executable_dir = bundle_root_dir + "/MacOS"
+    bundle_plugins_dir = bundle_root_dir + "/PlugIns"
+  }
+
+
+```
 ## **cflags***: Flags passed to the C compiler.
 
 ```
@@ -4046,7 +4286,7 @@
 ```
   For action and action_foreach targets, inputs should be the inputs to
   script that don't vary. These should be all .py files that the script
-  uses via imports (the main script itself will be an implcit dependency
+  uses via imports (the main script itself will be an implicit dependency
   of the action so need not be listed).
 
   For action targets, inputs and sources are treated the same, but from
@@ -5254,7 +5494,7 @@
   {{source_name_part}}
       The filename part of the source file with no directory or
       extension. This will generally be used for specifying a
-      transformation from a soruce file to a destination file with the
+      transformation from a source file to a destination file with the
       same name but different extension.
         "//foo/bar/baz.txt" => "baz"
 
diff --git a/tools/gn/function_toolchain.cc b/tools/gn/function_toolchain.cc
index 9eeae98..727be54 100644
--- a/tools/gn/function_toolchain.cc
+++ b/tools/gn/function_toolchain.cc
@@ -407,6 +407,10 @@
     "      \"stamp\": Tool for creating stamp files\n"
     "      \"copy\": Tool to copy files.\n"
     "\n"
+    "    Platform specific tools:\n"
+    "      \"copy_bundle_data\": [iOS, OS X] Tool to copy files in a bundle.\n"
+    "      \"compile_xcassets\": [iOS, OS X] Tool to compile asset catalogs.\n"
+    "\n"
     "Tool variables\n"
     "\n"
     "    command  [string with substitutions]\n"
@@ -718,6 +722,18 @@
     "  {{source}} which is the source of the copy. The stamp tool allows\n"
     "  only the common tool substitutions.\n"
     "\n"
+    "  The copy_bundle_data and compile_xcassets tools only allows the common\n"
+    "  tool substitutions. Both tools are required to create iOS/OS X bundles\n"
+    "  and need only be defined on those platforms.\n"
+    "\n"
+    "  The copy_bundle_data tool will be called with one source and needs to\n"
+    "  copy (optionally optimizing the data representation) to its output. It\n"
+    "  may be called with a directory as input and it needs to be recursively\n"
+    "  copied.\n"
+    "\n"
+    "  The compile_xcassets tool will be called with one or more source (each\n"
+    "  an asset catalog) that needs to be compiled to a single output.\n"
+    "\n"
     "Separate linking and dependencies for shared libraries\n"
     "\n"
     "  Shared libraries are special in that not all changes to them require\n"
@@ -812,9 +828,13 @@
   } else if (IsLinkerTool(tool_type)) {
     subst_validator = &IsValidLinkerSubstitution;
     subst_output_validator = &IsValidLinkerOutputsSubstitution;
-  } else if (tool_type == Toolchain::TYPE_COPY) {
+  } else if (tool_type == Toolchain::TYPE_COPY ||
+             tool_type == Toolchain::TYPE_COPY_BUNDLE_DATA) {
     subst_validator = &IsValidCopySubstitution;
     subst_output_validator = &IsValidCopySubstitution;
+  } else if (tool_type == Toolchain::TYPE_COMPILE_XCASSETS) {
+    subst_validator = &IsValidCompileXCassetsSubstitution;
+    subst_output_validator = &IsValidCompileXCassetsSubstitution;
   } else {
     subst_validator = &IsValidToolSubstitution;
     subst_output_validator = &IsValidToolSubstitution;
@@ -851,9 +871,12 @@
     return Value();
   }
 
-  if (tool_type != Toolchain::TYPE_COPY && tool_type != Toolchain::TYPE_STAMP) {
-    // All tools except the copy and stamp tools should have outputs. The copy
-    // and stamp tool's outputs are generated internally.
+  if (tool_type != Toolchain::TYPE_COPY &&
+      tool_type != Toolchain::TYPE_STAMP &&
+      tool_type != Toolchain::TYPE_COPY_BUNDLE_DATA &&
+      tool_type != Toolchain::TYPE_COMPILE_XCASSETS) {
+    // All tools should have outputs, except the copy, stamp, copy_bundle_data
+    // and compile_xcassets tools that generate their outputs internally.
     if (!ReadOutputs(&block_scope, function, subst_output_validator,
                      tool.get(), err))
       return Value();
diff --git a/tools/gn/functions.cc b/tools/gn/functions.cc
index 37b3401..fc51cc8 100644
--- a/tools/gn/functions.cc
+++ b/tools/gn/functions.cc
@@ -804,6 +804,7 @@
     INSERT_FUNCTION(Action, true)
     INSERT_FUNCTION(ActionForEach, true)
     INSERT_FUNCTION(BundleData, true)
+    INSERT_FUNCTION(CreateBundle, true)
     INSERT_FUNCTION(Copy, true)
     INSERT_FUNCTION(Executable, true)
     INSERT_FUNCTION(Group, true)
diff --git a/tools/gn/functions.h b/tools/gn/functions.h
index 1341720..3ce945e 100644
--- a/tools/gn/functions.h
+++ b/tools/gn/functions.h
@@ -89,6 +89,15 @@
                     BlockNode* block,
                     Err* err);
 
+extern const char kCreateBundle[];
+extern const char kCreateBundle_HelpShort[];
+extern const char kCreateBundle_Help[];
+Value RunCreateBundle(Scope* scope,
+                      const FunctionCallNode* function,
+                      const std::vector<Value>& args,
+                      BlockNode* block,
+                      Err* err);
+
 extern const char kConfig[];
 extern const char kConfig_HelpShort[];
 extern const char kConfig_Help[];
diff --git a/tools/gn/functions_target.cc b/tools/gn/functions_target.cc
index 7a293c7..7ec5a50 100644
--- a/tools/gn/functions_target.cc
+++ b/tools/gn/functions_target.cc
@@ -247,13 +247,57 @@
 
 const char kBundleData[] = "bundle_data";
 const char kBundleData_HelpShort[] =
-    "bundle_data: Declare a target without output.";
+    "bundle_data: [iOS/OS X] Declare a target without output.";
 const char kBundleData_Help[] =
-    "bundle_data: Declare a target without output.\n"
+    "bundle_data: [iOS/OS X] Declare a target without output.\n"
     "\n"
     "  This target type allows to declare data that is required at runtime.\n"
     "  It is used to inform \"create_bundle\" targets of the files to copy\n"
-    "  into generated bundle, see \"gn help create_bundle\" for help.\n";
+    "  into generated bundle, see \"gn help create_bundle\" for help.\n"
+    "\n"
+    "  The target must define a list of files as \"sources\" and a single\n"
+    "  \"outputs\". If there are multiple files, source expansions must be\n"
+    "  used to express the output. The output must reference a file inside\n"
+    "  of {{bundle_root_dir}}.\n"
+    "\n"
+    "  This target can be used on all platforms though it is designed only to\n"
+    "  generate iOS/OS X bundle. In cross-platform projects, it is advised to\n"
+    "  put it behind iOS/Mac conditionals.\n"
+    "\n"
+    "  See \"gn help create_bundle\" for more information.\n"
+    "\n"
+    "Variables\n"
+    "\n"
+    "  sources*, outputs*, deps, data_deps, public_deps, visibility\n"
+    "  * = required\n"
+    "\n"
+    "Examples\n"
+    "\n"
+    "  bundle_data(\"icudata\") {\n"
+    "    sources = [ \"sources/data/in/icudtl.dat\" ]\n"
+    "    outputs = [ \"{{bundle_resources_dir}}/{{source_file_part}}\" ]\n"
+    "  }\n"
+    "\n"
+    "  bundle_data(\"base_unittests_bundle_data]\") {\n"
+    "    sources = [ \"test/data\" ]\n"
+    "    outputs = [\n"
+    "      \"{{bundle_resources_dir}}/{{source_root_relative_dir}}/\" +\n"
+    "          \"{{source_file_part}}\"\n"
+    "    ]\n"
+    "  }\n"
+    "\n"
+    "  bundle_data(\"material_typography_bundle_data\") {\n"
+    "    sources = [\n"
+    "      \"src/MaterialTypography.bundle/Roboto-Bold.ttf\",\n"
+    "      \"src/MaterialTypography.bundle/Roboto-Italic.ttf\",\n"
+    "      \"src/MaterialTypography.bundle/Roboto-Regular.ttf\",\n"
+    "      \"src/MaterialTypography.bundle/Roboto-Thin.ttf\",\n"
+    "    ]\n"
+    "    outputs = [\n"
+    "      \"{{bundle_resources_dir}}/MaterialTypography.bundle/\"\n"
+    "          \"{{source_file_part}}\"\n"
+    "    ]\n"
+    "  }\n";
 
 Value RunBundleData(Scope* scope,
                     const FunctionCallNode* function,
@@ -264,6 +308,109 @@
                               block, err);
 }
 
+// create_bundle ---------------------------------------------------------------
+
+const char kCreateBundle[] = "create_bundle";
+const char kCreateBundle_HelpShort[] =
+    "create_bundle: [iOS/OS X] Build an OS X / iOS bundle.";
+const char kCreateBundle_Help[] =
+    "create_bundle: [iOS/OS X] Build an OS X / iOS bundle.\n"
+    "\n"
+    "  This target generates an iOS/OS X bundle (which is a directory with a\n"
+    "  well-know structure). This target does not define any sources, instead\n"
+    "  they are computed from all \"bundle_data\" target this one depends on\n"
+    "  transitively (the recursion stops at \"create_bundle\" targets).\n"
+    "\n"
+    "  The \"bundle_*_dir\" properties must be defined. They will be used for\n"
+    "  the expansion of {{bundle_*_dir}} rules in \"bundle_data\" outputs.\n"
+    "\n"
+    "  This target can be used on all platforms though it is designed only to\n"
+    "  generate iOS/OS X bundle. In cross-platform projects, it is advised to\n"
+    "  put it behind iOS/Mac conditionals.\n"
+    "\n"
+    "Variables\n"
+    "\n"
+    "  bundle_root_dir*, bundle_resources_dir*, bundle_executable_dir*,\n"
+    "  bundle_plugins_dir*, deps, data_deps, public_deps, visibility\n"
+    "  * = required\n"
+    "\n"
+    "Example\n"
+    "\n"
+    "  # Defines a template to create an application. On most platform, this\n"
+    "  # is just an alias for an \"executable\" target, but on iOS/OS X, it\n"
+    "  # builds an application bundle.\n"
+    "  template(\"app\") {\n"
+    "    if (!is_ios && !is_mac) {\n"
+    "      executable(target_name) {\n"
+    "        forward_variables_from(invoker, \"*\")\n"
+    "      }\n"
+    "    } else {\n"
+    "      app_name = target_name\n"
+    "      gen_path = target_gen_dir\n"
+    "\n"
+    "      action(\"${app_name}_generate_info_plist\") {\n"
+    "        script = [ \"//build/ios/ios_gen_plist.py\" ]\n"
+    "        sources = [ \"templates/Info.plist\" ]\n"
+    "        outputs = [ \"$gen_path/Info.plist\" ]\n"
+    "        args = rebase_path(sources, root_build_dir) +\n"
+    "               rebase_path(outputs, root_build_dir)\n"
+    "      }\n"
+    "\n"
+    "      bundle_data(\"${app_name}_bundle_info_plist\") {\n"
+    "        deps = [ \":${app_name}_generate_info_plist\" ]\n"
+    "        sources = [ \"$gen_path/Info.plist\" ]\n"
+    "        outputs = [ \"{{bundle_root_dir}}/Info.plist\" ]\n"
+    "      }\n"
+    "\n"
+    "      executable(\"${app_name}_generate_executable\") {\n"
+    "        forward_variables_from(invoker, \"*\", [\n"
+    "                                                \"output_name\",\n"
+    "                                                \"visibility\",\n"
+    "                                               ])\n"
+    "        output_name =\n"
+    "            rebase_path(\"$gen_path/$app_name\", root_build_dir)\n"
+    "      }\n"
+    "\n"
+    "      bundle_data(\"${app_name}_bundle_executable\") {\n"
+    "        deps = [ \":${app_name}_generate_executable\" ]\n"
+    "        sources = [ \"$gen_path/$app_name\" ]\n"
+    "        outputs = [ \"{{bundle_executable_dir}}/$app_name\" ]\n"
+    "      }\n"
+    "\n"
+    "      create_bundle(\"${app_name}.app\") {\n"
+    "        deps = [\n"
+    "          \":${app_name}_bundle_executable\",\n"
+    "          \":${app_name}_bundle_info_plist\",\n"
+    "        ]\n"
+    "        if (is_ios) {\n"
+    "          bundle_root_dir = \"${root_build_dir}/$target_name\"\n"
+    "          bundle_resources_dir = bundle_root_dir\n"
+    "          bundle_executable_dir = bundle_root_dir\n"
+    "          bundle_plugins_dir = bundle_root_dir + \"/Plugins\"\n"
+    "        } else {\n"
+    "          bundle_root_dir = \"${root_build_dir}/target_name/Contents\"\n"
+    "          bundle_resources_dir = bundle_root_dir + \"/Resources\"\n"
+    "          bundle_executable_dir = bundle_root_dir + \"/MacOS\"\n"
+    "          bundle_plugins_dir = bundle_root_dir + \"/Plugins\"\n"
+    "        }\n"
+    "      }\n"
+    "\n"
+    "      group(target_name) {\n"
+    "        forward_variables_from(invoker, [\"visibility\"])\n"
+    "        deps = [ \":${app_name}.app\" ]\n"
+    "      }\n"
+    "    }\n"
+    "  }\n";
+
+Value RunCreateBundle(Scope* scope,
+                      const FunctionCallNode* function,
+                      const std::vector<Value>& args,
+                      BlockNode* block,
+                      Err* err) {
+  return ExecuteGenericTarget(functions::kCreateBundle, scope, function, args,
+                              block, err);
+}
+
 // copy ------------------------------------------------------------------------
 
 const char kCopy[] = "copy";
diff --git a/tools/gn/gn.gyp b/tools/gn/gn.gyp
index 3cd81e3..79f83a7 100644
--- a/tools/gn/gn.gyp
+++ b/tools/gn/gn.gyp
@@ -24,8 +24,12 @@
         'builder.h',
         'builder_record.cc',
         'builder_record.h',
+        'bundle_data.cc',
+        'bundle_data.h',
         'bundle_data_target_generator.cc',
         'bundle_data_target_generator.h',
+        'bundle_file_rule.cc',
+        'bundle_file_rule.h',
         'c_include_iterator.cc',
         'c_include_iterator.h',
         'command_args.cc',
@@ -50,6 +54,8 @@
         'config_values_generator.h',
         'copy_target_generator.cc',
         'copy_target_generator.h',
+        'create_bundle_target_generator.cc',
+        'create_bundle_target_generator.h',
         'deps_iterator.cc',
         'deps_iterator.h',
         'eclipse_writer.cc',
@@ -116,6 +122,8 @@
         'ninja_bundle_data_target_writer.h',
         'ninja_copy_target_writer.cc',
         'ninja_copy_target_writer.h',
+        'ninja_create_bundle_target_writer.cc',
+        'ninja_create_bundle_target_writer.h',
         'ninja_group_target_writer.cc',
         'ninja_group_target_writer.h',
         'ninja_target_writer.cc',
@@ -247,6 +255,7 @@
         'ninja_action_target_writer_unittest.cc',
         'ninja_binary_target_writer_unittest.cc',
         'ninja_copy_target_writer_unittest.cc',
+        'ninja_create_bundle_target_writer_unittest.cc',
         'ninja_group_target_writer_unittest.cc',
         'ninja_target_writer_unittest.cc',
         'ninja_toolchain_writer_unittest.cc',
diff --git a/tools/gn/ninja_create_bundle_target_writer.cc b/tools/gn/ninja_create_bundle_target_writer.cc
new file mode 100644
index 0000000..b92001d
--- /dev/null
+++ b/tools/gn/ninja_create_bundle_target_writer.cc
@@ -0,0 +1,115 @@
+// 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/ninja_create_bundle_target_writer.h"
+
+#include "tools/gn/filesystem_utils.h"
+#include "tools/gn/ninja_utils.h"
+#include "tools/gn/output_file.h"
+#include "tools/gn/scheduler.h"
+#include "tools/gn/substitution_writer.h"
+#include "tools/gn/target.h"
+#include "tools/gn/toolchain.h"
+
+namespace {
+
+void FailWithMissingToolError(Toolchain::ToolType tool, const Target* target) {
+  const std::string& tool_name = Toolchain::ToolTypeToName(tool);
+  g_scheduler->FailWithError(Err(
+      nullptr, tool_name + " tool not defined",
+      "The toolchain " +
+          target->toolchain()->label().GetUserVisibleName(false) + "\n"
+          "used by target " + target->label().GetUserVisibleName(false) + "\n"
+          "doesn't define a \"" + tool_name + "\" tool."));
+}
+
+}  // namespace
+
+NinjaCreateBundleTargetWriter::NinjaCreateBundleTargetWriter(
+    const Target* target,
+    std::ostream& out)
+    : NinjaTargetWriter(target, out) {}
+
+NinjaCreateBundleTargetWriter::~NinjaCreateBundleTargetWriter() {}
+
+void NinjaCreateBundleTargetWriter::Run() {
+  if (!target_->toolchain()->GetTool(Toolchain::TYPE_COPY_BUNDLE_DATA)) {
+    FailWithMissingToolError(Toolchain::TYPE_COPY_BUNDLE_DATA, target_);
+    return;
+  }
+
+  if (!target_->toolchain()->GetTool(Toolchain::TYPE_COMPILE_XCASSETS)) {
+    FailWithMissingToolError(Toolchain::TYPE_COMPILE_XCASSETS, target_);
+    return;
+  }
+
+  if (!target_->toolchain()->GetTool(Toolchain::TYPE_STAMP)) {
+    FailWithMissingToolError(Toolchain::TYPE_STAMP, target_);
+    return;
+  }
+
+  std::vector<OutputFile> output_files;
+  OutputFile input_dep =
+      WriteInputDepsStampAndGetDep(std::vector<const Target*>());
+
+  for (const BundleFileRule& file_rule : target_->bundle_data().file_rules()) {
+    for (const SourceFile& source_file : file_rule.sources()) {
+      OutputFile output_file = file_rule.ApplyPatternToSourceAsOutputFile(
+          settings_, target_->bundle_data(), source_file);
+      output_files.push_back(output_file);
+
+      out_ << "build ";
+      path_output_.WriteFile(out_, output_file);
+      out_ << ": "
+           << GetNinjaRulePrefixForToolchain(settings_)
+           << Toolchain::ToolTypeToName(Toolchain::TYPE_COPY_BUNDLE_DATA)
+           << " ";
+      path_output_.WriteFile(out_, source_file);
+      if (!input_dep.value().empty()) {
+        out_ << " || ";
+        path_output_.WriteFile(out_, input_dep);
+      }
+      out_ << std::endl;
+    }
+  }
+
+  if (!target_->bundle_data().asset_catalog_sources().empty()) {
+    OutputFile output_file(
+        settings_->build_settings(),
+        target_->bundle_data().GetCompiledAssetCatalogPath());
+    output_files.push_back(output_file);
+
+    out_ << "build ";
+    path_output_.WriteFile(out_, output_file);
+    out_ << ": "
+         << GetNinjaRulePrefixForToolchain(settings_)
+         << Toolchain::ToolTypeToName(Toolchain::TYPE_COMPILE_XCASSETS);
+
+    std::set<SourceFile> asset_catalog_bundles;
+    for (const auto& source : target_->bundle_data().asset_catalog_sources()) {
+      SourceFile asset_catalog_bundle;
+      CHECK(IsSourceFileFromAssetCatalog(source, &asset_catalog_bundle));
+      if (asset_catalog_bundles.find(asset_catalog_bundle) !=
+          asset_catalog_bundles.end())
+        continue;
+      out_ << " ";
+      path_output_.WriteFile(out_, asset_catalog_bundle);
+      asset_catalog_bundles.insert(asset_catalog_bundle);
+    }
+
+    out_ << " |";
+    for (const auto& source : target_->bundle_data().asset_catalog_sources()) {
+      out_ << " ";
+      path_output_.WriteFile(out_, source);
+    }
+    if (!input_dep.value().empty()) {
+      out_ << " || ";
+      path_output_.WriteFile(out_, input_dep);
+    }
+    out_ << std::endl;
+  }
+
+  out_ << std::endl;
+  WriteStampForTarget(output_files, std::vector<OutputFile>());
+}
diff --git a/tools/gn/ninja_create_bundle_target_writer.h b/tools/gn/ninja_create_bundle_target_writer.h
new file mode 100644
index 0000000..42b900f
--- /dev/null
+++ b/tools/gn/ninja_create_bundle_target_writer.h
@@ -0,0 +1,23 @@
+// 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_NINJA_CREATE_BUNDLE_TARGET_WRITER_H_
+#define TOOLS_GN_NINJA_CREATE_BUNDLE_TARGET_WRITER_H_
+
+#include "base/macros.h"
+#include "tools/gn/ninja_target_writer.h"
+
+// Writes a .ninja file for a bundle_data target type.
+class NinjaCreateBundleTargetWriter : public NinjaTargetWriter {
+ public:
+  NinjaCreateBundleTargetWriter(const Target* target, std::ostream& out);
+  ~NinjaCreateBundleTargetWriter() override;
+
+  void Run() override;
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(NinjaCreateBundleTargetWriter);
+};
+
+#endif  // TOOLS_GN_NINJA_CREATE_BUNDLE_TARGET_WRITER_H_
diff --git a/tools/gn/ninja_create_bundle_target_writer_unittest.cc b/tools/gn/ninja_create_bundle_target_writer_unittest.cc
new file mode 100644
index 0000000..88e6fba
--- /dev/null
+++ b/tools/gn/ninja_create_bundle_target_writer_unittest.cc
@@ -0,0 +1,173 @@
+// 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/ninja_create_bundle_target_writer.h"
+
+#include <algorithm>
+#include <sstream>
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "tools/gn/target.h"
+#include "tools/gn/test_with_scope.h"
+
+namespace {
+
+void SetupBundleDataDir(BundleData* bundle_data, const std::string& root_dir) {
+  bundle_data->root_dir().assign(root_dir + "/bar.bundle");
+  bundle_data->resources_dir().assign(bundle_data->root_dir() + "/Resources");
+  bundle_data->executable_dir().assign(bundle_data->root_dir() + "/Executable");
+  bundle_data->plugins_dir().assign(bundle_data->root_dir() + "/PlugIns");
+}
+
+}  // namespace
+
+// Tests multiple files with an output pattern.
+TEST(NinjaCreateBundleTargetWriter, Run) {
+  TestWithScope setup;
+  Err err;
+
+  setup.build_settings()->SetBuildDir(SourceDir("//out/Debug/"));
+  Target target(setup.settings(), Label(SourceDir("//baz/"), "bar"));
+  target.set_output_type(Target::CREATE_BUNDLE);
+
+  SetupBundleDataDir(&target.bundle_data(), "//out/Debug");
+
+  std::vector<SourceFile> sources;
+  sources.push_back(SourceFile("//foo/input1.txt"));
+  sources.push_back(SourceFile("//foo/input2.txt"));
+  target.bundle_data().file_rules().push_back(BundleFileRule(
+      sources, SubstitutionPattern::MakeForTest(
+                   "{{bundle_resources_dir}}/{{source_file_part}}")));
+
+  target.SetToolchain(setup.toolchain());
+  ASSERT_TRUE(target.OnResolved(&err));
+
+  std::ostringstream out;
+  NinjaCreateBundleTargetWriter writer(&target, out);
+  writer.Run();
+
+  const char expected[] =
+      "build bar.bundle/Resources/input1.txt: copy_bundle_data "
+          "../../foo/input1.txt\n"
+      "build bar.bundle/Resources/input2.txt: copy_bundle_data "
+          "../../foo/input2.txt\n"
+      "\n"
+      "build obj/baz/bar.stamp: stamp "
+          "bar.bundle/Resources/input1.txt "
+          "bar.bundle/Resources/input2.txt\n";
+  std::string out_str = out.str();
+  EXPECT_EQ(expected, out_str);
+}
+
+// Tests multiple files from asset catalog.
+TEST(NinjaCreateBundleTargetWriter, AssetCatalog) {
+  TestWithScope setup;
+  Err err;
+
+  setup.build_settings()->SetBuildDir(SourceDir("//out/Debug/"));
+  Target target(setup.settings(), Label(SourceDir("//baz/"), "bar"));
+  target.set_output_type(Target::CREATE_BUNDLE);
+
+  SetupBundleDataDir(&target.bundle_data(), "//out/Debug");
+
+  std::vector<SourceFile>& asset_catalog_sources =
+      target.bundle_data().asset_catalog_sources();
+  asset_catalog_sources.push_back(
+      SourceFile("//foo/Foo.xcassets/foo.imageset/Contents.json"));
+  asset_catalog_sources.push_back(
+      SourceFile("//foo/Foo.xcassets/foo.imageset/FooIcon-29.png"));
+  asset_catalog_sources.push_back(
+      SourceFile("//foo/Foo.xcassets/foo.imageset/FooIcon-29@2x.png"));
+  asset_catalog_sources.push_back(
+      SourceFile("//foo/Foo.xcassets/foo.imageset/FooIcon-29@3x.png"));
+
+  target.SetToolchain(setup.toolchain());
+  ASSERT_TRUE(target.OnResolved(&err));
+
+  std::ostringstream out;
+  NinjaCreateBundleTargetWriter writer(&target, out);
+  writer.Run();
+
+  const char expected[] =
+      "build bar.bundle/Resources/Assets.car: compile_xcassets "
+          "../../foo/Foo.xcassets | "
+          "../../foo/Foo.xcassets/foo.imageset/Contents.json "
+          "../../foo/Foo.xcassets/foo.imageset/FooIcon-29.png "
+          "../../foo/Foo.xcassets/foo.imageset/FooIcon-29@2x.png "
+          "../../foo/Foo.xcassets/foo.imageset/FooIcon-29@3x.png\n"
+      "\n"
+      "build obj/baz/bar.stamp: stamp bar.bundle/Resources/Assets.car\n";
+  std::string out_str = out.str();
+  EXPECT_EQ(expected, out_str);
+}
+
+// Tests complex target with multiple bundle_data sources, including
+// some asset catalog.
+TEST(NinjaCreateBundleTargetWriter, OrderOnlyDeps) {
+  TestWithScope setup;
+  Err err;
+
+  setup.build_settings()->SetBuildDir(SourceDir("//out/Debug/"));
+  Target target(setup.settings(), Label(SourceDir("//baz/"), "bar"));
+  target.set_output_type(Target::CREATE_BUNDLE);
+
+  SetupBundleDataDir(&target.bundle_data(), "//out/Debug");
+
+  std::vector<SourceFile> sources1;
+  sources1.push_back(SourceFile("//foo/input1.txt"));
+  sources1.push_back(SourceFile("//foo/input2.txt"));
+  target.bundle_data().file_rules().push_back(BundleFileRule(
+      sources1, SubstitutionPattern::MakeForTest(
+                    "{{bundle_resources_dir}}/{{source_file_part}}")));
+
+  std::vector<SourceFile> sources2;
+  sources2.push_back(SourceFile("//qux/Info.plist"));
+  target.bundle_data().file_rules().push_back(BundleFileRule(
+      sources2,
+      SubstitutionPattern::MakeForTest("{{bundle_root_dir}}/Info.plist")));
+
+  std::vector<SourceFile> empty_source;
+  target.bundle_data().file_rules().push_back(BundleFileRule(
+      empty_source, SubstitutionPattern::MakeForTest(
+                        "{{bundle_plugins_dir}}/{{source_file_part}}")));
+
+  std::vector<SourceFile>& asset_catalog_sources =
+      target.bundle_data().asset_catalog_sources();
+  asset_catalog_sources.push_back(
+      SourceFile("//foo/Foo.xcassets/foo.imageset/Contents.json"));
+  asset_catalog_sources.push_back(
+      SourceFile("//foo/Foo.xcassets/foo.imageset/FooIcon-29.png"));
+  asset_catalog_sources.push_back(
+      SourceFile("//foo/Foo.xcassets/foo.imageset/FooIcon-29@2x.png"));
+  asset_catalog_sources.push_back(
+      SourceFile("//foo/Foo.xcassets/foo.imageset/FooIcon-29@3x.png"));
+
+  target.SetToolchain(setup.toolchain());
+  ASSERT_TRUE(target.OnResolved(&err));
+
+  std::ostringstream out;
+  NinjaCreateBundleTargetWriter writer(&target, out);
+  writer.Run();
+
+  const char expected[] =
+      "build bar.bundle/Resources/input1.txt: copy_bundle_data "
+          "../../foo/input1.txt\n"
+      "build bar.bundle/Resources/input2.txt: copy_bundle_data "
+          "../../foo/input2.txt\n"
+      "build bar.bundle/Info.plist: copy_bundle_data ../../qux/Info.plist\n"
+      "build bar.bundle/Resources/Assets.car: compile_xcassets "
+          "../../foo/Foo.xcassets | "
+          "../../foo/Foo.xcassets/foo.imageset/Contents.json "
+          "../../foo/Foo.xcassets/foo.imageset/FooIcon-29.png "
+          "../../foo/Foo.xcassets/foo.imageset/FooIcon-29@2x.png "
+          "../../foo/Foo.xcassets/foo.imageset/FooIcon-29@3x.png\n"
+      "\n"
+      "build obj/baz/bar.stamp: stamp "
+          "bar.bundle/Resources/input1.txt "
+          "bar.bundle/Resources/input2.txt "
+          "bar.bundle/Info.plist "
+          "bar.bundle/Resources/Assets.car\n";
+  std::string out_str = out.str();
+  EXPECT_EQ(expected, out_str);
+}
diff --git a/tools/gn/ninja_target_writer.cc b/tools/gn/ninja_target_writer.cc
index f4de7f2..486930b 100644
--- a/tools/gn/ninja_target_writer.cc
+++ b/tools/gn/ninja_target_writer.cc
@@ -15,6 +15,7 @@
 #include "tools/gn/ninja_binary_target_writer.h"
 #include "tools/gn/ninja_bundle_data_target_writer.h"
 #include "tools/gn/ninja_copy_target_writer.h"
+#include "tools/gn/ninja_create_bundle_target_writer.h"
 #include "tools/gn/ninja_group_target_writer.h"
 #include "tools/gn/ninja_utils.h"
 #include "tools/gn/output_file.h"
@@ -61,6 +62,9 @@
   if (target->output_type() == Target::BUNDLE_DATA) {
     NinjaBundleDataTargetWriter writer(target, file);
     writer.Run();
+  } else if (target->output_type() == Target::CREATE_BUNDLE) {
+    NinjaCreateBundleTargetWriter writer(target, file);
+    writer.Run();
   } else if (target->output_type() == Target::COPY_FILES) {
     NinjaCopyTargetWriter writer(target, file);
     writer.Run();
diff --git a/tools/gn/substitution_type.cc b/tools/gn/substitution_type.cc
index 93a32bd..e1ea14d 100644
--- a/tools/gn/substitution_type.cc
+++ b/tools/gn/substitution_type.cc
@@ -208,6 +208,11 @@
          type == SUBSTITUTION_SOURCE;
 }
 
+bool IsValidCompileXCassetsSubstitution(SubstitutionType type) {
+  return IsValidToolSubstitution(type) ||
+         type == SUBSTITUTION_LINKER_INPUTS;
+}
+
 bool EnsureValidSourcesSubstitutions(
     const std::vector<SubstitutionType>& types,
     const ParseNode* origin,
diff --git a/tools/gn/substitution_type.h b/tools/gn/substitution_type.h
index 96ae33a..3f7ec97 100644
--- a/tools/gn/substitution_type.h
+++ b/tools/gn/substitution_type.h
@@ -32,7 +32,7 @@
   SUBSTITUTION_SOURCE_OUT_DIR,  // {{source_out_dir}}
 
   // Valid for all compiler and linker tools. These depend on the target and
-  // no not vary on a per-file basis.
+  // do not vary on a per-file basis.
   SUBSTITUTION_LABEL,  // {{label}}
   SUBSTITUTION_LABEL_NAME,  // {{label_name}}
   SUBSTITUTION_ROOT_GEN_DIR,  // {{root_gen_dir}}
@@ -117,6 +117,7 @@
 bool IsValidLinkerSubstitution(SubstitutionType type);
 bool IsValidLinkerOutputsSubstitution(SubstitutionType type);
 bool IsValidCopySubstitution(SubstitutionType type);
+bool IsValidCompileXCassetsSubstitution(SubstitutionType type);
 
 // Like the "IsValid..." version above but checks a list of types and sets a
 // an error blaming the given source if the test fails.
diff --git a/tools/gn/target.cc b/tools/gn/target.cc
index 9435d47..714fd91 100644
--- a/tools/gn/target.cc
+++ b/tools/gn/target.cc
@@ -224,6 +224,8 @@
       return "ActionForEach";
     case BUNDLE_DATA:
       return "Bundle data";
+    case CREATE_BUNDLE:
+      return "Create bundle";
     default:
       return "";
   }
@@ -277,6 +279,7 @@
     all_libs_.append(cur.libs().begin(), cur.libs().end());
   }
 
+  PullRecursiveBundleData();
   PullDependentTargetLibs();
   PullRecursiveHardDeps();
   if (!ResolvePrecompiledHeaders(err))
@@ -318,6 +321,7 @@
          output_type_ == ACTION ||
          output_type_ == ACTION_FOREACH ||
          output_type_ == COPY_FILES ||
+         output_type_ == CREATE_BUNDLE ||
          (output_type_ == STATIC_LIBRARY && complete_static_lib_);
 }
 
@@ -476,12 +480,48 @@
   }
 }
 
+void Target::PullRecursiveBundleData() {
+  if (output_type_ != CREATE_BUNDLE)
+    return;
+
+  std::set<const Target*> visited;
+  std::vector<const Target*> deps;
+  deps.push_back(this);
+
+  while (!deps.empty()) {
+    const Target* current = deps.back();
+    deps.pop_back();
+
+    if (visited.find(current) != visited.end())
+      continue;
+    visited.insert(current);
+
+    if (current->output_type_ == BUNDLE_DATA)
+      bundle_data_.AddFileRuleFromTarget(current);
+
+    for (const LabelTargetPair& pair : current->GetDeps(DEPS_ALL)) {
+      DCHECK(pair.ptr);
+      DCHECK(pair.ptr->toolchain_);
+      if (visited.find(pair.ptr) != visited.end())
+        continue;
+
+      if (pair.ptr->output_type() == CREATE_BUNDLE)
+        continue;
+
+      deps.push_back(pair.ptr);
+    }
+  }
+
+  bundle_data_.GetSourceFiles(&sources_);
+}
+
 void Target::FillOutputFiles() {
   const Tool* tool = toolchain_->GetToolForTargetFinalOutput(this);
   bool check_tool_outputs = false;
   switch (output_type_) {
     case GROUP:
     case BUNDLE_DATA:
+    case CREATE_BUNDLE:
     case SOURCE_SET:
     case COPY_FILES:
     case ACTION:
@@ -547,6 +587,10 @@
       NOTREACHED();
   }
 
+  // Count anything generated from bundle_data dependencies.
+  if (output_type_ == CREATE_BUNDLE)
+    bundle_data_.GetOutputFiles(settings(), &computed_outputs_);
+
   // Count all outputs from this tool as something generated by this target.
   if (check_tool_outputs) {
     SubstitutionWriter::ApplyListToLinkerAsOutputFile(
diff --git a/tools/gn/target.h b/tools/gn/target.h
index 9afd0fa..95e682f 100644
--- a/tools/gn/target.h
+++ b/tools/gn/target.h
@@ -13,6 +13,7 @@
 #include "base/logging.h"
 #include "base/macros.h"
 #include "tools/gn/action_values.h"
+#include "tools/gn/bundle_data.h"
 #include "tools/gn/config_values.h"
 #include "tools/gn/inherited_libraries.h"
 #include "tools/gn/item.h"
@@ -45,6 +46,7 @@
     ACTION,
     ACTION_FOREACH,
     BUNDLE_DATA,
+    CREATE_BUNDLE,
   };
 
   enum DepsIterationType {
@@ -136,12 +138,18 @@
   const std::vector<std::string>& data() const { return data_; }
   std::vector<std::string>& data() { return data_; }
 
+  // Information about the bundle. Only valid for CREATE_BUNDLE target after
+  // they have been resolved.
+  const BundleData& bundle_data() const { return bundle_data_; }
+  BundleData& bundle_data() { return bundle_data_; }
+
   // Returns true if targets depending on this one should have an order
   // dependency.
   bool hard_dep() const {
     return output_type_ == ACTION ||
            output_type_ == ACTION_FOREACH ||
-           output_type_ == COPY_FILES;
+           output_type_ == COPY_FILES ||
+           output_type_ == CREATE_BUNDLE;
   }
 
   // Returns the iterator range which can be used in range-based for loops
@@ -284,6 +292,7 @@
   void PullDependentTargetLibsFrom(const Target* dep, bool is_public);
   void PullDependentTargetLibs();
   void PullRecursiveHardDeps();
+  void PullRecursiveBundleData();
 
   // Fills the link and dependency output files when a target is resolved.
   void FillOutputFiles();
@@ -312,6 +321,7 @@
   bool testonly_;
   FileList inputs_;
   std::vector<std::string> data_;
+  BundleData bundle_data_;
 
   LabelTargetVector private_deps_;
   LabelTargetVector public_deps_;
diff --git a/tools/gn/target_generator.cc b/tools/gn/target_generator.cc
index 0a833a0..f5b4ec8 100644
--- a/tools/gn/target_generator.cc
+++ b/tools/gn/target_generator.cc
@@ -12,6 +12,7 @@
 #include "tools/gn/bundle_data_target_generator.h"
 #include "tools/gn/config.h"
 #include "tools/gn/copy_target_generator.h"
+#include "tools/gn/create_bundle_target_generator.h"
 #include "tools/gn/err.h"
 #include "tools/gn/filesystem_utils.h"
 #include "tools/gn/functions.h"
@@ -92,6 +93,10 @@
     BundleDataTargetGenerator generator(
         target.get(), scope, function_call, err);
     generator.Run();
+  } else if (output_type == functions::kCreateBundle) {
+    CreateBundleTargetGenerator generator(target.get(), scope, function_call,
+                                          err);
+    generator.Run();
   } else if (output_type == functions::kCopy) {
     CopyTargetGenerator generator(target.get(), scope, function_call, err);
     generator.Run();
diff --git a/tools/gn/target_unittest.cc b/tools/gn/target_unittest.cc
index 8376fff..8f6684f 100644
--- a/tools/gn/target_unittest.cc
+++ b/tools/gn/target_unittest.cc
@@ -763,3 +763,66 @@
   a2.assert_no_deps().push_back(disallow_a);
   ASSERT_TRUE(a2.OnResolved(&err));
 }
+
+TEST(Target, PullRecursiveBundleData) {
+  TestWithScope setup;
+  Err err;
+
+  // We have the following dependency graph:
+  // A (create_bundle) -> B (bundle_data)
+  //                  \-> C (create_bundle) -> D (bundle_data)
+  //                  \-> E (group) -> F (bundle_data)
+  //                               \-> B (bundle_data)
+  TestTarget a(setup, "//foo:a", Target::CREATE_BUNDLE);
+  TestTarget b(setup, "//foo:b", Target::BUNDLE_DATA);
+  TestTarget c(setup, "//foo:c", Target::CREATE_BUNDLE);
+  TestTarget d(setup, "//foo:d", Target::BUNDLE_DATA);
+  TestTarget e(setup, "//foo:e", Target::GROUP);
+  TestTarget f(setup, "//foo:f", Target::BUNDLE_DATA);
+  a.public_deps().push_back(LabelTargetPair(&b));
+  a.public_deps().push_back(LabelTargetPair(&c));
+  a.public_deps().push_back(LabelTargetPair(&e));
+  c.public_deps().push_back(LabelTargetPair(&d));
+  e.public_deps().push_back(LabelTargetPair(&f));
+  e.public_deps().push_back(LabelTargetPair(&b));
+
+  b.sources().push_back(SourceFile("//foo/b1.txt"));
+  b.sources().push_back(SourceFile("//foo/b2.txt"));
+  b.action_values().outputs() = SubstitutionList::MakeForTest(
+      "{{bundle_resources_dir}}/{{source_file_part}}");
+  ASSERT_TRUE(b.OnResolved(&err));
+
+  d.sources().push_back(SourceFile("//foo/d.txt"));
+  d.action_values().outputs() = SubstitutionList::MakeForTest(
+      "{{bundle_resources_dir}}/{{source_file_part}}");
+  ASSERT_TRUE(d.OnResolved(&err));
+
+  f.sources().push_back(SourceFile("//foo/f1.txt"));
+  f.sources().push_back(SourceFile("//foo/f2.txt"));
+  f.sources().push_back(SourceFile("//foo/f3.txt"));
+  f.sources().push_back(
+      SourceFile("//foo/Foo.xcassets/foo.imageset/Contents.json"));
+  f.sources().push_back(
+      SourceFile("//foo/Foo.xcassets/foo.imageset/FooEmpty-29.png"));
+  f.sources().push_back(
+      SourceFile("//foo/Foo.xcassets/foo.imageset/FooEmpty-29@2x.png"));
+  f.sources().push_back(
+      SourceFile("//foo/Foo.xcassets/foo.imageset/FooEmpty-29@3x.png"));
+  f.action_values().outputs() = SubstitutionList::MakeForTest(
+      "{{bundle_resources_dir}}/{{source_file_part}}");
+  ASSERT_TRUE(f.OnResolved(&err));
+
+  ASSERT_TRUE(e.OnResolved(&err));
+  ASSERT_TRUE(c.OnResolved(&err));
+  ASSERT_TRUE(a.OnResolved(&err));
+
+  // A gets its data from B and F.
+  ASSERT_EQ(a.bundle_data().file_rules().size(), 2u);
+  ASSERT_EQ(a.bundle_data().file_rules()[0].sources().size(), 2u);
+  ASSERT_EQ(a.bundle_data().file_rules()[1].sources().size(), 3u);
+  ASSERT_EQ(a.bundle_data().asset_catalog_sources().size(), 4u);
+
+  // C gets its data from D.
+  ASSERT_EQ(c.bundle_data().file_rules().size(), 1u);
+  ASSERT_EQ(c.bundle_data().file_rules()[0].sources().size(), 1u);
+}
diff --git a/tools/gn/test_with_scope.cc b/tools/gn/test_with_scope.cc
index 8890338..2be27fa 100644
--- a/tools/gn/test_with_scope.cc
+++ b/tools/gn/test_with_scope.cc
@@ -138,6 +138,18 @@
   SetCommandForTool("cp {{source}} {{output}}", copy_tool.get());
   toolchain->SetTool(Toolchain::TYPE_COPY, std::move(copy_tool));
 
+  // COPY_BUNDLE_DATA
+  scoped_ptr<Tool> copy_bundle_data_tool(new Tool);
+  SetCommandForTool("cp {{source}} {{output}}", copy_bundle_data_tool.get());
+  toolchain->SetTool(Toolchain::TYPE_COPY_BUNDLE_DATA,
+                     std::move(copy_bundle_data_tool));
+
+  // COMPILE_XCASSETS
+  scoped_ptr<Tool> compile_xcassets_tool(new Tool);
+  SetCommandForTool("touch {{output}}", compile_xcassets_tool.get());
+  toolchain->SetTool(Toolchain::TYPE_COMPILE_XCASSETS,
+                     std::move(compile_xcassets_tool));
+
   toolchain->ToolchainSetupComplete();
 }
 
diff --git a/tools/gn/toolchain.cc b/tools/gn/toolchain.cc
index ee8e7e8..280fb12 100644
--- a/tools/gn/toolchain.cc
+++ b/tools/gn/toolchain.cc
@@ -24,6 +24,8 @@
 const char* Toolchain::kToolLink = "link";
 const char* Toolchain::kToolStamp = "stamp";
 const char* Toolchain::kToolCopy = "copy";
+const char* Toolchain::kToolCopyBundleData = "copy_bundle_data";
+const char* Toolchain::kToolCompileXCAssets = "compile_xcassets";
 
 Toolchain::Toolchain(const Settings* settings, const Label& label)
     : Item(settings, label),
@@ -56,6 +58,8 @@
   if (str == kToolLink) return TYPE_LINK;
   if (str == kToolStamp) return TYPE_STAMP;
   if (str == kToolCopy) return TYPE_COPY;
+  if (str == kToolCopyBundleData) return TYPE_COPY_BUNDLE_DATA;
+  if (str == kToolCompileXCAssets) return TYPE_COMPILE_XCASSETS;
   return TYPE_NONE;
 }
 
@@ -74,6 +78,8 @@
     case TYPE_LINK: return kToolLink;
     case TYPE_STAMP: return kToolStamp;
     case TYPE_COPY: return kToolCopy;
+    case TYPE_COPY_BUNDLE_DATA: return kToolCopyBundleData;
+    case TYPE_COMPILE_XCASSETS: return kToolCompileXCAssets;
     default:
       NOTREACHED();
       return std::string();
@@ -154,6 +160,7 @@
     case Target::ACTION:
     case Target::ACTION_FOREACH:
     case Target::BUNDLE_DATA:
+    case Target::CREATE_BUNDLE:
     case Target::COPY_FILES:
       return TYPE_STAMP;
     default:
diff --git a/tools/gn/toolchain.h b/tools/gn/toolchain.h
index bcc70c0..47a3a44 100644
--- a/tools/gn/toolchain.h
+++ b/tools/gn/toolchain.h
@@ -44,6 +44,8 @@
     TYPE_LINK,
     TYPE_STAMP,
     TYPE_COPY,
+    TYPE_COPY_BUNDLE_DATA,
+    TYPE_COMPILE_XCASSETS,
 
     TYPE_NUMTYPES  // Must be last.
   };
@@ -60,6 +62,8 @@
   static const char* kToolLink;
   static const char* kToolStamp;
   static const char* kToolCopy;
+  static const char* kToolCopyBundleData;
+  static const char* kToolCompileXCAssets;
 
   Toolchain(const Settings* settings, const Label& label);
   ~Toolchain() override;
diff --git a/tools/gn/variables.cc b/tools/gn/variables.cc
index 0242904..fd7800f 100644
--- a/tools/gn/variables.cc
+++ b/tools/gn/variables.cc
@@ -434,6 +434,81 @@
     "    ]\n"
     "  }\n";
 
+const char kBundleRootDir[] = "bundle_root_dir";
+const char kBundleRootDir_HelpShort[] =
+    "bundle_root_dir: Expansion of {{bundle_root_dir}} in create_bundle.";
+const char kBundleRootDir_Help[] =
+    "bundle_root_dir: Expansion of {{bundle_root_dir}} in create_bundle.\n"
+    "\n"
+    "  A string corresponding to a path in root_build_dir.\n"
+    "\n"
+    "  This string is used by the \"create_bundle\" target to expand the\n"
+    "  {{bundle_root_dir}} of the \"bundle_data\" target it depends on.\n"
+    "  This must correspond to a path under root_build_dir.\n"
+    "\n"
+    "Example\n"
+    "\n"
+    "  bundle_data(\"info_plist\") {\n"
+    "    sources = [ \"Info.plist\" ]\n"
+    "    outputs = [ \"{{bundle_root_dir}}/Info.plist\" ]\n"
+    "  }\n"
+    "\n"
+    "  create_bundle(\"doom_melon.app\") {\n"
+    "    deps = [ \":info_plist\" ]\n"
+    "    bundle_root_dir = root_build_dir + \"/doom_melon.app/Contents\"\n"
+    "    bundle_resources_dir = bundle_root_dir + \"/Resources\"\n"
+    "    bundle_executable_dir = bundle_root_dir + \"/MacOS\"\n"
+    "    bundle_plugins_dir = bundle_root_dir + \"/PlugIns\"\n"
+    "  }\n";
+
+const char kBundleResourcesDir[] = "bundle_resources_dir";
+const char kBundleResourcesDir_HelpShort[] =
+    "bundle_resources_dir: "
+        "Expansion of {{bundle_resources_dir}} in create_bundle.";
+const char kBundleResourcesDir_Help[] =
+    "bundle_resources_dir: "
+        "Expansion of {{bundle_resources_dir}} in create_bundle.\n"
+    "\n"
+    "  A string corresponding to a path in $root_build_dir.\n"
+    "\n"
+    "  This string is used by the \"create_bundle\" target to expand the\n"
+    "  {{bundle_resources_dir}} of the \"bundle_data\" target it depends on.\n"
+    "  This must correspond to a path under \"bundle_root_dir\".\n"
+    "\n"
+    "  See \"gn help bundle_root_dir\" for examples.\n";
+
+const char kBundleExecutableDir[] = "bundle_executable_dir";
+const char kBundleExecutableDir_HelpShort[] =
+    "bundle_executable_dir: "
+        "Expansion of {{bundle_executable_dir}} in create_bundle.";
+const char kBundleExecutableDir_Help[] =
+    "bundle_executable_dir: "
+        "Expansion of {{bundle_executable_dir}} in create_bundle.\n"
+    "\n"
+    "  A string corresponding to a path in $root_build_dir.\n"
+    "\n"
+    "  This string is used by the \"create_bundle\" target to expand the\n"
+    "  {{bundle_executable_dir}} of the \"bundle_data\" target it depends on.\n"
+    "  This must correspond to a path under \"bundle_root_dir\".\n"
+    "\n"
+    "  See \"gn help bundle_root_dir\" for examples.\n";
+
+const char kBundlePlugInsDir[] = "bundle_plugins_dir";
+const char kBundlePlugInsDir_HelpShort[] =
+    "bundle_plugins_dir: "
+        "Expansion of {{bundle_plugins_dir}} in create_bundle.";
+const char kBundlePlugInsDir_Help[] =
+    "bundle_plugins_dir: "
+        "Expansion of {{bundle_plugins_dir}} in create_bundle.\n"
+    "\n"
+    "  A string corresponding to a path in $root_build_dir.\n"
+    "\n"
+    "  This string is used by the \"create_bundle\" target to expand the\n"
+    "  {{bundle_plugins_dir}} of the \"bundle_data\" target it depends on.\n"
+    "  This must correspond to a path under \"bundle_root_dir\".\n"
+    "\n"
+    "  See \"gn help bundle_root_dir\" for examples.\n";
+
 const char kCflags[] = "cflags";
 const char kCflags_HelpShort[] =
     "cflags: [string list] Flags passed to all C compiler variants.";
@@ -1457,6 +1532,10 @@
     INSERT_VARIABLE(Args)
     INSERT_VARIABLE(Asmflags)
     INSERT_VARIABLE(AssertNoDeps)
+    INSERT_VARIABLE(BundleRootDir)
+    INSERT_VARIABLE(BundleResourcesDir)
+    INSERT_VARIABLE(BundleExecutableDir)
+    INSERT_VARIABLE(BundlePlugInsDir)
     INSERT_VARIABLE(Cflags)
     INSERT_VARIABLE(CflagsC)
     INSERT_VARIABLE(CflagsCC)
diff --git a/tools/gn/variables.h b/tools/gn/variables.h
index ee68bd5..45fa0eb 100644
--- a/tools/gn/variables.h
+++ b/tools/gn/variables.h
@@ -91,6 +91,22 @@
 extern const char kAssertNoDeps_HelpShort[];
 extern const char kAssertNoDeps_Help[];
 
+extern const char kBundleRootDir[];
+extern const char kBundleRootDir_HelpShort[];
+extern const char kBundleRootDir_Help[];
+
+extern const char kBundleResourcesDir[];
+extern const char kBundleResourcesDir_HelpShort[];
+extern const char kBundleResourcesDir_Help[];
+
+extern const char kBundleExecutableDir[];
+extern const char kBundleExecutableDir_HelpShort[];
+extern const char kBundleExecutableDir_Help[];
+
+extern const char kBundlePlugInsDir[];
+extern const char kBundlePlugInsDir_HelpShort[];
+extern const char kBundlePlugInsDir_Help[];
+
 extern const char kCflags[];
 extern const char kCflags_HelpShort[];
 extern const char* kCflags_Help;