| Index: scripts/slave/annotated_run.py
|
| diff --git a/scripts/slave/annotated_run.py b/scripts/slave/annotated_run.py
|
| index d5bfd101ce26de0ba26dc5d26f813ebde43d1a31..04261217440221470910cc1da1887e1e269b80d6 100755
|
| --- a/scripts/slave/annotated_run.py
|
| +++ b/scripts/slave/annotated_run.py
|
| @@ -26,10 +26,43 @@ from common import annotator
|
| from common import chromium_utils
|
| from common import env
|
| from common import master_cfg_utils
|
| +from slave import gce
|
|
|
| # Logging instance.
|
| LOGGER = logging.getLogger('annotated_run')
|
|
|
| +# Return codes used by Butler/Annotee to indicate their failure (as opposed to
|
| +# a forwarded return code from the underlying process).
|
| +LOGDOG_ERROR_RETURNCODES = (
|
| + # Butler runtime error.
|
| + 250,
|
| + # Annotee runtime error.
|
| + 251,
|
| +)
|
| +
|
| +# Sentinel value that, if present in master config, matches all builders
|
| +# underneath that master.
|
| +WHITELIST_ALL = '*'
|
| +
|
| +# Whitelist of {master}=>[{builder}|WHITELIST_ALL] whitelisting specific masters
|
| +# and builders for experimental LogDog/Annotee export.
|
| +LOGDOG_WHITELIST_MASTER_BUILDERS = {
|
| +}
|
| +
|
| +# Configuration for a Pub/Sub topic.
|
| +PubSubConfig = collections.namedtuple('PubSubConfig', ('project', 'topic'))
|
| +
|
| +# LogDogPlatform is the set of platform-specific LogDog bootstrapping
|
| +# configuration parameters.
|
| +#
|
| +# See _logdog_get_streamserver_uri for "streamserver" parameter details.
|
| +LogDogPlatform = collections.namedtuple('LogDogPlatform', (
|
| + 'butler', 'annotee', 'credential_path', 'streamserver'))
|
| +
|
| +# A CIPD binary description, including the package name, version, and relative
|
| +# path of the binary within the package.
|
| +CipdBinary = collections.namedtuple('CipdBinary',
|
| + ('package', 'version', 'relpath'))
|
|
|
| # RecipeRuntime will probe this for values.
|
| # - First, (system, platform)
|
| @@ -37,11 +70,25 @@ LOGGER = logging.getLogger('annotated_run')
|
| # - Finally, (),
|
| PLATFORM_CONFIG = {
|
| # All systems.
|
| - (): {},
|
| + (): {
|
| + 'logdog_pubsub': PubSubConfig(
|
| + project='luci-logdog',
|
| + topic='logs',
|
| + ),
|
| + },
|
|
|
| # Linux
|
| ('Linux',): {
|
| 'run_cmd': ['/opt/infra-python/run.py'],
|
| + 'logdog_platform': LogDogPlatform(
|
| + butler=CipdBinary('infra/tools/luci/logdog/butler/linux-amd64',
|
| + 'latest', 'logdog_butler'),
|
| + annotee=CipdBinary('infra/tools/luci/logdog/annotee/linux-amd64',
|
| + 'latest', 'logdog_annotee'),
|
| + credential_path=(
|
| + '/creds/service_accounts/service-account-luci-logdog-pubsub.json'),
|
| + streamserver='unix',
|
| + ),
|
| },
|
|
|
| # Mac OSX
|
| @@ -61,9 +108,71 @@ PLATFORM_CONFIG = {
|
| # the recipe engine.
|
| Config = collections.namedtuple('Config', (
|
| 'run_cmd',
|
| + 'logdog_pubsub',
|
| + 'logdog_platform',
|
| ))
|
|
|
|
|
| +class Runtime(object):
|
| + """Runtime is the runtime context of the recipe execution.
|
| +
|
| + It is a ContextManager that tracks generated files and cleans them up at
|
| + exit.
|
| + """
|
| +
|
| + def __init__(self, leak=False):
|
| + self._tempdirs = []
|
| + self._leak = leak
|
| +
|
| + def cleanup(self, path):
|
| + self._tempdirs.append(path)
|
| +
|
| + def tempdir(self, base=None):
|
| + """Creates a temporary recipe-local working directory and yields it.
|
| +
|
| + This creates a temporary directory for this annotation run. Directory
|
| + cleanup is appended to the supplied Runtime.
|
| +
|
| + This creates two levels of directory:
|
| + <base>/.recipe_runtime
|
| + <base>/.recipe_runtime/tmpFOO
|
| +
|
| + On termination, the entire "<base>/.recipe_runtime" directory is deleted,
|
| + removing the subdirectory created by this instance as well as cleaning up
|
| + any other temporary subdirectories leaked by previous executions.
|
| +
|
| + Args:
|
| + rt (Runtime): Process-wide runtime.
|
| + base (str/None): The directory under which the tempdir should be created.
|
| + If None, the default temporary directory root will be used.
|
| + """
|
| + base = base or tempfile.gettempdir()
|
| + basedir = ensure_directory(base, '.recipe_runtime')
|
| + self.cleanup(basedir)
|
| + tdir = tempfile.mkdtemp(dir=basedir)
|
| + return tdir
|
| +
|
| + def __enter__(self):
|
| + return self
|
| +
|
| + def __exit__(self, _et, _ev, _tb):
|
| + self.close()
|
| +
|
| + def close(self):
|
| + if self._leak:
|
| + LOGGER.warning('(--leak) Leaking temporary paths: %s', self._tempdirs)
|
| + else:
|
| + for path in reversed(self._tempdirs):
|
| + try:
|
| + if os.path.isdir(path):
|
| + LOGGER.debug('Cleaning up temporary directory [%s].', path)
|
| + chromium_utils.RemoveDirectory(path)
|
| + except BaseException:
|
| + LOGGER.exception('Failed to clean up temporary directory [%s].',
|
| + path)
|
| + del(self._tempdirs[:])
|
| +
|
| +
|
| def get_config():
|
| """Returns (Config): The constructed Config object.
|
|
|
| @@ -83,6 +192,8 @@ def get_config():
|
| # Construct runtime configuration.
|
| return Config(
|
| run_cmd=platform_config.get('run_cmd'),
|
| + logdog_pubsub=platform_config.get('logdog_pubsub'),
|
| + logdog_platform=platform_config.get('logdog_platform'),
|
| )
|
|
|
|
|
| @@ -93,6 +204,29 @@ def ensure_directory(*path):
|
| return path
|
|
|
|
|
| +def _logdog_get_streamserver_uri(rt, typ):
|
| + """Returns (str): The Butler StreamServer URI.
|
| +
|
| + Args:
|
| + rt (Runtime): Process-wide runtime.
|
| + typ (str): The type of URI to generate. One of: ['unix'].
|
| + Raises:
|
| + LogDogBootstrapError: if |typ| is not a known type.
|
| + """
|
| + if typ == 'unix':
|
| + # We have to use a custom temporary directory here. This is due to the path
|
| + # length limitation on UNIX domain sockets, which is generally 104-108
|
| + # characters. We can't make that assumption about our standard recipe
|
| + # temporary directory.
|
| + sockdir = rt.tempdir()
|
| + uri = 'unix:%s' % (os.path.join(sockdir, 'butler.sock'),)
|
| + if len(uri) > 104:
|
| + raise LogDogBootstrapError('Generated URI exceeds UNIX domain socket '
|
| + 'name size: %s' % (uri,))
|
| + return uri
|
| + raise LogDogBootstrapError('No streamserver URI generator.')
|
| +
|
| +
|
| def _run_command(cmd, **kwargs):
|
| if kwargs.pop('dry_run', False):
|
| LOGGER.info('(Dry Run) Would have executed command: %s', cmd)
|
| @@ -115,33 +249,255 @@ def _check_command(cmd, **kwargs):
|
| return stdout
|
|
|
|
|
| -@contextlib.contextmanager
|
| -def recipe_tempdir(root=None, leak=False):
|
| - """Creates a temporary recipe-local working directory and yields it.
|
| +class LogDogNotBootstrapped(Exception):
|
| + pass
|
| +
|
| +
|
| +class LogDogBootstrapError(Exception):
|
| + pass
|
| +
|
| +
|
| +def ensure_directory(*path):
|
| + path = os.path.join(*path)
|
| + if not os.path.isdir(path):
|
| + os.makedirs(path)
|
| + return path
|
| +
|
| +
|
| +def _get_service_account_json(opts, credential_path):
|
| + """Returns (str/None): If specified, the path to the service account JSON.
|
| +
|
| + This method probes the local environment and returns a (possibly empty) list
|
| + of arguments to add to the Butler command line for authentication.
|
| +
|
| + If we're running on a GCE instance, no arguments will be returned, as GCE
|
| + service account is implicitly authenticated. If we're running on Baremetal,
|
| + a path to those credentials will be returned.
|
| +
|
| + Args:
|
| + rt (RecipeRuntime): The runtime environment.
|
| + Raises:
|
| + |LogDogBootstrapError| if no credentials could be found.
|
| + """
|
| + path = opts.logdog_service_account_json
|
| + if path:
|
| + return path
|
| +
|
| + if gce.Authenticator.is_gce():
|
| + LOGGER.info('Running on GCE. No credentials necessary.')
|
| + return None
|
| +
|
| + if os.path.isfile(credential_path):
|
| + return credential_path
|
| +
|
| + raise LogDogBootstrapError('Could not find service account credentials. '
|
| + 'Tried: %s' % (credential_path,))
|
| +
|
|
|
| - This creates a temporary directory for this annotation run that is
|
| - automatically cleaned up. It returns the directory.
|
| +def _logdog_install_cipd(path, *packages):
|
| + """Returns (list): The paths to the binaries in each of the packages.
|
| +
|
| + This method bootstraps CIPD in "path", installing the packages specified
|
| + by "packages" and returning the paths to their binaries.
|
|
|
| Args:
|
| - root (str/None): If not None, the root directory. Otherwise, |os.cwd| will
|
| - be used.
|
| - leak (bool): If true, don't clean up the temporary directory on exit.
|
| + path (str): The CIPD installation root.
|
| + packages (CipdBinary): The set of CIPD binary packages to install.
|
| """
|
| - basedir = ensure_directory((root or os.getcwd()), '.recipe_runtime')
|
| + verbosity = 0
|
| + level = logging.getLogger().level
|
| + if level <= logging.INFO:
|
| + verbosity += 1
|
| + if level <= logging.DEBUG:
|
| + verbosity += 1
|
| +
|
| + packages_path = os.path.join(path, 'packages.json')
|
| + pmap = {}
|
| + cmd = [
|
| + sys.executable,
|
| + os.path.join(env.Build, 'scripts', 'slave', 'cipd.py'),
|
| + '--dest-directory', path,
|
| + '--json-output', packages_path,
|
| + ] + (['--verbose'] * verbosity)
|
| + for p in packages:
|
| + cmd += ['-P', '%s@%s' % (p.package, p.version)]
|
| + pmap[p.package] = os.path.join(path, p.relpath)
|
| +
|
| try:
|
| - tdir = tempfile.mkdtemp(dir=basedir)
|
| - yield tdir
|
| - finally:
|
| - if basedir and os.path.isdir(basedir):
|
| - if not leak:
|
| - LOGGER.debug('Cleaning up temporary directory [%s].', basedir)
|
| - try:
|
| - chromium_utils.RemoveDirectory(basedir)
|
| - except Exception:
|
| - LOGGER.exception('Failed to clean up temporary directory [%s].',
|
| - basedir)
|
| + _check_command(cmd)
|
| + except subprocess.CalledProcessError:
|
| + LOGGER.exception('Failed to install LogDog CIPD packages.')
|
| + raise LogDogBootstrapError()
|
| +
|
| + # Resolve installed packages.
|
| + return tuple(pmap[p.package] for p in packages)
|
| +
|
| +
|
| +def _build_logdog_prefix(properties):
|
| + """Constructs a LogDog stream prefix from the supplied properties.
|
| +
|
| + The returned prefix is of the form:
|
| + bb/<mastername>/<buildername>/<buildnumber>
|
| +
|
| + Any path-incompatible characters will be flattened to underscores.
|
| + """
|
| + def normalize(s):
|
| + parts = []
|
| + for ch in str(s):
|
| + if ch.isalnum() or ch in ':_-.':
|
| + parts.append(ch)
|
| else:
|
| - LOGGER.warning('(--leak) Leaking temporary directory [%s].', basedir)
|
| + parts.append('_')
|
| + if not parts[0].isalnum():
|
| + parts.insert(0, 's_')
|
| + return ''.join(parts)
|
| +
|
| + components = {}
|
| + for f in ('mastername', 'buildername', 'buildnumber'):
|
| + prop = properties.get(f)
|
| + if not prop:
|
| + raise LogDogBootstrapError('Missing build property [%s].' % (f,))
|
| + components[f] = normalize(properties.get(f))
|
| + return 'bb/%(mastername)s/%(buildername)s/%(buildnumber)s' % components
|
| +
|
| +
|
| +def _logdog_bootstrap(rt, opts, tempdir, config, properties, cmd):
|
| + """Executes the recipe engine, bootstrapping it through LogDog/Annotee.
|
| +
|
| + This method executes the recipe engine, bootstrapping it through
|
| + LogDog/Annotee so its output and annotations are streamed to LogDog. The
|
| + bootstrap is configured to tee the annotations through STDOUT/STDERR so they
|
| + will still be sent to BuildBot.
|
| +
|
| + The overall setup here is:
|
| + [annotated_run.py] => [logdog_butler] => [logdog_annotee] => [recipes.py]
|
| +
|
| + Args:
|
| + rt (Runtime): Process-wide runtime.
|
| + opts (argparse.Namespace): Command-line options.
|
| + tempdir (str): The path to the session temporary directory.
|
| + config (Config): Recipe runtime configuration.
|
| + properties (dict): Build properties.
|
| + cmd (list): The recipe runner command list to bootstrap.
|
| +
|
| + Returns (int): The return code of the recipe runner process.
|
| +
|
| + Raises:
|
| + LogDogNotBootstrapped: if the recipe engine was not executed because the
|
| + LogDog bootstrap requirements are not available.
|
| + LogDogBootstrapError: if there was an error bootstrapping the recipe runner
|
| + through LogDog.
|
| + """
|
| + bootstrap_dir = ensure_directory(tempdir, 'logdog_bootstrap')
|
| +
|
| + plat = config.logdog_platform
|
| + if not plat:
|
| + raise LogDogNotBootstrapped('LogDog platform is not configured.')
|
| +
|
| + # Determine LogDog prefix.
|
| + prefix = _build_logdog_prefix(properties)
|
| +
|
| + # TODO(dnj): Consider moving this to a permanent directory on the bot so we
|
| + # don't CIPD-refresh each time.
|
| + cipd_path = os.path.join(bootstrap_dir, 'cipd')
|
| + butler, annotee = _logdog_install_cipd(cipd_path, plat.butler, plat.annotee)
|
| + if opts.logdog_butler_path:
|
| + butler = opts.logdog_butler_path
|
| + if opts.logdog_annotee_path:
|
| + annotee = opts.logdog_annotee_path
|
| +
|
| + if not config.logdog_pubsub:
|
| + raise LogDogNotBootstrapped('No Pub/Sub configured.')
|
| + if not config.logdog_pubsub.project:
|
| + raise LogDogNotBootstrapped('No Pub/Sub project configured.')
|
| + if not config.logdog_pubsub.topic:
|
| + raise LogDogNotBootstrapped('No Pub/Sub topic configured.')
|
| +
|
| + # Determine LogDog verbosity.
|
| + logdog_verbose = []
|
| + if opts.logdog_verbose == 0:
|
| + pass
|
| + elif opts.logdog_verbose == 1:
|
| + logdog_verbose.append('-log_level=info')
|
| + else:
|
| + logdog_verbose.append('-log_level=debug')
|
| +
|
| + service_account_args = []
|
| + service_account_json = _get_service_account_json(opts, plat.credential_path)
|
| + if service_account_json:
|
| + service_account_args += ['-service-account-json', service_account_json]
|
| +
|
| + # Generate our Butler stream server URI.
|
| + streamserver_uri = _logdog_get_streamserver_uri(rt, plat.streamserver)
|
| +
|
| + # Dump the bootstrapped Annotee command to JSON for Annotee to load.
|
| + #
|
| + # Annotee can run accept bootstrap parameters through either JSON or
|
| + # command-line, but using JSON effectively steps around any sort of command-
|
| + # line length limits such as those experienced on Windows.
|
| + cmd_json = os.path.join(bootstrap_dir, 'annotee_cmd.json')
|
| + with open(cmd_json, 'w') as fd:
|
| + json.dump(cmd, fd)
|
| +
|
| + # Butler Command.
|
| + cmd = [
|
| + butler,
|
| + '-prefix', prefix,
|
| + '-output', 'pubsub,project="%(project)s",topic="%(topic)s"' % (
|
| + config.logdog_pubsub._asdict()),
|
| + ]
|
| + cmd += logdog_verbose
|
| + cmd += service_account_args
|
| + cmd += [
|
| + 'run',
|
| + '-stdout', 'tee=stdout',
|
| + '-stderr', 'tee=stderr',
|
| + '-streamserver-uri', streamserver_uri,
|
| + '--',
|
| + ]
|
| +
|
| + # Annotee Command.
|
| + cmd += [
|
| + annotee,
|
| + '-butler-stream-server', streamserver_uri,
|
| + '-annotate', 'tee',
|
| + '-name-base', 'recipes',
|
| + '-print-summary',
|
| + '-tee',
|
| + '-json-args-path', cmd_json,
|
| + ]
|
| + cmd += logdog_verbose
|
| +
|
| + rv, _ = _run_command(cmd, dry_run=opts.dry_run)
|
| + if rv in LOGDOG_ERROR_RETURNCODES:
|
| + raise LogDogBootstrapError('LogDog Error (%d)' % (rv,))
|
| + return rv
|
| +
|
| +
|
| +def _should_run_logdog(properties):
|
| + """Returns (bool): True if LogDog should be used for this run.
|
| +
|
| + Args:
|
| + properties (dict): The factory properties for this recipe run.
|
| + """
|
| + mastername = properties.get('mastername')
|
| + buildername = properties.get('buildername')
|
| + if not all((mastername, buildername)):
|
| + LOGGER.warning('Required mastername/buildername is not set.')
|
| + return False
|
| +
|
| + # Key on mastername.
|
| + bdict = LOGDOG_WHITELIST_MASTER_BUILDERS.get(mastername)
|
| + if bdict is not None:
|
| + # Key on buildername.
|
| + if WHITELIST_ALL in bdict or buildername in bdict:
|
| + LOGGER.info('Whitelisted master %s, builder %s.',
|
| + mastername, buildername)
|
| + return True
|
| +
|
| + LOGGER.info('Master %s, builder %s is not whitelisted for LogDog.',
|
| + mastername, buildername)
|
| + return False
|
|
|
|
|
| def get_recipe_properties(workdir, build_properties,
|
| @@ -297,6 +653,22 @@ def get_args(argv):
|
| action='store_true', default=False,
|
| help='use factory properties loaded from disk on the slave')
|
|
|
| + group = parser.add_argument_group('LogDog Bootstrap')
|
| + group.add_argument('--logdog-verbose',
|
| + action='count', default=0,
|
| + help='Increase LogDog verbosity. This can be specified multiple times.')
|
| + group.add_argument('--logdog-force', action='store_true',
|
| + help='Force LogDog bootstrapping, even if the system is not configured.')
|
| + group.add_argument('--logdog-butler-path',
|
| + help='Path to the LogDog Butler. If empty, one will be probed/downloaded '
|
| + 'from CIPD.')
|
| + group.add_argument('--logdog-annotee-path',
|
| + help='Path to the LogDog Annotee. If empty, one will be '
|
| + 'probed/downloaded from CIPD.')
|
| + group.add_argument('--logdog-service-account-json',
|
| + help='Path to the service account JSON. If one is not provided, the '
|
| + 'local system credentials will be used.')
|
| +
|
| return parser.parse_args(argv)
|
|
|
|
|
| @@ -311,7 +683,7 @@ def update_scripts():
|
| gclient_name = 'gclient'
|
| if sys.platform.startswith('win'):
|
| gclient_name += '.bat'
|
| - gclient_path = os.path.join(env.Build, '..', 'depot_tools',
|
| + gclient_path = os.path.join(env.Build, os.pardir, 'depot_tools',
|
| gclient_name)
|
| gclient_cmd = [gclient_path, 'sync', '--force', '--verbose', '--jobs=2']
|
| try:
|
| @@ -424,6 +796,51 @@ def write_monitoring_event(config, datadir, build_properties):
|
| LOGGER.warning("Failed to send monitoring event.", exc_info=True)
|
|
|
|
|
| +def _exec_recipe(rt, opts, tdir, config, properties):
|
| + # Find out if the recipe we intend to run is in build_internal's recipes. If
|
| + # so, use recipes.py from there, otherwise use the one from build.
|
| + recipe_file = properties['recipe'].replace('/', os.path.sep) + '.py'
|
| +
|
| + # Use the standard recipe runner unless the recipes are explicitly in the
|
| + # "build_limited" repository.
|
| + recipe_runner = os.path.join(env.Build,
|
| + 'scripts', 'slave', 'recipes.py')
|
| + if env.BuildInternal:
|
| + build_limited = os.path.join(env.BuildInternal, 'scripts', 'slave')
|
| + if os.path.exists(os.path.join(build_limited, 'recipes', recipe_file)):
|
| + recipe_runner = os.path.join(build_limited, 'recipes.py')
|
| +
|
| + # Dump properties to JSON and build recipe command.
|
| + props_file = os.path.join(tdir, 'recipe_properties.json')
|
| + with open(props_file, 'w') as fh:
|
| + json.dump(properties, fh)
|
| +
|
| + cmd = [
|
| + sys.executable, '-u', recipe_runner,
|
| + 'run',
|
| + '--workdir=%s' % os.getcwd(),
|
| + '--properties-file=%s' % props_file,
|
| + properties['recipe'],
|
| + ]
|
| +
|
| + status = None
|
| + try:
|
| + if opts.logdog_force or _should_run_logdog(properties):
|
| + status = _logdog_bootstrap(rt, opts, tdir, config, properties, cmd)
|
| + except LogDogNotBootstrapped as e:
|
| + LOGGER.info('Not bootstrapped: %s', e.message)
|
| + except LogDogBootstrapError as e:
|
| + LOGGER.warning('Could not bootstrap LogDog: %s', e.message)
|
| + except Exception as e:
|
| + LOGGER.exception('Exception while bootstrapping LogDog.')
|
| + finally:
|
| + if status is None:
|
| + LOGGER.info('Not using LogDog. Invoking `recipes.py` directly.')
|
| + status, _ = _run_command(cmd, dry_run=opts.dry_run)
|
| +
|
| + return status
|
| +
|
| +
|
| def main(argv):
|
| opts = get_args(argv)
|
|
|
| @@ -436,7 +853,8 @@ def main(argv):
|
| clean_old_recipe_engine()
|
|
|
| # Enter our runtime environment.
|
| - with recipe_tempdir(leak=opts.leak) as tdir:
|
| + with Runtime(leak=opts.leak) as rt:
|
| + tdir = rt.tempdir(os.getcwd())
|
| LOGGER.debug('Using temporary directory: [%s].', tdir)
|
|
|
| # Load factory properties and configuration.
|
| @@ -450,19 +868,6 @@ def main(argv):
|
| config = get_config()
|
| LOGGER.debug('Loaded runtime configuration: %s', config)
|
|
|
| - # Find out if the recipe we intend to run is in build_internal's recipes. If
|
| - # so, use recipes.py from there, otherwise use the one from build.
|
| - recipe_file = properties['recipe'].replace('/', os.path.sep) + '.py'
|
| -
|
| - # Use the standard recipe runner unless the recipes are explicitly in the
|
| - # "build_limited" repository.
|
| - recipe_runner = os.path.join(env.Build,
|
| - 'scripts', 'slave', 'recipes.py')
|
| - if env.BuildInternal:
|
| - build_limited = os.path.join(env.BuildInternal, 'scripts', 'slave')
|
| - if os.path.exists(os.path.join(build_limited, 'recipes', recipe_file)):
|
| - recipe_runner = os.path.join(build_limited, 'recipes.py')
|
| -
|
| # Setup monitoring directory and send a monitoring event.
|
| build_data_dir = ensure_directory(tdir, 'build_data')
|
| properties['build_data_dir'] = build_data_dir
|
| @@ -470,21 +875,8 @@ def main(argv):
|
| # Write our annotated_run.py monitoring event.
|
| write_monitoring_event(config, build_data_dir, properties)
|
|
|
| - # Dump properties to JSON and build recipe command.
|
| - props_file = os.path.join(tdir, 'recipe_properties.json')
|
| - with open(props_file, 'w') as fh:
|
| - json.dump(properties, fh)
|
| - cmd = [
|
| - sys.executable, '-u', recipe_runner,
|
| - 'run',
|
| - '--workdir=%s' % os.getcwd(),
|
| - '--properties-file=%s' % props_file,
|
| - properties['recipe'],
|
| - ]
|
| -
|
| - status, _ = _run_command(cmd, dry_run=opts.dry_run)
|
| -
|
| - return status
|
| + # Execute our recipe.
|
| + return _exec_recipe(rt, opts, tdir, config, properties)
|
|
|
|
|
| def shell_main(argv):
|
|
|