10 from optparse import OptionGroup
11 from optparse import OptionGroup
12 from optparse import OptionParser
13 from sys import stderr
14 from sys import stdout
16 ###############################################################################
18 def errf( format, *args ):
19 stderr.write( ('ERROR: ' + format + '\n') % args )
22 def outf( format, *args ):
23 stdout.write( (format + '\n') % args )
25 ###############################################################################
27 ## Expand values of iterable object into a decent string representation.
29 def expandValues( obj ):
33 return '{ ' + buf[2:] + ' }'
35 ###############################################################################
37 ## Find executable by searching path.
38 ## On success, returns full pathname of executable.
39 ## On fail, returns None.
41 def findExecutable( name ):
42 if len( os.path.split(name)[0] ):
43 return name if os.access( name, os.X_OK ) else None
45 if not os.environ.has_key( 'PATH' ) or os.environ[ 'PATH' ] == '':
48 path = os.environ['PATH']
50 for dir in path.split( os.pathsep ):
51 f = os.path.join( dir, name )
52 if os.access( f, os.X_OK ):
56 ###############################################################################
59 ## good for darwin9.6.0 and linux
61 n = os.sysconf( 'SC_NPROCESSORS_ONLN' )
69 n = int( os.environ['NUMBER_OF_PROCESSORS'] )
77 ###############################################################################
79 ## taken from python2.6 -- we need it
80 def relpath(path, start=os.path.curdir):
81 """Return a relative version of a path"""
84 raise ValueError("no path specified")
86 start_list = os.path.abspath(start).split(os.sep)
87 path_list = os.path.abspath(path).split(os.sep)
89 # Work out how much of the filepath is shared by start and path.
90 i = len(os.path.commonprefix([start_list, path_list]))
92 rel_list = [os.pardir] * (len(start_list)-i) + path_list[i:]
95 return os.path.join(*rel_list)
97 ###############################################################################
99 # compute project dir which should be 2 dirs below this script
100 build_dir = os.curdir
101 project_dir = os.path.normpath( sys.argv[0] )
103 project_dir = os.path.dirname( project_dir )
104 if len( project_dir ) == 0:
105 project_dir = os.curdir
106 initial_project_dir = project_dir
108 ###############################################################################
110 ## model gnu-autotools platform guess
113 ## (PROC)-(VENDOR)-(SYSTEM)(RELEASE)-(EXTRA)
116 ## i386-apple-darwin9.6.0 (Mac OS X 10.5.6 Intel)
117 ## powerpc-apple-darwin9.6.0 (Mac OS X 10.5.6 PPC)
118 ## i686-pc-cygwin (Cygwin, Microsoft Vista)
119 ## x86_64-unknown-linux-gnu (Linux, Fedora 10 x86_64)
122 def __init__( self ):
123 self.machine = 'unknown'
124 self.vendor = 'unknown'
125 self.system = 'unknown'
126 self.systemc = 'Unknown'
127 self.release = '0.0.0'
130 p_system = platform.system().lower()
131 p_systemc = platform.system()
132 p_release = platform.release().lower()
133 p_processor = platform.processor().lower()
134 p_machine = platform.machine().lower()
136 if re.match( 'cygwin', p_system ):
137 self.machine = p_machine
139 self.system = 'cygwin'
140 self.systemc = 'Cygwin'
143 elif re.match( 'darwin', p_system ):
144 self.machine = p_machine
145 self.vendor = 'apple'
146 self.system = p_system
147 self.systemc = p_systemc
148 self.release = p_release
150 elif re.match( 'linux', p_system ):
151 self.machine = p_machine
152 self.vendor = 'unknown'
153 self.system = p_system
154 self.systemc = p_systemc
157 self.title = 'Linux %s' % (p_machine)
159 errf( 'unrecognized host system: %s', p_system )
163 return '%s-%s-%s%s-%s' % (self.machine,self.vendor,self.system,self.release,self.extra)
165 return '%s-%s-%s%s' % (self.machine,self.vendor,self.system,self.release)
167 def match( self, spec ):
168 return fnmatch.fnmatch( str(self), spec )
170 ###############################################################################
172 # a tool represents a command-line tool which may be searched for in PATH
174 def __init__( self, parent, optional, var, *pool ):
176 self.optional = optional
181 parent.register( self )
183 def addToConfig( self, config ):
184 config.add( self.var, self.found )
186 def addToGroup( self, group ):
187 group.add_option( '', '--' + self.name, help='specify %s location' % (self.name), default=None, metavar='EXE' )
189 def locate( self, options ):
190 spec = options.__dict__[self.name]
191 pool = self.pool if not spec else [spec]
193 self.found = findExecutable( p )
195 outf( 'located %s: %s', self.name, self.found )
198 outf( 'missing: %s (optional)', self.name )
200 errf( 'unable to locate tool: %s', self.name )
202 ## a select tool picks first found from a list of tools
203 class SelectTool( Tool ):
204 def __init__( self, parent, var, name, *pool ):
212 self.poolMap[p.name] = p
214 parent.register( self )
216 def addToConfig( self, config ):
217 config.add( self.var, self.found )
219 def addToGroup( self, group ):
220 group.add_option( '', '--' + self.name, help='select %s mode: %s' % (self.name,expandValues(self.poolMap)),
221 default=self.name, metavar='MODE' )
223 def locate( self, options ):
224 spec = options.__dict__[self.name]
225 if spec in self.poolMap:
231 outf( 'selected %s: %s', self.name, self.found )
233 errf( 'require at least one location of: %s', expandValues( self.poolMap ))
235 ###############################################################################
238 def __init__( self ):
240 Tool( self, False, 'AR.exe', 'ar' )
241 Tool( self, False, 'CP.exe', 'cp' )
242 Tool( self, True, 'CURL.exe', 'curl' )
243 Tool( self, False, 'GCC.gcc', 'gcc', 'gcc-4' )
244 Tool( self, False, 'M4.exe', 'm4' )
245 Tool( self, False, 'MKDIR.exe', 'mkdir' )
246 Tool( self, False, 'PATCH.exe', 'patch' )
247 Tool( self, False, 'RM.exe', 'rm' )
248 Tool( self, False, 'TAR.exe', 'tar' )
249 Tool( self, True, 'WGET.exe', 'wget' )
251 SelectTool( self, 'FETCH.select', 'fetch', self.wget, self.curl )
253 def register( self, item ):
254 self.__dict__[item.name] = item
255 self.items.append( item )
257 ###############################################################################
259 class OptionMode( list ):
260 def __init__( self, default, *items ):
261 super( OptionMode, self ).__init__( items )
262 self.default = items[default]
263 self.mode = self.default
266 return ' '.join( self ).replace( self.mode, '*'+self.mode )
268 def addToGroup( self, group, option, name ):
269 group.add_option( '', option, help='select %s mode: %s' % (name,self), default=self.mode, metavar='MODE' )
271 def setFromOption( self, name, mode ):
273 errf( 'invalid %s mode: %s', name, mode )
276 ###############################################################################
282 makeTool = Tool( None, False, 'CONF.make', 'gmake', 'make' )
285 debugMode = OptionMode( 0, 'none', 'min', 'std', 'max' )
286 optimizeMode = OptionMode( 1, 'none', 'speed', 'size' )
288 ## populate platform-specific architecture modes
289 if guessHost.match( 'i386-*-darwin8.*' ):
290 archMode = OptionMode( 0, 'i386', 'ppc' )
291 elif guessHost.match( 'powerpc-*-darwin8.*' ):
292 archMode = OptionMode( 1, 'i386', 'ppc' )
293 elif guessHost.match( 'i386-*-darwin9.*' ):
294 archMode = OptionMode( 0, 'i386', 'x86_64', 'ppc', 'ppc64' )
295 elif guessHost.match( 'powerpc-*-darwin9.*' ):
296 archMode = OptionMode( 2, 'i386', 'x86_64', 'ppc', 'ppc64' )
298 archMode = OptionMode( 0, guessHost.machine )
300 if guessHost.match( '*-*-darwin*' ):
302 d_prefix = '/Applications'
305 d_prefix = '/usr/local'
308 parser = OptionParser( 'Usage: %prog' )
310 ## add hidden options
311 parser.add_option( '', '--conf-method', default='terminal', action='store', help=optparse.SUPPRESS_HELP )
313 ## add install options
314 group = OptionGroup( parser, 'Installation Options' )
315 group.add_option( '', '--prefix', default=d_prefix, action='store',
316 help='specify destination for final products (%s)' % (d_prefix) )
317 parser.add_option_group( group )
319 group = OptionGroup( parser, 'Feature Options' )
321 group.add_option( '', '--disable-xcode', default=False, action='store_true',
322 help='disable Xcode (Darwin only)' )
323 group.add_option( '', '--disable-gtk', default=False, action='store_true',
324 help='disable GTK GUI (Linux only)' )
325 parser.add_option_group( group )
327 ## add launch options
328 group = OptionGroup( parser, 'Launch Options' )
329 group.add_option( '', '--launch', default=False, action='store_true',
330 help='launch build, capture log and wait for completion' )
331 group.add_option( '', '--launch-jobs', default=1, action='store', metavar='N',
332 help='allow N jobs at once; 0 to match CPU count (1)' )
333 group.add_option( '', '--launch-args', default=None, action='store', metavar='ARGS',
334 help='specify additional ARGS for launch command' )
335 group.add_option( '', '--launch-dir', default='build', action='store', metavar='DIR',
336 help='specify scratch DIR to use for build (build)' )
337 group.add_option( '', '--launch-force', default=False, action='store_true',
338 help='force use of scratch directory even if exists' )
339 group.add_option( '', '--launch-log', default='log.txt', action='store', metavar='FILE',
340 help='specify log FILE (log.txt)' )
341 group.add_option( '', '--launch-quiet', default=False, action='store_true',
342 help='do not echo build output' )
343 parser.add_option_group( group )
345 ## add compile options
346 group = OptionGroup( parser, 'Compiler Options' )
347 debugMode.addToGroup( group, '--debug', 'debug' )
348 optimizeMode.addToGroup( group, '--optimize', 'optimize' )
349 archMode.addToGroup( group, '--arch', 'architecutre' )
350 parser.add_option_group( group )
353 group = OptionGroup( parser, 'Tool Options' )
354 makeTool.addToGroup( group )
355 for tool in tools.items:
356 tool.addToGroup( group )
357 parser.add_option_group( group )
359 (options,args) = parser.parse_args()
363 m = re.match( '([^=]+)=(.*)', arg )
365 exports.append( m.groups() )
367 ## recompute values when launch mode
369 options.launch_jobs = int(options.launch_jobs)
370 build_dir = options.launch_dir
371 if os.path.isabs( build_dir ):
372 project_dir = os.getcwd()
374 project_dir = os.path.normpath( relpath( project_dir, build_dir ))
375 if options.launch_jobs == 0:
376 options.launch_jobs = computeNumCPU()
377 if options.launch_jobs < 1:
378 options.launch_jobs = 1
379 elif options.launch_jobs > 8:
380 options.launch_jobs = 8
382 ## make sure configure does not run in source root
383 if os.path.abspath( project_dir ) == os.path.abspath( build_dir ):
384 errf( 'scratch (build) directory must not be the same as source root' )
387 debugMode.setFromOption( 'debug', options.debug )
388 optimizeMode.setFromOption( 'optimize', options.optimize )
389 archMode.setFromOption( 'architecture', options.arch )
391 ## update guessBuild as per architecture mode
392 if guessHost.match( '*-*-darwin*' ):
393 if archMode.mode == 'i386':
394 guessBuild.machine = 'i386'
395 elif archMode.mode == 'x86_64':
396 guessBuild.machine = 'x86_64'
397 elif archMode.mode == 'ppc':
398 guessBuild.machine = 'powerpc'
399 elif archMode.mode == 'ppc64':
400 guessBuild.machine = 'powerpc64'
402 guessBuild.machine = archMode.mode
403 guessBuild.cross = 0 if archMode.default == archMode.mode else 1
406 makeTool.locate( options )
407 for tool in tools.items:
408 tool.locate( options )
410 ###############################################################################
412 ## Repository object.
413 ## Holds information gleaned from subversion working dir.
415 ## Builds are classed into one of the following types:
418 ## must be built from official svn with '/tags/' in the url
420 ## must be built from official svn but is not a release
425 def __init__( self ):
426 self.url = 'svn://nowhere.com/project/unknown'
427 self.root = 'svn://nowhere.com/project'
428 self.branch = 'unknown'
429 self.uuid = '00000000-0000-0000-0000-000000000000';
431 self.date = '0000-00-00 00:00:00 -0000'
432 self.wcversion = 'exported'
434 self.type = 'unofficial'
436 # parse output: svnversion PROJECT_DIR
437 cmd = 'svnversion ' + initial_project_dir
438 print 'running: %s' % (cmd)
440 p = subprocess.Popen( cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
442 if p.returncode == 0:
443 self.wcversion = p.stdout.readline().rstrip()
447 # parse output: svn info PROJECT_DIR
448 cmd = 'svn info ' + initial_project_dir
449 print 'running: %s' % (cmd)
451 p = subprocess.Popen( cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
453 if p.returncode == 0:
454 for line in p.stdout:
455 (name,value) = re.match( '([^:]+):\\s+(.+)', line.rstrip() ).groups()
458 elif name == 'Repository Root':
460 elif name == 'Repository UUID':
462 elif name == 'Revision':
463 self.rev = int( value )
464 elif name == 'Last Changed Date':
465 # strip chars in parens
466 if value.find( ' (' ):
467 self.date = value[0:value.find(' (')]
473 i = self.url.rfind( '/' )
474 if i != -1 and i < len(self.url)-1:
475 self.branch = self.url[i+1:]
477 # official UUID behavior
478 if self.uuid == 'b64f7644-9d1e-0410-96f1-a4d463321fa5':
480 m = re.match( '([^:]+)://([^/]+)/(.+)', self.url )
481 if m and re.match( 'tags/', m.group( 3 )):
482 self.type = 'release'
484 self.type = 'developer'
486 ###############################################################################
489 ## Contains manually updated version numbers consistent with HB releases
490 ## and other project metadata.
493 def __init__( self ):
494 if repo.type == 'unofficial':
495 self.name = 'NoNameBrand'
496 self.acro_lower = 'nnb'
497 self.acro_upper = 'NNB'
498 self.url_website = 'http://nonamebrand.com'
499 self.url_community = 'http://forum.nonamebrand.com'
500 self.url_irc = 'irc://irc.freenode.net/nonamebrand'
502 self.name = 'HandBrake'
503 self.acro_lower = 'hb'
504 self.acro_upper = 'HB'
505 self.url_website = 'http://handbrake.fr'
506 self.url_community = 'http://forum.handbrake.fr'
507 self.url_irc = 'irc://irc.freenode.net/handbrake'
509 self.name_lower = self.name.lower()
510 self.name_upper = self.name.upper()
516 appcastfmt = 'http://handbrake.fr/appcast%s.xml'
518 if repo.type == 'release':
519 self.version = '%d.%d.%d' % (self.vmajor,self.vminor,self.vpoint)
520 self.url_appcast = appcastfmt % ('')
521 self.build = time.strftime('%Y%m%d') + '00'
522 self.title = '%s %s (%s)' % (self.name,self.version,self.build)
523 elif repo.type == 'developer':
524 self.version = 'svn%d' % (repo.rev)
525 self.url_appcast = appcastfmt % ('_unstable')
526 self.build = time.strftime('%Y%m%d') + '01'
527 self.title = '%s svn%d (%s)' % (self.name,repo.rev,self.build)
529 self.version = 'svn%d' % (repo.rev)
530 self.version = '%d.%d.%d' % (self.vmajor,self.vminor,self.vpoint)
531 self.url_appcast = appcastfmt % ('_unofficial')
532 self.build = time.strftime('%Y%m%d') + '99'
533 self.title = 'Unofficial svn%d (%s)' % (repo.rev,self.build)
535 ###############################################################################
537 ## Config object used to output gnu-make or gnu-m4 output.
539 ## Use add() to add NAME/VALUE pairs suitable for both make/m4.
540 ## Use addBlank() to add a linefeed for both make/m4.
541 ## Use addMake() to add a make-specific line.
542 ## Use addM4() to add a m4-specific line.
545 def __init__( self ):
548 def add( self, name, value ):
549 self._items.append( (name,value) )
551 def addBlank( self ):
552 self._items.append( None )
554 def addComment( self, format, *args ):
555 self.addMake( '## ' + format % args )
556 self.addM4( 'dnl ' + format % args )
558 def addMake( self, line ):
559 self._items.append( ('?make',line) )
561 def addM4( self, line ):
562 self._items.append( ('?m4',line) )
564 def output( self, file, type ):
566 for item in self._items:
567 if item == None or item[0].find( '?' ) == 0:
569 if len(item[0]) > namelen:
570 namelen = len(item[0])
571 for item in self._items:
574 file.write( 'dnl\n' )
578 if item[0].find( '?' ) == 0:
579 if item[0].find( type, 1 ) == 1:
580 file.write( '%s\n' % (item[1]) )
584 self._outputM4( file, namelen, item[0], item[1] )
586 self._outputMake( file, namelen, item[0], item[1] )
588 def _outputMake( self, file, namelen, name, value ):
589 file.write( '%-*s = %s\n' % (namelen, name, value ))
591 def _outputM4( self, file, namelen, name, value ):
593 name = '<<__%s>>,' % name.replace( '.', '_' )
594 file.write( 'define(%-*s <<%s>>)dnl\n' % (namelen, name, value ))
596 ###############################################################################
598 ## create configure line, stripping arg --launch, quoting others
600 for arg in sys.argv[1:]:
601 if arg == '--launch':
603 configure.append( "'%s'" % (arg.replace("'", '%c%c%c%c%c' % (0x27,0x22,0x27,0x22,0x27))) )
605 ## create singletones
610 config.addComment( 'generated by configure on %s', time.strftime( '%c' ))
613 config.add( 'CONF.args', ' '.join( configure ))
616 config.add( 'HB.title', project.title )
617 config.add( 'HB.name', project.name )
618 config.add( 'HB.name.lower', project.name_lower )
619 config.add( 'HB.name.upper', project.name_upper )
620 config.add( 'HB.acro.lower', project.acro_lower )
621 config.add( 'HB.acro.upper', project.acro_upper )
623 config.add( 'HB.url.website', project.url_website )
624 config.add( 'HB.url.community', project.url_community )
625 config.add( 'HB.url.irc', project.url_irc )
626 config.add( 'HB.url.appcast', project.url_appcast )
628 config.add( 'HB.version.major', project.vmajor )
629 config.add( 'HB.version.minor', project.vminor )
630 config.add( 'HB.version.point', project.vpoint )
631 config.add( 'HB.version', project.version )
632 config.add( 'HB.version.hex', '%04x%02x%02x%02x%06x' % (project.vmajor,project.vminor,project.vpoint,0,repo.rev) )
634 config.add( 'HB.build', project.build )
636 config.add( 'HB.repo.url', repo.url )
637 config.add( 'HB.repo.root', repo.root )
638 config.add( 'HB.repo.branch', repo.branch )
639 config.add( 'HB.repo.uuid', repo.uuid )
640 config.add( 'HB.repo.rev', repo.rev )
641 config.add( 'HB.repo.date', repo.date )
642 config.add( 'HB.repo.wcversion', repo.wcversion )
643 config.add( 'HB.repo.official', repo.official )
644 config.add( 'HB.repo.type', repo.type )
647 config.add( 'HOST.spec', guessHost )
648 config.add( 'HOST.machine', guessHost.machine )
649 config.add( 'HOST.vendor', guessHost.vendor )
650 config.add( 'HOST.system', guessHost.system )
651 config.add( 'HOST.systemc', guessHost.systemc )
652 config.add( 'HOST.release', guessHost.release )
653 config.add( 'HOST.title', '%s %s' % (guessHost.systemc,archMode.default) )
654 config.add( 'HOST.extra', guessHost.extra )
655 config.add( 'HOST.ncpu', computeNumCPU() )
658 config.add( 'BUILD.spec', guessBuild )
659 config.add( 'BUILD.machine', guessBuild.machine )
660 config.add( 'BUILD.vendor', guessBuild.vendor )
661 config.add( 'BUILD.system', guessBuild.system )
662 config.add( 'BUILD.systemc', guessBuild.systemc )
663 config.add( 'BUILD.release', guessBuild.release )
664 config.add( 'BUILD.title', '%s %s' % (guessBuild.systemc,archMode.mode) )
665 config.add( 'BUILD.extra', guessBuild.extra )
666 config.add( 'BUILD.method', 'terminal' )
667 config.add( 'BUILD.cross', guessBuild.cross )
668 config.add( 'BUILD.date', time.strftime('%c') )
669 config.add( 'BUILD.arch', archMode.mode )
670 config.add( 'BUILD.jobs', computeNumCPU() )
673 config.add( 'CONF.method', options.conf_method )
676 config.add( 'BUILD/', os.curdir + os.sep )
677 config.add( 'PROJECT/', project_dir + os.sep )
680 config.add( 'INSTALL.prefix', options.prefix )
681 config.add( 'INSTALL.prefix/', '$(INSTALL.prefix)/' )
684 config.add( 'FEATURE.xcode', 0 if not hasattr(options, 'disable_xcode') or options.disable_xcode else 1 )
685 config.add( 'FEATURE.gtk', 0 if options.disable_gtk else 1 )
688 config.addMake( '## include main definitions' )
689 config.addMake( 'include $(PROJECT/)make/include/main.defs' )
692 for tool in tools.items:
693 tool.addToConfig( config )
696 config.add( 'GCC.archs', archMode.mode if guessBuild.cross else '' )
697 config.add( 'GCC.g', options.debug )
698 config.add( 'GCC.O', options.optimize )
703 config.add( nv[0], nv[1] )
706 config.addMake( '## include (optional) customization file' )
707 config.addMake( '-include $(BUID/)GNUmakefile.custom' )
710 config.addMake( '## include main rules' )
711 config.addMake( 'include $(PROJECT/)make/include/main.rules' )
713 ###############################################################################
715 # generate make or m4 file
716 def generate( type ):
718 fname = 'GNUmakefile'
720 fname = os.path.join( 'project', project.name_lower + '.m4' )
722 raise ValueError, 'unknown file type: ' + type
724 ftmp = fname + '.tmp'
726 pdir = os.path.dirname( fname )
728 if not os.path.exists( pdir ):
733 outf( 'generating %s', fname )
734 file = open( ftmp, 'w' )
735 config.output( file, type )
746 errf( 'failed writing to %s\n%s', ftmp, x )
749 os.rename( ftmp, fname )
751 errf( 'failed writing to %s\n%s', fname, x )
753 ###############################################################################
755 if not options.launch:
760 ###############################################################################
762 if os.path.exists( options.launch_dir ):
763 if not options.launch_force:
764 errf( 'scratch directory already exists: %s', options.launch_dir )
766 outf( 'creating %s', options.launch_dir )
767 os.makedirs( options.launch_dir )
769 outf( 'chdir %s', options.launch_dir )
770 os.chdir( options.launch_dir )
774 outf( 'opening %s', options.launch_log )
776 log = open( options.launch_log, 'w' )
778 errf( 'open failure: %s', x )
780 cmd = '%s -j%d' % (makeTool.found,options.launch_jobs)
781 if options.launch_args:
782 cmd += ' ' + options.launch_args
785 timeBegin = time.time()
786 s = '###\n### TIME: %s\n### launch: %s\n###\n' % (time.asctime(),cmd)
787 stdout.write( s ); stdout.flush()
788 log.write( s ); log.flush()
792 pipe = subprocess.Popen( cmd, shell=True, bufsize=1, stdout=subprocess.PIPE, stderr=subprocess.STDOUT )
794 errf( 'launch failure: %s', x )
795 for line in pipe.stdout:
796 if not options.launch_quiet:
797 stdout.write( line ); stdout.flush()
798 log.write( line ); log.flush()
802 timeEnd = time.time()
803 elapsed = timeEnd - timeBegin
804 result = '%s (exit code %d)' % ('success' if pipe.returncode == 0 else 'failed',pipe.returncode)
805 s = '###\n### TIME: %s\n### finished: %.2f seconds\n### %s\n###\n' % (time.asctime(),elapsed,result)
806 stdout.write( s ); stdout.flush()
807 log.write( s ); log.flush()