#!/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. # # Command usage: # # update.py -c - T -l -U -b # # Where: # -c - Optional path to an examples file. # If not proved the default is 'examples.json' # -T - GitHub token for secure access (required) # -l - Optional Level for providing logging output. Can be one of, # CRITICAL, ERROR, WARNING, INFO, DEBUG # If not provided the default is 'INFO' # -U - GitHub user for forked repos # -b - Branch to be updated # # NOTE only one of -U or -b can be specified. # # mbed-os tag to which all examples will be updated # import os from os.path import dirname, abspath, basename import sys import logging import argparse import json import subprocess import shutil import stat import re from github import Github, GithubException ROOT = abspath(dirname(dirname(dirname(dirname(__file__))))) sys.path.insert(0, ROOT) import examples_lib as lib from examples_lib import SUPPORTED_TOOLCHAINS def run_cmd(command, exit_on_failure=False): """ Run a system command and return the result status Description: Passes a command to the system and returns a True/False result, once the command has been executed, indicating success/failure. Commands are passed as a list of tokens. E.g. The command 'git remote -v' would be passed in as ['git', 'remote', '-v'] 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 """ update_log.debug('[Exec] %s', ' '.join(command)) return_code = subprocess.call(command, shell=True) if return_code: update_log.warning("Command '%s' failed with return code: %s", ' '.join(command), return_code) if exit_on_failure: sys.exit(1) return return_code def run_cmd_with_output(command, exit_on_failure=False): """ Run a system command and return the result status plus output Description: 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 list of tokens. E.g. The command 'git remote -v' would be passed in as ['git', 'remote', '-v'] Args: command - system command as a list of tokens exit_on_failure - If True exit the program on failure (default = False) Returns: returncode - True/False indicating the success/failure of the command output - The output of the command if it was successful, else empty string """ update_log.debug('[Exec] %s', ' '.join(command)) returncode = 0 output = "" try: output = subprocess.check_output(command, shell=True) except subprocess.CalledProcessError as e: update_log.warning("Command '%s' failed with return code: %s", ' '.join(command), e.returncode) 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: update_log.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 ret - True if the fork was synchronised successfully, False otherwise """ logstr = "In: " + os.getcwd() update_log.debug(logstr) for cmd in [['git', 'remote', 'add', 'armmbed', arm_example], ['git', 'fetch', 'armmbed'], ['git', 'reset', '--hard', 'armmbed/master'], ['git', 'push', '-f', 'origin']]: if run_cmd(cmd): update_log.error("Fork preparation failed") return False return True def prepare_branch(branch): """ Set up at branch ready for use in updating examples Description: This function checks whether or not the supplied branch exists. If it does not, the branch is created and pushed to the origin. The branch is then switched to. Args: arm_example - Full GitHub repo path for original example ret - True if the fork was synchronised successfully, False otherwise """ update_log.debug("Preparing branch: %s", branch) # Check if branch already exists or not. cmd = ['git', 'branch'] return_code, output = run_cmd_with_output(cmd) if not branch in output: # OOB branch does not exist thus create it and then check it out cmd = ['git', 'checkout', '-b', branch] return_code = run_cmd(cmd) if not return_code: # Push new branch upstream cmd = ['git', 'push', '-u', 'origin', branch] return_code = run_cmd(cmd) else: cmd = ['git', 'checkout', branch] return_code = run_cmd(cmd) if return_code: update_log.error("Failed to prepare branch: %s", branch) return False return True def upgrade_example(github, example, tag, ref, user='ARMmbed', branch='master'): """ 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 and branch specified. Only two options are valid: 1) ARMmbed + non master branch This option will update the branch directly in the ARMmbed repo. If the branch does not exist it will be first created. 2) alternative user + master branch This option assumes that a fork of the repo exists in the specified user's account. The fork will first be updated so that it is up to date with the upstream version , then the fork will be updated and a PR raised against the upstream ie ARMmbed repo. 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 (defaults to 'ARMmbed' if not supplied) branch - branch to update (defaults to 'master' if not supplied) returns True if the upgrade was successful, False otherwise """ ret = False update_log.info("Updating example '%s'", example['name']) update_log.debug("User: %s", user) update_log.debug("Branch: %s", branch) # First check validity of user/branch combination if ((user == 'ARMmbed' and branch == 'master') or (user != 'ARMmbed' and branch != 'master')): update_log.error("Invalid user/branch combination") return False cwd = os.getcwd() upstream_repo = 'ARMmbed/'+ example['name'] update_repo = "https://github.com/" + user + '/' + example['name'] update_log.debug("Upstream repository: %s", upstream_repo) update_log.debug("Update repository: %s", update_repo) # Check access to mbed-os repo try: repo = github.get_repo(upstream_repo, False) except: update_log.error("Upstream repo: %s, does not exist - skipping", upstream_repo) return False # Clone the example repo clone_cmd = ['git', 'clone', 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 not the default, then a fork will be used. Thus # synchronise the user fork with the upstream if user != 'ARMmbed': prepare_fork(example['github']) if branch != 'master': prepare_branch(branch) 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 the default commit message commit_message = 'Updating mbed-os to ' + tag # Setup and run the commit command commit_cmd = ['git', 'commit', '-m', commit_message] 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 user != 'ARMmbed': body = "Please test/merge this PR and then tag Master with " + 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=body) ret = True except GithubException as e: # Default to False update_log.error("Pull request creation failed with error: %s", e) else: ret = True else: update_log.error("Git push command failed.") else: update_log.error("Git commit command failed.") else: update_log.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): update_log.info("'%s' directory already exists. Deleting...", path) rmtree_readonly(path) os.makedirs(path) if __name__ == '__main__': parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument('tag', help="mbed-os tag to which all examples will be updated") 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") parser.add_argument('-l', '--log-level', help="Level for providing logging output", default='INFO') exclusive = parser.add_mutually_exclusive_group(required=True) exclusive.add_argument('-U', '--github_user', help="GitHub user for forked repos, mutually exclusive to branch option") exclusive.add_argument('-b', '--branch', help="Branch to be updated, mutually exclusive to user option") args = parser.parse_args() default = getattr(logging, 'INFO') level = getattr(logging, args.log_level.upper(), default) # Set logging level logging.basicConfig(level=level) update_log = logging.getLogger("Update") # Load the config file with open(os.path.join(os.path.dirname(__file__), args.config_file)) as config: if not config: update_log.error("Failed to load config file '%s'", args.config_file) sys.exit(1) json_data = json.load(config) # Create working directory create_work_directory('examples') github = Github(args.github_token) # Get the github sha corresponding to the specified mbed-os tag cmd = ['git', 'rev-list', '-1', args.tag] return_code, ref = run_cmd_with_output(cmd) if return_code: update_log.error("Could not obtain SHA for tag: %s", args.tag) sys.exit(1) # Loop through the examples failures = [] successes = [] results = {} 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. # Only user or branch can be specified on the command line if args.github_user: result = upgrade_example(github, example, args.tag, ref, user=args.github_user) else: result = upgrade_example(github, example, args.tag, ref, branch=args.branch) if result: successes += [example['name']] else: failures += [example['name']] os.chdir('../') # Finish the script and report the results update_log.info("Finished updating examples") if successes: for success in successes: update_log.info(" SUCCEEDED: %s", success) if failures: for fail in failures: update_log.info(" FAILED: %s", fail)