| // Copyright (c) 2013 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 "base/command_line.h" | 
 | #include "base/files/file_util.h" | 
 | #include "base/logging.h" | 
 | #include "base/strings/string_number_conversions.h" | 
 | #include "base/strings/utf_string_conversions.h" | 
 | #include "tools/gn/err.h" | 
 | #include "tools/gn/exec_process.h" | 
 | #include "tools/gn/filesystem_utils.h" | 
 | #include "tools/gn/functions.h" | 
 | #include "tools/gn/input_conversion.h" | 
 | #include "tools/gn/input_file.h" | 
 | #include "tools/gn/parse_tree.h" | 
 | #include "tools/gn/scheduler.h" | 
 | #include "tools/gn/trace.h" | 
 | #include "tools/gn/value.h" | 
 | #include "util/build_config.h" | 
 | #include "util/ticks.h" | 
 |  | 
 | namespace functions { | 
 |  | 
 | namespace { | 
 |  | 
 | bool CheckExecScriptPermissions(const BuildSettings* build_settings, | 
 |                                 const FunctionCallNode* function, | 
 |                                 Err* err) { | 
 |   const std::set<SourceFile>* whitelist = | 
 |       build_settings->exec_script_whitelist(); | 
 |   if (!whitelist) | 
 |     return true;  // No whitelist specified, don't check. | 
 |  | 
 |   LocationRange function_range = function->GetRange(); | 
 |   if (!function_range.begin().file()) | 
 |     return true;  // No file, might be some internal thing, implicitly pass. | 
 |  | 
 |   if (whitelist->find(function_range.begin().file()->name()) != | 
 |       whitelist->end()) | 
 |     return true;  // Whitelisted, this is OK. | 
 |  | 
 |   // Disallowed case. | 
 |   *err = Err( | 
 |       function, "Disallowed exec_script call.", | 
 |       "The use of exec_script use is restricted in this build. exec_script\n" | 
 |       "is discouraged because it can slow down the GN run and is easily\n" | 
 |       "abused.\n" | 
 |       "\n" | 
 |       "Generally nontrivial work should be done as build steps rather than\n" | 
 |       "when GN is run. For example, if you need to compute a nontrivial\n" | 
 |       "preprocessor define, it will be better to have an action target\n" | 
 |       "generate a header containing the define rather than blocking the GN\n" | 
 |       "run to compute the value.\n" | 
 |       "\n" | 
 |       "The allowed callers of exec_script is maintained in the \"//.gn\" file\n" | 
 |       "if you need to modify the whitelist."); | 
 |   return false; | 
 | } | 
 |  | 
 | }  // namespace | 
 |  | 
 | const char kExecScript[] = "exec_script"; | 
 | const char kExecScript_HelpShort[] = | 
 |     "exec_script: Synchronously run a script and return the output."; | 
 | const char kExecScript_Help[] = | 
 |     R"(exec_script: Synchronously run a script and return the output. | 
 |  | 
 |   exec_script(filename, | 
 |               arguments = [], | 
 |               input_conversion = "", | 
 |               file_dependencies = []) | 
 |  | 
 |   Runs the given script, returning the stdout of the script. The build | 
 |   generation will fail if the script does not exist or returns a nonzero exit | 
 |   code. | 
 |  | 
 |   The current directory when executing the script will be the root build | 
 |   directory. If you are passing file names, you will want to use the | 
 |   rebase_path() function to make file names relative to this path (see "gn help | 
 |   rebase_path"). | 
 |  | 
 | Arguments: | 
 |  | 
 |   filename: | 
 |       File name of python script to execute. Non-absolute names will be treated | 
 |       as relative to the current build file. | 
 |  | 
 |   arguments: | 
 |       A list of strings to be passed to the script as arguments. May be | 
 |       unspecified or the empty list which means no arguments. | 
 |  | 
 |   input_conversion: | 
 |       Controls how the file is read and parsed. See "gn help input_conversion". | 
 |  | 
 |       If unspecified, defaults to the empty string which causes the script | 
 |       result to be discarded. exec script will return None. | 
 |  | 
 |   dependencies: | 
 |       (Optional) A list of files that this script reads or otherwise depends | 
 |       on. These dependencies will be added to the build result such that if any | 
 |       of them change, the build will be regenerated and the script will be | 
 |       re-run. | 
 |  | 
 |       The script itself will be an implicit dependency so you do not need to | 
 |       list it. | 
 |  | 
 | Example | 
 |  | 
 |   all_lines = exec_script( | 
 |       "myscript.py", [some_input], "list lines", | 
 |       [ rebase_path("data_file.txt", root_build_dir) ]) | 
 |  | 
 |   # This example just calls the script with no arguments and discards the | 
 |   # result. | 
 |   exec_script("//foo/bar/myscript.py") | 
 | )"; | 
 |  | 
 | Value RunExecScript(Scope* scope, | 
 |                     const FunctionCallNode* function, | 
 |                     const std::vector<Value>& args, | 
 |                     Err* err) { | 
 |   if (args.size() < 1 || args.size() > 4) { | 
 |     *err = Err(function->function(), "Wrong number of arguments to exec_script", | 
 |                "I expected between one and four arguments."); | 
 |     return Value(); | 
 |   } | 
 |  | 
 |   const Settings* settings = scope->settings(); | 
 |   const BuildSettings* build_settings = settings->build_settings(); | 
 |   const SourceDir& cur_dir = scope->GetSourceDir(); | 
 |  | 
 |   if (!CheckExecScriptPermissions(build_settings, function, err)) | 
 |     return Value(); | 
 |  | 
 |   // Find the python script to run. | 
 |   std::string script_source_path = cur_dir.ResolveRelativeAs( | 
 |       true, args[0], err, | 
 |       scope->settings()->build_settings()->root_path_utf8()); | 
 |   if (err->has_error()) | 
 |     return Value(); | 
 |   base::FilePath script_path = | 
 |       build_settings->GetFullPath(script_source_path, true); | 
 |   if (!build_settings->secondary_source_path().empty() && | 
 |       !base::PathExists(script_path)) { | 
 |     // Fall back to secondary source root when the file doesn't exist. | 
 |     script_path = | 
 |         build_settings->GetFullPathSecondary(script_source_path, true); | 
 |   } | 
 |  | 
 |   ScopedTrace trace(TraceItem::TRACE_SCRIPT_EXECUTE, script_source_path); | 
 |   trace.SetToolchain(settings->toolchain_label()); | 
 |  | 
 |   // Add all dependencies of this script, including the script itself, to the | 
 |   // build deps. | 
 |   g_scheduler->AddGenDependency(script_path); | 
 |   if (args.size() == 4) { | 
 |     const Value& deps_value = args[3]; | 
 |     if (!deps_value.VerifyTypeIs(Value::LIST, err)) | 
 |       return Value(); | 
 |  | 
 |     for (const auto& dep : deps_value.list_value()) { | 
 |       if (!dep.VerifyTypeIs(Value::STRING, err)) | 
 |         return Value(); | 
 |       g_scheduler->AddGenDependency(build_settings->GetFullPath( | 
 |           cur_dir.ResolveRelativeAs( | 
 |               true, dep, err, | 
 |               scope->settings()->build_settings()->root_path_utf8()), | 
 |           true)); | 
 |       if (err->has_error()) | 
 |         return Value(); | 
 |     } | 
 |   } | 
 |  | 
 |   // Make the command line. | 
 |   const base::FilePath& python_path = build_settings->python_path(); | 
 |   base::CommandLine cmdline(python_path); | 
 |  | 
 |   // CommandLine tries to interpret arguments by default.  Passing "--" disables | 
 |   // this for everything following the "--", so pass this as the very first | 
 |   // thing to python.  Python ignores a -- before the .py file, and this makes | 
 |   // CommandLine let through arguments without modifying them. | 
 |   cmdline.AppendArg("--"); | 
 |  | 
 |   cmdline.AppendArgPath(script_path); | 
 |  | 
 |   if (args.size() >= 2) { | 
 |     // Optional command-line arguments to the script. | 
 |     const Value& script_args = args[1]; | 
 |     if (!script_args.VerifyTypeIs(Value::LIST, err)) | 
 |       return Value(); | 
 |     for (const auto& arg : script_args.list_value()) { | 
 |       if (!arg.VerifyTypeIs(Value::STRING, err)) | 
 |         return Value(); | 
 |       cmdline.AppendArg(arg.string_value()); | 
 |     } | 
 |   } | 
 |  | 
 |   // Log command line for debugging help. | 
 |   trace.SetCommandLine(cmdline); | 
 |   Ticks begin_exec = 0; | 
 |   if (g_scheduler->verbose_logging()) { | 
 | #if defined(OS_WIN) | 
 |     g_scheduler->Log("Pythoning", | 
 |                      base::UTF16ToUTF8(cmdline.GetCommandLineString())); | 
 | #else | 
 |     g_scheduler->Log("Pythoning", cmdline.GetCommandLineString()); | 
 | #endif | 
 |     begin_exec = TicksNow(); | 
 |   } | 
 |  | 
 |   base::FilePath startup_dir = | 
 |       build_settings->GetFullPath(build_settings->build_dir()); | 
 |   // The first time a build is run, no targets will have been written so the | 
 |   // build output directory won't exist. We need to make sure it does before | 
 |   // running any scripts with this as its startup directory, although it will | 
 |   // be relatively rare that the directory won't exist by the time we get here. | 
 |   // | 
 |   // If this shows up on benchmarks, we can cache whether we've done this | 
 |   // or not and skip creating the directory. | 
 |   base::CreateDirectory(startup_dir); | 
 |  | 
 |   // Execute the process. | 
 |   // TODO(brettw) set the environment block. | 
 |   std::string output; | 
 |   std::string stderr_output; | 
 |   int exit_code = 0; | 
 |   { | 
 |     if (!internal::ExecProcess(cmdline, startup_dir, &output, &stderr_output, | 
 |                                &exit_code)) { | 
 |       *err = Err( | 
 |           function->function(), "Could not execute python.", | 
 |           "I was trying to execute \"" + FilePathToUTF8(python_path) + "\"."); | 
 |       return Value(); | 
 |     } | 
 |   } | 
 |   if (g_scheduler->verbose_logging()) { | 
 |     g_scheduler->Log( | 
 |         "Pythoning", | 
 |         script_source_path + " took " + | 
 |             base::Int64ToString( | 
 |                 TicksDelta(TicksNow(), begin_exec).InMilliseconds()) + | 
 |             "ms"); | 
 |   } | 
 |  | 
 |   if (exit_code != 0) { | 
 |     std::string msg = | 
 |         "Current dir: " + FilePathToUTF8(startup_dir) + | 
 |         "\nCommand: " + FilePathToUTF8(cmdline.GetCommandLineString()) + | 
 |         "\nReturned " + base::IntToString(exit_code); | 
 |     if (!output.empty()) | 
 |       msg += " and printed out:\n\n" + output; | 
 |     else | 
 |       msg += "."; | 
 |     if (!stderr_output.empty()) | 
 |       msg += "\nstderr:\n\n" + stderr_output; | 
 |  | 
 |     *err = | 
 |         Err(function->function(), "Script returned non-zero exit code.", msg); | 
 |     return Value(); | 
 |   } | 
 |  | 
 |   // Default to None value for the input conversion if unspecified. | 
 |   return ConvertInputToValue(scope->settings(), output, function, | 
 |                              args.size() >= 3 ? args[2] : Value(), err); | 
 | } | 
 |  | 
 | }  // namespace functions |