#!/usr/bin/env python # # 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 multiprocessing import optparse import os import subprocess import sys import time import utils 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 = [ 'ia32', 'x64', 'simarm', 'arm', 'arm_x64', 'simarmv6', 'armv6', 'simarmv5te', 'armv5te', 'simarm64', 'arm64', 'simdbc', 'simdbc64', 'armsimdbc', 'armsimdbc64', 'simarm_x64' ] usage = """\ usage: %%prog [options] [targets] This script invokes ninja to build Dart. """ def BuildOptions(): result = optparse.OptionParser(usage=usage) result.add_option( "-a", "--arch", help='Target architectures (comma-separated).', metavar='[all,' + ','.join(AVAILABLE_ARCHS) + ']', default=utils.GuessArchitecture()) result.add_option( "-b", "--bytecode", help='Build with the kernel bytecode interpreter. DEPRECATED.', default=False, action='store_true') result.add_option( "-j", type=int, help='Ninja -j option for Goma builds.', default=1000) result.add_option( "-l", type=int, help='Ninja -l option for Goma builds.', default=64) result.add_option( "-m", "--mode", help='Build variants (comma-separated).', metavar='[all,debug,release,product]', default='debug') result.add_option( "--no-start-goma", help="Don't try to start goma", default=False, action='store_true') result.add_option( "--os", help='Target OSs (comma-separated).', metavar='[all,host,android]', default='host') # TODO(38701): Remove this and everything that references it once the # forked NNBD SDK is merged back in. result.add_option( "--nnbd", help='Use the NNBD fork of the SDK.', default=False, action='store_true') result.add_option( "-v", "--verbose", help='Verbose output.', default=False, action="store_true") return result def ProcessOsOption(os_name): if os_name == 'host': return HOST_OS return os_name def ProcessOptions(options, args): if options.arch == 'all': options.arch = 'ia32,x64,simarm,simarm64,simdbc64' if options.mode == 'all': options.mode = 'debug,release,product' if options.os == 'all': options.os = 'host,android' options.mode = options.mode.split(',') options.arch = options.arch.split(',') options.os = options.os.split(',') for mode in options.mode: if not mode in ['debug', 'release', 'product']: print("Unknown mode %s" % mode) return False for arch in options.arch: if not arch in AVAILABLE_ARCHS: print("Unknown arch %s" % arch) return False options.os = [ProcessOsOption(os_name) for os_name in options.os] for os_name in options.os: if not os_name in ['android', 'freebsd', 'linux', 'macos', 'win32']: print("Unknown os %s" % os_name) return False if os_name != HOST_OS: if os_name != 'android': print("Unsupported target os %s" % os_name) return False if not HOST_OS in ['linux', 'macos']: print("Cross-compilation to %s is not supported on host os %s." % (os_name, HOST_OS)) return False if not arch in [ 'ia32', 'x64', 'arm', 'arm_x64', 'armv6', 'armv5te', 'arm64', 'simdbc', 'simdbc64' ]: print( "Cross-compilation to %s is not supported for architecture %s." % (os_name, arch)) return False # We have not yet tweaked the v8 dart build to work with the Android # NDK/SDK, so don't try to build it. if not args: print( "For android builds you must specify a target, such as 'runtime'." ) return False return True 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 GenerateBuildfilesIfNeeded(): if os.path.exists(utils.GetBuildDir(HOST_OS)): return True command = [ 'python', os.path.join(DART_ROOT, 'tools', 'generate_buildfiles.py') ] print("Running " + ' '.join(command)) process = subprocess.Popen(command) process.wait() if process.returncode != 0: print("Tried to generate missing buildfiles, but failed. " "Try running manually:\n\t$ " + ' '.join(command)) return False return True def RunGNIfNeeded(out_dir, target_os, mode, arch, use_nnbd): if os.path.isfile(os.path.join(out_dir, 'args.gn')): return gn_os = 'host' if target_os == HOST_OS else target_os gn_command = [ 'python', os.path.join(DART_ROOT, 'tools', 'gn.py'), '-m', mode, '-a', arch, '--os', gn_os, '-v', ] if use_nnbd: gn_command.append('--nnbd') process = subprocess.Popen(gn_command) process.wait() if process.returncode != 0: print("Tried to run GN, but it failed. Try running it manually: \n\t$ " + ' '.join(gn_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 = [ 'python', 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): build_config = utils.GetBuildConf(mode, arch, target_os, options.nnbd) out_dir = utils.GetBuildRoot(HOST_OS, mode, arch, target_os, options.nnbd) using_goma = False # TODO(zra): Remove auto-run of gn, replace with prompt for user to run # gn.py manually. RunGNIfNeeded(out_dir, target_os, mode, arch, options.nnbd) command = ['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): start_time = time.time() print(' '.join(args)) process = subprocess.Popen(args, 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 RunOneGomaBuildCommand(args): try: print(' '.join(args)) process = subprocess.Popen(args, stdin=None) process.wait() print(' '.join(args) + " done.") return process.returncode except KeyboardInterrupt: return 1 def Main(): starttime = time.time() # Parse the options. parser = BuildOptions() (options, args) = parser.parse_args() if not ProcessOptions(options, args): parser.print_help() return 1 # Determine which targets to build. By default we build the "all" target. if len(args) == 0: targets = ['all'] else: targets = args if not GenerateBuildfilesIfNeeded(): return 1 # Build all targets for each requested configuration. configs = [] for target_os in options.os: for mode in options.mode: for arch in options.arch: configs.append( BuildOneConfig(options, targets, target_os, mode, arch)) # Build regular configs. goma_builds = [] for (build_config, args, goma) in configs: if args is None: return 1 if goma: goma_builds.append(args) elif RunOneBuildCommand(build_config, args) != 0: return 1 # Run goma builds in parallel. pool = multiprocessing.Pool(multiprocessing.cpu_count()) results = pool.map(RunOneGomaBuildCommand, goma_builds, chunksize=1) for r in results: if r != 0: return 1 endtime = time.time() print("The build took %.3f seconds" % (endtime - starttime)) return 0 if __name__ == '__main__': sys.exit(Main())