Source code
Revision control
Copy as Markdown
Other Tools
#!/usr/bin/python3 -u
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
"""
Thunderbird build environment prep for run-task,
for use with comm-central derived repositories.
This script is meant to run prior to run-task on repositories like
comm-central that need to check out a copy of a mozilla repository
in order to build.
A project will have a file named ".gecko_rev.yml" in it's root. See the
constant "GECKO_REV_CONF" if you want to change that. To download it, the
script uses the project repository URL and the revision number.
Those are defined in the environment variables:
COMM_HEAD_REPOSITORY
COMM_HEAD_REV
.gecko_rev.yml has a structure like (for comm-central):
```
GECKO_HEAD_REF: default
```
or for branches:
```
GECKO_HEAD_REF: THUNDERBIRD_60_VERBRANCH
GECKO_HEAD_REV: 6a830d12f15493a70b1192022c9985eba2139910
Note about GECKO_HEAD_REV and GECKO_HEAD_REF:
GECKO_HEAD_REF is a branch name or "default".
GECKO_HEAD_REV is a revision hash.
```
"""
import sys
import os
import socket
import time
from datetime import datetime
from pprint import pformat
import urllib.error
import urllib.request
import yaml
if sys.version_info[0:2] < (3, 5):
print('run-task-wrapper requires Python 3.5+')
sys.exit(1)
GECKO_REV_CONF = ".gecko_rev.yml"
DEBUG = bool(os.environ.get("RTW_DEBUG", False))
def print_message(msg, prefix=__file__, level=""):
"""
Print messages.
:param object msg: message to print, usually a string, but not always
:param str prefix: message prefix
:param str level: message level (DEBUG, ERROR, INFO)
"""
if not isinstance(msg, str):
msg = pformat(msg)
now = datetime.utcnow().isoformat()
# slice microseconds to 3 decimals.
now = now[:-3] if now[-7:-6] == '.' else now
if level:
sys.stdout.write('[{prefix} {now}Z] {level}: {msg}\n'.format(
prefix=prefix, now=now, level=level, msg=msg))
else:
sys.stdout.write('[{prefix} {now}Z] {msg}\n'.format(
prefix=prefix, now=now, msg=msg))
sys.stdout.flush()
def error_exit(msg):
"""Print the error message and exit with error."""
print_message(msg, level="ERROR")
if DEBUG:
raise Exception(msg)
sys.exit(1)
def print_debug(msg):
"""Prints a message with DEBUG prefix if DEBUG is enabled
with the environment variable "RTW_DEBUG".
"""
if DEBUG:
print_message(msg, level="DEBUG")
def check_environ():
"""Check that the necessary environment variables to find the
comm- repository are defined. (Set in .taskcluster.yml)
:return: tuple(str, str)
"""
print_debug("Checking environment variables...")
project_head_repo = os.environ.get("COMM_HEAD_REPOSITORY", None)
project_head_rev = os.environ.get("COMM_HEAD_REV", None)
if project_head_repo is None or project_head_rev is None:
error_exit("Environment NOT Ok:\n\tHead: {}\n\tRev: {}\n").format(
project_head_repo, project_head_rev)
print_debug("Environment Ok:\n\tHead: {}\n\tRev: {}\n".format(
project_head_repo, project_head_rev))
return project_head_repo, project_head_rev
def download_url(url, retry=1):
"""Downloads the given URL. Naively retries (when asked) upon failure
:param url: str
:param retry: int
:return: str
"""
# Use 1-based counting for display and calculation purposes.
for i in range(1, retry+1):
try:
print_message('Fetching {}. Attempt {} of {}.'.format(
url, i, retry))
with urllib.request.urlopen(url, timeout=10) as response:
data = response.read().decode("utf-8")
return data
except (urllib.error.URLError, socket.timeout) as exc:
print_message('Unable to retrieve {}'.format(url))
if isinstance(exc, urllib.error.URLError):
print_message(exc.reason)
else: # socket.timeout
print_message('Connection timed out.')
if i < retry: # No more retries
wait_time = i * 5 # fail #1: sleep 5s. #2, sleep 10s
print_message('Retrying in {} seconds.'.format(wait_time))
time.sleep(wait_time)
error_exit('No more retry attempts! Aborting.')
def fetch_gecko_conf(project_head_repo, project_revision):
"""Downloads .gecko_rev.yml from the project repository
:param project_head_repo: str
:param project_revision: str
:return: dict
"""
gecko_conf_url = '/'.join(
[project_head_repo, 'raw-file', project_revision, GECKO_REV_CONF])
gecko_conf_yml = download_url(gecko_conf_url, retry=5)
try:
gecko_conf = yaml.safe_load(gecko_conf_yml)
return gecko_conf
except yaml.YAMLError as exc:
err_txt = ["Error processing Gecko YAML configuration."]
if hasattr(exc, "problem_mark"):
mark = exc.problem_mark # pylint: disable=no-member
err_txt.append("Error position: line {}, column {}".format(
mark.line + 1, mark.column + 1))
error_exit('\n'.join(err_txt))
def update_environment(gecko_conf):
"""Adds the new variables defined in gecko_conf to the
running environment.
:param gecko_conf: dict
"""
print_message("Updating environment with:")
print_message(gecko_conf)
os.environ.update(gecko_conf)
print_debug("New environment:")
print_debug(os.environ)
def exec_run_task(args):
"""Executes run-task with a modified environment."""
print_message("Executing: {}".format(pformat(args)))
os.execv(args[0], args[0:])
def main():
"""Main function."""
args = sys.argv[1:] # Remaining args starting with run-task
project_head_repo, project_revision = check_environ()
gecko_conf = fetch_gecko_conf(project_head_repo, project_revision)
update_environment(gecko_conf)
exec_run_task(args)
if __name__ == "__main__":
main()