3 # Open \______ \ ____ ____ | | _\_ |__ _______ ___
4 # Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
5 # Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
6 # Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
10 # Copyright (c) 2009 Dominik Riebeling
12 # All files in this archive are subject to the GNU General Public License.
13 # See the file COPYING in the source tree root for full license agreement.
15 # This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
16 # KIND, either express or implied.
19 # Automate building releases for deployment.
20 # Run from any folder to build
22 # - any tag (using the -t option)
23 # - any local folder (using the -p option)
24 # Will build a binary archive (tar.bz2 / zip) and source archive.
25 # The source archive won't be built for local builds. Trunk and
26 # tag builds will retrieve the sources directly from svn and build
27 # below the systems temporary folder.
29 # If the required Qt installation isn't in PATH use --qmake option.
30 # Tested on Linux and MinGW / W32
32 # requires python which package (http://code.google.com/p/which/)
33 # requires pysvn package.
34 # requires upx.exe in PATH on Windows.
49 # modules that are not part of python itself.
53 print "Fatal: This script requires the pysvn package to run."
54 print " See http://pysvn.tigris.org/."
59 print "Fatal: This script requires the which package to run."
60 print " See http://code.google.com/p/which/."
64 import multiprocessing
65 cpus
= multiprocessing
.cpu_count()
66 print "Info: %s cores found." % cpus
68 print "Warning: multiprocessing module not found. Assuming 1 core."
71 # Windows nees some special treatment. Differentiate between program name
72 # and executable filename.
75 environment
= os
.environ
82 # Paths and files to retrieve from svn when creating a tarball.
83 # This is a mixed list, holding both paths and filenames.
85 # set this to true to run upx on the resulting binary, false to skip this step.
89 # OS X: files to copy into the bundle. Workaround for out-of-tree builds.
92 # DLL files to ignore when searching for required DLL files.
93 systemdlls
= ['advapi32.dll',
113 print "Usage: %s [options]" % myself
114 print " -q, --qmake=<qmake> path to qmake"
115 print " -p, --project=<pro> path to .pro file for building with local tree"
116 print " -t, --tag=<tag> use specified tag from svn"
117 print " -a, --add=<file> add file to build folder before building"
118 print " -s, --source-only only create source archive"
119 print " -b, --binary-only only create binary archive"
121 print " -n, --makensis=<file> path to makensis for building Windows setup program."
122 if sys
.platform
!= "darwin":
123 print " -d, --dynamic link dynamically instead of static"
124 print " -k, --keep-temp keep temporary folder on build failure"
125 print " -h, --help this help"
126 print " If neither a project file nor tag is specified trunk will get downloaded"
129 def getsources(svnsrv
, filelist
, dest
):
130 '''Get the files listed in filelist from svnsrv and put it at dest.'''
131 client
= pysvn
.Client()
132 print "Checking out sources from %s, please wait." % svnsrv
134 for elem
in filelist
:
135 url
= re
.subn('/$', '', svnsrv
+ elem
)[0]
136 destpath
= re
.subn('/$', '', dest
+ elem
)[0]
137 # make sure the destination path does exist
138 d
= os
.path
.dirname(destpath
)
139 if not os
.path
.exists(d
):
143 client
.export(url
, destpath
)
145 print "SVN client error: %s" % sys
.exc_value
146 print "URL: %s, destination: %s" % (url
, destpath
)
148 print "Checkout finished."
152 def gettrunkrev(svnsrv
):
153 '''Get the revision of trunk for svnsrv'''
154 client
= pysvn
.Client()
155 entries
= client
.info2(svnsrv
, recurse
=False)
156 return entries
[0][1].rev
.number
159 def findversion(versionfile
):
160 '''figure most recent program version from version.h,
161 returns version string.'''
162 h
= open(versionfile
, "r")
165 r
= re
.compile("#define +VERSION +\"(.[0-9\.a-z]+)\"")
167 s
= re
.compile("\$Revision: +([0-9]+)")
170 print "WARNING: Revision not found!"
175 '''Search for Qt4 installation. Return path to qmake.'''
176 print "Searching for Qt"
177 bins
= ["qmake", "qmake-qt4"]
180 q
= which
.which(binary
)
191 def checkqt(qmakebin
):
192 '''Check if given path to qmake exists and is a suitable version.'''
194 # check if binary exists
195 if not os
.path
.exists(qmakebin
):
196 print "Specified qmake path does not exist!"
199 output
= subprocess
.Popen([qmakebin
, "-version"], stdout
=subprocess
.PIPE
,
200 stderr
=subprocess
.PIPE
)
201 cmdout
= output
.communicate()
202 # don't check the qmake return code here, Qt3 doesn't return 0 on -version.
204 r
= re
.compile("Qt[^0-9]+([0-9\.]+[a-z]*)")
207 print "Qt found: %s" % m
.group(1)
208 s
= re
.compile("4\..*")
209 n
= re
.search(s
, m
.group(1))
215 def qmake(qmake
="qmake", projfile
=project
, wd
=".", static
=True):
216 print "Running qmake in %s..." % wd
217 command
= [qmake
, "-config", "release", "-config", "noccache"]
219 command
.append("-config")
220 command
.append("static")
221 command
.append(projfile
)
222 output
= subprocess
.Popen(command
, stdout
=subprocess
.PIPE
, cwd
=wd
, env
=environment
)
224 if not output
.returncode
== 0:
225 print "qmake returned an error!"
236 command
.append(str(cpus
))
237 output
= subprocess
.Popen(command
, stdout
=subprocess
.PIPE
, cwd
=wd
)
239 c
= output
.stdout
.readline()
240 sys
.stdout
.write(".")
242 if not output
.poll() == None:
243 sys
.stdout
.write("\n")
245 if not output
.returncode
== 0:
246 print "Build failed!"
249 if sys
.platform
!= "darwin":
250 # strip. OS X handles this via macdeployqt.
251 print "Stripping binary."
252 output
= subprocess
.Popen(["strip", progexe
], stdout
=subprocess
.PIPE
, cwd
=wd
)
254 if not output
.returncode
== 0:
255 print "Stripping failed!"
262 print "UPX'ing binary ..."
263 output
= subprocess
.Popen(["upx", progexe
], stdout
=subprocess
.PIPE
, cwd
=wd
)
265 if not output
.returncode
== 0:
266 print "UPX'ing failed!"
271 def runnsis(versionstring
, nsis
, script
, srcfolder
):
272 # run script through nsis to create installer.
273 print "Running NSIS ..."
275 # Assume the generated installer gets placed in the same folder the nsi
276 # script lives in. This seems to be a valid assumption unless the nsi
277 # script specifies a path. NSIS expects files relative to source folder so
278 # copy progexe. Additional files are injected into the nsis script.
280 # FIXME: instead of copying binaries around copy the NSI file and inject
282 b
= srcfolder
+ "/" + os
.path
.dirname(script
) + "/" + os
.path
.dirname(progexe
)
283 if not os
.path
.exists(b
):
285 shutil
.copy(srcfolder
+ "/" + progexe
, b
)
286 output
= subprocess
.Popen([nsis
, srcfolder
+ "/" + script
], stdout
=subprocess
.PIPE
)
288 if not output
.returncode
== 0:
291 setupfile
= program
+ "-" + versionstring
+ "-setup.exe"
292 # find output filename in nsis script file
294 for line
in open(srcfolder
+ "/" + script
):
295 if re
.match(r
'^[^;]*OutFile\s+', line
) != None:
296 nsissetup
= re
.sub(r
'^[^;]*OutFile\s+"(.+)"', r
'\1', line
).rstrip()
298 print "Could not retrieve output file name!"
300 shutil
.copy(srcfolder
+ "/" + os
.path
.dirname(script
) + "/" + nsissetup
, setupfile
)
304 def nsisfileinject(nsis
, outscript
, filelist
):
305 '''Inject files in filelist into NSIS script file after the File line
306 containing the main binary. This assumes that the main binary is present
307 in the NSIS script and that all additiona files (dlls etc) to get placed
309 output
= open(outscript
, "w")
310 for line
in open(nsis
, "r"):
312 # inject files after the progexe binary. Match the basename only to avoid path mismatches.
313 if re
.match(r
'^\s*File\s*.*' + os
.path
.basename(progexe
), line
, re
.IGNORECASE
):
315 injection
= " File /oname=$INSTDIR\\" + os
.path
.basename(f
) + " " + os
.path
.normcase(f
) + "\n"
316 output
.write(injection
)
317 output
.write(" ; end of injected files\n")
321 def finddlls(program
, extrapaths
= []):
322 '''Check program for required DLLs. Find all required DLLs except ignored
323 ones and return a list of DLL filenames (including path).'''
324 # ask objdump about dependencies.
325 output
= subprocess
.Popen(["objdump", "-x", program
], stdout
=subprocess
.PIPE
)
326 cmdout
= output
.communicate()
328 # create list of used DLLs. Store as lower case as W32 is case-insensitive.
330 for line
in cmdout
[0].split('\n'):
331 if re
.match(r
'\s*DLL Name', line
) != None:
332 dll
= re
.sub(r
'^\s*DLL Name:\s+([a-zA-Z_\-0-9\.\+]+).*$', r
'\1', line
)
333 dlls
.append(dll
.lower())
335 # find DLLs in extrapaths and PATH environment variable.
338 if file in systemdlls
:
339 print file + ": System DLL"
342 for path
in extrapaths
:
343 if os
.path
.exists(path
+ "/" + file):
344 dllpath
= re
.sub(r
"\\", r
"/", path
+ "/" + file)
345 print file + ": found at " + dllpath
346 dllpaths
.append(dllpath
)
350 dllpath
= re
.sub(r
"\\", r
"/", which
.which(file))
351 print file + ": found at " + dllpath
352 dllpaths
.append(dllpath
)
354 print file + ": NOT FOUND."
358 def zipball(versionstring
, buildfolder
):
359 '''package created binary'''
360 print "Creating binary zipball."
361 archivebase
= program
+ "-" + versionstring
362 outfolder
= buildfolder
+ "/" + archivebase
363 archivename
= archivebase
+ ".zip"
364 # create output folder
366 # move program files to output folder
367 for f
in programfiles
:
368 if re
.match(r
'^(/|[a-zA-Z]:)', f
) != None:
369 shutil
.copy(f
, outfolder
)
371 shutil
.copy(buildfolder
+ "/" + f
, outfolder
)
372 # create zipball from output folder
373 zf
= zipfile
.ZipFile(archivename
, mode
='w', compression
=zipfile
.ZIP_DEFLATED
)
374 for root
, dirs
, files
in os
.walk(outfolder
):
376 physname
= os
.path
.join(root
, name
)
377 filename
= re
.sub("^" + buildfolder
, "", physname
)
378 zf
.write(physname
, filename
)
380 physname
= os
.path
.join(root
, name
)
381 filename
= re
.sub("^" + buildfolder
, "", physname
)
382 zf
.write(physname
, filename
)
384 # remove output folder
385 shutil
.rmtree(outfolder
)
389 def tarball(versionstring
, buildfolder
):
390 '''package created binary'''
391 print "Creating binary tarball."
392 archivebase
= program
+ "-" + versionstring
393 outfolder
= buildfolder
+ "/" + archivebase
394 archivename
= archivebase
+ ".tar.bz2"
395 # create output folder
397 # move program files to output folder
398 for f
in programfiles
:
399 shutil
.copy(buildfolder
+ "/" + f
, outfolder
)
400 # create tarball from output folder
401 tf
= tarfile
.open(archivename
, mode
='w:bz2')
402 tf
.add(outfolder
, archivebase
)
404 # remove output folder
405 shutil
.rmtree(outfolder
)
409 def macdeploy(versionstring
, buildfolder
):
410 '''package created binary to dmg'''
411 dmgfile
= program
+ "-" + versionstring
+ ".dmg"
412 appbundle
= buildfolder
+ "/" + progexe
414 # workaround to Qt issues when building out-of-tree. Copy files into bundle.
415 sourcebase
= buildfolder
+ re
.sub('[^/]+.pro$', '', project
) + "/"
417 for src
in bundlecopy
:
418 shutil
.copy(sourcebase
+ src
, appbundle
+ "/" + bundlecopy
[src
])
419 # end of Qt workaround
421 output
= subprocess
.Popen(["macdeployqt", progexe
, "-dmg"], stdout
=subprocess
.PIPE
, cwd
=buildfolder
)
423 if not output
.returncode
== 0:
424 print "macdeployqt failed!"
426 # copy dmg to output folder
427 shutil
.copy(buildfolder
+ "/" + program
+ ".dmg", dmgfile
)
430 def filehashes(filename
):
431 '''Calculate md5 and sha1 hashes for a given file.'''
432 if not os
.path
.exists(filename
):
436 f
= open(filename
, 'rb')
443 return [m
.hexdigest(), s
.hexdigest()]
446 def filestats(filename
):
447 if not os
.path
.exists(filename
):
449 st
= os
.stat(filename
)
450 print filename
, "\n", "-" * len(filename
)
451 print "Size: %i bytes" % st
.st_size
452 h
= filehashes(filename
)
453 print "md5sum: %s" % h
[0]
454 print "sha1sum: %s" % h
[1]
455 print "-" * len(filename
), "\n"
458 def tempclean(workfolder
, nopro
):
460 print "Cleaning up working folder %s" % workfolder
461 shutil
.rmtree(workfolder
)
463 print "Project file specified or cleanup disabled!"
464 print "Temporary files kept at %s" % workfolder
468 startup
= time
.time()
471 opts
, args
= getopt
.getopt(sys
.argv
[1:], "q:p:t:a:n:sbdkh",
472 ["qmake=", "project=", "tag=", "add=", "makensis=", "source-only", "binary-only", "dynamic", "keep-temp", "help"])
473 except getopt
.GetoptError
, err
:
479 svnbase
= svnserver
+ "trunk/"
487 if sys
.platform
!= "darwin":
492 if o
in ("-q", "--qmake"):
494 if o
in ("-p", "--project"):
497 if o
in ("-t", "--tag"):
499 svnbase
= svnserver
+ "tags/" + tag
+ "/"
500 if o
in ("-a", "--add"):
502 if o
in ("-n", "--makensis"):
504 if o
in ("-s", "--source-only"):
506 if o
in ("-b", "--binary-only"):
508 if o
in ("-d", "--dynamic") and sys
.platform
!= "darwin":
510 if o
in ("-k", "--keep-temp"):
512 if o
in ("-h", "--help"):
516 if source
== False and binary
== False:
517 print "Building build neither source nor binary means nothing to do. Exiting."
526 print "ERROR: No suitable Qt installation found."
529 # create working folder. Use current directory if -p option used.
531 w
= tempfile
.mkdtemp()
532 # make sure the path doesn't contain backslashes to prevent issues
533 # later when running on windows.
534 workfolder
= re
.sub(r
'\\', '/', w
)
536 sourcefolder
= workfolder
+ "/" + tag
+ "/"
537 archivename
= tag
+ "-src.tar.bz2"
538 # get numeric version part from tag
539 ver
= "v" + re
.sub('^[^\d]+', '', tag
)
541 trunk
= gettrunkrev(svnbase
)
542 sourcefolder
= workfolder
+ "/" + program
+ "-r" + str(trunk
) + "/"
543 archivename
= program
+ "-r" + str(trunk
) + "-src.tar.bz2"
544 ver
= "r" + str(trunk
)
545 os
.mkdir(sourcefolder
)
550 # check if project file explicitly given. If yes, don't get sources from svn
552 proj
= sourcefolder
+ project
553 # get sources and pack source tarball
554 if not getsources(svnbase
, svnpaths
, sourcefolder
) == 0:
555 tempclean(workfolder
, cleanup
and not keeptemp
)
559 tf
= tarfile
.open(archivename
, mode
='w:bz2')
560 tf
.add(sourcefolder
, os
.path
.basename(re
.subn('/$', '', sourcefolder
)[0]))
563 shutil
.rmtree(workfolder
)
566 # figure version from sources. Need to take path to project file into account.
567 versionfile
= re
.subn('[\w\.]+$', "version.h", proj
)[0]
568 ver
= findversion(versionfile
)
571 if not os
.path
.exists(proj
):
572 print "ERROR: path to project file wrong."
575 # copy specified (--add) files to working folder
577 shutil
.copy(f
, sourcefolder
)
578 buildstart
= time
.time()
579 header
= "Building %s %s" % (program
, ver
)
581 print len(header
) * "="
584 if not qmake(qm
, proj
, sourcefolder
, static
) == 0:
585 tempclean(workfolder
, cleanup
and not keeptemp
)
587 if not build(sourcefolder
) == 0:
588 tempclean(workfolder
, cleanup
and not keeptemp
)
590 buildtime
= time
.time() - buildstart
591 if sys
.platform
== "win32":
593 if not upxfile(sourcefolder
) == 0:
594 tempclean(workfolder
, cleanup
and not keeptemp
)
596 dllfiles
= finddlls(sourcefolder
+ "/" + progexe
, [os
.path
.dirname(qm
)])
597 if dllfiles
.count
> 0:
598 programfiles
.extend(dllfiles
)
599 archive
= zipball(ver
, sourcefolder
)
600 # only when running native right now.
601 if nsisscript
!= "" and makensis
!= "":
602 nsisfileinject(sourcefolder
+ "/" + nsisscript
, sourcefolder
+ "/" + nsisscript
+ ".tmp", dllfiles
)
603 runnsis(ver
, makensis
, nsisscript
+ ".tmp", sourcefolder
)
604 elif sys
.platform
== "darwin":
605 archive
= macdeploy(ver
, sourcefolder
)
607 if os
.uname()[4].endswith("64"):
609 archive
= tarball(ver
, sourcefolder
)
611 # remove temporary files
612 tempclean(workfolder
, cleanup
)
615 headline
= "Build Summary for %s" % program
616 print "\n", headline
, "\n", "=" * len(headline
)
617 if not archivename
== "":
618 filestats(archivename
)
620 duration
= time
.time() - startup
621 durmins
= (int)(duration
/ 60)
622 dursecs
= (int)(duration
% 60)
623 buildmins
= (int)(buildtime
/ 60)
624 buildsecs
= (int)(buildtime
% 60)
625 print "Overall time %smin %ssec, building took %smin %ssec." % \
626 (durmins
, dursecs
, buildmins
, buildsecs
)
629 if __name__
== "__main__":