• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "base/command_line.h"
6 #include "base/files/file_util.h"
7 #include "base/logging.h"
8 #include "base/strings/string_number_conversions.h"
9 #include "base/strings/utf_string_conversions.h"
10 #include "gn/err.h"
11 #include "gn/exec_process.h"
12 #include "gn/filesystem_utils.h"
13 #include "gn/functions.h"
14 #include "gn/input_conversion.h"
15 #include "gn/input_file.h"
16 #include "gn/parse_tree.h"
17 #include "gn/scheduler.h"
18 #include "gn/trace.h"
19 #include "gn/value.h"
20 #include "util/build_config.h"
21 #include "util/ticks.h"
22 
23 namespace functions {
24 
25 namespace {
26 
CheckExecScriptPermissions(const BuildSettings * build_settings,const FunctionCallNode * function,Err * err)27 bool CheckExecScriptPermissions(const BuildSettings* build_settings,
28                                 const FunctionCallNode* function,
29                                 Err* err) {
30   const SourceFileSet* whitelist = build_settings->exec_script_whitelist();
31   if (!whitelist)
32     return true;  // No whitelist specified, don't check.
33 
34   LocationRange function_range = function->GetRange();
35   if (!function_range.begin().file())
36     return true;  // No file, might be some internal thing, implicitly pass.
37 
38   if (whitelist->find(function_range.begin().file()->name()) !=
39       whitelist->end())
40     return true;  // Whitelisted, this is OK.
41 
42   // Disallowed case.
43   *err = Err(
44       function, "Disallowed exec_script call.",
45       "The use of exec_script use is restricted in this build. exec_script\n"
46       "is discouraged because it can slow down the GN run and is easily\n"
47       "abused.\n"
48       "\n"
49       "Generally nontrivial work should be done as build steps rather than\n"
50       "when GN is run. For example, if you need to compute a nontrivial\n"
51       "preprocessor define, it will be better to have an action target\n"
52       "generate a header containing the define rather than blocking the GN\n"
53       "run to compute the value.\n"
54       "\n"
55       "The allowed callers of exec_script is maintained in the \"//.gn\" file\n"
56       "if you need to modify the whitelist.");
57   return false;
58 }
59 
60 }  // namespace
61 
62 const char kExecScript[] = "exec_script";
63 const char kExecScript_HelpShort[] =
64     "exec_script: Synchronously run a script and return the output.";
65 const char kExecScript_Help[] =
66     R"(exec_script: Synchronously run a script and return the output.
67 
68   exec_script(filename,
69               arguments = [],
70               input_conversion = "",
71               file_dependencies = [])
72 
73   Runs the given script, returning the stdout of the script. The build
74   generation will fail if the script does not exist or returns a nonzero exit
75   code.
76 
77   The current directory when executing the script will be the root build
78   directory. If you are passing file names, you will want to use the
79   rebase_path() function to make file names relative to this path (see "gn help
80   rebase_path").
81 
82   The default script interpreter is Python ("python" on POSIX, "python.exe" or
83   "python.bat" on Windows). This can be configured by the script_executable
84   variable, see "gn help dotfile".
85 
86 Arguments:
87 
88   filename:
89       File name of script to execute. Non-absolute names will be treated as
90       relative to the current build file.
91 
92   arguments:
93       A list of strings to be passed to the script as arguments. May be
94       unspecified or the empty list which means no arguments.
95 
96   input_conversion:
97       Controls how the file is read and parsed. See `gn help io_conversion`.
98 
99       If unspecified, defaults to the empty string which causes the script
100       result to be discarded. exec script will return None.
101 
102   dependencies:
103       (Optional) A list of files that this script reads or otherwise depends
104       on. These dependencies will be added to the build result such that if any
105       of them change, the build will be regenerated and the script will be
106       re-run.
107 
108       The script itself will be an implicit dependency so you do not need to
109       list it.
110 
111 Example
112 
113   all_lines = exec_script(
114       "myscript.py", [some_input], "list lines",
115       [ rebase_path("data_file.txt", root_build_dir) ])
116 
117   # This example just calls the script with no arguments and discards the
118   # result.
119   exec_script("//foo/bar/myscript.py")
120 )";
121 
RunExecScript(Scope * scope,const FunctionCallNode * function,const std::vector<Value> & args,Err * err)122 Value RunExecScript(Scope* scope,
123                     const FunctionCallNode* function,
124                     const std::vector<Value>& args,
125                     Err* err) {
126   if (args.size() < 1 || args.size() > 4) {
127     *err = Err(function->function(), "Wrong number of arguments to exec_script",
128                "I expected between one and four arguments.");
129     return Value();
130   }
131 
132   const Settings* settings = scope->settings();
133   const BuildSettings* build_settings = settings->build_settings();
134   const SourceDir& cur_dir = scope->GetSourceDir();
135 
136   if (!CheckExecScriptPermissions(build_settings, function, err))
137     return Value();
138 
139   // Find the script to run.
140   std::string script_source_path = cur_dir.ResolveRelativeAs(
141       true, args[0], err,
142       scope->settings()->build_settings()->root_path_utf8());
143   if (err->has_error())
144     return Value();
145   base::FilePath script_path =
146       build_settings->GetFullPath(script_source_path, true);
147   if (!build_settings->secondary_source_path().empty() &&
148       !base::PathExists(script_path)) {
149     // Fall back to secondary source root when the file doesn't exist.
150     script_path =
151         build_settings->GetFullPathSecondary(script_source_path, true);
152   }
153 
154   ScopedTrace trace(TraceItem::TRACE_SCRIPT_EXECUTE, script_source_path);
155   trace.SetToolchain(settings->toolchain_label());
156 
157   // Add all dependencies of this script, including the script itself, to the
158   // build deps.
159   g_scheduler->AddGenDependency(script_path);
160   if (args.size() == 4) {
161     const Value& deps_value = args[3];
162     if (!deps_value.VerifyTypeIs(Value::LIST, err))
163       return Value();
164 
165     for (const auto& dep : deps_value.list_value()) {
166       if (!dep.VerifyTypeIs(Value::STRING, err))
167         return Value();
168       g_scheduler->AddGenDependency(build_settings->GetFullPath(
169           cur_dir.ResolveRelativeAs(
170               true, dep, err,
171               scope->settings()->build_settings()->root_path_utf8()),
172           true));
173       if (err->has_error())
174         return Value();
175     }
176   }
177 
178   // Make the command line.
179   base::CommandLine cmdline(base::CommandLine::NO_PROGRAM);
180 
181   // CommandLine tries to interpret arguments by default.  Disable that so
182   // that the arguments will be passed through exactly as specified.
183   cmdline.SetParseSwitches(false);
184 
185   // If an interpreter path is set, initialize it as the first entry and
186   // pass script_path as the first argument. Otherwise, set the
187   // program to script_path directly.
188   const base::FilePath& interpreter_path = build_settings->python_path();
189   if (!interpreter_path.empty()) {
190     cmdline.SetProgram(interpreter_path);
191     cmdline.AppendArgPath(script_path);
192   } else {
193     cmdline.SetProgram(script_path);
194   }
195 
196   if (args.size() >= 2) {
197     // Optional command-line arguments to the script.
198     const Value& script_args = args[1];
199     if (!script_args.VerifyTypeIs(Value::LIST, err))
200       return Value();
201     for (const auto& arg : script_args.list_value()) {
202       if (!arg.VerifyTypeIs(Value::STRING, err))
203         return Value();
204       cmdline.AppendArg(arg.string_value());
205     }
206   }
207 
208   // Log command line for debugging help.
209   trace.SetCommandLine(cmdline);
210   Ticks begin_exec = 0;
211   if (g_scheduler->verbose_logging()) {
212 #if defined(OS_WIN)
213     g_scheduler->Log("Executing",
214                      base::UTF16ToUTF8(cmdline.GetCommandLineString()));
215 #else
216     g_scheduler->Log("Executing", cmdline.GetCommandLineString());
217 #endif
218     begin_exec = TicksNow();
219   }
220 
221   base::FilePath startup_dir =
222       build_settings->GetFullPath(build_settings->build_dir());
223   // The first time a build is run, no targets will have been written so the
224   // build output directory won't exist. We need to make sure it does before
225   // running any scripts with this as its startup directory, although it will
226   // be relatively rare that the directory won't exist by the time we get here.
227   //
228   // If this shows up on benchmarks, we can cache whether we've done this
229   // or not and skip creating the directory.
230   base::CreateDirectory(startup_dir);
231 
232   // Execute the process.
233   // TODO(brettw) set the environment block.
234   std::string output;
235   std::string stderr_output;
236   int exit_code = 0;
237   {
238     if (!internal::ExecProcess(cmdline, startup_dir, &output, &stderr_output,
239                                &exit_code)) {
240       *err = Err(
241           function->function(), "Could not execute interpreter.",
242           "I was trying to execute \"" + FilePathToUTF8(interpreter_path) +
243           "\".");
244       return Value();
245     }
246   }
247   if (g_scheduler->verbose_logging()) {
248     g_scheduler->Log(
249         "Executing",
250         script_source_path + " took " +
251             base::Int64ToString(
252                 TicksDelta(TicksNow(), begin_exec).InMilliseconds()) +
253             "ms");
254   }
255 
256   if (exit_code != 0) {
257     std::string msg =
258         "Current dir: " + FilePathToUTF8(startup_dir) +
259         "\nCommand: " + FilePathToUTF8(cmdline.GetCommandLineString()) +
260         "\nReturned " + base::IntToString(exit_code);
261     if (!output.empty())
262       msg += " and printed out:\n\n" + output;
263     else
264       msg += ".";
265     if (!stderr_output.empty())
266       msg += "\nstderr:\n\n" + stderr_output;
267 
268     *err =
269         Err(function->function(), "Script returned non-zero exit code.", msg);
270     return Value();
271   }
272 
273   // Default to None value for the input conversion if unspecified.
274   return ConvertInputToValue(scope->settings(), output, function,
275                              args.size() >= 3 ? args[2] : Value(), err);
276 }
277 
278 }  // namespace functions
279