Index: sitescripts/extensions/bin/createNightlies.py |
diff --git a/sitescripts/extensions/bin/createNightlies.py b/sitescripts/extensions/bin/createNightlies.py |
index 9ea72c2ce25be7aee8ca5c1040a6bb33e85bc730..e1a2c3c8132f4183fa0c14dac6ceb6224665dbee 100644 |
--- a/sitescripts/extensions/bin/createNightlies.py |
+++ b/sitescripts/extensions/bin/createNightlies.py |
@@ -23,6 +23,7 @@ Nightly builds generation script |
""" |
+import argparse |
import ConfigParser |
import base64 |
import hashlib |
@@ -70,6 +71,8 @@ class NightlyBuild(object): |
generating changelogs and documentation. |
""" |
+ downloadable_repos = {'gecko'} |
+ |
def __init__(self, config): |
""" |
Creates a NightlyBuild instance; we are simply |
@@ -105,8 +108,6 @@ class NightlyBuild(object): |
""" |
command = ['hg', 'id', '-n', '--config', 'defaults.id=', self.tempdir] |
build = subprocess.check_output(command).strip() |
- if self.config.type == 'gecko': |
- build += 'beta' |
return build |
def getChanges(self): |
@@ -141,6 +142,14 @@ class NightlyBuild(object): |
if os.path.isfile(depscript): |
subprocess.check_call([sys.executable, depscript, '-q']) |
+ def symlink_or_copy(self, source, target): |
+ if hasattr(os, 'symlink'): |
+ if os.path.exists(target): |
+ os.remove(target) |
+ os.symlink(os.path.basename(source), target) |
+ else: |
+ shutil.copyfile(source, target) |
+ |
def writeChangelog(self, changes): |
""" |
write the changelog file into the cloned repository |
@@ -156,12 +165,7 @@ class NightlyBuild(object): |
template.stream({'changes': changes}).dump(changelogPath, encoding='utf-8') |
linkPath = os.path.join(baseDir, '00latest.changelog.xhtml') |
- if hasattr(os, 'symlink'): |
- if os.path.exists(linkPath): |
- os.remove(linkPath) |
- os.symlink(os.path.basename(changelogPath), linkPath) |
- else: |
- shutil.copyfile(changelogPath, linkPath) |
+ self.symlink_or_copy(changelogPath, linkPath) |
def readGeckoMetadata(self): |
""" |
@@ -171,10 +175,11 @@ class NightlyBuild(object): |
""" |
import buildtools.packagerChrome as packager |
metadata = packager.readMetadata(self.tempdir, self.config.type) |
- self.extensionID = metadata.get('general', 'id') |
+ self.extensionID = packager.get_app_id(False, metadata) |
self.version = packager.getBuildVersion(self.tempdir, metadata, False, |
self.buildNum) |
self.basename = metadata.get('general', 'basename') |
+ self.min_version = metadata.get('compat', 'gecko') |
def readAndroidMetadata(self): |
""" |
@@ -221,7 +226,7 @@ class NightlyBuild(object): |
def readSafariMetadata(self): |
import sitescripts.extensions.bin.legacy.packagerSafari as packager |
- from buildtools import xarfile |
+ from sitescripts.extensions.bin.legacy import xarfile |
metadata = packager.readMetadata(self.tempdir, self.config.type) |
certs = xarfile.read_certificates_and_key(self.config.keyFile)[0] |
@@ -258,6 +263,10 @@ class NightlyBuild(object): |
manifestPath = os.path.join(baseDir, 'updates.xml') |
templateName = 'androidUpdateManifest' |
autoescape = True |
+ elif self.config.type == 'gecko': |
+ manifestPath = os.path.join(baseDir, 'updates.json') |
+ templateName = 'geckoUpdateManifest' |
+ autoescape = True |
else: |
return |
@@ -302,12 +311,7 @@ class NightlyBuild(object): |
for suffix in ['-x86.msi', '-x64.msi', '-gpo-x86.msi', '-gpo-x64.msi']: |
linkPath = os.path.join(baseDir, '00latest%s' % suffix) |
outputPath = os.path.join(baseDir, self.basename + '-' + version + suffix) |
- if hasattr(os, 'symlink'): |
- if os.path.exists(linkPath): |
- os.remove(linkPath) |
- os.symlink(os.path.basename(outputPath), linkPath) |
- else: |
- shutil.copyfile(outputPath, linkPath) |
+ self.symlink_or_copy(outputPath, linkPath) |
def build(self): |
""" |
@@ -345,8 +349,13 @@ class NightlyBuild(object): |
if spiderMonkeyBinary: |
env = dict(env, SPIDERMONKEY_BINARY=spiderMonkeyBinary) |
- command = [os.path.join(self.tempdir, 'build.py'), |
- 'build', '-t', self.config.type, '-b', self.buildNum] |
+ command = [os.path.join(self.tempdir, 'build.py')] |
+ if self.config.type == 'safari': |
+ command.extend(['-t', self.config.type, 'build']) |
+ else: |
+ command.extend(['build', '-t', self.config.type]) |
+ command.extend(['-b', self.buildNum]) |
+ |
if self.config.type not in {'gecko', 'edge'}: |
command.extend(['-k', self.config.keyFile]) |
command.append(self.path) |
@@ -355,13 +364,10 @@ class NightlyBuild(object): |
if not os.path.exists(self.path): |
raise Exception("Build failed, output file hasn't been created") |
- linkPath = os.path.join(baseDir, '00latest%s' % self.config.packageSuffix) |
- if hasattr(os, 'symlink'): |
- if os.path.exists(linkPath): |
- os.remove(linkPath) |
- os.symlink(os.path.basename(self.path), linkPath) |
- else: |
- shutil.copyfile(self.path, linkPath) |
+ if self.config.type not in self.downloadable_repos: |
+ linkPath = os.path.join(baseDir, |
+ '00latest' + self.config.packageSuffix) |
+ self.symlink_or_copy(self.path, linkPath) |
def retireBuilds(self): |
""" |
@@ -414,9 +420,45 @@ class NightlyBuild(object): |
template = get_template(get_config().get('extensions', 'nightlyIndexPage')) |
template.stream({'config': self.config, 'links': links}).dump(outputPath) |
- def uploadToMozillaAddons(self): |
- import urllib3 |
+ def read_downloads_lockfile(self): |
+ path = get_config().get('extensions', 'downloadLockFile') |
+ try: |
+ with open(path, 'r') as fp: |
+ current = json.load(fp) |
+ except IOError: |
+ logging.warning('No lockfile found. Creating ' + path) |
+ current = {} |
+ return current |
+ |
+ def write_downloads_lockfile(self, values): |
+ path = get_config().get('extensions', 'downloadLockFile') |
+ with open(path, 'w') as fp: |
+ json.dump(values, fp) |
+ |
+ def add_to_downloads_lockfile(self, platform, values): |
+ current = self.read_downloads_lockfile() |
+ |
+ current.setdefault(platform, []) |
+ current[platform].append(values) |
+ |
+ self.write_downloads_lockfile(current) |
+ |
+ def remove_from_downloads_lockfile(self, platform, filter_key, |
+ filter_value): |
+ current = self.read_downloads_lockfile() |
+ try: |
+ for i, entry in enumerate(current[platform]): |
+ if entry[filter_key] == filter_value: |
+ del current[platform][i] |
+ if len(current[platform]) == 0: |
+ del current[platform] |
+ except KeyError: |
+ pass |
+ self.write_downloads_lockfile(current) |
+ |
+ def generate_jwt_request(self, issuer, secret, url, method, data=None, |
+ add_headers=[]): |
header = { |
'alg': 'HS256', # HMAC-SHA256 |
'typ': 'JWT', |
@@ -424,21 +466,33 @@ class NightlyBuild(object): |
issued = int(time.time()) |
payload = { |
- 'iss': get_config().get('extensions', 'amo_key'), |
+ 'iss': issuer, |
'jti': random.random(), |
'iat': issued, |
'exp': issued + 60, |
} |
- input = '{}.{}'.format( |
+ hmac_data = '{}.{}'.format( |
base64.b64encode(json.dumps(header)), |
base64.b64encode(json.dumps(payload)) |
) |
- signature = hmac.new(get_config().get('extensions', 'amo_secret'), |
- msg=input, |
+ signature = hmac.new(secret, msg=hmac_data, |
digestmod=hashlib.sha256).digest() |
- token = '{}.{}'.format(input, base64.b64encode(signature)) |
+ token = '{}.{}'.format(hmac_data, base64.b64encode(signature)) |
+ |
+ request = urllib2.Request(url, data) |
+ request.add_header('Authorization', 'JWT ' + token) |
+ for header in add_headers: |
+ request.add_header(*header) |
+ request.get_method = lambda: method |
+ |
+ return request |
+ |
+ def uploadToMozillaAddons(self): |
+ import urllib3 |
+ |
+ config = get_config() |
upload_url = ('https://addons.mozilla.org/api/v3/addons/{}/' |
'versions/{}/').format(self.extensionID, self.version) |
@@ -452,10 +506,14 @@ class NightlyBuild(object): |
) |
}) |
- request = urllib2.Request(upload_url, data=data) |
- request.add_header('Content-Type', content_type) |
- request.add_header('Authorization', 'JWT ' + token) |
- request.get_method = lambda: 'PUT' |
+ request = self.generate_jwt_request( |
+ config.get('extensions', 'amo_key'), |
+ config.get('extensions', 'amo_secret'), |
+ upload_url, |
+ 'PUT', |
+ data, |
+ (('Content-Type', content_type),), |
Sebastian Noack
2018/03/09 16:16:27
Nit: I missed this one before. Again, comma after
tlucas
2018/03/12 07:22:46
Done.
Vasily Kuznetsov
2018/03/13 17:49:59
I actually prefer the comma after the last argumen
|
+ ) |
try: |
urllib2.urlopen(request).close() |
@@ -466,6 +524,74 @@ class NightlyBuild(object): |
e.close() |
raise |
+ self.add_to_downloads_lockfile( |
+ self.config.type, |
+ { |
+ 'buildtype': 'devbuild', |
+ 'app_id': self.extensionID, |
+ 'version': self.version, |
+ } |
+ ) |
+ os.remove(self.path) |
+ |
+ def download_from_mozilla_addons(self, buildtype, version, app_id): |
+ config = get_config() |
+ iss = config.get('extensions', 'amo_key') |
+ secret = config.get('extensions', 'amo_secret') |
+ |
+ url = ('https://addons.mozilla.org/api/v3/addons/{}/' |
+ 'versions/{}/').format(app_id, version) |
+ |
+ request = self.generate_jwt_request(iss, secret, url, 'GET') |
+ response = json.load(urllib2.urlopen(request)) |
+ |
+ necessary = ['passed_review', 'reviewed', 'processed', 'valid'] |
+ if all(response[x] for x in necessary): |
+ download_url = response['files'][0]['download_url'] |
+ checksum = response['files'][0]['hash'] |
+ |
+ filename = '{}-{}.xpi'.format(self.basename, version) |
+ file_path = os.path.join( |
+ config.get('extensions', 'nightliesDirectory'), |
+ self.basename, |
+ filename |
+ ) |
+ |
+ request = self.generate_jwt_request(iss, secret, download_url, |
+ 'GET') |
+ try: |
+ response = urllib2.urlopen(request) |
+ except urllib2.HTTPError as e: |
+ logging.error(e.read()) |
+ |
+ # Verify the extension's integrity |
+ file_content = response.read() |
+ sha256 = hashlib.sha256(file_content) |
+ returned_checksum = '{}:{}'.format(sha256.name, sha256.hexdigest()) |
+ |
+ if returned_checksum != checksum: |
+ logging.error('Checksum could not be verified: {} vs {}' |
+ ''.format(checksum, returned_checksum)) |
+ |
+ with open(file_path, 'w') as fp: |
+ fp.write(file_content) |
+ |
+ self.update_link = os.path.join( |
+ config.get('extensions', 'nightliesURL'), |
+ self.basename, |
+ filename |
+ ) |
+ |
+ self.remove_from_downloads_lockfile(self.config.type, |
+ 'version', |
+ version) |
+ elif not response['passed_review'] or not response['valid']: |
+ # When the review failed for any reason, we want to know about it |
+ logging.error(json.dumps(response, indent=4)) |
+ self.remove_from_downloads_lockfile(self.config.type, |
+ 'version', |
+ version) |
+ |
def uploadToChromeWebStore(self): |
opener = urllib2.build_opener(HTTPErrorBodyHandler) |
@@ -662,12 +788,12 @@ class NightlyBuild(object): |
# create development build |
self.build() |
+ if self.config.type not in self.downloadable_repos: |
+ # write out changelog |
+ self.writeChangelog(self.getChanges()) |
- # write out changelog |
- self.writeChangelog(self.getChanges()) |
- |
- # write update manifest |
- self.writeUpdateManifest() |
+ # write update manifest |
+ self.writeUpdateManifest() |
# retire old builds |
versions = self.retireBuilds() |
@@ -675,8 +801,9 @@ class NightlyBuild(object): |
if self.config.type == 'ie': |
self.writeIEUpdateManifest(versions) |
- # update index page |
- self.updateIndex(versions) |
+ if self.config.type not in self.downloadable_repos: |
+ # update index page |
+ self.updateIndex(versions) |
# update nightlies config |
self.config.latestRevision = self.revision |
@@ -695,13 +822,45 @@ class NightlyBuild(object): |
if self.tempdir: |
shutil.rmtree(self.tempdir, ignore_errors=True) |
+ def download(self): |
+ with open(get_config().get('extensions', 'downloadLockFile')) as fp: |
+ download_info = json.load(fp) |
+ |
+ downloads = self.downloadable_repos.intersection(download_info.keys()) |
+ |
+ if self.config.type in downloads: |
+ try: |
+ self.copyRepository() |
+ self.readGeckoMetadata() |
+ |
+ for data in download_info[self.config.type]: |
+ self.version = data['version'] |
-def main(): |
+ self.download_from_mozilla_addons(**data) |
+ |
+ # write out changelog |
+ self.writeChangelog(self.getChanges()) |
+ |
+ # write update manifest |
+ self.writeUpdateManifest() |
+ |
+ # retire old builds |
+ versions = self.retireBuilds() |
+ # update index page |
+ self.updateIndex(versions) |
+ finally: |
+ # clean up |
+ if self.tempdir: |
+ shutil.rmtree(self.tempdir, ignore_errors=True) |
+ |
+ |
+def main(download=False): |
""" |
main function for createNightlies.py |
""" |
nightlyConfig = ConfigParser.SafeConfigParser() |
nightlyConfigFile = get_config().get('extensions', 'nightliesData') |
+ |
if os.path.exists(nightlyConfigFile): |
nightlyConfig.read(nightlyConfigFile) |
@@ -712,7 +871,9 @@ def main(): |
build = None |
try: |
build = NightlyBuild(repo) |
- if build.hasChanges(): |
+ if download: |
+ build.download() |
+ elif build.hasChanges(): |
build.run() |
except Exception as ex: |
logging.error('The build for %s failed:', repo) |
@@ -723,4 +884,7 @@ def main(): |
if __name__ == '__main__': |
- main() |
+ parser = argparse.ArgumentParser() |
+ parser.add_argument('--download', action='store_true', default=False) |
+ args = parser.parse_args() |
+ main(args.download) |