1 # -*- coding: utf-8 -*-
3 # common.py - part of the FDroid server tools
4 # Copyright (C) 2010-12, Ciaran Gultnieks, ciaran@ciarang.com
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Affero General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU Affero General Public License for more details.
16 # You should have received a copy of the GNU Affero General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 import glob, os, sys, re
25 def getvcs(vcstype, remote, local):
27 return vcs_git(remote, local)
28 elif vcstype == 'svn':
29 return vcs_svn(remote, local)
30 elif vcstype == 'git-svn':
31 return vcs_gitsvn(remote, local)
33 return vcs_hg(remote, local)
34 elif vcstype == 'bzr':
35 return vcs_bzr(remote, local)
36 elif vcstype == 'srclib':
37 return vcs_srclib(remote, local)
38 raise VCSException("Invalid vcs type " + vcstype)
41 def __init__(self, remote, local):
43 # It's possible to sneak a username and password in with
44 # the remote address... (this really only applies to svn
45 # and we should probably be more specific!)
46 index = remote.find('@')
48 self.username = remote[:index]
49 remote = remote[index+1:]
50 index = self.username.find(':')
52 raise VCSException("Password required with username")
53 self.password = self.username[index+1:]
54 self.username = self.username[:index]
60 self.refreshed = False
63 # Take the local repository to a clean version of the given revision, which
64 # is specificed in the VCS's native format. Beforehand, the repository can
65 # be dirty, or even non-existent. If the repository does already exist
66 # locally, it will be updated from the origin, but only once in the
67 # lifetime of the vcs object.
68 # None is acceptable for 'rev' if you know you are cloning a clean copy of
69 # the repo - otherwise it must specify a valid revision.
70 def gotorevision(self, rev):
71 raise VCSException("This VCS type doesn't define gotorevision")
73 # Initialise and update submodules
74 def initsubmodules(self):
75 raise VCSException('Submodules not supported for this vcs type')
77 # Returns the srclib (name, path) used in setting up the current
84 # If the local directory exists, but is somehow not a git repository, git
85 # will traverse up the directory tree until it finds one that is (i.e.
86 # fdroidserver) and then we'll proceed to destory it! This is called as
89 p = subprocess.Popen(['git', 'rev-parse', '--show-toplevel'],
90 stdout=subprocess.PIPE, cwd=self.local)
91 result = p.communicate()[0].rstrip()
92 if not result.endswith(self.local):
93 raise VCSException('Repository mismatch')
95 def gotorevision(self, rev):
96 if not os.path.exists(self.local):
97 # Brand new checkout...
98 if subprocess.call(['git', 'clone', self.remote, self.local]) != 0:
99 raise VCSException("Git clone failed")
103 # Discard any working tree changes...
104 if subprocess.call(['git', 'reset', '--hard'], cwd=self.local) != 0:
105 raise VCSException("Git reset failed")
106 # Remove untracked files now, in case they're tracked in the target
107 # revision (it happens!)...
108 if subprocess.call(['git', 'clean', '-dffx'], cwd=self.local) != 0:
109 raise VCSException("Git clean failed")
110 if not self.refreshed:
111 # Get latest commits and tags from remote...
112 if subprocess.call(['git', 'fetch', 'origin'],
113 cwd=self.local) != 0:
114 raise VCSException("Git fetch failed")
115 if subprocess.call(['git', 'fetch', '--tags', 'origin'],
116 cwd=self.local) != 0:
117 raise VCSException("Git fetch failed")
118 self.refreshed = True
119 # Check out the appropriate revision...
121 if subprocess.call(['git', 'checkout', rev], cwd=self.local) != 0:
122 raise VCSException("Git checkout failed")
123 # Get rid of any uncontrolled files left behind...
124 if subprocess.call(['git', 'clean', '-dffx'], cwd=self.local) != 0:
125 raise VCSException("Git clean failed")
127 def initsubmodules(self):
129 if subprocess.call(['git', 'submodule', 'init'],
130 cwd=self.local) != 0:
131 raise VCSException("Git submodule init failed")
132 if subprocess.call(['git', 'submodule', 'update'],
133 cwd=self.local) != 0:
134 raise VCSException("Git submodule update failed")
137 class vcs_gitsvn(vcs):
139 # If the local directory exists, but is somehow not a git repository, git
140 # will traverse up the directory tree until it finds one that is (i.e.
141 # fdroidserver) and then we'll proceed to destory it! This is called as
144 p = subprocess.Popen(['git', 'rev-parse', '--show-toplevel'],
145 stdout=subprocess.PIPE, cwd=self.local)
146 result = p.communicate()[0].rstrip()
147 if not result.endswith(self.local):
148 raise VCSException('Repository mismatch')
150 def gotorevision(self, rev):
151 if not os.path.exists(self.local):
152 # Brand new checkout...
153 if subprocess.call(['git', 'svn', 'clone', self.remote, self.local]) != 0:
154 raise VCSException("Git clone failed")
158 # Discard any working tree changes...
159 if subprocess.call(['git', 'reset', '--hard'], cwd=self.local) != 0:
160 raise VCSException("Git reset failed")
161 # Remove untracked files now, in case they're tracked in the target
162 # revision (it happens!)...
163 if subprocess.call(['git', 'clean', '-dffx'], cwd=self.local) != 0:
164 raise VCSException("Git clean failed")
165 if not self.refreshed:
166 # Get new commits and tags from repo...
167 if subprocess.call(['git', 'svn', 'rebase'],
168 cwd=self.local) != 0:
169 raise VCSException("Git svn rebase failed")
170 self.refreshed = True
172 # Figure out the git commit id corresponding to the svn revision...
173 p = subprocess.Popen(['git', 'svn', 'find-rev', 'r' + rev],
174 cwd=self.local, stdout=subprocess.PIPE)
175 rev = p.communicate()[0].rstrip()
176 if p.returncode != 0:
177 raise VCSException("Failed to get git treeish from svn rev")
178 # Check out the appropriate revision...
179 if subprocess.call(['git', 'checkout', rev], cwd=self.local) != 0:
180 raise VCSException("Git checkout failed")
181 # Get rid of any uncontrolled files left behind...
182 if subprocess.call(['git', 'clean', '-dffx'], cwd=self.local) != 0:
183 raise VCSException("Git clean failed")
188 if self.username is None:
189 return ['--non-interactive']
190 return ['--username', self.username,
191 '--password', self.password,
194 def gotorevision(self, rev):
195 if not os.path.exists(self.local):
196 if subprocess.call(['svn', 'checkout', self.remote, self.local] +
197 self.userargs()) != 0:
198 raise VCSException("Svn checkout failed")
202 r"svn status | awk '/\?/ {print $2}' | xargs rm -rf"):
203 if subprocess.call(svncommand, cwd=self.local,
205 raise VCSException("Svn reset failed")
206 if not self.refreshed:
207 if subprocess.call(['svn', 'update'] +
208 self.userargs(), cwd=self.local) != 0:
209 raise VCSException("Svn update failed")
210 self.refreshed = True
212 revargs = ['-r', rev]
213 if subprocess.call(['svn', 'update', '--force'] + revargs +
214 self.userargs(), cwd=self.local) != 0:
215 raise VCSException("Svn update failed")
220 def gotorevision(self, rev):
221 if not os.path.exists(self.local):
222 if subprocess.call(['hg', 'clone', self.remote, self.local]) !=0:
223 raise VCSException("Hg clone failed")
225 if subprocess.call('hg status -u | xargs rm -rf',
226 cwd=self.local, shell=True) != 0:
227 raise VCSException("Hg clean failed")
228 if not self.refreshed:
229 if subprocess.call(['hg', 'pull'],
230 cwd=self.local) != 0:
231 raise VCSException("Hg pull failed")
232 self.refreshed = True
235 if subprocess.call(['hg', 'checkout', '-C'] + revargs,
236 cwd=self.local) != 0:
237 raise VCSException("Hg checkout failed")
242 def gotorevision(self, rev):
243 if not os.path.exists(self.local):
244 if subprocess.call(['bzr', 'branch', self.remote, self.local]) != 0:
245 raise VCSException("Bzr branch failed")
247 if subprocess.call(['bzr', 'clean-tree', '--force',
248 '--unknown', '--ignored'], cwd=self.local) != 0:
249 raise VCSException("Bzr revert failed")
250 if not self.refreshed:
251 if subprocess.call(['bzr', 'pull'],
252 cwd=self.local) != 0:
253 raise VCSException("Bzr update failed")
254 self.refreshed = True
256 revargs = ['-r', rev]
257 if subprocess.call(['bzr', 'revert'] + revargs,
258 cwd=self.local) != 0:
259 raise VCSException("Bzr revert failed")
261 class vcs_srclib(vcs):
263 def gotorevision(self, rev):
266 extlib_dir = 'build/extlib'
268 if os.path.exists(self.local):
269 shutil.rmtree(self.local)
271 if self.remote.find(':') != -1:
272 srclib, path = self.remote.split(':')
276 libdir = getsrclib(srclib + '@' + rev, extlib_dir)
277 self.srclib = (srclib, libdir)
279 libdir = os.path.join(libdir, path)
280 shutil.copytree(libdir, self.local)
284 # Get the type expected for a given metadata field.
285 def metafieldtype(name):
286 if name == 'Description':
288 if name == 'Requires Root':
290 if name == 'Build Version':
292 if name == 'Use Built':
297 # Parse metadata for a single application.
299 # 'metafile' - the filename to read. The package id for the application comes
300 # from this filename. Pass None to get a blank entry.
302 # Returns a dictionary containing all the details of the application. There are
303 # two major kinds of information in the dictionary. Keys beginning with capital
304 # letters correspond directory to identically named keys in the metadata file.
305 # Keys beginning with lower case letters are generated in one way or another,
306 # and are not found verbatim in the metadata.
308 # Known keys not originating from the metadata are:
310 # 'id' - the application's package ID
311 # 'builds' - a list of dictionaries containing build information
312 # for each defined build
313 # 'comments' - a list of comments from the metadata file. Each is
314 # a tuple of the form (field, comment) where field is
315 # the name of the field it preceded in the metadata
316 # file. Where field is None, the comment goes at the
317 # end of the file. Alternatively, 'build:version' is
318 # for a comment before a particular build version.
319 # 'descriptionlines' - original lines of description as formatted in the
322 def parse_metadata(metafile, **kw):
324 def parse_buildline(lines):
325 value = "".join(lines)
326 parts = [p.replace("\\,", ",")
327 for p in re.split(r"(?<!\\),", value)]
329 raise MetaDataException("Invalid build format: " + value + " in " + metafile.name)
331 thisbuild['origlines'] = lines
332 thisbuild['version'] = parts[0]
333 thisbuild['vercode'] = parts[1]
334 thisbuild['commit'] = parts[2]
336 pk, pv = p.split('=', 1)
340 def add_comments(key):
341 for comment in curcomments:
342 thisinfo['comments'].append((key, comment))
347 if not isinstance(metafile, file):
348 metafile = open(metafile, "r")
349 thisinfo['id'] = metafile.name[9:-4]
350 if kw.get("verbose", False):
351 print "Reading metadata for " + thisinfo['id']
353 thisinfo['id'] = None
355 # Defaults for fields that come from metadata...
356 thisinfo['Name'] = None
357 thisinfo['Category'] = 'None'
358 thisinfo['Description'] = []
359 thisinfo['Summary'] = ''
360 thisinfo['License'] = 'Unknown'
361 thisinfo['Web Site'] = ''
362 thisinfo['Source Code'] = ''
363 thisinfo['Issue Tracker'] = ''
364 thisinfo['Donate'] = None
365 thisinfo['Disabled'] = None
366 thisinfo['AntiFeatures'] = None
367 thisinfo['Update Check Mode'] = 'Market'
368 thisinfo['Current Version'] = ''
369 thisinfo['Current Version Code'] = '0'
370 thisinfo['Repo Type'] = ''
371 thisinfo['Repo'] = ''
372 thisinfo['Requires Root'] = False
374 # General defaults...
375 thisinfo['builds'] = []
376 thisinfo['comments'] = []
385 for line in metafile:
386 line = line.rstrip('\r\n')
390 if line.startswith("#"):
391 curcomments.append(line)
393 index = line.find(':')
395 raise MetaDataException("Invalid metadata in " + metafile.name + " at: " + line)
397 value = line[index+1:]
399 # Translate obsolete fields...
400 if field == 'Market Version':
401 field = 'Current Version'
402 if field == 'Market Version Code':
403 field = 'Current Version Code'
405 fieldtype = metafieldtype(field)
406 if fieldtype != 'build':
408 if fieldtype == 'multiline':
412 raise MetaDataException("Unexpected text on same line as " + field + " in " + metafile.name)
413 elif fieldtype == 'string':
414 thisinfo[field] = value
415 elif fieldtype == 'flag':
417 thisinfo[field] = True
419 thisinfo[field] = False
421 raise MetaDataException("Expected Yes or No for " + field + " in " + metafile.name)
422 elif fieldtype == 'build':
423 if value.endswith("\\"):
425 buildlines = [value[:-1]]
427 thisinfo['builds'].append(parse_buildline([value]))
428 add_comments('build:' + thisinfo['builds'][-1]['version'])
429 elif fieldtype == 'obsolete':
430 pass # Just throw it away!
432 raise MetaDataException("Unrecognised field type for " + field + " in " + metafile.name)
433 elif mode == 1: # Multiline field
437 thisinfo[field].append(line)
438 elif mode == 2: # Line continuation mode in Build Version
439 if line.endswith("\\"):
440 buildlines.append(line[:-1])
442 buildlines.append(line)
443 thisinfo['builds'].append(
444 parse_buildline(buildlines))
445 add_comments('build:' + thisinfo['builds'][-1]['version'])
449 # Mode at end of file should always be 0...
451 raise MetaDataException(field + " not terminated in " + metafile.name)
453 raise MetaDataException("Unterminated continuation in " + metafile.name)
455 if len(thisinfo['Description']) == 0:
456 thisinfo['Description'].append('No description available')
458 # Ensure all AntiFeatures are recognised...
459 if thisinfo['AntiFeatures']:
460 parts = thisinfo['AntiFeatures'].split(",")
462 if (part != "Ads" and
463 part != "Tracking" and
464 part != "NonFreeNet" and
465 part != "NonFreeDep" and
466 part != "NonFreeAdd"):
467 raise MetaDataException("Unrecognised antifeature '" + part + "' in " \
472 # Write a metadata file.
474 # 'dest' - The path to the output file
475 # 'app' - The app data
476 def write_metadata(dest, app):
478 def writecomments(key):
479 for pf, comment in app['comments']:
481 mf.write(comment + '\n')
483 def writefield(field, value=None):
487 mf.write(field + ':' + value + '\n')
491 writefield('Disabled')
492 if app['AntiFeatures']:
493 writefield('AntiFeatures')
494 writefield('Category')
495 writefield('License')
496 writefield('Web Site')
497 writefield('Source Code')
498 writefield('Issue Tracker')
504 writefield('Summary')
505 writefield('Description', '')
506 for line in app['Description']:
507 mf.write(line + '\n')
510 if app['Requires Root']:
511 writefield('Requires Root', 'Yes')
513 if len(app['Repo Type']) > 0:
514 writefield('Repo Type')
517 for build in app['builds']:
518 writecomments('build:' + build['version'])
519 mf.write('Build Version:')
520 if build.has_key('origlines'):
521 # Keeping the original formatting if we loaded it from a file...
522 mf.write('\\\n'.join(build['origlines']) + '\n')
524 mf.write(build['version'] + ',' + build['vercode'] + ',' +
526 for key,value in build.iteritems():
527 if key not in ['version', 'vercode', 'commit']:
528 mf.write(',' + key + '=' + value)
530 if len(app['builds']) > 0:
532 writefield('Update Check Mode')
533 if len(app['Current Version']) > 0:
534 writefield('Current Version')
535 writefield('Current Version Code')
541 # Read all metadata. Returns a list of 'app' objects (which are dictionaries as
542 # returned by the parse_metadata function.
543 def read_metadata(verbose=False):
545 for metafile in sorted(glob.glob(os.path.join('metadata', '*.txt'))):
547 print "Reading " + metafile
548 apps.append(parse_metadata(metafile, verbose=verbose))
552 # Parse multiple lines of description as written in a metadata file, returning
554 def parse_description(lines):
560 if not text.endswith('\n') and len(text) > 0:
566 # Extract some information from the AndroidManifest.xml at the given path.
567 # Returns (version, vercode, package), any or all of which might be None.
568 def parse_androidmanifest(manifest):
570 vcsearch = re.compile(r'.*android:versionCode="([^"]+)".*').search
571 vnsearch = re.compile(r'.*android:versionName="([^"]+)".*').search
572 psearch = re.compile(r'.*package="([^"]+)".*').search
576 for line in file(manifest):
578 matches = psearch(line)
580 package = matches.group(1)
582 matches = vnsearch(line)
584 version = matches.group(1)
586 matches = vcsearch(line)
588 vercode = matches.group(1)
589 return (version, vercode, package)
592 class BuildException(Exception):
593 def __init__(self, value, stdout = None, stderr = None):
599 ret = repr(self.value)
601 ret = ret + "\n==== stdout begin ====\n" + str(self.stdout) + "\n==== stdout end ===="
603 ret = ret + "\n==== stderr begin ====\n" + str(self.stderr) + "\n==== stderr end ===="
606 class VCSException(Exception):
607 def __init__(self, value):
611 return repr(self.value)
613 class MetaDataException(Exception):
614 def __init__(self, value):
618 return repr(self.value)
621 # Get the specified source library.
622 # Returns the path to it.
623 # TODO: These are currently just hard-coded in this method. It will be a
624 # metadata-driven system eventually, but not yet.
625 def getsrclib(spec, extlib_dir):
626 name, ref = spec.split('@')
628 if name == 'GreenDroid':
629 sdir = os.path.join(extlib_dir, 'GreenDroid')
631 'https://github.com/cyrilmottier/GreenDroid.git', sdir)
632 vcs.gotorevision(ref)
633 return os.path.join(sdir, 'GreenDroid')
635 if name == 'ActionBarSherlock':
636 sdir = os.path.join(extlib_dir, 'ActionBarSherlock')
638 'https://github.com/JakeWharton/ActionBarSherlock.git', sdir)
639 vcs.gotorevision(ref)
640 libdir = os.path.join(sdir, 'library')
641 if subprocess.call(['android', 'update', 'project', '-p',
643 raise BuildException('Error updating ActionBarSherlock project')
646 if name == 'FacebookSDK':
647 sdir = os.path.join(extlib_dir, 'FacebookSDK')
649 'git://github.com/facebook/facebook-android-sdk.git', sdir)
650 vcs.gotorevision(ref)
651 libdir = os.path.join(sdir, 'facebook')
652 if subprocess.call(['android', 'update', 'project', '-p',
654 raise BuildException('Error updating FacebookSDK project')
658 sdir = os.path.join(extlib_dir, 'OI')
659 vcs = getvcs('git-svn',
660 'http://openintents.googlecode.com/svn/trunk/', sdir)
661 vcs.gotorevision(ref)
664 if name == 'JOpenDocument':
665 sdir = os.path.join(extlib_dir, 'JOpenDocument')
667 'https://github.com/andiwand/JOpenDocument.git', sdir)
668 vcs.gotorevision(ref)
669 shutil.rmtree(os.path.join(sdir, 'bin'))
672 raise BuildException('Unknown srclib ' + name)
675 # Prepare the source code for a particular build
676 # 'vcs' - the appropriate vcs object for the application
677 # 'app' - the application details from the metadata
678 # 'build' - the build details from the metadata
679 # 'build_dir' - the path to the build directory, usually
681 # 'extlib_dir' - the path to the external libraries directory, usually
683 # 'sdk_path' - the path to the Android SDK
684 # 'ndk_path' - the path to the Android NDK
685 # 'javacc_path' - the path to javacc
686 # Returns the root directory, which may be the same as 'build_dir' or may
687 # be a subdirectory of it.
688 def prepare_source(vcs, app, build, build_dir, extlib_dir, sdk_path, ndk_path, javacc_path):
690 # Optionally, the actual app source can be in a subdirectory...
691 if build.has_key('subdir'):
692 root_dir = os.path.join(build_dir, build['subdir'])
696 # Get a working copy of the right revision...
697 print "Getting source for revision " + build['commit']
698 vcs.gotorevision(build['commit'])
700 # Check that a subdir (if we're using one) exists. This has to happen
701 # after the checkout, since it might not exist elsewhere...
702 if not os.path.exists(root_dir):
703 raise BuildException('Missing subdir ' + root_dir)
705 # Initialise submodules if requred...
706 if build.get('submodules', 'no') == 'yes':
709 # Run an init command if one is required...
710 if build.has_key('init'):
712 if subprocess.call(init, cwd=root_dir, shell=True) != 0:
713 raise BuildException("Error running init command")
715 # Generate (or update) the ant build file, build.xml...
716 if (build.get('update', '.') != 'no' and
717 not build.has_key('maven')):
718 parms = [os.path.join(sdk_path, 'tools', 'android'),
719 'update', 'project', '-p', '.']
720 parms.append('--subprojects')
721 if build.has_key('target'):
723 parms.append(build['target'])
724 update_dirs = build.get('update', '.').split(';')
725 # Force build.xml update if necessary...
726 if build.get('update', '.') == 'force' or build.has_key('target'):
728 buildxml = os.path.join(root_dir, 'build.xml')
729 if os.path.exists(buildxml):
730 print 'Force-removing old build.xml'
732 for d in update_dirs:
733 if subprocess.call(parms, cwd=root_dir + '/' + d) != 0:
734 raise BuildException("Failed to update project")
736 # If the app has ant set up to sign the release, we need to switch
737 # that off, because we want the unsigned apk...
738 for propfile in ('build.properties', 'default.properties', 'ant.properties'):
739 if os.path.exists(os.path.join(root_dir, propfile)):
740 if subprocess.call(['sed','-i','s/^key.store/#/',
741 propfile], cwd=root_dir) !=0:
742 raise BuildException("Failed to amend %s" % propfile)
744 # Update the local.properties file...
745 locprops = os.path.join(root_dir, 'local.properties')
746 if os.path.exists(locprops):
747 f = open(locprops, 'r')
750 # Fix old-fashioned 'sdk-location' by copying
751 # from sdk.dir, if necessary...
752 if build.get('oldsdkloc', 'no') == "yes":
753 sdkloc = re.match(r".*^sdk.dir=(\S+)$.*", props,
755 props += "\nsdk-location=" + sdkloc + "\n"
756 # Add ndk location...
757 props+= "\nndk.dir=" + ndk_path + "\n"
758 # Add java.encoding if necessary...
759 if build.has_key('encoding'):
760 props += "\njava.encoding=" + build['encoding'] + "\n"
761 f = open(locprops, 'w')
765 # Insert version code and number into the manifest if necessary...
766 if build.has_key('forceversion'):
767 if subprocess.call(['sed','-r','-i',
768 's/android:versionName="[^"]+"/android:versionName="' + build['version'] + '"/g',
769 'AndroidManifest.xml'], cwd=root_dir) !=0:
770 raise BuildException("Failed to amend manifest")
771 if build.has_key('forcevercode'):
772 if subprocess.call(['sed','-r','-i',
773 's/android:versionCode="[^"]+"/android:versionCode="' + build['vercode'] + '"/g',
774 'AndroidManifest.xml'], cwd=root_dir) !=0:
775 raise BuildException("Failed to amend manifest")
777 # Delete unwanted file...
778 if build.has_key('rm'):
779 dest = os.path.join(build_dir, build['rm'])
780 if os.path.exists(dest):
783 # Fix apostrophes translation files if necessary...
784 if build.get('fixapos', 'no') == 'yes':
785 for root, dirs, files in os.walk(os.path.join(root_dir, 'res')):
786 for filename in files:
787 if filename.endswith('.xml'):
788 if subprocess.call(['sed','-i','s@' +
789 r"\([^\\]\)'@\1\\'" +
791 os.path.join(root, filename)]) != 0:
792 raise BuildException("Failed to amend " + filename)
794 # Fix translation files if necessary...
795 if build.get('fixtrans', 'no') == 'yes':
796 for root, dirs, files in os.walk(os.path.join(root_dir, 'res')):
797 for filename in files:
798 if filename.endswith('.xml'):
799 f = open(os.path.join(root, filename))
807 index = line.find("%", index)
810 next = line[index+1:index+2]
811 if next == "s" or next == "d":
812 line = (line[:index+1] +
819 # We only want to insert the positional arguments
820 # when there is more than one argument...
826 outlines.append(line)
829 f = open(os.path.join(root, filename), 'w')
830 f.writelines(outlines)
833 # Add required external libraries...
834 if build.has_key('extlibs'):
835 libsdir = os.path.join(root_dir, 'libs')
836 if not os.path.exists(libsdir):
838 for lib in build['extlibs'].split(';'):
839 libf = os.path.basename(lib)
840 shutil.copyfile(os.path.join(extlib_dir, lib),
841 os.path.join(libsdir, libf))
843 # Get required source libraries...
845 if build.has_key('srclibs'):
846 for lib in build['srclibs'].split(';'):
847 name, _ = lib.split('@')
848 srclibpaths.append((name, getsrclib(lib, extlib_dir)))
849 basesrclib = vcs.getsrclib()
850 # If one was used for the main source, add that too.
852 srclibpaths.append(basesrclib)
854 # There should never be gen or bin directories in the source, so just get
856 for baddir in ['gen', 'bin']:
857 badpath = os.path.join(root_dir, baddir)
858 if os.path.exists(badpath):
859 shutil.rmtree(badpath)
861 # Apply patches if any
863 for patch in build['patch'].split(';'):
864 print "Applying " + patch
865 patch_path = os.path.join('metadata', app['id'], patch)
866 if subprocess.call(['patch', '-p1',
867 '-i', os.path.abspath(patch_path)], cwd=build_dir) != 0:
868 raise BuildException("Failed to apply patch %s" % patch_path)
870 # Run a pre-build command if one is required...
871 if build.has_key('prebuild'):
872 prebuild = build['prebuild']
873 # Substitute source library paths into prebuild commands...
874 for name, libpath in srclibpaths:
875 libpath = os.path.relpath(libpath, root_dir)
876 prebuild = prebuild.replace('$$' + name + '$$', libpath)
877 if subprocess.call(prebuild, cwd=root_dir, shell=True) != 0:
878 raise BuildException("Error running pre-build command")
880 # Special case init functions for funambol...
881 if build.get('initfun', 'no') == "yes":
883 if subprocess.call(['sed','-i','s@' +
884 '<taskdef resource="net/sf/antcontrib/antcontrib.properties" />' +
886 '<taskdef resource="net/sf/antcontrib/antcontrib.properties">' +
888 '<pathelement location="/usr/share/java/ant-contrib.jar"/>' +
892 'build.xml'], cwd=root_dir) !=0:
893 raise BuildException("Failed to amend build.xml")
895 if subprocess.call(['sed','-i','s@' +
896 '\${user.home}/funambol/build/android/build.properties' +
900 'build.xml'], cwd=root_dir) !=0:
901 raise BuildException("Failed to amend build.xml")
903 buildxml = os.path.join(root_dir, 'build.xml')
904 f = open(buildxml, 'r')
909 for line in xml.splitlines():
911 if line.find("jarsigner") != -1:
914 xmlout += line + "\n"
916 if line.find("/exec") != -1:
920 f = open(buildxml, 'w')
924 if subprocess.call(['sed','-i','s@' +
925 'platforms/android-2.0' +
927 'platforms/android-8' +
929 'build.xml'], cwd=root_dir) !=0:
930 raise BuildException("Failed to amend build.xml")
933 os.path.join(root_dir, "build.properties.example"),
934 os.path.join(root_dir, "build.properties"))
936 if subprocess.call(['sed','-i','s@' +
939 'javacchome=' + javacc_path +
941 'build.properties'], cwd=root_dir) !=0:
942 raise BuildException("Failed to amend build.properties")
944 if subprocess.call(['sed','-i','s@' +
947 'sdk-folder=' + sdk_path +
949 'build.properties'], cwd=root_dir) !=0:
950 raise BuildException("Failed to amend build.properties")
952 if subprocess.call(['sed','-i','s@' +
953 'android.sdk.version.*'+
955 'android.sdk.version=2.0' +
957 'build.properties'], cwd=root_dir) !=0:
958 raise BuildException("Failed to amend build.properties")
963 # Scan the source code in the given directory (and all subdirectories)
964 # and return a list of potential problems.
965 def scan_source(build_dir, root_dir, thisbuild):
969 # Common known non-free blobs:
970 usual_suspects = ['flurryagent',
972 'libgoogleanalytics',
978 # Iterate through all files in the source code...
979 for r,d,f in os.walk(build_dir):
982 if r.find('/.hg/') == -1:
984 # Path (relative) to the file...
985 fp = os.path.join(r, curfile)
987 for suspect in usual_suspects:
988 if curfile.lower().find(suspect) != -1:
989 msg = 'Found probable non-free blob ' + fp
992 if curfile.endswith('.java'):
993 for line in file(fp):
995 if line.find('DexClassLoader') != -1:
996 msg = 'Found DexClassLoader in ' + fp
999 # if line.lower().find('all rights reserved') != -1:
1000 # msg = 'All rights reserved in ' + fp
1001 # problems.append(msg)
1003 # Presence of a jni directory without buildjni=yes might
1004 # indicate a problem...
1005 if (os.path.exists(os.path.join(root_dir, 'jni')) and
1006 thisbuild.get('buildjni') is None):
1007 msg = 'Found jni directory, but buildjni is not enabled'
1008 problems.append(msg)
1016 self.path = os.path.join('stats', 'known_apks.txt')
1018 if os.path.exists(self.path):
1019 for line in file( self.path):
1020 t = line.rstrip().split(' ')
1022 self.apks[t[0]] = (t[1], None)
1024 self.apks[t[0]] = (t[1], time.strptime(t[2], '%Y-%m-%d'))
1025 self.changed = False
1027 def writeifchanged(self):
1029 if not os.path.exists('stats'):
1031 f = open(self.path, 'w')
1033 for apk, app in self.apks.iteritems():
1035 line = apk + ' ' + appid
1037 line += ' ' + time.strftime('%Y-%m-%d', added)
1039 for line in sorted(lst):
1040 f.write(line + '\n')
1043 def recordapk(self, apk, app):
1044 if not apk in self.apks:
1045 self.apks[apk] = (app, time.gmtime(time.time()))
1048 def getapp(self, apkname):
1049 if apkname in self.apks:
1050 return self.apks[apkname]
1053 def getlatest(self, num):
1055 for apk, app in self.apks.iteritems():
1059 if apps[appid] > added:
1063 sortedapps = sorted(apps.iteritems(), key=operator.itemgetter(1))[-num:]
1065 for app, added in sortedapps: