Make all bundle_foo_dir properties optional

The bundle_foo_dir properties are only required if they are used by
a bundle_data expansion ({{bundle_foo_dir}}). Do not report an error
if they are not defined unless there is an expansion using them.

Bug: https://bugs.chromium.org/p/gn/issues/detail?id=64
Change-Id: I277fdfc209fb110352f4614addba7f35f127db51
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/4482
Reviewed-by: Nico Weber <thakis@chromium.org>
Commit-Queue: Nico Weber <thakis@chromium.org>
diff --git a/docs/reference.md b/docs/reference.md
index 8019687..9571fb3 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -685,6 +685,7 @@
       "vs2013" - Visual Studio 2013 project/solution files.
       "vs2015" - Visual Studio 2015 project/solution files.
       "vs2017" - Visual Studio 2017 project/solution files.
+      "vs2019" - Visual Studio 2019 project/solution files.
       "xcode" - Xcode workspace/solution files.
       "qtcreator" - QtCreator project files.
       "json" - JSON file containing target information
@@ -1385,8 +1386,9 @@
   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.
+  The "bundle_*_dir" are be used for the expansion of {{bundle_*_dir}} rules in
+  "bundle_data" outputs. The properties are optional but must be defined if any
+  of the "bundle_data" target use them.
 
   This target can be used on all platforms though it is designed only to
   generate iOS or macOS bundle. In cross-platform projects, it is advised to put
@@ -1420,13 +1422,12 @@
 #### **Variables**
 
 ```
-  bundle_root_dir*, bundle_contents_dir*, bundle_resources_dir*,
-  bundle_executable_dir*, bundle_plugins_dir*, bundle_deps_filter, deps,
+  bundle_root_dir, bundle_contents_dir, bundle_resources_dir,
+  bundle_executable_dir, bundle_plugins_dir, bundle_deps_filter, deps,
   data_deps, public_deps, visibility, product_type, code_signing_args,
   code_signing_script, code_signing_sources, code_signing_outputs,
   xcode_extra_attributes, xcode_test_application_name, partial_info_plist,
   metadata
-  * = required
 ```
 
 #### **Example**
@@ -4287,9 +4288,12 @@
     ]
   }
 ```
-### <a name="var_bundle_executable_dir"></a>**bundle_executable_dir**: Expansion of {{bundle_executable_dir}} in create_bundle.
+### <a name="var_bundle_executable_dir"></a>**bundle_executable_dir**
 
 ```
+  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
@@ -4312,7 +4316,8 @@
 ### <a name="var_bundle_resources_dir"></a>**bundle_resources_dir**
 
 ```
-  bundle_resources_dir: Expansion of {{bundle_resources_dir}} in create_bundle.
+  bundle_resources_dir: Expansion of {{bundle_resources_dir}} in
+                        create_bundle.
 
   A string corresponding to a path in $root_build_dir.
 
diff --git a/tools/gn/bundle_data.cc b/tools/gn/bundle_data.cc
index 9f2cf2b..095e989 100644
--- a/tools/gn/bundle_data.cc
+++ b/tools/gn/bundle_data.cc
@@ -112,20 +112,29 @@
   }
 }
 
-void BundleData::GetOutputFiles(const Settings* settings,
-                                OutputFiles* outputs) const {
+bool BundleData::GetOutputFiles(const Settings* settings,
+                                const Target* target,
+                                OutputFiles* outputs,
+                                Err* err) const {
   SourceFiles outputs_as_sources;
-  GetOutputsAsSourceFiles(settings, &outputs_as_sources);
+  if (!GetOutputsAsSourceFiles(settings, target, &outputs_as_sources, err))
+    return false;
   for (const SourceFile& source_file : outputs_as_sources)
     outputs->push_back(OutputFile(settings->build_settings(), source_file));
+  return true;
 }
 
-void BundleData::GetOutputsAsSourceFiles(const Settings* settings,
-                                         SourceFiles* outputs_as_source) const {
+bool BundleData::GetOutputsAsSourceFiles(const Settings* settings,
+                                         const Target* target,
+                                         SourceFiles* outputs_as_source,
+                                         Err* err) 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));
+      SourceFile expanded_source_file;
+      if (!file_rule.ApplyPatternToSource(settings, target, *this, source,
+                                          &expanded_source_file, err))
+        return false;
+      outputs_as_source->push_back(expanded_source_file);
     }
   }
 
@@ -146,6 +155,8 @@
 
   if (!root_dir_.is_null())
     outputs_as_source->push_back(GetBundleRootDirOutput(settings));
+
+  return true;
 }
 
 SourceFile BundleData::GetCompiledAssetCatalogPath() const {
diff --git a/tools/gn/bundle_data.h b/tools/gn/bundle_data.h
index dc63bbd..60254a9 100644
--- a/tools/gn/bundle_data.h
+++ b/tools/gn/bundle_data.h
@@ -44,11 +44,16 @@
   void GetSourceFiles(SourceFiles* sources) const;
 
   // Returns the list of outputs.
-  void GetOutputFiles(const Settings* settings, OutputFiles* outputs) const;
+  bool GetOutputFiles(const Settings* settings,
+                      const Target* target,
+                      OutputFiles* outputs,
+                      Err* err) const;
 
   // Returns the list of outputs as SourceFile.
-  void GetOutputsAsSourceFiles(const Settings* settings,
-                               SourceFiles* outputs_as_source) const;
+  bool GetOutputsAsSourceFiles(const Settings* settings,
+                               const Target* target,
+                               SourceFiles* outputs_as_source,
+                               Err* err) const;
 
   // Returns the path to the compiled asset catalog. Only valid if
   // assets_catalog_sources() is not empty.
diff --git a/tools/gn/bundle_file_rule.cc b/tools/gn/bundle_file_rule.cc
index 4d4ca3a..78012a0 100644
--- a/tools/gn/bundle_file_rule.cc
+++ b/tools/gn/bundle_file_rule.cc
@@ -4,11 +4,32 @@
 
 #include "tools/gn/bundle_file_rule.h"
 
+#include "base/strings/stringprintf.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"
+#include "tools/gn/variables.h"
+
+namespace {
+
+Err ErrMissingPropertyForExpansion(const Settings* settings,
+                                   const Target* target,
+                                   const BundleFileRule* bundle_file_rule,
+                                   const char* property_name) {
+  std::string label = bundle_file_rule->target()->label().GetUserVisibleName(
+      settings->default_toolchain_label());
+
+  return Err(target->defined_from(),
+             base::StringPrintf("Property %s is required.", property_name),
+             base::StringPrintf(
+                 "In order to expand {{%s}} in %s, the "
+                 "property needs to be defined in the create_bundle target.",
+                 property_name, label.c_str()));
+}
+
+}  // namespace
 
 BundleFileRule::BundleFileRule(const Target* bundle_data_target,
                                const std::vector<SourceFile> sources,
@@ -22,10 +43,12 @@
 
 BundleFileRule::~BundleFileRule() = default;
 
-SourceFile BundleFileRule::ApplyPatternToSource(
-    const Settings* settings,
-    const BundleData& bundle_data,
-    const SourceFile& source_file) const {
+bool BundleFileRule::ApplyPatternToSource(const Settings* settings,
+                                          const Target* target,
+                                          const BundleData& bundle_data,
+                                          const SourceFile& source_file,
+                                          SourceFile* expanded_source_file,
+                                          Err* err) const {
   std::string output_path;
   for (const auto& subrange : pattern_.ranges()) {
     switch (subrange.type) {
@@ -33,18 +56,43 @@
         output_path.append(subrange.literal);
         break;
       case SUBSTITUTION_BUNDLE_ROOT_DIR:
+        if (bundle_data.contents_dir().is_null()) {
+          *err = ErrMissingPropertyForExpansion(settings, target, this,
+                                                variables::kBundleRootDir);
+          return false;
+        }
         output_path.append(bundle_data.root_dir().value());
         break;
       case SUBSTITUTION_BUNDLE_CONTENTS_DIR:
+        if (bundle_data.contents_dir().is_null()) {
+          *err = ErrMissingPropertyForExpansion(settings, target, this,
+                                                variables::kBundleContentsDir);
+          return false;
+        }
         output_path.append(bundle_data.contents_dir().value());
         break;
       case SUBSTITUTION_BUNDLE_RESOURCES_DIR:
+        if (bundle_data.resources_dir().is_null()) {
+          *err = ErrMissingPropertyForExpansion(settings, target, this,
+                                                variables::kBundleResourcesDir);
+          return false;
+        }
         output_path.append(bundle_data.resources_dir().value());
         break;
       case SUBSTITUTION_BUNDLE_EXECUTABLE_DIR:
+        if (bundle_data.executable_dir().is_null()) {
+          *err = ErrMissingPropertyForExpansion(
+              settings, target, this, variables::kBundleExecutableDir);
+          return false;
+        }
         output_path.append(bundle_data.executable_dir().value());
         break;
       case SUBSTITUTION_BUNDLE_PLUGINS_DIR:
+        if (bundle_data.contents_dir().is_null()) {
+          *err = ErrMissingPropertyForExpansion(settings, target, this,
+                                                variables::kBundlePlugInsDir);
+          return false;
+        }
         output_path.append(bundle_data.plugins_dir().value());
         break;
       default:
@@ -54,13 +102,24 @@
         break;
     }
   }
-  return SourceFile(SourceFile::SWAP_IN, &output_path);
+  *expanded_source_file = SourceFile(SourceFile::SWAP_IN, &output_path);
+  return true;
 }
 
-OutputFile BundleFileRule::ApplyPatternToSourceAsOutputFile(
+bool BundleFileRule::ApplyPatternToSourceAsOutputFile(
     const Settings* settings,
+    const Target* target,
     const BundleData& bundle_data,
-    const SourceFile& source_file) const {
-  return OutputFile(settings->build_settings(),
-                    ApplyPatternToSource(settings, bundle_data, source_file));
+    const SourceFile& source_file,
+    OutputFile* expanded_output_file,
+    Err* err) const {
+  SourceFile expanded_source_file;
+  if (!ApplyPatternToSource(settings, target, bundle_data, source_file,
+                            &expanded_source_file, err)) {
+    return false;
+  }
+
+  *expanded_output_file =
+      OutputFile(settings->build_settings(), expanded_source_file);
+  return true;
 }
diff --git a/tools/gn/bundle_file_rule.h b/tools/gn/bundle_file_rule.h
index 372e628..b7a2428 100644
--- a/tools/gn/bundle_file_rule.h
+++ b/tools/gn/bundle_file_rule.h
@@ -27,13 +27,18 @@
 
   // 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;
+  bool ApplyPatternToSource(const Settings* settings,
+                            const Target* target,
+                            const BundleData& bundle_data,
+                            const SourceFile& source_file,
+                            SourceFile* expanded_source_file,
+                            Err* err) const;
+  bool ApplyPatternToSourceAsOutputFile(const Settings* settings,
+                                        const Target* target,
+                                        const BundleData& bundle_data,
+                                        const SourceFile& source_file,
+                                        OutputFile* expanded_output_file,
+                                        Err* err) const;
 
   // Returns the associated target (of type Target::BUNDLE_DATA). May be
   // null during testing.
diff --git a/tools/gn/create_bundle_target_generator.cc b/tools/gn/create_bundle_target_generator.cc
index 79edd78..768697d 100644
--- a/tools/gn/create_bundle_target_generator.cc
+++ b/tools/gn/create_bundle_target_generator.cc
@@ -78,6 +78,8 @@
     const SourceDir& bundle_root_dir,
     const base::StringPiece& name,
     SourceDir* bundle_dir) {
+  // All bundle_foo_dir properties are optional. They are only required if they
+  // are used in an expansion. The check is performed there.
   const Value* value = scope_->GetValue(name, true);
   if (!value)
     return true;
diff --git a/tools/gn/desc_builder.cc b/tools/gn/desc_builder.cc
index e377f3e..d029843 100644
--- a/tools/gn/desc_builder.cc
+++ b/tools/gn/desc_builder.cc
@@ -685,9 +685,12 @@
       res->SetWithoutPathExpansion(variables::kOutputs, std::move(list));
     } else if (target_->output_type() == Target::CREATE_BUNDLE ||
                target_->output_type() == Target::GENERATED_FILE) {
+      Err err;
       std::vector<SourceFile> output_files;
-      target_->bundle_data().GetOutputsAsSourceFiles(target_->settings(),
-                                                     &output_files);
+      if (!target_->bundle_data().GetOutputsAsSourceFiles(
+              target_->settings(), target_, &output_files, &err)) {
+        err.PrintToStdout();
+      }
       res->SetWithoutPathExpansion(variables::kOutputs,
                                    RenderValue(output_files));
     } else if (target_->output_type() == Target::ACTION_FOREACH ||
diff --git a/tools/gn/functions_target.cc b/tools/gn/functions_target.cc
index 24fc248..72ccf93 100644
--- a/tools/gn/functions_target.cc
+++ b/tools/gn/functions_target.cc
@@ -327,8 +327,9 @@
   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.
+  The "bundle_*_dir" are be used for the expansion of {{bundle_*_dir}} rules in
+  "bundle_data" outputs. The properties are optional but must be defined if any
+  of the "bundle_data" target use them.
 
   This target can be used on all platforms though it is designed only to
   generate iOS or macOS bundle. In cross-platform projects, it is advised to put
@@ -358,13 +359,12 @@
 
 Variables
 
-  bundle_root_dir*, bundle_contents_dir*, bundle_resources_dir*,
-  bundle_executable_dir*, bundle_plugins_dir*, bundle_deps_filter, deps,
+  bundle_root_dir, bundle_contents_dir, bundle_resources_dir,
+  bundle_executable_dir, bundle_plugins_dir, bundle_deps_filter, deps,
   data_deps, public_deps, visibility, product_type, code_signing_args,
   code_signing_script, code_signing_sources, code_signing_outputs,
   xcode_extra_attributes, xcode_test_application_name, partial_info_plist,
   metadata
-  * = required
 
 Example
 
diff --git a/tools/gn/ninja_create_bundle_target_writer.cc b/tools/gn/ninja_create_bundle_target_writer.cc
index 9834aba..cc816e1 100644
--- a/tools/gn/ninja_create_bundle_target_writer.cc
+++ b/tools/gn/ninja_create_bundle_target_writer.cc
@@ -135,12 +135,18 @@
   // steps as this is most likely implemented using hardlink in the common case.
   // See NinjaCopyTargetWriter::WriteCopyRules() for a detailed explanation.
   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);
+    // There is no need to check for errors here as the substitution will have
+    // been performed when computing the list of output of the target during
+    // the Target::OnResolved phase earlier.
+    OutputFile expanded_output_file;
+    file_rule.ApplyPatternToSourceAsOutputFile(
+        settings_, target_, target_->bundle_data(), source_file,
+        &expanded_output_file,
+        /*err=*/nullptr);
+    output_files->push_back(expanded_output_file);
 
     out_ << "build ";
-    path_output_.WriteFile(out_, output_file);
+    path_output_.WriteFile(out_, expanded_output_file);
     out_ << ": " << GetNinjaRulePrefixForToolchain(settings_)
          << Toolchain::ToolTypeToName(Toolchain::TYPE_COPY_BUNDLE_DATA) << " ";
     path_output_.WriteFile(out_, source_file);
diff --git a/tools/gn/target.cc b/tools/gn/target.cc
index c61758b..94fb994 100644
--- a/tools/gn/target.cc
+++ b/tools/gn/target.cc
@@ -381,7 +381,8 @@
   if (!ResolvePrecompiledHeaders(err))
     return false;
 
-  FillOutputFiles();
+  if (!FillOutputFiles(err))
+    return false;
 
   if (!CheckVisibility(err))
     return false;
@@ -629,7 +630,7 @@
   bundle_data_.OnTargetResolved(this);
 }
 
-void Target::FillOutputFiles() {
+bool Target::FillOutputFiles(Err* err) {
   const Tool* tool = toolchain_->GetToolForTargetFinalOutput(this);
   bool check_tool_outputs = false;
   switch (output_type_) {
@@ -712,8 +713,10 @@
   }
 
   // Count anything generated from bundle_data dependencies.
-  if (output_type_ == CREATE_BUNDLE)
-    bundle_data_.GetOutputFiles(settings(), &computed_outputs_);
+  if (output_type_ == CREATE_BUNDLE) {
+    if (!bundle_data_.GetOutputFiles(settings(), this, &computed_outputs_, err))
+      return false;
+  }
 
   // Count all outputs from this tool as something generated by this target.
   if (check_tool_outputs) {
@@ -734,6 +737,8 @@
   action_values_.GetOutputsAsSourceFiles(this, &outputs_as_sources);
   for (const SourceFile& out : outputs_as_sources)
     computed_outputs_.push_back(OutputFile(settings()->build_settings(), out));
+
+  return true;
 }
 
 bool Target::ResolvePrecompiledHeaders(Err* err) {
diff --git a/tools/gn/target.h b/tools/gn/target.h
index abc357e..b73ae57 100644
--- a/tools/gn/target.h
+++ b/tools/gn/target.h
@@ -346,7 +346,7 @@
   void PullRecursiveBundleData();
 
   // Fills the link and dependency output files when a target is resolved.
-  void FillOutputFiles();
+  bool FillOutputFiles(Err* err);
 
   // Checks precompiled headers from configs and makes sure the resulting
   // values are in config_values_.
diff --git a/tools/gn/target_unittest.cc b/tools/gn/target_unittest.cc
index 9d9173e..b2d70fe 100644
--- a/tools/gn/target_unittest.cc
+++ b/tools/gn/target_unittest.cc
@@ -1044,12 +1044,18 @@
   e.public_deps().push_back(LabelTargetPair(&f));
   e.public_deps().push_back(LabelTargetPair(&b));
 
+  a.bundle_data().root_dir() = SourceDir("//out/foo_a.bundle");
+  a.bundle_data().resources_dir() = SourceDir("//out/foo_a.bundle/Resources");
+
   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));
 
+  c.bundle_data().root_dir() = SourceDir("//out/foo_e.bundle");
+  c.bundle_data().resources_dir() = SourceDir("//out/foo_e.bundle/Resources");
+
   d.sources().push_back(SourceFile("//foo/d.txt"));
   d.action_values().outputs() = SubstitutionList::MakeForTest(
       "{{bundle_resources_dir}}/{{source_file_part}}");