#!/usr/bin/env python # Copyright 2013 Ben Vanik. All Rights Reserved. """ """ __author__ = 'ben.vanik@gmail.com (Ben Vanik)' import os import re import shutil import subprocess import sys def main(): # Add self to the root search path. sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) # Augment path to include our fancy things. os.environ['PATH'] += os.pathsep + os.pathsep.join([ os.path.abspath('third_party/ninja/'), os.path.abspath('third_party/gyp/') ]) # Check python version. if not sys.version_info[:2] == (2, 7): print('ERROR: python 2.7 required') print('(unfortunately gyp doesn\'t work with 3!)') sys.exit(1) return # Grab Visual Studio version and execute shell to set up environment. if sys.platform == 'win32': vs_version = import_vs_environment() if vs_version != 2013 and vs_version != 2015: print('ERROR: Visual Studio 2013 or 2015 not found!') print('Ensure you have the VS120COMNTOOLS environment variable!') sys.exit(1) return # Grab all commands. commands = discover_commands() # Parse command name and dispatch. try: if len(sys.argv) < 2: raise ValueError('No command given') command_name = sys.argv[1] if not command_name in commands: raise ValueError('Command "%s" not found' % (command_name)) command = commands[command_name] return_code = run_command(command=command, args=sys.argv[2:], cwd=os.getcwd()) except ValueError: print(usage(commands)) return_code = 1 except Exception as e: #print e raise return_code = 1 sys.exit(return_code) def import_vs_environment(): """Finds the installed Visual Studio version and imports interesting environment variables into os.environ. Returns: A version such as 2010, 2012, or 2013 or None if no VS is found. """ version = 0 tools_path = '' if 'VS150COMNTOOLS' in os.environ: version = 2015 tools_path = os.environ['VS150COMNTOOLS'] elif 'VS140COMNTOOLS' in os.environ: # TODO(benvanik): remove preview tools. version = 2015 tools_path = os.environ['VS140COMNTOOLS'] elif 'VS120COMNTOOLS' in os.environ: version = 2013 tools_path = os.environ['VS120COMNTOOLS'] elif 'VS110COMNTOOLS' in os.environ: version = 2012 tools_path = os.environ['VS110COMNTOOLS'] elif 'VS100COMNTOOLS' in os.environ: version = 2010 tools_path = os.environ['VS100COMNTOOLS'] if version == 0: return None tools_path = os.path.join(tools_path, '..\\..\\vc\\vcvarsall.bat') args = [tools_path, '&&', 'set'] popen = subprocess.Popen( args, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) variables, _ = popen.communicate() envvars_to_save = ( 'devenvdir', 'include', 'lib', 'libpath', 'path', 'pathext', 'systemroot', 'temp', 'tmp', 'windowssdkdir', ) for line in variables.splitlines(): for envvar in envvars_to_save: if re.match(envvar + '=', line.lower()): var, setting = line.split('=', 1) if envvar == 'path': setting = os.path.dirname(sys.executable) + os.pathsep + setting os.environ[var.upper()] = setting break os.environ['VSVERSION'] = str(version) return version def discover_commands(): """Looks for all commands and returns a dictionary of them. In the future commands could be discovered on disk. Returns: A dictionary containing name-to-Command mappings. """ commands = { 'setup': SetupCommand(), 'pull': PullCommand(), 'gyp': GypCommand(), 'build': BuildCommand(), 'test': TestCommand(), 'clean': CleanCommand(), 'nuke': NukeCommand(), } return commands def usage(commands): """Gets usage info that can be displayed to the user. Args: commands: A command dictionary from discover_commands. Returns: A string containing usage info and a command listing. """ s = 'xenia-build.py command [--help]\n' s += '\n' s += 'Commands:\n' command_names = sorted(commands.keys()) for command_name in command_names: s += ' %s\n' % (command_name) command_help = commands[command_name].help_short if command_help: s += ' %s\n' % (command_help) return s def run_command(command, args, cwd): """Runs a command with the given context. Args: command: Command to run. args: Arguments, with the app and command name stripped. cwd: Current working directory. Returns: 0 if the command succeeded and non-zero otherwise. Raises: ValueError: The command could not be found or was not specified. """ # TODO(benvanik): parse arguments/etc. return command.execute(args, cwd) def has_bin(bin): """Checks whether the given binary is present. """ for path in os.environ["PATH"].split(os.pathsep): path = path.strip('"') exe_file = os.path.join(path, bin) if os.path.isfile(exe_file) and os.access(exe_file, os.X_OK): return True exe_file = exe_file + '.exe' if os.path.isfile(exe_file) and os.access(exe_file, os.X_OK): return True return None def shell_call(command, throw_on_error=True): """Executes a shell command. Args: command: Command to execute. throw_on_error: Whether to throw an error or return the status code. Returns: If throw_on_error is False the status code of the call will be returned. """ if throw_on_error: subprocess.check_call(command, shell=True) return 0 else: return subprocess.call(command, shell=True) class Command(object): """Base type for commands. """ def __init__(self, name, help_short=None, help_long=None, *args, **kwargs): """Initializes a command. Args: name: The name of the command exposed to the management script. help_short: Help text printed alongside the command when queried. help_long: Extended help text when viewing command help. """ self.name = name self.help_short = help_short self.help_long = help_long def execute(self, args, cwd): """Executes the command. Args: args: Arguments list. cwd: Current working directory. Returns: Return code of the command. """ return 1 def post_update_deps(config): """Runs common tasks that should be executed after any deps are changed. Args: config: 'debug' or 'release'. """ pass class SetupCommand(Command): """'setup' command.""" def __init__(self, *args, **kwargs): super(SetupCommand, self).__init__( name='setup', help_short='Setup the build environment.', *args, **kwargs) def execute(self, args, cwd): print('Setting up the build environment...') print('') # Setup submodules. print('- git submodule init / update...') shell_call('git submodule init') shell_call('git submodule update') print('') # Disable core.filemode on Windows to prevent weird file mode diffs in git. # TODO(benvanik): check cygwin test - may be wrong when using Windows python if os.path.exists('/Cygwin.bat'): print('- setting filemode off on cygwin...') shell_call('git config core.filemode false') shell_call('git submodule foreach git config core.filemode false') print('') # Run the ninja bootstrap to build it, if it's missing. if (not '--no-ninja' in args and not os.path.exists('third_party/ninja/ninja') and not os.path.exists('third_party/ninja/ninja.exe')): print('- preparing ninja...') os.chdir('third_party/ninja') # Windows needs --x64 to force building the 64-bit ninja. extra_args = '' #if sys.platform == 'win32': # extra_args = '--x64' shell_call('python configure.py --bootstrap ' + extra_args) os.chdir(cwd) print('') # binutils (with vmx128). print('- Building binutils...') if sys.platform == 'win32': # TODO(benvanik): cygwin or vagrant print('WARNING: binutils build not supported yet') else: shell_call('third_party/binutils/build.sh') print('') post_update_deps('debug') post_update_deps('release') print('- running gyp...') run_all_gyps() print('') print('Success!') return 0 class PullCommand(Command): """'pull' command.""" def __init__(self, *args, **kwargs): super(PullCommand, self).__init__( name='pull', help_short='Pulls the repo and all dependencies.', *args, **kwargs) def execute(self, args, cwd): print('Pulling...') print('') print('- pulling self...') shell_call('git pull') print('') print('- pulling dependencies...') shell_call('git submodule update') print('') post_update_deps('debug') post_update_deps('release') print('- running gyp...') run_all_gyps() print('') print('Success!') return 0 def run_gyp(format): """Runs gyp on the main project with the given format. Args: format: gyp -f value. """ shell_call(' '.join([ 'gyp', '-f %s' % (format), # Removes the out/ from ninja builds. '-G output_dir=.', '--depth=.', '--toplevel-dir=.', '--generator-output=build/xenia/', # Set the VS version. '-G msvs_version=%s' % (os.environ.get('VSVERSION', 2015)), #'-D windows_sdk_dir=%s' % (os.environ['WINDOWSSDKDIR']), '-D windows_sdk_dir="C:\\Program Files (x86)\\Windows Kits\\8.1"', 'xenia.gyp', ])) def run_all_gyps(): """Runs all gyp configurations. """ run_gyp('ninja') if sys.platform == 'darwin': run_gyp('xcode') elif sys.platform == 'win32': run_gyp('msvs') class GypCommand(Command): """'gyp' command.""" def __init__(self, *args, **kwargs): super(GypCommand, self).__init__( name='gyp', help_short='Runs gyp to update all projects.', *args, **kwargs) def execute(self, args, cwd): print('Running gyp...') print('') # Update GYP. run_all_gyps() print('Success!') return 0 class BuildCommand(Command): """'build' command.""" def __init__(self, *args, **kwargs): super(BuildCommand, self).__init__( name='build', help_short='Builds the project.', *args, **kwargs) def execute(self, args, cwd): # TODO(benvanik): add arguments: # --force debug = '--debug' in args config = 'Debug' if debug else 'Release' target = '' for arg in args: if not arg.startswith('--'): target = arg print('Building %s...' % (config)) print('') print('- running gyp for ninja...') run_gyp('ninja') print('') if not target: print('- building all:%s...' % (config)) result = shell_call('ninja -C build/xenia/%s' % (config), throw_on_error=False) else: print('- building %s:%s...' % (target, config)) result = shell_call('ninja -C build/xenia/%s %s' % (config, target), throw_on_error=False) print('') if result != 0: return result print('Success!') return 0 class TestCommand(Command): """'test' command.""" def __init__(self, *args, **kwargs): super(TestCommand, self).__init__( name='test', help_short='Runs all tests.', *args, **kwargs) def execute(self, args, cwd): print('Testing...') print('') # Run base alloy tests. print('Launching alloy-test runner...') result = shell_call('"build/xenia/Debug/alloy-test"') print('') if result != 0: return result # First run make and update all of the test files. if sys.platform == 'win32': # TODO(benvanik): use cygwin/vagrant/whatever print('WARNING: test files not updated!'); else: print('Updating test files...') result = shell_call('./src/alloy/frontend/ppc/test/update.sh') print('') if result != 0: return result # Start the test runner. print('Launching alloy-ppc-test runner...') result = shell_call('"build/xenia/Debug/alloy-ppc-test"') print('') if result != 0: return result return result class CleanCommand(Command): """'clean' command.""" def __init__(self, *args, **kwargs): super(CleanCommand, self).__init__( name='clean', help_short='Removes intermediate files and build output.', *args, **kwargs) def execute(self, args, cwd): print('Cleaning build artifacts...') print('') print('- removing build/xenia/...') if os.path.isdir('build/xenia/'): shutil.rmtree('build/xenia/') print('') print('Success!') return 0 class NukeCommand(Command): """'nuke' command.""" def __init__(self, *args, **kwargs): super(NukeCommand, self).__init__( name='nuke', help_short='Removes all build/ output.', *args, **kwargs) def execute(self, args, cwd): print('Cleaning build artifacts...') print('') print('- removing build/...') if os.path.isdir('build/'): shutil.rmtree('build/') print('') print('Success!') return 0 if __name__ == '__main__': main()