Files
sdk/tools/build.py
T
Ryan Macnak 60e76a406b [build] Pass targets from build.py to ninja unchanged.
Passing the empty set of targets to build.py now passes the empty set to ninja instead of substituting "all". Because ninja's default target is "default" instead of "all", this changes the meaning of `build.py` to `build.py default` instead of `build.py all`.

Bug: https://github.com/dart-lang/sdk/issues/52442
Change-Id: Ia82b7bdfc54a5dafdfe7a6426e5620ad7532dd87
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/305061
Reviewed-by: Derek Xu <derekx@google.com>
Commit-Queue: Ryan Macnak <rmacnak@google.com>
2023-05-23 20:34:00 +00:00

307 lines
10 KiB
Python
Executable File

#!/usr/bin/env python3
#
# Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
# for details. All rights reserved. Use of this source code is governed by a
# BSD-style license that can be found in the LICENSE file.
import argparse
import io
import json
import os
import subprocess
import sys
import time
import utils
import gn as gn_py
HOST_OS = utils.GuessOS()
HOST_CPUS = utils.GuessCpus()
SCRIPT_DIR = os.path.dirname(sys.argv[0])
DART_ROOT = os.path.realpath(os.path.join(SCRIPT_DIR, '..'))
AVAILABLE_ARCHS = utils.ARCH_FAMILY.keys()
usage = """\
usage: %%prog [options] [targets]
This script invokes ninja to build Dart.
"""
def BuildOptions():
parser = argparse.ArgumentParser(
description='Runs GN (if necessary) followed by ninja',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
config_group = parser.add_argument_group('Configuration Related Arguments')
gn_py.AddCommonConfigurationArgs(config_group)
gn_group = parser.add_argument_group('GN Related Arguments')
gn_py.AddCommonGnOptionArgs(gn_group)
other_group = parser.add_argument_group('Other Arguments')
gn_py.AddOtherArgs(other_group)
other_group.add_argument("-j",
type=int,
help='Ninja -j option for Goma builds.',
default=1000)
other_group.add_argument("-l",
type=int,
help='Ninja -l option for Goma builds.',
default=64)
other_group.add_argument("--no-start-goma",
help="Don't try to start goma",
default=False,
action='store_true')
other_group.add_argument(
"--check-clean",
help="Check that a second invocation of Ninja has nothing to do",
default=False,
action='store_true')
parser.add_argument('build_targets', nargs='*')
return parser
def NotifyBuildDone(build_config, success, start):
if not success:
print("BUILD FAILED")
sys.stdout.flush()
# Display a notification if build time exceeded DART_BUILD_NOTIFICATION_DELAY.
notification_delay = float(
os.getenv('DART_BUILD_NOTIFICATION_DELAY', sys.float_info.max))
if (time.time() - start) < notification_delay:
return
if success:
message = 'Build succeeded.'
else:
message = 'Build failed.'
title = build_config
command = None
if HOST_OS == 'macos':
# Use AppleScript to display a UI non-modal notification.
script = 'display notification "%s" with title "%s" sound name "Glass"' % (
message, title)
command = "osascript -e '%s' &" % script
elif HOST_OS == 'linux':
if success:
icon = 'dialog-information'
else:
icon = 'dialog-error'
command = "notify-send -i '%s' '%s' '%s' &" % (icon, message, title)
elif HOST_OS == 'win32':
if success:
icon = 'info'
else:
icon = 'error'
command = (
"powershell -command \""
"[reflection.assembly]::loadwithpartialname('System.Windows.Forms')"
"| Out-Null;"
"[reflection.assembly]::loadwithpartialname('System.Drawing')"
"| Out-Null;"
"$n = new-object system.windows.forms.notifyicon;"
"$n.icon = [system.drawing.systemicons]::information;"
"$n.visible = $true;"
"$n.showballoontip(%d, '%s', '%s', "
"[system.windows.forms.tooltipicon]::%s);\"") % (
5000, # Notification stays on for this many milliseconds
message,
title,
icon)
if command:
# Ignore return code, if this command fails, it doesn't matter.
os.system(command)
def UseGoma(out_dir):
args_gn = os.path.join(out_dir, 'args.gn')
return 'use_goma = true' in open(args_gn, 'r').read()
# Try to start goma, but don't bail out if we can't. Instead print an error
# message, and let the build fail with its own error messages as well.
goma_started = False
def EnsureGomaStarted(out_dir):
global goma_started
if goma_started:
return True
args_gn_path = os.path.join(out_dir, 'args.gn')
goma_dir = None
with open(args_gn_path, 'r') as fp:
for line in fp:
if 'goma_dir' in line:
words = line.split()
goma_dir = words[2][1:-1] # goma_dir = "/path/to/goma"
if not goma_dir:
print('Could not find goma for ' + out_dir)
return False
if not os.path.exists(goma_dir) or not os.path.isdir(goma_dir):
print('Could not find goma at ' + goma_dir)
return False
goma_ctl = os.path.join(goma_dir, 'goma_ctl.py')
goma_ctl_command = [
'python3',
goma_ctl,
'ensure_start',
]
process = subprocess.Popen(goma_ctl_command)
process.wait()
if process.returncode != 0:
print(
"Tried to run goma_ctl.py, but it failed. Try running it manually: "
+ "\n\t" + ' '.join(goma_ctl_command))
return False
goma_started = True
return True
# Returns a tuple (build_config, command to run, whether goma is used)
def BuildOneConfig(options, targets, target_os, mode, arch, sanitizer):
build_config = utils.GetBuildConf(mode, arch, target_os, sanitizer)
out_dir = utils.GetBuildRoot(HOST_OS, mode, arch, target_os, sanitizer)
using_goma = False
command = ['buildtools/ninja/ninja', '-C', out_dir]
if options.verbose:
command += ['-v']
if UseGoma(out_dir):
if options.no_start_goma or EnsureGomaStarted(out_dir):
using_goma = True
command += [('-j%s' % str(options.j))]
command += [('-l%s' % str(options.l))]
else:
# If we couldn't ensure that goma is started, let the build start, but
# slowly so we can see any helpful error messages that pop out.
command += ['-j1']
command += targets
return (build_config, command, using_goma)
def RunOneBuildCommand(build_config, args, env):
start_time = time.time()
print(' '.join(args))
process = subprocess.Popen(args, env=env, stdin=None)
process.wait()
if process.returncode != 0:
NotifyBuildDone(build_config, success=False, start=start_time)
return 1
else:
NotifyBuildDone(build_config, success=True, start=start_time)
return 0
def CheckCleanBuild(build_config, args, env):
args = args + ['-n', '-d', 'explain']
print(' '.join(args))
process = subprocess.Popen(args,
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
stdin=None)
out, err = process.communicate()
process.wait()
if process.returncode != 0:
return 1
if 'ninja: no work to do' not in out.decode('utf-8'):
print(err.decode('utf-8'))
return 1
return 0
def SanitizerEnvironmentVariables():
with io.open('tools/bots/test_matrix.json', encoding='utf-8') as fd:
config = json.loads(fd.read())
env = dict()
for k, v in config['sanitizer_options'].items():
env[str(k)] = str(v)
symbolizer_path = config['sanitizer_symbolizer'].get(HOST_OS, None)
if symbolizer_path:
symbolizer_path = str(os.path.join(DART_ROOT, symbolizer_path))
env['ASAN_SYMBOLIZER_PATH'] = symbolizer_path
env['LSAN_SYMBOLIZER_PATH'] = symbolizer_path
env['MSAN_SYMBOLIZER_PATH'] = symbolizer_path
env['TSAN_SYMBOLIZER_PATH'] = symbolizer_path
env['UBSAN_SYMBOLIZER_PATH'] = symbolizer_path
return env
def Main():
starttime = time.time()
# Parse the options.
parser = BuildOptions()
options = parser.parse_args()
targets = options.build_targets
if not gn_py.ProcessOptions(options):
parser.print_help()
return 1
# If binaries are built with sanitizers we should use those flags.
# If the binaries are not built with sanitizers the flag should have no
# effect.
env = dict(os.environ)
env.update(SanitizerEnvironmentVariables())
# Always run GN before building.
gn_py.RunGnOnConfiguredConfigurations(options)
# Build all targets for each requested configuration.
configs = []
for target_os in options.os:
for mode in options.mode:
for arch in options.arch:
for sanitizer in options.sanitizer:
configs.append(
BuildOneConfig(options, targets, target_os, mode, arch,
sanitizer))
# Build regular configs.
goma_builds = []
for (build_config, args, goma) in configs:
if args is None:
return 1
if goma:
goma_builds.append([env, args])
elif RunOneBuildCommand(build_config, args, env=env) != 0:
return 1
# Run goma builds in parallel.
active_goma_builds = []
for (env, args) in goma_builds:
print(' '.join(args))
process = subprocess.Popen(args, env=env)
active_goma_builds.append([args, process])
while active_goma_builds:
time.sleep(0.1)
for goma_build in active_goma_builds:
(args, process) = goma_build
if process.poll() is not None:
print(' '.join(args) + " done.")
active_goma_builds.remove(goma_build)
if process.returncode != 0:
for (_, to_kill) in active_goma_builds:
to_kill.terminate()
return 1
if options.check_clean:
for (build_config, args, goma) in configs:
if CheckCleanBuild(build_config, args, env=env) != 0:
return 1
endtime = time.time()
print("The build took %.3f seconds" % (endtime - starttime))
return 0
if __name__ == '__main__':
sys.exit(Main())