Left: | ||
Right: |
LEFT | RIGHT |
---|---|
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, time, base64 | 29 import tempfile, re, shutil, urlparse, pipes, time, urllib2, struct |
30 from datetime import datetime | 30 from datetime import datetime |
31 from urllib import urlencode | 31 from urllib import urlencode |
32 from urllib2 import urlopen | |
33 from xml.dom.minidom import parse as parseXml | 32 from xml.dom.minidom import parse as parseXml |
34 from sitescripts.utils import get_config, setupStderr, get_template | 33 from sitescripts.utils import get_config, setupStderr, get_template |
35 from sitescripts.extensions.utils import compareVersions, Configuration | 34 from sitescripts.extensions.utils import compareVersions, Configuration |
36 | 35 |
37 MAX_BUILDS = 50 | 36 MAX_BUILDS = 50 |
38 | 37 |
39 | 38 |
40 class NightlyBuild(object): | 39 class NightlyBuild(object): |
41 """ | 40 """ |
42 Performs the build process for an extension, | 41 Performs the build process for an extension, |
(...skipping 345 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
388 subprocess.check_call(command) | 387 subprocess.check_call(command) |
389 | 388 |
390 try: | 389 try: |
391 import buildtools.build as build | 390 import buildtools.build as build |
392 outputPath = os.path.join(self.config.docsDirectory, self.basename) | 391 outputPath = os.path.join(self.config.docsDirectory, self.basename) |
393 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) |
394 finally: | 393 finally: |
395 shutil.rmtree(docsdir, ignore_errors=True) | 394 shutil.rmtree(docsdir, ignore_errors=True) |
396 | 395 |
397 def uploadToChromeWebStore(self): | 396 def uploadToChromeWebStore(self): |
398 import M2Crypto | 397 # use refresh token to obtain a valid access token |
399 | 398 # https://developers.google.com/accounts/docs/OAuth2WebServer#refresh |
400 # log in with the Google Developer Service Account | 399 |
401 # https://developers.google.com/accounts/docs/OAuth2ServiceAccount#creatingj wt | 400 response = json.load(urllib2.urlopen( |
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', | 401 'https://accounts.google.com/o/oauth2/token', |
428 | 402 |
429 urlencode([ | 403 urlencode([ |
430 ('grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer'), | 404 ('refresh_token', self.config.refreshToken), |
431 ('assertion', jwt), | 405 ('client_id', self.config.clientID), |
406 ('client_secret', self.config.clientSecret), | |
407 ('grant_type', 'refresh_token'), | |
432 ]) | 408 ]) |
433 )) | 409 )) |
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 | 410 |
435 # upload a new version with the Chrome Web Store API | 411 # upload a new version with the Chrome Web Store API |
436 # https://developer.chrome.com/webstore/using_webstore_api#uploadexisitng | 412 # https://developer.chrome.com/webstore/using_webstore_api#uploadexisitng |
437 | 413 |
438 subprocess.check_call([ | 414 request = urllib2.Request('https://www.googleapis.com/upload/chromewebstore/ v1.1/items/' + self.config.galleryID) |
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 | 415 request.get_method = lambda: 'PUT' |
440 | 416 request.add_header('Authorization', '%s %s' % (response['token_type'], respo nse['access_token'])) |
441 '--header', 'Authorization: %s %s' % (response['token_type'], response['ac cess_token']) | 417 request.add_header('x-goog-api-version', '2') |
442 '--header', 'x-goog-api-version: 2', | 418 |
443 '--request', 'PUT', | 419 with open(self.path, 'rb') as file: |
444 '--upload-file', self.path, | 420 if file.read(8) != 'Cr24\x02\x00\x00\x00': |
445 '--silent', | 421 raise Exception('not a chrome extension or unknown CRX version') |
446 ]) | 422 |
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.
| |
423 # skip public key and signature | |
424 file.seek(sum(struct.unpack('<II', file.read(8))), os.SEEK_CUR) | |
425 | |
426 request.add_header('Content-Length', os.fstat(file.fileno()).st_size - fil e.tell()) | |
427 request.add_data(file) | |
428 | |
429 response = json.load(urllib2.urlopen(request)) | |
430 | |
431 if response['uploadState'] == 'FAILURE': | |
432 raise Exception(response['itemError']) | |
447 | 433 |
448 def run(self): | 434 def run(self): |
449 """ | 435 """ |
450 Run the nightly build process for one extension | 436 Run the nightly build process for one extension |
451 """ | 437 """ |
452 try: | 438 try: |
453 if self.config.type == 'ie': | 439 if self.config.type == 'ie': |
454 # We cannot build IE builds, simply list the builds already in | 440 # We cannot build IE builds, simply list the builds already in |
455 # the directory. Basename has to be deduced from the repository name. | 441 # the directory. Basename has to be deduced from the repository name. |
456 self.basename = os.path.basename(self.config.repository) | 442 self.basename = os.path.basename(self.config.repository) |
(...skipping 28 matching lines...) Expand all Loading... | |
485 | 471 |
486 if self.config.type == 'ie': | 472 if self.config.type == 'ie': |
487 self.writeIEUpdateManifest(versions) | 473 self.writeIEUpdateManifest(versions) |
488 | 474 |
489 # update index page | 475 # update index page |
490 self.updateIndex(versions) | 476 self.updateIndex(versions) |
491 | 477 |
492 # update nightlies config | 478 # update nightlies config |
493 self.config.latestRevision = self.revision | 479 self.config.latestRevision = self.revision |
494 | 480 |
495 if self.config.type == 'chrome': | 481 if self.type == 'chrome' and self.config.clientID and self.config.clientSe cret and self.config.refreshToken: |
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() | 482 self.uploadToChromeWebStore() |
497 finally: | 483 finally: |
498 # clean up | 484 # clean up |
499 if self.tempdir: | 485 if self.tempdir: |
500 shutil.rmtree(self.tempdir, ignore_errors=True) | 486 shutil.rmtree(self.tempdir, ignore_errors=True) |
501 | 487 |
502 | 488 |
503 def main(): | 489 def main(): |
504 """ | 490 """ |
505 main function for createNightlies.py | 491 main function for createNightlies.py |
(...skipping 17 matching lines...) Expand all Loading... | |
523 except Exception, ex: | 509 except Exception, ex: |
524 print >>sys.stderr, "The build for %s failed:" % repo | 510 print >>sys.stderr, "The build for %s failed:" % repo |
525 traceback.print_exc() | 511 traceback.print_exc() |
526 | 512 |
527 file = open(nightlyConfigFile, 'wb') | 513 file = open(nightlyConfigFile, 'wb') |
528 nightlyConfig.write(file) | 514 nightlyConfig.write(file) |
529 | 515 |
530 | 516 |
531 if __name__ == '__main__': | 517 if __name__ == '__main__': |
532 main() | 518 main() |
LEFT | RIGHT |