mbed-os
Dependents: cobaLCDJoyMotor_Thread odometry_omni_3roda_v3 odometry_omni_3roda_v1 odometry_omni_3roda_v2 ... more
Diff: tools/test/examples/update.py
- Revision:
- 0:b74591d5ab33
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/test/examples/update.py Mon Dec 11 17:54:04 2017 +0000 @@ -0,0 +1,562 @@ +#!/usr/bin/env python + +# This script is used to update the version of mbed-os used within a specified set of example +# applications. The list of examples to be updated lives in the examples.json file and is +# shared with the examples.py script. Logging is used to provide varying levels of output +# during execution. +# +# There are two modes that can be used: +# 1) Update the ARMmbed/master branch of the specified example +# +# This is done by updating a user fork of the example and then raising a pull request +# against ARMmbed/master. +# +# 2) Update a different ARMmbed branch of the specified example +# +# A branch to update is specified. If it doesn't already exist then it is first created. +# This branch will be updated and the change automatically pushed. The new branch will +# be created from the specified source branch. +# +# The modes are controlled via configuration data in the json file. +# E.g. +# +# "update-config" : { +# "help" : "Update each example repo with a version of mbed-os identified by the tag", +# "via-fork" : { +# "help" : "-f cmd line option. Update a fork", +# "github-user" : "adbridge" +# }, +# "via-branch" : { +# "help" : "-b cmd line option. Update dst branch, created from src branch", +# "src-branch" : "mbed-os-5.5.0-rc1-oob", +# "dst-branch" : "mbed-os-5.5.0-rc2-oob" +# }, +# "tag" : "mbed-os-5.5.0-rc2" +# +# +# Command usage: +# +# update.py -c <config file> - T <github_token> -f -b -s +# +# Where: +# -c <config file> - Optional path to an examples file. +# If not proved the default is 'examples.json' +# -T <github_token> - GitHub token for secure access (required) +# -f - Update forked repos. This will use the 'github-user' parameter in +# the 'via-fork' section. +# -b - Update branched repos. This will use the "src-branch" and +# "dst-branch" parameters in the 'via-branch' section. The destination +# branch is created from the source branch (if it doesn't already exist). +# -s - Show the status of any pull requests with a tag matching that in the +# json config file +# +# The options -f, -b and -s are mutually exlusive. Only one can be specified. +# +# + +import os +from os.path import dirname, abspath, basename, join +import sys +import logging +import argparse +import json +import subprocess +import shutil +import stat +import re +from github import Github, GithubException +from jinja2 import FileSystemLoader, StrictUndefined +from jinja2.environment import Environment + +ROOT = abspath(dirname(dirname(dirname(dirname(__file__))))) +sys.path.insert(0, ROOT) + +import examples_lib as lib +from examples_lib import SUPPORTED_TOOLCHAINS + +userlog = logging.getLogger("Update") + +# Set logging level +userlog.setLevel(logging.DEBUG) + +# Everything is output to the log file +logfile = os.path.join(os.getcwd(), 'update.log') +fh = logging.FileHandler(logfile) +fh.setLevel(logging.DEBUG) + +# create console handler with a higher log level +ch = logging.StreamHandler() +ch.setLevel(logging.INFO) + +formatter = logging.Formatter('%(name)s: %(levelname)s - %(message)s') +ch.setFormatter(formatter) +fh.setFormatter(formatter) + +# add the handlers to the logger +userlog.addHandler(fh) +userlog.addHandler(ch) + +def run_cmd(command, exit_on_failure=False): + """ Run a system command returning a status result + + This is just a wrapper for the run_cmd_with_output() function, but + only returns the status of the call. + + Args: + command - system command as a list of tokens + exit_on_failure - If True exit the program on failure (default = False) + + Returns: + return_code - True/False indicating the success/failure of the command + """ + return_code, _ = run_cmd_with_output(command, exit_on_failure) + return return_code + +def run_cmd_with_output(command, exit_on_failure=False): + """ Run a system command returning a status result and any command output + + Passes a command to the system and returns a True/False result once the + command has been executed, indicating success/failure. If the command was + successful then the output from the command is returned to the caller. + Commands are passed as a string. + E.g. The command 'git remote -v' would be passed in as "git remote -v" + + Args: + command - system command as a string + exit_on_failure - If True exit the program on failure (default = False) + + Returns: + return_code - True/False indicating the success/failure of the command + output - The output of the command if it was successful, else empty string + """ + text = '[Exec] ' + command + userlog.debug(text) + returncode = 0 + output = "" + try: + output = subprocess.check_output(command, shell=True) + except subprocess.CalledProcessError as e: + text = "The command " + str(command) + "failed with return code: " + str(e.returncode) + userlog.warning(text) + returncode = e.returncode + if exit_on_failure: + sys.exit(1) + return returncode, output + + +def rmtree_readonly(directory): + """ Deletes a readonly directory tree. + + Args: + directory - tree to delete + """ + def remove_readonly(func, path, _): + os.chmod(path, stat.S_IWRITE) + func(path) + + shutil.rmtree(directory, onerror=remove_readonly) + +def find_all_examples(path): + """ Search the path for examples + + Description: + + Searches the path specified for sub-example folders, ie those containing an + mbed-os.lib file. If found adds the path to the sub-example to a list which is + then returned. + + Args: + path - path to search. + examples - (returned) list of paths to example directories. + + """ + examples = [] + for root, dirs, files in os.walk(path): + if 'mbed-os.lib' in files: + examples += [root] + + return examples + +def upgrade_single_example(example, tag, directory, ref): + """ Update the mbed-os version for a single example + + Description: + + Updates the mbed-os.lib file in the example specified to correspond to the + version specified by the GitHub tag supplied. Also deals with + multiple sub-examples in the GitHub repo, updating them in the same way. + + Args: + example - json example object containing the GitHub repo to update. + tag - GitHub tag corresponding to a version of mbed-os to upgrade to. + directory - directory path for the example. + ref - SHA corresponding to the supplied tag + returns - True if the upgrade was successful, False otherwise. + + """ + cwd = os.getcwd() + os.chdir(directory) + + return_code = False + + if os.path.isfile("mbed-os.lib"): + # Rename command will fail on some OS's if the target file already exist, + # so ensure if it does, it is deleted first. + if os.path.isfile("mbed-os.lib_bak"): + os.remove("mbed-os.lib_bak") + + os.rename("mbed-os.lib", "mbed-os.lib_bak") + else: + userlog.error("Failed to backup mbed-os.lib prior to updating.") + return False + + # mbed-os.lib file contains one line with the following format + # e.g. https://github.com/ARMmbed/mbed-os/#0789928ee7f2db08a419fa4a032fffd9bd477aa7 + lib_re = re.compile('https://github.com/ARMmbed/mbed-os/#[A-Za-z0-9]+') + updated = False + + # Scan through mbed-os.lib line by line + with open('mbed-os.lib_bak', 'r') as ip, open('mbed-os.lib', 'w') as op: + for line in ip: + + opline = line + + regexp = lib_re.match(line) + if regexp: + opline = 'https://github.com/ARMmbed/mbed-os/#' + ref + updated = True + + op.write(opline) + + if updated: + # Setup and run the git add command + cmd = "git add mbed-os.lib" + return_code = run_cmd(cmd) + + os.chdir(cwd) + return not return_code + +def prepare_fork(arm_example): + """ Synchronises a cloned fork to ensure it is up to date with the original. + + Description: + + This function sets a fork of an ARMmbed repo to be up to date with the + repo it was forked from. It does this by hard resetting to the ARMmbed + master branch. + + Args: + arm_example - Full GitHub repo path for original example + + """ + + logstr = "In: " + os.getcwd() + userlog.debug(logstr) + + for cmd in ["git remote add armmbed " + str(arm_example), + "git fetch armmbed", + "git reset --hard armmbed/master", + "git push -f origin"]: + run_cmd(cmd, exit_on_failure=True) + +def prepare_branch(src, dst): + """ Set up at branch ready for use in updating examples + + Description: + + This function checks whether or not the supplied dst branch exists. + If it does not, the branch is created from the src and pushed to the origin. + The branch is then switched to. + + Args: + src - branch to create the dst branch from + dst - branch to update + + """ + + userlog.debug("Preparing branch: %s", dst) + + # Check if branch already exists or not. + # We can use the 'git branch -r' command. This returns all the remote branches for + # the current repo. + # The output consists of a list of lines of the form: + # origin/<branch> + # From these we need to extract just the branch names to a list and then check if + # the specified dst exists in that list + branches = [] + cmd = "git branch -r" + _, output = run_cmd_with_output(cmd, exit_on_failure=True) + + branches = [line.split('/')[1] for line in output.split('\n') if 'origin' in line and not '->' in line] + + if not dst in branches: + + # OOB branch does not exist thus create it, first ensuring we are on + # the src branch and then check it out + + for cmd in ["git checkout " + str(src), + "git checkout -b " + str(dst), + "git push -u origin " + str(dst)]: + + run_cmd(cmd, exit_on_failure=True) + + else: + cmd = "git checkout " + str(dst) + run_cmd(cmd, exit_on_failure=True) + +def upgrade_example(github, example, tag, ref, user, src, dst, template): + """ Upgrade all versions of mbed-os.lib found in the specified example repo + + Description: + + Clone a version of the example specified and upgrade all versions of + mbed-os.lib found within its tree. The version cloned and how it + is upgraded depends on the user, src and dst settings. + 1) user == None + The destination branch will be updated with the version of mbed-os + idenfied by the tag. If the destination branch does not exist then it + will be created from the source branch. + + 2) user != None + The master branch of a fork of the example will be updated with the + version of mbed-os identified by the tag. + + Args: + github - GitHub instance to allow internal git commands to be run + example - json example object containing the GitHub repo to update. + tag - GitHub tag corresponding to a version of mbed-os to upgrade to. + ref - SHA corresponding to the tag + user - GitHub user name + src - branch to create the dst branch from + dst - branch to update + + returns True if the upgrade was successful, False otherwise + """ + + # If a user has not been specified then branch update will be used and thus + # the git user will be ARMmbed. + if not user: + user = 'ARMmbed' + + ret = False + userlog.info("Updating example '%s'", example['name']) + userlog.debug("User: %s", user) + userlog.debug("Src branch: %s", (src or "None")) + userlog.debug("Dst branch: %s", (dst or "None")) + + cwd = os.getcwd() + + update_repo = "https://github.com/" + user + '/' + example['name'] + userlog.debug("Update repository: %s", update_repo) + + # Clone the example repo + clone_cmd = "git clone " + str(update_repo) + return_code = run_cmd(clone_cmd) + + if not return_code: + + # Find all examples + example_directories = find_all_examples(example['name']) + + os.chdir(example['name']) + + # If the user is ARMmbed then a branch is used. + if user == 'ARMmbed': + prepare_branch(src, dst) + else: + prepare_fork(example['github']) + + for example_directory in example_directories: + if not upgrade_single_example(example, tag, os.path.relpath(example_directory, example['name']), ref): + os.chdir(cwd) + return False + + # Setup and run the commit command + commit_cmd = "git commit -m \"Updating mbed-os to " + tag + "\"" + return_code = run_cmd(commit_cmd) + if not return_code: + + # Setup and run the push command + push_cmd = "git push origin" + return_code = run_cmd(push_cmd) + + if not return_code: + # If the user is not ARMmbed then a fork is being used + if user != 'ARMmbed': + + upstream_repo = 'ARMmbed/'+ example['name'] + userlog.debug("Upstream repository: %s", upstream_repo) + # Check access to mbed-os repo + try: + repo = github.get_repo(upstream_repo, False) + + except: + userlog.error("Upstream repo: %s, does not exist - skipping", upstream_repo) + return False + + jinja_loader = FileSystemLoader(template) + jinja_environment = Environment(loader=jinja_loader, + undefined=StrictUndefined) + pr_body = jinja_environment.get_template("pr.tmpl").render(tag=tag) + + # Raise a PR from release-candidate to master + user_fork = user + ':master' + try: + pr = repo.create_pull(title='Updating mbed-os to ' + tag, head=user_fork, base='master', body=pr_body) + ret = True + except GithubException as e: + # Default to False + userlog.error("Pull request creation failed with error: %s", e) + else: + ret = True + else: + userlog.error("Git push command failed.") + else: + userlog.error("Git commit command failed.") + else: + userlog.error("Git clone %s failed", update_repo) + + os.chdir(cwd) + return ret + +def create_work_directory(path): + """ Create a new directory specified in 'path', overwrite if the directory already + exists. + + Args: + path - directory path to be created. + + """ + if os.path.exists(path): + userlog.info("'%s' directory already exists. Deleting...", path) + rmtree_readonly(path) + + os.makedirs(path) + +def check_update_status(examples, github, tag): + """ Check the status of previously raised update pull requests + + Args: + examples - list of examples which should have had PRs raised against them. + github - github rest API instance + tag - release tag used for the update + + """ + + for example in examples: + + repo_name = ''.join(['ARMmbed/', example['name']]) + try: + repo = github.get_repo(repo_name, False) + + except Exception as exc: + text = "Cannot access: " + str(repo_name) + userlog.error(text) + userlog.exception(exc) + sys.exit(1) + + # Create the full repository filter component + org_str = ''.join(['repo:ARMmbed/', example['name']]) + filt = ' '.join([org_str, 'is:pr', tag]) + merged = False + + issues = github.search_issues(query=(filt)) + pr_list = [repo.get_pull(issue.number) for issue in issues] + + # Should only be one matching PR but just in case, go through paginated list + for pr in pr_list: + if pr.merged: + userlog.info("%s - '%s': MERGED", example['name'], pr.title) + elif pr.state == 'open': + userlog.info("%s - '%s': PENDING", example['name'], pr.title) + elif pr.state == 'closed': + userlog.info("%s - '%s': CLOSED NOT MERGED", example['name'], pr.title) + else: + userlog.error("%s: Cannot find a pull request for %s", example['name'], tag) + +if __name__ == '__main__': + + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument('-c', '--config_file', help="Path to the configuration file (default is 'examples.json')", default='examples.json') + parser.add_argument('-T', '--github_token', help="GitHub token for secure access") + + exclusive = parser.add_mutually_exclusive_group(required=True) + exclusive.add_argument('-f', '--fork', help="Update a fork", action='store_true') + exclusive.add_argument('-b', '--branch', help="Update a branch", action='store_true') + exclusive.add_argument('-s', '--status', help="Show examples update status", action='store_true') + + args = parser.parse_args() + + # Load the config file + with open(os.path.join(os.path.dirname(__file__), args.config_file)) as config: + if not config: + userlog.error("Failed to load config file '%s'", args.config_file) + sys.exit(1) + json_data = json.load(config) + + + github = Github(args.github_token) + config = json_data['update-config'] + tag = config['tag'] + + user = None + src = "master" + dst = None + + if args.status: + + # This option should only be called after an update has been performed + check_update_status(json_data['examples'], github, tag) + exit(0) + + # Create working directory + create_work_directory('examples') + + if args.fork: + user = config['via-fork']['github-user'] + elif args.branch: + src = config['via-branch']['src-branch'] + dst = config['via-branch']['dst-branch'] + else: + userlog.error("Must specify either -f or -b command line option") + exit(1) + + # Get the github sha corresponding to the specified mbed-os tag + cmd = "git rev-list -1 " + tag + return_code, ref = run_cmd_with_output(cmd) + + if return_code: + userlog.error("Could not obtain SHA for tag: %s", tag) + sys.exit(1) + + # Loop through the examples + failures = [] + successes = [] + results = {} + template = dirname(abspath(__file__)) + + os.chdir('examples') + + for example in json_data['examples']: + # Determine if this example should be updated and if so update any found + # mbed-os.lib files. + + result = upgrade_example(github, example, tag, ref, user, src, dst, template) + + if result: + successes += [example['name']] + else: + failures += [example['name']] + + os.chdir('../') + + # Finish the script and report the results + userlog.info("Finished updating examples") + if successes: + for success in successes: + userlog.info(" SUCCEEDED: %s", success) + + if failures: + for fail in failures: + userlog.info(" FAILED: %s", fail)