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