OLD | NEW |
1 # coding: utf-8 | 1 # coding: utf-8 |
2 | 2 |
3 # This file is part of the Adblock Plus web scripts, | 3 # This file is part of the Adblock Plus web scripts, |
4 # Copyright (C) 2006-2013 Eyeo GmbH | 4 # Copyright (C) 2006-2013 Eyeo GmbH |
5 # | 5 # |
6 # Adblock Plus is free software: you can redistribute it and/or modify | 6 # Adblock Plus is free software: you can redistribute it and/or modify |
7 # it under the terms of the GNU General Public License version 3 as | 7 # it under the terms of the GNU General Public License version 3 as |
8 # published by the Free Software Foundation. | 8 # published by the Free Software Foundation. |
9 # | 9 # |
10 # Adblock Plus is distributed in the hope that it will be useful, | 10 # Adblock Plus is distributed in the hope that it will be useful, |
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of | 11 # but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 # GNU General Public License for more details. | 13 # GNU General Public License for more details. |
14 # | 14 # |
15 # You should have received a copy of the GNU General Public License | 15 # You should have received a copy of the GNU General Public License |
16 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. | 16 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. |
17 | 17 |
18 """ | 18 """ |
19 | 19 |
20 Nightly builds generation script | 20 Nightly builds generation script |
21 ================================ | 21 ================================ |
22 | 22 |
23 This script generates nightly builds of extensions, together | 23 This script generates nightly builds of extensions, together |
24 with changelogs and documentation. | 24 with changelogs and documentation. |
25 | 25 |
26 """ | 26 """ |
27 | 27 |
28 import sys, os, os.path, codecs, subprocess, ConfigParser, traceback, json, hash
lib | 28 import sys, os, os.path, codecs, subprocess, ConfigParser, traceback, json, hash
lib |
29 import tempfile, re, shutil, urlparse, pipes | 29 import tempfile, re, shutil, urlparse, pipes, time, base64, urllib2 |
30 from datetime import datetime | 30 from datetime import datetime |
| 31 from urllib import urlencode |
31 from xml.dom.minidom import parse as parseXml | 32 from xml.dom.minidom import parse as parseXml |
32 from sitescripts.utils import get_config, setupStderr, get_template | 33 from sitescripts.utils import get_config, setupStderr, get_template |
33 from sitescripts.extensions.utils import compareVersions, Configuration | 34 from sitescripts.extensions.utils import compareVersions, Configuration |
34 | 35 |
35 MAX_BUILDS = 50 | 36 MAX_BUILDS = 50 |
36 | 37 |
37 | 38 |
38 class NightlyBuild(object): | 39 class NightlyBuild(object): |
39 """ | 40 """ |
40 Performs the build process for an extension, | 41 Performs the build process for an extension, |
(...skipping 238 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
279 | 280 |
280 | 281 |
281 def build(self): | 282 def build(self): |
282 """ | 283 """ |
283 run the build command in the tempdir | 284 run the build command in the tempdir |
284 """ | 285 """ |
285 baseDir = os.path.join(self.config.nightliesDirectory, self.basename) | 286 baseDir = os.path.join(self.config.nightliesDirectory, self.basename) |
286 if not os.path.exists(baseDir): | 287 if not os.path.exists(baseDir): |
287 os.makedirs(baseDir) | 288 os.makedirs(baseDir) |
288 outputFile = "%s-%s%s" % (self.basename, self.version, self.config.packageSu
ffix) | 289 outputFile = "%s-%s%s" % (self.basename, self.version, self.config.packageSu
ffix) |
289 outputPath = os.path.join(baseDir, outputFile) | 290 self.path = os.path.join(baseDir, outputFile) |
290 self.updateURL = urlparse.urljoin(self.config.nightliesURL, self.basename +
'/' + outputFile + '?update') | 291 self.updateURL = urlparse.urljoin(self.config.nightliesURL, self.basename +
'/' + outputFile + '?update') |
291 | 292 |
292 if self.config.type == 'android': | 293 if self.config.type == 'android': |
293 apkFile = open(outputPath, 'wb') | 294 apkFile = open(self.path, 'wb') |
294 | 295 |
295 try: | 296 try: |
296 try: | 297 try: |
297 port = get_config().get('extensions', 'androidBuildPort') | 298 port = get_config().get('extensions', 'androidBuildPort') |
298 except ConfigParser.NoOptionError: | 299 except ConfigParser.NoOptionError: |
299 port = '22' | 300 port = '22' |
300 buildCommand = ['ssh', '-p', port, get_config().get('extensions', 'andro
idBuildHost')] | 301 buildCommand = ['ssh', '-p', port, get_config().get('extensions', 'andro
idBuildHost')] |
301 buildCommand.extend(map(pipes.quote, ['/home/android/bin/makedebugbuild.
py', '--revision', self.revision, '--version', self.version, '--stdout'])) | 302 buildCommand.extend(map(pipes.quote, ['/home/android/bin/makedebugbuild.
py', '--revision', self.revision, '--version', self.version, '--stdout'])) |
302 subprocess.check_call(buildCommand, stdout=apkFile, close_fds=True) | 303 subprocess.check_call(buildCommand, stdout=apkFile, close_fds=True) |
303 except: | 304 except: |
304 # clear broken output if any | 305 # clear broken output if any |
305 if os.path.exists(outputPath): | 306 if os.path.exists(self.path): |
306 os.remove(outputPath) | 307 os.remove(self.path) |
307 raise | 308 raise |
308 elif self.config.type == 'chrome' or self.config.type == 'opera': | 309 elif self.config.type == 'chrome' or self.config.type == 'opera': |
309 import buildtools.packagerChrome as packager | 310 import buildtools.packagerChrome as packager |
310 packager.createBuild(self.tempdir, type=self.config.type, outFile=outputPa
th, buildNum=self.revision, keyFile=self.config.keyFile, experimentalAPI=self.co
nfig.experimental) | 311 packager.createBuild(self.tempdir, type=self.config.type, outFile=self.pat
h, buildNum=self.revision, keyFile=self.config.keyFile, experimentalAPI=self.con
fig.experimental) |
311 elif self.config.type == 'safari': | 312 elif self.config.type == 'safari': |
312 import buildtools.packagerSafari as packager | 313 import buildtools.packagerSafari as packager |
313 packager.createBuild(self.tempdir, type=self.config.type, outFile=outputPa
th, buildNum=self.revision, keyFile=self.config.keyFile) | 314 packager.createBuild(self.tempdir, type=self.config.type, outFile=self.pat
h, buildNum=self.revision, keyFile=self.config.keyFile) |
314 else: | 315 else: |
315 import buildtools.packagerGecko as packager | 316 import buildtools.packagerGecko as packager |
316 packager.createBuild(self.tempdir, outFile=outputPath, buildNum=self.revis
ion, keyFile=self.config.keyFile) | 317 packager.createBuild(self.tempdir, outFile=self.path, buildNum=self.revisi
on, keyFile=self.config.keyFile) |
317 | 318 |
318 if not os.path.exists(outputPath): | 319 if not os.path.exists(self.path): |
319 raise Exception("Build failed, output file hasn't been created") | 320 raise Exception("Build failed, output file hasn't been created") |
320 | 321 |
321 linkPath = os.path.join(baseDir, '00latest%s' % self.config.packageSuffix) | 322 linkPath = os.path.join(baseDir, '00latest%s' % self.config.packageSuffix) |
322 if hasattr(os, 'symlink'): | 323 if hasattr(os, 'symlink'): |
323 if os.path.exists(linkPath): | 324 if os.path.exists(linkPath): |
324 os.remove(linkPath) | 325 os.remove(linkPath) |
325 os.symlink(os.path.basename(outputPath), linkPath) | 326 os.symlink(os.path.basename(self.path), linkPath) |
326 else: | 327 else: |
327 shutil.copyfile(outputPath, linkPath) | 328 shutil.copyfile(self.path, linkPath) |
328 | 329 |
329 def retireBuilds(self): | 330 def retireBuilds(self): |
330 """ | 331 """ |
331 removes outdated builds, returns the sorted version numbers of remaining | 332 removes outdated builds, returns the sorted version numbers of remaining |
332 builds | 333 builds |
333 """ | 334 """ |
334 baseDir = os.path.join(self.config.nightliesDirectory, self.basename) | 335 baseDir = os.path.join(self.config.nightliesDirectory, self.basename) |
335 versions = [] | 336 versions = [] |
336 prefix = self.basename + '-' | 337 prefix = self.basename + '-' |
337 suffix = self.config.packageSuffix | 338 suffix = self.config.packageSuffix |
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
385 command = ['hg', 'archive', '-R', get_config().get('extensions', 'jsdocRepos
itory'), '-r', 'default', docsdir] | 386 command = ['hg', 'archive', '-R', get_config().get('extensions', 'jsdocRepos
itory'), '-r', 'default', docsdir] |
386 subprocess.check_call(command) | 387 subprocess.check_call(command) |
387 | 388 |
388 try: | 389 try: |
389 import buildtools.build as build | 390 import buildtools.build as build |
390 outputPath = os.path.join(self.config.docsDirectory, self.basename) | 391 outputPath = os.path.join(self.config.docsDirectory, self.basename) |
391 build.generateDocs(self.tempdir, None, [("-t", docsdir), ("-q", "")], [out
putPath], self.config.type) | 392 build.generateDocs(self.tempdir, None, [("-t", docsdir), ("-q", "")], [out
putPath], self.config.type) |
392 finally: | 393 finally: |
393 shutil.rmtree(docsdir, ignore_errors=True) | 394 shutil.rmtree(docsdir, ignore_errors=True) |
394 | 395 |
| 396 def uploadToChromeWebStore(self): |
| 397 import M2Crypto |
| 398 |
| 399 # log in with the Google Developer Service Account |
| 400 # https://developers.google.com/accounts/docs/OAuth2ServiceAccount#creatingj
wt |
| 401 |
| 402 base64encode = lambda data: base64.urlsafe_b64encode(data).rstrip('=') |
| 403 now = int(time.mktime(time.gmtime())) |
| 404 |
| 405 jwt = '%s.%s' % ( |
| 406 base64encode(json.dumps({ |
| 407 'alg': 'RS256', |
| 408 'typ': 'JWT', |
| 409 })), |
| 410 base64encode(json.dumps({ |
| 411 'iss': self.config.serviceAccountEmail, |
| 412 'scope': 'https://www.googleapis.com/auth/chromewebstore', |
| 413 'aud': 'https://accounts.google.com/o/oauth2/token', |
| 414 'exp': now + 3600, # fails if less than about an hour |
| 415 'iat': now, |
| 416 })), |
| 417 ) |
| 418 |
| 419 jwt = '%s.%s' % (jwt, base64encode( |
| 420 M2Crypto.RSA.load_key(self.config.serviceAccountKey).sign( |
| 421 hashlib.sha256(jwt).digest(), 'sha256' |
| 422 ) |
| 423 )) |
| 424 |
| 425 response = json.load(urllib2.urlopen( |
| 426 'https://accounts.google.com/o/oauth2/token', |
| 427 |
| 428 urlencode([ |
| 429 ('grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer'), |
| 430 ('assertion', jwt), |
| 431 ]) |
| 432 )) |
| 433 |
| 434 # upload a new version with the Chrome Web Store API |
| 435 # https://developer.chrome.com/webstore/using_webstore_api#uploadexisitng |
| 436 |
| 437 request = urllib2.Request('https://www.googleapis.com/upload/chromewebstore/
v1.1/items/' + self.config.galleryID) |
| 438 request.get_method = lambda: 'PUT' |
| 439 request.add_header('Authorization', '%s %s' % (response['token_type'], respo
nse['access_token'])) |
| 440 request.add_header('x-goog-api-version', '2') |
| 441 |
| 442 with open(self.path, 'rb') as file: |
| 443 request.add_header('Content-Length', '%d' % os.fstat(file.fileno()).st_siz
e) |
| 444 request.add_data(file) |
| 445 |
| 446 response = json.load(urllib2.urlopen(request)) |
| 447 |
| 448 if response['uploadState'] == 'FAILURE': |
| 449 raise Exception(response['itemError']) |
| 450 |
395 def run(self): | 451 def run(self): |
396 """ | 452 """ |
397 Run the nightly build process for one extension | 453 Run the nightly build process for one extension |
398 """ | 454 """ |
399 try: | 455 try: |
400 if self.config.type == 'ie': | 456 if self.config.type == 'ie': |
401 # We cannot build IE builds, simply list the builds already in | 457 # We cannot build IE builds, simply list the builds already in |
402 # the directory. Basename has to be deduced from the repository name. | 458 # the directory. Basename has to be deduced from the repository name. |
403 self.basename = os.path.basename(self.config.repository) | 459 self.basename = os.path.basename(self.config.repository) |
404 else: | 460 else: |
(...skipping 26 matching lines...) Expand all Loading... |
431 versions = self.retireBuilds() | 487 versions = self.retireBuilds() |
432 | 488 |
433 if self.config.type == 'ie': | 489 if self.config.type == 'ie': |
434 self.writeIEUpdateManifest(versions) | 490 self.writeIEUpdateManifest(versions) |
435 | 491 |
436 # update index page | 492 # update index page |
437 self.updateIndex(versions) | 493 self.updateIndex(versions) |
438 | 494 |
439 # update nightlies config | 495 # update nightlies config |
440 self.config.latestRevision = self.revision | 496 self.config.latestRevision = self.revision |
| 497 |
| 498 if self.config.serviceAccountEmail and self.config.serviceAccountKey: |
| 499 self.uploadToChromeWebStore() |
441 finally: | 500 finally: |
442 # clean up | 501 # clean up |
443 if self.tempdir: | 502 if self.tempdir: |
444 shutil.rmtree(self.tempdir, ignore_errors=True) | 503 shutil.rmtree(self.tempdir, ignore_errors=True) |
445 | 504 |
446 | 505 |
447 def main(): | 506 def main(): |
448 """ | 507 """ |
449 main function for createNightlies.py | 508 main function for createNightlies.py |
450 """ | 509 """ |
(...skipping 16 matching lines...) Expand all Loading... |
467 except Exception, ex: | 526 except Exception, ex: |
468 print >>sys.stderr, "The build for %s failed:" % repo | 527 print >>sys.stderr, "The build for %s failed:" % repo |
469 traceback.print_exc() | 528 traceback.print_exc() |
470 | 529 |
471 file = open(nightlyConfigFile, 'wb') | 530 file = open(nightlyConfigFile, 'wb') |
472 nightlyConfig.write(file) | 531 nightlyConfig.write(file) |
473 | 532 |
474 | 533 |
475 if __name__ == '__main__': | 534 if __name__ == '__main__': |
476 main() | 535 main() |
OLD | NEW |