2 # -*- coding: utf-8 -*-
4 # build.py - part of the FDroid server tools
5 # Copyright (C) 2010-12, Ciaran Gultnieks, ciaran@ciarang.com
6 # Copyright (C) 2013 Daniel Martà <mvdan@mvdan.cc>
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU Affero General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU Affero General Public License for more details.
18 # You should have received a copy of the GNU Affero General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
30 from optparse import OptionParser
33 from common import BuildException, VCSException, FDroidPopen
35 def get_builder_vm_id():
36 vd = os.path.join('builder', '.vagrant')
38 # Vagrant 1.2 (and maybe 1.1?) it's a directory tree...
39 with open(os.path.join(vd, 'machines', 'default', 'virtualbox', 'id')) as vf:
43 # Vagrant 1.0 - it's a json file...
44 with open(os.path.join('builder', '.vagrant')) as vf:
46 return v['active']['default']
48 def got_valid_builder_vm():
49 """Returns True if we have a valid-looking builder vm
51 if not os.path.exists(os.path.join('builder', 'Vagrantfile')):
53 vd = os.path.join('builder', '.vagrant')
54 if not os.path.exists(vd):
56 if not os.path.isdir(vd):
57 # Vagrant 1.0 - if the directory is there, it's valid...
59 # Vagrant 1.2 - the directory can exist, but the id can be missing...
60 if not os.path.exists(os.path.join(vd, 'machines', 'default', 'virtualbox', 'id')):
65 # Note that 'force' here also implies test mode.
66 def build_server(app, thisbuild, vcs, build_dir, output_dir, sdk_path, force):
67 """Do a build on the build server."""
71 # Reset existing builder machine to a clean state if possible.
73 if not options.resetserver:
74 print "Checking for valid existing build server"
75 if got_valid_builder_vm():
76 print "...VM is present"
77 p = subprocess.Popen(['VBoxManage', 'snapshot', get_builder_vm_id(), 'list', '--details'],
78 cwd='builder', stdout=subprocess.PIPE)
79 output = p.communicate()[0]
80 if output.find('fdroidclean') != -1:
81 print "...snapshot exists - resetting build server to clean state"
82 p = subprocess.Popen(['vagrant', 'status'],
83 cwd='builder', stdout=subprocess.PIPE)
84 output = p.communicate()[0]
85 if output.find('running') != -1:
87 subprocess.call(['vagrant', 'suspend'], cwd='builder')
88 if subprocess.call(['VBoxManage', 'snapshot', get_builder_vm_id(), 'restore', 'fdroidclean'],
90 print "...reset to snapshot - server is valid"
91 if subprocess.call(['vagrant', 'up'], cwd='builder') != 0:
92 raise BuildException("Failed to start build server")
95 print "...failed to reset to snapshot"
97 print "...snapshot doesn't exist - VBoxManage snapshot list:\n" + output
99 # If we can't use the existing machine for any reason, make a
100 # new one from scratch.
102 if os.path.exists('builder'):
103 print "Removing broken/incomplete/unwanted build server"
104 subprocess.call(['vagrant', 'destroy', '-f'], cwd='builder')
105 shutil.rmtree('builder')
108 p = subprocess.Popen('vagrant --version', shell=True, stdout=subprocess.PIPE)
109 vver = p.communicate()[0]
110 if vver.startswith('Vagrant version 1.2'):
111 with open('builder/Vagrantfile', 'w') as vf:
112 vf.write('Vagrant.configure("2") do |config|\n')
113 vf.write('config.vm.box = "buildserver"\n')
116 with open('builder/Vagrantfile', 'w') as vf:
117 vf.write('Vagrant::Config.run do |config|\n')
118 vf.write('config.vm.box = "buildserver"\n')
121 print "Starting new build server"
122 if subprocess.call(['vagrant', 'up'], cwd='builder') != 0:
123 raise BuildException("Failed to start build server")
125 # Open SSH connection to make sure it's working and ready...
126 print "Connecting to virtual machine..."
127 if subprocess.call('vagrant ssh-config >sshconfig',
128 cwd='builder', shell=True) != 0:
129 raise BuildException("Error getting ssh config")
130 vagranthost = 'default' # Host in ssh config file
131 sshconfig = ssh.SSHConfig()
132 sshf = open('builder/sshconfig', 'r')
133 sshconfig.parse(sshf)
135 sshconfig = sshconfig.lookup(vagranthost)
136 sshs = ssh.SSHClient()
137 sshs.set_missing_host_key_policy(ssh.AutoAddPolicy())
138 idfile = sshconfig['identityfile']
139 if idfile.startswith('"') and idfile.endswith('"'):
140 idfile = idfile[1:-1]
141 sshs.connect(sshconfig['hostname'], username=sshconfig['user'],
142 port=int(sshconfig['port']), timeout=300, look_for_keys=False,
146 print "Saving clean state of new build server"
147 if subprocess.call(['vagrant', 'suspend'], cwd='builder') != 0:
148 raise BuildException("Failed to suspend build server")
149 print "...waiting a sec..."
151 if subprocess.call(['VBoxManage', 'snapshot', get_builder_vm_id(), 'take', 'fdroidclean'],
153 raise BuildException("Failed to take snapshot")
154 print "Restarting new build server"
155 if subprocess.call(['vagrant', 'up'], cwd='builder') != 0:
156 raise BuildException("Failed to start build server")
157 # Make sure it worked...
158 p = subprocess.Popen(['VBoxManage', 'snapshot', get_builder_vm_id(), 'list', '--details'],
159 cwd='builder', stdout=subprocess.PIPE)
160 output = p.communicate()[0]
161 if output.find('fdroidclean') == -1:
162 raise BuildException("Failed to take snapshot.")
166 # Get SSH configuration settings for us to connect...
167 print "Getting ssh configuration..."
168 subprocess.call('vagrant ssh-config >sshconfig',
169 cwd='builder', shell=True)
170 vagranthost = 'default' # Host in ssh config file
172 # Load and parse the SSH config...
173 sshconfig = ssh.SSHConfig()
174 sshf = open('builder/sshconfig', 'r')
175 sshconfig.parse(sshf)
177 sshconfig = sshconfig.lookup(vagranthost)
179 # Open SSH connection...
180 print "Connecting to virtual machine..."
181 sshs = ssh.SSHClient()
182 sshs.set_missing_host_key_policy(ssh.AutoAddPolicy())
183 idfile = sshconfig['identityfile']
184 if idfile.startswith('"') and idfile.endswith('"'):
185 idfile = idfile[1:-1]
186 sshs.connect(sshconfig['hostname'], username=sshconfig['user'],
187 port=int(sshconfig['port']), timeout=300, look_for_keys=False,
190 # Get an SFTP connection...
191 ftp = sshs.open_sftp()
192 ftp.get_channel().settimeout(15)
194 # Put all the necessary files in place...
195 ftp.chdir('/home/vagrant')
197 # Helper to copy the contents of a directory to the server...
199 root = os.path.dirname(path)
200 main = os.path.basename(path)
202 for r, d, f in os.walk(path):
203 rr = os.path.relpath(r, root)
208 lfile = os.path.join(root, rr, ff)
209 if not os.path.islink(lfile):
211 ftp.chmod(ff, os.stat(lfile).st_mode)
212 for i in range(len(rr.split('/'))):
216 print "Preparing server for build..."
217 serverpath = os.path.abspath(os.path.dirname(__file__))
218 ftp.put(os.path.join(serverpath, 'build.py'), 'build.py')
219 ftp.put(os.path.join(serverpath, 'common.py'), 'common.py')
220 ftp.put(os.path.join(serverpath, '..', 'config.buildserver.py'), 'config.py')
222 # Copy the metadata - just the file for this app...
223 ftp.mkdir('metadata')
225 ftp.chdir('metadata')
226 ftp.put(os.path.join('metadata', app['id'] + '.txt'),
228 # And patches if there are any...
229 if os.path.exists(os.path.join('metadata', app['id'])):
230 send_dir(os.path.join('metadata', app['id']))
232 ftp.chdir('/home/vagrant')
233 # Create the build directory...
238 # Copy any extlibs that are required...
239 if 'extlibs' in thisbuild:
240 ftp.chdir('/home/vagrant/build/extlib')
241 for lib in thisbuild['extlibs'].split(';'):
245 if d not in ftp.listdir():
248 ftp.put(os.path.join('build/extlib', lib), lp[-1])
251 # Copy any srclibs that are required...
253 if 'srclibs' in thisbuild:
254 for lib in thisbuild['srclibs'].split(';'):
256 name, _ = lib.split('@')
258 print "Processing srclib '" + name + "'"
259 srclibpaths.append((name, common.getsrclib(lib, 'build/srclib', sdk_path, basepath=True, prepare=False)))
260 # If one was used for the main source, add that too.
261 basesrclib = vcs.getsrclib()
263 srclibpaths.append(basesrclib)
264 for name, lib in srclibpaths:
265 print "Sending srclib '" + lib + "'"
266 ftp.chdir('/home/vagrant/build/srclib')
267 if not os.path.exists(lib):
268 raise BuildException("Missing srclib directory '" + lib + "'")
269 fv = '.fdroidvcs-' + name
270 ftp.put(os.path.join('build/srclib', fv), fv)
272 # Copy the metadata file too...
273 ftp.chdir('/home/vagrant/srclibs')
274 ftp.put(os.path.join('srclibs', name + '.txt'),
276 # Copy the main app source code
277 # (no need if it's a srclib)
278 if (not basesrclib) and os.path.exists(build_dir):
279 ftp.chdir('/home/vagrant/build')
280 fv = '.fdroidvcs-' + app['id']
281 ftp.put(os.path.join('build', fv), fv)
284 # Execute the build script...
285 print "Starting build..."
286 chan = sshs.get_transport().open_session()
287 cmdline = 'python build.py --on-server'
289 cmdline += ' --force --test'
290 cmdline += ' -p ' + app['id'] + ' --vercode ' + thisbuild['vercode']
291 chan.exec_command('bash -c ". ~/.bsenv && ' + cmdline + '"')
294 while not chan.exit_status_ready():
295 while chan.recv_ready():
296 output += chan.recv(1024)
297 while chan.recv_stderr_ready():
298 error += chan.recv_stderr(1024)
299 print "...getting exit status"
300 returncode = chan.recv_exit_status()
301 while chan.recv_ready():
302 output += chan.recv(1024)
303 while chan.recv_stderr_ready():
304 error += chan.recv_stderr(1024)
306 raise BuildException("Build.py failed on server for %s:%s" % (app['id'], thisbuild['version']), output, error)
308 # Retrieve the built files...
309 print "Retrieving build output..."
311 ftp.chdir('/home/vagrant/tmp')
313 ftp.chdir('/home/vagrant/unsigned')
314 apkfile = common.getapkname(app,thisbuild)
315 tarball = common.getsrcname(app,thisbuild)
317 ftp.get(apkfile, os.path.join(output_dir, apkfile))
318 ftp.get(tarball, os.path.join(output_dir, tarball))
320 raise BuildException("Build failed for %s:%s - missing output files" % (app['id'], thisbuild['version']), output, error)
325 # Suspend the build server.
326 print "Suspending build server"
327 subprocess.call(['vagrant', 'suspend'], cwd='builder')
329 def adapt_gradle(path, verbose):
331 print "Adapting build.gradle at %s" % path
333 subprocess.call(['sed', '-i',
334 's@buildToolsVersion[ ]*["\\\'][0-9\.]*["\\\']@buildToolsVersion "'+build_tools+'"@g', path])
336 subprocess.call(['sed', '-i',
337 's@com.android.tools.build:gradle:[0-9\.\+]*@com.android.tools.build:gradle:'+gradle_plugin+'@g', path])
340 def build_local(app, thisbuild, vcs, build_dir, output_dir, srclib_dir, extlib_dir, tmp_dir, install, force, verbose, onserver):
341 """Do a build locally."""
343 # Prepare the source code...
344 root_dir, srclibpaths = common.prepare_source(vcs, app, thisbuild,
345 build_dir, srclib_dir, extlib_dir, sdk_path, ndk_path,
346 javacc_path, mvn3, verbose, onserver)
348 # We need to clean via the build tool in case the binary dirs are
349 # different from the default ones
351 if 'maven' in thisbuild:
352 print "Cleaning Maven project..."
353 cmd = [mvn3, 'clean', '-Dandroid.sdk.path=' + sdk_path]
355 if '@' in thisbuild['maven']:
356 maven_dir = os.path.join(root_dir, thisbuild['maven'].split('@')[1])
360 p = FDroidPopen(cmd, cwd=maven_dir, verbose=verbose)
361 elif 'gradle' in thisbuild:
362 print "Cleaning Gradle project..."
363 cmd = [gradle, 'clean']
365 if '@' in thisbuild['gradle']:
366 gradle_dir = os.path.join(root_dir, thisbuild['gradle'].split('@')[1])
368 gradle_dir = root_dir
370 p = FDroidPopen(cmd, cwd=gradle_dir, verbose=verbose)
371 elif thisbuild.get('update', '.') != 'no':
372 print "Cleaning Ant project..."
373 cmd = ['ant', 'clean']
374 p = FDroidPopen(cmd, cwd=root_dir, verbose=verbose)
376 if p is not None and p.returncode != 0:
377 raise BuildException("Error cleaning %s:%s" %
378 (app['id'], thisbuild['version']), p.stdout, p.stderr)
381 print "Cleaning jni dirs..."
383 'libs/armeabi-v7a', 'libs/armeabi',
384 'libs/mips', 'libs/x86']:
385 badpath = os.path.join(build_dir, baddir)
386 if os.path.exists(badpath):
387 print "Removing '%s'" % badpath
388 shutil.rmtree(badpath)
390 # Scan before building...
391 print "Scanning source for common problems..."
392 buildprobs = common.scan_source(build_dir, root_dir, thisbuild)
393 if len(buildprobs) > 0:
394 print 'Scanner found ' + str(len(buildprobs)) + ' problems:'
395 for problem in buildprobs:
396 print '...' + problem
398 raise BuildException("Can't build due to " +
399 str(len(buildprobs)) + " scanned problems")
401 # Build the source tarball right before we build the release...
402 print "Creating source tarball..."
403 tarname = common.getsrcname(app,thisbuild)
404 tarball = tarfile.open(os.path.join(tmp_dir, tarname), "w:gz")
406 for vcs_dir in ['.svn', '.git', '.hg', '.bzr']:
407 if f.endswith(vcs_dir):
410 tarball.add(build_dir, tarname, exclude=tarexc)
413 # Run a build command if one is required...
414 if 'build' in thisbuild:
415 cmd = thisbuild['build']
416 # Substitute source library paths into commands...
417 for name, libpath in srclibpaths:
418 libpath = os.path.relpath(libpath, root_dir)
419 cmd = cmd.replace('$$' + name + '$$', libpath)
420 cmd = cmd.replace('$$SDK$$', sdk_path)
421 cmd = cmd.replace('$$NDK$$', ndk_path)
422 cmd = cmd.replace('$$MVN3$$', mvn3)
424 print "Running 'build' commands in %s" % root_dir
426 p = FDroidPopen(['bash', '-x', '-c', cmd],
427 cwd=root_dir, verbose=verbose)
429 if p.returncode != 0:
430 raise BuildException("Error running build command for %s:%s" %
431 (app['id'], thisbuild['version']), p.stdout, p.stderr)
433 # Build native stuff if required...
434 if thisbuild.get('buildjni') not in (None, 'no'):
435 print "Building native libraries..."
436 jni_components = thisbuild.get('buildjni')
437 if jni_components == 'yes':
438 jni_components = ['']
440 jni_components = [c.strip() for c in jni_components.split(';')]
441 ndkbuild = os.path.join(ndk_path, "ndk-build")
442 for d in jni_components:
444 print "Running ndk-build in " + root_dir + '/' + d
445 manifest = root_dir + '/' + d + '/AndroidManifest.xml'
446 if os.path.exists(manifest):
447 # Read and write the whole AM.xml to fix newlines and avoid
448 # the ndk r8c or later 'wordlist' errors. The outcome of this
449 # under gnu/linux is the same as when using tools like
450 # dos2unix, but the native python way is faster and will
451 # work in non-unix systems.
452 manifest_text = open(manifest, 'U').read()
453 open(manifest, 'w').write(manifest_text)
454 # In case the AM.xml read was big, free the memory
456 p = FDroidPopen([ndkbuild], cwd=os.path.join(root_dir,d),
458 if p.returncode != 0:
459 raise BuildException("NDK build failed for %s:%s" % (app['id'], thisbuild['version']), p.stdout, p.stderr)
462 # Build the release...
463 if 'maven' in thisbuild:
464 print "Building Maven project..."
466 if '@' in thisbuild['maven']:
467 maven_dir = os.path.join(root_dir, thisbuild['maven'].split('@')[1])
471 mvncmd = [mvn3, '-Dandroid.sdk.path=' + sdk_path]
473 mvncmd += ['-Dandroid.sign.debug=true', 'package', 'android:deploy']
475 mvncmd += ['-Dandroid.sign.debug=false', '-Dandroid.release=true', 'package']
476 if 'target' in thisbuild:
477 target = thisbuild["target"].split('-')[1]
478 subprocess.call(['sed', '-i',
479 's@<platform>[0-9]*</platform>@<platform>'+target+'</platform>@g',
480 'pom.xml'], cwd=root_dir)
481 if '@' in thisbuild['maven']:
482 subprocess.call(['sed', '-i',
483 's@<platform>[0-9]*</platform>@<platform>'+target+'</platform>@g',
484 'pom.xml'], cwd=maven_dir)
486 if 'mvnflags' in thisbuild:
487 mvncmd += thisbuild['mvnflags']
489 p = FDroidPopen(mvncmd, cwd=maven_dir, verbose=verbose, apkoutput=True)
491 elif 'gradle' in thisbuild:
492 print "Building Gradle project..."
493 if '@' in thisbuild['gradle']:
494 flavour = thisbuild['gradle'].split('@')[0]
495 gradle_dir = thisbuild['gradle'].split('@')[1]
496 gradle_dir = os.path.join(root_dir, gradle_dir)
498 flavour = thisbuild['gradle']
499 gradle_dir = root_dir
502 if 'compilesdk' in thisbuild:
503 level = thisbuild["compilesdk"].split('-')[1]
504 subprocess.call(['sed', '-i',
505 's@compileSdkVersion[ ]*[0-9]*@compileSdkVersion '+level+'@g',
506 'build.gradle'], cwd=root_dir)
507 if '@' in thisbuild['gradle']:
508 subprocess.call(['sed', '-i',
509 's@compileSdkVersion[ ]*[0-9]*@compileSdkVersion '+level+'@g',
510 'build.gradle'], cwd=gradle_dir)
512 for root, dirs, files in os.walk(build_dir):
514 if f == 'build.gradle':
515 adapt_gradle(os.path.join(root, f), verbose)
518 if flavour in ['main', 'yes', '']:
522 if 'preassemble' in thisbuild:
523 for task in thisbuild['preassemble'].split():
524 commands.append(task)
526 commands += ['assemble'+flavour+'Debug', 'install'+flavour+'Debug']
528 commands += ['assemble'+flavour+'Release']
530 p = FDroidPopen(commands, cwd=gradle_dir, verbose=verbose)
533 print "Building Ant project..."
536 cmd += ['debug','install']
537 elif 'antcommand' in thisbuild:
538 cmd += [thisbuild['antcommand']]
541 p = FDroidPopen(cmd, cwd=root_dir, verbose=verbose, apkoutput=True)
543 if p.returncode != 0:
544 raise BuildException("Build failed for %s:%s" % (app['id'], thisbuild['version']), p.stdout, p.stderr)
545 print "Successfully built version " + thisbuild['version'] + ' of ' + app['id']
550 # Find the apk name in the output...
551 if 'bindir' in thisbuild:
552 bindir = os.path.join(build_dir, thisbuild['bindir'])
553 elif 'maven' in thisbuild:
554 bindir = os.path.join(root_dir, 'target')
556 bindir = os.path.join(root_dir, 'bin')
557 if 'maven' in thisbuild:
558 m = re.match(r".*^\[INFO\] .*apkbuilder.*/([^/]*)\.apk",
559 p.stdout_apk, re.S|re.M)
561 m = re.match(r".*^\[INFO\] Creating additional unsigned apk file .*/([^/]+)\.apk[^l]",
562 p.stdout_apk, re.S|re.M)
564 # This format is found in com.github.mobile, com.yubico.yubitotp and com.botbrew.basil for example...
565 m = re.match(r'.*^\[INFO\] [^$]*aapt \[package,[^$]*' + bindir + '/([^/]+)\.ap[_k][,\]]',
566 p.stdout_apk, re.S|re.M)
568 raise BuildException('Failed to find output')
570 src = os.path.join(bindir, src) + '.apk'
571 elif 'gradle' in thisbuild:
573 if 'subdir' in thisbuild:
574 dd = os.path.join(dd, thisbuild['subdir'])
575 if flavour in ['main', 'yes', '']:
576 name = '-'.join([os.path.basename(dd), 'release', 'unsigned'])
578 name = '-'.join([os.path.basename(dd), flavour, 'release', 'unsigned'])
579 src = os.path.join(dd, 'build', 'apk', name+'.apk')
581 src = re.match(r".*^.*Creating (.+) for release.*$.*", p.stdout_apk,
583 src = os.path.join(bindir, src)
585 # Make sure it's not debuggable...
586 if common.isApkDebuggable(src):
587 raise BuildException("APK is debuggable")
589 # By way of a sanity check, make sure the version and version
590 # code in our new apk match what we expect...
591 print "Checking " + src
592 if not os.path.exists(src):
593 raise BuildException("Unsigned apk is not at expected location of " + src)
595 p = subprocess.Popen([os.path.join(sdk_path, 'build-tools', build_tools, 'aapt'),
596 'dump', 'badging', src],
597 stdout=subprocess.PIPE)
598 output = p.communicate()[0]
603 for line in output.splitlines():
604 if line.startswith("package:"):
605 pat = re.compile(".*name='([a-zA-Z0-9._]*)'.*")
606 foundid = re.match(pat, line).group(1)
607 pat = re.compile(".*versionCode='([0-9]*)'.*")
608 vercode = re.match(pat, line).group(1)
609 pat = re.compile(".*versionName='([^']*)'.*")
610 version = re.match(pat, line).group(1)
611 if thisbuild.get('novcheck', 'no') == "yes":
612 vercode = thisbuild['vercode']
613 version = thisbuild['version']
614 if not version or not vercode:
615 raise BuildException("Could not find version information in build in output")
617 raise BuildException("Could not find package ID in output")
618 if foundid != app['id']:
619 raise BuildException("Wrong package ID - build " + foundid + " but expected " + app['id'])
621 # Some apps (e.g. Timeriffic) have had the bonkers idea of
622 # including the entire changelog in the version number. Remove
623 # it so we can compare. (TODO: might be better to remove it
624 # before we compile, in fact)
625 index = version.find(" //")
627 version = version[:index]
629 if (version != thisbuild['version'] or
630 vercode != thisbuild['vercode']):
631 raise BuildException(("Unexpected version/version code in output;"
632 " APK: '%s' / '%s', "
633 " Expected: '%s' / '%s'")
634 % (version, str(vercode), thisbuild['version'], str(thisbuild['vercode']))
637 # Copy the unsigned apk to our destination directory for further
638 # processing (by publish.py)...
639 dest = os.path.join(output_dir, common.getapkname(app,thisbuild))
640 shutil.copyfile(src, dest)
642 # Move the source tarball into the output directory...
643 if output_dir != tmp_dir:
644 shutil.move(os.path.join(tmp_dir, tarname),
645 os.path.join(output_dir, tarname))
648 def trybuild(app, thisbuild, build_dir, output_dir, also_check_dir, srclib_dir, extlib_dir,
649 tmp_dir, repo_dir, vcs, test, server, install, force, verbose, onserver):
651 Build a particular version of an application, if it needs building.
653 :param output_dir: The directory where the build output will go. Usually
654 this is the 'unsigned' directory.
655 :param repo_dir: The repo directory - used for checking if the build is
657 :paaram also_check_dir: An additional location for checking if the build
658 is necessary (usually the archive repo)
659 :param test: True if building in test mode, in which case the build will
660 always happen, even if the output already exists. In test mode, the
661 output directory should be a temporary location, not any of the real
664 :returns: True if the build was done, False if it wasn't necessary.
667 dest_apk = common.getapkname(app, thisbuild)
669 dest = os.path.join(output_dir, dest_apk)
670 dest_repo = os.path.join(repo_dir, dest_apk)
673 if os.path.exists(dest) or os.path.exists(dest_repo):
677 dest_also = os.path.join(also_check_dir, dest_apk)
678 if os.path.exists(dest_also):
681 if 'disable' in thisbuild:
684 print "Building version " + thisbuild['version'] + ' of ' + app['id']
687 # When using server mode, still keep a local cache of the repo, by
688 # grabbing the source now.
689 vcs.gotorevision(thisbuild['commit'])
691 build_server(app, thisbuild, vcs, build_dir, output_dir, sdk_path, force)
693 build_local(app, thisbuild, vcs, build_dir, output_dir, srclib_dir, extlib_dir, tmp_dir, install, force, verbose, onserver)
697 def parse_commandline():
698 """Parse the command line. Returns options, args."""
700 parser = OptionParser()
701 parser.add_option("-v", "--verbose", action="store_true", default=False,
702 help="Spew out even more information than normal")
703 parser.add_option("-p", "--package", default=None,
704 help="Build only the specified package")
705 parser.add_option("-c", "--vercode", default=None,
706 help="Build only the specified version code")
707 parser.add_option("-l", "--latest", action="store_true", default=False,
708 help="Build only the latest version code available")
709 parser.add_option("-s", "--stop", action="store_true", default=False,
710 help="Make the build stop on exceptions")
711 parser.add_option("-t", "--test", action="store_true", default=False,
712 help="Test mode - put output in the tmp directory only, and always build, even if the output already exists.")
713 parser.add_option("--server", action="store_true", default=False,
714 help="Use build server")
715 parser.add_option("--resetserver", action="store_true", default=False,
716 help="Reset and create a brand new build server, even if the existing one appears to be ok.")
717 parser.add_option("--on-server", dest="onserver", action="store_true", default=False,
718 help="Specify that we're running on the build server")
719 parser.add_option("-f", "--force", action="store_true", default=False,
720 help="Force build of disabled apps, and carries on regardless of scan problems. Only allowed in test mode.")
721 parser.add_option("--install", action="store_true", default=False,
722 help="Use 'ant debug install' to build and install a " +
723 "debug version on your device or emulator. " +
724 "Implies --force and --test")
725 parser.add_option("--all", action="store_true", default=False,
726 help="Use with --install, when not using --package"
727 " to confirm you really want to build and install everything.")
728 parser.add_option("-w", "--wiki", default=False, action="store_true",
729 help="Update the wiki")
730 options, args = parser.parse_args()
732 # Force --stop with --on-server to get cotrect exit code
736 # The --install option implies --test and --force...
739 print "Can't install when building on a build server."
741 if not options.package and not options.all:
742 print "This would build and install everything in the repo to the device."
743 print "You probably want to use --package and maybe also --vercode."
744 print "If you really want to install everything, use --all."
749 if options.force and not options.test:
750 print "Force is only allowed in test mode"
761 # Read configuration...
762 globals()['build_server_always'] = False
763 globals()['mvn3'] = "mvn3"
764 globals()['archive_older'] = 0
765 execfile('config.py', globals())
767 options, args = parse_commandline()
768 if build_server_always:
769 options.server = True
770 if options.resetserver and not options.server:
771 print "Using --resetserver without --server makes no sense"
775 apps = common.read_metadata(options.verbose, xref=not options.onserver)
778 if not os.path.isdir(log_dir):
779 print "Creating log directory"
783 if not os.path.isdir(tmp_dir):
784 print "Creating temporary directory"
790 output_dir = 'unsigned'
791 if not os.path.isdir(output_dir):
792 print "Creating output directory"
793 os.makedirs(output_dir)
795 if archive_older != 0:
796 also_check_dir = 'archive'
798 also_check_dir = None
803 if not os.path.isdir(build_dir):
804 print "Creating build directory"
805 os.makedirs(build_dir)
806 srclib_dir = os.path.join(build_dir, 'srclib')
807 extlib_dir = os.path.join(build_dir, 'extlib')
809 # Filter apps and build versions according to command-line options, etc...
811 apps = [app for app in apps if app['id'] == options.package]
813 print "No such package"
815 apps = [app for app in apps if (options.force or not app['Disabled']) and
816 app['builds'] and len(app['Repo Type']) > 0 and len(app['builds']) > 0]
818 print "Nothing to do - all apps are disabled or have no builds defined."
822 app['builds'] = [b for b in app['builds']
823 if str(b['vercode']) == options.vercode]
826 m = max([i['vercode'] for i in app['builds']], key=int)
827 app['builds'] = [b for b in app['builds'] if b['vercode'] == m]
831 site = mwclient.Site((wiki_protocol, wiki_server), path=wiki_path)
832 site.login(wiki_user, wiki_password)
834 # Build applications...
841 for thisbuild in app['builds']:
845 # For the first build of a particular app, we need to set up
846 # the source repo. We can reuse it on subsequent builds, if
849 if app['Repo Type'] == 'srclib':
850 build_dir = os.path.join('build', 'srclib', app['Repo'])
852 build_dir = os.path.join('build', app['id'])
854 # Set up vcs interface and make sure we have the latest code...
856 print "Getting {0} vcs interface for {1}".format(
857 app['Repo Type'], app['Repo'])
858 vcs = common.getvcs(app['Repo Type'], app['Repo'], build_dir, sdk_path)
863 print "Checking " + thisbuild['version']
864 if trybuild(app, thisbuild, build_dir, output_dir, also_check_dir,
865 srclib_dir, extlib_dir, tmp_dir, repo_dir, vcs, options.test,
866 options.server, options.install, options.force,
867 options.verbose, options.onserver):
868 build_succeeded.append(app)
869 wikilog = "Build succeeded"
870 except BuildException as be:
871 logfile = open(os.path.join(log_dir, app['id'] + '.log'), 'a+')
872 logfile.write(str(be))
874 print "Could not build app %s due to BuildException: %s" % (app['id'], be)
877 failed_apps[app['id']] = be
878 wikilog = be.get_wikitext()
879 except VCSException as vcse:
880 print "VCS error while building app %s: %s" % (app['id'], vcse)
883 failed_apps[app['id']] = vcse
885 except Exception as e:
886 print "Could not build app %s due to unknown error: %s" % (app['id'], traceback.format_exc())
889 failed_apps[app['id']] = e
892 if options.wiki and wikilog:
894 newpage = site.Pages[app['id'] + '/lastbuild']
898 txt = "Build completed at " + time.strftime("%Y-%m-%d %H:%M:%SZ", time.gmtime()) + "\n\n" + txt
899 newpage.save(wikilog, summary='Build log')
901 print "Error while attempting to publish build log"
903 for app in build_succeeded:
904 print "success: %s" % (app['id'])
906 if not options.verbose:
907 for fa in failed_apps:
908 print "Build for app %s failed:\n%s" % (fa, failed_apps[fa])
911 if len(build_succeeded) > 0:
912 print str(len(build_succeeded)) + ' builds succeeded'
913 if len(failed_apps) > 0:
914 print str(len(failed_apps)) + ' builds failed'
918 if __name__ == "__main__":