chromium/tools/android/python_utils/subprocess_utils.py

# 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()