#!/usr/bin/env python # # @file develop.py # @authors Bryan O'Sullivan, Mark Palange, Aaron Brashears # @brief Fire and forget script to appropriately configure cmake for SL. # # $LicenseInfo:firstyear=2007&license=viewerlgpl$ # Second Life Viewer Source Code # Copyright (C) 2010, Linden Research, Inc. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; # version 2.1 of the License only. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA # # Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA # $/LicenseInfo$ import errno import getopt import os import random import re import shutil import socket import sys import commands import subprocess class CommandError(Exception): pass def mkdir(path): try: os.mkdir(path) return path except OSError, err: if err.errno != errno.EEXIST or not os.path.isdir(path): raise def getcwd(): cwd = os.getcwd() if 'a' <= cwd[0] <= 'z' and cwd[1] == ':': # CMake wants DOS drive letters to be in uppercase. The above # condition never asserts on platforms whose full path names # always begin with a slash, so we don't need to test whether # we are running on Windows. cwd = cwd[0].upper() + cwd[1:] return cwd def quote(opts): return '"' + '" "'.join([ opt.replace('"', '') for opt in opts ]) + '"' class PlatformSetup(object): generator = None build_types = {} for t in ('Debug', 'Release', 'RelWithDebInfo'): build_types[t.lower()] = t build_type = build_types['relwithdebinfo'] standalone = 'OFF' unattended = 'OFF' universal = 'OFF' project_name = 'SecondLife' distcc = True cmake_opts = [] word_size = 32 using_express = False def __init__(self): self.script_dir = os.path.realpath( os.path.dirname(__import__(__name__).__file__)) def os(self): '''Return the name of the OS.''' raise NotImplemented('os') def arch(self): '''Return the CPU architecture.''' return None def platform(self): '''Return a stringified two-tuple of the OS name and CPU architecture.''' ret = self.os() if self.arch(): ret += '-' + self.arch() return ret def build_dirs(self): '''Return the top-level directories in which builds occur. This can return more than one directory, e.g. if doing a 32-bit viewer and server build on Linux.''' return ['build-' + self.platform()] def cmake_commandline(self, src_dir, build_dir, opts, simple): '''Return the command line to run cmake with.''' args = dict( dir=src_dir, generator=self.generator, opts=quote(opts), standalone=self.standalone, unattended=self.unattended, word_size=self.word_size, type=self.build_type.upper(), ) #if simple: # return 'cmake %(opts)s %(dir)r' % args return ('cmake -DCMAKE_BUILD_TYPE:STRING=%(type)s ' '-DSTANDALONE:BOOL=%(standalone)s ' '-DUNATTENDED:BOOL=%(unattended)s ' '-DWORD_SIZE:STRING=%(word_size)s ' '-G %(generator)r %(opts)s %(dir)r' % args) def run_cmake(self, args=[]): '''Run cmake.''' # do a sanity check to make sure we have a generator if not hasattr(self, 'generator'): raise "No generator available for '%s'" % (self.__name__,) cwd = getcwd() created = [] try: for d in self.build_dirs(): simple = True if mkdir(d): created.append(d) simple = False try: os.chdir(d) cmd = self.cmake_commandline(cwd, d, args, simple) print 'Running %r in %r' % (cmd, d) self.run(cmd, 'cmake') finally: os.chdir(cwd) except: # If we created a directory in which to run cmake and # something went wrong, the directory probably just # contains garbage, so delete it. os.chdir(cwd) for d in created: print 'Cleaning %r' % d shutil.rmtree(d) raise def parse_build_opts(self, arguments): opts, targets = getopt.getopt(arguments, 'o:', ['option=']) build_opts = [] for o, a in opts: if o in ('-o', '--option'): build_opts.append(a) return build_opts, targets def run_build(self, opts, targets): '''Build the default targets for this platform.''' raise NotImplemented('run_build') def cleanup(self): '''Delete all build directories.''' cleaned = 0 for d in self.build_dirs(): if os.path.isdir(d): print 'Cleaning %r' % d shutil.rmtree(d) cleaned += 1 if not cleaned: print 'Nothing to clean up!' def is_internal_tree(self): '''Indicate whether we are building in an internal source tree.''' return os.path.isdir(os.path.join(self.script_dir, 'newsim')) def find_in_path(self, name, defval=None, basename=False): for ext in self.exe_suffixes: name_ext = name + ext if os.sep in name_ext: path = os.path.abspath(name_ext) if os.access(path, os.X_OK): return [basename and os.path.basename(path) or path] for p in os.getenv('PATH', self.search_path).split(os.pathsep): path = os.path.join(p, name_ext) if os.access(path, os.X_OK): return [basename and os.path.basename(path) or path] if defval: return [defval] return [] class UnixSetup(PlatformSetup): '''Generic Unixy build instructions.''' search_path = '/usr/bin:/usr/local/bin' exe_suffixes = ('',) def __init__(self): super(UnixSetup, self).__init__() self.generator = 'Unix Makefiles' def os(self): return 'unix' def arch(self): cpu = os.uname()[-1] if cpu.endswith('386'): cpu = 'i386' elif cpu.endswith('86'): cpu = 'i686' elif cpu in ('athlon',): cpu = 'i686' elif cpu == 'Power Macintosh': cpu = 'ppc' elif cpu == 'x86_64' and self.word_size == 32: cpu = 'i686' return cpu def run(self, command, name=None): '''Run a program. If the program fails, raise an exception.''' sys.stdout.flush() ret = os.system(command) if ret: if name is None: name = command.split(None, 1)[0] if os.WIFEXITED(ret): st = os.WEXITSTATUS(ret) if st == 127: event = 'was not found' else: event = 'exited with status %d' % st elif os.WIFSIGNALED(ret): event = 'was killed by signal %d' % os.WTERMSIG(ret) else: event = 'died unexpectedly (!?) with 16-bit status %d' % ret raise CommandError('the command %r %s' % (name, event)) class LinuxSetup(UnixSetup): def __init__(self): super(LinuxSetup, self).__init__() try: self.debian_sarge = open('/etc/debian_version').read().strip() == '3.1' except: self.debian_sarge = False def os(self): return 'linux' def build_dirs(self): # Only build the server code if we have it. platform_build = '%s-%s' % (self.platform(), self.build_type.lower()) if self.arch() == 'i686' and self.is_internal_tree(): return ['viewer-' + platform_build, 'server-' + platform_build] elif self.arch() == 'x86_64' and self.is_internal_tree(): # the viewer does not build in 64bit -- kdu5 issues # we can either use openjpeg, or overhaul our viewer to handle kdu5 or higher # doug knows about kdu issues return ['server-' + platform_build] else: return ['viewer-' + platform_build] def cmake_commandline(self, src_dir, build_dir, opts, simple): args = dict( dir=src_dir, generator=self.generator, opts=quote(opts), standalone=self.standalone, unattended=self.unattended, type=self.build_type.upper(), project_name=self.project_name, word_size=self.word_size, ) if not self.is_internal_tree(): args.update({'cxx':'g++', 'server':'OFF', 'viewer':'ON'}) else: if self.distcc: distcc = self.find_in_path('distcc') baseonly = True else: distcc = [] baseonly = False if 'server' in build_dir: gcc = distcc + self.find_in_path( self.debian_sarge and 'g++-3.3' or 'g++-4.1', 'g++', baseonly) args.update({'cxx': ' '.join(gcc), 'server': 'ON', 'viewer': 'OFF'}) else: gcc41 = distcc + self.find_in_path('g++-4.1', 'g++', baseonly) args.update({'cxx': ' '.join(gcc41), 'server': 'OFF', 'viewer': 'ON'}) cmd = (('cmake -DCMAKE_BUILD_TYPE:STRING=%(type)s ' '-G %(generator)r -DSERVER:BOOL=%(server)s ' '-DVIEWER:BOOL=%(viewer)s -DSTANDALONE:BOOL=%(standalone)s ' '-DUNATTENDED:BOOL=%(unattended)s ' '-DWORD_SIZE:STRING=%(word_size)s ' '-DROOT_PROJECT_NAME:STRING=%(project_name)s ' '%(opts)s %(dir)r') % args) if 'CXX' not in os.environ: args.update({'cmd':cmd}) cmd = ('CXX=%(cxx)r %(cmd)s' % args) return cmd def run_build(self, opts, targets): job_count = None for i in range(len(opts)): if opts[i].startswith('-j'): try: job_count = int(opts[i][2:]) except ValueError: try: job_count = int(opts[i+1]) except ValueError: job_count = True def get_cpu_count(): count = 0 for line in open('/proc/cpuinfo'): if re.match(r'processor\s*:', line): count += 1 return count def localhost(): count = get_cpu_count() return 'localhost/' + str(count), count def get_distcc_hosts(): try: hosts = [] name = os.getenv('DISTCC_DIR', '/etc/distcc') + '/hosts' for l in open(name): l = l[l.find('#')+1:].strip() if l: hosts.append(l) return hosts except IOError: return (os.getenv('DISTCC_HOSTS', '').split() or [localhost()[0]]) def count_distcc_hosts(): cpus = 0 hosts = 0 for host in get_distcc_hosts(): m = re.match(r'.*/(\d+)', host) hosts += 1 cpus += m and int(m.group(1)) or 1 return hosts, cpus def mk_distcc_hosts(basename, range, num_cpus): '''Generate a list of LL-internal machines to build on.''' loc_entry, cpus = localhost() hosts = [loc_entry] dead = [] stations = [s for s in xrange(range) if s not in dead] random.shuffle(stations) hosts += ['%s%d.lindenlab.com/%d,lzo' % (basename, s, num_cpus) for s in stations] cpus += 2 * len(stations) return ' '.join(hosts), cpus if job_count is None: hosts, job_count = count_distcc_hosts() hostname = socket.gethostname() if hosts == 1: if hostname.startswith('station'): hosts, job_count = mk_distcc_hosts('station', 36, 2) os.environ['DISTCC_HOSTS'] = hosts if hostname.startswith('eniac'): hosts, job_count = mk_distcc_hosts('eniac', 71, 2) os.environ['DISTCC_HOSTS'] = hosts if hostname.startswith('build'): max_jobs = 6 else: max_jobs = 12 if job_count > max_jobs: job_count = max_jobs; opts.extend(['-j', str(job_count)]) if targets: targets = ' '.join(targets) else: targets = 'all' for d in self.build_dirs(): cmd = 'make -C %r %s %s' % (d, ' '.join(opts), targets) print 'Running %r' % cmd self.run(cmd) class DarwinSetup(UnixSetup): def __init__(self): super(DarwinSetup, self).__init__() self.generator = 'Xcode' def os(self): return 'darwin' def arch(self): if self.universal == 'ON': return 'universal' else: return UnixSetup.arch(self) def cmake_commandline(self, src_dir, build_dir, opts, simple): args = dict( dir=src_dir, generator=self.generator, opts=quote(opts), standalone=self.standalone, word_size=self.word_size, unattended=self.unattended, project_name=self.project_name, universal=self.universal, type=self.build_type.upper(), ) if self.universal == 'ON': args['universal'] = '-DCMAKE_OSX_ARCHITECTURES:STRING=\'i386;ppc\'' #if simple: # return 'cmake %(opts)s %(dir)r' % args return ('cmake -G %(generator)r ' '-DCMAKE_BUILD_TYPE:STRING=%(type)s ' '-DSTANDALONE:BOOL=%(standalone)s ' '-DUNATTENDED:BOOL=%(unattended)s ' '-DWORD_SIZE:STRING=%(word_size)s ' '-DROOT_PROJECT_NAME:STRING=%(project_name)s ' '%(universal)s ' '%(opts)s %(dir)r' % args) def run_build(self, opts, targets): cwd = getcwd() if targets: targets = ' '.join(['-target ' + repr(t) for t in targets]) else: targets = '' cmd = ('xcodebuild -configuration %s %s %s | grep -v "^[[:space:]]*setenv" ; exit ${PIPESTATUS[0]}' % (self.build_type, ' '.join(opts), targets)) for d in self.build_dirs(): try: os.chdir(d) print 'Running %r in %r' % (cmd, d) self.run(cmd) finally: os.chdir(cwd) class WindowsSetup(PlatformSetup): gens = { 'vc71' : { 'gen' : r'Visual Studio 7 .NET 2003', 'ver' : r'7.1' }, 'vc80' : { 'gen' : r'Visual Studio 8 2005', 'ver' : r'8.0' }, 'vc90' : { 'gen' : r'Visual Studio 9 2008', 'ver' : r'9.0' } } gens['vs2003'] = gens['vc71'] gens['vs2005'] = gens['vc80'] gens['vs2008'] = gens['vc90'] search_path = r'C:\windows' exe_suffixes = ('.exe', '.bat', '.com') def __init__(self): super(WindowsSetup, self).__init__() self._generator = None self.incredibuild = False def _get_generator(self): if self._generator is None: for version in 'vc80 vc90 vc71'.split(): if self.find_visual_studio(version): self._generator = version print 'Building with ', self.gens[version]['gen'] break else: print >> sys.stderr, 'Cannot find a Visual Studio installation, testing for express editions' for version in 'vc80 vc90 vc71'.split(): if self.find_visual_studio_express(version): self._generator = version self.using_express = True print 'Building with ', self.gens[version]['gen'] , "Express edition" break else: print >> sys.stderr, 'Cannot find any Visual Studio installation' sys.exit(1) return self._generator def _set_generator(self, gen): self._generator = gen generator = property(_get_generator, _set_generator) def os(self): return 'win32' def build_dirs(self): return ['build-' + self.generator] def cmake_commandline(self, src_dir, build_dir, opts, simple): args = dict( dir=src_dir, generator=self.gens[self.generator.lower()]['gen'], opts=quote(opts), standalone=self.standalone, unattended=self.unattended, project_name=self.project_name, word_size=self.word_size, ) #if simple: # return 'cmake %(opts)s "%(dir)s"' % args return ('cmake -G "%(generator)s" ' '-DSTANDALONE:BOOL=%(standalone)s ' '-DUNATTENDED:BOOL=%(unattended)s ' '-DWORD_SIZE:STRING=%(word_size)s ' '-DROOT_PROJECT_NAME:STRING=%(project_name)s ' '%(opts)s "%(dir)s"' % args) def get_HKLM_registry_value(self, key_str, value_str): import _winreg reg = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE) key = _winreg.OpenKey(reg, key_str) value = _winreg.QueryValueEx(key, value_str)[0] print 'Found: %s' % value return value def find_visual_studio(self, gen=None): if gen is None: gen = self._generator gen = gen.lower() value_str = (r'EnvironmentDirectory') key_str = (r'SOFTWARE\Microsoft\VisualStudio\%s\Setup\VS' % self.gens[gen]['ver']) print ('Reading VS environment from HKEY_LOCAL_MACHINE\%s\%s' % (key_str, value_str)) try: return self.get_HKLM_registry_value(key_str, value_str) except WindowsError, err: key_str = (r'SOFTWARE\Wow6432Node\Microsoft\VisualStudio\%s\Setup\VS' % self.gens[gen]['ver']) try: return self.get_HKLM_registry_value(key_str, value_str) except: print >> sys.stderr, "Didn't find ", self.gens[gen]['gen'] return '' def find_visual_studio_express(self, gen=None): if gen is None: gen = self._generator gen = gen.lower() try: import _winreg key_str = (r'SOFTWARE\Microsoft\VCEXpress\%s\Setup\VC' % self.gens[gen]['ver']) value_str = (r'ProductDir') print ('Reading VS environment from HKEY_LOCAL_MACHINE\%s\%s' % (key_str, value_str)) print key_str reg = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE) key = _winreg.OpenKey(reg, key_str) value = _winreg.QueryValueEx(key, value_str)[0]+"IDE" print 'Found: %s' % value return value except WindowsError, err: print >> sys.stderr, "Didn't find ", self.gens[gen]['gen'] return '' def get_build_cmd(self): if self.incredibuild: config = self.build_type if self.gens[self.generator]['ver'] in [ r'8.0', r'9.0' ]: config = '\"%s|Win32\"' % config executable = 'buildconsole' cmd = "%(bin)s %(prj)s.sln /build /cfg=%(cfg)s" % {'prj': self.project_name, 'cfg': config, 'bin': executable} return (executable, cmd) environment = self.find_visual_studio() if environment == '': environment = self.find_visual_studio_express() if environment == '': print >> sys.stderr, "Something went very wrong during build stage, could not find a Visual Studio installation." else: build_dirs=self.build_dirs(); print >> sys.stderr, "\nSolution generation complete, it can can now be found in:", build_dirs[0] print >> sys.stderr, "\nPlease see https://wiki.secondlife.com/wiki/Microsoft_Visual_Studio#Extra_steps_for_Visual_Studio_Express_editions for express specific information" exit(0) # devenv.com is CLI friendly, devenv.exe... not so much. executable = '%sdevenv.com' % (self.find_visual_studio(),) cmd = ('"%s" %s.sln /build %s' % (executable, self.project_name, self.build_type)) return (executable, cmd) def run(self, command, name=None, retry_on=None, retries=1): '''Run a program. If the program fails, raise an exception.''' assert name is not None, 'On windows an executable path must be given in name. [DEV-44838]' if os.path.isfile(name): path = name else: path = self.find_in_path(name)[0] while retries: retries = retries - 1 print "develop.py tries to run:", command ret = subprocess.call(command, executable=path) print "got ret", ret, "from", command if ret == 0: break else: error = 'exited with status %d' % ret if retry_on is not None and retry_on == ret: print "Retrying... the command %r %s" % (name, error) else: raise CommandError('the command %r %s' % (name, error)) def run_cmake(self, args=[]): '''Override to add the vstool.exe call after running cmake.''' PlatformSetup.run_cmake(self, args) if self.unattended == 'OFF': if self.using_express == False: self.run_vstool() def run_vstool(self): for build_dir in self.build_dirs(): stamp = os.path.join(build_dir, 'vstool.txt') try: prev_build = open(stamp).read().strip() except IOError: prev_build = '' if prev_build == self.build_type: # Only run vstool if the build type has changed. continue executable = os.path.join('tools','vstool','VSTool.exe') vstool_cmd = (executable + ' --solution ' + os.path.join(build_dir,'SecondLife.sln') + ' --config ' + self.build_type + ' --startup secondlife-bin') print 'Running %r in %r' % (vstool_cmd, getcwd()) self.run(vstool_cmd, name=executable) print >> open(stamp, 'w'), self.build_type def run_build(self, opts, targets): for t in targets: assert t.strip(), 'Unexpected empty targets: ' + repr(targets) cwd = getcwd() executable, build_cmd = self.get_build_cmd() for d in self.build_dirs(): try: os.chdir(d) if targets: for t in targets: cmd = '%s /project %s %s' % (build_cmd, t, ' '.join(opts)) print 'Running %r in %r' % (cmd, d) self.run(cmd, name=executable, retry_on=4, retries=3) else: cmd = '%s %s' % (build_cmd, ' '.join(opts)) print 'Running %r in %r' % (cmd, d) self.run(cmd, name=executable, retry_on=4, retries=3) finally: os.chdir(cwd) class CygwinSetup(WindowsSetup): def __init__(self): super(CygwinSetup, self).__init__() self.generator = 'vc80' def cmake_commandline(self, src_dir, build_dir, opts, simple): dos_dir = commands.getoutput("cygpath -w %s" % src_dir) args = dict( dir=dos_dir, generator=self.gens[self.generator.lower()]['gen'], opts=quote(opts), standalone=self.standalone, unattended=self.unattended, project_name=self.project_name, word_size=self.word_size, ) #if simple: # return 'cmake %(opts)s "%(dir)s"' % args return ('cmake -G "%(generator)s" ' '-DUNATTENDED:BOOl=%(unattended)s ' '-DSTANDALONE:BOOL=%(standalone)s ' '-DWORD_SIZE:STRING=%(word_size)s ' '-DROOT_PROJECT_NAME:STRING=%(project_name)s ' '%(opts)s "%(dir)s"' % args) setup_platform = { 'darwin': DarwinSetup, 'linux2': LinuxSetup, 'win32' : WindowsSetup, 'cygwin' : CygwinSetup } usage_msg = ''' Usage: develop.py [options] [command [command-options]] Options: -h | --help print this help message --standalone build standalone, without Linden prebuild libraries --unattended build unattended, do not invoke any tools requiring a human response --universal build a universal binary on Mac OS X (unsupported) -t | --type=NAME build type ("Debug", "Release", or "RelWithDebInfo") -m32 | -m64 build architecture (32-bit or 64-bit) -N | --no-distcc disable use of distcc -G | --generator=NAME generator name Windows: VC71 or VS2003 (default), VC80 (VS2005) or VC90 (VS2008) Mac OS X: Xcode (default), Unix Makefiles Linux: Unix Makefiles (default), KDevelop3 -p | --project=NAME set the root project name. (Doesn't effect makefiles) Commands: build configure and build default target clean delete all build directories, does not affect sources configure configure project by running cmake (default if none given) printbuilddirs print the build directory that will be used Command-options for "configure": We use cmake variables to change the build configuration. -DSERVER:BOOL=OFF Don't configure simulator/dataserver/etc -DVIEWER:BOOL=OFF Don't configure the viewer -DPACKAGE:BOOL=ON Create "package" target to make installers -DLOCALIZESETUP:BOOL=ON Create one win_setup target per supported language Examples: Set up a viewer-only project for your system: develop.py configure -DSERVER:BOOL=OFF Set up a Visual Studio 2005 project with "package" target: develop.py -G vc80 configure -DPACKAGE:BOOL=ON ''' def main(arguments): setup = setup_platform[sys.platform]() try: opts, args = getopt.getopt( arguments, '?hNt:p:G:m:', ['help', 'standalone', 'no-distcc', 'unattended', 'universal', 'type=', 'incredibuild', 'generator=', 'project=']) except getopt.GetoptError, err: print >> sys.stderr, 'Error:', err print >> sys.stderr, """ Note: You must pass -D options to cmake after the "configure" command For example: develop.py configure -DSERVER:BOOL=OFF""" print >> sys.stderr, usage_msg.strip() sys.exit(1) for o, a in opts: if o in ('-?', '-h', '--help'): print usage_msg.strip() sys.exit(0) elif o in ('--standalone',): setup.standalone = 'ON' elif o in ('--unattended',): setup.unattended = 'ON' elif o in ('--universal',): setup.universal = 'ON' elif o in ('-m',): if a in ('32', '64'): setup.word_size = int(a) else: print >> sys.stderr, 'Error: unknown word size', repr(a) print >> sys.stderr, 'Supported word sizes: 32, 64' sys.exit(1) elif o in ('-t', '--type'): try: setup.build_type = setup.build_types[a.lower()] except KeyError: print >> sys.stderr, 'Error: unknown build type', repr(a) print >> sys.stderr, 'Supported build types:' types = setup.build_types.values() types.sort() for t in types: print ' ', t sys.exit(1) elif o in ('-G', '--generator'): setup.generator = a elif o in ('-N', '--no-distcc'): setup.distcc = False elif o in ('-p', '--project'): setup.project_name = a elif o in ('--incredibuild'): setup.incredibuild = True else: print >> sys.stderr, 'INTERNAL ERROR: unhandled option', repr(o) sys.exit(1) if not args: setup.run_cmake() return try: cmd = args.pop(0) if cmd in ('cmake', 'configure'): setup.run_cmake(args) elif cmd == 'build': if os.getenv('DISTCC_DIR') is None: distcc_dir = os.path.join(getcwd(), '.distcc') if not os.path.exists(distcc_dir): os.mkdir(distcc_dir) print "setting DISTCC_DIR to %s" % distcc_dir os.environ['DISTCC_DIR'] = distcc_dir else: print "DISTCC_DIR is set to %s" % os.getenv('DISTCC_DIR') for d in setup.build_dirs(): if not os.path.exists(d): raise CommandError('run "develop.py cmake" first') setup.run_cmake() opts, targets = setup.parse_build_opts(args) setup.run_build(opts, targets) elif cmd == 'clean': if args: raise CommandError('clean takes no arguments') setup.cleanup() elif cmd == 'printbuilddirs': for d in setup.build_dirs(): print >> sys.stdout, d else: print >> sys.stderr, 'Error: unknown subcommand', repr(cmd) print >> sys.stderr, "(run 'develop.py --help' for help)" sys.exit(1) except getopt.GetoptError, err: print >> sys.stderr, 'Error with %r subcommand: %s' % (cmd, err) sys.exit(1) if __name__ == '__main__': try: main(sys.argv[1:]) except CommandError, err: print >> sys.stderr, 'Error:', err sys.exit(1)