| OLD | NEW | 
|    1 # This file is part of the Adblock Plus web scripts, |    1 # This file is part of the Adblock Plus web scripts, | 
|    2 # Copyright (C) 2006-2016 Eyeo GmbH |    2 # Copyright (C) 2006-2016 Eyeo GmbH | 
|    3 # |    3 # | 
|    4 # Adblock Plus is free software: you can redistribute it and/or modify |    4 # Adblock Plus is free software: you can redistribute it and/or modify | 
|    5 # it under the terms of the GNU General Public License version 3 as |    5 # it under the terms of the GNU General Public License version 3 as | 
|    6 # published by the Free Software Foundation. |    6 # published by the Free Software Foundation. | 
|    7 # |    7 # | 
|    8 # Adblock Plus is distributed in the hope that it will be useful, |    8 # Adblock Plus is distributed in the hope that it will be useful, | 
|    9 # but WITHOUT ANY WARRANTY; without even the implied warranty of |    9 # but WITHOUT ANY WARRANTY; without even the implied warranty of | 
|   10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |   10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | 
|   11 # GNU General Public License for more details. |   11 # GNU General Public License for more details. | 
|   12 # |   12 # | 
|   13 # You should have received a copy of the GNU General Public License |   13 # You should have received a copy of the GNU General Public License | 
|   14 # along with Adblock Plus.  If not, see <http://www.gnu.org/licenses/>. |   14 # along with Adblock Plus.  If not, see <http://www.gnu.org/licenses/>. | 
|   15  |   15  | 
|   16 """ |   16 """ | 
|   17  |   17  | 
|   18 Nightly builds generation script |   18 Nightly builds generation script | 
|   19 ================================ |   19 ================================ | 
|   20  |   20  | 
|   21   This script generates nightly builds of extensions, together |   21   This script generates nightly builds of extensions, together | 
|   22   with changelogs and documentation. |   22   with changelogs and documentation. | 
|   23  |   23  | 
|   24 """ |   24 """ | 
|   25  |   25  | 
|   26 import ConfigParser |   26 import ConfigParser | 
|   27 import base64 |   27 import base64 | 
|   28 from datetime import datetime |  | 
|   29 import hashlib |   28 import hashlib | 
|   30 import hmac |   29 import hmac | 
|   31 import json |   30 import json | 
|   32 import logging |   31 import logging | 
|   33 import os |   32 import os | 
|   34 import pipes |   33 import pipes | 
|   35 import random |   34 import random | 
|   36 import shutil |   35 import shutil | 
|   37 import struct |   36 import struct | 
|   38 import subprocess |   37 import subprocess | 
|   39 import sys |   38 import sys | 
|   40 import tempfile |   39 import tempfile | 
|   41 import time |   40 import time | 
|   42 from urllib import urlencode |   41 from urllib import urlencode | 
|   43 import urllib2 |   42 import urllib2 | 
|   44 import urlparse |   43 import urlparse | 
 |   44 import zipfile | 
 |   45 import contextlib | 
 |   46  | 
|   45 from xml.dom.minidom import parse as parseXml |   47 from xml.dom.minidom import parse as parseXml | 
|   46  |   48  | 
|   47 from sitescripts.extensions.utils import ( |   49 from sitescripts.extensions.utils import ( | 
|   48     compareVersions, Configuration, |   50     compareVersions, Configuration, | 
|   49     writeAndroidUpdateManifest |   51     writeAndroidUpdateManifest | 
|   50 ) |   52 ) | 
|   51 from sitescripts.utils import get_config, get_template |   53 from sitescripts.utils import get_config, get_template | 
|   52  |   54  | 
|   53 MAX_BUILDS = 50 |   55 MAX_BUILDS = 50 | 
|   54  |   56  | 
|   55  |   57  | 
 |   58 # Google and Microsoft APIs use HTTP error codes with error message in | 
 |   59 # body. So we add the response body to the HTTPError to get more | 
 |   60 # meaningful error messages. | 
 |   61 class HTTPErrorBodyHandler(urllib2.HTTPDefaultErrorHandler): | 
 |   62     def http_error_default(self, req, fp, code, msg, hdrs): | 
 |   63         raise urllib2.HTTPError(req.get_full_url(), code, | 
 |   64                                 '{}\n{}'.format(msg, fp.read()), hdrs, fp) | 
 |   65  | 
 |   66  | 
|   56 class NightlyBuild(object): |   67 class NightlyBuild(object): | 
|   57     """ |   68     """ | 
|   58       Performs the build process for an extension, |   69       Performs the build process for an extension, | 
|   59       generating changelogs and documentation. |   70       generating changelogs and documentation. | 
|   60     """ |   71     """ | 
|   61  |   72  | 
|   62     def __init__(self, config): |   73     def __init__(self, config): | 
|   63         """ |   74         """ | 
|   64           Creates a NightlyBuild instance; we are simply |   75           Creates a NightlyBuild instance; we are simply | 
|   65           recording the configuration settings here. |   76           recording the configuration settings here. | 
| (...skipping 159 matching lines...) Expand 10 before | Expand all | Expand 10 after  Loading... | 
|  225         metadata = packager.readMetadata(self.tempdir, self.config.type) |  236         metadata = packager.readMetadata(self.tempdir, self.config.type) | 
|  226         certs = xarfile.read_certificates_and_key(self.config.keyFile)[0] |  237         certs = xarfile.read_certificates_and_key(self.config.keyFile)[0] | 
|  227  |  238  | 
|  228         self.certificateID = packager.get_developer_identifier(certs) |  239         self.certificateID = packager.get_developer_identifier(certs) | 
|  229         self.version = packager.getBuildVersion(self.tempdir, metadata, False, |  240         self.version = packager.getBuildVersion(self.tempdir, metadata, False, | 
|  230                                                 self.buildNum) |  241                                                 self.buildNum) | 
|  231         self.shortVersion = metadata.get('general', 'version') |  242         self.shortVersion = metadata.get('general', 'version') | 
|  232         self.basename = metadata.get('general', 'basename') |  243         self.basename = metadata.get('general', 'basename') | 
|  233         self.updatedFromGallery = False |  244         self.updatedFromGallery = False | 
|  234  |  245  | 
 |  246     def read_edge_metadata(self): | 
 |  247         """ | 
 |  248           Read Edge-specific metadata from metadata file. | 
 |  249         """ | 
 |  250         from buildtools import packager | 
 |  251         # Now read metadata file | 
 |  252         metadata = packager.readMetadata(self.tempdir, self.config.type) | 
 |  253         self.version = packager.getBuildVersion(self.tempdir, metadata, False, | 
 |  254                                                 self.buildNum) | 
 |  255         self.basename = metadata.get('general', 'basename') | 
 |  256  | 
 |  257         self.compat = [] | 
 |  258  | 
|  235     def writeUpdateManifest(self): |  259     def writeUpdateManifest(self): | 
|  236         """ |  260         """ | 
|  237           Writes update manifest for the current build |  261           Writes update manifest for the current build | 
|  238         """ |  262         """ | 
|  239         baseDir = os.path.join(self.config.nightliesDirectory, self.basename) |  263         baseDir = os.path.join(self.config.nightliesDirectory, self.basename) | 
|  240         if self.config.type == 'safari': |  264         if self.config.type == 'safari': | 
|  241             manifestPath = os.path.join(baseDir, 'updates.plist') |  265             manifestPath = os.path.join(baseDir, 'updates.plist') | 
|  242             templateName = 'safariUpdateManifest' |  266             templateName = 'safariUpdateManifest' | 
|  243             autoescape = True |  267             autoescape = True | 
|  244         elif self.config.type == 'android': |  268         elif self.config.type == 'android': | 
| (...skipping 86 matching lines...) Expand 10 before | Expand all | Expand 10 after  Loading... | 
|  331                     os.remove(self.path) |  355                     os.remove(self.path) | 
|  332                 raise |  356                 raise | 
|  333         else: |  357         else: | 
|  334             env = os.environ |  358             env = os.environ | 
|  335             spiderMonkeyBinary = self.config.spiderMonkeyBinary |  359             spiderMonkeyBinary = self.config.spiderMonkeyBinary | 
|  336             if spiderMonkeyBinary: |  360             if spiderMonkeyBinary: | 
|  337                 env = dict(env, SPIDERMONKEY_BINARY=spiderMonkeyBinary) |  361                 env = dict(env, SPIDERMONKEY_BINARY=spiderMonkeyBinary) | 
|  338  |  362  | 
|  339             command = [os.path.join(self.tempdir, 'build.py'), |  363             command = [os.path.join(self.tempdir, 'build.py'), | 
|  340                        '-t', self.config.type, 'build', '-b', self.buildNum] |  364                        '-t', self.config.type, 'build', '-b', self.buildNum] | 
|  341             if self.config.type not in {'gecko', 'gecko-webext'}: |  365             if self.config.type not in {'gecko', 'gecko-webext', 'edge'}: | 
|  342                 command.extend(['-k', self.config.keyFile]) |  366                 command.extend(['-k', self.config.keyFile]) | 
|  343             command.append(self.path) |  367             command.append(self.path) | 
|  344             subprocess.check_call(command, env=env) |  368             subprocess.check_call(command, env=env) | 
|  345  |  369  | 
|  346         if not os.path.exists(self.path): |  370         if not os.path.exists(self.path): | 
|  347             raise Exception("Build failed, output file hasn't been created") |  371             raise Exception("Build failed, output file hasn't been created") | 
|  348  |  372  | 
|  349         linkPath = os.path.join(baseDir, '00latest%s' % self.config.packageSuffi
     x) |  373         linkPath = os.path.join(baseDir, '00latest%s' % self.config.packageSuffi
     x) | 
|  350         if hasattr(os, 'symlink'): |  374         if hasattr(os, 'symlink'): | 
|  351             if os.path.exists(linkPath): |  375             if os.path.exists(linkPath): | 
| (...skipping 99 matching lines...) Expand 10 before | Expand all | Expand 10 after  Loading... | 
|  451         try: |  475         try: | 
|  452             urllib2.urlopen(request).close() |  476             urllib2.urlopen(request).close() | 
|  453         except urllib2.HTTPError as e: |  477         except urllib2.HTTPError as e: | 
|  454             try: |  478             try: | 
|  455                 logging.error(e.read()) |  479                 logging.error(e.read()) | 
|  456             finally: |  480             finally: | 
|  457                 e.close() |  481                 e.close() | 
|  458             raise |  482             raise | 
|  459  |  483  | 
|  460     def uploadToChromeWebStore(self): |  484     def uploadToChromeWebStore(self): | 
|  461         # Google APIs use HTTP error codes with error message in body. So we add |  | 
|  462         # the response body to the HTTPError to get more meaningful error messag
     es. |  | 
|  463  |  | 
|  464         class HTTPErrorBodyHandler(urllib2.HTTPDefaultErrorHandler): |  | 
|  465             def http_error_default(self, req, fp, code, msg, hdrs): |  | 
|  466                 raise urllib2.HTTPError(req.get_full_url(), code, '%s\n%s' % (ms
     g, fp.read()), hdrs, fp) |  | 
|  467  |  485  | 
|  468         opener = urllib2.build_opener(HTTPErrorBodyHandler) |  486         opener = urllib2.build_opener(HTTPErrorBodyHandler) | 
|  469  |  487  | 
|  470         # use refresh token to obtain a valid access token |  488         # use refresh token to obtain a valid access token | 
|  471         # https://developers.google.com/accounts/docs/OAuth2WebServer#refresh |  489         # https://developers.google.com/accounts/docs/OAuth2WebServer#refresh | 
|  472  |  490  | 
|  473         response = json.load(opener.open( |  491         response = json.load(opener.open( | 
|  474             'https://accounts.google.com/o/oauth2/token', |  492             'https://accounts.google.com/o/oauth2/token', | 
|  475  |  493  | 
|  476             urlencode([ |  494             urlencode([ | 
| (...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after  Loading... | 
|  513         request.get_method = lambda: 'POST' |  531         request.get_method = lambda: 'POST' | 
|  514         request.add_header('Authorization', auth_token) |  532         request.add_header('Authorization', auth_token) | 
|  515         request.add_header('x-goog-api-version', '2') |  533         request.add_header('x-goog-api-version', '2') | 
|  516         request.add_header('Content-Length', '0') |  534         request.add_header('Content-Length', '0') | 
|  517  |  535  | 
|  518         response = json.load(opener.open(request)) |  536         response = json.load(opener.open(request)) | 
|  519  |  537  | 
|  520         if any(status not in ('OK', 'ITEM_PENDING_REVIEW') for status in respons
     e['status']): |  538         if any(status not in ('OK', 'ITEM_PENDING_REVIEW') for status in respons
     e['status']): | 
|  521             raise Exception({'status': response['status'], 'statusDetail': respo
     nse['statusDetail']}) |  539             raise Exception({'status': response['status'], 'statusDetail': respo
     nse['statusDetail']}) | 
|  522  |  540  | 
 |  541     def get_windows_store_access_token(self): | 
 |  542         # use refresh token to obtain a valid access token | 
 |  543         # https://docs.microsoft.com/en-us/azure/active-directory/active-directo
     ry-protocols-oauth-code#refreshing-the-access-tokens | 
 |  544         server = 'https://login.microsoftonline.com' | 
 |  545         token_path = '{}/{}/oauth2/token'.format(server, self.config.tenantID) | 
 |  546  | 
 |  547         opener = urllib2.build_opener(HTTPErrorBodyHandler) | 
 |  548         post_data = urlencode([ | 
 |  549             ('refresh_token', self.config.refreshToken), | 
 |  550             ('client_id', self.config.clientID), | 
 |  551             ('client_secret', self.config.clientSecret), | 
 |  552             ('grant_type', 'refresh_token'), | 
 |  553             ('resource', 'https://graph.windows.net') | 
 |  554         ]) | 
 |  555         request = urllib2.Request(token_path, post_data) | 
 |  556         with contextlib.closing(opener.open(request)) as response: | 
 |  557             data = json.load(response) | 
 |  558             auth_token = '{0[token_type]} {0[access_token]}'.format(data) | 
 |  559  | 
 |  560         return auth_token | 
 |  561  | 
 |  562     def upload_appx_file_to_windows_store(self, file_upload_url): | 
 |  563         # Add .appx file to a .zip file | 
 |  564         zip_path = os.path.splitext(self.path)[0] + '.zip' | 
 |  565         with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf: | 
 |  566             zf.write(self.path, os.path.basename(self.path)) | 
 |  567  | 
 |  568         # Upload that .zip file | 
 |  569         file_upload_url = file_upload_url.replace('+', '%2B') | 
 |  570         request = urllib2.Request(file_upload_url) | 
 |  571         request.get_method = lambda: 'PUT' | 
 |  572         request.add_header('x-ms-blob-type', 'BlockBlob') | 
 |  573  | 
 |  574         opener = urllib2.build_opener(HTTPErrorBodyHandler) | 
 |  575  | 
 |  576         with open(zip_path, 'rb') as file: | 
 |  577             request.add_header('Content-Length', | 
 |  578                                os.fstat(file.fileno()).st_size - file.tell()) | 
 |  579             request.add_data(file) | 
 |  580             opener.open(request).close() | 
 |  581  | 
 |  582     # Clone the previous submission for the new one. Largely based on code | 
 |  583     # from https://msdn.microsoft.com/en-us/windows/uwp/monetize/python-code-exa
     mples-for-the-windows-store-submission-api#create-an-app-submission | 
 |  584     def upload_to_windows_store(self): | 
 |  585         opener = urllib2.build_opener(HTTPErrorBodyHandler) | 
 |  586  | 
 |  587         headers = {'Authorization': self.get_windows_store_access_token(), | 
 |  588                    'Content-type': 'application/json'} | 
 |  589  | 
 |  590         # Get application | 
 |  591         # https://docs.microsoft.com/en-us/windows/uwp/monetize/get-an-app | 
 |  592         api_path = '{}/v1.0/my/applications/{}'.format( | 
 |  593             'https://manage.devcenter.microsoft.com', | 
 |  594             self.config.devbuildGalleryID | 
 |  595         ) | 
 |  596  | 
 |  597         request = urllib2.Request(api_path, None, headers) | 
 |  598         with contextlib.closing(opener.open(request)) as response: | 
 |  599             app_obj = json.load(response) | 
 |  600  | 
 |  601         # Delete existing in-progress submission | 
 |  602         # https://docs.microsoft.com/en-us/windows/uwp/monetize/delete-an-app-su
     bmission | 
 |  603         submissions_path = api_path + '/submissions' | 
 |  604         if 'pendingApplicationSubmission' in app_obj: | 
 |  605             remove_id = app_obj['pendingApplicationSubmission']['id'] | 
 |  606             remove_path = '{}/{}'.format(submissions_path, remove_id) | 
 |  607             request = urllib2.Request(remove_path, '', headers) | 
 |  608             request.get_method = lambda: 'DELETE' | 
 |  609             opener.open(request).close() | 
 |  610  | 
 |  611         # Create submission | 
 |  612         # https://msdn.microsoft.com/en-us/windows/uwp/monetize/create-an-app-su
     bmission | 
 |  613         request = urllib2.Request(submissions_path, '', headers) | 
 |  614         request.get_method = lambda: 'POST' | 
 |  615         with contextlib.closing(opener.open(request)) as response: | 
 |  616             submission = json.load(response) | 
 |  617  | 
 |  618         submission_id = submission['id'] | 
 |  619         file_upload_url = submission['fileUploadUrl'] | 
 |  620  | 
 |  621         new_submission_path = '{}/{}'.format(submissions_path, | 
 |  622                                              submission_id) | 
 |  623  | 
 |  624         request = urllib2.Request(new_submission_path, None, headers) | 
 |  625         opener.open(request).close() | 
 |  626  | 
 |  627         self.upload_appx_file_to_windows_store(file_upload_url) | 
 |  628  | 
 |  629         # Commit submission | 
 |  630         # https://msdn.microsoft.com/en-us/windows/uwp/monetize/commit-an-app-su
     bmission | 
 |  631         commit_path = '{}/commit'.format(new_submission_path) | 
 |  632         request = urllib2.Request(commit_path, '', headers) | 
 |  633         request.get_method = lambda: 'POST' | 
 |  634         with contextlib.closing(opener.open(request)) as response: | 
 |  635             submission = json.load(response) | 
 |  636  | 
 |  637         if submission['status'] != 'CommitStarted': | 
 |  638             raise Exception({'status': submission['status'], | 
 |  639                              'statusDetails': submission['statusDetails']}) | 
 |  640  | 
|  523     def run(self): |  641     def run(self): | 
|  524         """ |  642         """ | 
|  525           Run the nightly build process for one extension |  643           Run the nightly build process for one extension | 
|  526         """ |  644         """ | 
|  527         try: |  645         try: | 
|  528             if self.config.type == 'ie': |  646             if self.config.type == 'ie': | 
|  529                 # We cannot build IE builds, simply list the builds already in |  647                 # We cannot build IE builds, simply list the builds already in | 
|  530                 # the directory. Basename has to be deduced from the repository 
     name. |  648                 # the directory. Basename has to be deduced from the repository 
     name. | 
|  531                 self.basename = os.path.basename(self.config.repository) |  649                 self.basename = os.path.basename(self.config.repository) | 
|  532             else: |  650             else: | 
|  533                 # copy the repository into a temporary directory |  651                 # copy the repository into a temporary directory | 
|  534                 self.copyRepository() |  652                 self.copyRepository() | 
|  535                 self.buildNum = self.getCurrentBuild() |  653                 self.buildNum = self.getCurrentBuild() | 
|  536  |  654  | 
|  537                 # get meta data from the repository |  655                 # get meta data from the repository | 
|  538                 if self.config.type == 'android': |  656                 if self.config.type == 'android': | 
|  539                     self.readAndroidMetadata() |  657                     self.readAndroidMetadata() | 
|  540                 elif self.config.type == 'chrome': |  658                 elif self.config.type == 'chrome': | 
|  541                     self.readChromeMetadata() |  659                     self.readChromeMetadata() | 
|  542                 elif self.config.type == 'safari': |  660                 elif self.config.type == 'safari': | 
|  543                     self.readSafariMetadata() |  661                     self.readSafariMetadata() | 
|  544                 elif self.config.type in {'gecko', 'gecko-webext'}: |  662                 elif self.config.type in {'gecko', 'gecko-webext'}: | 
|  545                     self.readGeckoMetadata() |  663                     self.readGeckoMetadata() | 
 |  664                 elif self.config.type == 'edge': | 
 |  665                     self.read_edge_metadata() | 
|  546                 else: |  666                 else: | 
|  547                     raise Exception('Unknown build type {}' % self.config.type) |  667                     raise Exception('Unknown build type {}' % self.config.type) | 
|  548  |  668  | 
|  549                 # create development build |  669                 # create development build | 
|  550                 self.build() |  670                 self.build() | 
|  551  |  671  | 
|  552                 # write out changelog |  672                 # write out changelog | 
|  553                 self.writeChangelog(self.getChanges()) |  673                 self.writeChangelog(self.getChanges()) | 
|  554  |  674  | 
|  555                 # write update manifest |  675                 # write update manifest | 
| (...skipping 11 matching lines...) Expand all  Loading... | 
|  567  |  687  | 
|  568             # update nightlies config |  688             # update nightlies config | 
|  569             self.config.latestRevision = self.revision |  689             self.config.latestRevision = self.revision | 
|  570  |  690  | 
|  571             if (self.config.type in {'gecko', 'gecko-webext'} and |  691             if (self.config.type in {'gecko', 'gecko-webext'} and | 
|  572                     self.config.galleryID and |  692                     self.config.galleryID and | 
|  573                     get_config().has_option('extensions', 'amo_key')): |  693                     get_config().has_option('extensions', 'amo_key')): | 
|  574                 self.uploadToMozillaAddons() |  694                 self.uploadToMozillaAddons() | 
|  575             elif self.config.type == 'chrome' and self.config.clientID and self.
     config.clientSecret and self.config.refreshToken: |  695             elif self.config.type == 'chrome' and self.config.clientID and self.
     config.clientSecret and self.config.refreshToken: | 
|  576                 self.uploadToChromeWebStore() |  696                 self.uploadToChromeWebStore() | 
 |  697             elif self.config.type == 'edge' and self.config.clientID and self.co
     nfig.clientSecret and self.config.refreshToken and self.config.tenantID: | 
 |  698                 self.upload_to_windows_store() | 
 |  699  | 
|  577         finally: |  700         finally: | 
|  578             # clean up |  701             # clean up | 
|  579             if self.tempdir: |  702             if self.tempdir: | 
|  580                 shutil.rmtree(self.tempdir, ignore_errors=True) |  703                 shutil.rmtree(self.tempdir, ignore_errors=True) | 
|  581  |  704  | 
|  582  |  705  | 
|  583 def main(): |  706 def main(): | 
|  584     """ |  707     """ | 
|  585       main function for createNightlies.py |  708       main function for createNightlies.py | 
|  586     """ |  709     """ | 
| (...skipping 14 matching lines...) Expand all  Loading... | 
|  601         except Exception as ex: |  724         except Exception as ex: | 
|  602             logging.error('The build for %s failed:', repo) |  725             logging.error('The build for %s failed:', repo) | 
|  603             logging.exception(ex) |  726             logging.exception(ex) | 
|  604  |  727  | 
|  605     file = open(nightlyConfigFile, 'wb') |  728     file = open(nightlyConfigFile, 'wb') | 
|  606     nightlyConfig.write(file) |  729     nightlyConfig.write(file) | 
|  607  |  730  | 
|  608  |  731  | 
|  609 if __name__ == '__main__': |  732 if __name__ == '__main__': | 
|  610     main() |  733     main() | 
| OLD | NEW |