blob: 08d9b0f3e4a3a9b5cee2b8a2984dfe1756090bdc [file] [log] [blame]
Takuto Ikuta237cc462022-01-19 18:04:15 +00001#!/usr/bin/env python3
Bruce Dawsonebebd952017-05-31 14:24:38 -07002# Copyright (c) 2017 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
Bruce Dawsonebebd952017-05-31 14:24:38 -07005"""
6This script (intended to be invoked by autoninja or autoninja.bat) detects
Simeon Anfinrud5dba9c92020-09-03 20:02:05 +00007whether a build is accelerated using a service like goma. If so, it runs with a
8large -j value, and otherwise it chooses a small one. This auto-adjustment
9makes using remote build acceleration simpler and safer, and avoids errors that
10can cause slow goma builds or swap-storms on unaccelerated builds.
Bruce Dawson30c1cba2023-09-15 18:20:32 +000011
12autoninja tries to detect relevant build settings such as use_remoteexec, and it
13does handle import statements, but it can't handle conditional setting of build
14settings.
Bruce Dawsonebebd952017-05-31 14:24:38 -070015"""
16
Bruce Dawsone952fae2021-02-27 23:33:37 +000017import multiprocessing
Bruce Dawsonebebd952017-05-31 14:24:38 -070018import os
Takuto Ikuta6a1494e2022-05-06 01:22:16 +000019import platform
Bruce Dawsonebebd952017-05-31 14:24:38 -070020import re
Bruce Dawsone952fae2021-02-27 23:33:37 +000021import subprocess
Bruce Dawsonebebd952017-05-31 14:24:38 -070022import sys
23
Henrique Ferreiro8bde1642023-09-14 11:41:19 +000024if sys.platform in ['darwin', 'linux']:
Mike Frysinger124bb8e2023-09-06 05:48:55 +000025 import resource
Sylvain Defresne7b4ecc72023-07-27 16:24:54 +000026
Yoshisato Yanagisawa4b497072018-11-07 02:52:33 +000027SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
28
Yoshisato Yanagisawaf66e5512018-11-15 00:40:39 +000029
Bruce Dawson30c1cba2023-09-15 18:20:32 +000030def _gn_lines(output_dir, path):
31 """
32 Generator function that returns args.gn lines one at a time, following
33 import directives as needed.
34 """
35 import_re = re.compile(r'\s*import\("(.*)"\)')
36 with open(path, encoding='utf-8') as f:
37 for line in f:
38 match = import_re.match(line)
39 if match:
Samuel Attard8a259982023-09-18 17:12:55 +000040 raw_import_path = match.groups()[0]
41 if raw_import_path[:2] == "//":
42 import_path = os.path.normpath(
43 os.path.join(output_dir, '..', '..',
44 raw_import_path[2:]))
45 else:
46 import_path = os.path.normpath(
47 os.path.join(os.path.dirname(path), raw_import_path))
Bruce Dawson30c1cba2023-09-15 18:20:32 +000048 for import_line in _gn_lines(output_dir, import_path):
49 yield import_line
50 else:
51 yield line
52
53
Takuto Ikuta381db682022-04-27 23:54:02 +000054def main(args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000055 # The -t tools are incompatible with -j
56 t_specified = False
57 j_specified = False
58 offline = False
59 output_dir = '.'
60 input_args = args
61 # On Windows the autoninja.bat script passes along the arguments enclosed in
62 # double quotes. This prevents multiple levels of parsing of the special '^'
63 # characters needed when compiling a single file but means that this script
64 # gets called with a single argument containing all of the actual arguments,
65 # separated by spaces. When this case is detected we need to do argument
66 # splitting ourselves. This means that arguments containing actual spaces
67 # are not supported by autoninja, but that is not a real limitation.
68 if (sys.platform.startswith('win') and len(args) == 2
69 and input_args[1].count(' ') > 0):
70 input_args = args[:1] + args[1].split()
Bruce Dawson655afeb2020-11-02 23:30:37 +000071
Mike Frysinger124bb8e2023-09-06 05:48:55 +000072 # Ninja uses getopt_long, which allow to intermix non-option arguments.
73 # To leave non supported parameters untouched, we do not use getopt.
74 for index, arg in enumerate(input_args[1:]):
75 if arg.startswith('-j'):
76 j_specified = True
77 if arg.startswith('-t'):
78 t_specified = True
79 if arg == '-C':
80 # + 1 to get the next argument and +1 because we trimmed off
81 # input_args[0]
82 output_dir = input_args[index + 2]
83 elif arg.startswith('-C'):
84 # Support -Cout/Default
85 output_dir = arg[2:]
86 elif arg in ('-o', '--offline'):
87 offline = True
88 elif arg == '-h':
89 print('autoninja: Use -o/--offline to temporary disable goma.',
90 file=sys.stderr)
91 print(file=sys.stderr)
Bruce Dawsone952fae2021-02-27 23:33:37 +000092
Takuto Ikuta381db682022-04-27 23:54:02 +000093 use_goma = False
Mike Frysinger124bb8e2023-09-06 05:48:55 +000094 use_remoteexec = False
95 use_rbe = False
96 use_siso = False
Yoshisato Yanagisawa43a35d22018-11-15 03:00:51 +000097
Mike Frysinger124bb8e2023-09-06 05:48:55 +000098 # Attempt to auto-detect remote build acceleration. We support gn-based
99 # builds, where we look for args.gn in the build tree, and cmake-based
100 # builds where we look for rules.ninja.
101 if os.path.exists(os.path.join(output_dir, 'args.gn')):
Bruce Dawson30c1cba2023-09-15 18:20:32 +0000102 for line in _gn_lines(output_dir, os.path.join(output_dir, 'args.gn')):
103 # use_goma, use_remoteexec, or use_rbe will activate build
104 # acceleration.
105 #
106 # This test can match multi-argument lines. Examples of this
107 # are: is_debug=false use_goma=true is_official_build=false
108 # use_goma=false# use_goma=true This comment is ignored
109 #
110 # Anything after a comment is not consider a valid argument.
111 line_without_comment = line.split('#')[0]
112 if re.search(r'(^|\s)(use_goma)\s*=\s*true($|\s)',
113 line_without_comment):
114 use_goma = True
115 continue
116 if re.search(r'(^|\s)(use_remoteexec)\s*=\s*true($|\s)',
117 line_without_comment):
118 use_remoteexec = True
119 continue
120 if re.search(r'(^|\s)(use_rbe)\s*=\s*true($|\s)',
121 line_without_comment):
122 use_rbe = True
123 continue
124 if re.search(r'(^|\s)(use_siso)\s*=\s*true($|\s)',
125 line_without_comment):
126 use_siso = True
127 continue
Bruce Dawsonb3b46a22019-09-06 15:57:52 +0000128
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000129 siso_marker = os.path.join(output_dir, '.siso_deps')
130 if use_siso:
131 ninja_marker = os.path.join(output_dir, '.ninja_log')
132 # autosiso generates a .ninja_log file so the mere existence of a
133 # .ninja_log file doesn't imply that a ninja build was done. However
134 # if there is a .ninja_log but no .siso_deps then that implies a
135 # ninja build.
136 if os.path.exists(ninja_marker) and not os.path.exists(siso_marker):
137 return (
138 'echo Run gn clean before switching from ninja to siso in '
139 '%s' % output_dir)
140 siso = ['autosiso'] if use_remoteexec else ['siso', 'ninja']
141 if sys.platform.startswith('win'):
142 # An explicit 'call' is needed to make sure the invocation of
143 # autosiso returns to autoninja.bat, and the command prompt
144 # title gets reset.
145 siso = ['call'] + siso
146 return ' '.join(siso + input_args[1:])
Michael Savigny20eda952021-01-20 01:16:27 +0000147
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000148 if os.path.exists(siso_marker):
149 return (
150 'echo Run gn clean before switching from siso to ninja in %s' %
151 output_dir)
Ben Segall467991e2023-08-09 19:02:09 +0000152
Takuto Ikuta381db682022-04-27 23:54:02 +0000153 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000154 for relative_path in [
155 '', # GN keeps them in the root of output_dir
156 'CMakeFiles'
157 ]:
158 path = os.path.join(output_dir, relative_path, 'rules.ninja')
159 if os.path.exists(path):
Bruce Dawson30c1cba2023-09-15 18:20:32 +0000160 with open(path, encoding='utf-8') as file_handle:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000161 for line in file_handle:
162 if re.match(r'^\s*command\s*=\s*\S+gomacc', line):
163 use_goma = True
164 break
Takuto Ikuta381db682022-04-27 23:54:02 +0000165
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000166 # Strip -o/--offline so ninja doesn't see them.
167 input_args = [arg for arg in input_args if arg not in ('-o', '--offline')]
Takuto Ikuta381db682022-04-27 23:54:02 +0000168
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000169 # If GOMA_DISABLED is set to "true", "t", "yes", "y", or "1"
170 # (case-insensitive) then gomacc will use the local compiler instead of
171 # doing a goma compile. This is convenient if you want to briefly disable
172 # goma. It avoids having to rebuild the world when transitioning between
173 # goma/non-goma builds. However, it is not as fast as doing a "normal"
174 # non-goma build because an extra process is created for each compile step.
175 # Checking this environment variable ensures that autoninja uses an
176 # appropriate -j value in this situation.
177 goma_disabled_env = os.environ.get('GOMA_DISABLED', '0').lower()
178 if offline or goma_disabled_env in ['true', 't', 'yes', 'y', '1']:
179 use_goma = False
Takuto Ikuta381db682022-04-27 23:54:02 +0000180
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000181 if use_goma:
182 gomacc_file = 'gomacc.exe' if sys.platform.startswith(
183 'win') else 'gomacc'
184 goma_dir = os.environ.get('GOMA_DIR',
185 os.path.join(SCRIPT_DIR, '.cipd_bin'))
186 gomacc_path = os.path.join(goma_dir, gomacc_file)
187 # Don't invoke gomacc if it doesn't exist.
188 if os.path.exists(gomacc_path):
189 # Check to make sure that goma is running. If not, don't start the
190 # build.
191 status = subprocess.call([gomacc_path, 'port'],
192 stdout=subprocess.PIPE,
193 stderr=subprocess.PIPE,
194 shell=False)
195 if status == 1:
196 print(
197 'Goma is not running. Use "goma_ctl ensure_start" to start '
198 'it.',
199 file=sys.stderr)
200 if sys.platform.startswith('win'):
201 # Set an exit code of 1 in the batch file.
202 print('cmd "/c exit 1"')
203 else:
204 # Set an exit code of 1 by executing 'false' in the bash
205 # script.
206 print('false')
207 sys.exit(1)
208
209 # A large build (with or without goma) tends to hog all system resources.
210 # Launching the ninja process with 'nice' priorities improves this
211 # situation.
212 prefix_args = []
213 if (sys.platform.startswith('linux')
214 and os.environ.get('NINJA_BUILD_IN_BACKGROUND', '0') == '1'):
215 # nice -10 is process priority 10 lower than default 0
216 # ionice -c 3 is IO priority IDLE
217 prefix_args = ['nice'] + ['-10']
218
219 # Tell goma or reclient to do local compiles. On Windows these environment
220 # variables are set by the wrapper batch file.
221 offline_env = ['RBE_remote_disabled=1', 'GOMA_DISABLED=1'
222 ] if offline and not sys.platform.startswith('win') else []
223
Henrique Ferreiro8bde1642023-09-14 11:41:19 +0000224 # On macOS and most Linux distributions, the default limit of open file
225 # descriptors is too low (256 and 1024, respectively).
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000226 # This causes a large j value to result in 'Too many open files' errors.
227 # Check whether the limit can be raised to a large enough value. If yes,
228 # use `ulimit -n .... &&` as a prefix to increase the limit when running
229 # ninja.
Henrique Ferreiro8bde1642023-09-14 11:41:19 +0000230 prepend_command = []
231 if sys.platform in ['darwin', 'linux']:
232 # Increase the number of allowed open file descriptors to the maximum.
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000233 fileno_limit, hard_limit = resource.getrlimit(resource.RLIMIT_NOFILE)
Henrique Ferreiro8bde1642023-09-14 11:41:19 +0000234 if fileno_limit < hard_limit:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000235 try:
236 resource.setrlimit(resource.RLIMIT_NOFILE,
Henrique Ferreiro8bde1642023-09-14 11:41:19 +0000237 (hard_limit, hard_limit))
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000238 except Exception as _:
239 pass
240 fileno_limit, hard_limit = resource.getrlimit(
241 resource.RLIMIT_NOFILE)
Henrique Ferreiro8bde1642023-09-14 11:41:19 +0000242 if fileno_limit == hard_limit:
243 prepend_command = ['ulimit', '-n', f'{fileno_limit}', '&&']
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000244
245 # Call ninja.py so that it can find ninja binary installed by DEPS or one in
246 # PATH.
247 ninja_path = os.path.join(SCRIPT_DIR, 'ninja.py')
248 # If using remoteexec, use ninja_reclient.py which wraps ninja.py with
249 # starting and stopping reproxy.
250 if use_remoteexec:
251 ninja_path = os.path.join(SCRIPT_DIR, 'ninja_reclient.py')
252
Henrique Ferreiro8bde1642023-09-14 11:41:19 +0000253 args = prepend_command + offline_env + prefix_args + [
254 sys.executable, ninja_path
255 ] + input_args[1:]
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000256
257 num_cores = multiprocessing.cpu_count()
258 if not j_specified and not t_specified:
259 if not offline and (use_goma or use_remoteexec or use_rbe):
260 args.append('-j')
261 default_core_multiplier = 80
262 if platform.machine() in ('x86_64', 'AMD64'):
263 # Assume simultaneous multithreading and therefore half as many
264 # cores as logical processors.
265 num_cores //= 2
266
267 core_multiplier = int(
268 os.environ.get('NINJA_CORE_MULTIPLIER',
269 default_core_multiplier))
270 j_value = num_cores * core_multiplier
271
272 core_limit = int(os.environ.get('NINJA_CORE_LIMIT', j_value))
273 j_value = min(j_value, core_limit)
274
Henrique Ferreiro8bde1642023-09-14 11:41:19 +0000275 # On Windows, a -j higher than 1000 doesn't improve build times.
Henrique Ferreiro7eb4e482023-09-20 20:25:20 +0000276 # On macOS, ninja is limited to at most FD_SETSIZE (1024) open file
Henrique Ferreiro8bde1642023-09-14 11:41:19 +0000277 # descriptors.
Henrique Ferreiro7eb4e482023-09-20 20:25:20 +0000278 if sys.platform in ['darwin', 'win32']:
279 j_value = min(j_value, 1000)
280
281 # Use a j value that reliably works with the open file descriptors
282 # limit.
Henrique Ferreiro8bde1642023-09-14 11:41:19 +0000283 if sys.platform in ['darwin', 'linux']:
Henrique Ferreiro8bde1642023-09-14 11:41:19 +0000284 j_value = min(j_value, int(fileno_limit * 0.8))
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000285
286 args.append('%d' % j_value)
287 else:
288 j_value = num_cores
289 # Ninja defaults to |num_cores + 2|
290 j_value += int(os.environ.get('NINJA_CORE_ADDITION', '2'))
291 args.append('-j')
292 args.append('%d' % j_value)
293
294 # On Windows, fully quote the path so that the command processor doesn't
295 # think the whole output is the command. On Linux and Mac, if people put
296 # depot_tools in directories with ' ', shell would misunderstand ' ' as a
297 # path separation. TODO(yyanagisawa): provide proper quoting for Windows.
298 # see https://cs.chromium.org/chromium/src/tools/mb/mb.py
299 for i in range(len(args)):
300 if (i == 0 and sys.platform.startswith('win')) or ' ' in args[i]:
301 args[i] = '"%s"' % args[i].replace('"', '\\"')
302
303 if os.environ.get('NINJA_SUMMARIZE_BUILD', '0') == '1':
304 args += ['-d', 'stats']
305
306 return ' '.join(args)
Takuto Ikuta381db682022-04-27 23:54:02 +0000307
308
309if __name__ == '__main__':
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000310 print(main(sys.argv))