| Left: | ||
| Right: |
| 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 |
| 30 from datetime import datetime | 30 from datetime import datetime |
| 31 from urllib import urlencode | |
| 32 from urllib2 import urlopen | |
| 31 from xml.dom.minidom import parse as parseXml | 33 from xml.dom.minidom import parse as parseXml |
| 32 from sitescripts.utils import get_config, setupStderr, get_template | 34 from sitescripts.utils import get_config, setupStderr, get_template |
| 33 from sitescripts.extensions.utils import compareVersions, Configuration | 35 from sitescripts.extensions.utils import compareVersions, Configuration |
| 34 | 36 |
| 35 MAX_BUILDS = 50 | 37 MAX_BUILDS = 50 |
| 36 | 38 |
| 37 | 39 |
| 38 class NightlyBuild(object): | 40 class NightlyBuild(object): |
| 39 """ | 41 """ |
| 40 Performs the build process for an extension, | 42 Performs the build process for an extension, |
| (...skipping 238 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 279 | 281 |
| 280 | 282 |
| 281 def build(self): | 283 def build(self): |
| 282 """ | 284 """ |
| 283 run the build command in the tempdir | 285 run the build command in the tempdir |
| 284 """ | 286 """ |
| 285 baseDir = os.path.join(self.config.nightliesDirectory, self.basename) | 287 baseDir = os.path.join(self.config.nightliesDirectory, self.basename) |
| 286 if not os.path.exists(baseDir): | 288 if not os.path.exists(baseDir): |
| 287 os.makedirs(baseDir) | 289 os.makedirs(baseDir) |
| 288 outputFile = "%s-%s%s" % (self.basename, self.version, self.config.packageSu ffix) | 290 outputFile = "%s-%s%s" % (self.basename, self.version, self.config.packageSu ffix) |
| 289 outputPath = os.path.join(baseDir, outputFile) | 291 self.path = os.path.join(baseDir, outputFile) |
| 290 self.updateURL = urlparse.urljoin(self.config.nightliesURL, self.basename + '/' + outputFile + '?update') | 292 self.updateURL = urlparse.urljoin(self.config.nightliesURL, self.basename + '/' + outputFile + '?update') |
| 291 | 293 |
| 292 if self.config.type == 'android': | 294 if self.config.type == 'android': |
| 293 apkFile = open(outputPath, 'wb') | 295 apkFile = open(self.path, 'wb') |
| 294 | 296 |
| 295 try: | 297 try: |
| 296 try: | 298 try: |
| 297 port = get_config().get('extensions', 'androidBuildPort') | 299 port = get_config().get('extensions', 'androidBuildPort') |
| 298 except ConfigParser.NoOptionError: | 300 except ConfigParser.NoOptionError: |
| 299 port = '22' | 301 port = '22' |
| 300 buildCommand = ['ssh', '-p', port, get_config().get('extensions', 'andro idBuildHost')] | 302 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'])) | 303 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) | 304 subprocess.check_call(buildCommand, stdout=apkFile, close_fds=True) |
| 303 except: | 305 except: |
| 304 # clear broken output if any | 306 # clear broken output if any |
| 305 if os.path.exists(outputPath): | 307 if os.path.exists(self.path): |
| 306 os.remove(outputPath) | 308 os.remove(self.path) |
| 307 raise | 309 raise |
| 308 elif self.config.type == 'chrome' or self.config.type == 'opera': | 310 elif self.config.type == 'chrome' or self.config.type == 'opera': |
| 309 import buildtools.packagerChrome as packager | 311 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) | 312 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': | 313 elif self.config.type == 'safari': |
| 312 import buildtools.packagerSafari as packager | 314 import buildtools.packagerSafari as packager |
| 313 packager.createBuild(self.tempdir, type=self.config.type, outFile=outputPa th, buildNum=self.revision, keyFile=self.config.keyFile) | 315 packager.createBuild(self.tempdir, type=self.config.type, outFile=self.pat h, buildNum=self.revision, keyFile=self.config.keyFile) |
| 314 else: | 316 else: |
| 315 import buildtools.packagerGecko as packager | 317 import buildtools.packagerGecko as packager |
| 316 packager.createBuild(self.tempdir, outFile=outputPath, buildNum=self.revis ion, keyFile=self.config.keyFile) | 318 packager.createBuild(self.tempdir, outFile=self.path, buildNum=self.revisi on, keyFile=self.config.keyFile) |
| 317 | 319 |
| 318 if not os.path.exists(outputPath): | 320 if not os.path.exists(self.path): |
| 319 raise Exception("Build failed, output file hasn't been created") | 321 raise Exception("Build failed, output file hasn't been created") |
| 320 | 322 |
| 321 linkPath = os.path.join(baseDir, '00latest%s' % self.config.packageSuffix) | 323 linkPath = os.path.join(baseDir, '00latest%s' % self.config.packageSuffix) |
| 322 if hasattr(os, 'symlink'): | 324 if hasattr(os, 'symlink'): |
| 323 if os.path.exists(linkPath): | 325 if os.path.exists(linkPath): |
| 324 os.remove(linkPath) | 326 os.remove(linkPath) |
| 325 os.symlink(os.path.basename(outputPath), linkPath) | 327 os.symlink(os.path.basename(self.path), linkPath) |
| 326 else: | 328 else: |
| 327 shutil.copyfile(outputPath, linkPath) | 329 shutil.copyfile(self.path, linkPath) |
| 328 | 330 |
| 329 def retireBuilds(self): | 331 def retireBuilds(self): |
| 330 """ | 332 """ |
| 331 removes outdated builds, returns the sorted version numbers of remaining | 333 removes outdated builds, returns the sorted version numbers of remaining |
| 332 builds | 334 builds |
| 333 """ | 335 """ |
| 334 baseDir = os.path.join(self.config.nightliesDirectory, self.basename) | 336 baseDir = os.path.join(self.config.nightliesDirectory, self.basename) |
| 335 versions = [] | 337 versions = [] |
| 336 prefix = self.basename + '-' | 338 prefix = self.basename + '-' |
| 337 suffix = self.config.packageSuffix | 339 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] | 387 command = ['hg', 'archive', '-R', get_config().get('extensions', 'jsdocRepos itory'), '-r', 'default', docsdir] |
| 386 subprocess.check_call(command) | 388 subprocess.check_call(command) |
| 387 | 389 |
| 388 try: | 390 try: |
| 389 import buildtools.build as build | 391 import buildtools.build as build |
| 390 outputPath = os.path.join(self.config.docsDirectory, self.basename) | 392 outputPath = os.path.join(self.config.docsDirectory, self.basename) |
| 391 build.generateDocs(self.tempdir, None, [("-t", docsdir), ("-q", "")], [out putPath], self.config.type) | 393 build.generateDocs(self.tempdir, None, [("-t", docsdir), ("-q", "")], [out putPath], self.config.type) |
| 392 finally: | 394 finally: |
| 393 shutil.rmtree(docsdir, ignore_errors=True) | 395 shutil.rmtree(docsdir, ignore_errors=True) |
| 394 | 396 |
| 397 def uploadToChromeWebStore(self): | |
| 398 import M2Crypto | |
| 399 | |
| 400 # log in with the Google Developer Service Account | |
| 401 # https://developers.google.com/accounts/docs/OAuth2ServiceAccount#creatingj wt | |
| 402 | |
| 403 base64encode = lambda data: base64.urlsafe_b64encode(data).rstrip('=') | |
| 404 now = int(time.mktime(time.gmtime())) | |
| 405 | |
| 406 jwt = '%s.%s' % ( | |
| 407 base64encode(json.dumps({ | |
| 408 'alg': 'RS256', | |
| 409 'typ': 'JWT', | |
| 410 })), | |
| 411 base64encode(json.dumps({ | |
| 412 'iss': self.config.serviceAccountEmail, | |
| 413 'scope': 'https://www.googleapis.com/auth/chromewebstore', | |
| 414 'aud':'https://accounts.google.com/o/oauth2/token', | |
| 415 'exp': now + 3600, # fails if less than about an hour | |
| 416 'iat': now, | |
| 417 })), | |
| 418 ) | |
| 419 | |
| 420 jwt = '%s.%s' % (jwt, base64encode( | |
| 421 M2Crypto.RSA.load_key(self.config.serviceAccountKey).sign( | |
| 422 hashlib.sha256(jwt).digest(), 'sha256' | |
| 423 ) | |
| 424 )) | |
| 425 | |
| 426 response = json.load(urlopen( | |
| 427 'https://accounts.google.com/o/oauth2/token', | |
| 428 | |
| 429 urlencode([ | |
| 430 ('grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer'), | |
| 431 ('assertion', jwt), | |
| 432 ]) | |
| 433 )) | |
|
Wladimir Palant
2014/04/20 06:47:53
Check for error responses?
Sebastian Noack
2014/04/20 16:03:08
This API uses standard HTTP error codes, which ar
| |
| 434 | |
| 435 # upload a new version with the Chrome Web Store API | |
| 436 # https://developer.chrome.com/webstore/using_webstore_api#uploadexisitng | |
| 437 | |
| 438 subprocess.check_call([ | |
|
Sebastian Noack
2014/04/19 16:15:34
I use curl instead of urllib here, because urllib
Wladimir Palant
2014/04/20 06:47:53
We do file uploads with urllib in buildtools/local
Sebastian Noack
2014/04/20 16:03:08
It was actually easier to use urllib2 as I thought
| |
| 439 'curl', 'https://www.googleapis.com/upload/chromewebstore/v1.1/items/' + s elf.config.galleryID | |
| 440 | |
| 441 '--header', 'Authorization: %s %s' % (response['token_type'], response['ac cess_token']) | |
| 442 '--header', 'x-goog-api-version: 2', | |
| 443 '--request', 'PUT', | |
| 444 '--upload-file', self.path, | |
| 445 '--silent', | |
| 446 ]) | |
|
Wladimir Palant
2014/04/20 06:47:53
Here as well - please check for error responses, i
Sebastian Noack
2014/04/20 16:03:08
Done.
| |
| 447 | |
| 395 def run(self): | 448 def run(self): |
| 396 """ | 449 """ |
| 397 Run the nightly build process for one extension | 450 Run the nightly build process for one extension |
| 398 """ | 451 """ |
| 399 try: | 452 try: |
| 400 if self.config.type == 'ie': | 453 if self.config.type == 'ie': |
| 401 # We cannot build IE builds, simply list the builds already in | 454 # We cannot build IE builds, simply list the builds already in |
| 402 # the directory. Basename has to be deduced from the repository name. | 455 # the directory. Basename has to be deduced from the repository name. |
| 403 self.basename = os.path.basename(self.config.repository) | 456 self.basename = os.path.basename(self.config.repository) |
| 404 else: | 457 else: |
| (...skipping 26 matching lines...) Expand all Loading... | |
| 431 versions = self.retireBuilds() | 484 versions = self.retireBuilds() |
| 432 | 485 |
| 433 if self.config.type == 'ie': | 486 if self.config.type == 'ie': |
| 434 self.writeIEUpdateManifest(versions) | 487 self.writeIEUpdateManifest(versions) |
| 435 | 488 |
| 436 # update index page | 489 # update index page |
| 437 self.updateIndex(versions) | 490 self.updateIndex(versions) |
| 438 | 491 |
| 439 # update nightlies config | 492 # update nightlies config |
| 440 self.config.latestRevision = self.revision | 493 self.config.latestRevision = self.revision |
| 494 | |
| 495 if self.config.type == 'chrome': | |
|
Wladimir Palant
2014/04/20 06:47:53
This step needs to be optional, only if the necess
Sebastian Noack
2014/04/20 16:03:08
Done.
| |
| 496 self.uploadToChromeWebStore() | |
| 441 finally: | 497 finally: |
| 442 # clean up | 498 # clean up |
| 443 if self.tempdir: | 499 if self.tempdir: |
| 444 shutil.rmtree(self.tempdir, ignore_errors=True) | 500 shutil.rmtree(self.tempdir, ignore_errors=True) |
| 445 | 501 |
| 446 | 502 |
| 447 def main(): | 503 def main(): |
| 448 """ | 504 """ |
| 449 main function for createNightlies.py | 505 main function for createNightlies.py |
| 450 """ | 506 """ |
| (...skipping 16 matching lines...) Expand all Loading... | |
| 467 except Exception, ex: | 523 except Exception, ex: |
| 468 print >>sys.stderr, "The build for %s failed:" % repo | 524 print >>sys.stderr, "The build for %s failed:" % repo |
| 469 traceback.print_exc() | 525 traceback.print_exc() |
| 470 | 526 |
| 471 file = open(nightlyConfigFile, 'wb') | 527 file = open(nightlyConfigFile, 'wb') |
| 472 nightlyConfig.write(file) | 528 nightlyConfig.write(file) |
| 473 | 529 |
| 474 | 530 |
| 475 if __name__ == '__main__': | 531 if __name__ == '__main__': |
| 476 main() | 532 main() |
| OLD | NEW |