# Lint as: python3
# Copyright 2021 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Helper functions for running commands as subprocesses."""
import logging
import pathlib
import shutil
import sys
import subprocess
from typing import Optional, Sequence, Union
_PYTHON_UTILS_PATH = pathlib.Path(__file__).resolve().parents[0]
if str(_PYTHON_UTILS_PATH) not in sys.path:
sys.path.append(str(_PYTHON_UTILS_PATH))
import git_metadata_utils
def resolve_ninja() -> str:
# Prefer the version on PATH, but fallback to known version if PATH doesn't
# have one (e.g. on bots).
if shutil.which('ninja') is None:
return str(git_metadata_utils.get_chromium_src_path() /
'third_party/ninja/ninja')
return 'ninja'
def resolve_autoninja():
# Prefer the version on PATH, but fallback to known version if PATH doesn't
# have one (e.g. on bots).
if shutil.which('autoninja') is None:
return str(git_metadata_utils.get_chromium_src_path() /
'third_party/depot_tools/autoninja')
return 'autoninja'
def run_command(
command: Sequence[str],
*, # Ensures that the rest of the args are passed explicitly.
cwd: Optional[Union[str, pathlib.Path]] = None,
cmd_input: Optional[str] = None,
exitcode_only: bool = False) -> str:
"""Runs a command and returns the output as a string.
For example, run_command(['echo', ' Hello, world!\n']) returns the string
'Hello, world!' and run_command(['pwd'], cwd='/usr/local/bin') returns the
string '/usr/local/bin'.
Args:
command:
A sequence of strings representing the command to run, starting with the
executable name followed by the arguments.
cwd:
The working directory in which to run the command; if not specified,
defaults to the current working directory.
cmd_input:
Input text that should be automatically passed to the process's stdin.
exitcode_only:
Avoid re-raising any errors or logging any output for errors. Useful for
commands that are expected to return a non-zero exit status.
Returns:
If |exitcode_only| is True, then only the exitcode is returned.
Otherwise, output (stdout) of the command as a string. Leading and trailing
whitespace are stripped from the output.
Raises
CalledProcessError:
The command returned a non-zero exit code indicating failure; the error
code is printed along with the error message (stderr) and output (stdout),
if any.
FileNotFoundError: The executable specified in the command was not found.
"""
try:
run_result: subprocess.CompletedProcess = subprocess.run(
command,
capture_output=True,
text=True,
check=not exitcode_only,
cwd=cwd,
input=cmd_input)
except subprocess.CalledProcessError as e:
command_str = ' '.join(command)
error_msg = f'Command "{command_str}" failed with code {e.returncode}.'
if e.stderr:
error_msg += f'\nSTDERR: {e.stderr}'
if e.stdout:
error_msg += f'\nSTDOUT: {e.stdout}'
logging.error(error_msg)
raise
if exitcode_only:
return run_result.returncode
return str(run_result.stdout).strip()