| ''' |
| Task runner. |
| ''' |
| import argparse |
| import copy |
| import json |
| import os |
| import re |
| import subprocess |
| import sys |
| import tempfile |
| import utils |
| import repos |
| |
| |
| _userdir = os.path.abspath('.') |
| _hooksdir = os.path.abspath(os.path.join(os.path.dirname(__file__), 'hooks')) |
| _artifact_input_regex = \ |
| re.compile(r'\s*#?\s*build\s+get\s+([a-zA-Z_\-0-9]+)') |
| _artifact_input_regex_ex = \ |
| re.compile(r'\s*#?\s*build\s+get\s+([a-zA-Z_\-0-9]+)\s+--from=([a-zA-Z_\-0-9]+)') |
| _artifact_param_regex = \ |
| re.compile(r'build\s+arg\s+([a-zA-Z_\-0-9]+)\s*') |
| _artifact_optional_param_regex = \ |
| re.compile(r'build\s+arg\s+--optional\s+([a-zA-Z_\-0-9]+)\s*') |
| |
| |
| def _determine_task_inputs(taskfile): |
| artifacts = dict() |
| parameters = set() |
| for line in taskfile: |
| m = _artifact_input_regex_ex.search(line) |
| if m: |
| name = m.group(1) |
| repo = m.group(2) |
| artifacts[name] = repo |
| continue |
| m = _artifact_input_regex.search(line) |
| if m: |
| name = m.group(1) |
| artifacts[name] = name |
| continue |
| m = _artifact_optional_param_regex.search(line) |
| if m: |
| name = m.group(1) |
| optional = True |
| parameter = (name, optional) |
| parameters.add(parameter) |
| continue |
| m = _artifact_param_regex.search(line) |
| if m: |
| name = m.group(1) |
| optional = False |
| parameter = (name, optional) |
| parameters.add(parameter) |
| continue |
| |
| if 'config' in artifacts: |
| sys.stderr.write("%s: Artifact name 'config' is reserved\n" % taskfile) |
| sys.exit(1) |
| |
| return (artifacts, list(parameters)) |
| |
| |
| def _get_configfilename(taskfilename): |
| configname = taskfilename |
| if configname.endswith('.sh'): |
| configname = configname[:-3] |
| configname += '.json' |
| return configname |
| |
| |
| def _find_repo_config(taskdir, reponame, extra_searchpath=[]): |
| repo_searchpath = extra_searchpath + ['repos'] |
| |
| for path in repo_searchpath: |
| full_path = os.path.join(taskdir, path, reponame+'.json') |
| if os.path.exists(full_path): |
| return full_path |
| sys.stderr.write("There is no configuration for repository '%s'\n" % |
| reponame) |
| sys.stderr.write("Note: Searchpath: %s\n" % ", ".join(repo_searchpath)) |
| sys.exit(1) |
| |
| |
| def _read_repo_config(taskdir, reponame, extra_searchpath): |
| configfile = _find_repo_config(taskdir, reponame, extra_searchpath) |
| with open(configfile) as file: |
| try: |
| repoconfig = json.load(file) |
| except ValueError as e: |
| sys.stderr.write("%s: error: %s\n" % (configfile, e)) |
| sys.exit(1) |
| |
| copyfrom = repoconfig.get('copyfrom') |
| if copyfrom is not None: |
| merged_config = repoconfig |
| del merged_config['copyfrom'] |
| copyfrom_config = _read_repo_config(taskdir, copyfrom, extra_searchpath) |
| for key, val in copyfrom_config.items(): |
| merged_config[key] = val |
| repoconfig = merged_config |
| |
| # Check repoconfig for errors. |
| type = repoconfig.get('type') |
| if type is None: |
| sys.stderr.write("No type specified in repo config '%s'\n" % |
| configfile) |
| sys.exit(1) |
| repohandler = repos.modules.get(type) |
| if repohandler is None: |
| sys.stderr.write("Unknown type '%s' in repo config '%s'\n" % |
| (type, configfile)) |
| sys.exit(1) |
| try: |
| repohandler.verify(repoconfig) |
| except Exception as e: |
| sys.stderr.write("Invalid repo config '%s': %s\n" % |
| (configfile, e)) |
| sys.exit(1) |
| return repoconfig |
| |
| |
| def _make_task_argparser(command_name, debughelper_mode=False, |
| hostname_arg=False): |
| p = argparse.ArgumentParser(prog=('task %s' % command_name)) |
| p.set_defaults(name=None) |
| p.set_defaults(local=False) |
| p.set_defaults(existing=False) |
| p.set_defaults(rewrite_local=False) |
| if hostname_arg: |
| p.add_argument('hostname') |
| p.add_argument('task') |
| p.add_argument('-a', '--artifact', action='append', default=[], |
| dest='artifacts') |
| p.add_argument('-r', '--ref', action='append', default=[], dest='refs') |
| p.add_argument('-D', '--define', action='append', default=[], dest='defs') |
| p.add_argument('-v', '--verbose', action='store_true', default=False) |
| if debughelper_mode: |
| p.add_argument('-l', '--local', action='store_true') |
| p.add_argument('-e', '--existing', action='store_true') |
| p.add_argument('-L', '--rewrite-local', action='store_true') |
| else: |
| p.add_argument('-n', '--name') |
| return p |
| |
| |
| def _rewrite_local_git_urls(buildconfig): |
| ''' |
| Prefix all git repository urls in buildconfig that start with a slash |
| (they reference local files) with a prefix of the local machine/user. |
| ''' |
| hostname = utils.check_output(['hostname', '-f']).strip() |
| user = utils.check_output(['whoami']).strip() |
| for name, config in buildconfig.items(): |
| if not isinstance(config, dict): |
| continue |
| url = config.get('url') |
| if url is not None and url.startswith('/'): |
| config['url'] = '%s@%s:%s' % (user, hostname, url) |
| |
| |
| class BuildConfig(object): |
| def __init__(self, taskfilename, taskname, config): |
| self.taskfilename = taskfilename |
| self.taskname = taskname |
| self.config = config |
| |
| |
| def _make_buildconfig(argconfig): |
| if argconfig.verbose: |
| utils.verbose = True |
| taskfilename = os.path.abspath(argconfig.task) |
| taskname = argconfig.name |
| if taskname is None: |
| taskname = os.path.basename(taskfilename).partition('.')[0] |
| |
| try: |
| with open(taskfilename) as taskfile: |
| artifact_parameters, parameters = _determine_task_inputs(taskfile) |
| except IOError as e: |
| sys.stderr.write("%s\n" % (e,)) |
| sys.exit(1) |
| |
| # Resolve arguments |
| buildconfig = dict() |
| mandatory_parameters = set() |
| optional_parameters = set() |
| for parameter, optional in parameters: |
| if optional: |
| optional_parameters.add(parameter) |
| else: |
| mandatory_parameters.add(parameter) |
| for d in argconfig.defs: |
| name, eq, val = d.partition('=') |
| if eq != '=': |
| sys.stderr.write("Expected 'key=value' for -D argument\n") |
| sys.exit(1) |
| if name not in mandatory_parameters and \ |
| name not in optional_parameters: |
| sys.stderr.write("Warning: task does not have parameter '%s'\n" % |
| name) |
| buildconfig[name] = val |
| mandatory_parameters.discard(name) |
| if len(mandatory_parameters) > 0: |
| for param in mandatory_parameters: |
| sys.stderr.write("Error: No value for mandatory parameter '%s'\n" % |
| param) |
| sys.stderr.write("Note: Use the `-D parameter=value` option\n") |
| sys.exit(1) |
| |
| # Resolve artifact inputs |
| extra_searchpath = ["repos.try"] if argconfig.local else [] |
| taskdir = os.path.dirname(taskfilename) |
| repo_overrides = {} |
| for i in argconfig.artifacts: |
| name, eq, val = i.partition('=') |
| if eq != '=': |
| sys.stderr.write("Expected 'name=url' for -i argument\n") |
| sys.exit(1) |
| if name not in artifact_parameters: |
| sys.stderr.write("Warning: task does not have input '%s'\n" % name) |
| artifact_parameters[name] = name |
| if '://' not in val: |
| # TODO: Support local dirs... |
| sys.stderr.write("Expected URL for input '%s'\n" % name) |
| sys.exit(1) |
| repo_overrides[name] = {'type': 'url', 'url': val} |
| for i in argconfig.refs: |
| name, eq, val = i.partition('=') |
| if eq != '=': |
| sys.stderr.write("Expected 'repo=ref' for -r argument\n") |
| sys.exit(1) |
| if name not in artifact_parameters: |
| sys.stderr.write("Warning: task does not have input '%s'\n" % name) |
| continue |
| reponame = artifact_parameters[name] |
| repoconfig = _read_repo_config(taskdir, reponame, extra_searchpath) |
| type = repoconfig['type'] |
| if type == 'git': |
| repoconfig['rev'] = val |
| else: |
| sys.stderr.write("Cannot override revision of repo '%s'\n" % name) |
| repo_overrides[name] = repoconfig |
| |
| for name, reponame in artifact_parameters.items(): |
| repoconfig = repo_overrides.get(name) |
| if repoconfig is None: |
| if argconfig.existing: |
| buildconfig[name] = {'type': 'existing'} |
| continue |
| repoconfig = _read_repo_config(taskdir, reponame, extra_searchpath) |
| type = repoconfig['type'] |
| repohandler = repos.modules.get(type) |
| try: |
| repohandler.resolve_latest(repoconfig) |
| except Exception as e: |
| sys.stderr.write("While resolving %s:\n" % reponame) |
| sys.stderr.write("%s\n" % e) |
| sys.exit(1) |
| buildconfig[name] = repoconfig |
| |
| if argconfig.rewrite_local: |
| _rewrite_local_git_urls(buildconfig) |
| |
| return BuildConfig(taskfilename, taskname, buildconfig) |
| |
| |
| def _extract_buildid(build): |
| ''' |
| Extract buildid from buildscript. By convention the first line of a |
| complete buildscript has to start with `buildid='` |
| ''' |
| firstline = build.splitlines()[0] |
| if not firstline.startswith("buildid='") or not firstline.endswith("'"): |
| sys.stderr.write( |
| "error: build malformed (must start with buildid='...')\n") |
| sys.exit(1) |
| return firstline[9:-1] |
| |
| |
| def _make_buildscript(hook, buildconfig, keep_buildconfig=False): |
| if not keep_buildconfig: |
| buildconfig_file = tempfile.NamedTemporaryFile(prefix='buildconfig_', |
| delete=False) |
| buildconfig_filename = buildconfig_file.name |
| else: |
| buildconfig_filename = 'buildconfig.json' |
| buildconfig_file = open(buildconfig_filename, 'w') |
| |
| with buildconfig_file: |
| buildconfig_file.write(json.dumps(buildconfig.config, indent=2)) |
| buildconfig_file.write('\n') |
| buildconfig_file.close() |
| build = utils.check_output([hook, _userdir, buildconfig.taskfilename, |
| buildconfig_filename, |
| buildconfig.taskname], cwd=_hooksdir) |
| if not keep_buildconfig: |
| os.unlink(buildconfig_filename) |
| |
| # Extract buildid and create buildname |
| buildid = _extract_buildid(build) |
| buildname = buildconfig.taskname + "_" + buildid |
| |
| return build, buildname |
| |
| |
| def _mk_submit_results(buildname): |
| return utils.check_output(['./mk-submit-results', _userdir, buildname], |
| cwd=_hooksdir) |
| |
| |
| def _command_try(args): |
| '''Execute task locally.''' |
| p = _make_task_argparser('try') |
| argconfig = p.parse_args(args) |
| argconfig.local = True |
| buildconfig = _make_buildconfig(argconfig) |
| build, buildname = _make_buildscript('./mk-try-build', buildconfig) |
| |
| with tempfile.NamedTemporaryFile(delete=False) as tempf: |
| tempf.write(build) |
| tempf.close() |
| utils.check_call(['./exec-try-build', _userdir, tempf.name, buildname], |
| cwd=_hooksdir) |
| os.unlink(tempf.name) |
| |
| |
| def _command_submit(args): |
| '''Submit task to jenkins OneOff job.''' |
| p = _make_task_argparser('submit') |
| argconfig = p.parse_args(args) |
| buildconfig = _make_buildconfig(argconfig) |
| build, buildname = _make_buildscript('./mk-submit-build', buildconfig) |
| |
| build += _mk_submit_results(buildname) |
| |
| with tempfile.NamedTemporaryFile(delete=False) as tempf: |
| tempf.write(build) |
| tempf.close() |
| utils.check_call(['./submit', _userdir, tempf.name, buildname], |
| cwd=_hooksdir) |
| os.unlink(tempf.name) |
| |
| |
| def _command_jenkinsrun(args): |
| '''Run task as part of a jenkins job.''' |
| p = _make_task_argparser('jenkinsrun') |
| p.add_argument('-s', '--submit', action='store_true', default=False, |
| help='Submit results to artifact storage at end of task') |
| argconfig = p.parse_args(args) |
| argconfig.existing = True |
| buildconfig = _make_buildconfig(argconfig) |
| build, buildname = _make_buildscript('./mk-jenkinsrun-build', buildconfig, |
| keep_buildconfig=True) |
| |
| if argconfig.submit: |
| build += _mk_submit_results(buildname) |
| |
| with open("run.sh", "w") as runfile: |
| runfile.write(build) |
| |
| retcode = utils.call(['/bin/sh', 'run.sh']) |
| if retcode != 0: |
| sys.stdout.write("*Build failed!* (return code %s)\n" % retcode) |
| sys.stdout.flush() |
| taskdir = os.path.dirname(buildconfig.taskfilename) |
| repro_script = os.path.join(taskdir, 'repro_message.sh') |
| if os.access(repro_script, os.X_OK): |
| utils.check_call([repro_script, _userdir, |
| buildconfig.taskfilename]) |
| sys.exit(retcode) |
| |
| |
| def _command_sshrun(args): |
| '''Run task by logging into a remote machine with ssh.''' |
| p = _make_task_argparser('sshrun', hostname_arg=True) |
| argconfig = p.parse_args(args) |
| argconfig.local = True |
| argconfig.rewrite_local = True |
| buildconfig = _make_buildconfig(argconfig) |
| build, buildname = _make_buildscript('./mk-sshrun-build', buildconfig) |
| |
| run_file = tempfile.NamedTemporaryFile(prefix=buildname, delete=False) |
| with run_file: |
| run_file.write(build) |
| run_file.close() |
| try: |
| utils.check_call(['./sshrun', argconfig.hostname, run_file.name], |
| cwd=_hooksdir) |
| finally: |
| os.unlink(run_file.name) |
| |
| |
| def _command_resolve(args): |
| '''Print artifact resolution results. (debug helper)''' |
| p = _make_task_argparser('resolve', debughelper_mode=True) |
| argconfig = p.parse_args(args) |
| buildconfig = _make_buildconfig(argconfig) |
| |
| for name, config in sorted(buildconfig.config.items()): |
| if isinstance(config, dict): |
| url = config.get('url', '') |
| line = "%-15s\t%-30s" % (name, url) |
| rev = config.get('rev') |
| if rev is not None: |
| line += " " + rev |
| sys.stdout.write("%s\n" % line) |
| else: |
| sys.stdout.write("%-15s\t%s\n" % (name, config)) |
| |
| |
| def _command_buildconfig(args): |
| '''Produce buildconfig. (debug helper)''' |
| p = _make_task_argparser('buildconfig', debughelper_mode=True) |
| argconfig = p.parse_args(args) |
| buildconfig = _make_buildconfig(argconfig) |
| |
| json.dump(buildconfig.config, sys.stdout, indent=2, sort_keys=True) |
| sys.stdout.write('\n') |
| |
| |
| def main(): |
| argv = sys.argv |
| # Allow overriding _hooksdir for testing. |
| if len(argv) > 1 and argv[1].startswith("--hooks-dir="): |
| global _hooksdir |
| _hooksdir = argv[1].split('=', 1)[1] |
| del argv[1] |
| |
| commands = { |
| 'buildconfig': _command_buildconfig, |
| 'jenkinsrun': _command_jenkinsrun, |
| 'resolve': _command_resolve, |
| 'sshrun': _command_sshrun, |
| 'submit': _command_submit, |
| 'try': _command_try, |
| } |
| utils.run_subcommand(commands, argv, docstring=__doc__) |
| |
| |
| if __name__ == '__main__': |
| main() |