#!/usr/bin/env python # Copyright 2013 Ben Vanik. All Rights Reserved. """ """ __author__ = 'ben.vanik@gmail.com (Ben Vanik)' import os 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 sys.version_info < (2, 7): print 'ERROR: python 2.7+ required' 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 commands.has_key(command_name): 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 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 = commands.keys() command_names.sort() 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 os.path.exists('third_party/ninja/ninja') and not os.path.exists('third_party/ninja/ninja.exe')): print '- preparing ninja...' # Windows needs --x64 to force building the 64-bit ninja. extra_args = '' if sys.platform == 'win32': extra_args = '--x64' shell_call('python third_party/ninja/bootstrap.py ' + extra_args) print '' # Binutils. # TODO(benvanik): disable on Windows print '- binutils...' if sys.platform == 'win32': print 'WARNING: ignoring binutils on Windows... don\'t change tests!' else: if not os.path.exists('build/binutils'): os.makedirs('build/binutils') os.chdir('build/binutils') shell_call(' '.join([ '../../third_party/binutils/configure', '--disable-debug', '--disable-dependency-tracking', '--disable-werror', '--enable-interwork', '--enable-multilib', '--target=powerpc-none-elf', '--with-gnu-ld', '--with-gnu-as', ])) shell_call('make') os.chdir(cwd) 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', '--include=common.gypi', '-f %s' % (format), # Set the VS version. # TODO(benvanik): allow user to set? '-G msvs_version=2010', # Removes the out/ from ninja builds. '-G output_dir=.', '--depth=.', '--generator-output=build/xenia/', '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' print 'Building %s...' % (config) print '' print '- running gyp for ninja...' run_gyp('ninja') print '' print '- building xenia in %s...' % (config) result = shell_call('ninja -C build/xenia/%s' % (config), 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 '' # First run make and update all of the test files. # TOOD(benvanik): disable on Windows print 'Updating test files...' result = shell_call('make -C test/codegen/') print '' if result != 0: return result # Start the test runner. print 'Launching test runner...' result = shell_call('bin/xenia-test') print '' 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()