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